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>
1702 lines
76 KiB
Rust
1702 lines
76 KiB
Rust
//! 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.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<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, ¶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<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,
|
||
}
|
||
}
|