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
+164
View File
@@ -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)
}