a70e5fbc30
语法: 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>
166 lines
4.8 KiB
Rust
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)
|
|
}
|