|
|
|
@@ -0,0 +1,810 @@
|
|
|
|
|
// 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: <n>\r\n
|
|
|
|
|
// \r\n
|
|
|
|
|
// <n bytes of UTF-8 JSON>
|
|
|
|
|
//
|
|
|
|
|
// 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 <word>()
|
|
|
|
|
// 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<String, Any>`" }
|
|
|
|
|
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 <name>(".
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|