use aster_core::analysis::{self, SymbolKind}; use aster_core::lexer::Lexer; use aster_core::parser::Parser; use lsp_server::{Connection, Message, Request, Response}; use lsp_types::{ CompletionItem, CompletionItemKind, CompletionList, CompletionParams, Uri, }; const KEYWORDS: &[&str] = &[ "let", "const", "fn", "if", "else", "while", "for", "in", "break", "continue", "return", "true", "false", "nil", "try", "catch", "finally", "throw", ]; const BUILTINS: &[&str] = &[ "print", "input", "clock", "len", "typeof", "push", "pop", "split", "trim", "substring", "replace", "contains", "upper", "lower", "starts_with", "ends_with", "require", ]; pub fn handle_completion( documents: &std::collections::HashMap, connection: &Connection, req: Request, ) { let id = req.id; let params: CompletionParams = match serde_json::from_value(req.params) { Ok(p) => p, Err(e) => { eprintln!("Invalid completion params: {}", e); return; } }; let uri = params.text_document_position.text_document.uri; let position = params.text_document_position.position; let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or(""); let prefix = get_word_prefix(text, position); let mut items: Vec = Vec::new(); // Keywords for kw in KEYWORDS { if kw.starts_with(&prefix) { items.push(CompletionItem { label: kw.to_string(), kind: Some(CompletionItemKind::KEYWORD), detail: Some("keyword".into()), ..Default::default() }); } } // Builtins for b in BUILTINS { if b.starts_with(&prefix) { items.push(CompletionItem { label: b.to_string(), kind: Some(CompletionItemKind::FUNCTION), detail: Some("builtin".into()), ..Default::default() }); } } // User-defined symbols (from AST) let (tokens, _) = Lexer::new(text).tokenize(); let mut parser = Parser::new(tokens); let (stmts, _) = parser.parse(); let symbols = analysis::collect_symbols(&stmts); for sym in &symbols { if sym.name.starts_with(&prefix) { let (kind, detail) = match &sym.kind { SymbolKind::Variable => (CompletionItemKind::VARIABLE, None), SymbolKind::Function { params } => ( CompletionItemKind::FUNCTION, Some(format!("fn {}({})", sym.name, params.join(", "))), ), }; // Deduplicate: skip if already in the list if items.iter().any(|i| i.label == sym.name) { continue; } items.push(CompletionItem { label: sym.name.clone(), kind: Some(kind), detail, ..Default::default() }); } } let result = CompletionList { is_incomplete: false, items, }; let resp = Response::new_ok(id, result); let _ = connection.sender.send(Message::Response(resp)); } /// Extract the identifier prefix immediately before the cursor position. fn get_word_prefix(text: &str, position: lsp_types::Position) -> String { let line = match get_line(text, position.line as usize) { Some(l) => l, None => return String::new(), }; let col = position.character as usize; let prefix_bytes = if col > line.len() { line } else { &line[..col] }; // Walk backwards through the prefix to the start of an identifier let mut end = prefix_bytes.len(); while end > 0 { let ch = prefix_bytes.as_bytes()[end - 1] as char; if ch.is_alphanumeric() || ch == '_' { end -= 1; } else { break; } } prefix_bytes[end..].to_string() } /// Get the full word at a given position (for hover). pub fn get_word_at_position(text: &str, position: lsp_types::Position) -> String { let line = match get_line(text, position.line as usize) { Some(l) => l, None => return String::new(), }; let col = position.character as usize; let bytes = line.as_bytes(); // Find start of word let mut start = col.min(bytes.len()); while start > 0 { let ch = bytes[start - 1] as char; if ch.is_alphanumeric() || ch == '_' { start -= 1; } else { break; } } // Find end of word let mut end = col.min(bytes.len()); while end < bytes.len() { let ch = bytes[end] as char; if ch.is_alphanumeric() || ch == '_' { end += 1; } else { break; } } line[start..end].to_string() } fn get_line(text: &str, line_idx: usize) -> Option<&str> { text.lines().nth(line_idx) }