3b76f0f8e0
Adds src/main.el + manifest.el for each vessel in this workspace, ported from the Rust sources during the El consolidation pass on 2026-04-30. Each vessel now has both Rust (legacy) and El (target) sources side-by-side; Rust will be removed once the El paths are verified at runtime, vessel by vessel. Per-vessel work was split across multiple parallel agents reading the Rust to understand intent, then designing idiomatic El. Not 1:1 transliteration. Each ported vessel includes: - manifest.el per spec/language.md \u00a715.1 - src/main.el with the vessel's public surface - Compile verified via dist/platform/elc + cc against the el_runtime Known gaps surfaced during the port (held for follow-up): HMAC-SHA256 and base64 crypto, HTTP status code in handler returns, request headers in handler signatures, subprocess primitives, streaming responses, struct/enum types, browser/JS codegen target. Codegen bug list of 9 items tracked separately. The El sources here are runtime- ready under the canonical C runtime; the gaps are language/runtime extensions still in flight.
1239 lines
56 KiB
EmacsLisp
1239 lines
56 KiB
EmacsLisp
// main.el — el-lsp
|
|
//
|
|
// Language Server for El. Pure-El port of the el-lsp Rust crate.
|
|
//
|
|
// Entry surface (callable from any El program that imports this file):
|
|
//
|
|
// lsp_complete(source: String, pos: Int) -> String // JSON array
|
|
// lsp_hover(source: String, pos: Int) -> String // JSON object or "null"
|
|
// lsp_diagnostics(source: String) -> String // JSON array
|
|
// lsp_outline(source: String) -> String // JSON array
|
|
// lsp_format(source: String) -> String // JSON { content, changed }
|
|
// lsp_type_graph(source: String) -> String // JSON { nodes, edges }
|
|
// lsp_definition(source: String, pos: Int) -> String // JSON object or "null"
|
|
// lsp_references(source: String, pos: Int) -> String // JSON array
|
|
// lsp_jsonrpc(method: String, params_json: String) -> String
|
|
// lsp_stdio_loop() -> Void
|
|
//
|
|
// Tokenisation reuses the lexer.el idiom (chars list + index walk) for the
|
|
// pieces we need (identifier scan, string scan, line counting). The full
|
|
// type checker referenced by the Rust crate is not yet available in El, so
|
|
// hover/completion/diagnostics fall back to source-scan heuristics over the
|
|
// declared types/enums/functions in the file. The wire shape is preserved.
|
|
|
|
// ── Char helpers (mirrors lexer.el) ──────────────────────────────────────────
|
|
|
|
fn is_digit_ch(ch: String) -> Bool {
|
|
if ch == "0" { return true } if ch == "1" { return true }
|
|
if ch == "2" { return true } if ch == "3" { return true }
|
|
if ch == "4" { return true } if ch == "5" { return true }
|
|
if ch == "6" { return true } if ch == "7" { return true }
|
|
if ch == "8" { return true } if ch == "9" { return true }
|
|
false
|
|
}
|
|
|
|
fn is_alpha_ch(ch: String) -> Bool {
|
|
let lower: String = str_to_lower(ch)
|
|
if str_eq(ch, "") { return false }
|
|
if !str_eq(lower, str_to_upper(lower)) { return true } // letters differ in case
|
|
// covers a-z (lower-only) — uppercase already handled by inverse case test above
|
|
if str_contains("abcdefghijklmnopqrstuvwxyz", ch) { return true }
|
|
if str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ", ch) { return true }
|
|
false
|
|
}
|
|
|
|
fn is_ident_ch(ch: String) -> Bool {
|
|
if is_digit_ch(ch) { return true }
|
|
if is_alpha_ch(ch) { return true }
|
|
if ch == "_" { return true }
|
|
false
|
|
}
|
|
|
|
// ── Source navigation ────────────────────────────────────────────────────────
|
|
|
|
// Extract the identifier fragment immediately before `pos` (cursor).
|
|
fn extract_prefix(source: String, pos: Int) -> String {
|
|
let n: Int = str_len(source)
|
|
let end: Int = pos
|
|
if end > n { let end = n }
|
|
let start: Int = end
|
|
let chars: [String] = native_string_chars(source)
|
|
let total: Int = native_list_len(chars)
|
|
let scanning: Bool = true
|
|
while scanning {
|
|
if start <= 0 {
|
|
let scanning = false
|
|
} else {
|
|
let prev: String = native_list_get(chars, start - 1)
|
|
if is_ident_ch(prev) {
|
|
let start = start - 1
|
|
} else {
|
|
let scanning = false
|
|
}
|
|
}
|
|
}
|
|
str_slice(source, start, end)
|
|
}
|
|
|
|
// Extract the full token (alphanumerics + underscore) surrounding `pos`.
|
|
fn extract_token(source: String, pos: Int) -> String {
|
|
let n: Int = str_len(source)
|
|
if pos > n { return "" }
|
|
let chars: [String] = native_string_chars(source)
|
|
let total: Int = native_list_len(chars)
|
|
let start: Int = pos
|
|
let go_left: Bool = true
|
|
while go_left {
|
|
if start <= 0 {
|
|
let go_left = false
|
|
} else {
|
|
let prev: String = native_list_get(chars, start - 1)
|
|
if is_ident_ch(prev) {
|
|
let start = start - 1
|
|
} else {
|
|
let go_left = false
|
|
}
|
|
}
|
|
}
|
|
let end: Int = pos
|
|
let go_right: Bool = true
|
|
while go_right {
|
|
if end >= total {
|
|
let go_right = false
|
|
} else {
|
|
let cur: String = native_list_get(chars, end)
|
|
if is_ident_ch(cur) {
|
|
let end = end + 1
|
|
} else {
|
|
let go_right = false
|
|
}
|
|
}
|
|
}
|
|
str_slice(source, start, end)
|
|
}
|
|
|
|
// Translate a byte offset into 1-based (line, col).
|
|
fn pos_to_line_col(source: String, pos: Int) -> Map<String, Any> {
|
|
let n: Int = str_len(source)
|
|
let cap: Int = pos
|
|
if cap > n { let cap = n }
|
|
let chars: [String] = native_string_chars(source)
|
|
let i: Int = 0
|
|
let line: Int = 1
|
|
let col: Int = 1
|
|
while i < cap {
|
|
let ch: String = native_list_get(chars, i)
|
|
if ch == "\n" {
|
|
let line = line + 1
|
|
let col = 1
|
|
} else {
|
|
let col = col + 1
|
|
}
|
|
let i = i + 1
|
|
}
|
|
{ "line": line, "col": col }
|
|
}
|
|
|
|
// ── JSON encoding helpers ────────────────────────────────────────────────────
|
|
|
|
// Escape a string for JSON. Handles ", \, newline, tab.
|
|
fn json_escape(s: String) -> String {
|
|
let chars: [String] = native_string_chars(s)
|
|
let n: Int = native_list_len(chars)
|
|
let out: String = ""
|
|
let i: Int = 0
|
|
while i < n {
|
|
let ch: String = native_list_get(chars, i)
|
|
if ch == "\"" { let out = out + "\\\"" }
|
|
else { if ch == "\\" { let out = out + "\\\\" }
|
|
else { if ch == "\n" { let out = out + "\\n" }
|
|
else { if ch == "\r" { let out = out + "\\r" }
|
|
else { if ch == "\t" { let out = out + "\\t" }
|
|
else { let out = out + ch } } } } }
|
|
let i = i + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
fn json_str(s: String) -> String {
|
|
"\"" + json_escape(s) + "\""
|
|
}
|
|
|
|
fn json_kv_str(k: String, v: String) -> String {
|
|
json_str(k) + ":" + json_str(v)
|
|
}
|
|
|
|
fn json_kv_int(k: String, v: Int) -> String {
|
|
json_str(k) + ":" + int_to_str(v)
|
|
}
|
|
|
|
fn json_kv_float(k: String, v: Float) -> String {
|
|
json_str(k) + ":" + float_to_str(v)
|
|
}
|
|
|
|
fn json_kv_raw(k: String, raw_v: String) -> String {
|
|
json_str(k) + ":" + raw_v
|
|
}
|
|
|
|
// Join a list of pre-encoded JSON strings into a JSON array.
|
|
fn json_array_of(items: [String]) -> String {
|
|
let n: Int = native_list_len(items)
|
|
let out: String = "["
|
|
let i: Int = 0
|
|
while i < n {
|
|
if i > 0 { let out = out + "," }
|
|
let out = out + native_list_get(items, i)
|
|
let i = i + 1
|
|
}
|
|
out + "]"
|
|
}
|
|
|
|
// ── Keyword/builtin tables ───────────────────────────────────────────────────
|
|
|
|
fn keywords_list() -> [String] {
|
|
let ks: [String] = native_list_empty()
|
|
let ks = native_list_append(ks, "let")
|
|
let ks = native_list_append(ks, "fn")
|
|
let ks = native_list_append(ks, "type")
|
|
let ks = native_list_append(ks, "enum")
|
|
let ks = native_list_append(ks, "match")
|
|
let ks = native_list_append(ks, "return")
|
|
let ks = native_list_append(ks, "activate")
|
|
let ks = native_list_append(ks, "where")
|
|
let ks = native_list_append(ks, "sealed")
|
|
let ks = native_list_append(ks, "if")
|
|
let ks = native_list_append(ks, "else")
|
|
let ks = native_list_append(ks, "for")
|
|
let ks = native_list_append(ks, "in")
|
|
let ks = native_list_append(ks, "while")
|
|
let ks = native_list_append(ks, "true")
|
|
let ks = native_list_append(ks, "false")
|
|
let ks = native_list_append(ks, "test")
|
|
let ks = native_list_append(ks, "seed")
|
|
let ks = native_list_append(ks, "assert")
|
|
let ks = native_list_append(ks, "target")
|
|
let ks = native_list_append(ks, "protocol")
|
|
let ks = native_list_append(ks, "impl")
|
|
let ks = native_list_append(ks, "import")
|
|
let ks = native_list_append(ks, "from")
|
|
let ks = native_list_append(ks, "as")
|
|
let ks = native_list_append(ks, "with")
|
|
let ks = native_list_append(ks, "retry")
|
|
let ks = native_list_append(ks, "times")
|
|
let ks = native_list_append(ks, "fallback")
|
|
let ks = native_list_append(ks, "reason")
|
|
let ks = native_list_append(ks, "parallel")
|
|
let ks = native_list_append(ks, "trace")
|
|
let ks = native_list_append(ks, "requires")
|
|
let ks = native_list_append(ks, "deploy")
|
|
let ks = native_list_append(ks, "to")
|
|
let ks = native_list_append(ks, "via")
|
|
let ks = native_list_append(ks, "vessel")
|
|
let ks = native_list_append(ks, "cgi")
|
|
ks
|
|
}
|
|
|
|
// Built-in primitive type names.
|
|
fn builtin_type_names() -> [String] {
|
|
let ts: [String] = native_list_empty()
|
|
let ts = native_list_append(ts, "Int")
|
|
let ts = native_list_append(ts, "Float")
|
|
let ts = native_list_append(ts, "String")
|
|
let ts = native_list_append(ts, "Bool")
|
|
let ts = native_list_append(ts, "Uuid")
|
|
let ts = native_list_append(ts, "Void")
|
|
let ts = native_list_append(ts, "Any")
|
|
ts
|
|
}
|
|
|
|
fn builtin_type_doc(name: String) -> String {
|
|
if str_eq(name, "Int") { return "64-bit signed integer" }
|
|
if str_eq(name, "Float") { return "64-bit IEEE 754 double" }
|
|
if str_eq(name, "String") { return "UTF-8 string" }
|
|
if str_eq(name, "Bool") { return "Boolean value" }
|
|
if str_eq(name, "Uuid") { return "RFC 4122 UUID" }
|
|
if str_eq(name, "Void") { return "Unit type — no value" }
|
|
if str_eq(name, "Any") { return "Dynamically-typed value" }
|
|
""
|
|
}
|
|
|
|
// Built-in function names → signatures. Encoded as flat [name, sig, name, sig, ...]
|
|
// to keep type maps simple.
|
|
fn builtin_fns() -> [String] {
|
|
let xs: [String] = native_list_empty()
|
|
let xs = native_list_append(xs, "println") let xs = native_list_append(xs, "fn(value: String) -> Void")
|
|
let xs = native_list_append(xs, "print") let xs = native_list_append(xs, "fn(value: String) -> Void")
|
|
let xs = native_list_append(xs, "readline") let xs = native_list_append(xs, "fn() -> String")
|
|
let xs = native_list_append(xs, "args") let xs = native_list_append(xs, "fn() -> [String]")
|
|
let xs = native_list_append(xs, "int_to_str") let xs = native_list_append(xs, "fn(n: Int) -> String")
|
|
let xs = native_list_append(xs, "str_to_int") let xs = native_list_append(xs, "fn(s: String) -> Int")
|
|
let xs = native_list_append(xs, "str_len") let xs = native_list_append(xs, "fn(s: String) -> Int")
|
|
let xs = native_list_append(xs, "str_slice") let xs = native_list_append(xs, "fn(s: String, start: Int, end: Int) -> String")
|
|
let xs = native_list_append(xs, "str_concat") let xs = native_list_append(xs, "fn(a: String, b: String) -> String")
|
|
let xs = native_list_append(xs, "str_contains") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Bool")
|
|
let xs = native_list_append(xs, "str_starts_with") let xs = native_list_append(xs, "fn(s: String, p: String) -> Bool")
|
|
let xs = native_list_append(xs, "str_ends_with") let xs = native_list_append(xs, "fn(s: String, suf: String) -> Bool")
|
|
let xs = native_list_append(xs, "str_to_upper") let xs = native_list_append(xs, "fn(s: String) -> String")
|
|
let xs = native_list_append(xs, "str_to_lower") let xs = native_list_append(xs, "fn(s: String) -> String")
|
|
let xs = native_list_append(xs, "str_split") let xs = native_list_append(xs, "fn(s: String, sep: String) -> [String]")
|
|
let xs = native_list_append(xs, "str_trim") let xs = native_list_append(xs, "fn(s: String) -> String")
|
|
let xs = native_list_append(xs, "str_replace") let xs = native_list_append(xs, "fn(s: String, from: String, to: String) -> String")
|
|
let xs = native_list_append(xs, "str_index_of") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Int")
|
|
let xs = native_list_append(xs, "float_to_str") let xs = native_list_append(xs, "fn(f: Float) -> String")
|
|
let xs = native_list_append(xs, "str_to_float") let xs = native_list_append(xs, "fn(s: String) -> Float")
|
|
let xs = native_list_append(xs, "math_sqrt") let xs = native_list_append(xs, "fn(n: Float) -> Float")
|
|
let xs = native_list_append(xs, "math_log") let xs = native_list_append(xs, "fn(n: Float) -> Float")
|
|
let xs = native_list_append(xs, "math_sin") let xs = native_list_append(xs, "fn(n: Float) -> Float")
|
|
let xs = native_list_append(xs, "math_cos") let xs = native_list_append(xs, "fn(n: Float) -> Float")
|
|
let xs = native_list_append(xs, "math_pi") let xs = native_list_append(xs, "fn() -> Float")
|
|
let xs = native_list_append(xs, "el_abs") let xs = native_list_append(xs, "fn(n: Int) -> Int")
|
|
let xs = native_list_append(xs, "el_min") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int")
|
|
let xs = native_list_append(xs, "el_max") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int")
|
|
let xs = native_list_append(xs, "el_list_empty") let xs = native_list_append(xs, "fn() -> [Any]")
|
|
let xs = native_list_append(xs, "el_list_len") let xs = native_list_append(xs, "fn(l: [Any]) -> Int")
|
|
let xs = native_list_append(xs, "el_list_get") let xs = native_list_append(xs, "fn(l: [Any], i: Int) -> Any")
|
|
let xs = native_list_append(xs, "el_list_append") let xs = native_list_append(xs, "fn(l: [Any], v: Any) -> [Any]")
|
|
let xs = native_list_append(xs, "list_join") let xs = native_list_append(xs, "fn(l: [String], sep: String) -> String")
|
|
let xs = native_list_append(xs, "list_range") let xs = native_list_append(xs, "fn(start: Int, end: Int) -> [Int]")
|
|
let xs = native_list_append(xs, "el_map_get") let xs = native_list_append(xs, "fn(m: Map, k: String) -> Any")
|
|
let xs = native_list_append(xs, "el_map_set") let xs = native_list_append(xs, "fn(m: Map, k: String, v: Any) -> Map")
|
|
let xs = native_list_append(xs, "json_parse") let xs = native_list_append(xs, "fn(s: String) -> Any")
|
|
let xs = native_list_append(xs, "json_stringify") let xs = native_list_append(xs, "fn(v: Any) -> String")
|
|
let xs = native_list_append(xs, "json_get_string") let xs = native_list_append(xs, "fn(j: String, k: String) -> String")
|
|
let xs = native_list_append(xs, "json_get_int") let xs = native_list_append(xs, "fn(j: String, k: String) -> Int")
|
|
let xs = native_list_append(xs, "json_get_bool") let xs = native_list_append(xs, "fn(j: String, k: String) -> Bool")
|
|
let xs = native_list_append(xs, "http_get") let xs = native_list_append(xs, "fn(url: String) -> String")
|
|
let xs = native_list_append(xs, "http_post") let xs = native_list_append(xs, "fn(url: String, body: String) -> String")
|
|
let xs = native_list_append(xs, "fs_read") let xs = native_list_append(xs, "fn(path: String) -> String")
|
|
let xs = native_list_append(xs, "fs_write") let xs = native_list_append(xs, "fn(path: String, content: String) -> Bool")
|
|
let xs = native_list_append(xs, "fs_list") let xs = native_list_append(xs, "fn(path: String) -> [String]")
|
|
let xs = native_list_append(xs, "env") let xs = native_list_append(xs, "fn(key: String) -> String")
|
|
let xs = native_list_append(xs, "uuid_new") let xs = native_list_append(xs, "fn() -> String")
|
|
let xs = native_list_append(xs, "time_now") let xs = native_list_append(xs, "fn() -> Int")
|
|
let xs = native_list_append(xs, "time_now_utc") let xs = native_list_append(xs, "fn() -> Int")
|
|
let xs = native_list_append(xs, "sleep_ms") let xs = native_list_append(xs, "fn(ms: Int) -> Void")
|
|
let xs = native_list_append(xs, "engram_node") let xs = native_list_append(xs, "fn(content: String, kind: String, salience: Float) -> String")
|
|
let xs = native_list_append(xs, "engram_search") let xs = native_list_append(xs, "fn(q: String, limit: Int) -> [Map]")
|
|
let xs = native_list_append(xs, "engram_activate") let xs = native_list_append(xs, "fn(q: String, depth: Int) -> [Map]")
|
|
let xs = native_list_append(xs, "engram_neighbors") let xs = native_list_append(xs, "fn(id: String) -> [Map]")
|
|
let xs = native_list_append(xs, "engram_connect") let xs = native_list_append(xs, "fn(from: String, to: String, w: Float, rel: String) -> Void")
|
|
let xs = native_list_append(xs, "dharma_connect") let xs = native_list_append(xs, "fn(cgi: String) -> String")
|
|
let xs = native_list_append(xs, "dharma_send") let xs = native_list_append(xs, "fn(channel: String, content: String) -> String")
|
|
let xs = native_list_append(xs, "dharma_emit") let xs = native_list_append(xs, "fn(event: String, payload: String) -> Void")
|
|
let xs = native_list_append(xs, "dharma_field") let xs = native_list_append(xs, "fn(event: String) -> Map")
|
|
let xs = native_list_append(xs, "llm_call") let xs = native_list_append(xs, "fn(model: String, prompt: String) -> String")
|
|
xs
|
|
}
|
|
|
|
fn keyword_doc(kw: String) -> String {
|
|
if str_eq(kw, "let") { return "let name: Type = expr — declare an immutable binding." }
|
|
if str_eq(kw, "fn") { return "fn name(params) -> Ret { body } — define a function." }
|
|
if str_eq(kw, "type") { return "type Name { field: Type } — struct definition." }
|
|
if str_eq(kw, "enum") { return "enum Name { Variant1, Variant2(Type) } — enum definition." }
|
|
if str_eq(kw, "match") { return "match expr { Pattern => body } — pattern match." }
|
|
if str_eq(kw, "return") { return "return value — exit the enclosing function." }
|
|
if str_eq(kw, "if") { return "if cond { ... } else { ... } — conditional." }
|
|
if str_eq(kw, "for") { return "for item in collection { ... } — iterate." }
|
|
if str_eq(kw, "while") { return "while cond { ... } — loop while cond is truthy." }
|
|
if str_eq(kw, "import") { return "import \"path.el\" — pull in another module." }
|
|
if str_eq(kw, "from") { return "from module import { Name } — selective import." }
|
|
if str_eq(kw, "vessel") { return "vessel \"name\" { ... } — manifest declaration." }
|
|
if str_eq(kw, "cgi") { return "cgi \"name\" { dharma_id: ... } — CGI identity." }
|
|
if str_eq(kw, "activate") { return "activate Type where \"query\" — spreading-activation retrieval." }
|
|
if str_eq(kw, "sealed") { return "sealed { ... } — capability-restricted block." }
|
|
if str_eq(kw, "protocol") { return "protocol Name { fn method(self) -> R } — trait-like." }
|
|
if str_eq(kw, "impl") { return "impl Protocol for Type { ... } — protocol implementation." }
|
|
if str_eq(kw, "with") { return "with retry/fallback clause." }
|
|
if str_eq(kw, "retry") { return "retry times N — retry policy." }
|
|
if str_eq(kw, "fallback") { return "fallback { default } — fallback on failure." }
|
|
if str_eq(kw, "true") { return "Boolean literal." }
|
|
if str_eq(kw, "false") { return "Boolean literal." }
|
|
""
|
|
}
|
|
|
|
// ── Source-driven type/fn discovery (no full parser) ─────────────────────────
|
|
//
|
|
// The Rust crate calls into el-types::TypeChecker for a real env. That
|
|
// module isn't available in El yet, so we approximate by line-scan: find
|
|
// every `type X {`, `enum X {`, `protocol X {`, and `fn X(` declaration.
|
|
// Returns four parallel lists merged into one map for easy access.
|
|
|
|
fn first_ident_in(rest: String) -> String {
|
|
let trimmed: String = str_trim(rest)
|
|
let n: Int = str_len(trimmed)
|
|
if n == 0 { return "" }
|
|
let chars: [String] = native_string_chars(trimmed)
|
|
let i: Int = 0
|
|
let total: Int = native_list_len(chars)
|
|
while i < total {
|
|
let ch: String = native_list_get(chars, i)
|
|
if !is_ident_ch(ch) {
|
|
return str_slice(trimmed, 0, i)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
trimmed
|
|
}
|
|
|
|
// scan_decls — produce a Map<String, Any> with parallel lists:
|
|
// "types" : [String] user-defined struct/enum/protocol names
|
|
// "type_kind" : [String] "type" | "enum" | "protocol" matching above
|
|
// "fns" : [String] user-defined function names
|
|
// "type_lines" : [Int] 1-based line numbers
|
|
// "fn_lines" : [Int]
|
|
fn scan_decls(source: String) -> Map<String, Any> {
|
|
let lines: [String] = str_split(source, "\n")
|
|
let n: Int = native_list_len(lines)
|
|
let types: [String] = native_list_empty()
|
|
let kinds: [String] = native_list_empty()
|
|
let fns: [String] = native_list_empty()
|
|
let tlines: [Int] = native_list_empty()
|
|
let flines: [Int] = native_list_empty()
|
|
let i: Int = 0
|
|
while i < n {
|
|
let line: String = native_list_get(lines, i)
|
|
let trimmed: String = str_trim(line)
|
|
let lineno: Int = i + 1
|
|
if str_starts_with(trimmed, "fn ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let fns = native_list_append(fns, name)
|
|
let flines = native_list_append(flines, lineno)
|
|
}
|
|
} else { if str_starts_with(trimmed, "type ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let types = native_list_append(types, name)
|
|
let kinds = native_list_append(kinds, "type")
|
|
let tlines = native_list_append(tlines, lineno)
|
|
}
|
|
} else { if str_starts_with(trimmed, "enum ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let types = native_list_append(types, name)
|
|
let kinds = native_list_append(kinds, "enum")
|
|
let tlines = native_list_append(tlines, lineno)
|
|
}
|
|
} else { if str_starts_with(trimmed, "protocol ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let types = native_list_append(types, name)
|
|
let kinds = native_list_append(kinds, "protocol")
|
|
let tlines = native_list_append(tlines, lineno)
|
|
}
|
|
} } } }
|
|
let i = i + 1
|
|
}
|
|
{
|
|
"types": types,
|
|
"type_kind": kinds,
|
|
"fns": fns,
|
|
"type_lines": tlines,
|
|
"fn_lines": flines
|
|
}
|
|
}
|
|
|
|
// list_contains — case-sensitive membership test on [String].
|
|
fn list_contains_str(xs: [String], needle: String) -> Bool {
|
|
let n: Int = native_list_len(xs)
|
|
let i: Int = 0
|
|
while i < n {
|
|
if str_eq(native_list_get(xs, i), needle) { return true }
|
|
let i = i + 1
|
|
}
|
|
false
|
|
}
|
|
|
|
// Score a label against a prefix: 0 if no prefix match, else (prefix_len/label_len).
|
|
fn prefix_score(label: String, prefix: String) -> Float {
|
|
if str_eq(prefix, "") { return 0.5 }
|
|
let lower_label: String = str_to_lower(label)
|
|
let lower_prefix: String = str_to_lower(prefix)
|
|
if str_starts_with(lower_label, lower_prefix) {
|
|
let lp: Int = str_len(lower_prefix)
|
|
let ll: Int = str_len(lower_label)
|
|
if ll == 0 { return 0.0 }
|
|
return int_to_float(lp) / int_to_float(ll)
|
|
}
|
|
0.0
|
|
}
|
|
|
|
// Render a single completion item to JSON.
|
|
fn completion_json(label: String, kind: String, detail: String, doc: String, score: Float) -> String {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("label", label))
|
|
let parts = native_list_append(parts, json_kv_str("kind", kind))
|
|
let parts = native_list_append(parts, json_kv_str("detail", detail))
|
|
if str_eq(doc, "") {
|
|
let parts = native_list_append(parts, json_kv_raw("documentation", "null"))
|
|
} else {
|
|
let parts = native_list_append(parts, json_kv_str("documentation", doc))
|
|
}
|
|
let parts = native_list_append(parts, json_kv_float("score", score))
|
|
"{" + list_join(parts, ",") + "}"
|
|
}
|
|
|
|
// ── Public: completion ───────────────────────────────────────────────────────
|
|
|
|
fn lsp_complete(source: String, pos: Int) -> String {
|
|
let prefix: String = extract_prefix(source, pos)
|
|
let lower_prefix: String = str_to_lower(prefix)
|
|
let prefix_empty: Bool = str_eq(prefix, "")
|
|
let items: [String] = native_list_empty()
|
|
|
|
// Keywords
|
|
let kws: [String] = keywords_list()
|
|
let nk: Int = native_list_len(kws)
|
|
let i: Int = 0
|
|
while i < nk {
|
|
let kw: String = native_list_get(kws, i)
|
|
if prefix_empty {
|
|
let s: Float = prefix_score(kw, prefix)
|
|
let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s))
|
|
} else { if str_starts_with(kw, prefix) {
|
|
let s: Float = prefix_score(kw, prefix)
|
|
let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s))
|
|
} }
|
|
let i = i + 1
|
|
}
|
|
|
|
// Built-in types
|
|
let bts: [String] = builtin_type_names()
|
|
let nb: Int = native_list_len(bts)
|
|
let i: Int = 0
|
|
while i < nb {
|
|
let name: String = native_list_get(bts, i)
|
|
let lname: String = str_to_lower(name)
|
|
if prefix_empty {
|
|
let s: Float = 0.5 + 0.1
|
|
let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s))
|
|
} else { if str_starts_with(lname, lower_prefix) {
|
|
let s: Float = prefix_score(name, prefix) + 0.1
|
|
let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s))
|
|
} }
|
|
let i = i + 1
|
|
}
|
|
|
|
// User-defined types
|
|
let decls: Map<String, Any> = scan_decls(source)
|
|
let utypes: [String] = decls["types"]
|
|
let ukinds: [String] = decls["type_kind"]
|
|
let nu: Int = native_list_len(utypes)
|
|
let i: Int = 0
|
|
while i < nu {
|
|
let name: String = native_list_get(utypes, i)
|
|
let kind: String = native_list_get(ukinds, i)
|
|
let lname: String = str_to_lower(name)
|
|
let include: Bool = false
|
|
if prefix_empty { let include = true }
|
|
if str_starts_with(lname, lower_prefix) { let include = true }
|
|
if include {
|
|
let s: Float = prefix_score(name, prefix) + 0.15
|
|
let items = native_list_append(items, completion_json(name, "type", kind + " " + name, "User-defined " + kind + " " + name, s))
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
// Built-in functions
|
|
let bfns: [String] = builtin_fns()
|
|
let nf: Int = native_list_len(bfns)
|
|
let i: Int = 0
|
|
while i < nf {
|
|
let name: String = native_list_get(bfns, i)
|
|
let sig: String = native_list_get(bfns, i + 1)
|
|
let lname: String = str_to_lower(name)
|
|
let include: Bool = false
|
|
if prefix_empty { let include = true }
|
|
if str_starts_with(lname, lower_prefix) { let include = true }
|
|
if include {
|
|
let s: Float = prefix_score(name, prefix) + 0.11
|
|
let items = native_list_append(items, completion_json(name, "function", sig, "Built-in: " + name + " :: " + sig, s))
|
|
}
|
|
let i = i + 2
|
|
}
|
|
|
|
// User-defined functions
|
|
let ufns: [String] = decls["fns"]
|
|
let nuf: Int = native_list_len(ufns)
|
|
let i: Int = 0
|
|
while i < nuf {
|
|
let name: String = native_list_get(ufns, i)
|
|
let lname: String = str_to_lower(name)
|
|
let include: Bool = false
|
|
if prefix_empty { let include = true }
|
|
if str_starts_with(lname, lower_prefix) { let include = true }
|
|
if include {
|
|
let s: Float = prefix_score(name, prefix) + 0.12
|
|
let items = native_list_append(items, completion_json(name, "function", "fn " + name, "User function: " + name, s))
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
// Output is a list of pre-encoded JSON objects. Sorting by score is a
|
|
// stretch goal — el's runtime has no sort-by-key primitive. Items are
|
|
// already ordered by category (keywords first), which matches the most
|
|
// common editor expectation.
|
|
json_array_of(items)
|
|
}
|
|
|
|
// ── Public: hover ────────────────────────────────────────────────────────────
|
|
|
|
fn lsp_hover(source: String, pos: Int) -> String {
|
|
let token: String = extract_token(source, pos)
|
|
if str_eq(token, "") { return "null" }
|
|
|
|
// Built-in primitives
|
|
let doc: String = builtin_type_doc(token)
|
|
if !str_eq(doc, "") {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("type_name", token))
|
|
let parts = native_list_append(parts, json_kv_str("documentation", doc))
|
|
let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
|
|
return "{" + list_join(parts, ",") + "}"
|
|
}
|
|
|
|
// User-declared types
|
|
let decls: Map<String, Any> = scan_decls(source)
|
|
let utypes: [String] = decls["types"]
|
|
let ukinds: [String] = decls["type_kind"]
|
|
let nu: Int = native_list_len(utypes)
|
|
let i: Int = 0
|
|
while i < nu {
|
|
let name: String = native_list_get(utypes, i)
|
|
if str_eq(name, token) {
|
|
let kind: String = native_list_get(ukinds, i)
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("type_name", token))
|
|
let parts = native_list_append(parts, json_kv_str("documentation", kind + " " + token))
|
|
let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
|
|
return "{" + list_join(parts, ",") + "}"
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
// User-declared functions
|
|
let ufns: [String] = decls["fns"]
|
|
let nuf: Int = native_list_len(ufns)
|
|
let i: Int = 0
|
|
while i < nuf {
|
|
let name: String = native_list_get(ufns, i)
|
|
if str_eq(name, token) {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("type_name", token))
|
|
let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token))
|
|
let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
|
|
return "{" + list_join(parts, ",") + "}"
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
// Built-in functions
|
|
let bfns: [String] = builtin_fns()
|
|
let nf: Int = native_list_len(bfns)
|
|
let i: Int = 0
|
|
while i < nf {
|
|
let name: String = native_list_get(bfns, i)
|
|
if str_eq(name, token) {
|
|
let sig: String = native_list_get(bfns, i + 1)
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("type_name", token))
|
|
let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token + " :: " + sig))
|
|
let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
|
|
return "{" + list_join(parts, ",") + "}"
|
|
}
|
|
let i = i + 2
|
|
}
|
|
|
|
// Keyword
|
|
let kdoc: String = keyword_doc(token)
|
|
if !str_eq(kdoc, "") {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("type_name", token))
|
|
let parts = native_list_append(parts, json_kv_str("documentation", kdoc))
|
|
let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
|
|
return "{" + list_join(parts, ",") + "}"
|
|
}
|
|
|
|
"null"
|
|
}
|
|
|
|
// ── Public: diagnostics ──────────────────────────────────────────────────────
|
|
//
|
|
// Without the type checker port we surface only structural diagnostics:
|
|
// * unterminated string literal
|
|
// * unbalanced braces / parens / brackets
|
|
// * unknown type referenced after `:` in a let binding (unknown to source +
|
|
// builtins) — best-effort, conservative.
|
|
|
|
fn diag_json(message: String, severity: String, line: Int, col: Int) -> String {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("message", message))
|
|
let parts = native_list_append(parts, json_kv_str("severity", severity))
|
|
if line > 0 {
|
|
let parts = native_list_append(parts, json_kv_int("line", line))
|
|
let parts = native_list_append(parts, json_kv_int("col", col))
|
|
} else {
|
|
let parts = native_list_append(parts, json_kv_raw("line", "null"))
|
|
let parts = native_list_append(parts, json_kv_raw("col", "null"))
|
|
}
|
|
"{" + list_join(parts, ",") + "}"
|
|
}
|
|
|
|
fn lsp_diagnostics(source: String) -> String {
|
|
let chars: [String] = native_string_chars(source)
|
|
let n: Int = native_list_len(chars)
|
|
let diags: [String] = native_list_empty()
|
|
|
|
// Bracket balance + unterminated string scan
|
|
let depth_brace: Int = 0
|
|
let depth_paren: Int = 0
|
|
let depth_brack: Int = 0
|
|
let i: Int = 0
|
|
let line: Int = 1
|
|
let col: Int = 1
|
|
let in_string: Bool = false
|
|
let string_start_line: Int = 0
|
|
let string_start_col: Int = 0
|
|
let in_comment: Bool = false
|
|
while i < n {
|
|
let ch: String = native_list_get(chars, i)
|
|
if in_comment {
|
|
if ch == "\n" {
|
|
let in_comment = false
|
|
let line = line + 1
|
|
let col = 1
|
|
} else {
|
|
let col = col + 1
|
|
}
|
|
let i = i + 1
|
|
} else {
|
|
if in_string {
|
|
if ch == "\\" {
|
|
let i = i + 1
|
|
let col = col + 1
|
|
if i < n {
|
|
let esc: String = native_list_get(chars, i)
|
|
if esc == "\n" { let line = line + 1 let col = 1 } else { let col = col + 1 }
|
|
let i = i + 1
|
|
}
|
|
} else { if ch == "\"" {
|
|
let in_string = false
|
|
let i = i + 1
|
|
let col = col + 1
|
|
} else { if ch == "\n" {
|
|
let line = line + 1
|
|
let col = 1
|
|
let i = i + 1
|
|
} else {
|
|
let col = col + 1
|
|
let i = i + 1
|
|
} } }
|
|
} else {
|
|
if ch == "\"" {
|
|
let in_string = true
|
|
let string_start_line = line
|
|
let string_start_col = col
|
|
let i = i + 1
|
|
let col = col + 1
|
|
} else { if ch == "/" {
|
|
let next_i: Int = i + 1
|
|
let is_line_comment: Bool = false
|
|
if next_i < n {
|
|
let next_ch: String = native_list_get(chars, next_i)
|
|
if next_ch == "/" { let is_line_comment = true }
|
|
}
|
|
if is_line_comment {
|
|
let in_comment = true
|
|
let i = i + 2
|
|
let col = col + 2
|
|
} else {
|
|
let i = i + 1
|
|
let col = col + 1
|
|
}
|
|
} else { if ch == "{" {
|
|
let depth_brace = depth_brace + 1
|
|
let i = i + 1 let col = col + 1
|
|
} else { if ch == "}" {
|
|
let depth_brace = depth_brace - 1
|
|
if depth_brace < 0 {
|
|
let diags = native_list_append(diags, diag_json("unmatched `}`", "error", line, col))
|
|
let depth_brace = 0
|
|
}
|
|
let i = i + 1 let col = col + 1
|
|
} else { if ch == "(" {
|
|
let depth_paren = depth_paren + 1
|
|
let i = i + 1 let col = col + 1
|
|
} else { if ch == ")" {
|
|
let depth_paren = depth_paren - 1
|
|
if depth_paren < 0 {
|
|
let diags = native_list_append(diags, diag_json("unmatched `)`", "error", line, col))
|
|
let depth_paren = 0
|
|
}
|
|
let i = i + 1 let col = col + 1
|
|
} else { if ch == "[" {
|
|
let depth_brack = depth_brack + 1
|
|
let i = i + 1 let col = col + 1
|
|
} else { if ch == "]" {
|
|
let depth_brack = depth_brack - 1
|
|
if depth_brack < 0 {
|
|
let diags = native_list_append(diags, diag_json("unmatched `]`", "error", line, col))
|
|
let depth_brack = 0
|
|
}
|
|
let i = i + 1 let col = col + 1
|
|
} else { if ch == "\n" {
|
|
let line = line + 1
|
|
let col = 1
|
|
let i = i + 1
|
|
} else {
|
|
let i = i + 1
|
|
let col = col + 1
|
|
} } } } } } } } }
|
|
}
|
|
}
|
|
}
|
|
|
|
if in_string {
|
|
let diags = native_list_append(diags, diag_json("unterminated string literal", "error", string_start_line, string_start_col))
|
|
}
|
|
if depth_brace > 0 { let diags = native_list_append(diags, diag_json("unclosed `{` (missing `}`)", "error", 0, 0)) }
|
|
if depth_paren > 0 { let diags = native_list_append(diags, diag_json("unclosed `(` (missing `)`)", "error", 0, 0)) }
|
|
if depth_brack > 0 { let diags = native_list_append(diags, diag_json("unclosed `[` (missing `]`)", "error", 0, 0)) }
|
|
|
|
json_array_of(diags)
|
|
}
|
|
|
|
// ── Public: outline ──────────────────────────────────────────────────────────
|
|
|
|
fn outline_item_json(kind: String, name: String, line: Int) -> String {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("kind", kind))
|
|
let parts = native_list_append(parts, json_kv_str("name", name))
|
|
let parts = native_list_append(parts, json_kv_int("line", line))
|
|
"{" + list_join(parts, ",") + "}"
|
|
}
|
|
|
|
fn lsp_outline(source: String) -> String {
|
|
let lines: [String] = str_split(source, "\n")
|
|
let n: Int = native_list_len(lines)
|
|
let items: [String] = native_list_empty()
|
|
let i: Int = 0
|
|
while i < n {
|
|
let line: String = native_list_get(lines, i)
|
|
let trimmed: String = str_trim(line)
|
|
let lineno: Int = i + 1
|
|
if str_starts_with(trimmed, "fn ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let items = native_list_append(items, outline_item_json("fn", name, lineno))
|
|
}
|
|
} else { if str_starts_with(trimmed, "type ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let items = native_list_append(items, outline_item_json("type", name, lineno))
|
|
}
|
|
} else { if str_starts_with(trimmed, "enum ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let items = native_list_append(items, outline_item_json("enum", name, lineno))
|
|
}
|
|
} else { if str_starts_with(trimmed, "protocol ") {
|
|
let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed)))
|
|
if !str_eq(name, "") {
|
|
let items = native_list_append(items, outline_item_json("protocol", name, lineno))
|
|
}
|
|
} else { if str_starts_with(trimmed, "impl ") {
|
|
let rest: String = str_trim(str_slice(trimmed, 5, str_len(trimmed)))
|
|
let brace: Int = str_index_of(rest, "{")
|
|
let name: String = rest
|
|
if brace > 0 { let name = str_trim(str_slice(rest, 0, brace)) }
|
|
if !str_eq(name, "") {
|
|
let items = native_list_append(items, outline_item_json("impl", name, lineno))
|
|
}
|
|
} } } } }
|
|
let i = i + 1
|
|
}
|
|
json_array_of(items)
|
|
}
|
|
|
|
// ── Public: format ───────────────────────────────────────────────────────────
|
|
//
|
|
// Idiomatic-El formatter. Until `el fmt` is a real thing, we apply a minimal
|
|
// canonicalisation pass:
|
|
// * trim trailing whitespace on every line
|
|
// * collapse 3+ blank lines to 2
|
|
// * ensure file ends with exactly one newline
|
|
// This is safe for round-tripping and a noticeable quality-of-life win in
|
|
// the editor.
|
|
|
|
fn format_strip_trailing(line: String) -> String {
|
|
// str_trim trims both ends; we want only trailing whitespace stripped.
|
|
let n: Int = str_len(line)
|
|
if n == 0 { return line }
|
|
let chars: [String] = native_string_chars(line)
|
|
let i: Int = n - 1
|
|
let trimming: Bool = true
|
|
while trimming {
|
|
if i < 0 {
|
|
let trimming = false
|
|
} else {
|
|
let ch: String = native_list_get(chars, i)
|
|
if ch == " " { let i = i - 1 } else { if ch == "\t" { let i = i - 1 } else { let trimming = false } }
|
|
}
|
|
}
|
|
str_slice(line, 0, i + 1)
|
|
}
|
|
|
|
fn lsp_format(source: String) -> String {
|
|
let lines: [String] = str_split(source, "\n")
|
|
let n: Int = native_list_len(lines)
|
|
let cleaned: [String] = native_list_empty()
|
|
let i: Int = 0
|
|
let blank_run: Int = 0
|
|
while i < n {
|
|
let line: String = native_list_get(lines, i)
|
|
let stripped: String = format_strip_trailing(line)
|
|
if str_eq(stripped, "") {
|
|
let blank_run = blank_run + 1
|
|
if blank_run <= 2 {
|
|
let cleaned = native_list_append(cleaned, "")
|
|
}
|
|
} else {
|
|
let blank_run = 0
|
|
let cleaned = native_list_append(cleaned, stripped)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
let joined: String = list_join(cleaned, "\n")
|
|
// Ensure trailing newline.
|
|
let final: String = joined
|
|
if !str_ends_with(final, "\n") {
|
|
let final = final + "\n"
|
|
}
|
|
let changed: String = "false"
|
|
if !str_eq(final, source) { let changed = "true" }
|
|
"{" + json_kv_str("content", final) + "," + json_kv_raw("changed", changed) + "}"
|
|
}
|
|
|
|
// ── Public: type graph ───────────────────────────────────────────────────────
|
|
|
|
fn lsp_type_graph(source: String) -> String {
|
|
let nodes: [String] = native_list_empty()
|
|
let edges: [String] = native_list_empty()
|
|
|
|
// Built-in nodes
|
|
let bts: [String] = builtin_type_names()
|
|
let nb: Int = native_list_len(bts)
|
|
let i: Int = 0
|
|
while i < nb {
|
|
let name: String = native_list_get(bts, i)
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("id", name))
|
|
let parts = native_list_append(parts, json_kv_str("name", name))
|
|
let parts = native_list_append(parts, json_kv_str("kind", "builtin"))
|
|
let parts = native_list_append(parts, json_kv_raw("fields", "[]"))
|
|
let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}")
|
|
let i = i + 1
|
|
}
|
|
|
|
// User-declared types
|
|
let decls: Map<String, Any> = scan_decls(source)
|
|
let utypes: [String] = decls["types"]
|
|
let ukinds: [String] = decls["type_kind"]
|
|
let tlines: [Int] = decls["type_lines"]
|
|
let nu: Int = native_list_len(utypes)
|
|
let i: Int = 0
|
|
let bts_dup: [String] = builtin_type_names()
|
|
while i < nu {
|
|
let name: String = native_list_get(utypes, i)
|
|
let kind: String = native_list_get(ukinds, i)
|
|
// Skip if already a builtin (paranoia)
|
|
if !list_contains_str(bts_dup, name) {
|
|
let fields: [String] = collect_struct_fields(source, name)
|
|
let field_jsons: [String] = native_list_empty()
|
|
let fn2: Int = native_list_len(fields)
|
|
let j: Int = 0
|
|
while j < fn2 {
|
|
let field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j)))
|
|
let j = j + 1
|
|
}
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_str("id", name))
|
|
let parts = native_list_append(parts, json_kv_str("name", name))
|
|
let parts = native_list_append(parts, json_kv_str("kind", kind))
|
|
let parts = native_list_append(parts, json_kv_raw("fields", json_array_of(field_jsons)))
|
|
let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}")
|
|
|
|
// Edges: scan each field for a known type reference.
|
|
let j: Int = 0
|
|
while j < fn2 {
|
|
let field_str: String = native_list_get(fields, j)
|
|
let target: String = referenced_type(field_str, utypes)
|
|
if !str_eq(target, "") {
|
|
let eparts: [String] = native_list_empty()
|
|
let eparts = native_list_append(eparts, json_kv_str("from", name))
|
|
let eparts = native_list_append(eparts, json_kv_str("to", target))
|
|
let eparts = native_list_append(eparts, json_kv_str("label", "field"))
|
|
let edges = native_list_append(edges, "{" + list_join(eparts, ",") + "}")
|
|
}
|
|
let j = j + 1
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
"{" + json_kv_raw("nodes", json_array_of(nodes)) + "," + json_kv_raw("edges", json_array_of(edges)) + "}"
|
|
}
|
|
|
|
// Extract the field list of a `type Name { ... }` block as ["field: Type", ...]
|
|
// or for an enum, the variant labels. Best-effort line-by-line scan.
|
|
fn collect_struct_fields(source: String, type_name: String) -> [String] {
|
|
let lines: [String] = str_split(source, "\n")
|
|
let n: Int = native_list_len(lines)
|
|
let fields: [String] = native_list_empty()
|
|
let inside: Bool = false
|
|
let i: Int = 0
|
|
let header_a: String = "type " + type_name
|
|
let header_b: String = "enum " + type_name
|
|
let header_c: String = "protocol " + type_name
|
|
while i < n {
|
|
let line: String = native_list_get(lines, i)
|
|
let trimmed: String = str_trim(line)
|
|
if !inside {
|
|
if str_starts_with(trimmed, header_a) { let inside = true }
|
|
if str_starts_with(trimmed, header_b) { let inside = true }
|
|
if str_starts_with(trimmed, header_c) { let inside = true }
|
|
} else {
|
|
if str_starts_with(trimmed, "}") {
|
|
return fields
|
|
}
|
|
// skip the opening "{" line
|
|
if !str_eq(trimmed, "{") {
|
|
if !str_eq(trimmed, "") {
|
|
if !str_starts_with(trimmed, "//") {
|
|
// strip a trailing comma if present
|
|
let entry: String = trimmed
|
|
if str_ends_with(entry, ",") {
|
|
let entry = str_slice(entry, 0, str_len(entry) - 1)
|
|
}
|
|
let fields = native_list_append(fields, str_trim(entry))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
fields
|
|
}
|
|
|
|
// Pick the first known type name out of a `field: Type` string. Stops at non-ident.
|
|
fn referenced_type(field_str: String, known_types: [String]) -> String {
|
|
// Find ":" — type follows.
|
|
let colon: Int = str_index_of(field_str, ":")
|
|
if colon < 0 { return "" }
|
|
let after: String = str_trim(str_slice(field_str, colon + 1, str_len(field_str)))
|
|
// Strip array brackets and optional marker.
|
|
let trimmed_l: String = after
|
|
if str_starts_with(trimmed_l, "[") {
|
|
let end: Int = str_index_of(trimmed_l, "]")
|
|
if end > 1 {
|
|
let trimmed_l = str_trim(str_slice(trimmed_l, 1, end))
|
|
}
|
|
}
|
|
// First identifier.
|
|
let ident: String = first_ident_in(trimmed_l)
|
|
if str_eq(ident, "") { return "" }
|
|
let bts: [String] = builtin_type_names()
|
|
if list_contains_str(bts, ident) { return ident }
|
|
if list_contains_str(known_types, ident) { return ident }
|
|
""
|
|
}
|
|
|
|
// ── Public: definition / references ──────────────────────────────────────────
|
|
|
|
fn lsp_definition(source: String, pos: Int) -> String {
|
|
let word: String = extract_token(source, pos)
|
|
if str_eq(word, "") { return "null" }
|
|
let patterns: [String] = native_list_empty()
|
|
let patterns = native_list_append(patterns, "fn " + word)
|
|
let patterns = native_list_append(patterns, "type " + word)
|
|
let patterns = native_list_append(patterns, "enum " + word)
|
|
let patterns = native_list_append(patterns, "protocol " + word)
|
|
let np: Int = native_list_len(patterns)
|
|
let lines: [String] = str_split(source, "\n")
|
|
let nl: Int = native_list_len(lines)
|
|
let i: Int = 0
|
|
while i < nl {
|
|
let line: String = native_list_get(lines, i)
|
|
let j: Int = 0
|
|
while j < np {
|
|
let pat: String = native_list_get(patterns, j)
|
|
let idx: Int = str_index_of(line, pat)
|
|
if idx >= 0 {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_int("line", i + 1))
|
|
let parts = native_list_append(parts, json_kv_int("col", idx + 1))
|
|
let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line)))
|
|
return "{" + list_join(parts, ",") + "}"
|
|
}
|
|
let j = j + 1
|
|
}
|
|
let i = i + 1
|
|
}
|
|
"null"
|
|
}
|
|
|
|
fn lsp_references(source: String, pos: Int) -> String {
|
|
let word: String = extract_token(source, pos)
|
|
if str_eq(word, "") { return "[]" }
|
|
let lines: [String] = str_split(source, "\n")
|
|
let nl: Int = native_list_len(lines)
|
|
let refs: [String] = native_list_empty()
|
|
let i: Int = 0
|
|
while i < nl {
|
|
let line: String = native_list_get(lines, i)
|
|
let line_chars: [String] = native_string_chars(line)
|
|
let llen: Int = native_list_len(line_chars)
|
|
let wlen: Int = str_len(word)
|
|
let off: Int = 0
|
|
let scanning: Bool = true
|
|
while scanning {
|
|
let idx: Int = str_index_of(str_slice(line, off, str_len(line)), word)
|
|
if idx < 0 {
|
|
let scanning = false
|
|
} else {
|
|
let abs: Int = off + idx
|
|
// Word-boundary check
|
|
let before_ok: Bool = true
|
|
if abs > 0 {
|
|
let prev: String = native_list_get(line_chars, abs - 1)
|
|
if is_ident_ch(prev) { let before_ok = false }
|
|
}
|
|
let after_ok: Bool = true
|
|
let after_pos: Int = abs + wlen
|
|
if after_pos < llen {
|
|
let nxt: String = native_list_get(line_chars, after_pos)
|
|
if is_ident_ch(nxt) { let after_ok = false }
|
|
}
|
|
if before_ok {
|
|
if after_ok {
|
|
let parts: [String] = native_list_empty()
|
|
let parts = native_list_append(parts, json_kv_int("line", i + 1))
|
|
let parts = native_list_append(parts, json_kv_int("col", abs + 1))
|
|
let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line)))
|
|
let refs = native_list_append(refs, "{" + list_join(parts, ",") + "}")
|
|
}
|
|
}
|
|
let off = abs + wlen
|
|
if off >= str_len(line) { let scanning = false }
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
json_array_of(refs)
|
|
}
|
|
|
|
// ── JSON-RPC dispatch ────────────────────────────────────────────────────────
|
|
//
|
|
// Method names follow LSP-ish conventions but stay aligned with the Rust crate's
|
|
// HTTP surface. Params are a JSON string the dispatcher pulls fields out of via
|
|
// json_get_string / json_get_int.
|
|
//
|
|
// "textDocument/completion" → params { source, pos }
|
|
// "textDocument/hover" → params { source, pos }
|
|
// "textDocument/diagnostic" → params { source }
|
|
// "textDocument/formatting" → params { source }
|
|
// "textDocument/documentSymbol"→ params { source }
|
|
// "textDocument/definition" → params { source, pos }
|
|
// "textDocument/references" → params { source, pos }
|
|
// "el/typeGraph" → params { source }
|
|
|
|
fn lsp_jsonrpc(method: String, params_json: String) -> String {
|
|
let source: String = json_get_string(params_json, "source")
|
|
let pos: Int = json_get_int(params_json, "pos")
|
|
if str_eq(method, "textDocument/completion") { return lsp_complete(source, pos) }
|
|
if str_eq(method, "textDocument/hover") { return lsp_hover(source, pos) }
|
|
if str_eq(method, "textDocument/diagnostic") { return lsp_diagnostics(source) }
|
|
if str_eq(method, "textDocument/formatting") { return lsp_format(source) }
|
|
if str_eq(method, "textDocument/documentSymbol") { return lsp_outline(source) }
|
|
if str_eq(method, "textDocument/definition") { return lsp_definition(source, pos) }
|
|
if str_eq(method, "textDocument/references") { return lsp_references(source, pos) }
|
|
if str_eq(method, "el/typeGraph") { return lsp_type_graph(source) }
|
|
"{\"error\":\"unknown method: " + json_escape(method) + "\"}"
|
|
}
|
|
|
|
// ── stdio loop (best-effort; see runtime gap note in the report) ─────────────
|
|
//
|
|
// LSP framing is `Content-Length: N\r\n\r\n` then exactly N bytes. The El
|
|
// runtime exposes only line-buffered `readline()`, which strips `\n` and is
|
|
// hostile to the binary body read. We approximate by reading lines until a
|
|
// blank line ends the headers, then reading exactly one more line as the
|
|
// body (works for compact, no-newline payloads — i.e. minified JSON-RPC).
|
|
// A dedicated `read_n(bytes)` runtime primitive would unblock the strict
|
|
// implementation. See report.
|
|
|
|
fn read_headers() -> Map<String, Any> {
|
|
let content_len: Int = 0
|
|
let scanning: Bool = true
|
|
while scanning {
|
|
let line: String = readline()
|
|
if str_eq(line, "") {
|
|
let scanning = false
|
|
} else {
|
|
let cl_marker: String = "Content-Length:"
|
|
if str_starts_with(line, cl_marker) {
|
|
let val: String = str_trim(str_slice(line, str_len(cl_marker), str_len(line)))
|
|
let content_len = str_to_int(val)
|
|
}
|
|
}
|
|
}
|
|
{ "content_length": content_len }
|
|
}
|
|
|
|
fn lsp_stdio_loop() -> Void {
|
|
let running: Bool = true
|
|
while running {
|
|
let headers: Map<String, Any> = read_headers()
|
|
let body: String = readline()
|
|
if str_eq(body, "") {
|
|
let running = false
|
|
} else {
|
|
let method: String = json_get_string(body, "method")
|
|
let id_val: String = json_get_raw(body, "id")
|
|
if str_eq(id_val, "") { let id_val = "null" }
|
|
let params_raw: String = json_get_raw(body, "params")
|
|
let result: String = lsp_jsonrpc(method, params_raw)
|
|
let resp: String = "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}"
|
|
let frame: String = "Content-Length: " + int_to_str(str_len(resp)) + "\r\n\r\n" + resp
|
|
print(frame)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Entry ────────────────────────────────────────────────────────────────────
|
|
//
|
|
// When invoked as a standalone binary, default to stdio JSON-RPC so editors
|
|
// that drive the language server directly (without the HTTP wrapper) work
|
|
// out of the box. Pass a single CLI arg to short-circuit:
|
|
// ./el-lsp probe → emits a self-test diagnostic + exits
|
|
// ./el-lsp version → emits version + exits
|
|
|
|
fn lsp_main() -> Void {
|
|
let argv: [String] = args()
|
|
let n: Int = native_list_len(argv)
|
|
if n > 0 {
|
|
let cmd: String = native_list_get(argv, 0)
|
|
if str_eq(cmd, "version") {
|
|
println("el-lsp 0.1.0")
|
|
return
|
|
}
|
|
if str_eq(cmd, "probe") {
|
|
let sample: String = "fn main() -> Void { let x: Int = 42 }"
|
|
println(lsp_diagnostics(sample))
|
|
return
|
|
}
|
|
}
|
|
lsp_stdio_loop()
|
|
}
|
|
|
|
lsp_main()
|