feat: rebuild website with full learning funnel
Website rebuilt from scratch based on analysis of 11 beloved language websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc, Rust, Go). New website structure: - Homepage with hero, playground, three pillars, install guide - Language Tour with interactive lessons (hello world, types, effects) - Examples cookbook with categorized sidebar - API documentation index - Installation guide (Nix and source) - Sleek/noble design (black/gold, serif typography) Also includes: - New stdlib/json.lux module for JSON serialization - Enhanced stdlib/http.lux with middleware and routing - New string functions (charAt, indexOf, lastIndexOf, repeat) - LSP improvements (rename, signature help, formatting) - Package manager transitive dependency resolution - Updated documentation for effects and stdlib - New showcase example (task_manager.lux) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
639
src/lsp.rs
639
src/lsp.rs
@@ -4,30 +4,46 @@
|
||||
//! - 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},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting},
|
||||
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,
|
||||
TextDocumentSyncKind, Url, ReferenceParams, Location, DocumentSymbolParams,
|
||||
DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit,
|
||||
SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation,
|
||||
SignatureHelpOptions, DocumentFormattingParams, TextDocumentIdentifier,
|
||||
};
|
||||
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 by URI
|
||||
documents: HashMap<Url, String>,
|
||||
/// Document contents and symbol tables by URI
|
||||
documents: HashMap<Url, DocumentCache>,
|
||||
}
|
||||
|
||||
impl LspServer {
|
||||
@@ -63,6 +79,15 @@ impl LspServer {
|
||||
..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)),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
@@ -116,7 +141,7 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<GotoDefinition>(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);
|
||||
@@ -126,6 +151,56 @@ impl LspServer {
|
||||
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,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -138,15 +213,16 @@ impl LspServer {
|
||||
let params: DidOpenTextDocumentParams = serde_json::from_value(not.params)?;
|
||||
let uri = params.text_document.uri;
|
||||
let text = params.text_document.text;
|
||||
self.documents.insert(uri.clone(), text.clone());
|
||||
self.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() {
|
||||
self.documents.insert(uri.clone(), change.text.clone());
|
||||
self.publish_diagnostics(uri, &change.text)?;
|
||||
let text = change.text.clone();
|
||||
self.update_document(uri.clone(), text.clone());
|
||||
self.publish_diagnostics(uri, &text)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -154,6 +230,18 @@ impl LspServer {
|
||||
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,
|
||||
@@ -214,7 +302,43 @@ impl LspServer {
|
||||
let uri = params.text_document_position_params.text_document.uri;
|
||||
let position = params.text_document_position_params.position;
|
||||
|
||||
let source = self.documents.get(&uri)?;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
let source = &doc.text;
|
||||
|
||||
// Try to get info from 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 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();
|
||||
|
||||
return Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n*{}*{}", signature, kind_str, doc_str),
|
||||
}),
|
||||
range: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to hardcoded info
|
||||
|
||||
// Extract the word at the cursor position
|
||||
let word = self.get_word_at_position(source, position)?;
|
||||
@@ -320,28 +444,49 @@ impl LspServer {
|
||||
let position = params.text_document_position.position;
|
||||
|
||||
// Check context to provide relevant completions
|
||||
let source = self.documents.get(&uri)?;
|
||||
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/method completions
|
||||
if trigger_context == CompletionContext::ModuleAccess {
|
||||
// Add List module functions
|
||||
items.extend(self.get_list_completions());
|
||||
// Add String module functions
|
||||
items.extend(self.get_string_completions());
|
||||
// Add Option/Result completions
|
||||
items.extend(self.get_option_result_completions());
|
||||
// Add Console functions
|
||||
items.extend(self.get_console_completions());
|
||||
// Add Math functions
|
||||
items.extend(self.get_math_completions());
|
||||
} else {
|
||||
// General completions (keywords + common functions)
|
||||
items.extend(self.get_keyword_completions());
|
||||
items.extend(self.get_builtin_completions());
|
||||
items.extend(self.get_type_completions());
|
||||
// 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))
|
||||
@@ -353,7 +498,11 @@ impl LspServer {
|
||||
if offset > 0 {
|
||||
let prev_char = source.chars().nth(offset - 1);
|
||||
if prev_char == Some('.') {
|
||||
return CompletionContext::ModuleAccess;
|
||||
// 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
|
||||
@@ -400,16 +549,26 @@ impl LspServer {
|
||||
|
||||
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"),
|
||||
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"),
|
||||
]
|
||||
}
|
||||
@@ -495,14 +654,410 @@ impl LspServer {
|
||||
]
|
||||
}
|
||||
|
||||
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,
|
||||
params: GotoDefinitionParams,
|
||||
) -> Option<GotoDefinitionResponse> {
|
||||
// A full implementation would find the definition location
|
||||
// of the symbol at the given position
|
||||
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_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
|
||||
@@ -555,8 +1110,8 @@ where
|
||||
/// Context for completion suggestions
|
||||
#[derive(PartialEq)]
|
||||
enum CompletionContext {
|
||||
/// After a dot (e.g., "List.")
|
||||
ModuleAccess,
|
||||
/// After a dot with specific module (e.g., "List.", "Sql.")
|
||||
ModuleAccess(String),
|
||||
/// General context (keywords, types, etc.)
|
||||
General,
|
||||
}
|
||||
@@ -589,3 +1144,19 @@ fn completion_item_with_doc(
|
||||
..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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user