Files
aster/aster-lsp/src/completion.rs
T
0264408 a70e5fbc30 feat: try-catch-finally 异常处理
语法: try { ... } catch (e) { ... } finally { ... }
catch 和 finally 都是可选的,但至少需要一个。throw expr 抛出任意值。

- Lexer: 新增 try, catch, finally, throw 关键字
- AST: Stmt::Try { body, catch_var, catch_body, finally_body } + Stmt::Throw(Expr)
- Parser: try/catch/finally/throw 语句解析,更新 synchronize()
- Opcode: Throw = 46,FunctionProto 新增 exception_handlers 表
- ExceptionHandler: { try_start, try_end, catch_ip, catch_slot, finally_ip }
- Compiler: compile_try 将 finally 在成功/异常两条路径各内联一次
- VM: unwind() 栈展开 + find_handler() 处理器搜索
- RuntimeError(除零、未定义变量等)在 try 块内自动转为可捕获异常
- LSP: 补全关键字列表追加

已知限制: return/break/continue 在 try-finally 内部不会先执行 finally

16 个新测试,全部 343 个测试通过。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 14:37:38 +08:00

166 lines
4.8 KiB
Rust

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<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)
}