Files
lux/src/lsp.rs
Brandon Lucas 3d706cb32b feat: add record spread syntax { ...base, field: val }
Adds spread operator for records, allowing concise record updates:
  let p2 = { ...p, x: 5.0 }

Changes across the full pipeline:
- Lexer: new DotDotDot (...) token
- AST: optional spread field on Record variant
- Parser: detect ... at start of record expression
- Typechecker: merge spread record fields with explicit overrides
- Interpreter: evaluate spread, overlay explicit fields
- JS backend: emit native JS spread syntax
- C backend: copy spread into temp, assign overrides
- Formatter, linter, LSP, symbol table: propagate spread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:27 -05:00

1702 lines
76 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<SymbolTable>,
}
/// LSP Server for Lux
pub struct LspServer {
connection: Connection,
/// Document contents and symbol tables by URI
documents: HashMap<Url, DocumentCache>,
}
impl LspServer {
/// Run the LSP server
pub fn run() -> Result<(), Box<dyn Error + Sync + Send>> {
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<dyn Error + Sync + Send>> {
// 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<dyn Error + Sync + Send>> {
let req = match cast_request::<HoverRequest>(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::<Completion>(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::<GotoDefinition>(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::<References>(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::<DocumentSymbolRequest>(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::<Rename>(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::<SignatureHelpRequest>(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::<Formatting>(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::<InlayHintRequest>(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<dyn Error + Sync + Send>> {
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<dyn Error + Sync + Send>> {
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<Diagnostic> {
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<Hover> {
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<Hover> {
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<String>` — 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<Row>` — 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.01.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<String>` — 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<A> = 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<A, E> = 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<String> {
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<String> {
let chars: Vec<char> = 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<A>, f: A -> B): List<B>", "Transform each element in a list")),
"filter" => Some(("List.filter(list: List<A>, p: A -> Bool): List<A>", "Keep elements matching predicate")),
"fold" => Some(("List.fold(list: List<A>, init: B, f: (B, A) -> B): B", "Reduce list to single value")),
"reverse" => Some(("List.reverse(list: List<A>): List<A>", "Reverse a list")),
"concat" => Some(("List.concat(a: List<A>, b: List<A>): List<A>", "Concatenate two lists")),
"range" => Some(("List.range(start: Int, end: Int): List<Int>", "Create a list from start to end-1")),
"length" => Some(("List.length(list: List<A>): Int", "Get the length of a list")),
"head" => Some(("List.head(list: List<A>): Option<A>", "Get the first element")),
"tail" => Some(("List.tail(list: List<A>): List<A>", "Get all elements except the first")),
"isEmpty" => Some(("List.isEmpty(list: List<A>): Bool", "Check if list is empty")),
// Option/Result
"Some" => Some(("Some(value: A): Option<A>", "Wrap a value in Some")),
"None" => Some(("None: Option<A>", "The empty Option")),
"Ok" => Some(("Ok(value: A): Result<A, E>", "Successful result")),
"Err" => Some(("Err(error: E): Result<A, E>", "Error result")),
"isSome" => Some(("Option.isSome(opt: Option<A>): Bool", "Check if Option has a value")),
"isNone" => Some(("Option.isNone(opt: Option<A>): Bool", "Check if Option is empty")),
"getOrElse" => Some(("Option.getOrElse(opt: Option<A>, 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<A>", "Generic list/array type")),
"Option" => Some(("type Option<A> = Some(A) | None", "Optional value (Some or None)")),
"Result" => Some(("type Result<A, E> = 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<CompletionResponse> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<CompletionItem> {
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<GotoDefinitionResponse> {
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 <word>" 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 <word>" 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 <word>"
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<Vec<Location>> {
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<Location> = 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<DocumentSymbolResponse> {
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<SymbolInformation> = 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<String> {
let chars: Vec<char> = 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<WorkspaceEdit> {
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<TextEdit> = 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<SignatureHelp> {
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<char> = 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<ParameterInformation> {
// 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<SignatureHelp> {
let (sig, params): (&str, Vec<&str>) = match func_name {
// List functions
"map" => ("fn map<A, B>(list: List<A>, f: fn(A): B): List<B>", vec!["list: List<A>", "f: fn(A): B"]),
"filter" => ("fn filter<A>(list: List<A>, f: fn(A): Bool): List<A>", vec!["list: List<A>", "f: fn(A): Bool"]),
"fold" => ("fn fold<A, B>(list: List<A>, init: B, f: fn(B, A): B): B", vec!["list: List<A>", "init: B", "f: fn(B, A): B"]),
"head" => ("fn head<A>(list: List<A>): Option<A>", vec!["list: List<A>"]),
"tail" => ("fn tail<A>(list: List<A>): Option<List<A>>", vec!["list: List<A>"]),
"concat" => ("fn concat<A>(a: List<A>, b: List<A>): List<A>", vec!["a: List<A>", "b: List<A>"]),
"length" => ("fn length<A>(list: List<A>): Int", vec!["list: List<A>"]),
"get" => ("fn get<A>(list: List<A>, index: Int): Option<A>", vec!["list: List<A>", "index: Int"]),
// String functions
"split" => ("fn split(s: String, sep: String): List<String>", vec!["s: String", "sep: String"]),
"join" => ("fn join(list: List<String>, sep: String): String", vec!["list: List<String>", "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<A>(opt: Option<A>, default: A): A", vec!["opt: Option<A>", "default: A"]),
// Result functions
"mapErr" => ("fn mapErr<E, E2, T>(result: Result<T, E>, f: fn(E): E2): Result<T, E2>", vec!["result: Result<T, E>", "f: fn(E): E2"]),
_ => return None,
};
let param_infos: Vec<ParameterInformation> = 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<Vec<InlayHint>> {
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, &param_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, &param_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<String, Vec<String>> {
let mut params = HashMap::new();
for decl in &program.declarations {
if let crate::ast::Declaration::Function(f) = decl {
let names: Vec<String> = 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<Vec<TextEdit>> {
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<String, Vec<String>>,
hints: &mut Vec<InlayHint>,
) {
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<R>(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,
}
}