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:
2026-06-25 00:28:13 +08:00
parent 99c9e9c9bd
commit bdffe9e3c5
65 changed files with 1709 additions and 47 deletions
+193
View File
@@ -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(&params.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));
}