//! LSP (Language Server Protocol) server for Lux //! //! Provides IDE features like: //! - Diagnostics (errors and warnings) //! - Hover information //! - Go to definition //! - Find references //! - Completions //! - Document symbols //! - Rename refactoring //! - Signature help //! - Formatting use crate::parser::Parser; use crate::typechecker::TypeChecker; use crate::symbol_table::{SymbolTable, SymbolKind}; use crate::formatter::{format as format_source, FormatConfig}; use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response}; use lsp_types::{ notification::{DidChangeTextDocument, DidOpenTextDocument, Notification}, request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting, InlayHintRequest}, 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, ReferenceParams, Location, DocumentSymbolParams, DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit, SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation, SignatureHelpOptions, DocumentFormattingParams, InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams, }; use std::collections::HashMap; use std::error::Error; /// Cached document data struct DocumentCache { text: String, symbol_table: Option, } /// LSP Server for Lux pub struct LspServer { connection: Connection, /// Document contents and symbol tables 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)), references_provider: Some(lsp_types::OneOf::Left(true)), document_symbol_provider: Some(lsp_types::OneOf::Left(true)), rename_provider: Some(lsp_types::OneOf::Left(true)), signature_help_provider: Some(SignatureHelpOptions { trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), retrigger_characters: None, work_done_progress_options: Default::default(), }), document_formatting_provider: Some(lsp_types::OneOf::Left(true)), inlay_hint_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, }; let req = match cast_request::(req) { Ok((id, params)) => { let result = self.handle_references(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_document_symbols(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_rename(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_signature_help(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_formatting(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_inlay_hints(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.update_document(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() { let text = change.text.clone(); self.update_document(uri.clone(), text.clone()); self.publish_diagnostics(uri, &text)?; } } _ => {} } Ok(()) } fn update_document(&mut self, uri: Url, text: String) { // Build symbol table if parsing succeeds let symbol_table = Parser::parse_source(&text) .ok() .map(|program| SymbolTable::build(&program)); self.documents.insert(uri, DocumentCache { text, symbol_table, }); } 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 doc = self.documents.get(&uri)?; let source = &doc.text; // Try to get info from symbol table first (position-based lookup) if let Some(ref table) = doc.symbol_table { let offset = self.position_to_offset(source, position); if let Some(symbol) = table.definition_at_position(offset) { return Some(self.format_symbol_hover(symbol)); } } // Get the word under cursor let word = self.get_word_at_position(source, position)?; // When hovering on a keyword like 'fn', 'type', 'effect', 'let', 'trait', // look ahead to find the declaration name and show that symbol's info if let Some(ref table) = doc.symbol_table { if matches!(word.as_str(), "fn" | "type" | "effect" | "let" | "trait" | "handler" | "impl") { let offset = self.position_to_offset(source, position); if let Some(name) = self.find_next_ident(source, offset + word.len()) { for sym in table.global_symbols() { if sym.name == name { return Some(self.format_symbol_hover(sym)); } } } } // Try name-based lookup in symbol table (for usage sites) for sym in table.global_symbols() { if sym.name == word { return Some(self.format_symbol_hover(sym)); } } } // Check for module names (Console, List, String, etc.) if let Some(hover) = self.get_module_hover(&word) { return Some(hover); } // Rich documentation for behavioral property keywords if let Some((signature, doc_text)) = self.get_rich_symbol_info(&word) { return Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: format!("```lux\n{}\n```\n\n{}", signature, doc_text), }), range: None, }); } // Builtin keyword/function info if let Some((signature, doc_text)) = self.get_symbol_info(&word) { return Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: format!("```lux\n{}\n```\n\n{}", signature, doc_text), }), range: None, }); } None } /// Format a symbol into a hover response fn format_symbol_hover(&self, symbol: &crate::symbol_table::Symbol) -> Hover { let signature = symbol.type_signature.as_ref() .map(|s| s.as_str()) .unwrap_or(&symbol.name); let kind_str = match symbol.kind { SymbolKind::Function => "function", SymbolKind::Variable => "variable", SymbolKind::Parameter => "parameter", SymbolKind::Type => "type", SymbolKind::TypeParameter => "type parameter", SymbolKind::Variant => "variant", SymbolKind::Effect => "effect", SymbolKind::EffectOperation => "effect operation", SymbolKind::Field => "field", SymbolKind::Module => "module", }; let doc_str = symbol.documentation.as_ref() .map(|d| format!("\n\n{}", d)) .unwrap_or_default(); let formatted_sig = format_signature_for_hover(signature); let property_docs = extract_property_docs(signature); Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: format!( "```lux\n{}\n```\n*{}*{}{}", formatted_sig, kind_str, property_docs, doc_str ), }), range: None, } } /// Get hover info for built-in module names fn get_module_hover(&self, name: &str) -> Option { let (sig, doc) = match name { "Console" => ( "effect Console", "**Console I/O**\n\n\ - `Console.print(msg: String): Unit` — print to stdout\n\ - `Console.readLine(): String` — read a line from stdin\n\ - `Console.readInt(): Int` — read an integer from stdin", ), "File" => ( "effect File", "**File System**\n\n\ - `File.read(path: String): String` — read file contents\n\ - `File.write(path: String, content: String): Unit` — write to file\n\ - `File.append(path: String, content: String): Unit` — append to file\n\ - `File.exists(path: String): Bool` — check if file exists\n\ - `File.delete(path: String): Unit` — delete a file\n\ - `File.list(path: String): List` — list directory", ), "Http" => ( "effect Http", "**HTTP Client**\n\n\ - `Http.get(url: String): String` — GET request\n\ - `Http.post(url: String, body: String): String` — POST request\n\ - `Http.put(url: String, body: String): String` — PUT request\n\ - `Http.delete(url: String): String` — DELETE request", ), "Sql" => ( "effect Sql", "**SQL Database**\n\n\ - `Sql.open(path: String): Connection` — open database\n\ - `Sql.execute(conn: Connection, sql: String): Unit` — execute SQL\n\ - `Sql.query(conn: Connection, sql: String): List` — query rows\n\ - `Sql.close(conn: Connection): Unit` — close connection", ), "Random" => ( "effect Random", "**Random Number Generation**\n\n\ - `Random.int(min: Int, max: Int): Int` — random integer\n\ - `Random.float(): Float` — random float 0.0–1.0\n\ - `Random.bool(): Bool` — random boolean", ), "Time" => ( "effect Time", "**Time**\n\n\ - `Time.now(): Int` — current Unix timestamp (ms)\n\ - `Time.sleep(ms: Int): Unit` — sleep for milliseconds", ), "Process" => ( "effect Process", "**Process / System**\n\n\ - `Process.exec(cmd: String): String` — run shell command\n\ - `Process.env(name: String): String` — get env variable\n\ - `Process.args(): List` — command-line arguments\n\ - `Process.exit(code: Int): Unit` — exit with code", ), "Math" => ( "module Math", "**Math Functions**\n\n\ - `Math.abs(n: Int): Int` — absolute value\n\ - `Math.min(a: Int, b: Int): Int` — minimum\n\ - `Math.max(a: Int, b: Int): Int` — maximum\n\ - `Math.sqrt(n: Float): Float` — square root\n\ - `Math.pow(base: Float, exp: Float): Float` — power\n\ - `Math.floor(n: Float): Int` — round down\n\ - `Math.ceil(n: Float): Int` — round up", ), "List" => ( "module List", "**List Operations**\n\n\ - `List.map(list, f)` — transform each element\n\ - `List.filter(list, p)` — keep matching elements\n\ - `List.fold(list, init, f)` — reduce to single value\n\ - `List.head(list)` — first element (Option)\n\ - `List.tail(list)` — all except first (Option)\n\ - `List.length(list)` — number of elements\n\ - `List.concat(a, b)` — concatenate lists\n\ - `List.range(start, end)` — integer range\n\ - `List.reverse(list)` — reverse order\n\ - `List.get(list, i)` — element at index (Option)", ), "String" => ( "module String", "**String Operations**\n\n\ - `String.length(s)` — string length\n\ - `String.split(s, delim)` — split by delimiter\n\ - `String.join(list, delim)` — join with delimiter\n\ - `String.trim(s)` — trim whitespace\n\ - `String.contains(s, sub)` — check substring\n\ - `String.replace(s, from, to)` — replace occurrences\n\ - `String.startsWith(s, prefix)` — check prefix\n\ - `String.endsWith(s, suffix)` — check suffix\n\ - `String.substring(s, start, end)` — extract range\n\ - `String.chars(s)` — list of characters", ), "Option" => ( "type Option = Some(A) | None", "**Optional Value**\n\n\ - `Option.isSome(opt)` — has a value?\n\ - `Option.isNone(opt)` — is empty?\n\ - `Option.getOrElse(opt, default)` — unwrap or default\n\ - `Option.map(opt, f)` — transform if present\n\ - `Option.flatMap(opt, f)` — chain operations", ), "Result" => ( "type Result = Ok(A) | Err(E)", "**Result of Fallible Operation**\n\n\ - `Result.isOk(r)` — succeeded?\n\ - `Result.isErr(r)` — failed?\n\ - `Result.map(r, f)` — transform success value\n\ - `Result.mapErr(r, f)` — transform error value", ), _ => return None, }; Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: format!("```lux\n{}\n```\n{}", sig, doc), }), range: None, }) } fn get_word_at_position(&self, source: &str, position: Position) -> Option { let lines: Vec<&str> = source.lines().collect(); let line = lines.get(position.line as usize)?; let col = position.character as usize; // Find word boundaries let start = line[..col.min(line.len())] .rfind(|c: char| !c.is_alphanumeric() && c != '_') .map(|i| i + 1) .unwrap_or(0); let end = line[col.min(line.len())..] .find(|c: char| !c.is_alphanumeric() && c != '_') .map(|i| col + i) .unwrap_or(line.len()); if start < end { Some(line[start..end].to_string()) } else { None } } /// Find the next identifier in source after the given offset (skipping whitespace) fn find_next_ident(&self, source: &str, start: usize) -> Option { let chars: Vec = source.chars().collect(); let mut pos = start; // Skip whitespace while pos < chars.len() && (chars[pos] == ' ' || chars[pos] == '\t' || chars[pos] == '\n' || chars[pos] == '\r') { pos += 1; } // Collect identifier let ident_start = pos; while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') { pos += 1; } if pos > ident_start { Some(chars[ident_start..pos].iter().collect()) } else { None } } fn get_symbol_info(&self, word: &str) -> Option<(&'static str, &'static str)> { match word { // Keywords "fn" => Some(("fn name(params): ReturnType = body", "Declare a function")), "let" => Some(("let name = value", "Bind a value to a name")), "if" => Some(("if condition then expr else expr", "Conditional expression")), "match" => Some(("match value { pattern => expr, ... }", "Pattern matching")), "type" => Some(("type Name = TypeExpr", "Define a type alias or ADT")), "effect" => Some(("effect Name { fn op(): Type }", "Define an effect")), "handler" => Some(("handler { op() => impl }", "Handle effects")), "trait" => Some(("trait Name { fn method(): Type }", "Define a trait")), "impl" => Some(("impl Trait for Type { ... }", "Implement a trait")), // List functions "map" => Some(("List.map(list: List, f: A -> B): List", "Transform each element in a list")), "filter" => Some(("List.filter(list: List, p: A -> Bool): List", "Keep elements matching predicate")), "fold" => Some(("List.fold(list: List, init: B, f: (B, A) -> B): B", "Reduce list to single value")), "reverse" => Some(("List.reverse(list: List): List", "Reverse a list")), "concat" => Some(("List.concat(a: List, b: List): List", "Concatenate two lists")), "range" => Some(("List.range(start: Int, end: Int): List", "Create a list from start to end-1")), "length" => Some(("List.length(list: List): Int", "Get the length of a list")), "head" => Some(("List.head(list: List): Option", "Get the first element")), "tail" => Some(("List.tail(list: List): List", "Get all elements except the first")), "isEmpty" => Some(("List.isEmpty(list: List): Bool", "Check if list is empty")), // Option/Result "Some" => Some(("Some(value: A): Option", "Wrap a value in Some")), "None" => Some(("None: Option", "The empty Option")), "Ok" => Some(("Ok(value: A): Result", "Successful result")), "Err" => Some(("Err(error: E): Result", "Error result")), "isSome" => Some(("Option.isSome(opt: Option): Bool", "Check if Option has a value")), "isNone" => Some(("Option.isNone(opt: Option): Bool", "Check if Option is empty")), "getOrElse" => Some(("Option.getOrElse(opt: Option, default: A): A", "Get value or default")), // Console "print" => Some(("Console.print(msg: String): Unit", "Print a message to the console")), "readLine" => Some(("Console.readLine(): String", "Read a line from input")), "readInt" => Some(("Console.readInt(): Int", "Read an integer from input")), // Types "Int" => Some(("type Int", "64-bit signed integer")), "Float" => Some(("type Float", "64-bit floating point number")), "Bool" => Some(("type Bool", "Boolean (true or false)")), "String" => Some(("type String", "UTF-8 string")), "Unit" => Some(("type Unit", "Unit type (no value)")), "List" => Some(("type List", "Generic list/array type")), "Option" => Some(("type Option = Some(A) | None", "Optional value (Some or None)")), "Result" => Some(("type Result = Ok(A) | Err(E)", "Result of fallible operation")), // Built-in functions "toString" => Some(("toString(value: A): String", "Convert any value to a string")), _ => None, } } /// Rich documentation for behavioral properties and keywords fn get_rich_symbol_info(&self, word: &str) -> Option<(String, String)> { match word { "pure" => Some(( "is pure".to_string(), "**Behavioral Property: Pure**\n\n\ A pure function has no side effects and always produces the same output for the same inputs. \ The compiler can safely memoize calls, reorder them, or eliminate duplicates.\n\n\ ```lux\nfn add(a: Int, b: Int): Int is pure = a + b\n```\n\n\ **Guarantees:**\n\ - No effect operations (Console, File, Http, etc.)\n\ - Referential transparency: `f(x)` can be replaced with its result\n\ - Enables memoization and common subexpression elimination".to_string(), )), "total" => Some(( "is total".to_string(), "**Behavioral Property: Total**\n\n\ A total function always terminates and never throws exceptions. \ The compiler verifies termination through structural recursion analysis.\n\n\ ```lux\nfn factorial(n: Int): Int is total =\n if n <= 0 then 1\n else n * factorial(n - 1)\n```\n\n\ **Guarantees:**\n\ - Always produces a result (no infinite loops)\n\ - Cannot use the `Fail` effect\n\ - Recursive calls must be structurally decreasing".to_string(), )), "idempotent" => Some(( "is idempotent".to_string(), "**Behavioral Property: Idempotent**\n\n\ An idempotent function satisfies `f(f(x)) == f(x)` for all inputs. \ Applying it multiple times has the same effect as applying it once.\n\n\ ```lux\nfn abs(x: Int): Int is idempotent =\n if x < 0 then 0 - x else x\n\n\ fn clamp(x: Int): Int is idempotent =\n if x < 0 then 0\n else if x > 100 then 100\n else x\n```\n\n\ **Guarantees:**\n\ - `f(f(x)) == f(x)` for all valid inputs\n\ - Safe to retry without changing outcome\n\ - Compiler can deduplicate consecutive calls".to_string(), )), "deterministic" => Some(( "is deterministic".to_string(), "**Behavioral Property: Deterministic**\n\n\ A deterministic function always produces the same output for the same inputs, \ with no dependence on randomness, time, or external state.\n\n\ ```lux\nfn multiply(a: Int, b: Int): Int is deterministic = a * b\n```\n\n\ **Guarantees:**\n\ - Cannot use `Random` or `Time` effects\n\ - Same inputs always produce same outputs\n\ - Results can be cached across runs".to_string(), )), "commutative" => Some(( "is commutative".to_string(), "**Behavioral Property: Commutative**\n\n\ A commutative function satisfies `f(a, b) == f(b, a)`. \ The order of arguments doesn't affect the result.\n\n\ ```lux\nfn add(a: Int, b: Int): Int is commutative = a + b\nfn max(a: Int, b: Int): Int is commutative =\n if a > b then a else b\n```\n\n\ **Guarantees:**\n\ - Must have exactly 2 parameters\n\ - `f(a, b) == f(b, a)` for all inputs\n\ - Compiler can normalize argument order for optimization".to_string(), )), "run" => Some(( "run expr with { handlers }".to_string(), "**Effect Handler**\n\n\ Execute an effectful expression with explicit effect handlers. \ Must be bound to a variable at top level.\n\n\ ```lux\nlet result = run myFunction() with {\n Console = { /* handler */ }\n}\n```\n\n\ Handlers intercept effect operations and provide implementations.".to_string(), )), "with" => Some(( "with {Effect1, Effect2}".to_string(), "**Effect Declaration / Handler Block**\n\n\ Declares which effects a function may perform, or provides handlers in a `run` expression.\n\n\ ```lux\n// In function signature:\nfn greet(name: String): Unit with {Console} =\n Console.print(\"Hello, \" + name)\n\n\ // In run expression:\nlet _ = run greet(\"world\") with {}\n```".to_string(), )), _ => None, } } fn handle_completion(&self, params: CompletionParams) -> Option { let uri = params.text_document_position.text_document.uri; let position = params.text_document_position.position; // Check context to provide relevant completions let doc = self.documents.get(&uri)?; let source = &doc.text; let trigger_context = self.get_completion_context(source, position); let mut items = Vec::new(); // If triggered after a dot, provide module-specific completions match trigger_context { CompletionContext::ModuleAccess(ref module) => { match module.as_str() { "List" => items.extend(self.get_list_completions()), "String" => items.extend(self.get_string_completions()), "Option" | "Result" => items.extend(self.get_option_result_completions()), "Console" => items.extend(self.get_console_completions()), "Math" => items.extend(self.get_math_completions()), "Sql" => items.extend(self.get_sql_completions()), "File" => items.extend(self.get_file_completions()), "Process" => items.extend(self.get_process_completions()), "Http" => items.extend(self.get_http_completions()), "Random" => items.extend(self.get_random_completions()), "Time" => items.extend(self.get_time_completions()), _ => { // Unknown module, show all module completions items.extend(self.get_list_completions()); items.extend(self.get_string_completions()); items.extend(self.get_option_result_completions()); items.extend(self.get_console_completions()); items.extend(self.get_math_completions()); items.extend(self.get_sql_completions()); items.extend(self.get_file_completions()); items.extend(self.get_process_completions()); items.extend(self.get_http_completions()); items.extend(self.get_random_completions()); items.extend(self.get_time_completions()); } } } CompletionContext::General => { // General completions (keywords + common functions) items.extend(self.get_keyword_completions()); items.extend(self.get_builtin_completions()); items.extend(self.get_type_completions()); } } Some(CompletionResponse::Array(items)) } fn get_completion_context(&self, source: &str, position: Position) -> CompletionContext { // Find the character before the cursor let offset = self.position_to_offset(source, position); if offset > 0 { let prev_char = source.chars().nth(offset - 1); if prev_char == Some('.') { // Extract the module name before the dot if let Some(module_name) = self.get_word_at_offset(source, offset.saturating_sub(2)) { return CompletionContext::ModuleAccess(module_name); } return CompletionContext::ModuleAccess(String::new()); } } CompletionContext::General } fn position_to_offset(&self, source: &str, position: Position) -> usize { let mut offset = 0; for (line_idx, line) in source.lines().enumerate() { if line_idx == position.line as usize { return offset + (position.character as usize).min(line.len()); } offset += line.len() + 1; // +1 for newline } source.len() } fn get_keyword_completions(&self) -> Vec { vec![ completion_item("fn", CompletionItemKind::KEYWORD, "Function declaration"), completion_item("let", CompletionItemKind::KEYWORD, "Variable binding"), completion_item("if", CompletionItemKind::KEYWORD, "Conditional expression"), completion_item("then", CompletionItemKind::KEYWORD, "Then branch"), completion_item("else", CompletionItemKind::KEYWORD, "Else branch"), completion_item("match", CompletionItemKind::KEYWORD, "Pattern matching"), completion_item("type", CompletionItemKind::KEYWORD, "Type declaration"), completion_item("effect", CompletionItemKind::KEYWORD, "Effect declaration"), completion_item("handler", CompletionItemKind::KEYWORD, "Effect handler"), completion_item("trait", CompletionItemKind::KEYWORD, "Trait declaration"), completion_item("impl", CompletionItemKind::KEYWORD, "Trait implementation"), completion_item("import", CompletionItemKind::KEYWORD, "Import declaration"), completion_item("return", CompletionItemKind::KEYWORD, "Return from function"), completion_item("run", CompletionItemKind::KEYWORD, "Run effectful computation"), completion_item("with", CompletionItemKind::KEYWORD, "With handler"), completion_item("true", CompletionItemKind::KEYWORD, "Boolean true"), completion_item("false", CompletionItemKind::KEYWORD, "Boolean false"), ] } fn get_builtin_completions(&self) -> Vec { vec![ // Core modules completion_item("List", CompletionItemKind::MODULE, "List module"), completion_item("String", CompletionItemKind::MODULE, "String module"), completion_item("Console", CompletionItemKind::MODULE, "Console I/O effect"), completion_item("Math", CompletionItemKind::MODULE, "Math functions"), completion_item("Option", CompletionItemKind::MODULE, "Option type"), completion_item("Result", CompletionItemKind::MODULE, "Result type"), // Effect modules completion_item("Sql", CompletionItemKind::MODULE, "SQL database effect"), completion_item("File", CompletionItemKind::MODULE, "File system effect"), completion_item("Process", CompletionItemKind::MODULE, "Process/system effect"), completion_item("Http", CompletionItemKind::MODULE, "HTTP client effect"), completion_item("Random", CompletionItemKind::MODULE, "Random number effect"), completion_item("Time", CompletionItemKind::MODULE, "Time effect"), // Constructors completion_item("Some", CompletionItemKind::CONSTRUCTOR, "Option.Some constructor"), completion_item("None", CompletionItemKind::CONSTRUCTOR, "Option.None constructor"), completion_item("Ok", CompletionItemKind::CONSTRUCTOR, "Result.Ok constructor"), completion_item("Err", CompletionItemKind::CONSTRUCTOR, "Result.Err constructor"), // Functions completion_item("toString", CompletionItemKind::FUNCTION, "Convert value to string"), ] } fn get_type_completions(&self) -> Vec { vec![ completion_item("Int", CompletionItemKind::TYPE_PARAMETER, "Integer type"), completion_item("Float", CompletionItemKind::TYPE_PARAMETER, "Floating point type"), completion_item("Bool", CompletionItemKind::TYPE_PARAMETER, "Boolean type"), completion_item("String", CompletionItemKind::TYPE_PARAMETER, "String type"), completion_item("Unit", CompletionItemKind::TYPE_PARAMETER, "Unit type"), completion_item("List", CompletionItemKind::TYPE_PARAMETER, "List type"), completion_item("Option", CompletionItemKind::TYPE_PARAMETER, "Option type"), completion_item("Result", CompletionItemKind::TYPE_PARAMETER, "Result type"), ] } fn get_list_completions(&self) -> Vec { vec![ completion_item_with_doc("length", CompletionItemKind::METHOD, "List.length(list)", "Get the length of a list"), completion_item_with_doc("head", CompletionItemKind::METHOD, "List.head(list)", "Get first element (returns Option)"), completion_item_with_doc("tail", CompletionItemKind::METHOD, "List.tail(list)", "Get all but first element"), completion_item_with_doc("map", CompletionItemKind::METHOD, "List.map(list, fn)", "Transform each element"), completion_item_with_doc("filter", CompletionItemKind::METHOD, "List.filter(list, predicate)", "Keep elements matching predicate"), completion_item_with_doc("fold", CompletionItemKind::METHOD, "List.fold(list, init, fn)", "Reduce list to single value"), completion_item_with_doc("reverse", CompletionItemKind::METHOD, "List.reverse(list)", "Reverse list order"), completion_item_with_doc("concat", CompletionItemKind::METHOD, "List.concat(a, b)", "Concatenate two lists"), completion_item_with_doc("range", CompletionItemKind::METHOD, "List.range(start, end)", "Create list from range"), completion_item_with_doc("get", CompletionItemKind::METHOD, "List.get(list, index)", "Get element at index (returns Option)"), completion_item_with_doc("find", CompletionItemKind::METHOD, "List.find(list, predicate)", "Find first matching element"), completion_item_with_doc("isEmpty", CompletionItemKind::METHOD, "List.isEmpty(list)", "Check if list is empty"), completion_item_with_doc("take", CompletionItemKind::METHOD, "List.take(list, n)", "Take first n elements"), completion_item_with_doc("drop", CompletionItemKind::METHOD, "List.drop(list, n)", "Drop first n elements"), completion_item_with_doc("any", CompletionItemKind::METHOD, "List.any(list, predicate)", "Check if any element matches"), completion_item_with_doc("all", CompletionItemKind::METHOD, "List.all(list, predicate)", "Check if all elements match"), ] } fn get_string_completions(&self) -> Vec { vec![ completion_item_with_doc("length", CompletionItemKind::METHOD, "String.length(s)", "Get string length"), completion_item_with_doc("split", CompletionItemKind::METHOD, "String.split(s, delimiter)", "Split string by delimiter"), completion_item_with_doc("join", CompletionItemKind::METHOD, "String.join(list, delimiter)", "Join strings with delimiter"), completion_item_with_doc("trim", CompletionItemKind::METHOD, "String.trim(s)", "Remove leading/trailing whitespace"), completion_item_with_doc("contains", CompletionItemKind::METHOD, "String.contains(s, substr)", "Check if contains substring"), completion_item_with_doc("replace", CompletionItemKind::METHOD, "String.replace(s, from, to)", "Replace occurrences"), completion_item_with_doc("chars", CompletionItemKind::METHOD, "String.chars(s)", "Get list of characters"), completion_item_with_doc("lines", CompletionItemKind::METHOD, "String.lines(s)", "Split into lines"), completion_item_with_doc("fromChar", CompletionItemKind::METHOD, "String.fromChar(c)", "Convert char to string"), ] } fn get_option_result_completions(&self) -> Vec { vec![ completion_item_with_doc("isSome", CompletionItemKind::METHOD, "Option.isSome(opt)", "Check if Option has value"), completion_item_with_doc("isNone", CompletionItemKind::METHOD, "Option.isNone(opt)", "Check if Option is empty"), completion_item_with_doc("getOrElse", CompletionItemKind::METHOD, "Option.getOrElse(opt, default)", "Get value or default"), completion_item_with_doc("map", CompletionItemKind::METHOD, "Option.map(opt, fn)", "Transform value if present"), completion_item_with_doc("flatMap", CompletionItemKind::METHOD, "Option.flatMap(opt, fn)", "Chain Option operations"), completion_item_with_doc("isOk", CompletionItemKind::METHOD, "Result.isOk(result)", "Check if Result is Ok"), completion_item_with_doc("isErr", CompletionItemKind::METHOD, "Result.isErr(result)", "Check if Result is Err"), ] } fn get_console_completions(&self) -> Vec { vec![ completion_item_with_doc("print", CompletionItemKind::METHOD, "Console.print(msg)", "Print to console"), completion_item_with_doc("readLine", CompletionItemKind::METHOD, "Console.readLine()", "Read line from input"), completion_item_with_doc("readInt", CompletionItemKind::METHOD, "Console.readInt()", "Read integer from input"), ] } fn get_math_completions(&self) -> Vec { vec![ completion_item_with_doc("abs", CompletionItemKind::METHOD, "Math.abs(n)", "Absolute value"), completion_item_with_doc("min", CompletionItemKind::METHOD, "Math.min(a, b)", "Minimum of two values"), completion_item_with_doc("max", CompletionItemKind::METHOD, "Math.max(a, b)", "Maximum of two values"), completion_item_with_doc("sqrt", CompletionItemKind::METHOD, "Math.sqrt(n)", "Square root"), completion_item_with_doc("pow", CompletionItemKind::METHOD, "Math.pow(base, exp)", "Power function"), completion_item_with_doc("floor", CompletionItemKind::METHOD, "Math.floor(n)", "Round down"), completion_item_with_doc("ceil", CompletionItemKind::METHOD, "Math.ceil(n)", "Round up"), completion_item_with_doc("round", CompletionItemKind::METHOD, "Math.round(n)", "Round to nearest"), ] } fn get_sql_completions(&self) -> Vec { vec![ completion_item_with_doc("open", CompletionItemKind::METHOD, "Sql.open(path)", "Open SQLite database file"), completion_item_with_doc("openMemory", CompletionItemKind::METHOD, "Sql.openMemory()", "Open in-memory database"), completion_item_with_doc("close", CompletionItemKind::METHOD, "Sql.close(conn)", "Close database connection"), completion_item_with_doc("execute", CompletionItemKind::METHOD, "Sql.execute(conn, sql)", "Execute SQL statement"), completion_item_with_doc("query", CompletionItemKind::METHOD, "Sql.query(conn, sql)", "Query and return rows"), completion_item_with_doc("queryOne", CompletionItemKind::METHOD, "Sql.queryOne(conn, sql)", "Query single row"), completion_item_with_doc("beginTx", CompletionItemKind::METHOD, "Sql.beginTx(conn)", "Begin transaction"), completion_item_with_doc("commit", CompletionItemKind::METHOD, "Sql.commit(conn)", "Commit transaction"), completion_item_with_doc("rollback", CompletionItemKind::METHOD, "Sql.rollback(conn)", "Rollback transaction"), ] } fn get_file_completions(&self) -> Vec { vec![ completion_item_with_doc("read", CompletionItemKind::METHOD, "File.read(path)", "Read file contents"), completion_item_with_doc("write", CompletionItemKind::METHOD, "File.write(path, content)", "Write to file"), completion_item_with_doc("append", CompletionItemKind::METHOD, "File.append(path, content)", "Append to file"), completion_item_with_doc("exists", CompletionItemKind::METHOD, "File.exists(path)", "Check if file exists"), completion_item_with_doc("delete", CompletionItemKind::METHOD, "File.delete(path)", "Delete file"), completion_item_with_doc("list", CompletionItemKind::METHOD, "File.list(path)", "List directory contents"), ] } fn get_process_completions(&self) -> Vec { vec![ completion_item_with_doc("exec", CompletionItemKind::METHOD, "Process.exec(cmd)", "Execute shell command"), completion_item_with_doc("env", CompletionItemKind::METHOD, "Process.env(name)", "Get environment variable"), completion_item_with_doc("args", CompletionItemKind::METHOD, "Process.args()", "Get command-line arguments"), completion_item_with_doc("cwd", CompletionItemKind::METHOD, "Process.cwd()", "Get current directory"), completion_item_with_doc("exit", CompletionItemKind::METHOD, "Process.exit(code)", "Exit with code"), ] } fn get_http_completions(&self) -> Vec { vec![ completion_item_with_doc("get", CompletionItemKind::METHOD, "Http.get(url)", "HTTP GET request"), completion_item_with_doc("post", CompletionItemKind::METHOD, "Http.post(url, body)", "HTTP POST request"), completion_item_with_doc("put", CompletionItemKind::METHOD, "Http.put(url, body)", "HTTP PUT request"), completion_item_with_doc("delete", CompletionItemKind::METHOD, "Http.delete(url)", "HTTP DELETE request"), ] } fn get_random_completions(&self) -> Vec { vec![ completion_item_with_doc("int", CompletionItemKind::METHOD, "Random.int(min, max)", "Random integer in range"), completion_item_with_doc("float", CompletionItemKind::METHOD, "Random.float()", "Random float 0.0-1.0"), completion_item_with_doc("bool", CompletionItemKind::METHOD, "Random.bool()", "Random boolean"), ] } fn get_time_completions(&self) -> Vec { vec![ completion_item_with_doc("now", CompletionItemKind::METHOD, "Time.now()", "Current Unix timestamp (ms)"), completion_item_with_doc("sleep", CompletionItemKind::METHOD, "Time.sleep(ms)", "Sleep for milliseconds"), ] } fn handle_goto_definition( &self, params: GotoDefinitionParams, ) -> Option { let uri = params.text_document_position_params.text_document.uri; let position = params.text_document_position_params.position; let doc = self.documents.get(&uri)?; let source = &doc.text; // Try symbol table first if let Some(ref table) = doc.symbol_table { let offset = self.position_to_offset(source, position); if let Some(symbol) = table.definition_at_position(offset) { let range = span_to_range(source, symbol.span.start, symbol.span.end); return Some(GotoDefinitionResponse::Scalar(Location { uri, range, })); } } // Fall back to pattern matching let offset = self.position_to_offset(source, position); let word = self.get_word_at_offset(source, offset)?; // Search for function definition in the same file // Look for "fn " pattern let fn_pattern = format!("fn {}", word); if let Some(def_offset) = source.find(&fn_pattern) { let range = span_to_range(source, def_offset + 3, def_offset + 3 + word.len()); return Some(GotoDefinitionResponse::Scalar(Location { uri, range, })); } // Look for "let " pattern let let_pattern = format!("let {} ", word); if let Some(def_offset) = source.find(&let_pattern) { let range = span_to_range(source, def_offset + 4, def_offset + 4 + word.len()); return Some(GotoDefinitionResponse::Scalar(Location { uri, range, })); } // Look for type definition "type " let type_pattern = format!("type {}", word); if let Some(def_offset) = source.find(&type_pattern) { let range = span_to_range(source, def_offset + 5, def_offset + 5 + word.len()); return Some(GotoDefinitionResponse::Scalar(Location { uri, range, })); } None } fn handle_references(&self, params: ReferenceParams) -> Option> { let uri = params.text_document_position.text_document.uri; let position = params.text_document_position.position; let doc = self.documents.get(&uri)?; let source = &doc.text; if let Some(ref table) = doc.symbol_table { let offset = self.position_to_offset(source, position); if let Some(symbol) = table.definition_at_position(offset) { let refs = table.find_references(symbol.id); let locations: Vec = refs.iter() .map(|r| Location { uri: uri.clone(), range: span_to_range(source, r.span.start, r.span.end), }) .collect(); return Some(locations); } } None } fn handle_document_symbols(&self, params: DocumentSymbolParams) -> Option { let uri = params.text_document.uri; let doc = self.documents.get(&uri)?; let source = &doc.text; if let Some(ref table) = doc.symbol_table { let symbols: Vec = table.global_symbols() .iter() .map(|sym| { #[allow(deprecated)] SymbolInformation { name: sym.name.clone(), kind: symbol_kind_to_lsp(&sym.kind), tags: None, deprecated: None, location: Location { uri: uri.clone(), range: span_to_range(source, sym.span.start, sym.span.end), }, container_name: None, } }) .collect(); return Some(DocumentSymbolResponse::Flat(symbols)); } None } fn get_word_at_offset(&self, source: &str, offset: usize) -> Option { let chars: Vec = source.chars().collect(); if offset >= chars.len() { return None; } // Find start of word let mut start = offset; while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') { start -= 1; } // Find end of word let mut end = offset; while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') { end += 1; } if start == end { return None; } Some(chars[start..end].iter().collect()) } fn handle_rename(&self, params: RenameParams) -> Option { let uri = params.text_document_position.text_document.uri; let position = params.text_document_position.position; let new_name = params.new_name; let doc = self.documents.get(&uri)?; let source = &doc.text; if let Some(ref table) = doc.symbol_table { let offset = self.position_to_offset(source, position); if let Some(symbol) = table.definition_at_position(offset) { // Find all references to this symbol let refs = table.find_references(symbol.id); // Create text edits for each reference let edits: Vec = refs.iter() .map(|r| TextEdit { range: span_to_range(source, r.span.start, r.span.end), new_text: new_name.clone(), }) .collect(); // Return workspace edit let mut changes = HashMap::new(); changes.insert(uri, edits); return Some(WorkspaceEdit { changes: Some(changes), document_changes: None, change_annotations: None, }); } } None } fn handle_signature_help(&self, params: SignatureHelpParams) -> Option { let uri = params.text_document_position_params.text_document.uri; let position = params.text_document_position_params.position; let doc = self.documents.get(&uri)?; let source = &doc.text; let offset = self.position_to_offset(source, position); // Find the function call context by searching backwards for '(' let chars: Vec = source.chars().collect(); let mut paren_depth = 0; let mut comma_count = 0; let mut func_start = offset; for i in (0..offset).rev() { let c = chars.get(i)?; match c { ')' => paren_depth += 1, '(' => { if paren_depth == 0 { func_start = i; break; } paren_depth -= 1; } ',' if paren_depth == 0 => comma_count += 1, _ => {} } } // Get the function name before the opening paren if func_start == 0 { return None; } let func_name = self.get_word_at_offset(source, func_start - 1)?; // Look up function in symbol table if let Some(ref table) = doc.symbol_table { // Search for function definition for sym in table.global_symbols() { if sym.name == func_name { if let Some(ref sig) = sym.type_signature { // Parse parameters from signature let params = self.extract_parameters_from_signature(sig); let signature_info = SignatureInformation { label: sig.clone(), documentation: sym.documentation.as_ref().map(|d| { lsp_types::Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: d.clone(), }) }), parameters: Some(params), active_parameter: Some(comma_count as u32), }; return Some(SignatureHelp { signatures: vec![signature_info], active_signature: Some(0), active_parameter: Some(comma_count as u32), }); } } } } // Fall back to hardcoded signatures for built-in functions self.get_builtin_signature(&func_name, comma_count) } fn extract_parameters_from_signature(&self, sig: &str) -> Vec { // Parse "fn name(a: Int, b: String): ReturnType" format let mut params = Vec::new(); if let Some(start) = sig.find('(') { if let Some(end) = sig.find(')') { let params_str = &sig[start + 1..end]; for param in params_str.split(',') { let param = param.trim(); if !param.is_empty() { params.push(ParameterInformation { label: lsp_types::ParameterLabel::Simple(param.to_string()), documentation: None, }); } } } } params } fn get_builtin_signature(&self, func_name: &str, active_param: usize) -> Option { let (sig, params): (&str, Vec<&str>) = match func_name { // List functions "map" => ("fn map(list: List, f: fn(A): B): List", vec!["list: List", "f: fn(A): B"]), "filter" => ("fn filter(list: List, f: fn(A): Bool): List", vec!["list: List", "f: fn(A): Bool"]), "fold" => ("fn fold(list: List, init: B, f: fn(B, A): B): B", vec!["list: List", "init: B", "f: fn(B, A): B"]), "head" => ("fn head(list: List): Option", vec!["list: List"]), "tail" => ("fn tail(list: List): Option>", vec!["list: List"]), "concat" => ("fn concat(a: List, b: List): List", vec!["a: List", "b: List"]), "length" => ("fn length(list: List): Int", vec!["list: List"]), "get" => ("fn get(list: List, index: Int): Option", vec!["list: List", "index: Int"]), // String functions "split" => ("fn split(s: String, sep: String): List", vec!["s: String", "sep: String"]), "join" => ("fn join(list: List, sep: String): String", vec!["list: List", "sep: String"]), "replace" => ("fn replace(s: String, from: String, to: String): String", vec!["s: String", "from: String", "to: String"]), "substring" => ("fn substring(s: String, start: Int, end: Int): String", vec!["s: String", "start: Int", "end: Int"]), "contains" => ("fn contains(s: String, sub: String): Bool", vec!["s: String", "sub: String"]), // Option functions "getOrElse" => ("fn getOrElse(opt: Option, default: A): A", vec!["opt: Option", "default: A"]), // Result functions "mapErr" => ("fn mapErr(result: Result, f: fn(E): E2): Result", vec!["result: Result", "f: fn(E): E2"]), _ => return None, }; let param_infos: Vec = params.iter() .map(|p| ParameterInformation { label: lsp_types::ParameterLabel::Simple(p.to_string()), documentation: None, }) .collect(); Some(SignatureHelp { signatures: vec![SignatureInformation { label: sig.to_string(), documentation: None, parameters: Some(param_infos), active_parameter: Some(active_param as u32), }], active_signature: Some(0), active_parameter: Some(active_param as u32), }) } fn handle_inlay_hints(&self, params: InlayHintParams) -> Option> { let uri = params.text_document.uri; let doc = self.documents.get(&uri)?; let source = &doc.text; // Parse the document to get AST let program = Parser::parse_source(source).ok()?; // Type-check to get inferred types let mut checker = TypeChecker::new(); let _ = checker.check_program(&program); let mut hints = Vec::new(); // Collect parameter names for known functions (from symbol table) let param_names = self.collect_function_params(&program); for decl in &program.declarations { match decl { crate::ast::Declaration::Let(l) => { // Show inferred type for let bindings without explicit type annotations if l.typ.is_none() { if let Some(inferred_type) = checker.get_inferred_type(&l.name.name) { let type_str = format!(": {}", inferred_type); let pos = offset_to_position(source, l.name.span.end); hints.push(InlayHint { position: pos, label: InlayHintLabel::String(type_str), kind: Some(InlayHintKind::TYPE), text_edits: None, tooltip: None, padding_left: Some(false), padding_right: Some(true), data: None, }); } } // Walk into the value expression for call-site parameter hints collect_call_site_hints(source, &l.value, ¶m_names, &mut hints); } crate::ast::Declaration::Function(f) => { // Walk into the function body for call-site parameter hints collect_call_site_hints(source, &f.body, ¶m_names, &mut hints); } _ => {} } } if hints.is_empty() { None } else { Some(hints) } } /// Collect parameter names for all functions defined in the program fn collect_function_params(&self, program: &crate::ast::Program) -> HashMap> { let mut params = HashMap::new(); for decl in &program.declarations { if let crate::ast::Declaration::Function(f) = decl { let names: Vec = f.params.iter() .map(|p| p.name.name.clone()) .collect(); params.insert(f.name.name.clone(), names); } } // Add builtin function parameter names params.insert("map".into(), vec!["list".into(), "f".into()]); params.insert("filter".into(), vec!["list".into(), "predicate".into()]); params.insert("fold".into(), vec!["list".into(), "init".into(), "f".into()]); params.insert("concat".into(), vec!["a".into(), "b".into()]); params.insert("range".into(), vec!["start".into(), "end".into()]); params.insert("get".into(), vec!["list".into(), "index".into()]); params.insert("take".into(), vec!["list".into(), "n".into()]); params.insert("drop".into(), vec!["list".into(), "n".into()]); params.insert("split".into(), vec!["s".into(), "delimiter".into()]); params.insert("join".into(), vec!["list".into(), "delimiter".into()]); params.insert("replace".into(), vec!["s".into(), "old".into(), "new".into()]); params.insert("substring".into(), vec!["s".into(), "start".into(), "end".into()]); params.insert("contains".into(), vec!["s".into(), "substr".into()]); params.insert("getOrElse".into(), vec!["opt".into(), "default".into()]); params } fn handle_formatting(&self, params: DocumentFormattingParams) -> Option> { let uri = params.text_document.uri; let doc = self.documents.get(&uri)?; let source = &doc.text; // Use the Lux formatter with default config let config = FormatConfig::default(); match format_source(source, &config) { Ok(formatted) => { if formatted == *source { // No changes needed return Some(vec![]); } // Replace entire document let lines: Vec<&str> = source.lines().collect(); let last_line = lines.len().saturating_sub(1); let last_col = lines.last().map(|l| l.len()).unwrap_or(0); Some(vec![TextEdit { range: Range { start: Position { line: 0, character: 0 }, end: Position { line: last_line as u32, character: last_col as u32, }, }, new_text: formatted, }]) } Err(_) => { // Formatting failed, return no edits None } } } } /// Convert byte offsets to LSP Position /// Format a function signature for hover display, wrapping long lines fn format_signature_for_hover(sig: &str) -> String { // If it fits in ~60 chars, keep it on one line if sig.len() <= 60 { return sig.to_string(); } // Try to break at parameter list for function signatures if let Some(paren_start) = sig.find('(') { if let Some(paren_end) = sig.rfind(')') { let prefix = &sig[..paren_start + 1]; let params = &sig[paren_start + 1..paren_end]; let suffix = &sig[paren_end..]; // Split parameters and format each on its own line let param_parts: Vec<&str> = params.split(", ").collect(); if param_parts.len() > 1 { let indent = " "; let formatted_params = param_parts.join(&format!(",\n{}", indent)); return format!("{}\n{}{}\n{}", prefix, indent, formatted_params, suffix); } } } sig.to_string() } /// Extract behavioral property documentation from a signature string fn extract_property_docs(sig: &str) -> String { let properties = [ ("is pure", "**pure** — no side effects, same output for same inputs"), ("is total", "**total** — always terminates, no exceptions"), ("is idempotent", "**idempotent** — `f(f(x)) == f(x)`"), ("is deterministic", "**deterministic** — no randomness or time dependence"), ("is commutative", "**commutative** — `f(a, b) == f(b, a)`"), ]; let mut found = Vec::new(); for (keyword, description) in &properties { if sig.contains(keyword) { found.push(*description); } } if found.is_empty() { String::new() } else { format!("\n\n{}", found.join(" \n")) } } /// Recursively collect parameter name hints at call sites fn collect_call_site_hints( source: &str, expr: &crate::ast::Expr, param_names: &HashMap>, hints: &mut Vec, ) { use crate::ast::Expr; match expr { Expr::Call { func, args, .. } => { // Get the function name for parameter lookup let func_name = match func.as_ref() { Expr::Var(ident) => Some(ident.name.clone()), // Module.method calls like List.map Expr::Field { object, field, .. } => { if let Expr::Var(_) = object.as_ref() { Some(field.name.clone()) } else { None } } _ => None, }; if let Some(name) = func_name { if let Some(names) = param_names.get(&name) { for (i, arg) in args.iter().enumerate() { if let Some(param_name) = names.get(i) { // Skip hint if the argument is already a variable with the same name if let Expr::Var(ident) = arg { if &ident.name == param_name { continue; } } // Skip hints for single-arg functions (obvious) if args.len() <= 1 { continue; } let pos = offset_to_position(source, arg.span().start); hints.push(InlayHint { position: pos, label: InlayHintLabel::String(format!("{}:", param_name)), kind: Some(InlayHintKind::PARAMETER), text_edits: None, tooltip: None, padding_left: Some(false), padding_right: Some(true), data: None, }); } } } } // Recurse into function expression and arguments collect_call_site_hints(source, func, param_names, hints); for arg in args { collect_call_site_hints(source, arg, param_names, hints); } } Expr::BinaryOp { left, right, .. } => { collect_call_site_hints(source, left, param_names, hints); collect_call_site_hints(source, right, param_names, hints); } Expr::UnaryOp { operand, .. } => { collect_call_site_hints(source, operand, param_names, hints); } Expr::If { condition, then_branch, else_branch, .. } => { collect_call_site_hints(source, condition, param_names, hints); collect_call_site_hints(source, then_branch, param_names, hints); collect_call_site_hints(source, else_branch, param_names, hints); } Expr::Let { value, body, .. } => { collect_call_site_hints(source, value, param_names, hints); collect_call_site_hints(source, body, param_names, hints); } Expr::Block { statements, result, .. } => { for stmt in statements { match stmt { crate::ast::Statement::Expr(e) => { collect_call_site_hints(source, e, param_names, hints); } crate::ast::Statement::Let { value, .. } => { collect_call_site_hints(source, value, param_names, hints); } } } collect_call_site_hints(source, result, param_names, hints); } Expr::Match { scrutinee, arms, .. } => { collect_call_site_hints(source, scrutinee, param_names, hints); for arm in arms { collect_call_site_hints(source, &arm.body, param_names, hints); } } Expr::Lambda { body, .. } => { collect_call_site_hints(source, body, param_names, hints); } Expr::Tuple { elements, .. } | Expr::List { elements, .. } => { for e in elements { collect_call_site_hints(source, e, param_names, hints); } } Expr::Record { spread, fields, .. } => { if let Some(spread_expr) = spread { collect_call_site_hints(source, spread_expr, param_names, hints); } for (_, e) in fields { collect_call_site_hints(source, e, param_names, hints); } } Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => { collect_call_site_hints(source, object, param_names, hints); } Expr::Run { expr, handlers, .. } => { collect_call_site_hints(source, expr, param_names, hints); for (_, handler_expr) in handlers { collect_call_site_hints(source, handler_expr, param_names, hints); } } Expr::Resume { value, .. } => { collect_call_site_hints(source, value, param_names, hints); } Expr::EffectOp { args, .. } => { for arg in args { collect_call_site_hints(source, arg, param_names, hints); } } Expr::Literal { .. } | Expr::Var(_) => {} } } 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") } } } /// Context for completion suggestions #[derive(PartialEq)] enum CompletionContext { /// After a dot with specific module (e.g., "List.", "Sql.") ModuleAccess(String), /// General context (keywords, types, etc.) General, } /// Create a simple completion item fn completion_item(label: &str, kind: CompletionItemKind, detail: &str) -> CompletionItem { CompletionItem { label: label.to_string(), kind: Some(kind), detail: Some(detail.to_string()), ..Default::default() } } /// Create a completion item with documentation fn completion_item_with_doc( label: &str, kind: CompletionItemKind, signature: &str, doc: &str, ) -> CompletionItem { CompletionItem { label: label.to_string(), kind: Some(kind), detail: Some(signature.to_string()), documentation: Some(lsp_types::Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: doc.to_string(), })), ..Default::default() } } /// Convert symbol kind to LSP symbol kind fn symbol_kind_to_lsp(kind: &SymbolKind) -> lsp_types::SymbolKind { match kind { SymbolKind::Function => lsp_types::SymbolKind::FUNCTION, SymbolKind::Variable => lsp_types::SymbolKind::VARIABLE, SymbolKind::Parameter => lsp_types::SymbolKind::VARIABLE, SymbolKind::Type => lsp_types::SymbolKind::CLASS, SymbolKind::TypeParameter => lsp_types::SymbolKind::TYPE_PARAMETER, SymbolKind::Variant => lsp_types::SymbolKind::ENUM_MEMBER, SymbolKind::Effect => lsp_types::SymbolKind::INTERFACE, SymbolKind::EffectOperation => lsp_types::SymbolKind::METHOD, SymbolKind::Field => lsp_types::SymbolKind::FIELD, SymbolKind::Module => lsp_types::SymbolKind::MODULE, } }