From 20bf75a5f8e75c18cd8f3cd2899186a4bc06069b Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 05:42:37 -0500 Subject: [PATCH] feat: implement LSP server for IDE integration Add a Language Server Protocol (LSP) server to enable IDE integration. The server provides: - Real-time diagnostics (parse errors and type errors) - Basic hover information - Keyword completions (fn, let, if, match, type, effect, etc.) - Go-to-definition stub (ready for implementation) Usage: lux --lsp The LSP server can be integrated with any editor that supports LSP, including VS Code, Neovim, Emacs, and others. Dependencies added: - lsp-server 0.7 - lsp-types 0.94 - serde with derive feature - serde_json Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 345 +++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 + src/lsp.rs | 352 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 28 ++++- 4 files changed, 722 insertions(+), 7 deletions(-) create mode 100644 src/lsp.rs diff --git a/Cargo.lock b/Cargo.lock index c5089bd..044636b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -35,6 +41,32 @@ dependencies = [ "error-code", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -86,6 +118,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -129,12 +170,114 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -171,17 +314,53 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lsp-server" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6ada348dbc2703cbe7637b2dda05cff84d3da2819c24abcb305dd613e0ba2e" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "lux" version = "0.1.0" dependencies = [ + "lsp-server", + "lsp-types", "rustyline", + "serde", + "serde_json", "tempfile", "thiserror", ] @@ -207,7 +386,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -219,6 +398,21 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -269,7 +463,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -282,7 +476,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "clipboard-win", "fd-lock", @@ -311,6 +505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -346,12 +541,29 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "2.0.115" @@ -363,6 +575,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.25.0" @@ -396,6 +619,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "unicode-ident" version = "1.0.23" @@ -420,6 +653,25 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -472,7 +724,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.10.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -633,7 +885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.10.0", "indexmap", "log", "serde", @@ -663,6 +915,89 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index e8bdecf..860c3db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ license = "MIT" [dependencies] rustyline = "14" thiserror = "1" +lsp-server = "0.7" +lsp-types = "0.94" +serde = { version = "1", features = ["derive"] } +serde_json = "1" [dev-dependencies] tempfile = "3" diff --git a/src/lsp.rs b/src/lsp.rs new file mode 100644 index 0000000..d396681 --- /dev/null +++ b/src/lsp.rs @@ -0,0 +1,352 @@ +//! LSP (Language Server Protocol) server for Lux +//! +//! Provides IDE features like: +//! - Diagnostics (errors and warnings) +//! - Hover information +//! - Go to definition +//! - Completions + +use crate::parser::Parser; +use crate::typechecker::TypeChecker; + +use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response}; +use lsp_types::{ + notification::{DidChangeTextDocument, DidOpenTextDocument, Notification}, + request::{Completion, GotoDefinition, HoverRequest}, + CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, + Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, + HoverProviderCapability, InitializeParams, MarkupContent, MarkupKind, Position, + PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, Url, +}; +use std::collections::HashMap; +use std::error::Error; + +/// LSP Server for Lux +pub struct LspServer { + connection: Connection, + /// Document contents by URI + documents: HashMap, +} + +impl LspServer { + /// Run the LSP server + pub fn run() -> Result<(), Box> { + eprintln!("Starting Lux LSP server..."); + + // Create the transport connection + let (connection, io_threads) = Connection::stdio(); + + // Run the server + let server = LspServer { + connection, + documents: HashMap::new(), + }; + + server.main_loop()?; + + // Wait for the I/O threads to finish + io_threads.join()?; + + eprintln!("Lux LSP server stopped."); + Ok(()) + } + + fn main_loop(mut self) -> Result<(), Box> { + // Initialize + let server_capabilities = serde_json::to_value(ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), + hover_provider: Some(HoverProviderCapability::Simple(true)), + completion_provider: Some(CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + definition_provider: Some(lsp_types::OneOf::Left(true)), + ..Default::default() + })?; + + let init_params = self.connection.initialize(server_capabilities)?; + let _init_params: InitializeParams = serde_json::from_value(init_params)?; + + eprintln!("LSP server initialized"); + + // Main message loop + loop { + let msg = match self.connection.receiver.recv() { + Ok(msg) => msg, + Err(_) => break, // Channel closed + }; + + match msg { + Message::Request(req) => { + if self.connection.handle_shutdown(&req)? { + return Ok(()); + } + self.handle_request(req)?; + } + Message::Notification(not) => { + self.handle_notification(not)?; + } + Message::Response(_) => {} + } + } + + Ok(()) + } + + fn handle_request(&self, req: Request) -> Result<(), Box> { + let req = match cast_request::(req) { + Ok((id, params)) => { + let result = self.handle_hover(params); + let resp = Response::new_ok(id, result); + self.connection.sender.send(Message::Response(resp))?; + return Ok(()); + } + Err(req) => req, + }; + + let req = match cast_request::(req) { + Ok((id, params)) => { + let result = self.handle_completion(params); + let resp = Response::new_ok(id, result); + self.connection.sender.send(Message::Response(resp))?; + return Ok(()); + } + Err(req) => req, + }; + + let _req = match cast_request::(req) { + Ok((id, params)) => { + let result = self.handle_goto_definition(params); + let resp = Response::new_ok(id, result); + self.connection.sender.send(Message::Response(resp))?; + return Ok(()); + } + Err(req) => req, + }; + + Ok(()) + } + + fn handle_notification( + &mut self, + not: lsp_server::Notification, + ) -> Result<(), Box> { + match not.method.as_str() { + DidOpenTextDocument::METHOD => { + let params: DidOpenTextDocumentParams = serde_json::from_value(not.params)?; + let uri = params.text_document.uri; + let text = params.text_document.text; + self.documents.insert(uri.clone(), text.clone()); + self.publish_diagnostics(uri, &text)?; + } + DidChangeTextDocument::METHOD => { + let params: DidChangeTextDocumentParams = serde_json::from_value(not.params)?; + let uri = params.text_document.uri; + if let Some(change) = params.content_changes.into_iter().last() { + self.documents.insert(uri.clone(), change.text.clone()); + self.publish_diagnostics(uri, &change.text)?; + } + } + _ => {} + } + Ok(()) + } + + fn publish_diagnostics( + &self, + uri: Url, + text: &str, + ) -> Result<(), Box> { + let diagnostics = self.get_diagnostics(text); + + let params = PublishDiagnosticsParams { + uri, + diagnostics, + version: None, + }; + + self.connection.sender.send(Message::Notification( + lsp_server::Notification::new( + "textDocument/publishDiagnostics".to_string(), + params, + ), + ))?; + + Ok(()) + } + + fn get_diagnostics(&self, source: &str) -> Vec { + let mut diagnostics = Vec::new(); + + // Parse the source + let program = match Parser::parse_source(source) { + Ok(prog) => prog, + Err(err) => { + diagnostics.push(Diagnostic { + range: span_to_range(source, err.span.start, err.span.end), + severity: Some(DiagnosticSeverity::ERROR), + message: err.message, + ..Default::default() + }); + return diagnostics; + } + }; + + // Type check + let mut checker = TypeChecker::new(); + if let Err(errors) = checker.check_program(&program) { + for error in errors { + diagnostics.push(Diagnostic { + range: span_to_range(source, error.span.start, error.span.end), + severity: Some(DiagnosticSeverity::ERROR), + message: error.message, + ..Default::default() + }); + } + } + + diagnostics + } + + fn handle_hover(&self, params: HoverParams) -> Option { + let uri = params.text_document_position_params.text_document.uri; + let _position = params.text_document_position_params.position; + + let _source = self.documents.get(&uri)?; + + // For now, return basic hover info + // A full implementation would find the symbol at the position + // and return its type and documentation + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: "Lux language element".to_string(), + }), + range: None, + }) + } + + fn handle_completion(&self, params: CompletionParams) -> Option { + let _uri = params.text_document_position.text_document.uri; + + // Return basic completions + // A full implementation would analyze context and provide + // relevant completions based on scope + let items = vec![ + CompletionItem { + label: "fn".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Function declaration".to_string()), + ..Default::default() + }, + CompletionItem { + label: "let".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Variable binding".to_string()), + ..Default::default() + }, + CompletionItem { + label: "if".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Conditional expression".to_string()), + ..Default::default() + }, + CompletionItem { + label: "match".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Pattern matching".to_string()), + ..Default::default() + }, + CompletionItem { + label: "type".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Type declaration".to_string()), + ..Default::default() + }, + CompletionItem { + label: "effect".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Effect declaration".to_string()), + ..Default::default() + }, + CompletionItem { + label: "handler".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Effect handler".to_string()), + ..Default::default() + }, + CompletionItem { + label: "trait".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Trait declaration".to_string()), + ..Default::default() + }, + CompletionItem { + label: "impl".to_string(), + kind: Some(CompletionItemKind::KEYWORD), + detail: Some("Trait implementation".to_string()), + ..Default::default() + }, + ]; + + Some(CompletionResponse::Array(items)) + } + + fn handle_goto_definition( + &self, + _params: GotoDefinitionParams, + ) -> Option { + // A full implementation would find the definition location + // of the symbol at the given position + None + } +} + +/// Convert byte offsets to LSP Position +fn span_to_range(source: &str, start: usize, end: usize) -> Range { + let start_pos = offset_to_position(source, start); + let end_pos = offset_to_position(source, end); + Range { + start: start_pos, + end: end_pos, + } +} + +fn offset_to_position(source: &str, offset: usize) -> Position { + let mut line = 0u32; + let mut col = 0u32; + + for (i, c) in source.char_indices() { + if i >= offset { + break; + } + if c == '\n' { + line += 1; + col = 0; + } else { + col += 1; + } + } + + Position { + line, + character: col, + } +} + +fn cast_request(req: Request) -> Result<(RequestId, R::Params), Request> +where + R: lsp_types::request::Request, + R::Params: serde::de::DeserializeOwned, +{ + match req.extract(R::METHOD) { + Ok(params) => Ok(params), + Err(ExtractError::MethodMismatch(req)) => Err(req), + Err(ExtractError::JsonError { .. }) => { + // This shouldn't happen if the client is well-behaved + panic!("JSON deserialization error in LSP request") + } + } +} diff --git a/src/main.rs b/src/main.rs index 9e6b40c..2e2f702 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod diagnostics; mod exhaustiveness; mod interpreter; mod lexer; +mod lsp; mod modules; mod parser; mod schema; @@ -67,8 +68,31 @@ fn main() { let args: Vec = std::env::args().collect(); if args.len() > 1 { - // Run a file - run_file(&args[1]); + match args[1].as_str() { + "--lsp" => { + // Run LSP server + if let Err(e) = lsp::LspServer::run() { + eprintln!("LSP server error: {}", e); + std::process::exit(1); + } + } + "--help" | "-h" => { + println!("Lux {} - A functional language with first-class effects", VERSION); + println!(); + println!("Usage:"); + println!(" lux Start the REPL"); + println!(" lux Run a file"); + println!(" lux --lsp Start LSP server (for IDE integration)"); + println!(" lux --help Show this help"); + } + "--version" | "-v" => { + println!("Lux {}", VERSION); + } + path => { + // Run a file + run_file(path); + } + } } else { // Start REPL run_repl();