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
+1
View File
@@ -1 +1,2 @@
/target /target
**/node_modules
+46 -9
View File
@@ -5,20 +5,57 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build & Run ## Build & Run
```bash ```bash
# Build # Build entire workspace
cargo build cargo build
# Run REPL (interactive) # Run REPL (interactive)
cargo run cargo run --bin aster
# Execute a script file # Execute a script file
cargo run -- examples/script.ast cargo run --bin aster -- aster-core/examples/script.ast
# Run tests (none yet; the project has no test/ directory) # Run tests
cargo test cargo test
``` ```
This is a pure-Rust project with no external dependencies (`edition = "2024"`). This is a Rust workspace. `aster-core` is a pure-Rust library with no external dependencies (`edition = "2024"`).
## Project structure
```
aster/
├── Cargo.toml # workspace root (aster-core, aster, aster-lsp)
├── aster-core/ # core library — lexer, parser, AST, interpreter
├── aster/ # REPL binary (thin wrapper around aster-core)
├── aster-lsp/ # LSP server (diagnostics, etc.)
└── vscode-ext/ # VS Code extension (syntax highlighting + LSP client)
```
## VS Code Extension
The `vscode-ext/` directory contains a VS Code extension that provides syntax highlighting and language server support for `.ast` files.
### Install (development)
```bash
# 1. Build the LSP server
cargo build --bin aster-lsp
# 2. Install extension npm dependencies
npm --prefix vscode-ext install
# 3. Register the extension in VS Code
# Windows: create junction
powershell -Command "New-Item -ItemType Junction -Path '$env:USERPROFILE\.vscode\extensions\aster-lang' -Target '$PWD\vscode-ext' -Force"
# macOS / Linux: symlink
ln -s "$PWD/vscode-ext" "$HOME/.vscode/extensions/aster-lang"
```
After installation, reload VS Code (`Ctrl+Shift+P` → "Developer: Reload Window"). Open any `.ast` file — syntax errors will appear as red squiggly underlines.
**Note:** The LSP server binary must be built (`cargo build --bin aster-lsp`) before the extension will work. After making changes to `aster-lsp`, rebuild and reload the VS Code window.
## Architecture ## Architecture
@@ -28,20 +65,20 @@ This is a pure-Rust project with no external dependencies (`edition = "2024"`).
Source text → Lexer → Tokens → Parser → AST → Interpreter → Output Source text → Lexer → Tokens → Parser → AST → Interpreter → Output
``` ```
### Module map ### Module map (`aster-core/src/`)
| Module | Responsibility | | Module | Responsibility |
|---|---| |---|---|
| `lexer/` | Tokenizer. `Lexer::tokenize()` takes `&str` and returns `(Vec<Token>, Vec<RuntimeError>)`. Tokens carry `line`/`column` for error reporting. Supports `//` single-line comments. | | `lexer/` | Tokenizer. `Lexer::tokenize()` takes `&str` and returns `(Vec<Token>, Vec<RuntimeError>)`. Tokens carry `line`/`column` for error reporting. Supports `//` single-line and `/* */` block comments. |
| `parser/` | Recursive-descent parser (Pratt-style for expressions). `Parser::parse()` returns `(Vec<Stmt>, Vec<RuntimeError>)`. Uses `synchronize()` for error recovery — skips to the next statement boundary on parse failure. | | `parser/` | Recursive-descent parser (Pratt-style for expressions). `Parser::parse()` returns `(Vec<Stmt>, Vec<RuntimeError>)`. Uses `synchronize()` for error recovery — skips to the next statement boundary on parse failure. |
| `ast/` | AST node definitions. `Expr` (in `expr.rs`) covers literals, variables, assignment, property access/indexing, unary/binary/logical ops, calls, lambdas, and object/array literals. `Stmt` (in `stmt.rs`) covers let, expression stmts, blocks, if/while/for, functions, return/break/continue. | | `ast/` | AST node definitions. `Expr` (in `expr.rs`) covers literals, variables, assignment, property access/indexing, unary/binary/logical ops, calls, lambdas, and object/array literals. `Stmt` (in `stmt.rs`) covers let, expression stmts, blocks, if/while/for, functions, return/break/continue. |
| `interpreter/` | Tree-walking evaluator. `Interpreter` holds an `env: Rc<RefCell<Env>>`. `Env` is a linked-list of scopes for lexical nesting. The `Signal` enum propagates `Return`/`Break`/`Continue` through the call stack. `builtins/` registers native functions (`print`, `input`, `clock`) organized as `io` and `os` modules (also available as global functions). | | `interpreter/` | Tree-walking evaluator. `Interpreter` holds an `env: Rc<RefCell<Env>>`. `Env` is a linked-list of scopes for lexical nesting. The `Signal` enum propagates `Return`/`Break`/`Continue` through the call stack. `builtins/` registers native functions (`print`, `input`, `clock`) organized as `io` and `os` modules (also available as global functions). |
| `error/` | Three error variants: `LexError` (line+column), `ParseError` (token), `RuntimeError` (optional token). All implement `Display` and `std::error::Error`. | | `error/` | Three error variants: `LexError` (line+column), `ParseError` (token), `RuntimeError` (optional token). All implement `Display` and `std::error::Error`. |
| `main.rs` | Two modes: `run_file` (parse-then-execute, exits with code 65 on lex/parse errors, 70 on runtime errors) and `run_repl` (per-line evaluation with `:exit`/`:reset` commands). | | `lib.rs` | Crate root — declares public modules, re-exports key types, provides `run_file()` and `run_repl()` convenience functions. |
### Key design decisions ### Key design decisions
- **No external crate dependencies.** Everything is built on Rust's stdlib. - **`aster-core` has no external crate dependencies.** Everything is built on Rust's stdlib. `aster-lsp` depends on `lsp-server` + `lsp-types`.
- **Rc<RefCell<>> for shared ownership.** `Env` chains, `Value::Object`, `Value::Array`, and `Value::Function` all use `Rc<RefCell<>>` for interior mutability — matching the dynamic, garbage-collected feel without a GC. - **Rc<RefCell<>> for shared ownership.** `Env` chains, `Value::Object`, `Value::Array`, and `Value::Function` all use `Rc<RefCell<>>` for interior mutability — matching the dynamic, garbage-collected feel without a GC.
- **Error-tolerant parsing.** Both lexer and parser collect errors into a `Vec` rather than failing on the first error, so multiple diagnostics can be reported per run. - **Error-tolerant parsing.** Both lexer and parser collect errors into a `Vec` rather than failing on the first error, so multiple diagnostics can be reported per run.
- **Closures capture by environment.** `Function` structs store the `Env` at definition time; lambda expressions (`fn(params) { body }`) work the same way. - **Closures capture by environment.** `Function` structs store the `Env` at definition time; lambda expressions (`fn(params) { body }`) work the same way.
Generated
+187
View File
@@ -5,3 +5,190 @@ version = 4
[[package]] [[package]]
name = "aster" name = "aster"
version = "0.1.0" version = "0.1.0"
dependencies = [
"aster-core",
]
[[package]]
name = "aster-core"
version = "0.1.0"
[[package]]
name = "aster-lsp"
version = "0.1.0"
dependencies = [
"aster-core",
"lsp-server",
"lsp-types",
"serde",
"serde_json",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lsp-server"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6ada348dbc2703cbe7637b2dda05cff84d3da2819c24abcb305dd613e0ba2e"
dependencies = [
"crossbeam-channel",
"log",
"serde",
"serde_derive",
"serde_json",
]
[[package]]
name = "lsp-types"
version = "0.97.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
dependencies = [
"bitflags",
"fluent-uri",
"serde",
"serde_json",
"serde_repr",
]
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+3 -6
View File
@@ -1,6 +1,3 @@
[package] [workspace]
name = "aster" members = ["aster-core", "aster", "aster-lsp"]
version = "0.1.0" resolver = "2"
edition = "2024"
[dependencies]
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "aster-core"
version = "0.1.0"
edition = "2024"
[dependencies]
+244
View File
@@ -0,0 +1,244 @@
use crate::ast::{Expr, Stmt};
use crate::lexer::{Token, TokenKind};
/// A symbol collected from walking the AST.
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub kind: SymbolKind,
}
#[derive(Debug, Clone)]
pub enum SymbolKind {
Variable,
Function { params: Vec<String> },
}
/// Collect all declared symbols from the given statements.
/// Walks into nested scopes but does NOT perform scope-aware filtering —
/// that's the caller's job (e.g. the LSP completion handler).
pub fn collect_symbols(stmts: &[Stmt]) -> Vec<Symbol> {
let mut symbols = Vec::new();
collect_stmts(stmts, &mut symbols);
symbols
}
fn collect_stmts(stmts: &[Stmt], symbols: &mut Vec<Symbol>) {
for stmt in stmts {
collect_stmt(stmt, symbols);
}
}
fn collect_stmt(stmt: &Stmt, symbols: &mut Vec<Symbol>) {
match stmt {
Stmt::Let { name, initializer, .. } => {
symbols.push(Symbol {
name: name.clone(),
kind: SymbolKind::Variable,
});
collect_expr(initializer, symbols);
}
Stmt::Function { name, params, body } => {
symbols.push(Symbol {
name: name.clone(),
kind: SymbolKind::Function {
params: params.clone(),
},
});
for p in params {
symbols.push(Symbol {
name: p.clone(),
kind: SymbolKind::Variable,
});
}
collect_stmts(body, symbols);
}
Stmt::Block(body) => collect_stmts(body, symbols),
Stmt::If {
condition,
then_branch,
else_branch,
} => {
collect_expr(condition, symbols);
collect_stmt(then_branch, symbols);
if let Some(else_b) = else_branch {
collect_stmt(else_b, symbols);
}
}
Stmt::While { condition, body } => {
collect_expr(condition, symbols);
collect_stmt(body, symbols);
}
Stmt::For {
initializer,
condition,
step,
body,
} => {
if let Some(init) = initializer {
collect_stmt(init, symbols);
}
if let Some(cond) = condition {
collect_expr(cond, symbols);
}
if let Some(step_expr) = step {
collect_expr(step_expr, symbols);
}
collect_stmt(body, symbols);
}
Stmt::ForIn {
var_name,
iterable,
body,
} => {
symbols.push(Symbol {
name: var_name.clone(),
kind: SymbolKind::Variable,
});
collect_expr(iterable, symbols);
collect_stmt(body, symbols);
}
Stmt::ExprStmt(expr) | Stmt::Return(Some(expr)) => {
collect_expr(expr, symbols);
}
Stmt::Return(None) | Stmt::Break | Stmt::Continue => {}
}
}
fn collect_expr(expr: &Expr, symbols: &mut Vec<Symbol>) {
match expr {
Expr::Literal(_) | Expr::Variable(_) => {}
Expr::Assign { value, .. } => collect_expr(value, symbols),
Expr::Get { object, .. } => collect_expr(object, symbols),
Expr::Set { object, value, .. } => {
collect_expr(object, symbols);
collect_expr(value, symbols);
}
Expr::ObjectLiteral { properties } => {
for (_, val) in properties {
collect_expr(val, symbols);
}
}
Expr::ArrayLiteral { elements } => {
for elem in elements {
collect_expr(elem, symbols);
}
}
Expr::IndexGet { array, index } => {
collect_expr(array, symbols);
collect_expr(index, symbols);
}
Expr::IndexSet {
array,
index,
value,
..
} => {
collect_expr(array, symbols);
collect_expr(index, symbols);
collect_expr(value, symbols);
}
Expr::Unary { right, .. } => collect_expr(right, symbols),
Expr::Binary { left, right, .. } | Expr::Logical { left, right, .. } => {
collect_expr(left, symbols);
collect_expr(right, symbols);
}
Expr::Ternary {
condition,
then_branch,
else_branch,
} => {
collect_expr(condition, symbols);
collect_expr(then_branch, symbols);
collect_expr(else_branch, symbols);
}
Expr::Call {
callee,
arguments,
} => {
collect_expr(callee, symbols);
for arg in arguments {
collect_expr(arg, symbols);
}
}
Expr::Lambda { params, body } => {
for p in params {
symbols.push(Symbol {
name: p.clone(),
kind: SymbolKind::Variable,
});
}
collect_stmts(body, symbols);
}
}
}
// ── Token-based position lookups ──────────────────────────────────
/// Scan tokens for the declaration site of `name`.
/// Recognises `let name`, `const name`, `fn name`, and `for (name in …)`.
/// Returns 1-based (line, column).
pub fn find_declaration(tokens: &[Token], name: &str) -> Option<(usize, usize)> {
let len = tokens.len();
let mut i = 0;
while i < len {
match &tokens[i].kind {
TokenKind::Let | TokenKind::Const => {
if let Some(tok) = tokens.get(i + 1) {
if let TokenKind::Identifier(n) = &tok.kind {
if n == name {
return Some((tok.line, tok.column));
}
}
}
}
TokenKind::Fn => {
// fn name( ... or fn name { ...
if let Some(tok) = tokens.get(i + 1) {
if let TokenKind::Identifier(n) = &tok.kind {
if n == name {
return Some((tok.line, tok.column));
}
}
}
}
TokenKind::For => {
// for ( name in ... )
// Skip past LeftParen
let mut j = i + 1;
if j < len && tokens[j].kind == TokenKind::LeftParen {
j += 1;
}
if j < len {
if let TokenKind::Identifier(n) = &tokens[j].kind {
if n == name {
return Some((tokens[j].line, tokens[j].column));
}
}
}
}
_ => {}
}
i += 1;
}
None
}
/// Scan tokens for every occurrence of `Identifier(name)`.
/// Returns 1-based (line, column) for each match.
pub fn find_all_references(tokens: &[Token], name: &str) -> Vec<(usize, usize)> {
tokens
.iter()
.filter_map(|tok| {
if let TokenKind::Identifier(n) = &tok.kind {
if n == name {
Some((tok.line, tok.column))
} else {
None
}
} else {
None
}
})
.collect()
}
+10 -32
View File
@@ -1,26 +1,26 @@
mod lexer; pub mod lexer;
mod ast; pub mod ast;
mod parser; pub mod parser;
mod interpreter; pub mod interpreter;
mod error; pub mod error;
pub mod analysis;
use lexer::Lexer; use lexer::Lexer;
use parser::Parser; use parser::Parser;
use interpreter::Interpreter; use interpreter::Interpreter;
use error::RuntimeError; use error::RuntimeError;
use std::env;
use std::fs;
use std::io::Write; use std::io::Write;
use std::io::stdin; use std::io::stdin;
use std::io::stdout; use std::io::stdout;
fn print_errors(errors: &[RuntimeError]) { pub fn print_errors(errors: &[RuntimeError]) {
for err in errors { for err in errors {
eprintln!("{}", err); eprintln!("{}", err);
} }
} }
fn run_file(filename: &str, src: String) { pub fn run_file(filename: &str, src: String) {
let (tokens, lex_errors) = Lexer::new(&src).tokenize(); let (tokens, lex_errors) = Lexer::new(&src).tokenize();
if !lex_errors.is_empty() { if !lex_errors.is_empty() {
print_errors(&lex_errors); print_errors(&lex_errors);
@@ -46,7 +46,7 @@ fn run_file(filename: &str, src: String) {
} }
} }
fn run_repl() { pub fn run_repl() {
println!("Welcome to Aster REPL!"); println!("Welcome to Aster REPL!");
println!("Type ':exit' to quit."); println!("Type ':exit' to quit.");
println!("Type ':reset' to reset the interpreter state."); println!("Type ':reset' to reset the interpreter state.");
@@ -100,25 +100,3 @@ fn run_repl() {
} }
} }
} }
fn main() {
let args: Vec<String> = env::args().collect();
match args.len() {
1 => run_repl(),
2 => {
let filename = &args[1];
match fs::read_to_string(filename) {
Ok(src) => run_file(filename, src),
Err(e) => {
eprintln!("Error reading file '{}': {}", filename, e);
std::process::exit(1);
}
}
}
_ => {
eprintln!("Usage: aster [script]");
std::process::exit(1);
}
}
}
+11
View File
@@ -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"
+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)
}
+61
View File
@@ -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));
}
}
+104
View File
@@ -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));
}
+11
View File
@@ -0,0 +1,11 @@
mod completion;
mod goto_def;
mod hover;
mod references;
mod rename;
mod server;
mod signature;
fn main() {
server::run();
}
+62
View File
@@ -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));
}
+71
View File
@@ -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));
}
+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));
}
+141
View File
@@ -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()
}
}
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "aster"
version = "0.1.0"
edition = "2024"
[dependencies]
aster-core = { path = "../aster-core" }
+24
View File
@@ -0,0 +1,24 @@
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
match args.len() {
1 => aster_core::run_repl(),
2 => {
let filename = &args[1];
match fs::read_to_string(filename) {
Ok(src) => aster_core::run_file(filename, src),
Err(e) => {
eprintln!("Error reading file '{}': {}", filename, e);
std::process::exit(1);
}
}
}
_ => {
eprintln!("Usage: aster [script]");
std::process::exit(1);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
.vscode/**
node_modules/**
.gitignore
+43
View File
@@ -0,0 +1,43 @@
const path = require('path');
const { LanguageClient, TransportKind } = require('vscode-languageclient/node');
/** @type {LanguageClient} */
let client;
/**
* @param {import('vscode').ExtensionContext} context
*/
function activate(context) {
const isWindows = process.platform === 'win32';
const serverExe = isWindows ? 'aster-lsp.exe' : 'aster-lsp';
// __dirname resolves to the real filesystem path (following the junction),
// so .. goes to the workspace root where target/ lives
const serverPath = path.join(__dirname, '..', 'target', 'debug', serverExe);
const serverOptions = {
command: serverPath,
args: [],
};
const clientOptions = {
documentSelector: [{ scheme: 'file', language: 'aster' }],
};
client = new LanguageClient(
'aster-lsp',
'Aster Language Server',
serverOptions,
clientOptions
);
client.start();
}
function deactivate() {
if (client) {
return client.stop();
}
}
module.exports = { activate, deactivate };
+26
View File
@@ -0,0 +1,26 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" },
{ "open": "'", "close": "'" }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
"wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*"
}
+96
View File
@@ -0,0 +1,96 @@
{
"name": "aster-lang",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aster-lang",
"version": "0.1.0",
"dependencies": {
"vscode-languageclient": "^9.0.0"
},
"engines": {
"vscode": "^1.85.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
"license": "MIT",
"dependencies": {
"minimatch": "^5.1.0",
"semver": "^7.3.7",
"vscode-languageserver-protocol": "3.17.5"
},
"engines": {
"vscode": "^1.82.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
}
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "aster-lang",
"displayName": "Aster Language Support",
"description": "Syntax highlighting and language support for the Aster scripting language",
"version": "0.1.0",
"publisher": "aster",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Programming Languages"
],
"main": "./extension.js",
"contributes": {
"languages": [
{
"id": "aster",
"aliases": [
"Aster",
"aster"
],
"extensions": [
".ast"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "aster",
"scopeName": "source.aster",
"path": "./syntaxes/aster.tmLanguage.json"
}
]
},
"dependencies": {
"vscode-languageclient": "^9.0.0"
}
}
+156
View File
@@ -0,0 +1,156 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Aster",
"scopeName": "source.aster",
"fileTypes": ["ast"],
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#numbers" },
{ "include": "#function-declaration" },
{ "include": "#keywords" },
{ "include": "#constants" },
{ "include": "#builtins" },
{ "include": "#operators" },
{ "include": "#punctuation" },
{ "include": "#identifiers" }
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.block.aster",
"begin": "/\\*",
"end": "\\*/",
"patterns": [
{
"name": "comment.block.aster",
"match": "."
}
]
},
{
"name": "comment.line.double-slash.aster",
"match": "//.*$"
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.aster",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.aster",
"match": "\\\\[ntr\"\\\\]"
}
]
},
{
"name": "string.quoted.single.aster",
"begin": "'",
"end": "'",
"patterns": [
{
"name": "constant.character.escape.aster",
"match": "\\\\[ntr'\\\\]"
}
]
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.aster",
"match": "\\b\\d+\\.\\d+\\b"
},
{
"name": "constant.numeric.integer.aster",
"match": "\\b\\d+\\b"
}
]
},
"function-declaration": {
"match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
"captures": {
"1": { "name": "keyword.control.aster" },
"2": { "name": "entity.name.function.aster" }
}
},
"keywords": {
"patterns": [
{
"name": "keyword.control.aster",
"match": "\\b(let|const|fn|if|else|while|for|in|break|continue|return)\\b"
}
]
},
"constants": {
"match": "\\b(true|false|nil)\\b",
"name": "constant.language.aster"
},
"builtins": {
"match": "\\b(print|input|clock|len|typeof|push|pop|split|trim|substring|replace|contains|upper|lower|starts_with|ends_with|require)\\b",
"name": "support.function.builtin.aster"
},
"operators": {
"patterns": [
{
"name": "keyword.operator.assignment.compound.aster",
"match": "\\+=|-=|\\*=|/=|%="
},
{
"name": "keyword.operator.logical.aster",
"match": "&&|\\|\\||!"
},
{
"name": "keyword.operator.comparison.aster",
"match": "==|!=|<=|>="
},
{
"name": "keyword.operator.assignment.aster",
"match": "="
},
{
"name": "keyword.operator.arithmetic.aster",
"match": "[+\\-*/%]"
},
{
"name": "keyword.operator.comparison.aster",
"match": "<|>"
}
]
},
"punctuation": {
"patterns": [
{
"name": "punctuation.section.scope.aster",
"match": "[{}]"
},
{
"name": "punctuation.section.brackets.aster",
"match": "[\\[\\]]"
},
{
"name": "punctuation.section.parens.aster",
"match": "[\\(\\)]"
},
{
"name": "punctuation.separator.aster",
"match": "[,;:\\.]"
},
{
"name": "keyword.operator.ternary.question.aster",
"match": "\\?"
}
]
},
"identifiers": {
"match": "[a-zA-Z_][a-zA-Z0-9_]*",
"name": "variable.other.aster"
}
}
}