// tools/lsp/el-lsp.el — El Language Server Protocol (LSP) server // // Implements the Language Server Protocol over stdin/stdout (JSON-RPC). // Editors (VSCode, Neovim, etc.) launch this binary as a child process and // communicate via JSON-RPC messages framed with Content-Length headers. // // Wire format (LSP spec §3.1): // Content-Length: \r\n // \r\n // // // This skeleton handles: // initialize — advertise server capabilities // initialized — no-op acknowledgement // textDocument/didOpen — store document source in state // textDocument/didChange — update stored source // textDocument/didClose — evict document from state // textDocument/completion — return completions (builtins + doc fns + keywords) // textDocument/hover — hover documentation for word at cursor // textDocument/definition — go-to-definition (scan for fn () // shutdown / exit — clean shutdown // // ── Seed primitive required ─────────────────────────────────────────────────── // // lsp_read_message() calls: // __read_n(n: Int) -> String // // This primitive reads exactly n bytes from stdin. It is NOT yet in the El // runtime. The seed agent must: // 1. Add to el-compiler/runtime/el_runtime.c: // // el_val_t __read_n(el_val_t n) { // int64_t count = (int64_t)n; // if (count <= 0) return EL_STR(strdup("")); // char *buf = malloc(count + 1); // if (!buf) return EL_STR(strdup("")); // size_t total = 0; // while (total < (size_t)count) { // size_t got = fread(buf + total, 1, (size_t)count - total, stdin); // if (got == 0) break; // total += got; // } // buf[total] = '\0'; // return EL_STR(buf); // } // // 2. Declare in el-compiler/runtime/el_runtime.h: // el_val_t __read_n(el_val_t n); // // 3. Add to builtin_arity table in el-compiler/src/codegen.el: // "arity" "1" "__read_n" // // 4. Rebuild the elc binary. // // ── Runtime modules required ────────────────────────────────────────────────── // runtime/string.el — str_*, int_to_str, str_to_int, bool_to_str, etc. // runtime/state.el — state_set, state_get, state_del // runtime/json.el — json_get, json_get_raw, json_escape_string, // json_array_get, json_build_array // (runtime/math.el, runtime/env.el, runtime/fs.el included by build.sh // but not directly used by the LSP itself) // ── Identifier character helpers ────────────────────────────────────────────── // _is_ident_char — true if the char code c can appear in an El identifier. // El identifiers: [A-Za-z_][A-Za-z0-9_]* fn _is_ident_char(c: Int) -> Bool { if c >= 65 { if c <= 90 { return true } } // A-Z if c >= 97 { if c <= 122 { return true } } // a-z if c >= 48 { if c <= 57 { return true } } // 0-9 if c == 95 { return true } // _ return false } // _ident_start_at — find the left boundary of an identifier starting at or // before column `col` in `text`. Returns the byte index where the identifier // starts, or `col` if no identifier extends left. fn _ident_start_at(text: String, col: Int) -> Int { let lo: Int = col let searching: Bool = true while searching { if lo <= 0 { let lo = 0 let searching = false } else { let ch: Int = str_char_code(text, lo - 1) if _is_ident_char(ch) { lo = lo - 1 } else { let searching = false } } } return lo } // _ident_end_at — find the right boundary of an identifier starting at `pos` // in `text`. Returns the byte index one past the last identifier character. fn _ident_end_at(text: String, pos: Int) -> Int { let n: Int = str_len(text) let hi: Int = pos let searching: Bool = true while searching { if hi >= n { let searching = false } else { let ch: Int = str_char_code(text, hi) if _is_ident_char(ch) { hi = hi + 1 } else { let searching = false } } } return hi } // lsp_word_at — extract the identifier word under/at cursor column `col` // in `line_text`. Returns "" when the cursor is not on an identifier. fn lsp_word_at(line_text: String, col: Int) -> String { let n: Int = str_len(line_text) if n == 0 { return "" } let c: Int = col if c >= n { c = n - 1 } if c < 0 { c = 0 } // If the cursor is not on an identifier character, return "". if !_is_ident_char(str_char_code(line_text, c)) { return "" } let lo: Int = _ident_start_at(line_text, c) let hi: Int = _ident_end_at(line_text, c) return str_slice(line_text, lo, hi) } // ── El builtins known to the LSP ───────────────────────────────────────────── fn lsp_builtin_names() -> [String] { let names: [String] = el_list_empty() // I/O names = el_list_append(names, "println") names = el_list_append(names, "print") names = el_list_append(names, "readline") // String names = el_list_append(names, "str_len") names = el_list_append(names, "str_eq") names = el_list_append(names, "str_concat") names = el_list_append(names, "str_slice") names = el_list_append(names, "str_contains") names = el_list_append(names, "str_starts_with") names = el_list_append(names, "str_ends_with") names = el_list_append(names, "str_replace") names = el_list_append(names, "str_index_of") names = el_list_append(names, "str_last_index_of") names = el_list_append(names, "str_split") names = el_list_append(names, "str_split_lines") names = el_list_append(names, "str_split_n") names = el_list_append(names, "str_join") names = el_list_append(names, "str_trim") names = el_list_append(names, "str_lstrip") names = el_list_append(names, "str_rstrip") names = el_list_append(names, "str_to_upper") names = el_list_append(names, "str_to_lower") names = el_list_append(names, "str_upper") names = el_list_append(names, "str_lower") names = el_list_append(names, "str_repeat") names = el_list_append(names, "str_reverse") names = el_list_append(names, "str_char_at") names = el_list_append(names, "str_char_code") names = el_list_append(names, "str_count") names = el_list_append(names, "str_count_lines") names = el_list_append(names, "str_pad_left") names = el_list_append(names, "str_pad_right") names = el_list_append(names, "str_strip_prefix") names = el_list_append(names, "str_strip_suffix") names = el_list_append(names, "str_find_chars") // Type conversions names = el_list_append(names, "int_to_str") names = el_list_append(names, "str_to_int") names = el_list_append(names, "float_to_str") names = el_list_append(names, "str_to_float") names = el_list_append(names, "bool_to_str") // Math names = el_list_append(names, "el_abs") names = el_list_append(names, "el_max") names = el_list_append(names, "el_min") names = el_list_append(names, "math_sqrt") names = el_list_append(names, "math_log") names = el_list_append(names, "math_sin") names = el_list_append(names, "math_cos") names = el_list_append(names, "math_pi") // List names = el_list_append(names, "el_list_empty") names = el_list_append(names, "el_list_append") names = el_list_append(names, "el_list_len") names = el_list_append(names, "el_list_get") names = el_list_append(names, "el_list_clone") names = el_list_append(names, "el_list_new") names = el_list_append(names, "list_push") names = el_list_append(names, "list_join") names = el_list_append(names, "list_range") // Map names = el_list_append(names, "el_map_new") names = el_list_append(names, "el_map_get") names = el_list_append(names, "el_map_set") // State names = el_list_append(names, "state_set") names = el_list_append(names, "state_get") names = el_list_append(names, "state_del") names = el_list_append(names, "state_keys") names = el_list_append(names, "state_has") names = el_list_append(names, "state_get_or") // JSON names = el_list_append(names, "json_get") names = el_list_append(names, "json_get_raw") names = el_list_append(names, "json_get_string") names = el_list_append(names, "json_get_int") names = el_list_append(names, "json_get_bool") names = el_list_append(names, "json_set") names = el_list_append(names, "json_parse") names = el_list_append(names, "json_stringify") names = el_list_append(names, "json_array_len") names = el_list_append(names, "json_array_get") names = el_list_append(names, "json_array_get_string") names = el_list_append(names, "json_build_object") names = el_list_append(names, "json_build_array") names = el_list_append(names, "json_escape_string") // Filesystem names = el_list_append(names, "fs_read") names = el_list_append(names, "fs_write") names = el_list_append(names, "fs_exists") names = el_list_append(names, "fs_mkdir") names = el_list_append(names, "fs_list") // HTTP names = el_list_append(names, "http_get") names = el_list_append(names, "http_post") names = el_list_append(names, "http_post_json") names = el_list_append(names, "http_serve") // Exec names = el_list_append(names, "exec") names = el_list_append(names, "exec_bg") names = el_list_append(names, "exec_capture") // Time names = el_list_append(names, "time_now") names = el_list_append(names, "sleep_ms") names = el_list_append(names, "sleep_secs") // UUID / env names = el_list_append(names, "uuid_new") names = el_list_append(names, "env") // Classification names = el_list_append(names, "is_letter") names = el_list_append(names, "is_digit") names = el_list_append(names, "is_alphanumeric") names = el_list_append(names, "is_whitespace") names = el_list_append(names, "url_encode") names = el_list_append(names, "url_decode") return names } fn lsp_keyword_names() -> [String] { let kws: [String] = el_list_empty() kws = el_list_append(kws, "fn") kws = el_list_append(kws, "let") kws = el_list_append(kws, "return") kws = el_list_append(kws, "if") kws = el_list_append(kws, "else") kws = el_list_append(kws, "while") kws = el_list_append(kws, "for") kws = el_list_append(kws, "in") kws = el_list_append(kws, "true") kws = el_list_append(kws, "false") kws = el_list_append(kws, "import") kws = el_list_append(kws, "type") kws = el_list_append(kws, "enum") kws = el_list_append(kws, "match") return kws } // ── JSON-RPC helpers ────────────────────────────────────────────────────────── // lsp_write_message — frame a JSON body as an LSP Content-Length message and // write it to stdout. fn lsp_write_message(json: String) { let body_len: Int = str_len(json) __print("Content-Length: " + int_to_str(body_len) + "\r\n\r\n" + json) } // lsp_read_header_line — read one header line, strip trailing \r, return it. // Returns "" on EOF. fn lsp_read_header_line() -> String { let line: String = readline() // readline() returns "" on EOF/stdin-close. if str_eq(line, "") { return "" } // Strip trailing \r if readline left it. return str_rstrip(line) } // lsp_parse_content_length — given a trimmed header line, return the // Content-Length value if this is a Content-Length header, otherwise -1. fn lsp_parse_content_length(line: String) -> Int { let prefix: String = "Content-Length: " if str_starts_with(line, prefix) { let value_part: String = str_slice(line, str_len(prefix), str_len(line)) return str_to_int(str_trim(value_part)) } return -1 } // lsp_read_message — read one LSP message from stdin. // // Protocol: // 1. Read lines until a blank line (end of headers). // 2. Extract Content-Length from the headers. // 3. Read exactly Content-Length bytes as the JSON body via __read_n(). // // Returns the JSON body, or "" on EOF / malformed message. // // DEPENDENCY: __read_n(n: Int) -> String — see top-of-file note. fn lsp_read_message() -> String { let content_length: Int = -1 let reading_headers: Bool = true while reading_headers { let line: String = lsp_read_header_line() if str_eq(line, "") { // EOF on the very first read means stdin was closed. if content_length < 0 { return "" } // Blank line after at least one header = end of headers. reading_headers = false } else { let cl: Int = lsp_parse_content_length(line) if cl >= 0 { content_length = cl } // Blank trimmed line = end of headers (no EOF). if str_eq(line, "") { reading_headers = false } } } if content_length < 0 { return "" } // Read exactly content_length bytes from stdin. // __read_n is the seed primitive described in the top-of-file note. return __read_n(content_length) } // ── Response builders ───────────────────────────────────────────────────────── fn lsp_make_response(id: String, result_json: String) -> String { return "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":" + result_json + "}" } fn lsp_make_error(id: String, code: Int, message: String) -> String { let escaped_msg: String = json_escape_string(message) return "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"code\":" + int_to_str(code) + ",\"message\":\"" + escaped_msg + "\"}}" } // ── Server capabilities ─────────────────────────────────────────────────────── fn lsp_capabilities() -> String { return "{" + "\"textDocumentSync\":1," + "\"completionProvider\":{\"triggerCharacters\":[\".\",\"_\"]}," + "\"hoverProvider\":true," + "\"definitionProvider\":true" + "}" } // ── Document state ──────────────────────────────────────────────────────────── fn doc_key(uri: String) -> String { return "doc_" + uri } fn doc_store(uri: String, text: String) { state_set(doc_key(uri), text) } fn doc_load(uri: String) -> String { return state_get(doc_key(uri)) } // ── Source analysis ─────────────────────────────────────────────────────────── // _scan_fn_name_at — given source and a position `i` that points just past // "fn ", skip whitespace and collect an identifier. Returns the function name // string (possibly "") and advances `out_pos` to the character after the name. // // Because El cannot return multiple values, we encode the end position into // state instead of returning it; callers who need it call // _scan_fn_end_pos() separately. fn _scan_fn_name_at(source: String, i: Int) -> String { let n: Int = str_len(source) let j: Int = i // Skip whitespace between "fn" and the name. let skipping: Bool = true while skipping { if j >= n { let skipping = false } else { let ch: Int = str_char_code(source, j) if ch == 32 { j = j + 1 } else { if ch == 9 { j = j + 1 } else { let skipping = false } } } } let name_start: Int = j // Collect identifier characters. let collecting: Bool = true while collecting { if j >= n { let collecting = false } else { if _is_ident_char(str_char_code(source, j)) { j = j + 1 } else { let collecting = false } } } // Store the end position so lsp_extract_fn_names can advance past it. state_set("_fn_scan_end", int_to_str(j)) return str_slice(source, name_start, j) } // lsp_extract_fn_names — scan El source for function definitions. // Looks for the three-character sequence "fn " and extracts the identifier // that follows. Returns a list of function name strings. fn lsp_extract_fn_names(source: String) -> [String] { let result: [String] = el_list_empty() let slen: Int = str_len(source) let i: Int = 0 while i < slen { // Try to match "fn " at position i (need at least 3 chars remaining). if i + 3 <= slen { let window: String = str_slice(source, i, i + 3) if str_eq(window, "fn ") { let fn_name: String = _scan_fn_name_at(source, i + 3) let end_pos_str: String = state_get("_fn_scan_end") let end_pos: Int = str_to_int(end_pos_str) if str_len(fn_name) > 0 { result = el_list_append(result, fn_name) } // Advance past the name to avoid re-matching sub-sequences. if end_pos > i { i = end_pos } else { i = i + 1 } } else { i = i + 1 } } else { i = i + 1 } } return result } // lsp_build_completion_items — convert a list of name strings into a JSON // array of LSP CompletionItem objects. // LSP CompletionItemKind: 3=Function, 6=Variable, 14=Keyword. fn lsp_build_completion_items(names: [String], kind: Int) -> [String] { let items: [String] = el_list_empty() let n: Int = el_list_len(names) let i: Int = 0 while i < n { let name: String = el_list_get(names, i) let escaped: String = json_escape_string(name) let item: String = "{\"label\":\"" + escaped + "\",\"kind\":" + int_to_str(kind) + "}" items = el_list_append(items, item) i = i + 1 } return items } // lsp_items_to_json — serialize a list of pre-encoded JSON item strings to a // JSON array string. fn lsp_items_to_json(items: [String]) -> String { let n: Int = el_list_len(items) let result: String = "[" let i: Int = 0 while i < n { if i > 0 { result = result + "," } result = result + el_list_get(items, i) i = i + 1 } return result + "]" } // ── El keyword and builtin documentation (for hover) ───────────────────────── fn lsp_keyword_doc(word: String) -> String { if str_eq(word, "fn") { return "`fn name(param: Type) -> ReturnType { ... }` — declare a function" } if str_eq(word, "let") { return "`let name: Type = value` — bind a variable" } if str_eq(word, "return") { return "`return expr` — return a value from a function" } if str_eq(word, "if") { return "`if condition { ... } else { ... }` — conditional" } if str_eq(word, "else") { return "`else { ... }` — else branch of an if statement" } if str_eq(word, "while") { return "`while condition { ... }` — loop while condition is true" } if str_eq(word, "for") { return "`for i in 0..n { ... }` — range loop" } if str_eq(word, "in") { return "Used in `for i in start..end` range loops" } if str_eq(word, "true") { return "`true` — boolean literal" } if str_eq(word, "false") { return "`false` — boolean literal" } if str_eq(word, "import") { return "`import \"path/to/module.el\"` — import a module" } if str_eq(word, "type") { return "`type Name { field: Type }` — define a struct" } if str_eq(word, "enum") { return "`enum Name { Variant, ... }` — define an enum" } if str_eq(word, "match") { return "`match expr { ... }` — pattern match" } return "" } fn lsp_builtin_doc(name: String) -> String { // I/O if str_eq(name, "println") { return "`println(s: String)` — print string + newline to stdout" } if str_eq(name, "print") { return "`print(s: String)` — print string without newline" } if str_eq(name, "readline") { return "`readline() -> String` — read one line from stdin" } // String if str_eq(name, "str_len") { return "`str_len(s: String) -> Int` — byte length of string" } if str_eq(name, "str_eq") { return "`str_eq(a: String, b: String) -> Bool` — string equality" } if str_eq(name, "str_concat") { return "`str_concat(a: String, b: String) -> String` — concatenate" } if str_eq(name, "str_slice") { return "`str_slice(s: String, start: Int, end: Int) -> String` — substring [start, end)" } if str_eq(name, "str_contains") { return "`str_contains(s: String, sub: String) -> Bool`" } if str_eq(name, "str_replace") { return "`str_replace(s: String, from: String, to: String) -> String`" } if str_eq(name, "str_split") { return "`str_split(s: String, sep: String) -> [String]`" } if str_eq(name, "str_split_lines"){ return "`str_split_lines(s: String) -> [String]` — split on newlines" } if str_eq(name, "str_join") { return "`str_join(parts: [String], sep: String) -> String`" } if str_eq(name, "str_trim") { return "`str_trim(s: String) -> String` — strip leading/trailing whitespace" } if str_eq(name, "str_starts_with"){ return "`str_starts_with(s: String, prefix: String) -> Bool`" } if str_eq(name, "str_ends_with") { return "`str_ends_with(s: String, suffix: String) -> Bool`" } if str_eq(name, "str_index_of") { return "`str_index_of(s: String, sub: String) -> Int` — first occurrence, or -1" } if str_eq(name, "str_to_upper") { return "`str_to_upper(s: String) -> String` — ASCII uppercase" } if str_eq(name, "str_to_lower") { return "`str_to_lower(s: String) -> String` — ASCII lowercase" } if str_eq(name, "str_char_at") { return "`str_char_at(s: String, i: Int) -> String` — one-char string at byte index i" } if str_eq(name, "str_char_code") { return "`str_char_code(s: String, i: Int) -> Int` — byte value at index i" } if str_eq(name, "str_repeat") { return "`str_repeat(s: String, n: Int) -> String`" } if str_eq(name, "str_reverse") { return "`str_reverse(s: String) -> String` — byte-reverse" } if str_eq(name, "str_pad_left") { return "`str_pad_left(s: String, width: Int, pad: String) -> String`" } if str_eq(name, "str_pad_right") { return "`str_pad_right(s: String, width: Int, pad: String) -> String`" } // Type conversions if str_eq(name, "int_to_str") { return "`int_to_str(n: Int) -> String` — decimal string" } if str_eq(name, "str_to_int") { return "`str_to_int(s: String) -> Int` — parse decimal string" } if str_eq(name, "bool_to_str") { return "`bool_to_str(b: Bool) -> String` — \"true\" or \"false\"" } if str_eq(name, "float_to_str") { return "`float_to_str(f: Float) -> String`" } // List if str_eq(name, "el_list_empty") { return "`el_list_empty() -> [T]` — create empty list" } if str_eq(name, "el_list_append") { return "`el_list_append(list: [T], elem: T) -> [T]` — append element" } if str_eq(name, "el_list_len") { return "`el_list_len(list: [T]) -> Int` — number of elements" } if str_eq(name, "el_list_get") { return "`el_list_get(list: [T], i: Int) -> T` — element at index" } // State if str_eq(name, "state_set") { return "`state_set(key: String, val: String)` — store in process state" } if str_eq(name, "state_get") { return "`state_get(key: String) -> String` — retrieve; \"\" if absent" } if str_eq(name, "state_del") { return "`state_del(key: String)` — remove key from state" } if str_eq(name, "state_has") { return "`state_has(key: String) -> Bool` — true if key present" } // JSON if str_eq(name, "json_get") { return "`json_get(json: String, key: String) -> String` — extract field" } if str_eq(name, "json_get_raw") { return "`json_get_raw(json: String, key: String) -> String` — raw JSON fragment" } if str_eq(name, "json_set") { return "`json_set(json: String, key: String, val: String) -> String`" } if str_eq(name, "json_parse") { return "`json_parse(s: String) -> Map`" } if str_eq(name, "json_stringify") { return "`json_stringify(v: Any) -> String`" } if str_eq(name, "json_array_len") { return "`json_array_len(arr: String) -> Int`" } if str_eq(name, "json_array_get") { return "`json_array_get(arr: String, i: Int) -> String`" } // Filesystem if str_eq(name, "fs_read") { return "`fs_read(path: String) -> String` — read file" } if str_eq(name, "fs_write") { return "`fs_write(path: String, content: String) -> Bool`" } if str_eq(name, "fs_exists") { return "`fs_exists(path: String) -> Bool`" } if str_eq(name, "fs_mkdir") { return "`fs_mkdir(path: String) -> Bool` — mkdir -p" } // HTTP if str_eq(name, "http_get") { return "`http_get(url: String) -> String` — HTTP GET, return body" } if str_eq(name, "http_post") { return "`http_post(url: String, body: String) -> String`" } if str_eq(name, "http_post_json") { return "`http_post_json(url: String, json: String) -> String`" } if str_eq(name, "http_serve") { return "`http_serve(port: Int, handler: String)` — start HTTP server" } // Exec if str_eq(name, "exec") { return "`exec(cmd: String) -> String` — run shell command, return stdout" } if str_eq(name, "exec_bg") { return "`exec_bg(cmd: String)` — run shell command in background" } // Time if str_eq(name, "sleep_ms") { return "`sleep_ms(ms: Int) -> Int` — sleep N milliseconds" } if str_eq(name, "sleep_secs") { return "`sleep_secs(s: Int) -> Int` — sleep N seconds" } // UUID if str_eq(name, "uuid_new") { return "`uuid_new() -> String` — generate a UUID v4" } if str_eq(name, "env") { return "`env(key: String) -> String` — get environment variable" } // Math if str_eq(name, "el_abs") { return "`el_abs(n: Int) -> Int` — absolute value" } if str_eq(name, "el_max") { return "`el_max(a: Int, b: Int) -> Int` — maximum" } if str_eq(name, "el_min") { return "`el_min(a: Int, b: Int) -> Int` — minimum" } if str_eq(name, "math_sqrt") { return "`math_sqrt(f: Float) -> Float` — square root" } return "" } // ── LSP method handlers ─────────────────────────────────────────────────────── fn handle_initialize(id: String, params: String) -> String { let caps: String = lsp_capabilities() let result: String = "{\"capabilities\":" + caps + ",\"serverInfo\":{\"name\":\"el-lsp\",\"version\":\"0.1.0\"}}" return lsp_make_response(id, result) } // handle_did_open — store document text; returns "" (notification, no response). fn handle_did_open(params: String) -> String { let td_raw: String = json_get_raw(params, "textDocument") let uri: String = json_get(td_raw, "uri") let text: String = json_get(td_raw, "text") doc_store(uri, text) return "" } // handle_did_change — update stored text; returns "" (notification). // textDocumentSync=1 (full): contentChanges[0].text has the entire new text. fn handle_did_change(params: String) -> String { let td_raw: String = json_get_raw(params, "textDocument") let uri: String = json_get(td_raw, "uri") let changes_raw: String = json_get_raw(params, "contentChanges") let first: String = json_array_get(changes_raw, 0) let text: String = json_get(first, "text") doc_store(uri, text) return "" } // handle_completion — completions: builtins + keywords + document functions. fn handle_completion(id: String, params: String) -> String { let td_raw: String = json_get_raw(params, "textDocument") let uri: String = json_get(td_raw, "uri") let source: String = doc_load(uri) let all_items: [String] = el_list_empty() // Builtins — CompletionItemKind.Variable = 6 let builtin_items: [String] = lsp_build_completion_items(lsp_builtin_names(), 6) let bi: Int = 0 while bi < el_list_len(builtin_items) { all_items = el_list_append(all_items, el_list_get(builtin_items, bi)) bi = bi + 1 } // Keywords — CompletionItemKind.Keyword = 14 let kw_items: [String] = lsp_build_completion_items(lsp_keyword_names(), 14) let ki: Int = 0 while ki < el_list_len(kw_items) { all_items = el_list_append(all_items, el_list_get(kw_items, ki)) ki = ki + 1 } // Document functions — CompletionItemKind.Function = 3 if !str_eq(source, "") { let doc_fns: [String] = lsp_extract_fn_names(source) let fn_items: [String] = lsp_build_completion_items(doc_fns, 3) let fi: Int = 0 while fi < el_list_len(fn_items) { all_items = el_list_append(all_items, el_list_get(fn_items, fi)) fi = fi + 1 } } let items_json: String = lsp_items_to_json(all_items) let result: String = "{\"isIncomplete\":false,\"items\":" + items_json + "}" return lsp_make_response(id, result) } // handle_hover — return markdown documentation for the word at cursor. fn handle_hover(id: String, params: String) -> String { let td_raw: String = json_get_raw(params, "textDocument") let uri: String = json_get(td_raw, "uri") let pos_raw: String = json_get_raw(params, "position") let line_num: Int = str_to_int(json_get(pos_raw, "line")) let col_num: Int = str_to_int(json_get(pos_raw, "character")) let source: String = doc_load(uri) if str_eq(source, "") { return lsp_make_response(id, "null") } let lines: [String] = str_split_lines(source) if line_num >= el_list_len(lines) { return lsp_make_response(id, "null") } let line_text: String = el_list_get(lines, line_num) let word: String = lsp_word_at(line_text, col_num) if str_eq(word, "") { return lsp_make_response(id, "null") } let doc: String = lsp_keyword_doc(word) if str_eq(doc, "") { doc = lsp_builtin_doc(word) } if str_eq(doc, "") { return lsp_make_response(id, "null") } let escaped_doc: String = json_escape_string(doc) let result: String = "{\"contents\":{\"kind\":\"markdown\",\"value\":\"" + escaped_doc + "\"}}" return lsp_make_response(id, result) } // _find_fn_def_line — search `lines` for a line containing "fn (". // Returns the 0-based line index, or -1 if not found. fn _find_fn_def_line(lines: [String], name: String) -> Int { let target: String = "fn " + name + "(" let num_lines: Int = el_list_len(lines) let li: Int = 0 let found: Int = -1 while li < num_lines { if found < 0 { let l: String = el_list_get(lines, li) if str_contains(l, target) { found = li } } li = li + 1 } return found } // handle_definition — go-to-definition: find the fn declaration for the word. fn handle_definition(id: String, params: String) -> String { let td_raw: String = json_get_raw(params, "textDocument") let uri: String = json_get(td_raw, "uri") let pos_raw: String = json_get_raw(params, "position") let line_num: Int = str_to_int(json_get(pos_raw, "line")) let col_num: Int = str_to_int(json_get(pos_raw, "character")) let source: String = doc_load(uri) if str_eq(source, "") { return lsp_make_response(id, "null") } let lines: [String] = str_split_lines(source) if line_num >= el_list_len(lines) { return lsp_make_response(id, "null") } let line_text: String = el_list_get(lines, line_num) let word: String = lsp_word_at(line_text, col_num) if str_eq(word, "") { return lsp_make_response(id, "null") } let def_line: Int = _find_fn_def_line(lines, word) if def_line < 0 { return lsp_make_response(id, "null") } // Column of the function name: after "fn " (3 chars). let def_text: String = el_list_get(lines, def_line) let fn_prefix_idx: Int = str_index_of(def_text, "fn ") let name_col: Int = fn_prefix_idx + 3 let name_end_col: Int = name_col + str_len(word) let escaped_uri: String = json_escape_string(uri) let result: String = "{\"uri\":\"" + escaped_uri + "\"," + "\"range\":{" + "\"start\":{\"line\":" + int_to_str(def_line) + ",\"character\":" + int_to_str(name_col) + "}," + "\"end\":{\"line\":" + int_to_str(def_line) + ",\"character\":" + int_to_str(name_end_col) + "}" + "}}" return lsp_make_response(id, result) } fn handle_shutdown(id: String) -> String { return lsp_make_response(id, "null") } // ── Main dispatch ───────────────────────────────────────────────────────────── fn lsp_dispatch(method: String, id: String, params: String) -> String { if str_eq(method, "initialize") { return handle_initialize(id, params) } if str_eq(method, "initialized") { return "" } if str_eq(method, "textDocument/didOpen") { return handle_did_open(params) } if str_eq(method, "textDocument/didChange"){ return handle_did_change(params) } if str_eq(method, "textDocument/didClose") { let uri: String = json_get(json_get_raw(params, "textDocument"), "uri") state_del(doc_key(uri)) return "" } if str_eq(method, "textDocument/completion") { return handle_completion(id, params) } if str_eq(method, "textDocument/hover") { return handle_hover(id, params) } if str_eq(method, "textDocument/definition") { return handle_definition(id, params) } if str_eq(method, "shutdown") { return handle_shutdown(id) } if str_eq(method, "exit") { state_set("lsp_exit", "1") return "" } // Unknown method with an id — return method-not-found. if !str_eq(id, "") { if !str_eq(id, "null") { return lsp_make_error(id, -32601, "Method not found: " + method) } } return "" } // ── Entry point ─────────────────────────────────────────────────────────────── fn main() { state_set("lsp_exit", "0") state_set("_fn_scan_end", "0") let running: Bool = true while running { let msg: String = lsp_read_message() if str_eq(msg, "") { // EOF — stdin closed, exit. running = false } else { let id: String = json_get(msg, "id") if str_eq(id, "") { id = "null" } let method: String = json_get(msg, "method") let params: String = json_get_raw(msg, "params") if str_eq(params, "") { params = "{}" } let response: String = lsp_dispatch(method, id, params) if !str_eq(response, "") { lsp_write_message(response) } if str_eq(state_get("lsp_exit"), "1") { running = false } } } }