feat: IDE支持 — workspace拆分、LSP服务器、VS Code扩展
**Workspace 拆分** - aster-core: 纯库,zero-dependency,包含 lexer/parser/ast/interpreter/error/analysis - aster: REPL 二进制,薄封装 aster-core - aster-lsp: LSP 语言服务器 **VS Code 扩展 (vscode-ext/)** - TextMate 语法高亮 (.ast 文件) - 语言配置 (注释切换、括号配对、自动缩进) - LSP 客户端 (extension.js) **LSP 服务端功能** - Diagnostics: 实时显示 lex/parse 错误红色波浪线 - Completion: 关键字 + 内置函数 + 用户定义符号补全 - Hover: 悬停显示变量/函数信息 - Signature Help: 函数参数提示 - Goto Definition: Ctrl+Click 跳转到声明处 - Find References: 查找所有引用位置 - Rename: F2 重命名符号 **新增 analysis 模块** - collect_symbols: AST 遍历收集符号 - find_declaration/find_all_references: Token 扫描定位声明和引用
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "aster-lsp"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
aster-core = { path = "../aster-core" }
|
||||
lsp-server = "0.7"
|
||||
lsp-types = "0.97"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,164 @@
|
||||
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",
|
||||
];
|
||||
|
||||
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<Uri, String>,
|
||||
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<CompletionItem> = 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)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis;
|
||||
use aster_core::lexer::Lexer;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Location, Position, Range, Uri};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
pub fn handle_goto_definition(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: GotoDefinitionParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid goto def params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position_params.text_document.uri;
|
||||
let position = params.text_document_position_params.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<GotoDefinitionResponse>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
|
||||
if let Some((line, col)) = analysis::find_declaration(&tokens, &word) {
|
||||
let l = (line as u32).saturating_sub(1);
|
||||
let c = (col as u32).saturating_sub(1);
|
||||
let location = Location {
|
||||
uri: uri.clone(),
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: l,
|
||||
character: c,
|
||||
},
|
||||
end: Position {
|
||||
line: l,
|
||||
character: c + word.len() as u32,
|
||||
},
|
||||
},
|
||||
};
|
||||
let response: GotoDefinitionResponse = GotoDefinitionResponse::Scalar(location);
|
||||
let resp = lsp_server::Response::new_ok(id, response);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
} else {
|
||||
let resp = lsp_server::Response::new_ok::<Option<GotoDefinitionResponse>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis::{self, SymbolKind};
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{HoverContents, MarkupContent, MarkupKind, TextDocumentPositionParams, Uri};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
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_hover(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: TextDocumentPositionParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid hover params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document.uri;
|
||||
let position = params.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check builtins
|
||||
if BUILTINS.contains(&word.as_str()) {
|
||||
let contents = HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("**builtin** `{}`", word),
|
||||
});
|
||||
let hover = lsp_types::Hover {
|
||||
contents,
|
||||
range: None,
|
||||
};
|
||||
let resp = lsp_server::Response::new_ok(id, hover);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check keywords
|
||||
const KEYWORDS: &[&str] = &[
|
||||
"let", "const", "fn", "if", "else", "while", "for", "in",
|
||||
"break", "continue", "return", "true", "false", "nil",
|
||||
];
|
||||
if KEYWORDS.contains(&word.as_str()) {
|
||||
let contents = HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("**keyword** `{}`", word),
|
||||
});
|
||||
let hover = lsp_types::Hover { contents, range: None };
|
||||
let resp = lsp_server::Response::new_ok(id, hover);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check user-defined symbols
|
||||
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 == word {
|
||||
let markdown = match &sym.kind {
|
||||
SymbolKind::Variable => {
|
||||
format!("**variable** `{}`", sym.name)
|
||||
}
|
||||
SymbolKind::Function { params } => {
|
||||
format!("**fn** `{}({})`", sym.name, params.join(", "))
|
||||
}
|
||||
};
|
||||
let contents = HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: markdown,
|
||||
});
|
||||
let hover = lsp_types::Hover { contents, range: None };
|
||||
let resp = lsp_server::Response::new_ok(id, hover);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Not found — return null
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod completion;
|
||||
mod goto_def;
|
||||
mod hover;
|
||||
mod references;
|
||||
mod rename;
|
||||
mod server;
|
||||
mod signature;
|
||||
|
||||
fn main() {
|
||||
server::run();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis;
|
||||
use aster_core::lexer::Lexer;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{Location, Position, Range, ReferenceParams, Uri};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
pub fn handle_references(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: ReferenceParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid references 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 word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<Vec<Location>>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let positions = analysis::find_all_references(&tokens, &word);
|
||||
|
||||
let locations: Vec<Location> = positions
|
||||
.into_iter()
|
||||
.map(|(line, col)| {
|
||||
let l = (line as u32).saturating_sub(1);
|
||||
let c = (col as u32).saturating_sub(1);
|
||||
Location {
|
||||
uri: uri.clone(),
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: l,
|
||||
character: c,
|
||||
},
|
||||
end: Position {
|
||||
line: l,
|
||||
character: c + word.len() as u32,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resp = lsp_server::Response::new_ok(id, locations);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis;
|
||||
use aster_core::lexer::Lexer;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{Position, Range, RenameParams, TextEdit, Uri, WorkspaceEdit};
|
||||
|
||||
use crate::completion;
|
||||
|
||||
pub fn handle_rename(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: RenameParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid rename params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
let new_name = params.new_name;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
let word = completion::get_word_at_position(text, position);
|
||||
|
||||
if word.is_empty() || word == new_name {
|
||||
let resp = lsp_server::Response::new_ok::<Option<WorkspaceEdit>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
let (tokens, _) = Lexer::new(text).tokenize();
|
||||
let positions = analysis::find_all_references(&tokens, &word);
|
||||
|
||||
let edits: Vec<TextEdit> = positions
|
||||
.into_iter()
|
||||
.map(|(line, col)| {
|
||||
let l = (line as u32).saturating_sub(1);
|
||||
let c = (col as u32).saturating_sub(1);
|
||||
TextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: l,
|
||||
character: c,
|
||||
},
|
||||
end: Position {
|
||||
line: l,
|
||||
character: c + word.len() as u32,
|
||||
},
|
||||
},
|
||||
new_text: new_name.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut changes = HashMap::new();
|
||||
changes.insert(uri, edits);
|
||||
|
||||
let edit = WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let resp = lsp_server::Response::new_ok(id, edit);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use aster_core::error::RuntimeError;
|
||||
|
||||
use lsp_server::{Connection, Message, Notification};
|
||||
use lsp_types::{
|
||||
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentParams, DidOpenTextDocumentParams, InitializeParams,
|
||||
Position, PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, Uri,
|
||||
};
|
||||
|
||||
use crate::completion;
|
||||
use crate::goto_def;
|
||||
use crate::hover;
|
||||
use crate::references;
|
||||
use crate::rename;
|
||||
use crate::signature;
|
||||
|
||||
pub fn run() {
|
||||
eprintln!("Aster LSP server starting...");
|
||||
|
||||
let (connection, io_threads) = Connection::stdio();
|
||||
|
||||
let capabilities = ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::FULL,
|
||||
)),
|
||||
completion_provider: Some(lsp_types::CompletionOptions::default()),
|
||||
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||
signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
|
||||
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
references_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
rename_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
..ServerCapabilities::default()
|
||||
};
|
||||
|
||||
let init_result = connection
|
||||
.initialize(serde_json::to_value(&capabilities).unwrap())
|
||||
.unwrap();
|
||||
let _init_params: InitializeParams = serde_json::from_value(init_result).unwrap();
|
||||
|
||||
eprintln!("Aster LSP initialized successfully");
|
||||
|
||||
let mut documents: HashMap<Uri, String> = HashMap::new();
|
||||
|
||||
for msg in &connection.receiver {
|
||||
match msg {
|
||||
Message::Request(req) => {
|
||||
if connection.handle_shutdown(&req).unwrap() {
|
||||
break;
|
||||
}
|
||||
handle_request(&documents, &connection, req);
|
||||
}
|
||||
Message::Notification(notif) => {
|
||||
handle_notification(&mut documents, &connection, notif);
|
||||
}
|
||||
Message::Response(_resp) => {}
|
||||
}
|
||||
}
|
||||
|
||||
io_threads.join().unwrap();
|
||||
eprintln!("Aster LSP server stopped.");
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: lsp_server::Request,
|
||||
) {
|
||||
match req.method.as_str() {
|
||||
"textDocument/completion" => {
|
||||
completion::handle_completion(documents, connection, req);
|
||||
}
|
||||
"textDocument/hover" => {
|
||||
hover::handle_hover(documents, connection, req);
|
||||
}
|
||||
"textDocument/signatureHelp" => {
|
||||
signature::handle_signature_help(documents, connection, req);
|
||||
}
|
||||
"textDocument/definition" => {
|
||||
goto_def::handle_goto_definition(documents, connection, req);
|
||||
}
|
||||
"textDocument/references" => {
|
||||
references::handle_references(documents, connection, req);
|
||||
}
|
||||
"textDocument/rename" => {
|
||||
rename::handle_rename(documents, connection, req);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unhandled request: {}", req.method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_notification(
|
||||
documents: &mut HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
notif: Notification,
|
||||
) {
|
||||
match notif.method.as_str() {
|
||||
"textDocument/didOpen" => {
|
||||
let params: DidOpenTextDocumentParams =
|
||||
serde_json::from_value(notif.params).unwrap();
|
||||
let uri = params.text_document.uri.clone();
|
||||
let text = params.text_document.text.clone();
|
||||
documents.insert(uri.clone(), text.clone());
|
||||
publish_diagnostics(connection, &uri, &text);
|
||||
}
|
||||
"textDocument/didChange" => {
|
||||
let params: DidChangeTextDocumentParams =
|
||||
serde_json::from_value(notif.params).unwrap();
|
||||
let uri = params.text_document.uri.clone();
|
||||
if let Some(change) = params.content_changes.into_iter().last() {
|
||||
documents.insert(uri.clone(), change.text.clone());
|
||||
publish_diagnostics(connection, &uri, &change.text);
|
||||
}
|
||||
}
|
||||
"textDocument/didClose" => {
|
||||
let params: DidCloseTextDocumentParams =
|
||||
serde_json::from_value(notif.params).unwrap();
|
||||
documents.remove(¶ms.text_document.uri);
|
||||
let clear = PublishDiagnosticsParams {
|
||||
uri: params.text_document.uri.clone(),
|
||||
diagnostics: vec![],
|
||||
version: None,
|
||||
};
|
||||
send_notification(connection, "textDocument/publishDiagnostics", clear);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_diagnostics(connection: &Connection, uri: &Uri, text: &str) {
|
||||
let (tokens, lex_errors) = Lexer::new(text).tokenize();
|
||||
let mut errors = lex_errors;
|
||||
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (_stmts, parse_errors) = parser.parse();
|
||||
errors.extend(parse_errors);
|
||||
|
||||
let diagnostics: Vec<Diagnostic> = errors
|
||||
.iter()
|
||||
.filter_map(error_to_diagnostic)
|
||||
.collect();
|
||||
|
||||
let params = PublishDiagnosticsParams {
|
||||
uri: uri.clone(),
|
||||
diagnostics,
|
||||
version: None,
|
||||
};
|
||||
|
||||
send_notification(connection, "textDocument/publishDiagnostics", params);
|
||||
}
|
||||
|
||||
fn error_to_diagnostic(error: &RuntimeError) -> Option<Diagnostic> {
|
||||
match error {
|
||||
RuntimeError::LexError { message, line, column } => {
|
||||
let l = (*line as u32).saturating_sub(1);
|
||||
let c = (*column as u32).saturating_sub(1);
|
||||
Some(Diagnostic {
|
||||
range: Range {
|
||||
start: Position { line: l, character: c },
|
||||
end: Position { line: l, character: c.saturating_add(1) },
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
message: format!("[Lex] {}", message),
|
||||
..Diagnostic::default()
|
||||
})
|
||||
}
|
||||
RuntimeError::ParseError { message, token } => {
|
||||
let l = (token.line as u32).saturating_sub(1);
|
||||
let c = (token.column as u32).saturating_sub(1);
|
||||
Some(Diagnostic {
|
||||
range: Range {
|
||||
start: Position { line: l, character: c },
|
||||
end: Position { line: l, character: c.saturating_add(1) },
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
message: format!("[Parse] {}", message),
|
||||
..Diagnostic::default()
|
||||
})
|
||||
}
|
||||
RuntimeError::RuntimeError { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_notification(connection: &Connection, method: &str, params: impl serde::Serialize) {
|
||||
let notification = Notification::new(method.to_string(), serde_json::to_value(params).unwrap());
|
||||
let _ = connection.sender.send(Message::Notification(notification));
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aster_core::analysis::{self, SymbolKind};
|
||||
use aster_core::lexer::Lexer;
|
||||
use aster_core::parser::Parser;
|
||||
use lsp_server::{Connection, Message, Request};
|
||||
use lsp_types::{
|
||||
ParameterInformation, SignatureHelp, SignatureHelpParams,
|
||||
SignatureInformation, Uri,
|
||||
};
|
||||
|
||||
pub fn handle_signature_help(
|
||||
documents: &HashMap<Uri, String>,
|
||||
connection: &Connection,
|
||||
req: Request,
|
||||
) {
|
||||
let id = req.id;
|
||||
let params: SignatureHelpParams = match serde_json::from_value(req.params) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Invalid signature help params: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let uri = params.text_document_position_params.text_document.uri;
|
||||
let position = params.text_document_position_params.position;
|
||||
|
||||
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
|
||||
|
||||
// Find the function being called at cursor
|
||||
let (func_name, arg_index) = find_call_at_position(text, position);
|
||||
|
||||
if func_name.is_empty() {
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Look up the function in user symbols
|
||||
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 == func_name {
|
||||
if let SymbolKind::Function { params } = &sym.kind {
|
||||
let param_labels: Vec<String> = params.iter().map(|p| p.clone()).collect();
|
||||
let sig = SignatureInformation {
|
||||
label: format!("{}({})", func_name, param_labels.join(", ")),
|
||||
documentation: None,
|
||||
parameters: Some(
|
||||
param_labels
|
||||
.iter()
|
||||
.map(|p| ParameterInformation {
|
||||
label: lsp_types::ParameterLabel::Simple(p.clone()),
|
||||
documentation: None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
active_parameter: None,
|
||||
};
|
||||
let help = SignatureHelp {
|
||||
signatures: vec![sig],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(arg_index.min(param_labels.len().saturating_sub(1)) as u32),
|
||||
};
|
||||
let resp = lsp_server::Response::new_ok(id, help);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resp = lsp_server::Response::new_ok::<Option<()>>(id, None);
|
||||
let _ = connection.sender.send(Message::Response(resp));
|
||||
}
|
||||
|
||||
/// Naive text-based detection of a function call at cursor position.
|
||||
/// Returns (function_name, argument_index).
|
||||
fn find_call_at_position(text: &str, position: lsp_types::Position) -> (String, usize) {
|
||||
let line = match text.lines().nth(position.line as usize) {
|
||||
Some(l) => l,
|
||||
None => return (String::new(), 0),
|
||||
};
|
||||
|
||||
let col = position.character as usize;
|
||||
let before_cursor = if col > line.len() { line } else { &line[..col] };
|
||||
|
||||
// Walk backwards from cursor counting parentheses depth
|
||||
let mut depth = 0;
|
||||
|
||||
let bytes = before_cursor.as_bytes();
|
||||
let mut i = bytes.len();
|
||||
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
let ch = bytes[i] as char;
|
||||
match ch {
|
||||
')' => depth += 1,
|
||||
'(' => {
|
||||
if depth > 0 {
|
||||
depth -= 1;
|
||||
} else {
|
||||
// Found the opening paren of our call
|
||||
// Count commas between here and cursor to get arg index
|
||||
let args_slice = &before_cursor[i + 1..];
|
||||
let arg_index = args_slice.bytes().filter(|&b| b == b',').count();
|
||||
|
||||
// Find the function name before '('
|
||||
let before_paren = before_cursor[..i].trim_end();
|
||||
return (extract_last_identifier(before_paren), arg_index);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(String::new(), 0)
|
||||
}
|
||||
|
||||
fn extract_last_identifier(s: &str) -> String {
|
||||
let bytes = s.as_bytes();
|
||||
let mut end = bytes.len();
|
||||
while end > 0 {
|
||||
let ch = bytes[end - 1] as char;
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '.' {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If there's a dot, take what's after it (method call)
|
||||
let ident = &s[end..];
|
||||
if let Some(dot_pos) = ident.rfind('.') {
|
||||
ident[dot_pos + 1..].to_string()
|
||||
} else {
|
||||
ident.to_string()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user