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:
2026-02-16 23:05:35 -05:00
parent 5a853702d1
commit 7e76acab18
44 changed files with 12468 additions and 3354 deletions

View File

@@ -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,
}
}