71a1e41f93
When a document is opened or changed, scan for `type Name { field: Type }` blocks
and `let var: Type` / `fn foo(param: Type)` annotations. On completion requests,
if the text before the cursor ends with `identifier.`, look up the variable's type
and return its fields as Field (kind=5) completion items instead of the full list.
1159 lines
57 KiB
EmacsLisp
1159 lines
57 KiB
EmacsLisp
// tools/lsp/el-lsp.el — Full El Language Server Protocol implementation
|
|
//
|
|
// Implements LSP over JSON-RPC 2.0 with Content-Length framing on stdin/stdout.
|
|
//
|
|
// Features:
|
|
// - textDocument/completion — builtins (full list) + user fn names + keywords
|
|
// - textDocument/hover — signatures + descriptions for all builtins
|
|
// - textDocument/definition — go-to-def for user-defined fns
|
|
// - textDocument/didOpen/didChange/didClose/didSave — full document sync
|
|
// - textDocument/publishDiagnostics — unclosed braces/parens, unterminated strings
|
|
// - initialize / shutdown / exit — clean lifecycle
|
|
//
|
|
// Requires __read_n(n: Int) -> String in el_runtime.c (see build.sh).
|
|
// Requires __print_raw(s: String) in el_runtime.c for framed writes.
|
|
// Build: tools/lsp/build.sh
|
|
|
|
// ── JSON-RPC framing ──────────────────────────────────────────────────────
|
|
|
|
// Read a Content-Length framed JSON-RPC message from stdin.
|
|
fn lsp_read_message() -> String {
|
|
let header_buf: String = ""
|
|
let found_end: Bool = false
|
|
let max_header: Int = 8192
|
|
let hi: Int = 0
|
|
while !found_end {
|
|
if hi >= max_header {
|
|
return ""
|
|
}
|
|
let ch: String = __read_n(1)
|
|
if str_eq(ch, "") {
|
|
return ""
|
|
}
|
|
let header_buf = header_buf + ch
|
|
let hlen: Int = str_len(header_buf)
|
|
if hlen >= 4 {
|
|
let tail: String = str_slice(header_buf, hlen - 4, hlen)
|
|
if str_eq(tail, "\r\n\r\n") {
|
|
let found_end = true
|
|
}
|
|
}
|
|
let hi = hi + 1
|
|
}
|
|
let cl_key: String = "Content-Length: "
|
|
let cl_pos: Int = str_index_of(header_buf, cl_key)
|
|
if cl_pos < 0 {
|
|
return ""
|
|
}
|
|
let after: String = str_slice(header_buf, cl_pos + str_len(cl_key), str_len(header_buf))
|
|
let cr_pos: Int = str_index_of(after, "\r")
|
|
let num_str: String = ""
|
|
if cr_pos >= 0 {
|
|
let num_str = str_slice(after, 0, cr_pos)
|
|
} else {
|
|
let nl_pos: Int = str_index_of(after, "\n")
|
|
if nl_pos >= 0 {
|
|
let num_str = str_slice(after, 0, nl_pos)
|
|
}
|
|
}
|
|
let body_len: Int = str_to_int(str_trim(num_str))
|
|
if body_len <= 0 {
|
|
return ""
|
|
}
|
|
__read_n(body_len)
|
|
}
|
|
|
|
// Write a Content-Length framed JSON-RPC message to stdout.
|
|
fn lsp_write_message(body: String) -> Void {
|
|
let len: Int = str_len(body)
|
|
__print_raw("Content-Length: " + int_to_str(len) + "\r\n\r\n" + body)
|
|
}
|
|
|
|
// ── JSON helpers ──────────────────────────────────────────────────────────
|
|
|
|
fn lsp_json_escape(s: String) -> String {
|
|
let s = str_replace(s, "\\", "\\\\")
|
|
let s = str_replace(s, "\"", "\\\"")
|
|
let s = str_replace(s, "\n", "\\n")
|
|
let s = str_replace(s, "\r", "\\r")
|
|
let s = str_replace(s, "\t", "\\t")
|
|
s
|
|
}
|
|
|
|
// ── JSON-RPC envelope builders ─────────────────────────────────────────────
|
|
|
|
fn lsp_response(id: String, result_json: String) -> String {
|
|
"{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":" + result_json + "}"
|
|
}
|
|
|
|
fn lsp_error_response(id: String, code: Int, message: String) -> String {
|
|
"{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"code\":" + int_to_str(code) + ",\"message\":\"" + lsp_json_escape(message) + "\"}}"
|
|
}
|
|
|
|
fn lsp_notification(method: String, params_json: String) -> String {
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"params\":" + params_json + "}"
|
|
}
|
|
|
|
// ── Document store ─────────────────────────────────────────────────────────
|
|
|
|
fn lsp_doc_key(uri: String) -> String {
|
|
"doc:" + uri
|
|
}
|
|
|
|
fn lsp_doc_store(uri: String, text: String) -> Void {
|
|
state_set(lsp_doc_key(uri), text)
|
|
}
|
|
|
|
fn lsp_doc_get(uri: String) -> String {
|
|
state_get(lsp_doc_key(uri))
|
|
}
|
|
|
|
fn lsp_doc_remove(uri: String) -> Void {
|
|
state_del(lsp_doc_key(uri))
|
|
}
|
|
|
|
// ── Builtin catalog ───────────────────────────────────────────────────────
|
|
// Format: "name|signature|description"
|
|
|
|
fn lsp_builtin_catalog() -> [String] {
|
|
let c: [String] = native_list_empty()
|
|
let c = native_list_append(c, "println|println(s: String) -> Void|Print s followed by a newline to stdout.")
|
|
let c = native_list_append(c, "print|print(s: String) -> Void|Print s to stdout without a newline.")
|
|
let c = native_list_append(c, "readline|readline() -> String|Read one line from stdin (newline stripped).")
|
|
let c = native_list_append(c, "str_eq|str_eq(a: String, b: String) -> Bool|Return true if a equals b.")
|
|
let c = native_list_append(c, "str_len|str_len(s: String) -> Int|Return the byte length of s.")
|
|
let c = native_list_append(c, "str_concat|str_concat(a: String, b: String) -> String|Concatenate a and b.")
|
|
let c = native_list_append(c, "str_slice|str_slice(s: String, start: Int, end: Int) -> String|Substring [start, end).")
|
|
let c = native_list_append(c, "str_contains|str_contains(s: String, sub: String) -> Bool|True if s contains sub.")
|
|
let c = native_list_append(c, "str_starts_with|str_starts_with(s: String, prefix: String) -> Bool|True if s starts with prefix.")
|
|
let c = native_list_append(c, "str_ends_with|str_ends_with(s: String, suffix: String) -> Bool|True if s ends with suffix.")
|
|
let c = native_list_append(c, "str_replace|str_replace(s: String, from: String, to: String) -> String|Replace all occurrences of from with to.")
|
|
let c = native_list_append(c, "str_to_upper|str_to_upper(s: String) -> String|Convert to uppercase.")
|
|
let c = native_list_append(c, "str_to_lower|str_to_lower(s: String) -> String|Convert to lowercase.")
|
|
let c = native_list_append(c, "str_trim|str_trim(s: String) -> String|Strip leading and trailing whitespace.")
|
|
let c = native_list_append(c, "str_lstrip|str_lstrip(s: String) -> String|Strip leading whitespace.")
|
|
let c = native_list_append(c, "str_rstrip|str_rstrip(s: String) -> String|Strip trailing whitespace.")
|
|
let c = native_list_append(c, "str_index_of|str_index_of(s: String, sub: String) -> Int|First byte offset of sub, or -1.")
|
|
let c = native_list_append(c, "str_last_index_of|str_last_index_of(s: String, sub: String) -> Int|Last byte offset of sub, or -1.")
|
|
let c = native_list_append(c, "str_split|str_split(s: String, sep: String) -> [String]|Split on sep.")
|
|
let c = native_list_append(c, "str_split_lines|str_split_lines(s: String) -> [String]|Split into lines.")
|
|
let c = native_list_append(c, "str_split_n|str_split_n(s: String, sep: String, n: Int) -> [String]|Split at most n times.")
|
|
let c = native_list_append(c, "str_join|str_join(parts: [String], sep: String) -> String|Join list with sep.")
|
|
let c = native_list_append(c, "str_char_at|str_char_at(s: String, i: Int) -> String|Single character at byte index i.")
|
|
let c = native_list_append(c, "str_char_code|str_char_code(s: String, i: Int) -> Int|Byte value at index i.")
|
|
let c = native_list_append(c, "str_pad_left|str_pad_left(s: String, width: Int, pad: String) -> String|Pad on the left to width.")
|
|
let c = native_list_append(c, "str_pad_right|str_pad_right(s: String, width: Int, pad: String) -> String|Pad on the right to width.")
|
|
let c = native_list_append(c, "str_repeat|str_repeat(s: String, n: Int) -> String|Repeat s n times.")
|
|
let c = native_list_append(c, "str_reverse|str_reverse(s: String) -> String|Reverse by codepoint.")
|
|
let c = native_list_append(c, "str_strip_prefix|str_strip_prefix(s: String, prefix: String) -> String|Remove prefix if present.")
|
|
let c = native_list_append(c, "str_strip_suffix|str_strip_suffix(s: String, suffix: String) -> String|Remove suffix if present.")
|
|
let c = native_list_append(c, "str_strip_chars|str_strip_chars(s: String, chars: String) -> String|Strip chars from both ends.")
|
|
let c = native_list_append(c, "str_count|str_count(s: String, sub: String) -> Int|Count non-overlapping occurrences.")
|
|
let c = native_list_append(c, "str_count_lines|str_count_lines(s: String) -> Int|Number of lines.")
|
|
let c = native_list_append(c, "str_find_chars|str_find_chars(s: String, any_of: String) -> Int|First index of any char in any_of.")
|
|
let c = native_list_append(c, "str_index_of_all|str_index_of_all(s: String, sub: String) -> [Int]|All byte offsets of sub.")
|
|
let c = native_list_append(c, "str_upper|str_upper(s: String) -> String|Convert to uppercase (alias).")
|
|
let c = native_list_append(c, "str_lower|str_lower(s: String) -> String|Convert to lowercase (alias).")
|
|
let c = native_list_append(c, "is_letter|is_letter(s: String) -> Bool|All ASCII letters.")
|
|
let c = native_list_append(c, "is_digit|is_digit(s: String) -> Bool|All ASCII digits.")
|
|
let c = native_list_append(c, "is_alphanumeric|is_alphanumeric(s: String) -> Bool|All ASCII alphanumeric.")
|
|
let c = native_list_append(c, "is_whitespace|is_whitespace(s: String) -> Bool|All whitespace.")
|
|
let c = native_list_append(c, "is_uppercase|is_uppercase(s: String) -> Bool|All uppercase ASCII.")
|
|
let c = native_list_append(c, "is_lowercase|is_lowercase(s: String) -> Bool|All lowercase ASCII.")
|
|
let c = native_list_append(c, "int_to_str|int_to_str(n: Int) -> String|Integer to decimal string.")
|
|
let c = native_list_append(c, "str_to_int|str_to_int(s: String) -> Int|Parse decimal string to integer.")
|
|
let c = native_list_append(c, "float_to_str|float_to_str(f: Float) -> String|Float to string.")
|
|
let c = native_list_append(c, "str_to_float|str_to_float(s: String) -> Float|Parse string to float.")
|
|
let c = native_list_append(c, "int_to_float|int_to_float(n: Int) -> Float|Integer to float.")
|
|
let c = native_list_append(c, "float_to_int|float_to_int(f: Float) -> Int|Truncate float to integer.")
|
|
let c = native_list_append(c, "format_float|format_float(f: Float, decimals: Int) -> String|Fixed decimal places.")
|
|
let c = native_list_append(c, "decimal_round|decimal_round(f: Float, decimals: Int) -> Float|Round to N decimal places.")
|
|
let c = native_list_append(c, "bool_to_str|bool_to_str(b: Bool) -> String|Bool to \"true\" or \"false\".")
|
|
let c = native_list_append(c, "el_abs|el_abs(n: Int) -> Int|Absolute value.")
|
|
let c = native_list_append(c, "el_max|el_max(a: Int, b: Int) -> Int|Maximum of two integers.")
|
|
let c = native_list_append(c, "el_min|el_min(a: Int, b: Int) -> Int|Minimum of two integers.")
|
|
let c = native_list_append(c, "math_sqrt|math_sqrt(f: Float) -> Float|Square root.")
|
|
let c = native_list_append(c, "math_log|math_log(f: Float) -> Float|Base-10 logarithm.")
|
|
let c = native_list_append(c, "math_ln|math_ln(f: Float) -> Float|Natural logarithm.")
|
|
let c = native_list_append(c, "math_sin|math_sin(f: Float) -> Float|Sine (radians).")
|
|
let c = native_list_append(c, "math_cos|math_cos(f: Float) -> Float|Cosine (radians).")
|
|
let c = native_list_append(c, "math_pi|math_pi() -> Float|Value of pi.")
|
|
let c = native_list_append(c, "el_list_empty|el_list_empty() -> [Any]|Create an empty list.")
|
|
let c = native_list_append(c, "el_list_append|el_list_append(list: [Any], elem: Any) -> [Any]|Append and return updated list.")
|
|
let c = native_list_append(c, "el_list_len|el_list_len(list: [Any]) -> Int|Number of elements.")
|
|
let c = native_list_append(c, "el_list_get|el_list_get(list: [Any], index: Int) -> Any|Element at index.")
|
|
let c = native_list_append(c, "el_list_clone|el_list_clone(list: [Any]) -> [Any]|Shallow copy.")
|
|
let c = native_list_append(c, "list_push|list_push(list: [Any], elem: Any) -> [Any]|Append elem (alias).")
|
|
let c = native_list_append(c, "list_push_front|list_push_front(list: [Any], elem: Any) -> [Any]|Prepend elem.")
|
|
let c = native_list_append(c, "list_join|list_join(list: [Any], sep: String) -> String|Join list with sep.")
|
|
let c = native_list_append(c, "list_range|list_range(start: Int, end: Int) -> [Int]|Integers from start to end-1.")
|
|
let c = native_list_append(c, "el_map_get|el_map_get(map: Map<String,Any>, key: String) -> Any|Get by key.")
|
|
let c = native_list_append(c, "el_map_set|el_map_set(map: Map<String,Any>, key: String, value: Any) -> Map<String,Any>|Set key.")
|
|
let c = native_list_append(c, "el_get_field|el_get_field(map: Map<String,Any>, key: String) -> Any|Get field.")
|
|
let c = native_list_append(c, "state_set|state_set(key: String, value: String) -> Void|Store in process state.")
|
|
let c = native_list_append(c, "state_get|state_get(key: String) -> String|Retrieve from process state, empty if absent.")
|
|
let c = native_list_append(c, "state_del|state_del(key: String) -> Void|Delete from process state.")
|
|
let c = native_list_append(c, "state_keys|state_keys() -> [String]|All keys in process state.")
|
|
let c = native_list_append(c, "json_get|json_get(json: String, key: String) -> String|Get string field from JSON object.")
|
|
let c = native_list_append(c, "json_get_string|json_get_string(json: String, key: String) -> String|Get string field.")
|
|
let c = native_list_append(c, "json_get_int|json_get_int(json: String, key: String) -> Int|Get integer field.")
|
|
let c = native_list_append(c, "json_get_float|json_get_float(json: String, key: String) -> Float|Get float field.")
|
|
let c = native_list_append(c, "json_get_bool|json_get_bool(json: String, key: String) -> Bool|Get boolean field.")
|
|
let c = native_list_append(c, "json_get_raw|json_get_raw(json: String, key: String) -> String|Get raw JSON fragment.")
|
|
let c = native_list_append(c, "json_set|json_set(json: String, key: String, value: String) -> String|Set key in JSON object.")
|
|
let c = native_list_append(c, "json_parse|json_parse(s: String) -> String|Parse and validate JSON.")
|
|
let c = native_list_append(c, "json_stringify|json_stringify(v: Any) -> String|Serialize to JSON.")
|
|
let c = native_list_append(c, "json_array_len|json_array_len(json: String) -> Int|JSON array length.")
|
|
let c = native_list_append(c, "json_array_get|json_array_get(json: String, index: Int) -> String|JSON array element.")
|
|
let c = native_list_append(c, "json_array_get_string|json_array_get_string(json: String, index: Int) -> String|String element at index.")
|
|
let c = native_list_append(c, "fs_read|fs_read(path: String) -> String|Read file contents.")
|
|
let c = native_list_append(c, "fs_write|fs_write(path: String, content: String) -> Bool|Write file.")
|
|
let c = native_list_append(c, "fs_list|fs_list(path: String) -> [String]|List directory entries.")
|
|
let c = native_list_append(c, "fs_exists|fs_exists(path: String) -> Bool|True if path exists.")
|
|
let c = native_list_append(c, "fs_mkdir|fs_mkdir(path: String) -> Bool|Create directory (mkdir -p).")
|
|
let c = native_list_append(c, "http_get|http_get(url: String) -> String|HTTP GET, return response body.")
|
|
let c = native_list_append(c, "http_post|http_post(url: String, body: String) -> String|HTTP POST with plain body.")
|
|
let c = native_list_append(c, "http_post_json|http_post_json(url: String, json_body: String) -> String|HTTP POST with JSON.")
|
|
let c = native_list_append(c, "http_get_with_headers|http_get_with_headers(url: String, headers: Map<String,Any>) -> String|GET with headers.")
|
|
let c = native_list_append(c, "http_post_with_headers|http_post_with_headers(url: String, body: String, headers: Map<String,Any>) -> String|POST with headers.")
|
|
let c = native_list_append(c, "http_serve|http_serve(port: Int, handler: Any) -> Void|Start HTTP server.")
|
|
let c = native_list_append(c, "http_serve_v2|http_serve_v2(port: Int, handler: Any) -> Void|Start HTTP server v2 (method,path,headers,body).")
|
|
let c = native_list_append(c, "http_response|http_response(status: Int, headers_json: String, body: String) -> String|Build HTTP response envelope.")
|
|
let c = native_list_append(c, "url_encode|url_encode(s: String) -> String|URL-encode (RFC 3986).")
|
|
let c = native_list_append(c, "url_decode|url_decode(s: String) -> String|URL-decode.")
|
|
let c = native_list_append(c, "time_now|time_now() -> String|Current local time as ISO 8601.")
|
|
let c = native_list_append(c, "time_now_utc|time_now_utc() -> String|Current UTC time as ISO 8601.")
|
|
let c = native_list_append(c, "sleep_secs|sleep_secs(secs: Int) -> Void|Sleep N seconds.")
|
|
let c = native_list_append(c, "sleep_ms|sleep_ms(ms: Int) -> Void|Sleep N milliseconds.")
|
|
let c = native_list_append(c, "time_format|time_format(ts: String, fmt: String) -> String|Format timestamp.")
|
|
let c = native_list_append(c, "time_add|time_add(ts: String, n: Int, unit: String) -> String|Add time to timestamp.")
|
|
let c = native_list_append(c, "time_diff|time_diff(ts1: String, ts2: String, unit: String) -> Int|Diff two timestamps.")
|
|
let c = native_list_append(c, "now|now() -> Instant|Current time as Instant.")
|
|
let c = native_list_append(c, "unix_seconds|unix_seconds(n: Int) -> Instant|Instant from Unix seconds.")
|
|
let c = native_list_append(c, "unix_millis|unix_millis(n: Int) -> Instant|Instant from Unix milliseconds.")
|
|
let c = native_list_append(c, "uuid_new|uuid_new() -> String|Random UUID v4.")
|
|
let c = native_list_append(c, "uuid_v4|uuid_v4() -> String|Random UUID v4 (alias).")
|
|
let c = native_list_append(c, "env|env(key: String) -> String|Read environment variable.")
|
|
let c = native_list_append(c, "args|args() -> [String]|Command-line arguments.")
|
|
let c = native_list_append(c, "exit_program|exit_program(code: Int) -> Void|Exit with code.")
|
|
let c = native_list_append(c, "exec_command|exec_command(cmd: String) -> Int|Run shell command, return exit code.")
|
|
let c = native_list_append(c, "exec_capture|exec_capture(cmd: String) -> String|Run shell command, capture stdout.")
|
|
let c = native_list_append(c, "sha256_hex|sha256_hex(input: String) -> String|SHA-256 as lowercase hex.")
|
|
let c = native_list_append(c, "sha256_bytes|sha256_bytes(input: String) -> String|SHA-256 as raw bytes.")
|
|
let c = native_list_append(c, "hmac_sha256_hex|hmac_sha256_hex(key: String, message: String) -> String|HMAC-SHA-256 hex.")
|
|
let c = native_list_append(c, "base64_encode|base64_encode(input: String) -> String|Base64 encode.")
|
|
let c = native_list_append(c, "base64_decode|base64_decode(input: String) -> String|Base64 decode.")
|
|
let c = native_list_append(c, "base64url_encode|base64url_encode(input: String) -> String|URL-safe base64 encode.")
|
|
let c = native_list_append(c, "base64url_decode|base64url_decode(input: String) -> String|URL-safe base64 decode.")
|
|
let c = native_list_append(c, "llm_call|llm_call(model: String, prompt: String) -> String|LLM with user prompt.")
|
|
let c = native_list_append(c, "llm_call_system|llm_call_system(model: String, system: String, user: String) -> String|LLM with system + user.")
|
|
let c = native_list_append(c, "llm_call_agentic|llm_call_agentic(model: String, system: String, user: String, tools: String) -> String|Agentic LLM.")
|
|
let c = native_list_append(c, "llm_vision|llm_vision(model: String, system: String, prompt: String, image: String) -> String|LLM with image.")
|
|
let c = native_list_append(c, "llm_models|llm_models() -> String|List available LLM models.")
|
|
let c = native_list_append(c, "llm_register_tool|llm_register_tool(name: String, handler_fn: String) -> Void|Register tool.")
|
|
let c = native_list_append(c, "engram_node|engram_node(content: String, node_type: String, salience: Float) -> String|Create engram node.")
|
|
let c = native_list_append(c, "engram_get_node|engram_get_node(id: String) -> Map<String,Any>|Get node by ID.")
|
|
let c = native_list_append(c, "engram_strengthen|engram_strengthen(node_id: String) -> Void|Increase salience.")
|
|
let c = native_list_append(c, "engram_forget|engram_forget(node_id: String) -> Void|Remove node.")
|
|
let c = native_list_append(c, "engram_search|engram_search(query: String, limit: Int) -> [Map<String,Any>]|Semantic search.")
|
|
let c = native_list_append(c, "engram_connect|engram_connect(from: String, to: String, weight: Float, rel: String) -> Void|Connect nodes.")
|
|
let c = native_list_append(c, "engram_activate|engram_activate(query: String, depth: Int) -> [Map<String,Any>]|Activate nodes.")
|
|
let c = native_list_append(c, "engram_save|engram_save(path: String) -> Bool|Save to file.")
|
|
let c = native_list_append(c, "engram_load|engram_load(path: String) -> Bool|Load from file.")
|
|
let c = native_list_append(c, "engram_node_count|engram_node_count() -> Int|Total nodes.")
|
|
let c = native_list_append(c, "engram_edge_count|engram_edge_count() -> Int|Total edges.")
|
|
let c = native_list_append(c, "engram_neighbors|engram_neighbors(node_id: String) -> [Map<String,Any>]|Neighboring nodes.")
|
|
let c = native_list_append(c, "engram_search_json|engram_search_json(query: String, limit: Int) -> String|Search as JSON.")
|
|
let c = native_list_append(c, "engram_stats_json|engram_stats_json() -> String|Graph stats as JSON.")
|
|
let c = native_list_append(c, "dharma_connect|dharma_connect(cgi_id: String) -> Bool|Connect to peer CGI.")
|
|
let c = native_list_append(c, "dharma_send|dharma_send(channel: String, content: String) -> String|Send to channel.")
|
|
let c = native_list_append(c, "dharma_activate|dharma_activate(query: String) -> String|Activate across network.")
|
|
let c = native_list_append(c, "dharma_emit|dharma_emit(event_type: String, payload: String) -> Void|Emit network event.")
|
|
let c = native_list_append(c, "dharma_field|dharma_field(event_type: String) -> String|Block until event.")
|
|
let c = native_list_append(c, "dharma_peers|dharma_peers() -> [String]|Connected peer IDs.")
|
|
let c = native_list_append(c, "native_list_empty|native_list_empty() -> [Any]|Empty list (VM alias).")
|
|
let c = native_list_append(c, "native_list_append|native_list_append(list: [Any], elem: Any) -> [Any]|Append (VM alias).")
|
|
let c = native_list_append(c, "native_list_len|native_list_len(list: [Any]) -> Int|Length (VM alias).")
|
|
let c = native_list_append(c, "native_list_get|native_list_get(list: [Any], i: Int) -> Any|Get element (VM alias).")
|
|
let c = native_list_append(c, "native_list_clone|native_list_clone(list: [Any]) -> [Any]|Clone (VM alias).")
|
|
let c = native_list_append(c, "native_string_chars|native_string_chars(s: String) -> [String]|Split into chars.")
|
|
let c = native_list_append(c, "native_int_to_str|native_int_to_str(n: Int) -> String|Int to string (VM alias).")
|
|
c
|
|
}
|
|
|
|
// Find a builtin catalog entry by name. Returns "name|sig|desc" or "".
|
|
fn lsp_builtin_find(name: String, catalog: [String]) -> String {
|
|
let n: Int = native_list_len(catalog)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let entry: String = native_list_get(catalog, i)
|
|
let bar: Int = str_index_of(entry, "|")
|
|
if bar >= 0 {
|
|
let ename: String = str_slice(entry, 0, bar)
|
|
if str_eq(ename, name) {
|
|
return entry
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
""
|
|
}
|
|
|
|
// ── Type registry & variable type map ─────────────────────────────────────
|
|
|
|
// Key helpers for type registry and vartype map in process state.
|
|
fn lsp_types_key(uri: String, type_name: String) -> String {
|
|
"types:" + uri + ":" + type_name
|
|
}
|
|
|
|
fn lsp_vartypes_key(uri: String) -> String {
|
|
"vartypes:" + uri
|
|
}
|
|
|
|
// Scan source for `type Name { field: Type, ... }` declarations.
|
|
// Stores each type's fields in state as "field1:Type1|field2:Type2|...".
|
|
fn lsp_scan_types(uri: String, source: String) -> Void {
|
|
let lines: [String] = str_split_lines(source)
|
|
let n: Int = native_list_len(lines)
|
|
let i: Int = 0
|
|
let in_type: Bool = false
|
|
let cur_type: String = ""
|
|
let fields: String = ""
|
|
while i < n {
|
|
let line: String = str_trim(native_list_get(lines, i))
|
|
if in_type {
|
|
// Check for closing brace (end of type block)
|
|
if str_starts_with(line, "}") {
|
|
// Save accumulated fields for cur_type
|
|
state_set(lsp_types_key(uri, cur_type), fields)
|
|
let in_type = false
|
|
let cur_type = ""
|
|
let fields = ""
|
|
} else {
|
|
// Try to parse "fieldName: TypeName," or "fieldName: TypeName"
|
|
let colon_pos: Int = str_index_of(line, ":")
|
|
if colon_pos > 0 {
|
|
let raw_name: String = str_trim(str_slice(line, 0, colon_pos))
|
|
let after_colon: String = str_trim(str_slice(line, colon_pos + 1, str_len(line)))
|
|
// Strip trailing comma
|
|
let type_name: String = after_colon
|
|
let tlen: Int = str_len(after_colon)
|
|
if tlen > 0 {
|
|
let last_ch: String = str_slice(after_colon, tlen - 1, tlen)
|
|
if str_eq(last_ch, ",") {
|
|
let type_name = str_trim(str_slice(after_colon, 0, tlen - 1))
|
|
}
|
|
}
|
|
// Validate field name (non-empty, starts with letter or _)
|
|
if str_len(raw_name) > 0 {
|
|
let fc: Int = str_char_code(raw_name, 0)
|
|
let valid_start: Bool = false
|
|
if fc >= 97 { if fc <= 122 { let valid_start = true } }
|
|
if fc >= 65 { if fc <= 90 { let valid_start = true } }
|
|
if fc == 95 { let valid_start = true }
|
|
if valid_start {
|
|
if str_len(fields) > 0 {
|
|
let fields = fields + "|"
|
|
}
|
|
let fields = fields + raw_name + ":" + type_name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Look for "type Name {" pattern
|
|
if str_starts_with(line, "type ") {
|
|
let after: String = str_slice(line, 5, str_len(line))
|
|
let brace_pos: Int = str_index_of(after, "{")
|
|
if brace_pos > 0 {
|
|
let tname: String = str_trim(str_slice(after, 0, brace_pos))
|
|
if str_len(tname) > 0 {
|
|
let in_type = true
|
|
let cur_type = tname
|
|
let fields = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
}
|
|
|
|
// Look up fields for a type name. Returns "field1:Type1|field2:Type2|..." or "".
|
|
fn lsp_get_type_fields(uri: String, type_name: String) -> String {
|
|
state_get(lsp_types_key(uri, type_name))
|
|
}
|
|
|
|
// Parse a single vartype entry "varName:TypeName" — extract the var name.
|
|
fn lsp_varentry_name(entry: String) -> String {
|
|
let colon: Int = str_index_of(entry, ":")
|
|
if colon <= 0 {
|
|
return ""
|
|
}
|
|
str_slice(entry, 0, colon)
|
|
}
|
|
|
|
// Parse a single vartype entry "varName:TypeName" — extract the type name.
|
|
fn lsp_varentry_type(entry: String) -> String {
|
|
let colon: Int = str_index_of(entry, ":")
|
|
if colon < 0 {
|
|
return ""
|
|
}
|
|
str_slice(entry, colon + 1, str_len(entry))
|
|
}
|
|
|
|
// Scan source for variable type annotations:
|
|
// let varName: TypeName = ...
|
|
// fn foo(varName: TypeName, ...) -> RetType {
|
|
// Stores in state as "var1:Type1|var2:Type2|...".
|
|
fn lsp_scan_vartypes(uri: String, source: String) -> Void {
|
|
let lines: [String] = str_split_lines(source)
|
|
let n: Int = native_list_len(lines)
|
|
let i: Int = 0
|
|
let entries: String = ""
|
|
while i < n {
|
|
let line: String = str_trim(native_list_get(lines, i))
|
|
// Handle "let varName: TypeName" declarations
|
|
if str_starts_with(line, "let ") {
|
|
let after_let: String = str_slice(line, 4, str_len(line))
|
|
let colon_pos: Int = str_index_of(after_let, ":")
|
|
if colon_pos > 0 {
|
|
let var_name: String = str_trim(str_slice(after_let, 0, colon_pos))
|
|
let after_colon: String = str_trim(str_slice(after_let, colon_pos + 1, str_len(after_let)))
|
|
// Extract type — stop at '=' or whitespace
|
|
let eq_pos: Int = str_index_of(after_colon, "=")
|
|
let raw_type: String = after_colon
|
|
if eq_pos > 0 {
|
|
let raw_type = str_trim(str_slice(after_colon, 0, eq_pos))
|
|
} else {
|
|
let sp_pos: Int = str_index_of(after_colon, " ")
|
|
if sp_pos > 0 {
|
|
let raw_type = str_trim(str_slice(after_colon, 0, sp_pos))
|
|
}
|
|
}
|
|
if str_len(var_name) > 0 {
|
|
if str_len(raw_type) > 0 {
|
|
if str_len(entries) > 0 {
|
|
let entries = entries + "|"
|
|
}
|
|
let entries = entries + var_name + ":" + raw_type
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Handle "fn name(param: Type, ...)" — extract each parameter's type annotation
|
|
if str_starts_with(line, "fn ") {
|
|
let paren_open: Int = str_index_of(line, "(")
|
|
let paren_close: Int = str_last_index_of(line, ")")
|
|
if paren_open > 0 {
|
|
if paren_close > paren_open {
|
|
let params_str: String = str_slice(line, paren_open + 1, paren_close)
|
|
// Split by comma to get each param
|
|
let params: [String] = str_split(params_str, ",")
|
|
let pn: Int = native_list_len(params)
|
|
let pi: Int = 0
|
|
while pi < pn {
|
|
let param: String = str_trim(native_list_get(params, pi))
|
|
let pc: Int = str_index_of(param, ":")
|
|
if pc > 0 {
|
|
let pname: String = str_trim(str_slice(param, 0, pc))
|
|
let ptype: String = str_trim(str_slice(param, pc + 1, str_len(param)))
|
|
if str_len(pname) > 0 {
|
|
if str_len(ptype) > 0 {
|
|
if str_len(entries) > 0 {
|
|
let entries = entries + "|"
|
|
}
|
|
let entries = entries + pname + ":" + ptype
|
|
}
|
|
}
|
|
}
|
|
let pi = pi + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
state_set(lsp_vartypes_key(uri), entries)
|
|
}
|
|
|
|
// Look up a variable's type. Returns type name or "".
|
|
fn lsp_get_var_type(uri: String, var_name: String) -> String {
|
|
let entries: String = state_get(lsp_vartypes_key(uri))
|
|
if str_eq(entries, "") {
|
|
return ""
|
|
}
|
|
let parts: [String] = str_split(entries, "|")
|
|
let n: Int = native_list_len(parts)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let entry: String = native_list_get(parts, i)
|
|
let ename: String = lsp_varentry_name(entry)
|
|
if str_eq(ename, var_name) {
|
|
return lsp_varentry_type(entry)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
""
|
|
}
|
|
|
|
// Scan both types and vartypes from source — call on open/change.
|
|
fn lsp_scan_document(uri: String, source: String) -> Void {
|
|
lsp_scan_types(uri, source)
|
|
lsp_scan_vartypes(uri, source)
|
|
}
|
|
|
|
// Given a line of text up to the cursor, extract the identifier before a trailing '.'.
|
|
// e.g. "let x = user." → "user"
|
|
// "foo.bar." → "bar" (innermost only — sufficient for one-level field access)
|
|
// Returns "" if the text doesn't end with "identifier.".
|
|
fn lsp_dot_ident_before(text: String) -> String {
|
|
let tlen: Int = str_len(text)
|
|
if tlen < 2 {
|
|
return ""
|
|
}
|
|
// Must end with '.'
|
|
let last_ch: String = str_slice(text, tlen - 1, tlen)
|
|
if !str_eq(last_ch, ".") {
|
|
return ""
|
|
}
|
|
// Find the identifier immediately before the dot
|
|
let dot_pos: Int = tlen - 1
|
|
// Walk backwards from dot_pos-1 collecting word chars
|
|
let end_idx: Int = dot_pos
|
|
let start_idx: Int = dot_pos
|
|
let going: Bool = true
|
|
while going {
|
|
if start_idx <= 0 {
|
|
let going = false
|
|
} else {
|
|
let c: Int = str_char_code(text, start_idx - 1)
|
|
let is_word: Bool = false
|
|
if c >= 97 { if c <= 122 { let is_word = true } }
|
|
if c >= 65 { if c <= 90 { let is_word = true } }
|
|
if c >= 48 { if c <= 57 { let is_word = true } }
|
|
if c == 95 { let is_word = true }
|
|
if is_word {
|
|
let start_idx = start_idx - 1
|
|
} else {
|
|
let going = false
|
|
}
|
|
}
|
|
}
|
|
if start_idx >= end_idx {
|
|
return ""
|
|
}
|
|
str_slice(text, start_idx, end_idx)
|
|
}
|
|
|
|
// Extract text on the given line up to (not including) char_no.
|
|
fn lsp_line_prefix(source: String, line_no: Int, char_no: Int) -> String {
|
|
let lines: [String] = str_split_lines(source)
|
|
if line_no >= native_list_len(lines) {
|
|
return ""
|
|
}
|
|
let line: String = native_list_get(lines, line_no)
|
|
let line_len: Int = str_len(line)
|
|
let end: Int = char_no
|
|
if end > line_len {
|
|
let end = line_len
|
|
}
|
|
str_slice(line, 0, end)
|
|
}
|
|
|
|
// Build field completion items for a given type. Returns JSON array string or "".
|
|
// Returns "" (not "[]") if no type/fields found, so caller can fall back.
|
|
fn lsp_field_completions(uri: String, type_name: String) -> String {
|
|
let fields_str: String = lsp_get_type_fields(uri, type_name)
|
|
if str_eq(fields_str, "") {
|
|
return ""
|
|
}
|
|
let field_entries: [String] = str_split(fields_str, "|")
|
|
let n: Int = native_list_len(field_entries)
|
|
let items: [String] = native_list_empty()
|
|
let i: Int = 0
|
|
while i < n {
|
|
let entry: String = native_list_get(field_entries, i)
|
|
let colon: Int = str_index_of(entry, ":")
|
|
if colon > 0 {
|
|
let fname: String = str_slice(entry, 0, colon)
|
|
let ftype: String = str_slice(entry, colon + 1, str_len(entry))
|
|
// kind=5 is Field in LSP
|
|
let items = native_list_append(items, lsp_completion_item(fname, 5, ftype))
|
|
}
|
|
let i = i + 1
|
|
}
|
|
"[" + str_join(items, ",") + "]"
|
|
}
|
|
|
|
// ── Source scanning ────────────────────────────────────────────────────────
|
|
|
|
// Extract user-defined function names from source text.
|
|
fn lsp_extract_fns(source: String) -> [String] {
|
|
let result: [String] = native_list_empty()
|
|
let lines: [String] = str_split_lines(source)
|
|
let n: Int = native_list_len(lines)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let line: String = str_trim(native_list_get(lines, i))
|
|
if str_starts_with(line, "fn ") {
|
|
let after: String = str_slice(line, 3, str_len(line))
|
|
let paren: Int = str_index_of(after, "(")
|
|
if paren > 0 {
|
|
let name: String = str_trim(str_slice(after, 0, paren))
|
|
if str_len(name) > 0 {
|
|
let result = native_list_append(result, name)
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
result
|
|
}
|
|
|
|
// Find the definition of fn_name in source.
|
|
// Returns "line:char" (0-based) or "" if not found.
|
|
fn lsp_find_def(source: String, fn_name: String) -> String {
|
|
let lines: [String] = str_split_lines(source)
|
|
let n: Int = native_list_len(lines)
|
|
let target1: String = "fn " + fn_name + "("
|
|
let target2: String = "fn " + fn_name + " ("
|
|
let i: Int = 0
|
|
while i < n {
|
|
let line: String = native_list_get(lines, i)
|
|
let trimmed: String = str_trim(line)
|
|
if str_starts_with(trimmed, target1) {
|
|
let col: Int = str_index_of(line, target1)
|
|
if col < 0 {
|
|
let col = 0
|
|
}
|
|
return int_to_str(i) + ":" + int_to_str(col)
|
|
}
|
|
if str_starts_with(trimmed, target2) {
|
|
let col: Int = str_index_of(line, target2)
|
|
if col < 0 {
|
|
let col = 0
|
|
}
|
|
return int_to_str(i) + ":" + int_to_str(col)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
""
|
|
}
|
|
|
|
// Get the identifier word at line_no:char_no in source (0-based).
|
|
fn lsp_word_at(source: String, line_no: Int, char_no: Int) -> String {
|
|
let lines: [String] = str_split_lines(source)
|
|
if line_no >= native_list_len(lines) {
|
|
return ""
|
|
}
|
|
let line: String = native_list_get(lines, line_no)
|
|
let line_len: Int = str_len(line)
|
|
if char_no >= line_len {
|
|
return ""
|
|
}
|
|
let start: Int = char_no
|
|
let going_back: Bool = true
|
|
while going_back {
|
|
if start <= 0 {
|
|
let going_back = false
|
|
} else {
|
|
let c: Int = str_char_code(line, start - 1)
|
|
let is_word: Bool = false
|
|
if c >= 97 { if c <= 122 { let is_word = true } }
|
|
if c >= 65 { if c <= 90 { let is_word = true } }
|
|
if c >= 48 { if c <= 57 { let is_word = true } }
|
|
if c == 95 { let is_word = true }
|
|
if is_word {
|
|
let start = start - 1
|
|
} else {
|
|
let going_back = false
|
|
}
|
|
}
|
|
}
|
|
let end: Int = char_no
|
|
let going_fwd: Bool = true
|
|
while going_fwd {
|
|
if end >= line_len {
|
|
let going_fwd = false
|
|
} else {
|
|
let c: Int = str_char_code(line, end)
|
|
let is_word: Bool = false
|
|
if c >= 97 { if c <= 122 { let is_word = true } }
|
|
if c >= 65 { if c <= 90 { let is_word = true } }
|
|
if c >= 48 { if c <= 57 { let is_word = true } }
|
|
if c == 95 { let is_word = true }
|
|
if is_word {
|
|
let end = end + 1
|
|
} else {
|
|
let going_fwd = false
|
|
}
|
|
}
|
|
}
|
|
if end <= start {
|
|
return ""
|
|
}
|
|
str_slice(line, start, end)
|
|
}
|
|
|
|
// ── Completions ────────────────────────────────────────────────────────────
|
|
|
|
fn lsp_completion_item(label: String, kind: Int, detail: String) -> String {
|
|
"{\"label\":\"" + lsp_json_escape(label) + "\",\"kind\":" + int_to_str(kind) + ",\"detail\":\"" + lsp_json_escape(detail) + "\"}"
|
|
}
|
|
|
|
fn lsp_build_completions(uri: String, catalog: [String], line_no: Int, char_no: Int) -> String {
|
|
let source: String = lsp_doc_get(uri)
|
|
// Check for dot-access: if the text before the cursor ends with "identifier.",
|
|
// return field completions for that variable's type.
|
|
let prefix: String = lsp_line_prefix(source, line_no, char_no)
|
|
let dot_ident: String = lsp_dot_ident_before(prefix)
|
|
if !str_eq(dot_ident, "") {
|
|
// Look up the variable's type
|
|
let var_type: String = lsp_get_var_type(uri, dot_ident)
|
|
if !str_eq(var_type, "") {
|
|
let field_items: String = lsp_field_completions(uri, var_type)
|
|
if !str_eq(field_items, "") {
|
|
return field_items
|
|
}
|
|
}
|
|
}
|
|
let items: [String] = native_list_empty()
|
|
// User-defined fns (kind=3 Function)
|
|
let user_fns: [String] = lsp_extract_fns(source)
|
|
let fn_n: Int = native_list_len(user_fns)
|
|
let i: Int = 0
|
|
while i < fn_n {
|
|
let name: String = native_list_get(user_fns, i)
|
|
let items = native_list_append(items, lsp_completion_item(name, 3, "user-defined fn"))
|
|
let i = i + 1
|
|
}
|
|
// Builtins (kind=3 Function)
|
|
let bn: Int = native_list_len(catalog)
|
|
let j: Int = 0
|
|
while j < bn {
|
|
let entry: String = native_list_get(catalog, j)
|
|
let bar1: Int = str_index_of(entry, "|")
|
|
if bar1 >= 0 {
|
|
let bname: String = str_slice(entry, 0, bar1)
|
|
let rest: String = str_slice(entry, bar1 + 1, str_len(entry))
|
|
let bar2: Int = str_index_of(rest, "|")
|
|
let sig: String = ""
|
|
if bar2 >= 0 {
|
|
let sig = str_slice(rest, 0, bar2)
|
|
} else {
|
|
let sig = rest
|
|
}
|
|
let items = native_list_append(items, lsp_completion_item(bname, 3, sig))
|
|
}
|
|
let j = j + 1
|
|
}
|
|
// Keywords (kind=14)
|
|
let kws: [String] = native_list_empty()
|
|
let kws = native_list_append(kws, "fn")
|
|
let kws = native_list_append(kws, "let")
|
|
let kws = native_list_append(kws, "return")
|
|
let kws = native_list_append(kws, "if")
|
|
let kws = native_list_append(kws, "else")
|
|
let kws = native_list_append(kws, "while")
|
|
let kws = native_list_append(kws, "for")
|
|
let kws = native_list_append(kws, "in")
|
|
let kws = native_list_append(kws, "true")
|
|
let kws = native_list_append(kws, "false")
|
|
let kws = native_list_append(kws, "import")
|
|
let kws = native_list_append(kws, "match")
|
|
let kws = native_list_append(kws, "break")
|
|
let kws = native_list_append(kws, "continue")
|
|
let kw_n: Int = native_list_len(kws)
|
|
let k: Int = 0
|
|
while k < kw_n {
|
|
let kw: String = native_list_get(kws, k)
|
|
let items = native_list_append(items, lsp_completion_item(kw, 14, "keyword"))
|
|
let k = k + 1
|
|
}
|
|
// Types (kind=8 Class)
|
|
let types: [String] = native_list_empty()
|
|
let types = native_list_append(types, "String")
|
|
let types = native_list_append(types, "Int")
|
|
let types = native_list_append(types, "Bool")
|
|
let types = native_list_append(types, "Float")
|
|
let types = native_list_append(types, "Void")
|
|
let types = native_list_append(types, "Any")
|
|
let types = native_list_append(types, "Instant")
|
|
let types = native_list_append(types, "Duration")
|
|
let t_n: Int = native_list_len(types)
|
|
let t: Int = 0
|
|
while t < t_n {
|
|
let ty: String = native_list_get(types, t)
|
|
let items = native_list_append(items, lsp_completion_item(ty, 8, "type"))
|
|
let t = t + 1
|
|
}
|
|
"[" + str_join(items, ",") + "]"
|
|
}
|
|
|
|
// ── Hover ──────────────────────────────────────────────────────────────────
|
|
|
|
fn lsp_keyword_hover(word: String) -> String {
|
|
if str_eq(word, "fn") { return "Declare a function:\n```el\nfn name(param: Type) -> ReturnType { ... }\n```" }
|
|
if str_eq(word, "let") { return "Bind a local variable:\n```el\nlet name: Type = value\n```" }
|
|
if str_eq(word, "return") { return "Return a value from a function." }
|
|
if str_eq(word, "if") { return "Conditional:\n```el\nif condition { ... } else { ... }\n```" }
|
|
if str_eq(word, "else") { return "Else branch of an if statement." }
|
|
if str_eq(word, "while") { return "Loop while condition is true:\n```el\nwhile condition { ... }\n```" }
|
|
if str_eq(word, "for") { return "Range loop:\n```el\nfor i in 0..n { ... }\n```" }
|
|
if str_eq(word, "in") { return "Used in for-in range loops." }
|
|
if str_eq(word, "true") { return "Boolean literal true." }
|
|
if str_eq(word, "false") { return "Boolean literal false." }
|
|
if str_eq(word, "import") { return "Import a module:\n```el\nimport \"path/to/module.el\"\n```" }
|
|
if str_eq(word, "match") { return "Pattern match:\n```el\nmatch value { \"a\" => ... }\n```" }
|
|
if str_eq(word, "break") { return "Break out of the current while loop." }
|
|
if str_eq(word, "continue") { return "Skip to the next iteration of the current while loop." }
|
|
if str_eq(word, "String") { return "Built-in string type. Strings are immutable byte sequences." }
|
|
if str_eq(word, "Int") { return "Built-in 64-bit signed integer type." }
|
|
if str_eq(word, "Bool") { return "Built-in boolean type. Values: true or false." }
|
|
if str_eq(word, "Float") { return "Built-in 64-bit floating-point type (IEEE 754 double)." }
|
|
if str_eq(word, "Void") { return "Return type for functions that produce no value." }
|
|
if str_eq(word, "Any") { return "Dynamically typed value — accepts any El type." }
|
|
""
|
|
}
|
|
|
|
fn lsp_build_hover(uri: String, line_no: Int, char_no: Int, catalog: [String]) -> String {
|
|
let source: String = lsp_doc_get(uri)
|
|
let word: String = lsp_word_at(source, line_no, char_no)
|
|
if str_eq(word, "") {
|
|
return "null"
|
|
}
|
|
// Keywords + types
|
|
let kw_doc: String = lsp_keyword_hover(word)
|
|
if !str_eq(kw_doc, "") {
|
|
let content: String = "**" + word + "** (keyword)\n\n" + kw_doc
|
|
return "{\"contents\":{\"kind\":\"markdown\",\"value\":\"" + lsp_json_escape(content) + "\"}}"
|
|
}
|
|
// Builtins
|
|
let entry: String = lsp_builtin_find(word, catalog)
|
|
if !str_eq(entry, "") {
|
|
let bar1: Int = str_index_of(entry, "|")
|
|
let rest: String = str_slice(entry, bar1 + 1, str_len(entry))
|
|
let bar2: Int = str_index_of(rest, "|")
|
|
let sig: String = ""
|
|
let desc: String = ""
|
|
if bar2 >= 0 {
|
|
let sig = str_slice(rest, 0, bar2)
|
|
let desc = str_slice(rest, bar2 + 1, str_len(rest))
|
|
} else {
|
|
let sig = rest
|
|
}
|
|
let content: String = "**" + word + "** (builtin)\n\n```el\n" + sig + "\n```\n\n" + desc
|
|
return "{\"contents\":{\"kind\":\"markdown\",\"value\":\"" + lsp_json_escape(content) + "\"}}"
|
|
}
|
|
// User-defined fns — find the fn signature line
|
|
let user_fns: [String] = lsp_extract_fns(source)
|
|
let uf_n: Int = native_list_len(user_fns)
|
|
let i: Int = 0
|
|
while i < uf_n {
|
|
let fn_name: String = native_list_get(user_fns, i)
|
|
if str_eq(fn_name, word) {
|
|
let lines: [String] = str_split_lines(source)
|
|
let ln: Int = native_list_len(lines)
|
|
let j: Int = 0
|
|
while j < ln {
|
|
let line: String = str_trim(native_list_get(lines, j))
|
|
if str_starts_with(line, "fn " + word + "(") {
|
|
let content: String = "**" + word + "** (user-defined)\n\n```el\n" + line + "\n```"
|
|
return "{\"contents\":{\"kind\":\"markdown\",\"value\":\"" + lsp_json_escape(content) + "\"}}"
|
|
}
|
|
let j = j + 1
|
|
}
|
|
let content: String = "**" + word + "** (user-defined function)"
|
|
return "{\"contents\":{\"kind\":\"markdown\",\"value\":\"" + lsp_json_escape(content) + "\"}}"
|
|
}
|
|
let i = i + 1
|
|
}
|
|
"null"
|
|
}
|
|
|
|
// ── Go-to-definition ───────────────────────────────────────────────────────
|
|
|
|
fn lsp_build_definition(uri: String, line_no: Int, char_no: Int) -> String {
|
|
let source: String = lsp_doc_get(uri)
|
|
let word: String = lsp_word_at(source, line_no, char_no)
|
|
if str_eq(word, "") {
|
|
return "null"
|
|
}
|
|
let pos_str: String = lsp_find_def(source, word)
|
|
if str_eq(pos_str, "") {
|
|
return "null"
|
|
}
|
|
let colon: Int = str_index_of(pos_str, ":")
|
|
if colon < 0 {
|
|
return "null"
|
|
}
|
|
let def_line: Int = str_to_int(str_slice(pos_str, 0, colon))
|
|
let def_char: Int = str_to_int(str_slice(pos_str, colon + 1, str_len(pos_str)))
|
|
"{\"uri\":\"" + lsp_json_escape(uri) + "\",\"range\":{\"start\":{\"line\":" + int_to_str(def_line) + ",\"character\":" + int_to_str(def_char) + "},\"end\":{\"line\":" + int_to_str(def_line) + ",\"character\":" + int_to_str(def_char + str_len(word)) + "}}}"
|
|
}
|
|
|
|
// ── Diagnostics ────────────────────────────────────────────────────────────
|
|
|
|
fn lsp_make_diag(line_no: Int, col_start: Int, col_end: Int, message: String, severity: Int) -> String {
|
|
"{\"range\":{\"start\":{\"line\":" + int_to_str(line_no) + ",\"character\":" + int_to_str(col_start) + "},\"end\":{\"line\":" + int_to_str(line_no) + ",\"character\":" + int_to_str(col_end) + "}},\"severity\":" + int_to_str(severity) + ",\"source\":\"el-lsp\",\"message\":\"" + lsp_json_escape(message) + "\"}"
|
|
}
|
|
|
|
fn lsp_compute_diagnostics(source: String) -> String {
|
|
let diags: [String] = native_list_empty()
|
|
let lines: [String] = str_split_lines(source)
|
|
let n: Int = native_list_len(lines)
|
|
let brace_depth: Int = 0
|
|
let paren_depth: Int = 0
|
|
let bracket_depth: Int = 0
|
|
let brace_line: Int = 0
|
|
let paren_line: Int = 0
|
|
let bracket_line: Int = 0
|
|
let i: Int = 0
|
|
while i < n {
|
|
let line: String = native_list_get(lines, i)
|
|
let line_len: Int = str_len(line)
|
|
let in_str: Bool = false
|
|
let escape_next: Bool = false
|
|
let str_start: Int = 0
|
|
let ci: Int = 0
|
|
while ci < line_len {
|
|
let ch: Int = str_char_code(line, ci)
|
|
if escape_next {
|
|
let escape_next = false
|
|
} else {
|
|
if ch == 92 {
|
|
if in_str { let escape_next = true }
|
|
} else {
|
|
if ch == 34 {
|
|
if in_str {
|
|
let in_str = false
|
|
} else {
|
|
let in_str = true
|
|
let str_start = ci
|
|
}
|
|
} else {
|
|
if !in_str {
|
|
if ch == 123 {
|
|
let brace_depth = brace_depth + 1
|
|
let brace_line = i
|
|
}
|
|
if ch == 125 {
|
|
if brace_depth > 0 {
|
|
let brace_depth = brace_depth - 1
|
|
} else {
|
|
let diags = native_list_append(diags, lsp_make_diag(i, ci, ci + 1, "Unexpected '}'", 1))
|
|
}
|
|
}
|
|
if ch == 40 {
|
|
let paren_depth = paren_depth + 1
|
|
let paren_line = i
|
|
}
|
|
if ch == 41 {
|
|
if paren_depth > 0 {
|
|
let paren_depth = paren_depth - 1
|
|
} else {
|
|
let diags = native_list_append(diags, lsp_make_diag(i, ci, ci + 1, "Unexpected ')'", 1))
|
|
}
|
|
}
|
|
if ch == 91 {
|
|
let bracket_depth = bracket_depth + 1
|
|
let bracket_line = i
|
|
}
|
|
if ch == 93 {
|
|
if bracket_depth > 0 {
|
|
let bracket_depth = bracket_depth - 1
|
|
} else {
|
|
let diags = native_list_append(diags, lsp_make_diag(i, ci, ci + 1, "Unexpected ']'", 1))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let ci = ci + 1
|
|
}
|
|
if in_str {
|
|
let diags = native_list_append(diags, lsp_make_diag(i, str_start, line_len, "Unterminated string literal", 1))
|
|
}
|
|
let i = i + 1
|
|
}
|
|
if brace_depth > 0 {
|
|
let diags = native_list_append(diags, lsp_make_diag(brace_line, 0, 1, "Unclosed '{' (opened near this line)", 1))
|
|
}
|
|
if paren_depth > 0 {
|
|
let diags = native_list_append(diags, lsp_make_diag(paren_line, 0, 1, "Unclosed '(' (opened near this line)", 1))
|
|
}
|
|
if bracket_depth > 0 {
|
|
let diags = native_list_append(diags, lsp_make_diag(bracket_line, 0, 1, "Unclosed '[' (opened near this line)", 1))
|
|
}
|
|
"[" + str_join(diags, ",") + "]"
|
|
}
|
|
|
|
fn lsp_publish_diagnostics(uri: String) -> Void {
|
|
let source: String = lsp_doc_get(uri)
|
|
let diags: String = lsp_compute_diagnostics(source)
|
|
let notif: String = lsp_notification("textDocument/publishDiagnostics",
|
|
"{\"uri\":\"" + lsp_json_escape(uri) + "\",\"diagnostics\":" + diags + "}")
|
|
lsp_write_message(notif)
|
|
}
|
|
|
|
// ── Method handlers ────────────────────────────────────────────────────────
|
|
|
|
fn lsp_handle_initialize(id: String) -> Void {
|
|
let caps: String = "{\"textDocumentSync\":1,\"completionProvider\":{\"triggerCharacters\":[\".\",\"_\",\"(\"]},\"hoverProvider\":true,\"definitionProvider\":true}"
|
|
let result: String = "{\"capabilities\":" + caps + ",\"serverInfo\":{\"name\":\"el-lsp\",\"version\":\"1.0.0\"}}"
|
|
lsp_write_message(lsp_response(id, result))
|
|
}
|
|
|
|
fn lsp_handle_completion(id: String, params: String, catalog: [String]) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
let pos: String = json_get_raw(params, "position")
|
|
let line_no: Int = json_get_int(pos, "line")
|
|
let char_no: Int = json_get_int(pos, "character")
|
|
let items: String = lsp_build_completions(uri, catalog, line_no, char_no)
|
|
let result: String = "{\"isIncomplete\":false,\"items\":" + items + "}"
|
|
lsp_write_message(lsp_response(id, result))
|
|
}
|
|
|
|
fn lsp_handle_hover(id: String, params: String, catalog: [String]) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
let pos: String = json_get_raw(params, "position")
|
|
let line_no: Int = json_get_int(pos, "line")
|
|
let char_no: Int = json_get_int(pos, "character")
|
|
let result: String = lsp_build_hover(uri, line_no, char_no, catalog)
|
|
lsp_write_message(lsp_response(id, result))
|
|
}
|
|
|
|
fn lsp_handle_definition(id: String, params: String) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
let pos: String = json_get_raw(params, "position")
|
|
let line_no: Int = json_get_int(pos, "line")
|
|
let char_no: Int = json_get_int(pos, "character")
|
|
let result: String = lsp_build_definition(uri, line_no, char_no)
|
|
lsp_write_message(lsp_response(id, result))
|
|
}
|
|
|
|
fn lsp_handle_did_open(params: String, catalog: [String]) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
let text: String = json_get_string(td, "text")
|
|
lsp_doc_store(uri, text)
|
|
lsp_scan_document(uri, text)
|
|
lsp_publish_diagnostics(uri)
|
|
}
|
|
|
|
fn lsp_handle_did_change(params: String) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
let changes: String = json_get_raw(params, "contentChanges")
|
|
let first: String = json_array_get(changes, 0)
|
|
let text: String = json_get_string(first, "text")
|
|
lsp_doc_store(uri, text)
|
|
lsp_scan_document(uri, text)
|
|
lsp_publish_diagnostics(uri)
|
|
}
|
|
|
|
fn lsp_handle_did_close(params: String) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
lsp_doc_remove(uri)
|
|
let notif: String = lsp_notification("textDocument/publishDiagnostics",
|
|
"{\"uri\":\"" + lsp_json_escape(uri) + "\",\"diagnostics\":[]}")
|
|
lsp_write_message(notif)
|
|
}
|
|
|
|
fn lsp_handle_did_save(params: String) -> Void {
|
|
let td: String = json_get_raw(params, "textDocument")
|
|
let uri: String = json_get_string(td, "uri")
|
|
lsp_publish_diagnostics(uri)
|
|
}
|
|
|
|
fn lsp_handle_shutdown(id: String) -> Void {
|
|
lsp_write_message(lsp_response(id, "null"))
|
|
}
|
|
|
|
// ── Main dispatch ──────────────────────────────────────────────────────────
|
|
|
|
// Returns false when the server should stop.
|
|
fn lsp_dispatch(msg: String, catalog: [String]) -> Bool {
|
|
let method: String = json_get_string(msg, "method")
|
|
let id_raw: String = json_get_raw(msg, "id")
|
|
let id: String = id_raw
|
|
if str_eq(id, "") {
|
|
let id = "null"
|
|
}
|
|
let params_raw: String = json_get_raw(msg, "params")
|
|
let params: String = params_raw
|
|
if str_eq(params, "") {
|
|
let params = "{}"
|
|
}
|
|
|
|
if str_eq(method, "initialize") {
|
|
lsp_handle_initialize(id)
|
|
return true
|
|
}
|
|
if str_eq(method, "initialized") {
|
|
return true
|
|
}
|
|
if str_eq(method, "shutdown") {
|
|
lsp_handle_shutdown(id)
|
|
return true
|
|
}
|
|
if str_eq(method, "exit") {
|
|
return false
|
|
}
|
|
if str_eq(method, "textDocument/didOpen") {
|
|
lsp_handle_did_open(params, catalog)
|
|
return true
|
|
}
|
|
if str_eq(method, "textDocument/didChange") {
|
|
lsp_handle_did_change(params)
|
|
return true
|
|
}
|
|
if str_eq(method, "textDocument/didClose") {
|
|
lsp_handle_did_close(params)
|
|
return true
|
|
}
|
|
if str_eq(method, "textDocument/didSave") {
|
|
lsp_handle_did_save(params)
|
|
return true
|
|
}
|
|
if str_eq(method, "textDocument/completion") {
|
|
lsp_handle_completion(id, params, catalog)
|
|
return true
|
|
}
|
|
if str_eq(method, "textDocument/hover") {
|
|
lsp_handle_hover(id, params, catalog)
|
|
return true
|
|
}
|
|
if str_eq(method, "textDocument/definition") {
|
|
lsp_handle_definition(id, params)
|
|
return true
|
|
}
|
|
// Unknown method — error for requests, ignore notifications
|
|
if !str_eq(id, "null") {
|
|
lsp_write_message(lsp_error_response(id, -32601, "Method not found: " + method))
|
|
}
|
|
true
|
|
}
|
|
|
|
fn main() -> Void {
|
|
let catalog: [String] = lsp_builtin_catalog()
|
|
let running: Bool = true
|
|
while running {
|
|
let msg: String = lsp_read_message()
|
|
if str_eq(msg, "") {
|
|
let running = false
|
|
} else {
|
|
let cont: Bool = lsp_dispatch(msg, catalog)
|
|
if !cont {
|
|
let running = false
|
|
}
|
|
}
|
|
}
|
|
}
|