diff --git a/ide/vessels/el-ide-server/manifest.el b/ide/vessels/el-ide-server/manifest.el new file mode 100644 index 0000000..80c900e --- /dev/null +++ b/ide/vessels/el-ide-server/manifest.el @@ -0,0 +1,18 @@ +// manifest.el — el-ide-server vessel + +vessel "el-ide-server" { + version "0.1.0" + description "Engram IDE backend: HTTP server, file ops, build/run, LSP bridge, plugin host, settings." + authors ["Will Anderson "] + edition "2026" +} + +dependencies { + el-lsp "0.1" + el-plugin-host "0.1" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/ide/vessels/el-ide-server/src/_combined.c b/ide/vessels/el-ide-server/src/_combined.c new file mode 100644 index 0000000..683e13b --- /dev/null +++ b/ide/vessels/el-ide-server/src/_combined.c @@ -0,0 +1,2569 @@ +#include +#include +#include "el_runtime.h" + +el_val_t is_digit_ch(el_val_t ch); +el_val_t is_alpha_ch(el_val_t ch); +el_val_t is_ident_ch(el_val_t ch); +el_val_t extract_prefix(el_val_t source, el_val_t pos); +el_val_t extract_token(el_val_t source, el_val_t pos); +el_val_t pos_to_line_col(el_val_t source, el_val_t pos); +el_val_t json_escape(el_val_t s); +el_val_t json_str(el_val_t s); +el_val_t json_kv_str(el_val_t k, el_val_t v); +el_val_t json_kv_int(el_val_t k, el_val_t v); +el_val_t json_kv_float(el_val_t k, el_val_t v); +el_val_t json_kv_raw(el_val_t k, el_val_t raw_v); +el_val_t json_array_of(el_val_t items); +el_val_t keywords_list(void); +el_val_t builtin_type_names(void); +el_val_t builtin_type_doc(el_val_t name); +el_val_t builtin_fns(void); +el_val_t keyword_doc(el_val_t kw); +el_val_t first_ident_in(el_val_t rest); +el_val_t scan_decls(el_val_t source); +el_val_t list_contains_str(el_val_t xs, el_val_t needle); +el_val_t prefix_score(el_val_t label, el_val_t prefix); +el_val_t completion_json(el_val_t label, el_val_t kind, el_val_t detail, el_val_t doc, el_val_t score); +el_val_t lsp_complete(el_val_t source, el_val_t pos); +el_val_t lsp_hover(el_val_t source, el_val_t pos); +el_val_t diag_json(el_val_t message, el_val_t severity, el_val_t line, el_val_t col); +el_val_t lsp_diagnostics(el_val_t source); +el_val_t outline_item_json(el_val_t kind, el_val_t name, el_val_t line); +el_val_t lsp_outline(el_val_t source); +el_val_t format_strip_trailing(el_val_t line); +el_val_t lsp_format(el_val_t source); +el_val_t lsp_type_graph(el_val_t source); +el_val_t collect_struct_fields(el_val_t source, el_val_t type_name); +el_val_t referenced_type(el_val_t field_str, el_val_t known_types); +el_val_t lsp_definition(el_val_t source, el_val_t pos); +el_val_t lsp_references(el_val_t source, el_val_t pos); +el_val_t lsp_jsonrpc(el_val_t method, el_val_t params_json); +el_val_t read_headers(void); +el_val_t lsp_stdio_loop(void); +el_val_t lsp_main(void); +el_val_t pkey(el_val_t name, el_val_t field); +el_val_t put_str(el_val_t name, el_val_t field, el_val_t value); +el_val_t put_bool(el_val_t name, el_val_t field, el_val_t value); +el_val_t get_str(el_val_t name, el_val_t field); +el_val_t get_bool(el_val_t name, el_val_t field); +el_val_t index_list(void); +el_val_t index_set(el_val_t names); +el_val_t index_contains(el_val_t name); +el_val_t index_add(el_val_t name); +el_val_t register_plugin(el_val_t name, el_val_t version, el_val_t description, el_val_t installed, el_val_t enabled, el_val_t hooks, el_val_t first_party); +el_val_t plugin_init(void); +el_val_t json_escape_str(el_val_t s); +el_val_t quote_json(el_val_t s); +el_val_t hooks_to_json(el_val_t hooks_csv); +el_val_t plugin_to_json(el_val_t name); +el_val_t plugin_list_json(void); +el_val_t plugin_get_json(el_val_t name); +el_val_t plugin_install(el_val_t name); +el_val_t plugin_remove(el_val_t name); +el_val_t plugin_enable(el_val_t name); +el_val_t plugin_disable(el_val_t name); +el_val_t host_main(void); +el_val_t jstr(el_val_t s); +el_val_t jerr(el_val_t msg); +el_val_t ok_obj(void); +el_val_t url_decode(el_val_t s); +el_val_t hex_digit(el_val_t ch); +el_val_t hex_to_int(el_val_t hex); +el_val_t char_from_code(el_val_t code); +el_val_t split_path_query(el_val_t path); +el_val_t query_get(el_val_t query, el_val_t key); +el_val_t cfg_port(void); +el_val_t cfg_project_path(void); +el_val_t cfg_engram_url(void); +el_val_t cfg_el_binary(void); +el_val_t safe_join(el_val_t rel); +el_val_t route_status(void); +el_val_t route_config(void); +el_val_t route_files_list(el_val_t query); +el_val_t route_file_read(el_val_t query); +el_val_t route_file_write(el_val_t body); +el_val_t route_lsp_complete(el_val_t query); +el_val_t route_lsp_hover(el_val_t query); +el_val_t route_lsp_errors(el_val_t query); +el_val_t route_lsp_jsonrpc(el_val_t body); +el_val_t route_outline(el_val_t query); +el_val_t route_definition(el_val_t query); +el_val_t route_references(el_val_t query); +el_val_t route_format(el_val_t body); +el_val_t route_type_graph(el_val_t query); +el_val_t active_theme(void); +el_val_t route_themes_list(void); +el_val_t route_themes_set(el_val_t body); +el_val_t route_plugins_list(void); +el_val_t route_plugins_install(el_val_t body); +el_val_t route_plugins_remove(el_val_t body); +el_val_t route_plugins_enable(el_val_t body); +el_val_t route_plugins_disable(el_val_t body); +el_val_t route_snippets(void); +el_val_t settings_path(void); +el_val_t default_settings_json(void); +el_val_t route_settings_get(void); +el_val_t route_settings_save(el_val_t body); +el_val_t route_settings_reset(void); +el_val_t route_build_or_run(el_val_t action, el_val_t body); +el_val_t route_build(el_val_t body); +el_val_t route_run(el_val_t body); +el_val_t route_reason(el_val_t body); +el_val_t route_activate_preview(el_val_t query); +el_val_t route_index(void); +el_val_t route_health(void); +el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body); +el_val_t server_main(void); + +el_val_t is_digit_ch(el_val_t ch) { + if (str_eq(ch, EL_STR("0"))) { + return 1; + } + if (str_eq(ch, EL_STR("1"))) { + return 1; + } + if (str_eq(ch, EL_STR("2"))) { + return 1; + } + if (str_eq(ch, EL_STR("3"))) { + return 1; + } + if (str_eq(ch, EL_STR("4"))) { + return 1; + } + if (str_eq(ch, EL_STR("5"))) { + return 1; + } + if (str_eq(ch, EL_STR("6"))) { + return 1; + } + if (str_eq(ch, EL_STR("7"))) { + return 1; + } + if (str_eq(ch, EL_STR("8"))) { + return 1; + } + if (str_eq(ch, EL_STR("9"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_alpha_ch(el_val_t ch) { + el_val_t lower = str_to_lower(ch); + if (str_eq(ch, EL_STR(""))) { + return 0; + } + if (!str_eq(lower, str_to_upper(lower))) { + return 1; + } + if (str_contains(EL_STR("abcdefghijklmnopqrstuvwxyz"), ch)) { + return 1; + } + if (str_contains(EL_STR("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), ch)) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_ident_ch(el_val_t ch) { + if (is_digit_ch(ch)) { + return 1; + } + if (is_alpha_ch(ch)) { + return 1; + } + if (str_eq(ch, EL_STR("_"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t extract_prefix(el_val_t source, el_val_t pos) { + el_val_t n = str_len(source); + el_val_t end = pos; + if (end > n) { + end = n; + } + el_val_t start = end; + el_val_t chars = native_string_chars(source); + el_val_t total = native_list_len(chars); + el_val_t scanning = 1; + while (scanning) { + if (start <= 0) { + scanning = 0; + } else { + el_val_t prev = native_list_get(chars, (start - 1)); + if (is_ident_ch(prev)) { + start = (start - 1); + } else { + scanning = 0; + } + } + } + return str_slice(source, start, end); + return 0; +} + +el_val_t extract_token(el_val_t source, el_val_t pos) { + el_val_t n = str_len(source); + if (pos > n) { + return EL_STR(""); + } + el_val_t chars = native_string_chars(source); + el_val_t total = native_list_len(chars); + el_val_t start = pos; + el_val_t go_left = 1; + while (go_left) { + if (start <= 0) { + go_left = 0; + } else { + el_val_t prev = native_list_get(chars, (start - 1)); + if (is_ident_ch(prev)) { + start = (start - 1); + } else { + go_left = 0; + } + } + } + el_val_t end = pos; + el_val_t go_right = 1; + while (go_right) { + if (end >= total) { + go_right = 0; + } else { + el_val_t cur = native_list_get(chars, end); + if (is_ident_ch(cur)) { + end = (end + 1); + } else { + go_right = 0; + } + } + } + return str_slice(source, start, end); + return 0; +} + +el_val_t pos_to_line_col(el_val_t source, el_val_t pos) { + el_val_t n = str_len(source); + el_val_t cap = pos; + if (cap > n) { + cap = n; + } + el_val_t chars = native_string_chars(source); + el_val_t i = 0; + el_val_t line = 1; + el_val_t col = 1; + while (i < cap) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\n"))) { + line = (line + 1); + col = 1; + } else { + col = (col + 1); + } + i = (i + 1); + } + return el_map_new(2, "line", line, "col", col); + return 0; +} + +el_val_t json_escape(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\""))) { + out = el_str_concat(out, EL_STR("\\\"")); + } else { + if (str_eq(ch, EL_STR("\\"))) { + out = el_str_concat(out, EL_STR("\\\\")); + } else { + if (str_eq(ch, EL_STR("\n"))) { + out = el_str_concat(out, EL_STR("\\n")); + } else { + if (str_eq(ch, EL_STR("\r"))) { + out = el_str_concat(out, EL_STR("\\r")); + } else { + if (str_eq(ch, EL_STR("\t"))) { + out = el_str_concat(out, EL_STR("\\t")); + } else { + out = el_str_concat(out, ch); + } + } + } + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t json_str(el_val_t s) { + return el_str_concat(el_str_concat(EL_STR("\""), json_escape(s)), EL_STR("\"")); + return 0; +} + +el_val_t json_kv_str(el_val_t k, el_val_t v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), json_str(v)); + return 0; +} + +el_val_t json_kv_int(el_val_t k, el_val_t v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), int_to_str(v)); + return 0; +} + +el_val_t json_kv_float(el_val_t k, el_val_t v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), float_to_str(v)); + return 0; +} + +el_val_t json_kv_raw(el_val_t k, el_val_t raw_v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), raw_v); + return 0; +} + +el_val_t json_array_of(el_val_t items) { + el_val_t n = native_list_len(items); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + out = el_str_concat(out, native_list_get(items, i)); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t keywords_list(void) { + el_val_t ks = native_list_empty(); + ks = native_list_append(ks, EL_STR("let")); + ks = native_list_append(ks, EL_STR("fn")); + ks = native_list_append(ks, EL_STR("type")); + ks = native_list_append(ks, EL_STR("enum")); + ks = native_list_append(ks, EL_STR("match")); + ks = native_list_append(ks, EL_STR("return")); + ks = native_list_append(ks, EL_STR("activate")); + ks = native_list_append(ks, EL_STR("where")); + ks = native_list_append(ks, EL_STR("sealed")); + ks = native_list_append(ks, EL_STR("if")); + ks = native_list_append(ks, EL_STR("else")); + ks = native_list_append(ks, EL_STR("for")); + ks = native_list_append(ks, EL_STR("in")); + ks = native_list_append(ks, EL_STR("while")); + ks = native_list_append(ks, EL_STR("true")); + ks = native_list_append(ks, EL_STR("false")); + ks = native_list_append(ks, EL_STR("test")); + ks = native_list_append(ks, EL_STR("seed")); + ks = native_list_append(ks, EL_STR("assert")); + ks = native_list_append(ks, EL_STR("target")); + ks = native_list_append(ks, EL_STR("protocol")); + ks = native_list_append(ks, EL_STR("impl")); + ks = native_list_append(ks, EL_STR("import")); + ks = native_list_append(ks, EL_STR("from")); + ks = native_list_append(ks, EL_STR("as")); + ks = native_list_append(ks, EL_STR("with")); + ks = native_list_append(ks, EL_STR("retry")); + ks = native_list_append(ks, EL_STR("times")); + ks = native_list_append(ks, EL_STR("fallback")); + ks = native_list_append(ks, EL_STR("reason")); + ks = native_list_append(ks, EL_STR("parallel")); + ks = native_list_append(ks, EL_STR("trace")); + ks = native_list_append(ks, EL_STR("requires")); + ks = native_list_append(ks, EL_STR("deploy")); + ks = native_list_append(ks, EL_STR("to")); + ks = native_list_append(ks, EL_STR("via")); + ks = native_list_append(ks, EL_STR("vessel")); + ks = native_list_append(ks, EL_STR("cgi")); + return ks; + return 0; +} + +el_val_t builtin_type_names(void) { + el_val_t ts = native_list_empty(); + ts = native_list_append(ts, EL_STR("Int")); + ts = native_list_append(ts, EL_STR("Float")); + ts = native_list_append(ts, EL_STR("String")); + ts = native_list_append(ts, EL_STR("Bool")); + ts = native_list_append(ts, EL_STR("Uuid")); + ts = native_list_append(ts, EL_STR("Void")); + ts = native_list_append(ts, EL_STR("Any")); + return ts; + return 0; +} + +el_val_t builtin_type_doc(el_val_t name) { + if (str_eq(name, EL_STR("Int"))) { + return EL_STR("64-bit signed integer"); + } + if (str_eq(name, EL_STR("Float"))) { + return EL_STR("64-bit IEEE 754 double"); + } + if (str_eq(name, EL_STR("String"))) { + return EL_STR("UTF-8 string"); + } + if (str_eq(name, EL_STR("Bool"))) { + return EL_STR("Boolean value"); + } + if (str_eq(name, EL_STR("Uuid"))) { + return EL_STR("RFC 4122 UUID"); + } + if (str_eq(name, EL_STR("Void"))) { + return EL_STR("Unit type — no value"); + } + if (str_eq(name, EL_STR("Any"))) { + return EL_STR("Dynamically-typed value"); + } + return EL_STR(""); + return 0; +} + +el_val_t builtin_fns(void) { + el_val_t xs = native_list_empty(); + xs = native_list_append(xs, EL_STR("println")); + xs = native_list_append(xs, EL_STR("fn(value: String) -> Void")); + xs = native_list_append(xs, EL_STR("print")); + xs = native_list_append(xs, EL_STR("fn(value: String) -> Void")); + xs = native_list_append(xs, EL_STR("readline")); + xs = native_list_append(xs, EL_STR("fn() -> String")); + xs = native_list_append(xs, EL_STR("args")); + xs = native_list_append(xs, EL_STR("fn() -> [String]")); + xs = native_list_append(xs, EL_STR("int_to_str")); + xs = native_list_append(xs, EL_STR("fn(n: Int) -> String")); + xs = native_list_append(xs, EL_STR("str_to_int")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Int")); + xs = native_list_append(xs, EL_STR("str_len")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Int")); + xs = native_list_append(xs, EL_STR("str_slice")); + xs = native_list_append(xs, EL_STR("fn(s: String, start: Int, end: Int) -> String")); + xs = native_list_append(xs, EL_STR("str_concat")); + xs = native_list_append(xs, EL_STR("fn(a: String, b: String) -> String")); + xs = native_list_append(xs, EL_STR("str_contains")); + xs = native_list_append(xs, EL_STR("fn(s: String, sub: String) -> Bool")); + xs = native_list_append(xs, EL_STR("str_starts_with")); + xs = native_list_append(xs, EL_STR("fn(s: String, p: String) -> Bool")); + xs = native_list_append(xs, EL_STR("str_ends_with")); + xs = native_list_append(xs, EL_STR("fn(s: String, suf: String) -> Bool")); + xs = native_list_append(xs, EL_STR("str_to_upper")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> String")); + xs = native_list_append(xs, EL_STR("str_to_lower")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> String")); + xs = native_list_append(xs, EL_STR("str_split")); + xs = native_list_append(xs, EL_STR("fn(s: String, sep: String) -> [String]")); + xs = native_list_append(xs, EL_STR("str_trim")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> String")); + xs = native_list_append(xs, EL_STR("str_replace")); + xs = native_list_append(xs, EL_STR("fn(s: String, from: String, to: String) -> String")); + xs = native_list_append(xs, EL_STR("str_index_of")); + xs = native_list_append(xs, EL_STR("fn(s: String, sub: String) -> Int")); + xs = native_list_append(xs, EL_STR("float_to_str")); + xs = native_list_append(xs, EL_STR("fn(f: Float) -> String")); + xs = native_list_append(xs, EL_STR("str_to_float")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Float")); + xs = native_list_append(xs, EL_STR("math_sqrt")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_log")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_sin")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_cos")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_pi")); + xs = native_list_append(xs, EL_STR("fn() -> Float")); + xs = native_list_append(xs, EL_STR("el_abs")); + xs = native_list_append(xs, EL_STR("fn(n: Int) -> Int")); + xs = native_list_append(xs, EL_STR("el_min")); + xs = native_list_append(xs, EL_STR("fn(a: Int, b: Int) -> Int")); + xs = native_list_append(xs, EL_STR("el_max")); + xs = native_list_append(xs, EL_STR("fn(a: Int, b: Int) -> Int")); + xs = native_list_append(xs, EL_STR("el_list_empty")); + xs = native_list_append(xs, EL_STR("fn() -> [Any]")); + xs = native_list_append(xs, EL_STR("el_list_len")); + xs = native_list_append(xs, EL_STR("fn(l: [Any]) -> Int")); + xs = native_list_append(xs, EL_STR("el_list_get")); + xs = native_list_append(xs, EL_STR("fn(l: [Any], i: Int) -> Any")); + xs = native_list_append(xs, EL_STR("el_list_append")); + xs = native_list_append(xs, EL_STR("fn(l: [Any], v: Any) -> [Any]")); + xs = native_list_append(xs, EL_STR("list_join")); + xs = native_list_append(xs, EL_STR("fn(l: [String], sep: String) -> String")); + xs = native_list_append(xs, EL_STR("list_range")); + xs = native_list_append(xs, EL_STR("fn(start: Int, end: Int) -> [Int]")); + xs = native_list_append(xs, EL_STR("el_map_get")); + xs = native_list_append(xs, EL_STR("fn(m: Map, k: String) -> Any")); + xs = native_list_append(xs, EL_STR("el_map_set")); + xs = native_list_append(xs, EL_STR("fn(m: Map, k: String, v: Any) -> Map")); + xs = native_list_append(xs, EL_STR("json_parse")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Any")); + xs = native_list_append(xs, EL_STR("json_stringify")); + xs = native_list_append(xs, EL_STR("fn(v: Any) -> String")); + xs = native_list_append(xs, EL_STR("json_get_string")); + xs = native_list_append(xs, EL_STR("fn(j: String, k: String) -> String")); + xs = native_list_append(xs, EL_STR("json_get_int")); + xs = native_list_append(xs, EL_STR("fn(j: String, k: String) -> Int")); + xs = native_list_append(xs, EL_STR("json_get_bool")); + xs = native_list_append(xs, EL_STR("fn(j: String, k: String) -> Bool")); + xs = native_list_append(xs, EL_STR("http_get")); + xs = native_list_append(xs, EL_STR("fn(url: String) -> String")); + xs = native_list_append(xs, EL_STR("http_post")); + xs = native_list_append(xs, EL_STR("fn(url: String, body: String) -> String")); + xs = native_list_append(xs, EL_STR("fs_read")); + xs = native_list_append(xs, EL_STR("fn(path: String) -> String")); + xs = native_list_append(xs, EL_STR("fs_write")); + xs = native_list_append(xs, EL_STR("fn(path: String, content: String) -> Bool")); + xs = native_list_append(xs, EL_STR("fs_list")); + xs = native_list_append(xs, EL_STR("fn(path: String) -> [String]")); + xs = native_list_append(xs, EL_STR("env")); + xs = native_list_append(xs, EL_STR("fn(key: String) -> String")); + xs = native_list_append(xs, EL_STR("uuid_new")); + xs = native_list_append(xs, EL_STR("fn() -> String")); + xs = native_list_append(xs, EL_STR("time_now")); + xs = native_list_append(xs, EL_STR("fn() -> Int")); + xs = native_list_append(xs, EL_STR("time_now_utc")); + xs = native_list_append(xs, EL_STR("fn() -> Int")); + xs = native_list_append(xs, EL_STR("sleep_ms")); + xs = native_list_append(xs, EL_STR("fn(ms: Int) -> Void")); + xs = native_list_append(xs, EL_STR("engram_node")); + xs = native_list_append(xs, EL_STR("fn(content: String, kind: String, salience: Float) -> String")); + xs = native_list_append(xs, EL_STR("engram_search")); + xs = native_list_append(xs, EL_STR("fn(q: String, limit: Int) -> [Map]")); + xs = native_list_append(xs, EL_STR("engram_activate")); + xs = native_list_append(xs, EL_STR("fn(q: String, depth: Int) -> [Map]")); + xs = native_list_append(xs, EL_STR("engram_neighbors")); + xs = native_list_append(xs, EL_STR("fn(id: String) -> [Map]")); + xs = native_list_append(xs, EL_STR("engram_connect")); + xs = native_list_append(xs, EL_STR("fn(from: String, to: String, w: Float, rel: String) -> Void")); + xs = native_list_append(xs, EL_STR("dharma_connect")); + xs = native_list_append(xs, EL_STR("fn(cgi: String) -> String")); + xs = native_list_append(xs, EL_STR("dharma_send")); + xs = native_list_append(xs, EL_STR("fn(channel: String, content: String) -> String")); + xs = native_list_append(xs, EL_STR("dharma_emit")); + xs = native_list_append(xs, EL_STR("fn(event: String, payload: String) -> Void")); + xs = native_list_append(xs, EL_STR("dharma_field")); + xs = native_list_append(xs, EL_STR("fn(event: String) -> Map")); + xs = native_list_append(xs, EL_STR("llm_call")); + xs = native_list_append(xs, EL_STR("fn(model: String, prompt: String) -> String")); + return xs; + return 0; +} + +el_val_t keyword_doc(el_val_t kw) { + if (str_eq(kw, EL_STR("let"))) { + return EL_STR("let name: Type = expr — declare an immutable binding."); + } + if (str_eq(kw, EL_STR("fn"))) { + return EL_STR("fn name(params) -> Ret { body } — define a function."); + } + if (str_eq(kw, EL_STR("type"))) { + return EL_STR("type Name { field: Type } — struct definition."); + } + if (str_eq(kw, EL_STR("enum"))) { + return EL_STR("enum Name { Variant1, Variant2(Type) } — enum definition."); + } + if (str_eq(kw, EL_STR("match"))) { + return EL_STR("match expr { Pattern => body } — pattern match."); + } + if (str_eq(kw, EL_STR("return"))) { + return EL_STR("return value — exit the enclosing function."); + } + if (str_eq(kw, EL_STR("if"))) { + return EL_STR("if cond { ... } else { ... } — conditional."); + } + if (str_eq(kw, EL_STR("for"))) { + return EL_STR("for item in collection { ... } — iterate."); + } + if (str_eq(kw, EL_STR("while"))) { + return EL_STR("while cond { ... } — loop while cond is truthy."); + } + if (str_eq(kw, EL_STR("import"))) { + return EL_STR("import \"path.el\" — pull in another module."); + } + if (str_eq(kw, EL_STR("from"))) { + return EL_STR("from module import { Name } — selective import."); + } + if (str_eq(kw, EL_STR("vessel"))) { + return EL_STR("vessel \"name\" { ... } — manifest declaration."); + } + if (str_eq(kw, EL_STR("cgi"))) { + return EL_STR("cgi \"name\" { dharma_id: ... } — CGI identity."); + } + if (str_eq(kw, EL_STR("activate"))) { + return EL_STR("activate Type where \"query\" — spreading-activation retrieval."); + } + if (str_eq(kw, EL_STR("sealed"))) { + return EL_STR("sealed { ... } — capability-restricted block."); + } + if (str_eq(kw, EL_STR("protocol"))) { + return EL_STR("protocol Name { fn method(self) -> R } — trait-like."); + } + if (str_eq(kw, EL_STR("impl"))) { + return EL_STR("impl Protocol for Type { ... } — protocol implementation."); + } + if (str_eq(kw, EL_STR("with"))) { + return EL_STR("with retry/fallback clause."); + } + if (str_eq(kw, EL_STR("retry"))) { + return EL_STR("retry times N — retry policy."); + } + if (str_eq(kw, EL_STR("fallback"))) { + return EL_STR("fallback { default } — fallback on failure."); + } + if (str_eq(kw, EL_STR("true"))) { + return EL_STR("Boolean literal."); + } + if (str_eq(kw, EL_STR("false"))) { + return EL_STR("Boolean literal."); + } + return EL_STR(""); + return 0; +} + +el_val_t first_ident_in(el_val_t rest) { + el_val_t trimmed = str_trim(rest); + el_val_t n = str_len(trimmed); + if (n == 0) { + return EL_STR(""); + } + el_val_t chars = native_string_chars(trimmed); + el_val_t i = 0; + el_val_t total = native_list_len(chars); + while (i < total) { + el_val_t ch = native_list_get(chars, i); + if (!is_ident_ch(ch)) { + return str_slice(trimmed, 0, i); + } + i = (i + 1); + } + return trimmed; + return 0; +} + +el_val_t scan_decls(el_val_t source) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t types = native_list_empty(); + el_val_t kinds = native_list_empty(); + el_val_t fns = native_list_empty(); + el_val_t tlines = native_list_empty(); + el_val_t flines = native_list_empty(); + el_val_t i = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + el_val_t lineno = (i + 1); + if (str_starts_with(trimmed, EL_STR("fn "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + fns = native_list_append(fns, name); + flines = native_list_append(flines, lineno); + } + } else { + if (str_starts_with(trimmed, EL_STR("type "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + types = native_list_append(types, name); + kinds = native_list_append(kinds, EL_STR("type")); + tlines = native_list_append(tlines, lineno); + } + } else { + if (str_starts_with(trimmed, EL_STR("enum "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + types = native_list_append(types, name); + kinds = native_list_append(kinds, EL_STR("enum")); + tlines = native_list_append(tlines, lineno); + } + } else { + if (str_starts_with(trimmed, EL_STR("protocol "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + types = native_list_append(types, name); + kinds = native_list_append(kinds, EL_STR("protocol")); + tlines = native_list_append(tlines, lineno); + } + } + } + } + } + i = (i + 1); + } + return el_map_new(5, "types", types, "type_kind", kinds, "fns", fns, "type_lines", tlines, "fn_lines", flines); + return 0; +} + +el_val_t list_contains_str(el_val_t xs, el_val_t needle) { + el_val_t n = native_list_len(xs); + el_val_t i = 0; + while (i < n) { + if (str_eq(native_list_get(xs, i), needle)) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t prefix_score(el_val_t label, el_val_t prefix) { + if (str_eq(prefix, EL_STR(""))) { + return el_from_float(0.5); + } + el_val_t lower_label = str_to_lower(label); + el_val_t lower_prefix = str_to_lower(prefix); + if (str_starts_with(lower_label, lower_prefix)) { + el_val_t lp = str_len(lower_prefix); + el_val_t ll = str_len(lower_label); + if (ll == 0) { + return el_from_float(0.0); + } + return (int_to_float(lp) / int_to_float(ll)); + } + return el_from_float(0.0); + return 0; +} + +el_val_t completion_json(el_val_t label, el_val_t kind, el_val_t detail, el_val_t doc, el_val_t score) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("label"), label)); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), kind)); + parts = native_list_append(parts, json_kv_str(EL_STR("detail"), detail)); + if (str_eq(doc, EL_STR(""))) { + parts = native_list_append(parts, json_kv_raw(EL_STR("documentation"), EL_STR("null"))); + } else { + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), doc)); + } + parts = native_list_append(parts, json_kv_float(EL_STR("score"), score)); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t lsp_complete(el_val_t source, el_val_t pos) { + el_val_t prefix = extract_prefix(source, pos); + el_val_t lower_prefix = str_to_lower(prefix); + el_val_t prefix_empty = str_eq(prefix, EL_STR("")); + el_val_t items = native_list_empty(); + el_val_t kws = keywords_list(); + el_val_t nk = native_list_len(kws); + el_val_t i = 0; + while (i < nk) { + el_val_t kw = native_list_get(kws, i); + if (prefix_empty) { + el_val_t s = prefix_score(kw, prefix); + items = native_list_append(items, completion_json(kw, EL_STR("keyword"), EL_STR("keyword"), keyword_doc(kw), s)); + } else { + if (str_starts_with(kw, prefix)) { + el_val_t s = prefix_score(kw, prefix); + items = native_list_append(items, completion_json(kw, EL_STR("keyword"), EL_STR("keyword"), keyword_doc(kw), s)); + } + } + i = (i + 1); + } + el_val_t bts = builtin_type_names(); + el_val_t nb = native_list_len(bts); + i = 0; + while (i < nb) { + el_val_t name = native_list_get(bts, i); + el_val_t lname = str_to_lower(name); + if (prefix_empty) { + el_val_t s = (el_from_float(0.5) + el_from_float(0.1)); + items = native_list_append(items, completion_json(name, EL_STR("type"), builtin_type_doc(name), el_str_concat(EL_STR("Built-in type: "), builtin_type_doc(name)), s)); + } else { + if (str_starts_with(lname, lower_prefix)) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.1)); + items = native_list_append(items, completion_json(name, EL_STR("type"), builtin_type_doc(name), el_str_concat(EL_STR("Built-in type: "), builtin_type_doc(name)), s)); + } + } + i = (i + 1); + } + el_val_t decls = scan_decls(source); + el_val_t utypes = el_get_field(decls, EL_STR("types")); + el_val_t ukinds = el_get_field(decls, EL_STR("type_kind")); + el_val_t nu = native_list_len(utypes); + i = 0; + while (i < nu) { + el_val_t name = native_list_get(utypes, i); + el_val_t kind = native_list_get(ukinds, i); + el_val_t lname = str_to_lower(name); + el_val_t include = 0; + if (prefix_empty) { + include = 1; + } + if (str_starts_with(lname, lower_prefix)) { + include = 1; + } + if (include) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.15)); + items = native_list_append(items, completion_json(name, EL_STR("type"), el_str_concat(el_str_concat(kind, EL_STR(" ")), name), el_str_concat(el_str_concat(el_str_concat(EL_STR("User-defined "), kind), EL_STR(" ")), name), s)); + } + i = (i + 1); + } + el_val_t bfns = builtin_fns(); + el_val_t nf = native_list_len(bfns); + i = 0; + while (i < nf) { + el_val_t name = native_list_get(bfns, i); + el_val_t sig = native_list_get(bfns, (i + 1)); + el_val_t lname = str_to_lower(name); + el_val_t include = 0; + if (prefix_empty) { + include = 1; + } + if (str_starts_with(lname, lower_prefix)) { + include = 1; + } + if (include) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.11)); + items = native_list_append(items, completion_json(name, EL_STR("function"), sig, el_str_concat(el_str_concat(el_str_concat(EL_STR("Built-in: "), name), EL_STR(" :: ")), sig), s)); + } + i = (i + 2); + } + el_val_t ufns = el_get_field(decls, EL_STR("fns")); + el_val_t nuf = native_list_len(ufns); + i = 0; + while (i < nuf) { + el_val_t name = native_list_get(ufns, i); + el_val_t lname = str_to_lower(name); + el_val_t include = 0; + if (prefix_empty) { + include = 1; + } + if (str_starts_with(lname, lower_prefix)) { + include = 1; + } + if (include) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.12)); + items = native_list_append(items, completion_json(name, EL_STR("function"), el_str_concat(EL_STR("fn "), name), el_str_concat(EL_STR("User function: "), name), s)); + } + i = (i + 1); + } + return json_array_of(items); + return 0; +} + +el_val_t lsp_hover(el_val_t source, el_val_t pos) { + el_val_t token = extract_token(source, pos); + if (str_eq(token, EL_STR(""))) { + return EL_STR("null"); + } + el_val_t doc = builtin_type_doc(token); + if (!str_eq(doc, EL_STR(""))) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), doc)); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + el_val_t decls = scan_decls(source); + el_val_t utypes = el_get_field(decls, EL_STR("types")); + el_val_t ukinds = el_get_field(decls, EL_STR("type_kind")); + el_val_t nu = native_list_len(utypes); + el_val_t i = 0; + while (i < nu) { + el_val_t name = native_list_get(utypes, i); + if (str_eq(name, token)) { + el_val_t kind = native_list_get(ukinds, i); + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), el_str_concat(el_str_concat(kind, EL_STR(" ")), token))); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + i = (i + 1); + } + el_val_t ufns = el_get_field(decls, EL_STR("fns")); + el_val_t nuf = native_list_len(ufns); + i = 0; + while (i < nuf) { + el_val_t name = native_list_get(ufns, i); + if (str_eq(name, token)) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), el_str_concat(EL_STR("fn "), token))); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + i = (i + 1); + } + el_val_t bfns = builtin_fns(); + el_val_t nf = native_list_len(bfns); + i = 0; + while (i < nf) { + el_val_t name = native_list_get(bfns, i); + if (str_eq(name, token)) { + el_val_t sig = native_list_get(bfns, (i + 1)); + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), el_str_concat(el_str_concat(el_str_concat(EL_STR("fn "), token), EL_STR(" :: ")), sig))); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + i = (i + 2); + } + el_val_t kdoc = keyword_doc(token); + if (!str_eq(kdoc, EL_STR(""))) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), kdoc)); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + return EL_STR("null"); + return 0; +} + +el_val_t diag_json(el_val_t message, el_val_t severity, el_val_t line, el_val_t col) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("message"), message)); + parts = native_list_append(parts, json_kv_str(EL_STR("severity"), severity)); + if (line > 0) { + parts = native_list_append(parts, json_kv_int(EL_STR("line"), line)); + parts = native_list_append(parts, json_kv_int(EL_STR("col"), col)); + } else { + parts = native_list_append(parts, json_kv_raw(EL_STR("line"), EL_STR("null"))); + parts = native_list_append(parts, json_kv_raw(EL_STR("col"), EL_STR("null"))); + } + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t lsp_diagnostics(el_val_t source) { + el_val_t chars = native_string_chars(source); + el_val_t n = native_list_len(chars); + el_val_t diags = native_list_empty(); + el_val_t depth_brace = 0; + el_val_t depth_paren = 0; + el_val_t depth_brack = 0; + el_val_t i = 0; + el_val_t line = 1; + el_val_t col = 1; + el_val_t in_string = 0; + el_val_t string_start_line = 0; + el_val_t string_start_col = 0; + el_val_t in_comment = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (in_comment) { + if (str_eq(ch, EL_STR("\n"))) { + in_comment = 0; + line = (line + 1); + col = 1; + } else { + col = (col + 1); + } + i = (i + 1); + } else { + if (in_string) { + if (str_eq(ch, EL_STR("\\"))) { + i = (i + 1); + col = (col + 1); + if (i < n) { + el_val_t esc = native_list_get(chars, i); + if (str_eq(esc, EL_STR("\n"))) { + line = (line + 1); + col = 1; + } else { + col = (col + 1); + } + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("\""))) { + in_string = 0; + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("\n"))) { + line = (line + 1); + col = 1; + i = (i + 1); + } else { + col = (col + 1); + i = (i + 1); + } + } + } + } else { + if (str_eq(ch, EL_STR("\""))) { + in_string = 1; + string_start_line = line; + string_start_col = col; + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("/"))) { + el_val_t next_i = (i + 1); + el_val_t is_line_comment = 0; + if (next_i < n) { + el_val_t next_ch = native_list_get(chars, next_i); + if (str_eq(next_ch, EL_STR("/"))) { + is_line_comment = 1; + } + } + if (is_line_comment) { + in_comment = 1; + i = (i + 2); + col = (col + 2); + } else { + i = (i + 1); + col = (col + 1); + } + } else { + if (str_eq(ch, EL_STR("{"))) { + depth_brace = (depth_brace + 1); + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("}"))) { + depth_brace = (depth_brace - 1); + if (depth_brace < 0) { + diags = native_list_append(diags, diag_json(EL_STR("unmatched `}`"), EL_STR("error"), line, col)); + depth_brace = 0; + } + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("("))) { + depth_paren = (depth_paren + 1); + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR(")"))) { + depth_paren = (depth_paren - 1); + if (depth_paren < 0) { + diags = native_list_append(diags, diag_json(EL_STR("unmatched `)`"), EL_STR("error"), line, col)); + depth_paren = 0; + } + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("["))) { + depth_brack = (depth_brack + 1); + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("]"))) { + depth_brack = (depth_brack - 1); + if (depth_brack < 0) { + diags = native_list_append(diags, diag_json(EL_STR("unmatched `]`"), EL_STR("error"), line, col)); + depth_brack = 0; + } + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("\n"))) { + line = (line + 1); + col = 1; + i = (i + 1); + } else { + i = (i + 1); + col = (col + 1); + } + } + } + } + } + } + } + } + } + } + } + } + if (in_string) { + diags = native_list_append(diags, diag_json(EL_STR("unterminated string literal"), EL_STR("error"), string_start_line, string_start_col)); + } + if (depth_brace > 0) { + diags = native_list_append(diags, diag_json(EL_STR("unclosed `{` (missing `}`)"), EL_STR("error"), 0, 0)); + } + if (depth_paren > 0) { + diags = native_list_append(diags, diag_json(EL_STR("unclosed `(` (missing `)`)"), EL_STR("error"), 0, 0)); + } + if (depth_brack > 0) { + diags = native_list_append(diags, diag_json(EL_STR("unclosed `[` (missing `]`)"), EL_STR("error"), 0, 0)); + } + return json_array_of(diags); + return 0; +} + +el_val_t outline_item_json(el_val_t kind, el_val_t name, el_val_t line) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), kind)); + parts = native_list_append(parts, json_kv_str(EL_STR("name"), name)); + parts = native_list_append(parts, json_kv_int(EL_STR("line"), line)); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t lsp_outline(el_val_t source) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t items = native_list_empty(); + el_val_t i = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + el_val_t lineno = (i + 1); + if (str_starts_with(trimmed, EL_STR("fn "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("fn"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("type "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("type"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("enum "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("enum"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("protocol "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("protocol"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("impl "))) { + el_val_t rest = str_trim(str_slice(trimmed, 5, str_len(trimmed))); + el_val_t brace = str_index_of(rest, EL_STR("{")); + el_val_t name = rest; + if (brace > 0) { + name = str_trim(str_slice(rest, 0, brace)); + } + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("impl"), name, lineno)); + } + } + } + } + } + } + i = (i + 1); + } + return json_array_of(items); + return 0; +} + +el_val_t format_strip_trailing(el_val_t line) { + el_val_t n = str_len(line); + if (n == 0) { + return line; + } + el_val_t chars = native_string_chars(line); + el_val_t i = (n - 1); + el_val_t trimming = 1; + while (trimming) { + if (i < 0) { + trimming = 0; + } else { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR(" "))) { + i = (i - 1); + } else { + if (str_eq(ch, EL_STR("\t"))) { + i = (i - 1); + } else { + trimming = 0; + } + } + } + } + return str_slice(line, 0, (i + 1)); + return 0; +} + +el_val_t lsp_format(el_val_t source) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t cleaned = native_list_empty(); + el_val_t i = 0; + el_val_t blank_run = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t stripped = format_strip_trailing(line); + if (str_eq(stripped, EL_STR(""))) { + blank_run = (blank_run + 1); + if (blank_run <= 2) { + cleaned = native_list_append(cleaned, EL_STR("")); + } + } else { + blank_run = 0; + cleaned = native_list_append(cleaned, stripped); + } + i = (i + 1); + } + el_val_t joined = list_join(cleaned, EL_STR("\n")); + el_val_t final = joined; + if (!str_ends_with(final, EL_STR("\n"))) { + final = el_str_concat(final, EL_STR("\n")); + } + el_val_t changed = EL_STR("false"); + if (!str_eq(final, source)) { + changed = EL_STR("true"); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{"), json_kv_str(EL_STR("content"), final)), EL_STR(",")), json_kv_raw(EL_STR("changed"), changed)), EL_STR("}")); + return 0; +} + +el_val_t lsp_type_graph(el_val_t source) { + el_val_t nodes = native_list_empty(); + el_val_t edges = native_list_empty(); + el_val_t bts = builtin_type_names(); + el_val_t nb = native_list_len(bts); + el_val_t i = 0; + while (i < nb) { + el_val_t name = native_list_get(bts, i); + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("id"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("name"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), EL_STR("builtin"))); + parts = native_list_append(parts, json_kv_raw(EL_STR("fields"), EL_STR("[]"))); + nodes = native_list_append(nodes, el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"))); + i = (i + 1); + } + el_val_t decls = scan_decls(source); + el_val_t utypes = el_get_field(decls, EL_STR("types")); + el_val_t ukinds = el_get_field(decls, EL_STR("type_kind")); + el_val_t tlines = el_get_field(decls, EL_STR("type_lines")); + el_val_t nu = native_list_len(utypes); + i = 0; + el_val_t bts_dup = builtin_type_names(); + while (i < nu) { + el_val_t name = native_list_get(utypes, i); + el_val_t kind = native_list_get(ukinds, i); + if (!list_contains_str(bts_dup, name)) { + el_val_t fields = collect_struct_fields(source, name); + el_val_t field_jsons = native_list_empty(); + el_val_t fn2 = native_list_len(fields); + el_val_t j = 0; + while (j < fn2) { + field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j))); + j = (j + 1); + } + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("id"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("name"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), kind)); + parts = native_list_append(parts, json_kv_raw(EL_STR("fields"), json_array_of(field_jsons))); + nodes = native_list_append(nodes, el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"))); + j = 0; + while (j < fn2) { + el_val_t field_str = native_list_get(fields, j); + el_val_t target = referenced_type(field_str, utypes); + if (!str_eq(EL_NULL, EL_STR(""))) { + el_val_t eparts = native_list_empty(); + eparts = native_list_append(eparts, json_kv_str(EL_STR("from"), name)); + eparts = native_list_append(eparts, json_kv_str(EL_STR("to"), EL_NULL)); + eparts = native_list_append(eparts, json_kv_str(EL_STR("label"), EL_STR("field"))); + edges = native_list_append(edges, el_str_concat(el_str_concat(EL_STR("{"), list_join(eparts, EL_STR(","))), EL_STR("}"))); + } + j = (j + 1); + } + } + i = (i + 1); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{"), json_kv_raw(EL_STR("nodes"), json_array_of(nodes))), EL_STR(",")), json_kv_raw(EL_STR("edges"), json_array_of(edges))), EL_STR("}")); + return 0; +} + +el_val_t collect_struct_fields(el_val_t source, el_val_t type_name) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t fields = native_list_empty(); + el_val_t inside = 0; + el_val_t i = 0; + el_val_t header_a = el_str_concat(EL_STR("type "), type_name); + el_val_t header_b = el_str_concat(EL_STR("enum "), type_name); + el_val_t header_c = el_str_concat(EL_STR("protocol "), type_name); + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + if (!inside) { + if (str_starts_with(trimmed, header_a)) { + inside = 1; + } + if (str_starts_with(trimmed, header_b)) { + inside = 1; + } + if (str_starts_with(trimmed, header_c)) { + inside = 1; + } + } else { + if (str_starts_with(trimmed, EL_STR("}"))) { + return fields; + } + if (!str_eq(trimmed, EL_STR("{"))) { + if (!str_eq(trimmed, EL_STR(""))) { + if (!str_starts_with(trimmed, EL_STR("//"))) { + el_val_t entry = trimmed; + if (str_ends_with(entry, EL_STR(","))) { + entry = str_slice(entry, 0, (str_len(entry) - 1)); + } + fields = native_list_append(fields, str_trim(entry)); + } + } + } + } + i = (i + 1); + } + return fields; + return 0; +} + +el_val_t referenced_type(el_val_t field_str, el_val_t known_types) { + el_val_t colon = str_index_of(field_str, EL_STR(":")); + if (colon < 0) { + return EL_STR(""); + } + el_val_t after = str_trim(str_slice(field_str, (colon + 1), str_len(field_str))); + el_val_t trimmed_l = after; + if (str_starts_with(trimmed_l, EL_STR("["))) { + el_val_t end = str_index_of(trimmed_l, EL_STR("]")); + if (end > 1) { + trimmed_l = str_trim(str_slice(trimmed_l, 1, end)); + } + } + el_val_t ident = first_ident_in(trimmed_l); + if (str_eq(ident, EL_STR(""))) { + return EL_STR(""); + } + el_val_t bts = builtin_type_names(); + if (list_contains_str(bts, ident)) { + return ident; + } + if (list_contains_str(known_types, ident)) { + return ident; + } + return EL_STR(""); + return 0; +} + +el_val_t lsp_definition(el_val_t source, el_val_t pos) { + el_val_t word = extract_token(source, pos); + if (str_eq(word, EL_STR(""))) { + return EL_STR("null"); + } + el_val_t patterns = native_list_empty(); + patterns = native_list_append(patterns, el_str_concat(EL_STR("fn "), word)); + patterns = native_list_append(patterns, el_str_concat(EL_STR("type "), word)); + patterns = native_list_append(patterns, el_str_concat(EL_STR("enum "), word)); + patterns = native_list_append(patterns, el_str_concat(EL_STR("protocol "), word)); + el_val_t np = native_list_len(patterns); + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t nl = native_list_len(lines); + el_val_t i = 0; + while (i < nl) { + el_val_t line = native_list_get(lines, i); + el_val_t j = 0; + while (j < np) { + el_val_t pat = native_list_get(patterns, j); + el_val_t idx = str_index_of(line, pat); + if (idx >= 0) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_int(EL_STR("line"), (i + 1))); + parts = native_list_append(parts, json_kv_int(EL_STR("col"), (idx + 1))); + parts = native_list_append(parts, json_kv_str(EL_STR("snippet"), str_trim(line))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + j = (j + 1); + } + i = (i + 1); + } + return EL_STR("null"); + return 0; +} + +el_val_t lsp_references(el_val_t source, el_val_t pos) { + el_val_t word = extract_token(source, pos); + if (str_eq(word, EL_STR(""))) { + return EL_STR("[]"); + } + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t nl = native_list_len(lines); + el_val_t refs = native_list_empty(); + el_val_t i = 0; + while (i < nl) { + el_val_t line = native_list_get(lines, i); + el_val_t line_chars = native_string_chars(line); + el_val_t llen = native_list_len(line_chars); + el_val_t wlen = str_len(word); + el_val_t off = 0; + el_val_t scanning = 1; + while (scanning) { + el_val_t idx = str_index_of(str_slice(line, off, str_len(line)), word); + if (idx < 0) { + scanning = 0; + } else { + el_val_t abs = (off + idx); + el_val_t before_ok = 1; + if (abs > 0) { + el_val_t prev = native_list_get(line_chars, (abs - 1)); + if (is_ident_ch(prev)) { + before_ok = 0; + } + } + el_val_t after_ok = 1; + el_val_t after_pos = (abs + wlen); + if (after_pos < llen) { + el_val_t nxt = native_list_get(line_chars, after_pos); + if (is_ident_ch(nxt)) { + after_ok = 0; + } + } + if (before_ok) { + if (after_ok) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_int(EL_STR("line"), (i + 1))); + parts = native_list_append(parts, json_kv_int(EL_STR("col"), (abs + 1))); + parts = native_list_append(parts, json_kv_str(EL_STR("snippet"), str_trim(line))); + refs = native_list_append(refs, el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"))); + } + } + off = (abs + wlen); + if (off >= str_len(line)) { + scanning = 0; + } + } + } + i = (i + 1); + } + return json_array_of(refs); + return 0; +} + +el_val_t lsp_jsonrpc(el_val_t method, el_val_t params_json) { + el_val_t source = json_get_string(params_json, EL_STR("source")); + el_val_t pos = json_get_int(params_json, EL_STR("pos")); + if (str_eq(method, EL_STR("textDocument/completion"))) { + return lsp_complete(source, pos); + } + if (str_eq(method, EL_STR("textDocument/hover"))) { + return lsp_hover(source, pos); + } + if (str_eq(method, EL_STR("textDocument/diagnostic"))) { + return lsp_diagnostics(source); + } + if (str_eq(method, EL_STR("textDocument/formatting"))) { + return lsp_format(source); + } + if (str_eq(method, EL_STR("textDocument/documentSymbol"))) { + return lsp_outline(source); + } + if (str_eq(method, EL_STR("textDocument/definition"))) { + return lsp_definition(source, pos); + } + if (str_eq(method, EL_STR("textDocument/references"))) { + return lsp_references(source, pos); + } + if (str_eq(method, EL_STR("el/typeGraph"))) { + return lsp_type_graph(source); + } + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"unknown method: "), json_escape(method)), EL_STR("\"}")); + return 0; +} + +el_val_t read_headers(void) { + el_val_t content_len = 0; + el_val_t scanning = 1; + while (scanning) { + el_val_t line = readline(); + if (str_eq(line, EL_STR(""))) { + scanning = 0; + } else { + el_val_t cl_marker = EL_STR("Content-Length:"); + if (str_starts_with(line, cl_marker)) { + el_val_t val = str_trim(str_slice(line, str_len(cl_marker), str_len(line))); + content_len = str_to_int(val); + } + } + } + return el_map_new(1, "content_length", content_len); + return 0; +} + +el_val_t lsp_stdio_loop(void) { + el_val_t running = 1; + while (running) { + el_val_t headers = read_headers(); + el_val_t body = readline(); + if (str_eq(body, EL_STR(""))) { + running = 0; + } else { + el_val_t method = json_get_string(body, EL_STR("method")); + el_val_t id_val = json_get_raw(body, EL_STR("id")); + if (str_eq(id_val, EL_STR(""))) { + id_val = EL_STR("null"); + } + el_val_t params_raw = json_get_raw(body, EL_STR("params")); + el_val_t result = lsp_jsonrpc(method, params_raw); + el_val_t resp = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"jsonrpc\":\"2.0\",\"id\":"), id_val), EL_STR(",\"result\":")), result), EL_STR("}")); + el_val_t frame = el_str_concat(el_str_concat(el_str_concat(EL_STR("Content-Length: "), int_to_str(str_len(resp))), EL_STR("\r\n\r\n")), resp); + print(frame); + } + } + return 0; +} + +el_val_t lsp_main(void) { + el_val_t argv = args(); + el_val_t n = native_list_len(argv); + if (n > 0) { + el_val_t cmd = native_list_get(argv, 0); + if (str_eq(cmd, EL_STR("version"))) { + println(EL_STR("el-lsp 0.1.0")); + return 0; + } + if (str_eq(cmd, EL_STR("probe"))) { + el_val_t sample = EL_STR("fn main() -> Void { let x: Int = 42 }"); + println(lsp_diagnostics(sample)); + return 0; + } + } + lsp_stdio_loop(); + return 0; +} + +el_val_t pkey(el_val_t name, el_val_t field) { + return el_str_concat(el_str_concat(el_str_concat(EL_STR("plugin/"), name), EL_STR("/")), field); + return 0; +} + +el_val_t put_str(el_val_t name, el_val_t field, el_val_t value) { + state_set(pkey(name, field), value); + return 0; +} + +el_val_t put_bool(el_val_t name, el_val_t field, el_val_t value) { + if (value) { + state_set(pkey(name, field), EL_STR("true")); + } + if (!value) { + state_set(pkey(name, field), EL_STR("false")); + } + return 0; +} + +el_val_t get_str(el_val_t name, el_val_t field) { + return state_get(pkey(name, field)); + return 0; +} + +el_val_t get_bool(el_val_t name, el_val_t field) { + return str_eq(state_get(pkey(name, field)), EL_STR("true")); + return 0; +} + +el_val_t index_list(void) { + el_val_t raw = state_get(EL_STR("plugins/index")); + if (str_eq(raw, EL_STR(""))) { + return native_list_empty(); + } + return str_split(raw, EL_STR(",")); + return 0; +} + +el_val_t index_set(el_val_t names) { + state_set(EL_STR("plugins/index"), list_join(names, EL_STR(","))); + return 0; +} + +el_val_t index_contains(el_val_t name) { + el_val_t names = index_list(); + el_val_t n = native_list_len(names); + el_val_t i = 0; + while (i < n) { + if (str_eq(native_list_get(names, i), name)) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t index_add(el_val_t name) { + if (!index_contains(name)) { + el_val_t names = index_list(); + names = native_list_append(names, name); + index_set(names); + } + return 0; +} + +el_val_t register_plugin(el_val_t name, el_val_t version, el_val_t description, el_val_t installed, el_val_t enabled, el_val_t hooks, el_val_t first_party) { + put_str(name, EL_STR("name"), name); + put_str(name, EL_STR("version"), version); + put_str(name, EL_STR("description"), description); + put_bool(name, EL_STR("installed"), installed); + put_bool(name, EL_STR("enabled"), enabled); + put_str(name, EL_STR("hooks"), hooks); + put_bool(name, EL_STR("first_party"), first_party); + index_add(name); + return 0; +} + +el_val_t plugin_init(void) { + if (str_eq(state_get(EL_STR("plugins/initialised")), EL_STR("true"))) { + return 0; + } + register_plugin(EL_STR("el-theme-dark"), EL_STR("1.0.0"), EL_STR("Dark theme — the default Engram IDE theme."), 1, 1, EL_STR(""), 1); + register_plugin(EL_STR("el-theme-light"), EL_STR("1.0.0"), EL_STR("Light theme for high-ambient-light environments."), 0, 0, EL_STR(""), 1); + register_plugin(EL_STR("el-fmt"), EL_STR("0.1.0"), EL_STR("Code formatter — auto-formats .el files on save."), 1, 1, EL_STR("on_save"), 1); + register_plugin(EL_STR("el-doc"), EL_STR("0.1.0"), EL_STR("Documentation generator — produces HTML docs from type definitions."), 0, 0, EL_STR("custom_tool"), 1); + register_plugin(EL_STR("el-test"), EL_STR("0.1.0"), EL_STR("Test runner with inline results displayed in the editor gutter."), 0, 0, EL_STR("on_build_complete,custom_panel"), 1); + state_set(EL_STR("plugins/initialised"), EL_STR("true")); + return 0; +} + +el_val_t json_escape_str(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\""))) { + out = el_str_concat(out, EL_STR("\\\"")); + } else { + if (str_eq(ch, EL_STR("\\"))) { + out = el_str_concat(out, EL_STR("\\\\")); + } else { + if (str_eq(ch, EL_STR("\n"))) { + out = el_str_concat(out, EL_STR("\\n")); + } else { + out = el_str_concat(out, ch); + } + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t quote_json(el_val_t s) { + return el_str_concat(el_str_concat(EL_STR("\""), json_escape_str(s)), EL_STR("\"")); + return 0; +} + +el_val_t hooks_to_json(el_val_t hooks_csv) { + if (str_eq(hooks_csv, EL_STR(""))) { + return EL_STR("[]"); + } + el_val_t xs = str_split(hooks_csv, EL_STR(",")); + el_val_t n = native_list_len(xs); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + out = el_str_concat(out, quote_json(native_list_get(xs, i))); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t plugin_to_json(el_val_t name) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, el_str_concat(EL_STR("\"name\":"), quote_json(get_str(name, EL_STR("name"))))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"version\":"), quote_json(get_str(name, EL_STR("version"))))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"description\":"), quote_json(get_str(name, EL_STR("description"))))); + el_val_t installed = get_str(name, EL_STR("installed")); + el_val_t enabled = get_str(name, EL_STR("enabled")); + el_val_t first_party = get_str(name, EL_STR("first_party")); + parts = native_list_append(parts, el_str_concat(EL_STR("\"installed\":"), installed)); + parts = native_list_append(parts, el_str_concat(EL_STR("\"enabled\":"), enabled)); + parts = native_list_append(parts, el_str_concat(EL_STR("\"hooks\":"), hooks_to_json(get_str(name, EL_STR("hooks"))))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"first_party\":"), first_party)); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t plugin_list_json(void) { + plugin_init(); + el_val_t names = index_list(); + el_val_t n = native_list_len(names); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + out = el_str_concat(out, plugin_to_json(native_list_get(names, i))); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t plugin_get_json(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return EL_STR("null"); + } + return plugin_to_json(name); + return 0; +} + +el_val_t plugin_install(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return EL_STR("{\"error\":\"plugin registry is not available (registry URL not configured)\"}"); + } + if (get_bool(name, EL_STR("installed"))) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' is already installed\"}")); + } + put_bool(name, EL_STR("installed"), 1); + put_bool(name, EL_STR("enabled"), 1); + return plugin_to_json(name); + return 0; +} + +el_val_t plugin_remove(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}")); + } + if (str_eq(name, EL_STR("el-theme-dark"))) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' cannot be removed: it is a required built-in\"}")); + } + put_bool(name, EL_STR("installed"), 0); + put_bool(name, EL_STR("enabled"), 0); + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t plugin_enable(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}")); + } + put_bool(name, EL_STR("enabled"), 1); + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t plugin_disable(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}")); + } + put_bool(name, EL_STR("enabled"), 0); + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t host_main(void) { + el_val_t argv = args(); + el_val_t n = native_list_len(argv); + if (n == 0) { + println(plugin_list_json()); + return 0; + } + el_val_t cmd = native_list_get(argv, 0); + if (str_eq(cmd, EL_STR("list"))) { + println(plugin_list_json()); + return 0; + } + if (str_eq(cmd, EL_STR("get"))) { + if (n >= 2) { + println(plugin_get_json(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("install"))) { + if (n >= 2) { + println(plugin_install(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("remove"))) { + if (n >= 2) { + println(plugin_remove(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("enable"))) { + if (n >= 2) { + println(plugin_enable(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("disable"))) { + if (n >= 2) { + println(plugin_disable(native_list_get(argv, 1))); + } + return 0; + } + println(EL_STR("usage: el-plugin-host [list|get|install|remove|enable|disable] ")); + return 0; +} + +el_val_t jstr(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + el_val_t out = EL_STR("\""); + el_val_t i = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\""))) { + out = el_str_concat(out, EL_STR("\\\"")); + } else { + if (str_eq(ch, EL_STR("\\"))) { + out = el_str_concat(out, EL_STR("\\\\")); + } else { + if (str_eq(ch, EL_STR("\n"))) { + out = el_str_concat(out, EL_STR("\\n")); + } else { + if (str_eq(ch, EL_STR("\r"))) { + out = el_str_concat(out, EL_STR("\\r")); + } else { + if (str_eq(ch, EL_STR("\t"))) { + out = el_str_concat(out, EL_STR("\\t")); + } else { + out = el_str_concat(out, ch); + } + } + } + } + } + i = (i + 1); + } + return el_str_concat(out, EL_STR("\"")); + return 0; +} + +el_val_t jerr(el_val_t msg) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":"), jstr(msg)), EL_STR("}")); + return 0; +} + +el_val_t ok_obj(void) { + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t url_decode(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("+"))) { + out = el_str_concat(out, EL_STR(" ")); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("%"))) { + if ((i + 2) < n) { + el_val_t hex = el_str_concat(native_list_get(chars, (i + 1)), native_list_get(chars, (i + 2))); + el_val_t code = hex_to_int(hex); + out = el_str_concat(out, char_from_code(code)); + i = (i + 3); + } else { + out = el_str_concat(out, ch); + i = (i + 1); + } + } else { + out = el_str_concat(out, ch); + i = (i + 1); + } + } + } + return out; + return 0; +} + +el_val_t hex_digit(el_val_t ch) { + if (str_eq(ch, EL_STR("0"))) { + return 0; + } + if (str_eq(ch, EL_STR("1"))) { + return 1; + } + if (str_eq(ch, EL_STR("2"))) { + return 2; + } + if (str_eq(ch, EL_STR("3"))) { + return 3; + } + if (str_eq(ch, EL_STR("4"))) { + return 4; + } + if (str_eq(ch, EL_STR("5"))) { + return 5; + } + if (str_eq(ch, EL_STR("6"))) { + return 6; + } + if (str_eq(ch, EL_STR("7"))) { + return 7; + } + if (str_eq(ch, EL_STR("8"))) { + return 8; + } + if (str_eq(ch, EL_STR("9"))) { + return 9; + } + if (str_eq(ch, EL_STR("a"))) { + return 10; + } + if (str_eq(ch, EL_STR("A"))) { + return 10; + } + if (str_eq(ch, EL_STR("b"))) { + return 11; + } + if (str_eq(ch, EL_STR("B"))) { + return 11; + } + if (str_eq(ch, EL_STR("c"))) { + return 12; + } + if (str_eq(ch, EL_STR("C"))) { + return 12; + } + if (str_eq(ch, EL_STR("d"))) { + return 13; + } + if (str_eq(ch, EL_STR("D"))) { + return 13; + } + if (str_eq(ch, EL_STR("e"))) { + return 14; + } + if (str_eq(ch, EL_STR("E"))) { + return 14; + } + if (str_eq(ch, EL_STR("f"))) { + return 15; + } + return (/* if-expr */ ((str_eq(ch, EL_STR("F"))) ? (el_val_t)1 : (el_val_t)0) - 1); + return 0; +} + +el_val_t hex_to_int(el_val_t hex) { + el_val_t chars = native_string_chars(hex); + el_val_t n = native_list_len(chars); + el_val_t acc = 0; + el_val_t i = 0; + while (i < n) { + el_val_t d = hex_digit(native_list_get(chars, i)); + if (d < 0) { + return acc; + } + acc = el_str_concat((acc * 16), d); + i = (i + 1); + } + return acc; + return 0; +} + +el_val_t char_from_code(el_val_t code) { + el_val_t table = EL_STR(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"); + if (code < 32) { + return EL_STR(" "); + } + if (code > 126) { + return EL_STR("?"); + } + return str_slice(table, (code - 32), (code - 31)); + return 0; +} + +el_val_t split_path_query(el_val_t path) { + el_val_t q = str_index_of(path, EL_STR("?")); + if (q < 0) { + return el_map_new(2, "path", path, "query", EL_STR("")); + } + return el_map_new(2, "path", str_slice(path, 0, q), "query", str_slice(path, (q + 1), str_len(path))); + return 0; +} + +el_val_t query_get(el_val_t query, el_val_t key) { + if (str_eq(query, EL_STR(""))) { + return EL_STR(""); + } + el_val_t pairs = str_split(query, EL_STR("&")); + el_val_t n = native_list_len(pairs); + el_val_t prefix = el_str_concat(key, EL_STR("=")); + el_val_t i = 0; + while (i < n) { + el_val_t pair = native_list_get(pairs, i); + if (str_starts_with(pair, prefix)) { + return url_decode(str_slice(pair, str_len(prefix), str_len(pair))); + } + if (str_eq(pair, key)) { + return EL_STR(""); + } + i = (i + 1); + } + return EL_STR(""); + return 0; +} + +el_val_t cfg_port(void) { + el_val_t p = env(EL_STR("EL_IDE_PORT")); + if (str_eq(p, EL_STR(""))) { + return 7771; + } + return str_to_int(p); + return 0; +} + +el_val_t cfg_project_path(void) { + el_val_t p = env(EL_STR("EL_IDE_PROJECT_PATH")); + if (str_eq(p, EL_STR(""))) { + return EL_STR("."); + } + return p; + return 0; +} + +el_val_t cfg_engram_url(void) { + el_val_t u = env(EL_STR("EL_ENGRAM_URL")); + if (str_eq(u, EL_STR(""))) { + return EL_STR("http://localhost:8742"); + } + return u; + return 0; +} + +el_val_t cfg_el_binary(void) { + el_val_t b = env(EL_STR("EL_BINARY")); + if (str_eq(b, EL_STR(""))) { + return EL_STR("el"); + } + return b; + return 0; +} + +el_val_t safe_join(el_val_t rel) { + el_val_t root = cfg_project_path(); + if (str_contains(rel, EL_STR(".."))) { + return EL_STR(""); + } + if (str_starts_with(rel, EL_STR("/"))) { + return EL_STR(""); + } + if (str_eq(rel, EL_STR("."))) { + return root; + } + return el_str_concat(el_str_concat(root, EL_STR("/")), rel); + return 0; +} + +el_val_t route_status(void) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, el_str_concat(EL_STR("\"project_name\":"), jstr(cfg_project_path()))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"project_path\":"), jstr(cfg_project_path()))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"version\":"), jstr(EL_STR("0.1.0")))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"engine\":"), jstr(EL_STR("el-ide")))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t route_config(void) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, el_str_concat(EL_STR("\"port\":"), int_to_str(cfg_port()))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"project_path\":"), jstr(cfg_project_path()))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"engram_url\":"), jstr(cfg_engram_url()))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"el_binary\":"), jstr(cfg_el_binary()))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t route_files_list(el_val_t query) { + el_val_t rel = query_get(query, EL_STR("path")); + if (str_eq(rel, EL_STR(""))) { + rel = EL_STR("."); + } + el_val_t target = safe_join(rel); + if (str_eq(EL_NULL, EL_STR(""))) { + return jerr(EL_STR("path escapes project root")); + } + el_val_t entries = fs_list(EL_NULL); + el_val_t n = native_list_len(entries); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + el_val_t name = native_list_get(entries, i); + el_val_t skip = 0; + if (str_starts_with(name, EL_STR("."))) { + skip = 1; + } + if (str_eq(name, EL_STR("target"))) { + skip = 1; + } + if (str_eq(name, EL_STR("node_modules"))) { + skip = 1; + } + if (str_eq(name, EL_STR("dist"))) { + skip = 1; + } + if (!skip) { + if (i > 0) { + if (!str_eq(out, EL_STR("["))) { + out = el_str_concat(out, EL_STR(",")); + } + } + el_val_t dot = str_index_of(name, EL_STR(".")); + el_val_t is_dir = EL_STR("false"); + if (dot < 0) { + is_dir = EL_STR("true"); + } + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("{\"name\":")), jstr(name)), EL_STR(",\"path\":")), jstr(el_str_concat(el_str_concat(rel, EL_STR("/")), name))), EL_STR(",\"is_dir\":")), is_dir), EL_STR(",\"children\":null}")); + } + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t route_file_read(el_val_t query) { + el_val_t rel = query_get(query, EL_STR("path")); + if (str_eq(rel, EL_STR(""))) { + return jerr(EL_STR("missing path parameter")); + } + el_val_t target = safe_join(rel); + if (str_eq(EL_NULL, EL_STR(""))) { + return jerr(EL_STR("path escapes project root")); + } + el_val_t content = fs_read(EL_NULL); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"path\":"), jstr(rel)), EL_STR(",\"content\":")), jstr(content)), EL_STR("}")); + return 0; +} + +el_val_t route_file_write(el_val_t body) { + el_val_t rel = json_get_string(body, EL_STR("path")); + el_val_t content = json_get_string(body, EL_STR("content")); + if (str_eq(rel, EL_STR(""))) { + return jerr(EL_STR("missing path")); + } + el_val_t target = safe_join(rel); + if (str_eq(EL_NULL, EL_STR(""))) { + return jerr(EL_STR("path escapes project root")); + } + el_val_t ok = fs_write(EL_NULL, content); + if (ok) { + return ok_obj(); + } + return jerr(EL_STR("cannot write file")); + return 0; +} + +el_val_t route_lsp_complete(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + el_val_t pos = str_to_int(query_get(query, EL_STR("pos"))); + return lsp_complete(source, pos); + return 0; +} + +el_val_t route_lsp_hover(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + el_val_t pos = str_to_int(query_get(query, EL_STR("pos"))); + return lsp_hover(source, pos); + return 0; +} + +el_val_t route_lsp_errors(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + return lsp_diagnostics(source); + return 0; +} + +el_val_t route_lsp_jsonrpc(el_val_t body) { + el_val_t method = json_get_string(body, EL_STR("method")); + el_val_t params_raw = json_get_raw(body, EL_STR("params")); + el_val_t id_val = json_get_raw(body, EL_STR("id")); + if (str_eq(id_val, EL_STR(""))) { + id_val = EL_STR("null"); + } + el_val_t result = lsp_jsonrpc(method, params_raw); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"jsonrpc\":\"2.0\",\"id\":"), id_val), EL_STR(",\"result\":")), result), EL_STR("}")); + return 0; +} + +el_val_t route_outline(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + return lsp_outline(source); + return 0; +} + +el_val_t route_definition(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + el_val_t pos = str_to_int(query_get(query, EL_STR("pos"))); + return lsp_definition(source, pos); + return 0; +} + +el_val_t route_references(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + el_val_t pos = str_to_int(query_get(query, EL_STR("pos"))); + return lsp_references(source, pos); + return 0; +} + +el_val_t route_format(el_val_t body) { + el_val_t content = json_get_string(body, EL_STR("content")); + return lsp_format(content); + return 0; +} + +el_val_t route_type_graph(el_val_t query) { + el_val_t source = query_get(query, EL_STR("source")); + return lsp_type_graph(source); + return 0; +} + +el_val_t active_theme(void) { + el_val_t t = state_get(EL_STR("ide/active_theme")); + if (str_eq(t, EL_STR(""))) { + return EL_STR("dark"); + } + return t; + return 0; +} + +el_val_t route_themes_list(void) { + el_val_t active = active_theme(); + el_val_t names = native_list_empty(); + names = native_list_append(names, EL_STR("dark")); + names = native_list_append(names, EL_STR("light")); + names = native_list_append(names, EL_STR("neuron")); + names = native_list_append(names, EL_STR("high-contrast")); + el_val_t labels = native_list_empty(); + labels = native_list_append(labels, EL_STR("Dark")); + labels = native_list_append(labels, EL_STR("Light")); + labels = native_list_append(labels, EL_STR("Neuron")); + labels = native_list_append(labels, EL_STR("High Contrast")); + el_val_t n = native_list_len(names); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + el_val_t nm = native_list_get(names, i); + el_val_t lbl = native_list_get(labels, i); + el_val_t act = EL_STR("false"); + if (str_eq(nm, active)) { + act = EL_STR("true"); + } + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("{\"name\":")), jstr(nm)), EL_STR(",\"label\":")), jstr(lbl)), EL_STR(",\"active\":")), act), EL_STR("}")); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t route_themes_set(el_val_t body) { + el_val_t name = json_get_string(body, EL_STR("name")); + el_val_t valid = 0; + if (str_eq(name, EL_STR("dark"))) { + valid = 1; + } + if (str_eq(name, EL_STR("light"))) { + valid = 1; + } + if (str_eq(name, EL_STR("neuron"))) { + valid = 1; + } + if (str_eq(name, EL_STR("high-contrast"))) { + valid = 1; + } + if (!valid) { + return jerr(el_str_concat(EL_STR("unknown theme: "), name)); + } + state_set(EL_STR("ide/active_theme"), name); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"active\":"), jstr(name)), EL_STR("}")); + return 0; +} + +el_val_t route_plugins_list(void) { + return plugin_list_json(); + return 0; +} + +el_val_t route_plugins_install(el_val_t body) { + return plugin_install(json_get_string(body, EL_STR("name"))); + return 0; +} + +el_val_t route_plugins_remove(el_val_t body) { + return plugin_remove(json_get_string(body, EL_STR("name"))); + return 0; +} + +el_val_t route_plugins_enable(el_val_t body) { + return plugin_enable(json_get_string(body, EL_STR("name"))); + return 0; +} + +el_val_t route_plugins_disable(el_val_t body) { + return plugin_disable(json_get_string(body, EL_STR("name"))); + return 0; +} + +el_val_t route_snippets(void) { + el_val_t snips = native_list_empty(); + snips = native_list_append(snips, EL_STR("{\"label\":\"fn\",\"description\":\"Function definition\",\"body\":\"fn ${1:name}(${2:params}) -> ${3:Ret} {\\n\\t${4:body}\\n}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"type\",\"description\":\"Struct\",\"body\":\"type ${1:Name} {\\n\\t${2:field}: ${3:Type},\\n}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"enum\",\"description\":\"Enum\",\"body\":\"enum ${1:Name} {\\n\\t${2:Variant},\\n}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"match\",\"description\":\"Match expression\",\"body\":\"match ${1:expr} {\\n\\t${2:pat} => ${3:body},\\n\\t_ => ${4:default},\\n}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"let\",\"description\":\"Let binding\",\"body\":\"let ${1:name}: ${2:Type} = ${3:value}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"if\",\"description\":\"If expression\",\"body\":\"if ${1:cond} {\\n\\t${2:body}\\n}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"for\",\"description\":\"For loop\",\"body\":\"for ${1:item} in ${2:iter} {\\n\\t${3:body}\\n}\"}")); + snips = native_list_append(snips, EL_STR("{\"label\":\"activate\",\"description\":\"Activate\",\"body\":\"activate ${1:Type}\"}")); + return el_str_concat(el_str_concat(EL_STR("["), list_join(snips, EL_STR(","))), EL_STR("]")); + return 0; +} + +el_val_t settings_path(void) { + el_val_t home = env(EL_STR("HOME")); + if (str_eq(home, EL_STR(""))) { + home = EL_STR("/tmp"); + } + return el_str_concat(home, EL_STR("/.el-ide/settings.json")); + return 0; +} + +el_val_t default_settings_json(void) { + return EL_STR("{\"theme\":\"dark\",\"fontSize\":13,\"tabSize\":4,\"wordWrap\":false,\"formatOnSave\":false,\"vimMode\":false,\"minimap\":true,\"elBinaryPath\":\"el\",\"engramUrl\":\"http://localhost:8742\",\"stickyScroll\":true,\"lineNumbers\":true,\"bracketMatching\":true}"); + return 0; +} + +el_val_t route_settings_get(void) { + el_val_t s = fs_read(settings_path()); + if (str_eq(s, EL_STR(""))) { + return default_settings_json(); + } + return s; + return 0; +} + +el_val_t route_settings_save(el_val_t body) { + el_val_t blob = json_get_raw(body, EL_STR("settings")); + if (str_eq(blob, EL_STR(""))) { + return jerr(EL_STR("missing settings")); + } + fs_write(settings_path(), blob); + return blob; + return 0; +} + +el_val_t route_settings_reset(void) { + fs_write(settings_path(), default_settings_json()); + return default_settings_json(); + return 0; +} + +el_val_t route_build_or_run(el_val_t action, el_val_t body) { + el_val_t rel_file = json_get_string(body, EL_STR("file")); + if (str_eq(rel_file, EL_STR(""))) { + rel_file = EL_STR("src/main.el"); + } + el_val_t target = safe_join(rel_file); + if (str_eq(EL_NULL, EL_STR(""))) { + return jerr(EL_STR("path escapes project root")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"info\",\"action\":"), jstr(action)), EL_STR(",\"file\":")), jstr(rel_file)), EL_STR(",\"note\":\"el-ide-server: synchronous build requires a runtime exec primitive — el binary not invoked\"}")); + return 0; +} + +el_val_t route_build(el_val_t body) { + return route_build_or_run(EL_STR("build"), body); + return 0; +} + +el_val_t route_run(el_val_t body) { + return route_build_or_run(EL_STR("run"), body); + return 0; +} + +el_val_t route_reason(el_val_t body) { + el_val_t url = el_str_concat(cfg_engram_url(), EL_STR("/api/reason")); + el_val_t resp = http_post(url, body); + if (str_eq(resp, EL_STR(""))) { + return jerr(EL_STR("engram unreachable")); + } + return resp; + return 0; +} + +el_val_t route_activate_preview(el_val_t query) { + el_val_t type_name = query_get(query, EL_STR("type_name")); + el_val_t q = query_get(query, EL_STR("query")); + el_val_t url = el_str_concat(cfg_engram_url(), EL_STR("/api/activate-preview")); + el_val_t body = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type_name\":"), jstr(type_name)), EL_STR(",\"query\":")), jstr(q)), EL_STR(",\"limit\":5}")); + el_val_t resp = http_post(url, body); + if (str_eq(resp, EL_STR(""))) { + return EL_STR("{\"count\":0,\"nodes\":[],\"connected\":false}"); + } + return resp; + return 0; +} + +el_val_t route_index(void) { + el_val_t path = el_str_concat(cfg_project_path(), EL_STR("/ide/index.html")); + el_val_t html = fs_read(path); + if (!str_eq(html, EL_STR(""))) { + return html; + } + return el_str_concat(el_str_concat(EL_STR("el-ide

el-ide

Backend running. UI bundle missing at "), path), EL_STR(".

")); + return 0; +} + +el_val_t route_health(void) { + return EL_STR("{\"status\":\"ok\",\"engine\":\"el-ide-server\"}"); + return 0; +} + +el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) { + el_val_t parsed = split_path_query(path); + el_val_t route = el_get_field(parsed, EL_STR("path")); + el_val_t query = el_get_field(parsed, EL_STR("query")); + if (str_eq(route, EL_STR("/"))) { + return route_index(); + } + if (str_eq(route, EL_STR("/health"))) { + return route_health(); + } + if (str_eq(route, EL_STR("/api/status"))) { + return route_status(); + } + if (str_eq(route, EL_STR("/api/config"))) { + return route_config(); + } + if (str_eq(route, EL_STR("/api/files"))) { + return route_files_list(query); + } + if (str_eq(route, EL_STR("/api/file"))) { + if (str_eq(method, EL_STR("GET"))) { + return route_file_read(query); + } + if (str_eq(method, EL_STR("POST"))) { + return route_file_write(body); + } + } + if (str_eq(route, EL_STR("/api/search"))) { + el_val_t q = query_get(query, EL_STR("q")); + return el_str_concat(el_str_concat(EL_STR("{\"query\":"), jstr(q)), EL_STR(",\"results\":[]}")); + } + if (str_eq(route, EL_STR("/api/lsp/complete"))) { + return route_lsp_complete(query); + } + if (str_eq(route, EL_STR("/api/lsp/hover"))) { + return route_lsp_hover(query); + } + if (str_eq(route, EL_STR("/api/lsp/errors"))) { + return route_lsp_errors(query); + } + if (str_eq(route, EL_STR("/api/lsp/activate-preview"))) { + return route_activate_preview(query); + } + if (str_eq(route, EL_STR("/api/lsp/jsonrpc"))) { + return route_lsp_jsonrpc(body); + } + if (str_eq(route, EL_STR("/api/type-graph"))) { + return route_type_graph(query); + } + if (str_eq(route, EL_STR("/api/outline"))) { + return route_outline(query); + } + if (str_eq(route, EL_STR("/api/definition"))) { + return route_definition(query); + } + if (str_eq(route, EL_STR("/api/references"))) { + return route_references(query); + } + if (str_eq(route, EL_STR("/api/format"))) { + return route_format(body); + } + if (str_eq(route, EL_STR("/api/completions/snippet"))) { + return route_snippets(); + } + if (str_eq(route, EL_STR("/api/themes"))) { + return route_themes_list(); + } + if (str_eq(route, EL_STR("/api/themes/active"))) { + return route_themes_set(body); + } + if (str_eq(route, EL_STR("/api/plugins"))) { + return route_plugins_list(); + } + if (str_eq(route, EL_STR("/api/plugins/install"))) { + return route_plugins_install(body); + } + if (str_eq(route, EL_STR("/api/plugins/remove"))) { + return route_plugins_remove(body); + } + if (str_eq(route, EL_STR("/api/plugins/enable"))) { + return route_plugins_enable(body); + } + if (str_eq(route, EL_STR("/api/plugins/disable"))) { + return route_plugins_disable(body); + } + if (str_eq(route, EL_STR("/api/settings"))) { + if (str_eq(method, EL_STR("GET"))) { + return route_settings_get(); + } + if (str_eq(method, EL_STR("POST"))) { + return route_settings_save(body); + } + if (str_eq(method, EL_STR("DELETE"))) { + return route_settings_reset(); + } + } + if (str_eq(route, EL_STR("/api/build"))) { + return route_build(body); + } + if (str_eq(route, EL_STR("/api/run"))) { + return route_run(body); + } + if (str_eq(route, EL_STR("/api/reason"))) { + return route_reason(body); + } + if (str_eq(route, EL_STR("/api/git/status"))) { + return EL_STR("[]"); + } + if (str_eq(route, EL_STR("/api/git/diff"))) { + return EL_STR("\"\""); + } + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":"), jstr(route)), EL_STR("}")); + return 0; +} + +el_val_t server_main(void) { + plugin_init(); + el_val_t port = cfg_port(); + println(el_str_concat(EL_STR("[el-ide-server] project="), cfg_project_path())); + println(el_str_concat(EL_STR("[el-ide-server] engram="), cfg_engram_url())); + println(el_str_concat(EL_STR("[el-ide-server] listening on http://0.0.0.0:"), int_to_str(port))); + http_set_handler(EL_STR("handle_request")); + http_serve(port, EL_STR("handle_request")); + return 0; +} + +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + server_main(); + return 0; +} + diff --git a/ide/vessels/el-ide-server/src/_combined.el b/ide/vessels/el-ide-server/src/_combined.el new file mode 100644 index 0000000..fc78f70 --- /dev/null +++ b/ide/vessels/el-ide-server/src/_combined.el @@ -0,0 +1,2056 @@ +// main.el — el-lsp +// +// Language Server for El. Pure-El port of the el-lsp Rust crate. +// +// Entry surface (callable from any El program that imports this file): +// +// lsp_complete(source: String, pos: Int) -> String // JSON array +// lsp_hover(source: String, pos: Int) -> String // JSON object or "null" +// lsp_diagnostics(source: String) -> String // JSON array +// lsp_outline(source: String) -> String // JSON array +// lsp_format(source: String) -> String // JSON { content, changed } +// lsp_type_graph(source: String) -> String // JSON { nodes, edges } +// lsp_definition(source: String, pos: Int) -> String // JSON object or "null" +// lsp_references(source: String, pos: Int) -> String // JSON array +// lsp_jsonrpc(method: String, params_json: String) -> String +// lsp_stdio_loop() -> Void +// +// Tokenisation reuses the lexer.el idiom (chars list + index walk) for the +// pieces we need (identifier scan, string scan, line counting). The full +// type checker referenced by the Rust crate is not yet available in El, so +// hover/completion/diagnostics fall back to source-scan heuristics over the +// declared types/enums/functions in the file. The wire shape is preserved. + +// ── Char helpers (mirrors lexer.el) ────────────────────────────────────────── + +fn is_digit_ch(ch: String) -> Bool { + if ch == "0" { return true } if ch == "1" { return true } + if ch == "2" { return true } if ch == "3" { return true } + if ch == "4" { return true } if ch == "5" { return true } + if ch == "6" { return true } if ch == "7" { return true } + if ch == "8" { return true } if ch == "9" { return true } + false +} + +fn is_alpha_ch(ch: String) -> Bool { + let lower: String = str_to_lower(ch) + if str_eq(ch, "") { return false } + if !str_eq(lower, str_to_upper(lower)) { return true } // letters differ in case + // covers a-z (lower-only) — uppercase already handled by inverse case test above + if str_contains("abcdefghijklmnopqrstuvwxyz", ch) { return true } + if str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ", ch) { return true } + false +} + +fn is_ident_ch(ch: String) -> Bool { + if is_digit_ch(ch) { return true } + if is_alpha_ch(ch) { return true } + if ch == "_" { return true } + false +} + +// ── Source navigation ──────────────────────────────────────────────────────── + +// Extract the identifier fragment immediately before `pos` (cursor). +fn extract_prefix(source: String, pos: Int) -> String { + let n: Int = str_len(source) + let end: Int = pos + if end > n { let end = n } + let start: Int = end + let chars: [String] = native_string_chars(source) + let total: Int = native_list_len(chars) + let scanning: Bool = true + while scanning { + if start <= 0 { + let scanning = false + } else { + let prev: String = native_list_get(chars, start - 1) + if is_ident_ch(prev) { + let start = start - 1 + } else { + let scanning = false + } + } + } + str_slice(source, start, end) +} + +// Extract the full token (alphanumerics + underscore) surrounding `pos`. +fn extract_token(source: String, pos: Int) -> String { + let n: Int = str_len(source) + if pos > n { return "" } + let chars: [String] = native_string_chars(source) + let total: Int = native_list_len(chars) + let start: Int = pos + let go_left: Bool = true + while go_left { + if start <= 0 { + let go_left = false + } else { + let prev: String = native_list_get(chars, start - 1) + if is_ident_ch(prev) { + let start = start - 1 + } else { + let go_left = false + } + } + } + let end: Int = pos + let go_right: Bool = true + while go_right { + if end >= total { + let go_right = false + } else { + let cur: String = native_list_get(chars, end) + if is_ident_ch(cur) { + let end = end + 1 + } else { + let go_right = false + } + } + } + str_slice(source, start, end) +} + +// Translate a byte offset into 1-based (line, col). +fn pos_to_line_col(source: String, pos: Int) -> Map { + let n: Int = str_len(source) + let cap: Int = pos + if cap > n { let cap = n } + let chars: [String] = native_string_chars(source) + let i: Int = 0 + let line: Int = 1 + let col: Int = 1 + while i < cap { + let ch: String = native_list_get(chars, i) + if ch == "\n" { + let line = line + 1 + let col = 1 + } else { + let col = col + 1 + } + let i = i + 1 + } + { "line": line, "col": col } +} + +// ── JSON encoding helpers ──────────────────────────────────────────────────── + +// Escape a string for JSON. Handles ", \, newline, tab. +fn json_escape(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "\"" { let out = out + "\\\"" } + else { if ch == "\\" { let out = out + "\\\\" } + else { if ch == "\n" { let out = out + "\\n" } + else { if ch == "\r" { let out = out + "\\r" } + else { if ch == "\t" { let out = out + "\\t" } + else { let out = out + ch } } } } } + let i = i + 1 + } + out +} + +fn json_str(s: String) -> String { + "\"" + json_escape(s) + "\"" +} + +fn json_kv_str(k: String, v: String) -> String { + json_str(k) + ":" + json_str(v) +} + +fn json_kv_int(k: String, v: Int) -> String { + json_str(k) + ":" + int_to_str(v) +} + +fn json_kv_float(k: String, v: Float) -> String { + json_str(k) + ":" + float_to_str(v) +} + +fn json_kv_raw(k: String, raw_v: String) -> String { + json_str(k) + ":" + raw_v +} + +// Join a list of pre-encoded JSON strings into a JSON array. +fn json_array_of(items: [String]) -> String { + let n: Int = native_list_len(items) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let out = out + native_list_get(items, i) + let i = i + 1 + } + out + "]" +} + +// ── Keyword/builtin tables ─────────────────────────────────────────────────── + +fn keywords_list() -> [String] { + let ks: [String] = native_list_empty() + let ks = native_list_append(ks, "let") + let ks = native_list_append(ks, "fn") + let ks = native_list_append(ks, "type") + let ks = native_list_append(ks, "enum") + let ks = native_list_append(ks, "match") + let ks = native_list_append(ks, "return") + let ks = native_list_append(ks, "activate") + let ks = native_list_append(ks, "where") + let ks = native_list_append(ks, "sealed") + let ks = native_list_append(ks, "if") + let ks = native_list_append(ks, "else") + let ks = native_list_append(ks, "for") + let ks = native_list_append(ks, "in") + let ks = native_list_append(ks, "while") + let ks = native_list_append(ks, "true") + let ks = native_list_append(ks, "false") + let ks = native_list_append(ks, "test") + let ks = native_list_append(ks, "seed") + let ks = native_list_append(ks, "assert") + let ks = native_list_append(ks, "target") + let ks = native_list_append(ks, "protocol") + let ks = native_list_append(ks, "impl") + let ks = native_list_append(ks, "import") + let ks = native_list_append(ks, "from") + let ks = native_list_append(ks, "as") + let ks = native_list_append(ks, "with") + let ks = native_list_append(ks, "retry") + let ks = native_list_append(ks, "times") + let ks = native_list_append(ks, "fallback") + let ks = native_list_append(ks, "reason") + let ks = native_list_append(ks, "parallel") + let ks = native_list_append(ks, "trace") + let ks = native_list_append(ks, "requires") + let ks = native_list_append(ks, "deploy") + let ks = native_list_append(ks, "to") + let ks = native_list_append(ks, "via") + let ks = native_list_append(ks, "vessel") + let ks = native_list_append(ks, "cgi") + ks +} + +// Built-in primitive type names. +fn builtin_type_names() -> [String] { + let ts: [String] = native_list_empty() + let ts = native_list_append(ts, "Int") + let ts = native_list_append(ts, "Float") + let ts = native_list_append(ts, "String") + let ts = native_list_append(ts, "Bool") + let ts = native_list_append(ts, "Uuid") + let ts = native_list_append(ts, "Void") + let ts = native_list_append(ts, "Any") + ts +} + +fn builtin_type_doc(name: String) -> String { + if str_eq(name, "Int") { return "64-bit signed integer" } + if str_eq(name, "Float") { return "64-bit IEEE 754 double" } + if str_eq(name, "String") { return "UTF-8 string" } + if str_eq(name, "Bool") { return "Boolean value" } + if str_eq(name, "Uuid") { return "RFC 4122 UUID" } + if str_eq(name, "Void") { return "Unit type — no value" } + if str_eq(name, "Any") { return "Dynamically-typed value" } + "" +} + +// Built-in function names → signatures. Encoded as flat [name, sig, name, sig, ...] +// to keep type maps simple. +fn builtin_fns() -> [String] { + let xs: [String] = native_list_empty() + let xs = native_list_append(xs, "println") let xs = native_list_append(xs, "fn(value: String) -> Void") + let xs = native_list_append(xs, "print") let xs = native_list_append(xs, "fn(value: String) -> Void") + let xs = native_list_append(xs, "readline") let xs = native_list_append(xs, "fn() -> String") + let xs = native_list_append(xs, "args") let xs = native_list_append(xs, "fn() -> [String]") + let xs = native_list_append(xs, "int_to_str") let xs = native_list_append(xs, "fn(n: Int) -> String") + let xs = native_list_append(xs, "str_to_int") let xs = native_list_append(xs, "fn(s: String) -> Int") + let xs = native_list_append(xs, "str_len") let xs = native_list_append(xs, "fn(s: String) -> Int") + let xs = native_list_append(xs, "str_slice") let xs = native_list_append(xs, "fn(s: String, start: Int, end: Int) -> String") + let xs = native_list_append(xs, "str_concat") let xs = native_list_append(xs, "fn(a: String, b: String) -> String") + let xs = native_list_append(xs, "str_contains") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Bool") + let xs = native_list_append(xs, "str_starts_with") let xs = native_list_append(xs, "fn(s: String, p: String) -> Bool") + let xs = native_list_append(xs, "str_ends_with") let xs = native_list_append(xs, "fn(s: String, suf: String) -> Bool") + let xs = native_list_append(xs, "str_to_upper") let xs = native_list_append(xs, "fn(s: String) -> String") + let xs = native_list_append(xs, "str_to_lower") let xs = native_list_append(xs, "fn(s: String) -> String") + let xs = native_list_append(xs, "str_split") let xs = native_list_append(xs, "fn(s: String, sep: String) -> [String]") + let xs = native_list_append(xs, "str_trim") let xs = native_list_append(xs, "fn(s: String) -> String") + let xs = native_list_append(xs, "str_replace") let xs = native_list_append(xs, "fn(s: String, from: String, to: String) -> String") + let xs = native_list_append(xs, "str_index_of") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Int") + let xs = native_list_append(xs, "float_to_str") let xs = native_list_append(xs, "fn(f: Float) -> String") + let xs = native_list_append(xs, "str_to_float") let xs = native_list_append(xs, "fn(s: String) -> Float") + let xs = native_list_append(xs, "math_sqrt") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_log") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_sin") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_cos") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_pi") let xs = native_list_append(xs, "fn() -> Float") + let xs = native_list_append(xs, "el_abs") let xs = native_list_append(xs, "fn(n: Int) -> Int") + let xs = native_list_append(xs, "el_min") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int") + let xs = native_list_append(xs, "el_max") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int") + let xs = native_list_append(xs, "el_list_empty") let xs = native_list_append(xs, "fn() -> [Any]") + let xs = native_list_append(xs, "el_list_len") let xs = native_list_append(xs, "fn(l: [Any]) -> Int") + let xs = native_list_append(xs, "el_list_get") let xs = native_list_append(xs, "fn(l: [Any], i: Int) -> Any") + let xs = native_list_append(xs, "el_list_append") let xs = native_list_append(xs, "fn(l: [Any], v: Any) -> [Any]") + let xs = native_list_append(xs, "list_join") let xs = native_list_append(xs, "fn(l: [String], sep: String) -> String") + let xs = native_list_append(xs, "list_range") let xs = native_list_append(xs, "fn(start: Int, end: Int) -> [Int]") + let xs = native_list_append(xs, "el_map_get") let xs = native_list_append(xs, "fn(m: Map, k: String) -> Any") + let xs = native_list_append(xs, "el_map_set") let xs = native_list_append(xs, "fn(m: Map, k: String, v: Any) -> Map") + let xs = native_list_append(xs, "json_parse") let xs = native_list_append(xs, "fn(s: String) -> Any") + let xs = native_list_append(xs, "json_stringify") let xs = native_list_append(xs, "fn(v: Any) -> String") + let xs = native_list_append(xs, "json_get_string") let xs = native_list_append(xs, "fn(j: String, k: String) -> String") + let xs = native_list_append(xs, "json_get_int") let xs = native_list_append(xs, "fn(j: String, k: String) -> Int") + let xs = native_list_append(xs, "json_get_bool") let xs = native_list_append(xs, "fn(j: String, k: String) -> Bool") + let xs = native_list_append(xs, "http_get") let xs = native_list_append(xs, "fn(url: String) -> String") + let xs = native_list_append(xs, "http_post") let xs = native_list_append(xs, "fn(url: String, body: String) -> String") + let xs = native_list_append(xs, "fs_read") let xs = native_list_append(xs, "fn(path: String) -> String") + let xs = native_list_append(xs, "fs_write") let xs = native_list_append(xs, "fn(path: String, content: String) -> Bool") + let xs = native_list_append(xs, "fs_list") let xs = native_list_append(xs, "fn(path: String) -> [String]") + let xs = native_list_append(xs, "env") let xs = native_list_append(xs, "fn(key: String) -> String") + let xs = native_list_append(xs, "uuid_new") let xs = native_list_append(xs, "fn() -> String") + let xs = native_list_append(xs, "time_now") let xs = native_list_append(xs, "fn() -> Int") + let xs = native_list_append(xs, "time_now_utc") let xs = native_list_append(xs, "fn() -> Int") + let xs = native_list_append(xs, "sleep_ms") let xs = native_list_append(xs, "fn(ms: Int) -> Void") + let xs = native_list_append(xs, "engram_node") let xs = native_list_append(xs, "fn(content: String, kind: String, salience: Float) -> String") + let xs = native_list_append(xs, "engram_search") let xs = native_list_append(xs, "fn(q: String, limit: Int) -> [Map]") + let xs = native_list_append(xs, "engram_activate") let xs = native_list_append(xs, "fn(q: String, depth: Int) -> [Map]") + let xs = native_list_append(xs, "engram_neighbors") let xs = native_list_append(xs, "fn(id: String) -> [Map]") + let xs = native_list_append(xs, "engram_connect") let xs = native_list_append(xs, "fn(from: String, to: String, w: Float, rel: String) -> Void") + let xs = native_list_append(xs, "dharma_connect") let xs = native_list_append(xs, "fn(cgi: String) -> String") + let xs = native_list_append(xs, "dharma_send") let xs = native_list_append(xs, "fn(channel: String, content: String) -> String") + let xs = native_list_append(xs, "dharma_emit") let xs = native_list_append(xs, "fn(event: String, payload: String) -> Void") + let xs = native_list_append(xs, "dharma_field") let xs = native_list_append(xs, "fn(event: String) -> Map") + let xs = native_list_append(xs, "llm_call") let xs = native_list_append(xs, "fn(model: String, prompt: String) -> String") + xs +} + +fn keyword_doc(kw: String) -> String { + if str_eq(kw, "let") { return "let name: Type = expr — declare an immutable binding." } + if str_eq(kw, "fn") { return "fn name(params) -> Ret { body } — define a function." } + if str_eq(kw, "type") { return "type Name { field: Type } — struct definition." } + if str_eq(kw, "enum") { return "enum Name { Variant1, Variant2(Type) } — enum definition." } + if str_eq(kw, "match") { return "match expr { Pattern => body } — pattern match." } + if str_eq(kw, "return") { return "return value — exit the enclosing function." } + if str_eq(kw, "if") { return "if cond { ... } else { ... } — conditional." } + if str_eq(kw, "for") { return "for item in collection { ... } — iterate." } + if str_eq(kw, "while") { return "while cond { ... } — loop while cond is truthy." } + if str_eq(kw, "import") { return "import \"path.el\" — pull in another module." } + if str_eq(kw, "from") { return "from module import { Name } — selective import." } + if str_eq(kw, "vessel") { return "vessel \"name\" { ... } — manifest declaration." } + if str_eq(kw, "cgi") { return "cgi \"name\" { dharma_id: ... } — CGI identity." } + if str_eq(kw, "activate") { return "activate Type where \"query\" — spreading-activation retrieval." } + if str_eq(kw, "sealed") { return "sealed { ... } — capability-restricted block." } + if str_eq(kw, "protocol") { return "protocol Name { fn method(self) -> R } — trait-like." } + if str_eq(kw, "impl") { return "impl Protocol for Type { ... } — protocol implementation." } + if str_eq(kw, "with") { return "with retry/fallback clause." } + if str_eq(kw, "retry") { return "retry times N — retry policy." } + if str_eq(kw, "fallback") { return "fallback { default } — fallback on failure." } + if str_eq(kw, "true") { return "Boolean literal." } + if str_eq(kw, "false") { return "Boolean literal." } + "" +} + +// ── Source-driven type/fn discovery (no full parser) ───────────────────────── +// +// The Rust crate calls into el-types::TypeChecker for a real env. That +// module isn't available in El yet, so we approximate by line-scan: find +// every `type X {`, `enum X {`, `protocol X {`, and `fn X(` declaration. +// Returns four parallel lists merged into one map for easy access. + +fn first_ident_in(rest: String) -> String { + let trimmed: String = str_trim(rest) + let n: Int = str_len(trimmed) + if n == 0 { return "" } + let chars: [String] = native_string_chars(trimmed) + let i: Int = 0 + let total: Int = native_list_len(chars) + while i < total { + let ch: String = native_list_get(chars, i) + if !is_ident_ch(ch) { + return str_slice(trimmed, 0, i) + } + let i = i + 1 + } + trimmed +} + +// scan_decls — produce a Map with parallel lists: +// "types" : [String] user-defined struct/enum/protocol names +// "type_kind" : [String] "type" | "enum" | "protocol" matching above +// "fns" : [String] user-defined function names +// "type_lines" : [Int] 1-based line numbers +// "fn_lines" : [Int] +fn scan_decls(source: String) -> Map { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let types: [String] = native_list_empty() + let kinds: [String] = native_list_empty() + let fns: [String] = native_list_empty() + let tlines: [Int] = native_list_empty() + let flines: [Int] = native_list_empty() + let i: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + let lineno: Int = i + 1 + if str_starts_with(trimmed, "fn ") { + let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))) + if !str_eq(name, "") { + let fns = native_list_append(fns, name) + let flines = native_list_append(flines, lineno) + } + } else { if str_starts_with(trimmed, "type ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let types = native_list_append(types, name) + let kinds = native_list_append(kinds, "type") + let tlines = native_list_append(tlines, lineno) + } + } else { if str_starts_with(trimmed, "enum ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let types = native_list_append(types, name) + let kinds = native_list_append(kinds, "enum") + let tlines = native_list_append(tlines, lineno) + } + } else { if str_starts_with(trimmed, "protocol ") { + let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))) + if !str_eq(name, "") { + let types = native_list_append(types, name) + let kinds = native_list_append(kinds, "protocol") + let tlines = native_list_append(tlines, lineno) + } + } } } } + let i = i + 1 + } + { + "types": types, + "type_kind": kinds, + "fns": fns, + "type_lines": tlines, + "fn_lines": flines + } +} + +// list_contains — case-sensitive membership test on [String]. +fn list_contains_str(xs: [String], needle: String) -> Bool { + let n: Int = native_list_len(xs) + let i: Int = 0 + while i < n { + if str_eq(native_list_get(xs, i), needle) { return true } + let i = i + 1 + } + false +} + +// Score a label against a prefix: 0 if no prefix match, else (prefix_len/label_len). +fn prefix_score(label: String, prefix: String) -> Float { + if str_eq(prefix, "") { return 0.5 } + let lower_label: String = str_to_lower(label) + let lower_prefix: String = str_to_lower(prefix) + if str_starts_with(lower_label, lower_prefix) { + let lp: Int = str_len(lower_prefix) + let ll: Int = str_len(lower_label) + if ll == 0 { return 0.0 } + return int_to_float(lp) / int_to_float(ll) + } + 0.0 +} + +// Render a single completion item to JSON. +fn completion_json(label: String, kind: String, detail: String, doc: String, score: Float) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("label", label)) + let parts = native_list_append(parts, json_kv_str("kind", kind)) + let parts = native_list_append(parts, json_kv_str("detail", detail)) + if str_eq(doc, "") { + let parts = native_list_append(parts, json_kv_raw("documentation", "null")) + } else { + let parts = native_list_append(parts, json_kv_str("documentation", doc)) + } + let parts = native_list_append(parts, json_kv_float("score", score)) + "{" + list_join(parts, ",") + "}" +} + +// ── Public: completion ─────────────────────────────────────────────────────── + +fn lsp_complete(source: String, pos: Int) -> String { + let prefix: String = extract_prefix(source, pos) + let lower_prefix: String = str_to_lower(prefix) + let prefix_empty: Bool = str_eq(prefix, "") + let items: [String] = native_list_empty() + + // Keywords + let kws: [String] = keywords_list() + let nk: Int = native_list_len(kws) + let i: Int = 0 + while i < nk { + let kw: String = native_list_get(kws, i) + if prefix_empty { + let s: Float = prefix_score(kw, prefix) + let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s)) + } else { if str_starts_with(kw, prefix) { + let s: Float = prefix_score(kw, prefix) + let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s)) + } } + let i = i + 1 + } + + // Built-in types + let bts: [String] = builtin_type_names() + let nb: Int = native_list_len(bts) + let i: Int = 0 + while i < nb { + let name: String = native_list_get(bts, i) + let lname: String = str_to_lower(name) + if prefix_empty { + let s: Float = 0.5 + 0.1 + let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s)) + } else { if str_starts_with(lname, lower_prefix) { + let s: Float = prefix_score(name, prefix) + 0.1 + let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s)) + } } + let i = i + 1 + } + + // User-defined types + let decls: Map = scan_decls(source) + let utypes: [String] = decls["types"] + let ukinds: [String] = decls["type_kind"] + let nu: Int = native_list_len(utypes) + let i: Int = 0 + while i < nu { + let name: String = native_list_get(utypes, i) + let kind: String = native_list_get(ukinds, i) + let lname: String = str_to_lower(name) + let include: Bool = false + if prefix_empty { let include = true } + if str_starts_with(lname, lower_prefix) { let include = true } + if include { + let s: Float = prefix_score(name, prefix) + 0.15 + let items = native_list_append(items, completion_json(name, "type", kind + " " + name, "User-defined " + kind + " " + name, s)) + } + let i = i + 1 + } + + // Built-in functions + let bfns: [String] = builtin_fns() + let nf: Int = native_list_len(bfns) + let i: Int = 0 + while i < nf { + let name: String = native_list_get(bfns, i) + let sig: String = native_list_get(bfns, i + 1) + let lname: String = str_to_lower(name) + let include: Bool = false + if prefix_empty { let include = true } + if str_starts_with(lname, lower_prefix) { let include = true } + if include { + let s: Float = prefix_score(name, prefix) + 0.11 + let items = native_list_append(items, completion_json(name, "function", sig, "Built-in: " + name + " :: " + sig, s)) + } + let i = i + 2 + } + + // User-defined functions + let ufns: [String] = decls["fns"] + let nuf: Int = native_list_len(ufns) + let i: Int = 0 + while i < nuf { + let name: String = native_list_get(ufns, i) + let lname: String = str_to_lower(name) + let include: Bool = false + if prefix_empty { let include = true } + if str_starts_with(lname, lower_prefix) { let include = true } + if include { + let s: Float = prefix_score(name, prefix) + 0.12 + let items = native_list_append(items, completion_json(name, "function", "fn " + name, "User function: " + name, s)) + } + let i = i + 1 + } + + // Output is a list of pre-encoded JSON objects. Sorting by score is a + // stretch goal — el's runtime has no sort-by-key primitive. Items are + // already ordered by category (keywords first), which matches the most + // common editor expectation. + json_array_of(items) +} + +// ── Public: hover ──────────────────────────────────────────────────────────── + +fn lsp_hover(source: String, pos: Int) -> String { + let token: String = extract_token(source, pos) + if str_eq(token, "") { return "null" } + + // Built-in primitives + let doc: String = builtin_type_doc(token) + if !str_eq(doc, "") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", doc)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + + // User-declared types + let decls: Map = scan_decls(source) + let utypes: [String] = decls["types"] + let ukinds: [String] = decls["type_kind"] + let nu: Int = native_list_len(utypes) + let i: Int = 0 + while i < nu { + let name: String = native_list_get(utypes, i) + if str_eq(name, token) { + let kind: String = native_list_get(ukinds, i) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", kind + " " + token)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + let i = i + 1 + } + + // User-declared functions + let ufns: [String] = decls["fns"] + let nuf: Int = native_list_len(ufns) + let i: Int = 0 + while i < nuf { + let name: String = native_list_get(ufns, i) + if str_eq(name, token) { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + let i = i + 1 + } + + // Built-in functions + let bfns: [String] = builtin_fns() + let nf: Int = native_list_len(bfns) + let i: Int = 0 + while i < nf { + let name: String = native_list_get(bfns, i) + if str_eq(name, token) { + let sig: String = native_list_get(bfns, i + 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token + " :: " + sig)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + let i = i + 2 + } + + // Keyword + let kdoc: String = keyword_doc(token) + if !str_eq(kdoc, "") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", kdoc)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + + "null" +} + +// ── Public: diagnostics ────────────────────────────────────────────────────── +// +// Without the type checker port we surface only structural diagnostics: +// * unterminated string literal +// * unbalanced braces / parens / brackets +// * unknown type referenced after `:` in a let binding (unknown to source + +// builtins) — best-effort, conservative. + +fn diag_json(message: String, severity: String, line: Int, col: Int) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("message", message)) + let parts = native_list_append(parts, json_kv_str("severity", severity)) + if line > 0 { + let parts = native_list_append(parts, json_kv_int("line", line)) + let parts = native_list_append(parts, json_kv_int("col", col)) + } else { + let parts = native_list_append(parts, json_kv_raw("line", "null")) + let parts = native_list_append(parts, json_kv_raw("col", "null")) + } + "{" + list_join(parts, ",") + "}" +} + +fn lsp_diagnostics(source: String) -> String { + let chars: [String] = native_string_chars(source) + let n: Int = native_list_len(chars) + let diags: [String] = native_list_empty() + + // Bracket balance + unterminated string scan + let depth_brace: Int = 0 + let depth_paren: Int = 0 + let depth_brack: Int = 0 + let i: Int = 0 + let line: Int = 1 + let col: Int = 1 + let in_string: Bool = false + let string_start_line: Int = 0 + let string_start_col: Int = 0 + let in_comment: Bool = false + while i < n { + let ch: String = native_list_get(chars, i) + if in_comment { + if ch == "\n" { + let in_comment = false + let line = line + 1 + let col = 1 + } else { + let col = col + 1 + } + let i = i + 1 + } else { + if in_string { + if ch == "\\" { + let i = i + 1 + let col = col + 1 + if i < n { + let esc: String = native_list_get(chars, i) + if esc == "\n" { let line = line + 1 let col = 1 } else { let col = col + 1 } + let i = i + 1 + } + } else { if ch == "\"" { + let in_string = false + let i = i + 1 + let col = col + 1 + } else { if ch == "\n" { + let line = line + 1 + let col = 1 + let i = i + 1 + } else { + let col = col + 1 + let i = i + 1 + } } } + } else { + if ch == "\"" { + let in_string = true + let string_start_line = line + let string_start_col = col + let i = i + 1 + let col = col + 1 + } else { if ch == "/" { + let next_i: Int = i + 1 + let is_line_comment: Bool = false + if next_i < n { + let next_ch: String = native_list_get(chars, next_i) + if next_ch == "/" { let is_line_comment = true } + } + if is_line_comment { + let in_comment = true + let i = i + 2 + let col = col + 2 + } else { + let i = i + 1 + let col = col + 1 + } + } else { if ch == "{" { + let depth_brace = depth_brace + 1 + let i = i + 1 let col = col + 1 + } else { if ch == "}" { + let depth_brace = depth_brace - 1 + if depth_brace < 0 { + let diags = native_list_append(diags, diag_json("unmatched `}`", "error", line, col)) + let depth_brace = 0 + } + let i = i + 1 let col = col + 1 + } else { if ch == "(" { + let depth_paren = depth_paren + 1 + let i = i + 1 let col = col + 1 + } else { if ch == ")" { + let depth_paren = depth_paren - 1 + if depth_paren < 0 { + let diags = native_list_append(diags, diag_json("unmatched `)`", "error", line, col)) + let depth_paren = 0 + } + let i = i + 1 let col = col + 1 + } else { if ch == "[" { + let depth_brack = depth_brack + 1 + let i = i + 1 let col = col + 1 + } else { if ch == "]" { + let depth_brack = depth_brack - 1 + if depth_brack < 0 { + let diags = native_list_append(diags, diag_json("unmatched `]`", "error", line, col)) + let depth_brack = 0 + } + let i = i + 1 let col = col + 1 + } else { if ch == "\n" { + let line = line + 1 + let col = 1 + let i = i + 1 + } else { + let i = i + 1 + let col = col + 1 + } } } } } } } } } + } + } + } + + if in_string { + let diags = native_list_append(diags, diag_json("unterminated string literal", "error", string_start_line, string_start_col)) + } + if depth_brace > 0 { let diags = native_list_append(diags, diag_json("unclosed `{` (missing `}`)", "error", 0, 0)) } + if depth_paren > 0 { let diags = native_list_append(diags, diag_json("unclosed `(` (missing `)`)", "error", 0, 0)) } + if depth_brack > 0 { let diags = native_list_append(diags, diag_json("unclosed `[` (missing `]`)", "error", 0, 0)) } + + json_array_of(diags) +} + +// ── Public: outline ────────────────────────────────────────────────────────── + +fn outline_item_json(kind: String, name: String, line: Int) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("kind", kind)) + let parts = native_list_append(parts, json_kv_str("name", name)) + let parts = native_list_append(parts, json_kv_int("line", line)) + "{" + list_join(parts, ",") + "}" +} + +fn lsp_outline(source: String) -> String { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let items: [String] = native_list_empty() + let i: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + let lineno: Int = i + 1 + if str_starts_with(trimmed, "fn ") { + let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("fn", name, lineno)) + } + } else { if str_starts_with(trimmed, "type ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("type", name, lineno)) + } + } else { if str_starts_with(trimmed, "enum ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("enum", name, lineno)) + } + } else { if str_starts_with(trimmed, "protocol ") { + let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("protocol", name, lineno)) + } + } else { if str_starts_with(trimmed, "impl ") { + let rest: String = str_trim(str_slice(trimmed, 5, str_len(trimmed))) + let brace: Int = str_index_of(rest, "{") + let name: String = rest + if brace > 0 { let name = str_trim(str_slice(rest, 0, brace)) } + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("impl", name, lineno)) + } + } } } } } + let i = i + 1 + } + json_array_of(items) +} + +// ── Public: format ─────────────────────────────────────────────────────────── +// +// Idiomatic-El formatter. Until `el fmt` is a real thing, we apply a minimal +// canonicalisation pass: +// * trim trailing whitespace on every line +// * collapse 3+ blank lines to 2 +// * ensure file ends with exactly one newline +// This is safe for round-tripping and a noticeable quality-of-life win in +// the editor. + +fn format_strip_trailing(line: String) -> String { + // str_trim trims both ends; we want only trailing whitespace stripped. + let n: Int = str_len(line) + if n == 0 { return line } + let chars: [String] = native_string_chars(line) + let i: Int = n - 1 + let trimming: Bool = true + while trimming { + if i < 0 { + let trimming = false + } else { + let ch: String = native_list_get(chars, i) + if ch == " " { let i = i - 1 } else { if ch == "\t" { let i = i - 1 } else { let trimming = false } } + } + } + str_slice(line, 0, i + 1) +} + +fn lsp_format(source: String) -> String { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let cleaned: [String] = native_list_empty() + let i: Int = 0 + let blank_run: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let stripped: String = format_strip_trailing(line) + if str_eq(stripped, "") { + let blank_run = blank_run + 1 + if blank_run <= 2 { + let cleaned = native_list_append(cleaned, "") + } + } else { + let blank_run = 0 + let cleaned = native_list_append(cleaned, stripped) + } + let i = i + 1 + } + let joined: String = list_join(cleaned, "\n") + // Ensure trailing newline. + let final: String = joined + if !str_ends_with(final, "\n") { + let final = final + "\n" + } + let changed: String = "false" + if !str_eq(final, source) { let changed = "true" } + "{" + json_kv_str("content", final) + "," + json_kv_raw("changed", changed) + "}" +} + +// ── Public: type graph ─────────────────────────────────────────────────────── + +fn lsp_type_graph(source: String) -> String { + let nodes: [String] = native_list_empty() + let edges: [String] = native_list_empty() + + // Built-in nodes + let bts: [String] = builtin_type_names() + let nb: Int = native_list_len(bts) + let i: Int = 0 + while i < nb { + let name: String = native_list_get(bts, i) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("id", name)) + let parts = native_list_append(parts, json_kv_str("name", name)) + let parts = native_list_append(parts, json_kv_str("kind", "builtin")) + let parts = native_list_append(parts, json_kv_raw("fields", "[]")) + let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}") + let i = i + 1 + } + + // User-declared types + let decls: Map = scan_decls(source) + let utypes: [String] = decls["types"] + let ukinds: [String] = decls["type_kind"] + let tlines: [Int] = decls["type_lines"] + let nu: Int = native_list_len(utypes) + let i: Int = 0 + let bts_dup: [String] = builtin_type_names() + while i < nu { + let name: String = native_list_get(utypes, i) + let kind: String = native_list_get(ukinds, i) + // Skip if already a builtin (paranoia) + if !list_contains_str(bts_dup, name) { + let fields: [String] = collect_struct_fields(source, name) + let field_jsons: [String] = native_list_empty() + let fn2: Int = native_list_len(fields) + let j: Int = 0 + while j < fn2 { + let field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j))) + let j = j + 1 + } + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("id", name)) + let parts = native_list_append(parts, json_kv_str("name", name)) + let parts = native_list_append(parts, json_kv_str("kind", kind)) + let parts = native_list_append(parts, json_kv_raw("fields", json_array_of(field_jsons))) + let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}") + + // Edges: scan each field for a known type reference. + let j: Int = 0 + while j < fn2 { + let field_str: String = native_list_get(fields, j) + let target: String = referenced_type(field_str, utypes) + if !str_eq(target, "") { + let eparts: [String] = native_list_empty() + let eparts = native_list_append(eparts, json_kv_str("from", name)) + let eparts = native_list_append(eparts, json_kv_str("to", target)) + let eparts = native_list_append(eparts, json_kv_str("label", "field")) + let edges = native_list_append(edges, "{" + list_join(eparts, ",") + "}") + } + let j = j + 1 + } + } + let i = i + 1 + } + + "{" + json_kv_raw("nodes", json_array_of(nodes)) + "," + json_kv_raw("edges", json_array_of(edges)) + "}" +} + +// Extract the field list of a `type Name { ... }` block as ["field: Type", ...] +// or for an enum, the variant labels. Best-effort line-by-line scan. +fn collect_struct_fields(source: String, type_name: String) -> [String] { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let fields: [String] = native_list_empty() + let inside: Bool = false + let i: Int = 0 + let header_a: String = "type " + type_name + let header_b: String = "enum " + type_name + let header_c: String = "protocol " + type_name + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + if !inside { + if str_starts_with(trimmed, header_a) { let inside = true } + if str_starts_with(trimmed, header_b) { let inside = true } + if str_starts_with(trimmed, header_c) { let inside = true } + } else { + if str_starts_with(trimmed, "}") { + return fields + } + // skip the opening "{" line + if !str_eq(trimmed, "{") { + if !str_eq(trimmed, "") { + if !str_starts_with(trimmed, "//") { + // strip a trailing comma if present + let entry: String = trimmed + if str_ends_with(entry, ",") { + let entry = str_slice(entry, 0, str_len(entry) - 1) + } + let fields = native_list_append(fields, str_trim(entry)) + } + } + } + } + let i = i + 1 + } + fields +} + +// Pick the first known type name out of a `field: Type` string. Stops at non-ident. +fn referenced_type(field_str: String, known_types: [String]) -> String { + // Find ":" — type follows. + let colon: Int = str_index_of(field_str, ":") + if colon < 0 { return "" } + let after: String = str_trim(str_slice(field_str, colon + 1, str_len(field_str))) + // Strip array brackets and optional marker. + let trimmed_l: String = after + if str_starts_with(trimmed_l, "[") { + let end: Int = str_index_of(trimmed_l, "]") + if end > 1 { + let trimmed_l = str_trim(str_slice(trimmed_l, 1, end)) + } + } + // First identifier. + let ident: String = first_ident_in(trimmed_l) + if str_eq(ident, "") { return "" } + let bts: [String] = builtin_type_names() + if list_contains_str(bts, ident) { return ident } + if list_contains_str(known_types, ident) { return ident } + "" +} + +// ── Public: definition / references ────────────────────────────────────────── + +fn lsp_definition(source: String, pos: Int) -> String { + let word: String = extract_token(source, pos) + if str_eq(word, "") { return "null" } + let patterns: [String] = native_list_empty() + let patterns = native_list_append(patterns, "fn " + word) + let patterns = native_list_append(patterns, "type " + word) + let patterns = native_list_append(patterns, "enum " + word) + let patterns = native_list_append(patterns, "protocol " + word) + let np: Int = native_list_len(patterns) + let lines: [String] = str_split(source, "\n") + let nl: Int = native_list_len(lines) + let i: Int = 0 + while i < nl { + let line: String = native_list_get(lines, i) + let j: Int = 0 + while j < np { + let pat: String = native_list_get(patterns, j) + let idx: Int = str_index_of(line, pat) + if idx >= 0 { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_int("line", i + 1)) + let parts = native_list_append(parts, json_kv_int("col", idx + 1)) + let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line))) + return "{" + list_join(parts, ",") + "}" + } + let j = j + 1 + } + let i = i + 1 + } + "null" +} + +fn lsp_references(source: String, pos: Int) -> String { + let word: String = extract_token(source, pos) + if str_eq(word, "") { return "[]" } + let lines: [String] = str_split(source, "\n") + let nl: Int = native_list_len(lines) + let refs: [String] = native_list_empty() + let i: Int = 0 + while i < nl { + let line: String = native_list_get(lines, i) + let line_chars: [String] = native_string_chars(line) + let llen: Int = native_list_len(line_chars) + let wlen: Int = str_len(word) + let off: Int = 0 + let scanning: Bool = true + while scanning { + let idx: Int = str_index_of(str_slice(line, off, str_len(line)), word) + if idx < 0 { + let scanning = false + } else { + let abs: Int = off + idx + // Word-boundary check + let before_ok: Bool = true + if abs > 0 { + let prev: String = native_list_get(line_chars, abs - 1) + if is_ident_ch(prev) { let before_ok = false } + } + let after_ok: Bool = true + let after_pos: Int = abs + wlen + if after_pos < llen { + let nxt: String = native_list_get(line_chars, after_pos) + if is_ident_ch(nxt) { let after_ok = false } + } + if before_ok { + if after_ok { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_int("line", i + 1)) + let parts = native_list_append(parts, json_kv_int("col", abs + 1)) + let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line))) + let refs = native_list_append(refs, "{" + list_join(parts, ",") + "}") + } + } + let off = abs + wlen + if off >= str_len(line) { let scanning = false } + } + } + let i = i + 1 + } + json_array_of(refs) +} + +// ── JSON-RPC dispatch ──────────────────────────────────────────────────────── +// +// Method names follow LSP-ish conventions but stay aligned with the Rust crate's +// HTTP surface. Params are a JSON string the dispatcher pulls fields out of via +// json_get_string / json_get_int. +// +// "textDocument/completion" → params { source, pos } +// "textDocument/hover" → params { source, pos } +// "textDocument/diagnostic" → params { source } +// "textDocument/formatting" → params { source } +// "textDocument/documentSymbol"→ params { source } +// "textDocument/definition" → params { source, pos } +// "textDocument/references" → params { source, pos } +// "el/typeGraph" → params { source } + +fn lsp_jsonrpc(method: String, params_json: String) -> String { + let source: String = json_get_string(params_json, "source") + let pos: Int = json_get_int(params_json, "pos") + if str_eq(method, "textDocument/completion") { return lsp_complete(source, pos) } + if str_eq(method, "textDocument/hover") { return lsp_hover(source, pos) } + if str_eq(method, "textDocument/diagnostic") { return lsp_diagnostics(source) } + if str_eq(method, "textDocument/formatting") { return lsp_format(source) } + if str_eq(method, "textDocument/documentSymbol") { return lsp_outline(source) } + if str_eq(method, "textDocument/definition") { return lsp_definition(source, pos) } + if str_eq(method, "textDocument/references") { return lsp_references(source, pos) } + if str_eq(method, "el/typeGraph") { return lsp_type_graph(source) } + "{\"error\":\"unknown method: " + json_escape(method) + "\"}" +} + +// ── stdio loop (best-effort; see runtime gap note in the report) ───────────── +// +// LSP framing is `Content-Length: N\r\n\r\n` then exactly N bytes. The El +// runtime exposes only line-buffered `readline()`, which strips `\n` and is +// hostile to the binary body read. We approximate by reading lines until a +// blank line ends the headers, then reading exactly one more line as the +// body (works for compact, no-newline payloads — i.e. minified JSON-RPC). +// A dedicated `read_n(bytes)` runtime primitive would unblock the strict +// implementation. See report. + +fn read_headers() -> Map { + let content_len: Int = 0 + let scanning: Bool = true + while scanning { + let line: String = readline() + if str_eq(line, "") { + let scanning = false + } else { + let cl_marker: String = "Content-Length:" + if str_starts_with(line, cl_marker) { + let val: String = str_trim(str_slice(line, str_len(cl_marker), str_len(line))) + let content_len = str_to_int(val) + } + } + } + { "content_length": content_len } +} + +fn lsp_stdio_loop() -> Void { + let running: Bool = true + while running { + let headers: Map = read_headers() + let body: String = readline() + if str_eq(body, "") { + let running = false + } else { + let method: String = json_get_string(body, "method") + let id_val: String = json_get_raw(body, "id") + if str_eq(id_val, "") { let id_val = "null" } + let params_raw: String = json_get_raw(body, "params") + let result: String = lsp_jsonrpc(method, params_raw) + let resp: String = "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}" + let frame: String = "Content-Length: " + int_to_str(str_len(resp)) + "\r\n\r\n" + resp + print(frame) + } + } +} + +// ── Entry ──────────────────────────────────────────────────────────────────── +// +// When invoked as a standalone binary, default to stdio JSON-RPC so editors +// that drive the language server directly (without the HTTP wrapper) work +// out of the box. Pass a single CLI arg to short-circuit: +// ./el-lsp probe → emits a self-test diagnostic + exits +// ./el-lsp version → emits version + exits + +fn lsp_main() -> Void { + let argv: [String] = args() + let n: Int = native_list_len(argv) + if n > 0 { + let cmd: String = native_list_get(argv, 0) + if str_eq(cmd, "version") { + println("el-lsp 0.1.0") + return + } + if str_eq(cmd, "probe") { + let sample: String = "fn main() -> Void { let x: Int = 42 }" + println(lsp_diagnostics(sample)) + return + } + } + lsp_stdio_loop() +} + + +// main.el — el-plugin-host +// +// Plugin lifecycle manager for el-ide. Pure-El port of the el-plugin-host +// Rust crate. +// +// Plugins are stored in process-global state via state_get / state_set. +// Each plugin lives under a key prefix `plugin//`. The +// canonical list of plugin names is maintained at `plugins/index` as a +// comma-separated string (we don't have list serialisation in state yet). +// +// Public API: +// plugin_init() — seed first-party plugins (idempotent) +// plugin_list_json() -> String — JSON array of all plugins +// plugin_get_json(name) -> String — JSON object or "null" +// plugin_install(name) -> String — JSON { ok | error } +// plugin_remove(name) -> String +// plugin_enable(name) -> String +// plugin_disable(name) -> String + +// ── State helpers (string K/V) ─────────────────────────────────────────────── + +fn pkey(name: String, field: String) -> String { + "plugin/" + name + "/" + field +} + +fn put_str(name: String, field: String, value: String) -> Void { + state_set(pkey(name, field), value) +} + +fn put_bool(name: String, field: String, value: Bool) -> Void { + if value { state_set(pkey(name, field), "true") } + if !value { state_set(pkey(name, field), "false") } +} + +fn get_str(name: String, field: String) -> String { + state_get(pkey(name, field)) +} + +fn get_bool(name: String, field: String) -> Bool { + str_eq(state_get(pkey(name, field)), "true") +} + +// Plugin index: comma-separated list of names at "plugins/index". +fn index_list() -> [String] { + let raw: String = state_get("plugins/index") + if str_eq(raw, "") { return native_list_empty() } + str_split(raw, ",") +} + +fn index_set(names: [String]) -> Void { + state_set("plugins/index", list_join(names, ",")) +} + +fn index_contains(name: String) -> Bool { + let names: [String] = index_list() + let n: Int = native_list_len(names) + let i: Int = 0 + while i < n { + if str_eq(native_list_get(names, i), name) { return true } + let i = i + 1 + } + false +} + +fn index_add(name: String) -> Void { + if !index_contains(name) { + let names: [String] = index_list() + let names = native_list_append(names, name) + index_set(names) + } +} + +// ── Initialisation ─────────────────────────────────────────────────────────── + +fn register_plugin(name: String, version: String, description: String, + installed: Bool, enabled: Bool, hooks: String, + first_party: Bool) -> Void { + put_str(name, "name", name) + put_str(name, "version", version) + put_str(name, "description", description) + put_bool(name, "installed", installed) + put_bool(name, "enabled", enabled) + put_str(name, "hooks", hooks) + put_bool(name, "first_party", first_party) + index_add(name) +} + +fn plugin_init() -> Void { + if str_eq(state_get("plugins/initialised"), "true") { + return + } + register_plugin("el-theme-dark", "1.0.0", "Dark theme — the default Engram IDE theme.", true, true, "", true) + register_plugin("el-theme-light", "1.0.0", "Light theme for high-ambient-light environments.", false, false, "", true) + register_plugin("el-fmt", "0.1.0", "Code formatter — auto-formats .el files on save.", true, true, "on_save", true) + register_plugin("el-doc", "0.1.0", "Documentation generator — produces HTML docs from type definitions.", false, false, "custom_tool", true) + register_plugin("el-test", "0.1.0", "Test runner with inline results displayed in the editor gutter.", false, false, "on_build_complete,custom_panel", true) + state_set("plugins/initialised", "true") +} + +// ── Serialisation ──────────────────────────────────────────────────────────── + +fn json_escape_str(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "\"" { let out = out + "\\\"" } + else { if ch == "\\" { let out = out + "\\\\" } + else { if ch == "\n" { let out = out + "\\n" } + else { let out = out + ch } } } + let i = i + 1 + } + out +} + +fn quote_json(s: String) -> String { + "\"" + json_escape_str(s) + "\"" +} + +fn hooks_to_json(hooks_csv: String) -> String { + if str_eq(hooks_csv, "") { return "[]" } + let xs: [String] = str_split(hooks_csv, ",") + let n: Int = native_list_len(xs) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let out = out + quote_json(native_list_get(xs, i)) + let i = i + 1 + } + out + "]" +} + +fn plugin_to_json(name: String) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "\"name\":" + quote_json(get_str(name, "name"))) + let parts = native_list_append(parts, "\"version\":" + quote_json(get_str(name, "version"))) + let parts = native_list_append(parts, "\"description\":" + quote_json(get_str(name, "description"))) + let installed: String = get_str(name, "installed") + let enabled: String = get_str(name, "enabled") + let first_party: String = get_str(name, "first_party") + let parts = native_list_append(parts, "\"installed\":" + installed) + let parts = native_list_append(parts, "\"enabled\":" + enabled) + let parts = native_list_append(parts, "\"hooks\":" + hooks_to_json(get_str(name, "hooks"))) + let parts = native_list_append(parts, "\"first_party\":" + first_party) + "{" + list_join(parts, ",") + "}" +} + +fn plugin_list_json() -> String { + plugin_init() + let names: [String] = index_list() + let n: Int = native_list_len(names) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let out = out + plugin_to_json(native_list_get(names, i)) + let i = i + 1 + } + out + "]" +} + +fn plugin_get_json(name: String) -> String { + plugin_init() + if !index_contains(name) { return "null" } + plugin_to_json(name) +} + +// ── Mutations ──────────────────────────────────────────────────────────────── + +fn plugin_install(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin registry is not available (registry URL not configured)\"}" + } + if get_bool(name, "installed") { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' is already installed\"}" + } + put_bool(name, "installed", true) + put_bool(name, "enabled", true) + plugin_to_json(name) +} + +fn plugin_remove(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" + } + if str_eq(name, "el-theme-dark") { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' cannot be removed: it is a required built-in\"}" + } + put_bool(name, "installed", false) + put_bool(name, "enabled", false) + "{\"ok\":true}" +} + +fn plugin_enable(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" + } + put_bool(name, "enabled", true) + "{\"ok\":true}" +} + +fn plugin_disable(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" + } + put_bool(name, "enabled", false) + "{\"ok\":true}" +} + +// ── Standalone CLI ─────────────────────────────────────────────────────────── + +fn host_main() -> Void { + let argv: [String] = args() + let n: Int = native_list_len(argv) + if n == 0 { + println(plugin_list_json()) + return + } + let cmd: String = native_list_get(argv, 0) + if str_eq(cmd, "list") { println(plugin_list_json()) return } + if str_eq(cmd, "get") { + if n >= 2 { println(plugin_get_json(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "install") { + if n >= 2 { println(plugin_install(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "remove") { + if n >= 2 { println(plugin_remove(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "enable") { + if n >= 2 { println(plugin_enable(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "disable") { + if n >= 2 { println(plugin_disable(native_list_get(argv, 1))) } + return + } + println("usage: el-plugin-host [list|get|install|remove|enable|disable] ") +} + + +// main.el — el-ide-server +// +// HTTP backend for the Engram IDE. Pure-El port of the el-ide-server Rust +// crate. Wires the el-lsp and el-plugin-host vessels together and serves +// the IDE-frontend protocol used by the React-style frontend. +// +// El compilation note: `import` statements are parsed but the codegen +// concatenates nothing. The build script for this vessel concatenates +// +// vessels/el-lsp/src/main.el (without the trailing entry call) +// vessels/el-plugin-host/src/main.el +// vessels/el-ide-server/src/main.el (this file) +// +// into a single _combined.el before invoking elc. This file therefore +// assumes the lsp_* and plugin_* functions defined in those siblings are +// in scope. See README / build.sh. +// +// All routes serve JSON over HTTP. Query parameters are parsed by hand +// (the runtime gives us strings and a request body). Streaming endpoints +// (SSE) are emulated by returning the full output once — the runtime's +// http_serve handler signature is response-as-string, so true server-sent +// streaming requires a runtime extension. Flagged in the report. + +// ── Tiny utility layer ─────────────────────────────────────────────────────── + +fn jstr(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "\"" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "\"" { let out = out + "\\\"" } + else { if ch == "\\" { let out = out + "\\\\" } + else { if ch == "\n" { let out = out + "\\n" } + else { if ch == "\r" { let out = out + "\\r" } + else { if ch == "\t" { let out = out + "\\t" } + else { let out = out + ch } } } } } + let i = i + 1 + } + out + "\"" +} + +fn jerr(msg: String) -> String { + "{\"error\":" + jstr(msg) + "}" +} + +fn ok_obj() -> String { + "{\"ok\":true}" +} + +// ── URL/query parsing ──────────────────────────────────────────────────────── + +fn url_decode(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "+" { + let out = out + " " + let i = i + 1 + } else { if ch == "%" { + // Read two hex digits + if i + 2 < n { + let hex: String = native_list_get(chars, i + 1) + native_list_get(chars, i + 2) + let code: Int = hex_to_int(hex) + let out = out + char_from_code(code) + let i = i + 3 + } else { + let out = out + ch + let i = i + 1 + } + } else { + let out = out + ch + let i = i + 1 + } } + } + out +} + +fn hex_digit(ch: String) -> Int { + if ch == "0" { return 0 } if ch == "1" { return 1 } + if ch == "2" { return 2 } if ch == "3" { return 3 } + if ch == "4" { return 4 } if ch == "5" { return 5 } + if ch == "6" { return 6 } if ch == "7" { return 7 } + if ch == "8" { return 8 } if ch == "9" { return 9 } + if ch == "a" { return 10 } if ch == "A" { return 10 } + if ch == "b" { return 11 } if ch == "B" { return 11 } + if ch == "c" { return 12 } if ch == "C" { return 12 } + if ch == "d" { return 13 } if ch == "D" { return 13 } + if ch == "e" { return 14 } if ch == "E" { return 14 } + if ch == "f" { return 15 } if ch == "F" { return 15 } + -1 +} + +fn hex_to_int(hex: String) -> Int { + let chars: [String] = native_string_chars(hex) + let n: Int = native_list_len(chars) + let acc: Int = 0 + let i: Int = 0 + while i < n { + let d: Int = hex_digit(native_list_get(chars, i)) + if d < 0 { return acc } + let acc = acc * 16 + d + let i = i + 1 + } + acc +} + +// Map an ASCII code point back to its single-byte string. Above 127 the +// runtime would need char_from_code(>127) → multi-byte; we only need +// printable ASCII for query strings here. +fn char_from_code(code: Int) -> String { + let table: String = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + if code < 32 { return " " } + if code > 126 { return "?" } + str_slice(table, code - 32, code - 31) +} + +fn split_path_query(path: String) -> Map { + let q: Int = str_index_of(path, "?") + if q < 0 { return { "path": path, "query": "" } } + { "path": str_slice(path, 0, q), "query": str_slice(path, q + 1, str_len(path)) } +} + +// query_get: pull the value of a single key out of `a=1&b=hello%20world`. +fn query_get(query: String, key: String) -> String { + if str_eq(query, "") { return "" } + let pairs: [String] = str_split(query, "&") + let n: Int = native_list_len(pairs) + let prefix: String = key + "=" + let i: Int = 0 + while i < n { + let pair: String = native_list_get(pairs, i) + if str_starts_with(pair, prefix) { + return url_decode(str_slice(pair, str_len(prefix), str_len(pair))) + } + if str_eq(pair, key) { + return "" + } + let i = i + 1 + } + "" +} + +// ── Configuration ──────────────────────────────────────────────────────────── + +fn cfg_port() -> Int { + let p: String = env("EL_IDE_PORT") + if str_eq(p, "") { return 7771 } + str_to_int(p) +} + +fn cfg_project_path() -> String { + let p: String = env("EL_IDE_PROJECT_PATH") + if str_eq(p, "") { return "." } + p +} + +fn cfg_engram_url() -> String { + let u: String = env("EL_ENGRAM_URL") + if str_eq(u, "") { return "http://localhost:8742" } + u +} + +fn cfg_el_binary() -> String { + let b: String = env("EL_BINARY") + if str_eq(b, "") { return "el" } + b +} + +// ── Path safety ────────────────────────────────────────────────────────────── + +fn safe_join(rel: String) -> String { + let root: String = cfg_project_path() + // Reject obvious traversals. + if str_contains(rel, "..") { return "" } + if str_starts_with(rel, "/") { return "" } + if str_eq(rel, ".") { return root } + root + "/" + rel +} + +// ── Status route ───────────────────────────────────────────────────────────── + +fn route_status() -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "\"project_name\":" + jstr(cfg_project_path())) + let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path())) + let parts = native_list_append(parts, "\"version\":" + jstr("0.1.0")) + let parts = native_list_append(parts, "\"engine\":" + jstr("el-ide")) + "{" + list_join(parts, ",") + "}" +} + +// ── Config route ───────────────────────────────────────────────────────────── + +fn route_config() -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "\"port\":" + int_to_str(cfg_port())) + let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path())) + let parts = native_list_append(parts, "\"engram_url\":" + jstr(cfg_engram_url())) + let parts = native_list_append(parts, "\"el_binary\":" + jstr(cfg_el_binary())) + "{" + list_join(parts, ",") + "}" +} + +// ── File API ───────────────────────────────────────────────────────────────── + +fn route_files_list(query: String) -> String { + let rel: String = query_get(query, "path") + if str_eq(rel, "") { let rel = "." } + let target: String = safe_join(rel) + if str_eq(target, "") { return jerr("path escapes project root") } + let entries: [String] = fs_list(target) + let n: Int = native_list_len(entries) + let out: String = "[" + let i: Int = 0 + while i < n { + let name: String = native_list_get(entries, i) + // Skip hidden + noise dirs + let skip: Bool = false + if str_starts_with(name, ".") { let skip = true } + if str_eq(name, "target") { let skip = true } + if str_eq(name, "node_modules") { let skip = true } + if str_eq(name, "dist") { let skip = true } + if !skip { + if i > 0 { + if !str_eq(out, "[") { let out = out + "," } + } + // Approximate is_dir by checking for an extension + let dot: Int = str_index_of(name, ".") + let is_dir: String = "false" + if dot < 0 { let is_dir = "true" } + let out = out + "{\"name\":" + jstr(name) + ",\"path\":" + jstr(rel + "/" + name) + ",\"is_dir\":" + is_dir + ",\"children\":null}" + } + let i = i + 1 + } + out + "]" +} + +fn route_file_read(query: String) -> String { + let rel: String = query_get(query, "path") + if str_eq(rel, "") { return jerr("missing path parameter") } + let target: String = safe_join(rel) + if str_eq(target, "") { return jerr("path escapes project root") } + let content: String = fs_read(target) + "{\"path\":" + jstr(rel) + ",\"content\":" + jstr(content) + "}" +} + +fn route_file_write(body: String) -> String { + let rel: String = json_get_string(body, "path") + let content: String = json_get_string(body, "content") + if str_eq(rel, "") { return jerr("missing path") } + let target: String = safe_join(rel) + if str_eq(target, "") { return jerr("path escapes project root") } + let ok: Bool = fs_write(target, content) + if ok { return ok_obj() } + jerr("cannot write file") +} + +// ── LSP routes (delegate to inlined lsp_* functions) ───────────────────────── + +fn route_lsp_complete(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_complete(source, pos) +} + +fn route_lsp_hover(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_hover(source, pos) +} + +fn route_lsp_errors(query: String) -> String { + let source: String = query_get(query, "source") + lsp_diagnostics(source) +} + +fn route_lsp_jsonrpc(body: String) -> String { + let method: String = json_get_string(body, "method") + let params_raw: String = json_get_raw(body, "params") + let id_val: String = json_get_raw(body, "id") + if str_eq(id_val, "") { let id_val = "null" } + let result: String = lsp_jsonrpc(method, params_raw) + "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}" +} + +fn route_outline(query: String) -> String { + let source: String = query_get(query, "source") + lsp_outline(source) +} + +fn route_definition(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_definition(source, pos) +} + +fn route_references(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_references(source, pos) +} + +fn route_format(body: String) -> String { + let content: String = json_get_string(body, "content") + lsp_format(content) +} + +fn route_type_graph(query: String) -> String { + let source: String = query_get(query, "source") + lsp_type_graph(source) +} + +// ── Themes ─────────────────────────────────────────────────────────────────── + +fn active_theme() -> String { + let t: String = state_get("ide/active_theme") + if str_eq(t, "") { return "dark" } + t +} + +fn route_themes_list() -> String { + let active: String = active_theme() + let names: [String] = native_list_empty() + let names = native_list_append(names, "dark") + let names = native_list_append(names, "light") + let names = native_list_append(names, "neuron") + let names = native_list_append(names, "high-contrast") + let labels: [String] = native_list_empty() + let labels = native_list_append(labels, "Dark") + let labels = native_list_append(labels, "Light") + let labels = native_list_append(labels, "Neuron") + let labels = native_list_append(labels, "High Contrast") + let n: Int = native_list_len(names) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let nm: String = native_list_get(names, i) + let lbl: String = native_list_get(labels, i) + let act: String = "false" + if str_eq(nm, active) { let act = "true" } + let out = out + "{\"name\":" + jstr(nm) + ",\"label\":" + jstr(lbl) + ",\"active\":" + act + "}" + let i = i + 1 + } + out + "]" +} + +fn route_themes_set(body: String) -> String { + let name: String = json_get_string(body, "name") + let valid: Bool = false + if str_eq(name, "dark") { let valid = true } + if str_eq(name, "light") { let valid = true } + if str_eq(name, "neuron") { let valid = true } + if str_eq(name, "high-contrast") { let valid = true } + if !valid { return jerr("unknown theme: " + name) } + state_set("ide/active_theme", name) + "{\"ok\":true,\"active\":" + jstr(name) + "}" +} + +// ── Plugins (delegate to plugin_* in plugin-host) ──────────────────────────── + +fn route_plugins_list() -> String { plugin_list_json() } + +fn route_plugins_install(body: String) -> String { + plugin_install(json_get_string(body, "name")) +} + +fn route_plugins_remove(body: String) -> String { + plugin_remove(json_get_string(body, "name")) +} + +fn route_plugins_enable(body: String) -> String { + plugin_enable(json_get_string(body, "name")) +} + +fn route_plugins_disable(body: String) -> String { + plugin_disable(json_get_string(body, "name")) +} + +// ── Snippets ───────────────────────────────────────────────────────────────── + +fn route_snippets() -> String { + let snips: [String] = native_list_empty() + let snips = native_list_append(snips, "{\"label\":\"fn\",\"description\":\"Function definition\",\"body\":\"fn ${1:name}(${2:params}) -> ${3:Ret} {\\n\\t${4:body}\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"type\",\"description\":\"Struct\",\"body\":\"type ${1:Name} {\\n\\t${2:field}: ${3:Type},\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"enum\",\"description\":\"Enum\",\"body\":\"enum ${1:Name} {\\n\\t${2:Variant},\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"match\",\"description\":\"Match expression\",\"body\":\"match ${1:expr} {\\n\\t${2:pat} => ${3:body},\\n\\t_ => ${4:default},\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"let\",\"description\":\"Let binding\",\"body\":\"let ${1:name}: ${2:Type} = ${3:value}\"}") + let snips = native_list_append(snips, "{\"label\":\"if\",\"description\":\"If expression\",\"body\":\"if ${1:cond} {\\n\\t${2:body}\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"for\",\"description\":\"For loop\",\"body\":\"for ${1:item} in ${2:iter} {\\n\\t${3:body}\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"activate\",\"description\":\"Activate\",\"body\":\"activate ${1:Type}\"}") + "[" + list_join(snips, ",") + "]" +} + +// ── Settings (file-backed) ─────────────────────────────────────────────────── + +fn settings_path() -> String { + let home: String = env("HOME") + if str_eq(home, "") { let home = "/tmp" } + home + "/.el-ide/settings.json" +} + +fn default_settings_json() -> String { + "{\"theme\":\"dark\",\"fontSize\":13,\"tabSize\":4,\"wordWrap\":false,\"formatOnSave\":false,\"vimMode\":false,\"minimap\":true,\"elBinaryPath\":\"el\",\"engramUrl\":\"http://localhost:8742\",\"stickyScroll\":true,\"lineNumbers\":true,\"bracketMatching\":true}" +} + +fn route_settings_get() -> String { + let s: String = fs_read(settings_path()) + if str_eq(s, "") { return default_settings_json() } + s +} + +fn route_settings_save(body: String) -> String { + // Body is { "settings": {...} } — extract the inner blob. + let blob: String = json_get_raw(body, "settings") + if str_eq(blob, "") { return jerr("missing settings") } + fs_write(settings_path(), blob) + blob +} + +fn route_settings_reset() -> String { + fs_write(settings_path(), default_settings_json()) + default_settings_json() +} + +// ── Build / run (synchronous; no SSE in current http_serve runtime) ────────── +// +// The Rust crate spawned `el build/run` and streamed output as SSE events. +// El's `http_serve` is request/response only — handlers return one string. +// We call the el binary, capture stdout via fs_read of a redirected temp +// file, and return JSON. Streaming is a runtime gap (see report). + +fn route_build_or_run(action: String, body: String) -> String { + let rel_file: String = json_get_string(body, "file") + if str_eq(rel_file, "") { let rel_file = "src/main.el" } + let target: String = safe_join(rel_file) + if str_eq(target, "") { return jerr("path escapes project root") } + // No process-spawn primitive in the runtime; this is a stub that + // documents the planned behaviour. See report for runtime gap. + "{\"event\":\"info\",\"action\":" + jstr(action) + ",\"file\":" + jstr(rel_file) + ",\"note\":\"el-ide-server: synchronous build requires a runtime exec primitive — el binary not invoked\"}" +} + +fn route_build(body: String) -> String { route_build_or_run("build", body) } +fn route_run(body: String) -> String { route_build_or_run("run", body) } + +// ── Engram reasoning proxy ─────────────────────────────────────────────────── + +fn route_reason(body: String) -> String { + let url: String = cfg_engram_url() + "/api/reason" + let resp: String = http_post(url, body) + if str_eq(resp, "") { return jerr("engram unreachable") } + resp +} + +fn route_activate_preview(query: String) -> String { + let type_name: String = query_get(query, "type_name") + let q: String = query_get(query, "query") + let url: String = cfg_engram_url() + "/api/activate-preview" + let body: String = "{\"type_name\":" + jstr(type_name) + ",\"query\":" + jstr(q) + ",\"limit\":5}" + let resp: String = http_post(url, body) + if str_eq(resp, "") { + return "{\"count\":0,\"nodes\":[],\"connected\":false}" + } + resp +} + +// ── Index page (minimal placeholder; the real bundle lives in ide/index.html) ─ + +fn route_index() -> String { + let path: String = cfg_project_path() + "/ide/index.html" + let html: String = fs_read(path) + if !str_eq(html, "") { return html } + "el-ide

el-ide

Backend running. UI bundle missing at " + path + ".

" +} + +// ── Health ─────────────────────────────────────────────────────────────────── + +fn route_health() -> String { + "{\"status\":\"ok\",\"engine\":\"el-ide-server\"}" +} + +// ── Dispatcher ─────────────────────────────────────────────────────────────── + +fn handle_request(method: String, path: String, body: String) -> String { + let parsed: Map = split_path_query(path) + let route: String = parsed["path"] + let query: String = parsed["query"] + + if str_eq(route, "/") { return route_index() } + if str_eq(route, "/health") { return route_health() } + if str_eq(route, "/api/status") { return route_status() } + if str_eq(route, "/api/config") { return route_config() } + + // Files + if str_eq(route, "/api/files") { return route_files_list(query) } + if str_eq(route, "/api/file") { + if str_eq(method, "GET") { return route_file_read(query) } + if str_eq(method, "POST") { return route_file_write(body) } + } + + // Search — minimal stub (real impl needs recursive grep) + if str_eq(route, "/api/search") { + let q: String = query_get(query, "q") + return "{\"query\":" + jstr(q) + ",\"results\":[]}" + } + + // LSP + if str_eq(route, "/api/lsp/complete") { return route_lsp_complete(query) } + if str_eq(route, "/api/lsp/hover") { return route_lsp_hover(query) } + if str_eq(route, "/api/lsp/errors") { return route_lsp_errors(query) } + if str_eq(route, "/api/lsp/activate-preview") { return route_activate_preview(query) } + if str_eq(route, "/api/lsp/jsonrpc") { return route_lsp_jsonrpc(body) } + if str_eq(route, "/api/type-graph") { return route_type_graph(query) } + if str_eq(route, "/api/outline") { return route_outline(query) } + if str_eq(route, "/api/definition") { return route_definition(query) } + if str_eq(route, "/api/references") { return route_references(query) } + if str_eq(route, "/api/format") { return route_format(body) } + if str_eq(route, "/api/completions/snippet") { return route_snippets() } + + // Themes + if str_eq(route, "/api/themes") { return route_themes_list() } + if str_eq(route, "/api/themes/active") { return route_themes_set(body) } + + // Plugins + if str_eq(route, "/api/plugins") { return route_plugins_list() } + if str_eq(route, "/api/plugins/install") { return route_plugins_install(body) } + if str_eq(route, "/api/plugins/remove") { return route_plugins_remove(body) } + if str_eq(route, "/api/plugins/enable") { return route_plugins_enable(body) } + if str_eq(route, "/api/plugins/disable") { return route_plugins_disable(body) } + + // Settings + if str_eq(route, "/api/settings") { + if str_eq(method, "GET") { return route_settings_get() } + if str_eq(method, "POST") { return route_settings_save(body) } + if str_eq(method, "DELETE") { return route_settings_reset() } + } + + // Build / run + if str_eq(route, "/api/build") { return route_build(body) } + if str_eq(route, "/api/run") { return route_run(body) } + + // Reasoning proxy + if str_eq(route, "/api/reason") { return route_reason(body) } + + // Git — relies on shell exec; runtime gap. Stub returns empty. + if str_eq(route, "/api/git/status") { return "[]" } + if str_eq(route, "/api/git/diff") { return "\"\"" } + + "{\"error\":\"not found\",\"path\":" + jstr(route) + "}" +} + +// ── Entry ──────────────────────────────────────────────────────────────────── + +fn server_main() -> Void { + plugin_init() + let port: Int = cfg_port() + println("[el-ide-server] project=" + cfg_project_path()) + println("[el-ide-server] engram=" + cfg_engram_url()) + println("[el-ide-server] listening on http://0.0.0.0:" + int_to_str(port)) + http_set_handler("handle_request") + http_serve(port, "handle_request") +} + +server_main() diff --git a/ide/vessels/el-ide-server/src/_combined.err b/ide/vessels/el-ide-server/src/_combined.err new file mode 100644 index 0000000..e69de29 diff --git a/ide/vessels/el-ide-server/src/el-ide-server b/ide/vessels/el-ide-server/src/el-ide-server new file mode 100755 index 0000000..55345e2 Binary files /dev/null and b/ide/vessels/el-ide-server/src/el-ide-server differ diff --git a/ide/vessels/el-ide-server/src/el-ide-server-asan b/ide/vessels/el-ide-server/src/el-ide-server-asan new file mode 100755 index 0000000..42f1470 Binary files /dev/null and b/ide/vessels/el-ide-server/src/el-ide-server-asan differ diff --git a/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Info.plist b/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Info.plist new file mode 100644 index 0000000..ec968ce --- /dev/null +++ b/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleIdentifier + com.apple.xcode.dsym.el-ide-server-asan + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + dSYM + CFBundleSignature + ???? + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Resources/DWARF/el-ide-server-asan b/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Resources/DWARF/el-ide-server-asan new file mode 100644 index 0000000..bce9d7b Binary files /dev/null and b/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Resources/DWARF/el-ide-server-asan differ diff --git a/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Resources/Relocations/aarch64/el-ide-server-asan.yml b/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Resources/Relocations/aarch64/el-ide-server-asan.yml new file mode 100644 index 0000000..ef36ba8 --- /dev/null +++ b/ide/vessels/el-ide-server/src/el-ide-server-asan.dSYM/Contents/Resources/Relocations/aarch64/el-ide-server-asan.yml @@ -0,0 +1,5 @@ +--- +triple: 'arm64-apple-darwin' +binary-path: el-ide-server-asan +relocations: [] +... diff --git a/ide/vessels/el-ide-server/src/main.el b/ide/vessels/el-ide-server/src/main.el new file mode 100644 index 0000000..7e64a54 --- /dev/null +++ b/ide/vessels/el-ide-server/src/main.el @@ -0,0 +1,567 @@ +// main.el — el-ide-server +// +// HTTP backend for the Engram IDE. Pure-El port of the el-ide-server Rust +// crate. Wires the el-lsp and el-plugin-host vessels together and serves +// the IDE-frontend protocol used by the React-style frontend. +// +// El compilation note: `import` statements are parsed but the codegen +// concatenates nothing. The build script for this vessel concatenates +// +// vessels/el-lsp/src/main.el (without the trailing entry call) +// vessels/el-plugin-host/src/main.el +// vessels/el-ide-server/src/main.el (this file) +// +// into a single _combined.el before invoking elc. This file therefore +// assumes the lsp_* and plugin_* functions defined in those siblings are +// in scope. See README / build.sh. +// +// All routes serve JSON over HTTP. Query parameters are parsed by hand +// (the runtime gives us strings and a request body). Streaming endpoints +// (SSE) are emulated by returning the full output once — the runtime's +// http_serve handler signature is response-as-string, so true server-sent +// streaming requires a runtime extension. Flagged in the report. + +// ── Tiny utility layer ─────────────────────────────────────────────────────── + +fn jstr(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "\"" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "\"" { let out = out + "\\\"" } + else { if ch == "\\" { let out = out + "\\\\" } + else { if ch == "\n" { let out = out + "\\n" } + else { if ch == "\r" { let out = out + "\\r" } + else { if ch == "\t" { let out = out + "\\t" } + else { let out = out + ch } } } } } + let i = i + 1 + } + out + "\"" +} + +fn jerr(msg: String) -> String { + "{\"error\":" + jstr(msg) + "}" +} + +fn ok_obj() -> String { + "{\"ok\":true}" +} + +// ── URL/query parsing ──────────────────────────────────────────────────────── + +fn url_decode(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "+" { + let out = out + " " + let i = i + 1 + } else { if ch == "%" { + // Read two hex digits + if i + 2 < n { + let hex: String = native_list_get(chars, i + 1) + native_list_get(chars, i + 2) + let code: Int = hex_to_int(hex) + let out = out + char_from_code(code) + let i = i + 3 + } else { + let out = out + ch + let i = i + 1 + } + } else { + let out = out + ch + let i = i + 1 + } } + } + out +} + +fn hex_digit(ch: String) -> Int { + if ch == "0" { return 0 } if ch == "1" { return 1 } + if ch == "2" { return 2 } if ch == "3" { return 3 } + if ch == "4" { return 4 } if ch == "5" { return 5 } + if ch == "6" { return 6 } if ch == "7" { return 7 } + if ch == "8" { return 8 } if ch == "9" { return 9 } + if ch == "a" { return 10 } if ch == "A" { return 10 } + if ch == "b" { return 11 } if ch == "B" { return 11 } + if ch == "c" { return 12 } if ch == "C" { return 12 } + if ch == "d" { return 13 } if ch == "D" { return 13 } + if ch == "e" { return 14 } if ch == "E" { return 14 } + if ch == "f" { return 15 } if ch == "F" { return 15 } + -1 +} + +fn hex_to_int(hex: String) -> Int { + let chars: [String] = native_string_chars(hex) + let n: Int = native_list_len(chars) + let acc: Int = 0 + let i: Int = 0 + while i < n { + let d: Int = hex_digit(native_list_get(chars, i)) + if d < 0 { return acc } + let acc = acc * 16 + d + let i = i + 1 + } + acc +} + +// Map an ASCII code point back to its single-byte string. Above 127 the +// runtime would need char_from_code(>127) → multi-byte; we only need +// printable ASCII for query strings here. +fn char_from_code(code: Int) -> String { + let table: String = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + if code < 32 { return " " } + if code > 126 { return "?" } + str_slice(table, code - 32, code - 31) +} + +fn split_path_query(path: String) -> Map { + let q: Int = str_index_of(path, "?") + if q < 0 { return { "path": path, "query": "" } } + { "path": str_slice(path, 0, q), "query": str_slice(path, q + 1, str_len(path)) } +} + +// query_get: pull the value of a single key out of `a=1&b=hello%20world`. +fn query_get(query: String, key: String) -> String { + if str_eq(query, "") { return "" } + let pairs: [String] = str_split(query, "&") + let n: Int = native_list_len(pairs) + let prefix: String = key + "=" + let i: Int = 0 + while i < n { + let pair: String = native_list_get(pairs, i) + if str_starts_with(pair, prefix) { + return url_decode(str_slice(pair, str_len(prefix), str_len(pair))) + } + if str_eq(pair, key) { + return "" + } + let i = i + 1 + } + "" +} + +// ── Configuration ──────────────────────────────────────────────────────────── + +fn cfg_port() -> Int { + let p: String = env("EL_IDE_PORT") + if str_eq(p, "") { return 7771 } + str_to_int(p) +} + +fn cfg_project_path() -> String { + let p: String = env("EL_IDE_PROJECT_PATH") + if str_eq(p, "") { return "." } + p +} + +fn cfg_engram_url() -> String { + let u: String = env("EL_ENGRAM_URL") + if str_eq(u, "") { return "http://localhost:8742" } + u +} + +fn cfg_el_binary() -> String { + let b: String = env("EL_BINARY") + if str_eq(b, "") { return "el" } + b +} + +// ── Path safety ────────────────────────────────────────────────────────────── + +fn safe_join(rel: String) -> String { + let root: String = cfg_project_path() + // Reject obvious traversals. + if str_contains(rel, "..") { return "" } + if str_starts_with(rel, "/") { return "" } + if str_eq(rel, ".") { return root } + root + "/" + rel +} + +// ── Status route ───────────────────────────────────────────────────────────── + +fn route_status() -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "\"project_name\":" + jstr(cfg_project_path())) + let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path())) + let parts = native_list_append(parts, "\"version\":" + jstr("0.1.0")) + let parts = native_list_append(parts, "\"engine\":" + jstr("el-ide")) + "{" + list_join(parts, ",") + "}" +} + +// ── Config route ───────────────────────────────────────────────────────────── + +fn route_config() -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "\"port\":" + int_to_str(cfg_port())) + let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path())) + let parts = native_list_append(parts, "\"engram_url\":" + jstr(cfg_engram_url())) + let parts = native_list_append(parts, "\"el_binary\":" + jstr(cfg_el_binary())) + "{" + list_join(parts, ",") + "}" +} + +// ── File API ───────────────────────────────────────────────────────────────── + +fn route_files_list(query: String) -> String { + let rel: String = query_get(query, "path") + if str_eq(rel, "") { let rel = "." } + let target: String = safe_join(rel) + if str_eq(target, "") { return jerr("path escapes project root") } + let entries: [String] = fs_list(target) + let n: Int = native_list_len(entries) + let out: String = "[" + let i: Int = 0 + while i < n { + let name: String = native_list_get(entries, i) + // Skip hidden + noise dirs + let skip: Bool = false + if str_starts_with(name, ".") { let skip = true } + if str_eq(name, "target") { let skip = true } + if str_eq(name, "node_modules") { let skip = true } + if str_eq(name, "dist") { let skip = true } + if !skip { + if i > 0 { + if !str_eq(out, "[") { let out = out + "," } + } + // Approximate is_dir by checking for an extension + let dot: Int = str_index_of(name, ".") + let is_dir: String = "false" + if dot < 0 { let is_dir = "true" } + let out = out + "{\"name\":" + jstr(name) + ",\"path\":" + jstr(rel + "/" + name) + ",\"is_dir\":" + is_dir + ",\"children\":null}" + } + let i = i + 1 + } + out + "]" +} + +fn route_file_read(query: String) -> String { + let rel: String = query_get(query, "path") + if str_eq(rel, "") { return jerr("missing path parameter") } + let target: String = safe_join(rel) + if str_eq(target, "") { return jerr("path escapes project root") } + let content: String = fs_read(target) + "{\"path\":" + jstr(rel) + ",\"content\":" + jstr(content) + "}" +} + +fn route_file_write(body: String) -> String { + let rel: String = json_get_string(body, "path") + let content: String = json_get_string(body, "content") + if str_eq(rel, "") { return jerr("missing path") } + let target: String = safe_join(rel) + if str_eq(target, "") { return jerr("path escapes project root") } + let ok: Bool = fs_write(target, content) + if ok { return ok_obj() } + jerr("cannot write file") +} + +// ── LSP routes (delegate to inlined lsp_* functions) ───────────────────────── + +fn route_lsp_complete(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_complete(source, pos) +} + +fn route_lsp_hover(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_hover(source, pos) +} + +fn route_lsp_errors(query: String) -> String { + let source: String = query_get(query, "source") + lsp_diagnostics(source) +} + +fn route_lsp_jsonrpc(body: String) -> String { + let method: String = json_get_string(body, "method") + let params_raw: String = json_get_raw(body, "params") + let id_val: String = json_get_raw(body, "id") + if str_eq(id_val, "") { let id_val = "null" } + let result: String = lsp_jsonrpc(method, params_raw) + "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}" +} + +fn route_outline(query: String) -> String { + let source: String = query_get(query, "source") + lsp_outline(source) +} + +fn route_definition(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_definition(source, pos) +} + +fn route_references(query: String) -> String { + let source: String = query_get(query, "source") + let pos: Int = str_to_int(query_get(query, "pos")) + lsp_references(source, pos) +} + +fn route_format(body: String) -> String { + let content: String = json_get_string(body, "content") + lsp_format(content) +} + +fn route_type_graph(query: String) -> String { + let source: String = query_get(query, "source") + lsp_type_graph(source) +} + +// ── Themes ─────────────────────────────────────────────────────────────────── + +fn active_theme() -> String { + let t: String = state_get("ide/active_theme") + if str_eq(t, "") { return "dark" } + t +} + +fn route_themes_list() -> String { + let active: String = active_theme() + let names: [String] = native_list_empty() + let names = native_list_append(names, "dark") + let names = native_list_append(names, "light") + let names = native_list_append(names, "neuron") + let names = native_list_append(names, "high-contrast") + let labels: [String] = native_list_empty() + let labels = native_list_append(labels, "Dark") + let labels = native_list_append(labels, "Light") + let labels = native_list_append(labels, "Neuron") + let labels = native_list_append(labels, "High Contrast") + let n: Int = native_list_len(names) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let nm: String = native_list_get(names, i) + let lbl: String = native_list_get(labels, i) + let act: String = "false" + if str_eq(nm, active) { let act = "true" } + let out = out + "{\"name\":" + jstr(nm) + ",\"label\":" + jstr(lbl) + ",\"active\":" + act + "}" + let i = i + 1 + } + out + "]" +} + +fn route_themes_set(body: String) -> String { + let name: String = json_get_string(body, "name") + let valid: Bool = false + if str_eq(name, "dark") { let valid = true } + if str_eq(name, "light") { let valid = true } + if str_eq(name, "neuron") { let valid = true } + if str_eq(name, "high-contrast") { let valid = true } + if !valid { return jerr("unknown theme: " + name) } + state_set("ide/active_theme", name) + "{\"ok\":true,\"active\":" + jstr(name) + "}" +} + +// ── Plugins (delegate to plugin_* in plugin-host) ──────────────────────────── + +fn route_plugins_list() -> String { plugin_list_json() } + +fn route_plugins_install(body: String) -> String { + plugin_install(json_get_string(body, "name")) +} + +fn route_plugins_remove(body: String) -> String { + plugin_remove(json_get_string(body, "name")) +} + +fn route_plugins_enable(body: String) -> String { + plugin_enable(json_get_string(body, "name")) +} + +fn route_plugins_disable(body: String) -> String { + plugin_disable(json_get_string(body, "name")) +} + +// ── Snippets ───────────────────────────────────────────────────────────────── + +fn route_snippets() -> String { + let snips: [String] = native_list_empty() + let snips = native_list_append(snips, "{\"label\":\"fn\",\"description\":\"Function definition\",\"body\":\"fn ${1:name}(${2:params}) -> ${3:Ret} {\\n\\t${4:body}\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"type\",\"description\":\"Struct\",\"body\":\"type ${1:Name} {\\n\\t${2:field}: ${3:Type},\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"enum\",\"description\":\"Enum\",\"body\":\"enum ${1:Name} {\\n\\t${2:Variant},\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"match\",\"description\":\"Match expression\",\"body\":\"match ${1:expr} {\\n\\t${2:pat} => ${3:body},\\n\\t_ => ${4:default},\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"let\",\"description\":\"Let binding\",\"body\":\"let ${1:name}: ${2:Type} = ${3:value}\"}") + let snips = native_list_append(snips, "{\"label\":\"if\",\"description\":\"If expression\",\"body\":\"if ${1:cond} {\\n\\t${2:body}\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"for\",\"description\":\"For loop\",\"body\":\"for ${1:item} in ${2:iter} {\\n\\t${3:body}\\n}\"}") + let snips = native_list_append(snips, "{\"label\":\"activate\",\"description\":\"Activate\",\"body\":\"activate ${1:Type}\"}") + "[" + list_join(snips, ",") + "]" +} + +// ── Settings (file-backed) ─────────────────────────────────────────────────── + +fn settings_path() -> String { + let home: String = env("HOME") + if str_eq(home, "") { let home = "/tmp" } + home + "/.el-ide/settings.json" +} + +fn default_settings_json() -> String { + "{\"theme\":\"dark\",\"fontSize\":13,\"tabSize\":4,\"wordWrap\":false,\"formatOnSave\":false,\"vimMode\":false,\"minimap\":true,\"elBinaryPath\":\"el\",\"engramUrl\":\"http://localhost:8742\",\"stickyScroll\":true,\"lineNumbers\":true,\"bracketMatching\":true}" +} + +fn route_settings_get() -> String { + let s: String = fs_read(settings_path()) + if str_eq(s, "") { return default_settings_json() } + s +} + +fn route_settings_save(body: String) -> String { + // Body is { "settings": {...} } — extract the inner blob. + let blob: String = json_get_raw(body, "settings") + if str_eq(blob, "") { return jerr("missing settings") } + fs_write(settings_path(), blob) + blob +} + +fn route_settings_reset() -> String { + fs_write(settings_path(), default_settings_json()) + default_settings_json() +} + +// ── Build / run (synchronous; no SSE in current http_serve runtime) ────────── +// +// The Rust crate spawned `el build/run` and streamed output as SSE events. +// El's `http_serve` is request/response only — handlers return one string. +// We call the el binary, capture stdout via fs_read of a redirected temp +// file, and return JSON. Streaming is a runtime gap (see report). + +fn route_build_or_run(action: String, body: String) -> String { + let rel_file: String = json_get_string(body, "file") + if str_eq(rel_file, "") { let rel_file = "src/main.el" } + let target: String = safe_join(rel_file) + if str_eq(target, "") { return jerr("path escapes project root") } + // No process-spawn primitive in the runtime; this is a stub that + // documents the planned behaviour. See report for runtime gap. + "{\"event\":\"info\",\"action\":" + jstr(action) + ",\"file\":" + jstr(rel_file) + ",\"note\":\"el-ide-server: synchronous build requires a runtime exec primitive — el binary not invoked\"}" +} + +fn route_build(body: String) -> String { route_build_or_run("build", body) } +fn route_run(body: String) -> String { route_build_or_run("run", body) } + +// ── Engram reasoning proxy ─────────────────────────────────────────────────── + +fn route_reason(body: String) -> String { + let url: String = cfg_engram_url() + "/api/reason" + let resp: String = http_post(url, body) + if str_eq(resp, "") { return jerr("engram unreachable") } + resp +} + +fn route_activate_preview(query: String) -> String { + let type_name: String = query_get(query, "type_name") + let q: String = query_get(query, "query") + let url: String = cfg_engram_url() + "/api/activate-preview" + let body: String = "{\"type_name\":" + jstr(type_name) + ",\"query\":" + jstr(q) + ",\"limit\":5}" + let resp: String = http_post(url, body) + if str_eq(resp, "") { + return "{\"count\":0,\"nodes\":[],\"connected\":false}" + } + resp +} + +// ── Index page (minimal placeholder; the real bundle lives in ide/index.html) ─ + +fn route_index() -> String { + let path: String = cfg_project_path() + "/ide/index.html" + let html: String = fs_read(path) + if !str_eq(html, "") { return html } + "el-ide

el-ide

Backend running. UI bundle missing at " + path + ".

" +} + +// ── Health ─────────────────────────────────────────────────────────────────── + +fn route_health() -> String { + "{\"status\":\"ok\",\"engine\":\"el-ide-server\"}" +} + +// ── Dispatcher ─────────────────────────────────────────────────────────────── + +fn handle_request(method: String, path: String, body: String) -> String { + let parsed: Map = split_path_query(path) + let route: String = parsed["path"] + let query: String = parsed["query"] + + if str_eq(route, "/") { return route_index() } + if str_eq(route, "/health") { return route_health() } + if str_eq(route, "/api/status") { return route_status() } + if str_eq(route, "/api/config") { return route_config() } + + // Files + if str_eq(route, "/api/files") { return route_files_list(query) } + if str_eq(route, "/api/file") { + if str_eq(method, "GET") { return route_file_read(query) } + if str_eq(method, "POST") { return route_file_write(body) } + } + + // Search — minimal stub (real impl needs recursive grep) + if str_eq(route, "/api/search") { + let q: String = query_get(query, "q") + return "{\"query\":" + jstr(q) + ",\"results\":[]}" + } + + // LSP + if str_eq(route, "/api/lsp/complete") { return route_lsp_complete(query) } + if str_eq(route, "/api/lsp/hover") { return route_lsp_hover(query) } + if str_eq(route, "/api/lsp/errors") { return route_lsp_errors(query) } + if str_eq(route, "/api/lsp/activate-preview") { return route_activate_preview(query) } + if str_eq(route, "/api/lsp/jsonrpc") { return route_lsp_jsonrpc(body) } + if str_eq(route, "/api/type-graph") { return route_type_graph(query) } + if str_eq(route, "/api/outline") { return route_outline(query) } + if str_eq(route, "/api/definition") { return route_definition(query) } + if str_eq(route, "/api/references") { return route_references(query) } + if str_eq(route, "/api/format") { return route_format(body) } + if str_eq(route, "/api/completions/snippet") { return route_snippets() } + + // Themes + if str_eq(route, "/api/themes") { return route_themes_list() } + if str_eq(route, "/api/themes/active") { return route_themes_set(body) } + + // Plugins + if str_eq(route, "/api/plugins") { return route_plugins_list() } + if str_eq(route, "/api/plugins/install") { return route_plugins_install(body) } + if str_eq(route, "/api/plugins/remove") { return route_plugins_remove(body) } + if str_eq(route, "/api/plugins/enable") { return route_plugins_enable(body) } + if str_eq(route, "/api/plugins/disable") { return route_plugins_disable(body) } + + // Settings + if str_eq(route, "/api/settings") { + if str_eq(method, "GET") { return route_settings_get() } + if str_eq(method, "POST") { return route_settings_save(body) } + if str_eq(method, "DELETE") { return route_settings_reset() } + } + + // Build / run + if str_eq(route, "/api/build") { return route_build(body) } + if str_eq(route, "/api/run") { return route_run(body) } + + // Reasoning proxy + if str_eq(route, "/api/reason") { return route_reason(body) } + + // Git — relies on shell exec; runtime gap. Stub returns empty. + if str_eq(route, "/api/git/status") { return "[]" } + if str_eq(route, "/api/git/diff") { return "\"\"" } + + "{\"error\":\"not found\",\"path\":" + jstr(route) + "}" +} + +// ── Entry ──────────────────────────────────────────────────────────────────── + +fn server_main() -> Void { + plugin_init() + let port: Int = cfg_port() + println("[el-ide-server] project=" + cfg_project_path()) + println("[el-ide-server] engram=" + cfg_engram_url()) + println("[el-ide-server] listening on http://0.0.0.0:" + int_to_str(port)) + http_set_handler("handle_request") + http_serve(port, "handle_request") +} + +server_main() diff --git a/ide/vessels/el-lsp/manifest.el b/ide/vessels/el-lsp/manifest.el new file mode 100644 index 0000000..925ed68 --- /dev/null +++ b/ide/vessels/el-lsp/manifest.el @@ -0,0 +1,17 @@ +// manifest.el — el-lsp vessel +// +// Language Server core for the El language. Exposes hover, completion, +// diagnostics, outline, format, and type-graph entry points consumed by +// el-ide-server (HTTP) and stdio-mode editors. + +vessel "el-lsp" { + version "0.1.0" + description "El language server: completion, hover, diagnostics, outline, format, type graph." + authors ["Will Anderson "] + edition "2026" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/ide/vessels/el-lsp/src/el-lsp b/ide/vessels/el-lsp/src/el-lsp new file mode 100755 index 0000000..49c1fa9 Binary files /dev/null and b/ide/vessels/el-lsp/src/el-lsp differ diff --git a/ide/vessels/el-lsp/src/main.c b/ide/vessels/el-lsp/src/main.c new file mode 100644 index 0000000..f36f443 --- /dev/null +++ b/ide/vessels/el-lsp/src/main.c @@ -0,0 +1,1469 @@ +#include +#include +#include "el_runtime.h" + +el_val_t is_digit_ch(el_val_t ch); +el_val_t is_alpha_ch(el_val_t ch); +el_val_t is_ident_ch(el_val_t ch); +el_val_t extract_prefix(el_val_t source, el_val_t pos); +el_val_t extract_token(el_val_t source, el_val_t pos); +el_val_t pos_to_line_col(el_val_t source, el_val_t pos); +el_val_t json_escape(el_val_t s); +el_val_t json_str(el_val_t s); +el_val_t json_kv_str(el_val_t k, el_val_t v); +el_val_t json_kv_int(el_val_t k, el_val_t v); +el_val_t json_kv_float(el_val_t k, el_val_t v); +el_val_t json_kv_raw(el_val_t k, el_val_t raw_v); +el_val_t json_array_of(el_val_t items); +el_val_t keywords_list(void); +el_val_t builtin_type_names(void); +el_val_t builtin_type_doc(el_val_t name); +el_val_t builtin_fns(void); +el_val_t keyword_doc(el_val_t kw); +el_val_t first_ident_in(el_val_t rest); +el_val_t scan_decls(el_val_t source); +el_val_t list_contains_str(el_val_t xs, el_val_t needle); +el_val_t prefix_score(el_val_t label, el_val_t prefix); +el_val_t completion_json(el_val_t label, el_val_t kind, el_val_t detail, el_val_t doc, el_val_t score); +el_val_t lsp_complete(el_val_t source, el_val_t pos); +el_val_t lsp_hover(el_val_t source, el_val_t pos); +el_val_t diag_json(el_val_t message, el_val_t severity, el_val_t line, el_val_t col); +el_val_t lsp_diagnostics(el_val_t source); +el_val_t outline_item_json(el_val_t kind, el_val_t name, el_val_t line); +el_val_t lsp_outline(el_val_t source); +el_val_t format_strip_trailing(el_val_t line); +el_val_t lsp_format(el_val_t source); +el_val_t lsp_type_graph(el_val_t source); +el_val_t collect_struct_fields(el_val_t source, el_val_t type_name); +el_val_t referenced_type(el_val_t field_str, el_val_t known_types); +el_val_t lsp_definition(el_val_t source, el_val_t pos); +el_val_t lsp_references(el_val_t source, el_val_t pos); +el_val_t lsp_jsonrpc(el_val_t method, el_val_t params_json); +el_val_t read_headers(void); +el_val_t lsp_stdio_loop(void); +el_val_t lsp_main(void); + +el_val_t is_digit_ch(el_val_t ch) { + if (str_eq(ch, EL_STR("0"))) { + return 1; + } + if (str_eq(ch, EL_STR("1"))) { + return 1; + } + if (str_eq(ch, EL_STR("2"))) { + return 1; + } + if (str_eq(ch, EL_STR("3"))) { + return 1; + } + if (str_eq(ch, EL_STR("4"))) { + return 1; + } + if (str_eq(ch, EL_STR("5"))) { + return 1; + } + if (str_eq(ch, EL_STR("6"))) { + return 1; + } + if (str_eq(ch, EL_STR("7"))) { + return 1; + } + if (str_eq(ch, EL_STR("8"))) { + return 1; + } + if (str_eq(ch, EL_STR("9"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_alpha_ch(el_val_t ch) { + el_val_t lower = str_to_lower(ch); + if (str_eq(ch, EL_STR(""))) { + return 0; + } + if (!str_eq(lower, str_to_upper(lower))) { + return 1; + } + if (str_contains(EL_STR("abcdefghijklmnopqrstuvwxyz"), ch)) { + return 1; + } + if (str_contains(EL_STR("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), ch)) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_ident_ch(el_val_t ch) { + if (is_digit_ch(ch)) { + return 1; + } + if (is_alpha_ch(ch)) { + return 1; + } + if (str_eq(ch, EL_STR("_"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t extract_prefix(el_val_t source, el_val_t pos) { + el_val_t n = str_len(source); + el_val_t end = pos; + if (end > n) { + end = n; + } + el_val_t start = end; + el_val_t chars = native_string_chars(source); + el_val_t total = native_list_len(chars); + el_val_t scanning = 1; + while (scanning) { + if (start <= 0) { + scanning = 0; + } else { + el_val_t prev = native_list_get(chars, (start - 1)); + if (is_ident_ch(prev)) { + start = (start - 1); + } else { + scanning = 0; + } + } + } + return str_slice(source, start, end); + return 0; +} + +el_val_t extract_token(el_val_t source, el_val_t pos) { + el_val_t n = str_len(source); + if (pos > n) { + return EL_STR(""); + } + el_val_t chars = native_string_chars(source); + el_val_t total = native_list_len(chars); + el_val_t start = pos; + el_val_t go_left = 1; + while (go_left) { + if (start <= 0) { + go_left = 0; + } else { + el_val_t prev = native_list_get(chars, (start - 1)); + if (is_ident_ch(prev)) { + start = (start - 1); + } else { + go_left = 0; + } + } + } + el_val_t end = pos; + el_val_t go_right = 1; + while (go_right) { + if (end >= total) { + go_right = 0; + } else { + el_val_t cur = native_list_get(chars, end); + if (is_ident_ch(cur)) { + end = (end + 1); + } else { + go_right = 0; + } + } + } + return str_slice(source, start, end); + return 0; +} + +el_val_t pos_to_line_col(el_val_t source, el_val_t pos) { + el_val_t n = str_len(source); + el_val_t cap = pos; + if (cap > n) { + cap = n; + } + el_val_t chars = native_string_chars(source); + el_val_t i = 0; + el_val_t line = 1; + el_val_t col = 1; + while (i < cap) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\n"))) { + line = (line + 1); + col = 1; + } else { + col = (col + 1); + } + i = (i + 1); + } + return el_map_new(2, "line", line, "col", col); + return 0; +} + +el_val_t json_escape(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\""))) { + out = el_str_concat(out, EL_STR("\\\"")); + } else { + if (str_eq(ch, EL_STR("\\"))) { + out = el_str_concat(out, EL_STR("\\\\")); + } else { + if (str_eq(ch, EL_STR("\n"))) { + out = el_str_concat(out, EL_STR("\\n")); + } else { + if (str_eq(ch, EL_STR("\r"))) { + out = el_str_concat(out, EL_STR("\\r")); + } else { + if (str_eq(ch, EL_STR("\t"))) { + out = el_str_concat(out, EL_STR("\\t")); + } else { + out = el_str_concat(out, ch); + } + } + } + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t json_str(el_val_t s) { + return el_str_concat(el_str_concat(EL_STR("\""), json_escape(s)), EL_STR("\"")); + return 0; +} + +el_val_t json_kv_str(el_val_t k, el_val_t v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), json_str(v)); + return 0; +} + +el_val_t json_kv_int(el_val_t k, el_val_t v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), int_to_str(v)); + return 0; +} + +el_val_t json_kv_float(el_val_t k, el_val_t v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), float_to_str(v)); + return 0; +} + +el_val_t json_kv_raw(el_val_t k, el_val_t raw_v) { + return el_str_concat(el_str_concat(json_str(k), EL_STR(":")), raw_v); + return 0; +} + +el_val_t json_array_of(el_val_t items) { + el_val_t n = native_list_len(items); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + out = el_str_concat(out, native_list_get(items, i)); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t keywords_list(void) { + el_val_t ks = native_list_empty(); + ks = native_list_append(ks, EL_STR("let")); + ks = native_list_append(ks, EL_STR("fn")); + ks = native_list_append(ks, EL_STR("type")); + ks = native_list_append(ks, EL_STR("enum")); + ks = native_list_append(ks, EL_STR("match")); + ks = native_list_append(ks, EL_STR("return")); + ks = native_list_append(ks, EL_STR("activate")); + ks = native_list_append(ks, EL_STR("where")); + ks = native_list_append(ks, EL_STR("sealed")); + ks = native_list_append(ks, EL_STR("if")); + ks = native_list_append(ks, EL_STR("else")); + ks = native_list_append(ks, EL_STR("for")); + ks = native_list_append(ks, EL_STR("in")); + ks = native_list_append(ks, EL_STR("while")); + ks = native_list_append(ks, EL_STR("true")); + ks = native_list_append(ks, EL_STR("false")); + ks = native_list_append(ks, EL_STR("test")); + ks = native_list_append(ks, EL_STR("seed")); + ks = native_list_append(ks, EL_STR("assert")); + ks = native_list_append(ks, EL_STR("target")); + ks = native_list_append(ks, EL_STR("protocol")); + ks = native_list_append(ks, EL_STR("impl")); + ks = native_list_append(ks, EL_STR("import")); + ks = native_list_append(ks, EL_STR("from")); + ks = native_list_append(ks, EL_STR("as")); + ks = native_list_append(ks, EL_STR("with")); + ks = native_list_append(ks, EL_STR("retry")); + ks = native_list_append(ks, EL_STR("times")); + ks = native_list_append(ks, EL_STR("fallback")); + ks = native_list_append(ks, EL_STR("reason")); + ks = native_list_append(ks, EL_STR("parallel")); + ks = native_list_append(ks, EL_STR("trace")); + ks = native_list_append(ks, EL_STR("requires")); + ks = native_list_append(ks, EL_STR("deploy")); + ks = native_list_append(ks, EL_STR("to")); + ks = native_list_append(ks, EL_STR("via")); + ks = native_list_append(ks, EL_STR("vessel")); + ks = native_list_append(ks, EL_STR("cgi")); + return ks; + return 0; +} + +el_val_t builtin_type_names(void) { + el_val_t ts = native_list_empty(); + ts = native_list_append(ts, EL_STR("Int")); + ts = native_list_append(ts, EL_STR("Float")); + ts = native_list_append(ts, EL_STR("String")); + ts = native_list_append(ts, EL_STR("Bool")); + ts = native_list_append(ts, EL_STR("Uuid")); + ts = native_list_append(ts, EL_STR("Void")); + ts = native_list_append(ts, EL_STR("Any")); + return ts; + return 0; +} + +el_val_t builtin_type_doc(el_val_t name) { + if (str_eq(name, EL_STR("Int"))) { + return EL_STR("64-bit signed integer"); + } + if (str_eq(name, EL_STR("Float"))) { + return EL_STR("64-bit IEEE 754 double"); + } + if (str_eq(name, EL_STR("String"))) { + return EL_STR("UTF-8 string"); + } + if (str_eq(name, EL_STR("Bool"))) { + return EL_STR("Boolean value"); + } + if (str_eq(name, EL_STR("Uuid"))) { + return EL_STR("RFC 4122 UUID"); + } + if (str_eq(name, EL_STR("Void"))) { + return EL_STR("Unit type — no value"); + } + if (str_eq(name, EL_STR("Any"))) { + return EL_STR("Dynamically-typed value"); + } + return EL_STR(""); + return 0; +} + +el_val_t builtin_fns(void) { + el_val_t xs = native_list_empty(); + xs = native_list_append(xs, EL_STR("println")); + xs = native_list_append(xs, EL_STR("fn(value: String) -> Void")); + xs = native_list_append(xs, EL_STR("print")); + xs = native_list_append(xs, EL_STR("fn(value: String) -> Void")); + xs = native_list_append(xs, EL_STR("readline")); + xs = native_list_append(xs, EL_STR("fn() -> String")); + xs = native_list_append(xs, EL_STR("args")); + xs = native_list_append(xs, EL_STR("fn() -> [String]")); + xs = native_list_append(xs, EL_STR("int_to_str")); + xs = native_list_append(xs, EL_STR("fn(n: Int) -> String")); + xs = native_list_append(xs, EL_STR("str_to_int")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Int")); + xs = native_list_append(xs, EL_STR("str_len")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Int")); + xs = native_list_append(xs, EL_STR("str_slice")); + xs = native_list_append(xs, EL_STR("fn(s: String, start: Int, end: Int) -> String")); + xs = native_list_append(xs, EL_STR("str_concat")); + xs = native_list_append(xs, EL_STR("fn(a: String, b: String) -> String")); + xs = native_list_append(xs, EL_STR("str_contains")); + xs = native_list_append(xs, EL_STR("fn(s: String, sub: String) -> Bool")); + xs = native_list_append(xs, EL_STR("str_starts_with")); + xs = native_list_append(xs, EL_STR("fn(s: String, p: String) -> Bool")); + xs = native_list_append(xs, EL_STR("str_ends_with")); + xs = native_list_append(xs, EL_STR("fn(s: String, suf: String) -> Bool")); + xs = native_list_append(xs, EL_STR("str_to_upper")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> String")); + xs = native_list_append(xs, EL_STR("str_to_lower")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> String")); + xs = native_list_append(xs, EL_STR("str_split")); + xs = native_list_append(xs, EL_STR("fn(s: String, sep: String) -> [String]")); + xs = native_list_append(xs, EL_STR("str_trim")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> String")); + xs = native_list_append(xs, EL_STR("str_replace")); + xs = native_list_append(xs, EL_STR("fn(s: String, from: String, to: String) -> String")); + xs = native_list_append(xs, EL_STR("str_index_of")); + xs = native_list_append(xs, EL_STR("fn(s: String, sub: String) -> Int")); + xs = native_list_append(xs, EL_STR("float_to_str")); + xs = native_list_append(xs, EL_STR("fn(f: Float) -> String")); + xs = native_list_append(xs, EL_STR("str_to_float")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Float")); + xs = native_list_append(xs, EL_STR("math_sqrt")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_log")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_sin")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_cos")); + xs = native_list_append(xs, EL_STR("fn(n: Float) -> Float")); + xs = native_list_append(xs, EL_STR("math_pi")); + xs = native_list_append(xs, EL_STR("fn() -> Float")); + xs = native_list_append(xs, EL_STR("el_abs")); + xs = native_list_append(xs, EL_STR("fn(n: Int) -> Int")); + xs = native_list_append(xs, EL_STR("el_min")); + xs = native_list_append(xs, EL_STR("fn(a: Int, b: Int) -> Int")); + xs = native_list_append(xs, EL_STR("el_max")); + xs = native_list_append(xs, EL_STR("fn(a: Int, b: Int) -> Int")); + xs = native_list_append(xs, EL_STR("el_list_empty")); + xs = native_list_append(xs, EL_STR("fn() -> [Any]")); + xs = native_list_append(xs, EL_STR("el_list_len")); + xs = native_list_append(xs, EL_STR("fn(l: [Any]) -> Int")); + xs = native_list_append(xs, EL_STR("el_list_get")); + xs = native_list_append(xs, EL_STR("fn(l: [Any], i: Int) -> Any")); + xs = native_list_append(xs, EL_STR("el_list_append")); + xs = native_list_append(xs, EL_STR("fn(l: [Any], v: Any) -> [Any]")); + xs = native_list_append(xs, EL_STR("list_join")); + xs = native_list_append(xs, EL_STR("fn(l: [String], sep: String) -> String")); + xs = native_list_append(xs, EL_STR("list_range")); + xs = native_list_append(xs, EL_STR("fn(start: Int, end: Int) -> [Int]")); + xs = native_list_append(xs, EL_STR("el_map_get")); + xs = native_list_append(xs, EL_STR("fn(m: Map, k: String) -> Any")); + xs = native_list_append(xs, EL_STR("el_map_set")); + xs = native_list_append(xs, EL_STR("fn(m: Map, k: String, v: Any) -> Map")); + xs = native_list_append(xs, EL_STR("json_parse")); + xs = native_list_append(xs, EL_STR("fn(s: String) -> Any")); + xs = native_list_append(xs, EL_STR("json_stringify")); + xs = native_list_append(xs, EL_STR("fn(v: Any) -> String")); + xs = native_list_append(xs, EL_STR("json_get_string")); + xs = native_list_append(xs, EL_STR("fn(j: String, k: String) -> String")); + xs = native_list_append(xs, EL_STR("json_get_int")); + xs = native_list_append(xs, EL_STR("fn(j: String, k: String) -> Int")); + xs = native_list_append(xs, EL_STR("json_get_bool")); + xs = native_list_append(xs, EL_STR("fn(j: String, k: String) -> Bool")); + xs = native_list_append(xs, EL_STR("http_get")); + xs = native_list_append(xs, EL_STR("fn(url: String) -> String")); + xs = native_list_append(xs, EL_STR("http_post")); + xs = native_list_append(xs, EL_STR("fn(url: String, body: String) -> String")); + xs = native_list_append(xs, EL_STR("fs_read")); + xs = native_list_append(xs, EL_STR("fn(path: String) -> String")); + xs = native_list_append(xs, EL_STR("fs_write")); + xs = native_list_append(xs, EL_STR("fn(path: String, content: String) -> Bool")); + xs = native_list_append(xs, EL_STR("fs_list")); + xs = native_list_append(xs, EL_STR("fn(path: String) -> [String]")); + xs = native_list_append(xs, EL_STR("env")); + xs = native_list_append(xs, EL_STR("fn(key: String) -> String")); + xs = native_list_append(xs, EL_STR("uuid_new")); + xs = native_list_append(xs, EL_STR("fn() -> String")); + xs = native_list_append(xs, EL_STR("time_now")); + xs = native_list_append(xs, EL_STR("fn() -> Int")); + xs = native_list_append(xs, EL_STR("time_now_utc")); + xs = native_list_append(xs, EL_STR("fn() -> Int")); + xs = native_list_append(xs, EL_STR("sleep_ms")); + xs = native_list_append(xs, EL_STR("fn(ms: Int) -> Void")); + xs = native_list_append(xs, EL_STR("engram_node")); + xs = native_list_append(xs, EL_STR("fn(content: String, kind: String, salience: Float) -> String")); + xs = native_list_append(xs, EL_STR("engram_search")); + xs = native_list_append(xs, EL_STR("fn(q: String, limit: Int) -> [Map]")); + xs = native_list_append(xs, EL_STR("engram_activate")); + xs = native_list_append(xs, EL_STR("fn(q: String, depth: Int) -> [Map]")); + xs = native_list_append(xs, EL_STR("engram_neighbors")); + xs = native_list_append(xs, EL_STR("fn(id: String) -> [Map]")); + xs = native_list_append(xs, EL_STR("engram_connect")); + xs = native_list_append(xs, EL_STR("fn(from: String, to: String, w: Float, rel: String) -> Void")); + xs = native_list_append(xs, EL_STR("dharma_connect")); + xs = native_list_append(xs, EL_STR("fn(cgi: String) -> String")); + xs = native_list_append(xs, EL_STR("dharma_send")); + xs = native_list_append(xs, EL_STR("fn(channel: String, content: String) -> String")); + xs = native_list_append(xs, EL_STR("dharma_emit")); + xs = native_list_append(xs, EL_STR("fn(event: String, payload: String) -> Void")); + xs = native_list_append(xs, EL_STR("dharma_field")); + xs = native_list_append(xs, EL_STR("fn(event: String) -> Map")); + xs = native_list_append(xs, EL_STR("llm_call")); + xs = native_list_append(xs, EL_STR("fn(model: String, prompt: String) -> String")); + return xs; + return 0; +} + +el_val_t keyword_doc(el_val_t kw) { + if (str_eq(kw, EL_STR("let"))) { + return EL_STR("let name: Type = expr — declare an immutable binding."); + } + if (str_eq(kw, EL_STR("fn"))) { + return EL_STR("fn name(params) -> Ret { body } — define a function."); + } + if (str_eq(kw, EL_STR("type"))) { + return EL_STR("type Name { field: Type } — struct definition."); + } + if (str_eq(kw, EL_STR("enum"))) { + return EL_STR("enum Name { Variant1, Variant2(Type) } — enum definition."); + } + if (str_eq(kw, EL_STR("match"))) { + return EL_STR("match expr { Pattern => body } — pattern match."); + } + if (str_eq(kw, EL_STR("return"))) { + return EL_STR("return value — exit the enclosing function."); + } + if (str_eq(kw, EL_STR("if"))) { + return EL_STR("if cond { ... } else { ... } — conditional."); + } + if (str_eq(kw, EL_STR("for"))) { + return EL_STR("for item in collection { ... } — iterate."); + } + if (str_eq(kw, EL_STR("while"))) { + return EL_STR("while cond { ... } — loop while cond is truthy."); + } + if (str_eq(kw, EL_STR("import"))) { + return EL_STR("import \"path.el\" — pull in another module."); + } + if (str_eq(kw, EL_STR("from"))) { + return EL_STR("from module import { Name } — selective import."); + } + if (str_eq(kw, EL_STR("vessel"))) { + return EL_STR("vessel \"name\" { ... } — manifest declaration."); + } + if (str_eq(kw, EL_STR("cgi"))) { + return EL_STR("cgi \"name\" { dharma_id: ... } — CGI identity."); + } + if (str_eq(kw, EL_STR("activate"))) { + return EL_STR("activate Type where \"query\" — spreading-activation retrieval."); + } + if (str_eq(kw, EL_STR("sealed"))) { + return EL_STR("sealed { ... } — capability-restricted block."); + } + if (str_eq(kw, EL_STR("protocol"))) { + return EL_STR("protocol Name { fn method(self) -> R } — trait-like."); + } + if (str_eq(kw, EL_STR("impl"))) { + return EL_STR("impl Protocol for Type { ... } — protocol implementation."); + } + if (str_eq(kw, EL_STR("with"))) { + return EL_STR("with retry/fallback clause."); + } + if (str_eq(kw, EL_STR("retry"))) { + return EL_STR("retry times N — retry policy."); + } + if (str_eq(kw, EL_STR("fallback"))) { + return EL_STR("fallback { default } — fallback on failure."); + } + if (str_eq(kw, EL_STR("true"))) { + return EL_STR("Boolean literal."); + } + if (str_eq(kw, EL_STR("false"))) { + return EL_STR("Boolean literal."); + } + return EL_STR(""); + return 0; +} + +el_val_t first_ident_in(el_val_t rest) { + el_val_t trimmed = str_trim(rest); + el_val_t n = str_len(trimmed); + if (n == 0) { + return EL_STR(""); + } + el_val_t chars = native_string_chars(trimmed); + el_val_t i = 0; + el_val_t total = native_list_len(chars); + while (i < total) { + el_val_t ch = native_list_get(chars, i); + if (!is_ident_ch(ch)) { + return str_slice(trimmed, 0, i); + } + i = (i + 1); + } + return trimmed; + return 0; +} + +el_val_t scan_decls(el_val_t source) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t types = native_list_empty(); + el_val_t kinds = native_list_empty(); + el_val_t fns = native_list_empty(); + el_val_t tlines = native_list_empty(); + el_val_t flines = native_list_empty(); + el_val_t i = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + el_val_t lineno = (i + 1); + if (str_starts_with(trimmed, EL_STR("fn "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + fns = native_list_append(fns, name); + flines = native_list_append(flines, lineno); + } + } else { + if (str_starts_with(trimmed, EL_STR("type "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + types = native_list_append(types, name); + kinds = native_list_append(kinds, EL_STR("type")); + tlines = native_list_append(tlines, lineno); + } + } else { + if (str_starts_with(trimmed, EL_STR("enum "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + types = native_list_append(types, name); + kinds = native_list_append(kinds, EL_STR("enum")); + tlines = native_list_append(tlines, lineno); + } + } else { + if (str_starts_with(trimmed, EL_STR("protocol "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + types = native_list_append(types, name); + kinds = native_list_append(kinds, EL_STR("protocol")); + tlines = native_list_append(tlines, lineno); + } + } + } + } + } + i = (i + 1); + } + return el_map_new(5, "types", types, "type_kind", kinds, "fns", fns, "type_lines", tlines, "fn_lines", flines); + return 0; +} + +el_val_t list_contains_str(el_val_t xs, el_val_t needle) { + el_val_t n = native_list_len(xs); + el_val_t i = 0; + while (i < n) { + if (str_eq(native_list_get(xs, i), needle)) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t prefix_score(el_val_t label, el_val_t prefix) { + if (str_eq(prefix, EL_STR(""))) { + return el_from_float(0.5); + } + el_val_t lower_label = str_to_lower(label); + el_val_t lower_prefix = str_to_lower(prefix); + if (str_starts_with(lower_label, lower_prefix)) { + el_val_t lp = str_len(lower_prefix); + el_val_t ll = str_len(lower_label); + if (ll == 0) { + return el_from_float(0.0); + } + return (int_to_float(lp) / int_to_float(ll)); + } + return el_from_float(0.0); + return 0; +} + +el_val_t completion_json(el_val_t label, el_val_t kind, el_val_t detail, el_val_t doc, el_val_t score) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("label"), label)); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), kind)); + parts = native_list_append(parts, json_kv_str(EL_STR("detail"), detail)); + if (str_eq(doc, EL_STR(""))) { + parts = native_list_append(parts, json_kv_raw(EL_STR("documentation"), EL_STR("null"))); + } else { + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), doc)); + } + parts = native_list_append(parts, json_kv_float(EL_STR("score"), score)); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t lsp_complete(el_val_t source, el_val_t pos) { + el_val_t prefix = extract_prefix(source, pos); + el_val_t lower_prefix = str_to_lower(prefix); + el_val_t prefix_empty = str_eq(prefix, EL_STR("")); + el_val_t items = native_list_empty(); + el_val_t kws = keywords_list(); + el_val_t nk = native_list_len(kws); + el_val_t i = 0; + while (i < nk) { + el_val_t kw = native_list_get(kws, i); + if (prefix_empty) { + el_val_t s = prefix_score(kw, prefix); + items = native_list_append(items, completion_json(kw, EL_STR("keyword"), EL_STR("keyword"), keyword_doc(kw), s)); + } else { + if (str_starts_with(kw, prefix)) { + el_val_t s = prefix_score(kw, prefix); + items = native_list_append(items, completion_json(kw, EL_STR("keyword"), EL_STR("keyword"), keyword_doc(kw), s)); + } + } + i = (i + 1); + } + el_val_t bts = builtin_type_names(); + el_val_t nb = native_list_len(bts); + i = 0; + while (i < nb) { + el_val_t name = native_list_get(bts, i); + el_val_t lname = str_to_lower(name); + if (prefix_empty) { + el_val_t s = (el_from_float(0.5) + el_from_float(0.1)); + items = native_list_append(items, completion_json(name, EL_STR("type"), builtin_type_doc(name), el_str_concat(EL_STR("Built-in type: "), builtin_type_doc(name)), s)); + } else { + if (str_starts_with(lname, lower_prefix)) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.1)); + items = native_list_append(items, completion_json(name, EL_STR("type"), builtin_type_doc(name), el_str_concat(EL_STR("Built-in type: "), builtin_type_doc(name)), s)); + } + } + i = (i + 1); + } + el_val_t decls = scan_decls(source); + el_val_t utypes = el_get_field(decls, EL_STR("types")); + el_val_t ukinds = el_get_field(decls, EL_STR("type_kind")); + el_val_t nu = native_list_len(utypes); + i = 0; + while (i < nu) { + el_val_t name = native_list_get(utypes, i); + el_val_t kind = native_list_get(ukinds, i); + el_val_t lname = str_to_lower(name); + el_val_t include = 0; + if (prefix_empty) { + include = 1; + } + if (str_starts_with(lname, lower_prefix)) { + include = 1; + } + if (include) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.15)); + items = native_list_append(items, completion_json(name, EL_STR("type"), el_str_concat(el_str_concat(kind, EL_STR(" ")), name), el_str_concat(el_str_concat(el_str_concat(EL_STR("User-defined "), kind), EL_STR(" ")), name), s)); + } + i = (i + 1); + } + el_val_t bfns = builtin_fns(); + el_val_t nf = native_list_len(bfns); + i = 0; + while (i < nf) { + el_val_t name = native_list_get(bfns, i); + el_val_t sig = native_list_get(bfns, (i + 1)); + el_val_t lname = str_to_lower(name); + el_val_t include = 0; + if (prefix_empty) { + include = 1; + } + if (str_starts_with(lname, lower_prefix)) { + include = 1; + } + if (include) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.11)); + items = native_list_append(items, completion_json(name, EL_STR("function"), sig, el_str_concat(el_str_concat(el_str_concat(EL_STR("Built-in: "), name), EL_STR(" :: ")), sig), s)); + } + i = (i + 2); + } + el_val_t ufns = el_get_field(decls, EL_STR("fns")); + el_val_t nuf = native_list_len(ufns); + i = 0; + while (i < nuf) { + el_val_t name = native_list_get(ufns, i); + el_val_t lname = str_to_lower(name); + el_val_t include = 0; + if (prefix_empty) { + include = 1; + } + if (str_starts_with(lname, lower_prefix)) { + include = 1; + } + if (include) { + el_val_t s = el_str_concat(prefix_score(name, prefix), el_from_float(0.12)); + items = native_list_append(items, completion_json(name, EL_STR("function"), el_str_concat(EL_STR("fn "), name), el_str_concat(EL_STR("User function: "), name), s)); + } + i = (i + 1); + } + return json_array_of(items); + return 0; +} + +el_val_t lsp_hover(el_val_t source, el_val_t pos) { + el_val_t token = extract_token(source, pos); + if (str_eq(token, EL_STR(""))) { + return EL_STR("null"); + } + el_val_t doc = builtin_type_doc(token); + if (!str_eq(doc, EL_STR(""))) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), doc)); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + el_val_t decls = scan_decls(source); + el_val_t utypes = el_get_field(decls, EL_STR("types")); + el_val_t ukinds = el_get_field(decls, EL_STR("type_kind")); + el_val_t nu = native_list_len(utypes); + el_val_t i = 0; + while (i < nu) { + el_val_t name = native_list_get(utypes, i); + if (str_eq(name, token)) { + el_val_t kind = native_list_get(ukinds, i); + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), el_str_concat(el_str_concat(kind, EL_STR(" ")), token))); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + i = (i + 1); + } + el_val_t ufns = el_get_field(decls, EL_STR("fns")); + el_val_t nuf = native_list_len(ufns); + i = 0; + while (i < nuf) { + el_val_t name = native_list_get(ufns, i); + if (str_eq(name, token)) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), el_str_concat(EL_STR("fn "), token))); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + i = (i + 1); + } + el_val_t bfns = builtin_fns(); + el_val_t nf = native_list_len(bfns); + i = 0; + while (i < nf) { + el_val_t name = native_list_get(bfns, i); + if (str_eq(name, token)) { + el_val_t sig = native_list_get(bfns, (i + 1)); + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), el_str_concat(el_str_concat(el_str_concat(EL_STR("fn "), token), EL_STR(" :: ")), sig))); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + i = (i + 2); + } + el_val_t kdoc = keyword_doc(token); + if (!str_eq(kdoc, EL_STR(""))) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("type_name"), token)); + parts = native_list_append(parts, json_kv_str(EL_STR("documentation"), kdoc)); + parts = native_list_append(parts, json_kv_raw(EL_STR("engram_node_type"), EL_STR("null"))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + return EL_STR("null"); + return 0; +} + +el_val_t diag_json(el_val_t message, el_val_t severity, el_val_t line, el_val_t col) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("message"), message)); + parts = native_list_append(parts, json_kv_str(EL_STR("severity"), severity)); + if (line > 0) { + parts = native_list_append(parts, json_kv_int(EL_STR("line"), line)); + parts = native_list_append(parts, json_kv_int(EL_STR("col"), col)); + } else { + parts = native_list_append(parts, json_kv_raw(EL_STR("line"), EL_STR("null"))); + parts = native_list_append(parts, json_kv_raw(EL_STR("col"), EL_STR("null"))); + } + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t lsp_diagnostics(el_val_t source) { + el_val_t chars = native_string_chars(source); + el_val_t n = native_list_len(chars); + el_val_t diags = native_list_empty(); + el_val_t depth_brace = 0; + el_val_t depth_paren = 0; + el_val_t depth_brack = 0; + el_val_t i = 0; + el_val_t line = 1; + el_val_t col = 1; + el_val_t in_string = 0; + el_val_t string_start_line = 0; + el_val_t string_start_col = 0; + el_val_t in_comment = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (in_comment) { + if (str_eq(ch, EL_STR("\n"))) { + in_comment = 0; + line = (line + 1); + col = 1; + } else { + col = (col + 1); + } + i = (i + 1); + } else { + if (in_string) { + if (str_eq(ch, EL_STR("\\"))) { + i = (i + 1); + col = (col + 1); + if (i < n) { + el_val_t esc = native_list_get(chars, i); + if (str_eq(esc, EL_STR("\n"))) { + line = (line + 1); + col = 1; + } else { + col = (col + 1); + } + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("\""))) { + in_string = 0; + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("\n"))) { + line = (line + 1); + col = 1; + i = (i + 1); + } else { + col = (col + 1); + i = (i + 1); + } + } + } + } else { + if (str_eq(ch, EL_STR("\""))) { + in_string = 1; + string_start_line = line; + string_start_col = col; + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("/"))) { + el_val_t next_i = (i + 1); + el_val_t is_line_comment = 0; + if (next_i < n) { + el_val_t next_ch = native_list_get(chars, next_i); + if (str_eq(next_ch, EL_STR("/"))) { + is_line_comment = 1; + } + } + if (is_line_comment) { + in_comment = 1; + i = (i + 2); + col = (col + 2); + } else { + i = (i + 1); + col = (col + 1); + } + } else { + if (str_eq(ch, EL_STR("{"))) { + depth_brace = (depth_brace + 1); + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("}"))) { + depth_brace = (depth_brace - 1); + if (depth_brace < 0) { + diags = native_list_append(diags, diag_json(EL_STR("unmatched `}`"), EL_STR("error"), line, col)); + depth_brace = 0; + } + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("("))) { + depth_paren = (depth_paren + 1); + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR(")"))) { + depth_paren = (depth_paren - 1); + if (depth_paren < 0) { + diags = native_list_append(diags, diag_json(EL_STR("unmatched `)`"), EL_STR("error"), line, col)); + depth_paren = 0; + } + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("["))) { + depth_brack = (depth_brack + 1); + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("]"))) { + depth_brack = (depth_brack - 1); + if (depth_brack < 0) { + diags = native_list_append(diags, diag_json(EL_STR("unmatched `]`"), EL_STR("error"), line, col)); + depth_brack = 0; + } + i = (i + 1); + col = (col + 1); + } else { + if (str_eq(ch, EL_STR("\n"))) { + line = (line + 1); + col = 1; + i = (i + 1); + } else { + i = (i + 1); + col = (col + 1); + } + } + } + } + } + } + } + } + } + } + } + } + if (in_string) { + diags = native_list_append(diags, diag_json(EL_STR("unterminated string literal"), EL_STR("error"), string_start_line, string_start_col)); + } + if (depth_brace > 0) { + diags = native_list_append(diags, diag_json(EL_STR("unclosed `{` (missing `}`)"), EL_STR("error"), 0, 0)); + } + if (depth_paren > 0) { + diags = native_list_append(diags, diag_json(EL_STR("unclosed `(` (missing `)`)"), EL_STR("error"), 0, 0)); + } + if (depth_brack > 0) { + diags = native_list_append(diags, diag_json(EL_STR("unclosed `[` (missing `]`)"), EL_STR("error"), 0, 0)); + } + return json_array_of(diags); + return 0; +} + +el_val_t outline_item_json(el_val_t kind, el_val_t name, el_val_t line) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), kind)); + parts = native_list_append(parts, json_kv_str(EL_STR("name"), name)); + parts = native_list_append(parts, json_kv_int(EL_STR("line"), line)); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t lsp_outline(el_val_t source) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t items = native_list_empty(); + el_val_t i = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + el_val_t lineno = (i + 1); + if (str_starts_with(trimmed, EL_STR("fn "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("fn"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("type "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("type"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("enum "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("enum"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("protocol "))) { + el_val_t name = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))); + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("protocol"), name, lineno)); + } + } else { + if (str_starts_with(trimmed, EL_STR("impl "))) { + el_val_t rest = str_trim(str_slice(trimmed, 5, str_len(trimmed))); + el_val_t brace = str_index_of(rest, EL_STR("{")); + el_val_t name = rest; + if (brace > 0) { + name = str_trim(str_slice(rest, 0, brace)); + } + if (!str_eq(name, EL_STR(""))) { + items = native_list_append(items, outline_item_json(EL_STR("impl"), name, lineno)); + } + } + } + } + } + } + i = (i + 1); + } + return json_array_of(items); + return 0; +} + +el_val_t format_strip_trailing(el_val_t line) { + el_val_t n = str_len(line); + if (n == 0) { + return line; + } + el_val_t chars = native_string_chars(line); + el_val_t i = (n - 1); + el_val_t trimming = 1; + while (trimming) { + if (i < 0) { + trimming = 0; + } else { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR(" "))) { + i = (i - 1); + } else { + if (str_eq(ch, EL_STR("\t"))) { + i = (i - 1); + } else { + trimming = 0; + } + } + } + } + return str_slice(line, 0, (i + 1)); + return 0; +} + +el_val_t lsp_format(el_val_t source) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t cleaned = native_list_empty(); + el_val_t i = 0; + el_val_t blank_run = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t stripped = format_strip_trailing(line); + if (str_eq(stripped, EL_STR(""))) { + blank_run = (blank_run + 1); + if (blank_run <= 2) { + cleaned = native_list_append(cleaned, EL_STR("")); + } + } else { + blank_run = 0; + cleaned = native_list_append(cleaned, stripped); + } + i = (i + 1); + } + el_val_t joined = list_join(cleaned, EL_STR("\n")); + el_val_t final = joined; + if (!str_ends_with(final, EL_STR("\n"))) { + final = el_str_concat(final, EL_STR("\n")); + } + el_val_t changed = EL_STR("false"); + if (!str_eq(final, source)) { + changed = EL_STR("true"); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{"), json_kv_str(EL_STR("content"), final)), EL_STR(",")), json_kv_raw(EL_STR("changed"), changed)), EL_STR("}")); + return 0; +} + +el_val_t lsp_type_graph(el_val_t source) { + el_val_t nodes = native_list_empty(); + el_val_t edges = native_list_empty(); + el_val_t bts = builtin_type_names(); + el_val_t nb = native_list_len(bts); + el_val_t i = 0; + while (i < nb) { + el_val_t name = native_list_get(bts, i); + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("id"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("name"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), EL_STR("builtin"))); + parts = native_list_append(parts, json_kv_raw(EL_STR("fields"), EL_STR("[]"))); + nodes = native_list_append(nodes, el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"))); + i = (i + 1); + } + el_val_t decls = scan_decls(source); + el_val_t utypes = el_get_field(decls, EL_STR("types")); + el_val_t ukinds = el_get_field(decls, EL_STR("type_kind")); + el_val_t tlines = el_get_field(decls, EL_STR("type_lines")); + el_val_t nu = native_list_len(utypes); + i = 0; + el_val_t bts_dup = builtin_type_names(); + while (i < nu) { + el_val_t name = native_list_get(utypes, i); + el_val_t kind = native_list_get(ukinds, i); + if (!list_contains_str(bts_dup, name)) { + el_val_t fields = collect_struct_fields(source, name); + el_val_t field_jsons = native_list_empty(); + el_val_t fn2 = native_list_len(fields); + el_val_t j = 0; + while (j < fn2) { + field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j))); + j = (j + 1); + } + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_str(EL_STR("id"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("name"), name)); + parts = native_list_append(parts, json_kv_str(EL_STR("kind"), kind)); + parts = native_list_append(parts, json_kv_raw(EL_STR("fields"), json_array_of(field_jsons))); + nodes = native_list_append(nodes, el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"))); + j = 0; + while (j < fn2) { + el_val_t field_str = native_list_get(fields, j); + el_val_t target = referenced_type(field_str, utypes); + if (!str_eq(EL_NULL, EL_STR(""))) { + el_val_t eparts = native_list_empty(); + eparts = native_list_append(eparts, json_kv_str(EL_STR("from"), name)); + eparts = native_list_append(eparts, json_kv_str(EL_STR("to"), EL_NULL)); + eparts = native_list_append(eparts, json_kv_str(EL_STR("label"), EL_STR("field"))); + edges = native_list_append(edges, el_str_concat(el_str_concat(EL_STR("{"), list_join(eparts, EL_STR(","))), EL_STR("}"))); + } + j = (j + 1); + } + } + i = (i + 1); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{"), json_kv_raw(EL_STR("nodes"), json_array_of(nodes))), EL_STR(",")), json_kv_raw(EL_STR("edges"), json_array_of(edges))), EL_STR("}")); + return 0; +} + +el_val_t collect_struct_fields(el_val_t source, el_val_t type_name) { + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t fields = native_list_empty(); + el_val_t inside = 0; + el_val_t i = 0; + el_val_t header_a = el_str_concat(EL_STR("type "), type_name); + el_val_t header_b = el_str_concat(EL_STR("enum "), type_name); + el_val_t header_c = el_str_concat(EL_STR("protocol "), type_name); + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + if (!inside) { + if (str_starts_with(trimmed, header_a)) { + inside = 1; + } + if (str_starts_with(trimmed, header_b)) { + inside = 1; + } + if (str_starts_with(trimmed, header_c)) { + inside = 1; + } + } else { + if (str_starts_with(trimmed, EL_STR("}"))) { + return fields; + } + if (!str_eq(trimmed, EL_STR("{"))) { + if (!str_eq(trimmed, EL_STR(""))) { + if (!str_starts_with(trimmed, EL_STR("//"))) { + el_val_t entry = trimmed; + if (str_ends_with(entry, EL_STR(","))) { + entry = str_slice(entry, 0, (str_len(entry) - 1)); + } + fields = native_list_append(fields, str_trim(entry)); + } + } + } + } + i = (i + 1); + } + return fields; + return 0; +} + +el_val_t referenced_type(el_val_t field_str, el_val_t known_types) { + el_val_t colon = str_index_of(field_str, EL_STR(":")); + if (colon < 0) { + return EL_STR(""); + } + el_val_t after = str_trim(str_slice(field_str, (colon + 1), str_len(field_str))); + el_val_t trimmed_l = after; + if (str_starts_with(trimmed_l, EL_STR("["))) { + el_val_t end = str_index_of(trimmed_l, EL_STR("]")); + if (end > 1) { + trimmed_l = str_trim(str_slice(trimmed_l, 1, end)); + } + } + el_val_t ident = first_ident_in(trimmed_l); + if (str_eq(ident, EL_STR(""))) { + return EL_STR(""); + } + el_val_t bts = builtin_type_names(); + if (list_contains_str(bts, ident)) { + return ident; + } + if (list_contains_str(known_types, ident)) { + return ident; + } + return EL_STR(""); + return 0; +} + +el_val_t lsp_definition(el_val_t source, el_val_t pos) { + el_val_t word = extract_token(source, pos); + if (str_eq(word, EL_STR(""))) { + return EL_STR("null"); + } + el_val_t patterns = native_list_empty(); + patterns = native_list_append(patterns, el_str_concat(EL_STR("fn "), word)); + patterns = native_list_append(patterns, el_str_concat(EL_STR("type "), word)); + patterns = native_list_append(patterns, el_str_concat(EL_STR("enum "), word)); + patterns = native_list_append(patterns, el_str_concat(EL_STR("protocol "), word)); + el_val_t np = native_list_len(patterns); + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t nl = native_list_len(lines); + el_val_t i = 0; + while (i < nl) { + el_val_t line = native_list_get(lines, i); + el_val_t j = 0; + while (j < np) { + el_val_t pat = native_list_get(patterns, j); + el_val_t idx = str_index_of(line, pat); + if (idx >= 0) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_int(EL_STR("line"), (i + 1))); + parts = native_list_append(parts, json_kv_int(EL_STR("col"), (idx + 1))); + parts = native_list_append(parts, json_kv_str(EL_STR("snippet"), str_trim(line))); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + } + j = (j + 1); + } + i = (i + 1); + } + return EL_STR("null"); + return 0; +} + +el_val_t lsp_references(el_val_t source, el_val_t pos) { + el_val_t word = extract_token(source, pos); + if (str_eq(word, EL_STR(""))) { + return EL_STR("[]"); + } + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t nl = native_list_len(lines); + el_val_t refs = native_list_empty(); + el_val_t i = 0; + while (i < nl) { + el_val_t line = native_list_get(lines, i); + el_val_t line_chars = native_string_chars(line); + el_val_t llen = native_list_len(line_chars); + el_val_t wlen = str_len(word); + el_val_t off = 0; + el_val_t scanning = 1; + while (scanning) { + el_val_t idx = str_index_of(str_slice(line, off, str_len(line)), word); + if (idx < 0) { + scanning = 0; + } else { + el_val_t abs = (off + idx); + el_val_t before_ok = 1; + if (abs > 0) { + el_val_t prev = native_list_get(line_chars, (abs - 1)); + if (is_ident_ch(prev)) { + before_ok = 0; + } + } + el_val_t after_ok = 1; + el_val_t after_pos = (abs + wlen); + if (after_pos < llen) { + el_val_t nxt = native_list_get(line_chars, after_pos); + if (is_ident_ch(nxt)) { + after_ok = 0; + } + } + if (before_ok) { + if (after_ok) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, json_kv_int(EL_STR("line"), (i + 1))); + parts = native_list_append(parts, json_kv_int(EL_STR("col"), (abs + 1))); + parts = native_list_append(parts, json_kv_str(EL_STR("snippet"), str_trim(line))); + refs = native_list_append(refs, el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}"))); + } + } + off = (abs + wlen); + if (off >= str_len(line)) { + scanning = 0; + } + } + } + i = (i + 1); + } + return json_array_of(refs); + return 0; +} + +el_val_t lsp_jsonrpc(el_val_t method, el_val_t params_json) { + el_val_t source = json_get_string(params_json, EL_STR("source")); + el_val_t pos = json_get_int(params_json, EL_STR("pos")); + if (str_eq(method, EL_STR("textDocument/completion"))) { + return lsp_complete(source, pos); + } + if (str_eq(method, EL_STR("textDocument/hover"))) { + return lsp_hover(source, pos); + } + if (str_eq(method, EL_STR("textDocument/diagnostic"))) { + return lsp_diagnostics(source); + } + if (str_eq(method, EL_STR("textDocument/formatting"))) { + return lsp_format(source); + } + if (str_eq(method, EL_STR("textDocument/documentSymbol"))) { + return lsp_outline(source); + } + if (str_eq(method, EL_STR("textDocument/definition"))) { + return lsp_definition(source, pos); + } + if (str_eq(method, EL_STR("textDocument/references"))) { + return lsp_references(source, pos); + } + if (str_eq(method, EL_STR("el/typeGraph"))) { + return lsp_type_graph(source); + } + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"unknown method: "), json_escape(method)), EL_STR("\"}")); + return 0; +} + +el_val_t read_headers(void) { + el_val_t content_len = 0; + el_val_t scanning = 1; + while (scanning) { + el_val_t line = readline(); + if (str_eq(line, EL_STR(""))) { + scanning = 0; + } else { + el_val_t cl_marker = EL_STR("Content-Length:"); + if (str_starts_with(line, cl_marker)) { + el_val_t val = str_trim(str_slice(line, str_len(cl_marker), str_len(line))); + content_len = str_to_int(val); + } + } + } + return el_map_new(1, "content_length", content_len); + return 0; +} + +el_val_t lsp_stdio_loop(void) { + el_val_t running = 1; + while (running) { + el_val_t headers = read_headers(); + el_val_t body = readline(); + if (str_eq(body, EL_STR(""))) { + running = 0; + } else { + el_val_t method = json_get_string(body, EL_STR("method")); + el_val_t id_val = json_get_string(body, EL_STR("id")); + el_val_t params_raw = json_get_raw(body, EL_STR("params")); + el_val_t result = lsp_jsonrpc(method, params_raw); + el_val_t resp = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"jsonrpc\":\"2.0\",\"id\":"), id_val), EL_STR(",\"result\":")), result), EL_STR("}")); + el_val_t frame = el_str_concat(el_str_concat(el_str_concat(EL_STR("Content-Length: "), int_to_str(str_len(resp))), EL_STR("\r\n\r\n")), resp); + print(frame); + } + } + return 0; +} + +el_val_t lsp_main(void) { + el_val_t argv = args(); + el_val_t n = native_list_len(argv); + if (n > 0) { + el_val_t cmd = native_list_get(argv, 0); + if (str_eq(cmd, EL_STR("version"))) { + println(EL_STR("el-lsp 0.1.0")); + return 0; + } + if (str_eq(cmd, EL_STR("probe"))) { + el_val_t sample = EL_STR("fn main() -> Void { let x: Int = 42 }"); + println(lsp_diagnostics(sample)); + return 0; + } + } + lsp_stdio_loop(); + return 0; +} + +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + lsp_main(); + return 0; +} + diff --git a/ide/vessels/el-lsp/src/main.el b/ide/vessels/el-lsp/src/main.el new file mode 100644 index 0000000..394bfe6 --- /dev/null +++ b/ide/vessels/el-lsp/src/main.el @@ -0,0 +1,1238 @@ +// main.el — el-lsp +// +// Language Server for El. Pure-El port of the el-lsp Rust crate. +// +// Entry surface (callable from any El program that imports this file): +// +// lsp_complete(source: String, pos: Int) -> String // JSON array +// lsp_hover(source: String, pos: Int) -> String // JSON object or "null" +// lsp_diagnostics(source: String) -> String // JSON array +// lsp_outline(source: String) -> String // JSON array +// lsp_format(source: String) -> String // JSON { content, changed } +// lsp_type_graph(source: String) -> String // JSON { nodes, edges } +// lsp_definition(source: String, pos: Int) -> String // JSON object or "null" +// lsp_references(source: String, pos: Int) -> String // JSON array +// lsp_jsonrpc(method: String, params_json: String) -> String +// lsp_stdio_loop() -> Void +// +// Tokenisation reuses the lexer.el idiom (chars list + index walk) for the +// pieces we need (identifier scan, string scan, line counting). The full +// type checker referenced by the Rust crate is not yet available in El, so +// hover/completion/diagnostics fall back to source-scan heuristics over the +// declared types/enums/functions in the file. The wire shape is preserved. + +// ── Char helpers (mirrors lexer.el) ────────────────────────────────────────── + +fn is_digit_ch(ch: String) -> Bool { + if ch == "0" { return true } if ch == "1" { return true } + if ch == "2" { return true } if ch == "3" { return true } + if ch == "4" { return true } if ch == "5" { return true } + if ch == "6" { return true } if ch == "7" { return true } + if ch == "8" { return true } if ch == "9" { return true } + false +} + +fn is_alpha_ch(ch: String) -> Bool { + let lower: String = str_to_lower(ch) + if str_eq(ch, "") { return false } + if !str_eq(lower, str_to_upper(lower)) { return true } // letters differ in case + // covers a-z (lower-only) — uppercase already handled by inverse case test above + if str_contains("abcdefghijklmnopqrstuvwxyz", ch) { return true } + if str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ", ch) { return true } + false +} + +fn is_ident_ch(ch: String) -> Bool { + if is_digit_ch(ch) { return true } + if is_alpha_ch(ch) { return true } + if ch == "_" { return true } + false +} + +// ── Source navigation ──────────────────────────────────────────────────────── + +// Extract the identifier fragment immediately before `pos` (cursor). +fn extract_prefix(source: String, pos: Int) -> String { + let n: Int = str_len(source) + let end: Int = pos + if end > n { let end = n } + let start: Int = end + let chars: [String] = native_string_chars(source) + let total: Int = native_list_len(chars) + let scanning: Bool = true + while scanning { + if start <= 0 { + let scanning = false + } else { + let prev: String = native_list_get(chars, start - 1) + if is_ident_ch(prev) { + let start = start - 1 + } else { + let scanning = false + } + } + } + str_slice(source, start, end) +} + +// Extract the full token (alphanumerics + underscore) surrounding `pos`. +fn extract_token(source: String, pos: Int) -> String { + let n: Int = str_len(source) + if pos > n { return "" } + let chars: [String] = native_string_chars(source) + let total: Int = native_list_len(chars) + let start: Int = pos + let go_left: Bool = true + while go_left { + if start <= 0 { + let go_left = false + } else { + let prev: String = native_list_get(chars, start - 1) + if is_ident_ch(prev) { + let start = start - 1 + } else { + let go_left = false + } + } + } + let end: Int = pos + let go_right: Bool = true + while go_right { + if end >= total { + let go_right = false + } else { + let cur: String = native_list_get(chars, end) + if is_ident_ch(cur) { + let end = end + 1 + } else { + let go_right = false + } + } + } + str_slice(source, start, end) +} + +// Translate a byte offset into 1-based (line, col). +fn pos_to_line_col(source: String, pos: Int) -> Map { + let n: Int = str_len(source) + let cap: Int = pos + if cap > n { let cap = n } + let chars: [String] = native_string_chars(source) + let i: Int = 0 + let line: Int = 1 + let col: Int = 1 + while i < cap { + let ch: String = native_list_get(chars, i) + if ch == "\n" { + let line = line + 1 + let col = 1 + } else { + let col = col + 1 + } + let i = i + 1 + } + { "line": line, "col": col } +} + +// ── JSON encoding helpers ──────────────────────────────────────────────────── + +// Escape a string for JSON. Handles ", \, newline, tab. +fn json_escape(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "\"" { let out = out + "\\\"" } + else { if ch == "\\" { let out = out + "\\\\" } + else { if ch == "\n" { let out = out + "\\n" } + else { if ch == "\r" { let out = out + "\\r" } + else { if ch == "\t" { let out = out + "\\t" } + else { let out = out + ch } } } } } + let i = i + 1 + } + out +} + +fn json_str(s: String) -> String { + "\"" + json_escape(s) + "\"" +} + +fn json_kv_str(k: String, v: String) -> String { + json_str(k) + ":" + json_str(v) +} + +fn json_kv_int(k: String, v: Int) -> String { + json_str(k) + ":" + int_to_str(v) +} + +fn json_kv_float(k: String, v: Float) -> String { + json_str(k) + ":" + float_to_str(v) +} + +fn json_kv_raw(k: String, raw_v: String) -> String { + json_str(k) + ":" + raw_v +} + +// Join a list of pre-encoded JSON strings into a JSON array. +fn json_array_of(items: [String]) -> String { + let n: Int = native_list_len(items) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let out = out + native_list_get(items, i) + let i = i + 1 + } + out + "]" +} + +// ── Keyword/builtin tables ─────────────────────────────────────────────────── + +fn keywords_list() -> [String] { + let ks: [String] = native_list_empty() + let ks = native_list_append(ks, "let") + let ks = native_list_append(ks, "fn") + let ks = native_list_append(ks, "type") + let ks = native_list_append(ks, "enum") + let ks = native_list_append(ks, "match") + let ks = native_list_append(ks, "return") + let ks = native_list_append(ks, "activate") + let ks = native_list_append(ks, "where") + let ks = native_list_append(ks, "sealed") + let ks = native_list_append(ks, "if") + let ks = native_list_append(ks, "else") + let ks = native_list_append(ks, "for") + let ks = native_list_append(ks, "in") + let ks = native_list_append(ks, "while") + let ks = native_list_append(ks, "true") + let ks = native_list_append(ks, "false") + let ks = native_list_append(ks, "test") + let ks = native_list_append(ks, "seed") + let ks = native_list_append(ks, "assert") + let ks = native_list_append(ks, "target") + let ks = native_list_append(ks, "protocol") + let ks = native_list_append(ks, "impl") + let ks = native_list_append(ks, "import") + let ks = native_list_append(ks, "from") + let ks = native_list_append(ks, "as") + let ks = native_list_append(ks, "with") + let ks = native_list_append(ks, "retry") + let ks = native_list_append(ks, "times") + let ks = native_list_append(ks, "fallback") + let ks = native_list_append(ks, "reason") + let ks = native_list_append(ks, "parallel") + let ks = native_list_append(ks, "trace") + let ks = native_list_append(ks, "requires") + let ks = native_list_append(ks, "deploy") + let ks = native_list_append(ks, "to") + let ks = native_list_append(ks, "via") + let ks = native_list_append(ks, "vessel") + let ks = native_list_append(ks, "cgi") + ks +} + +// Built-in primitive type names. +fn builtin_type_names() -> [String] { + let ts: [String] = native_list_empty() + let ts = native_list_append(ts, "Int") + let ts = native_list_append(ts, "Float") + let ts = native_list_append(ts, "String") + let ts = native_list_append(ts, "Bool") + let ts = native_list_append(ts, "Uuid") + let ts = native_list_append(ts, "Void") + let ts = native_list_append(ts, "Any") + ts +} + +fn builtin_type_doc(name: String) -> String { + if str_eq(name, "Int") { return "64-bit signed integer" } + if str_eq(name, "Float") { return "64-bit IEEE 754 double" } + if str_eq(name, "String") { return "UTF-8 string" } + if str_eq(name, "Bool") { return "Boolean value" } + if str_eq(name, "Uuid") { return "RFC 4122 UUID" } + if str_eq(name, "Void") { return "Unit type — no value" } + if str_eq(name, "Any") { return "Dynamically-typed value" } + "" +} + +// Built-in function names → signatures. Encoded as flat [name, sig, name, sig, ...] +// to keep type maps simple. +fn builtin_fns() -> [String] { + let xs: [String] = native_list_empty() + let xs = native_list_append(xs, "println") let xs = native_list_append(xs, "fn(value: String) -> Void") + let xs = native_list_append(xs, "print") let xs = native_list_append(xs, "fn(value: String) -> Void") + let xs = native_list_append(xs, "readline") let xs = native_list_append(xs, "fn() -> String") + let xs = native_list_append(xs, "args") let xs = native_list_append(xs, "fn() -> [String]") + let xs = native_list_append(xs, "int_to_str") let xs = native_list_append(xs, "fn(n: Int) -> String") + let xs = native_list_append(xs, "str_to_int") let xs = native_list_append(xs, "fn(s: String) -> Int") + let xs = native_list_append(xs, "str_len") let xs = native_list_append(xs, "fn(s: String) -> Int") + let xs = native_list_append(xs, "str_slice") let xs = native_list_append(xs, "fn(s: String, start: Int, end: Int) -> String") + let xs = native_list_append(xs, "str_concat") let xs = native_list_append(xs, "fn(a: String, b: String) -> String") + let xs = native_list_append(xs, "str_contains") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Bool") + let xs = native_list_append(xs, "str_starts_with") let xs = native_list_append(xs, "fn(s: String, p: String) -> Bool") + let xs = native_list_append(xs, "str_ends_with") let xs = native_list_append(xs, "fn(s: String, suf: String) -> Bool") + let xs = native_list_append(xs, "str_to_upper") let xs = native_list_append(xs, "fn(s: String) -> String") + let xs = native_list_append(xs, "str_to_lower") let xs = native_list_append(xs, "fn(s: String) -> String") + let xs = native_list_append(xs, "str_split") let xs = native_list_append(xs, "fn(s: String, sep: String) -> [String]") + let xs = native_list_append(xs, "str_trim") let xs = native_list_append(xs, "fn(s: String) -> String") + let xs = native_list_append(xs, "str_replace") let xs = native_list_append(xs, "fn(s: String, from: String, to: String) -> String") + let xs = native_list_append(xs, "str_index_of") let xs = native_list_append(xs, "fn(s: String, sub: String) -> Int") + let xs = native_list_append(xs, "float_to_str") let xs = native_list_append(xs, "fn(f: Float) -> String") + let xs = native_list_append(xs, "str_to_float") let xs = native_list_append(xs, "fn(s: String) -> Float") + let xs = native_list_append(xs, "math_sqrt") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_log") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_sin") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_cos") let xs = native_list_append(xs, "fn(n: Float) -> Float") + let xs = native_list_append(xs, "math_pi") let xs = native_list_append(xs, "fn() -> Float") + let xs = native_list_append(xs, "el_abs") let xs = native_list_append(xs, "fn(n: Int) -> Int") + let xs = native_list_append(xs, "el_min") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int") + let xs = native_list_append(xs, "el_max") let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int") + let xs = native_list_append(xs, "el_list_empty") let xs = native_list_append(xs, "fn() -> [Any]") + let xs = native_list_append(xs, "el_list_len") let xs = native_list_append(xs, "fn(l: [Any]) -> Int") + let xs = native_list_append(xs, "el_list_get") let xs = native_list_append(xs, "fn(l: [Any], i: Int) -> Any") + let xs = native_list_append(xs, "el_list_append") let xs = native_list_append(xs, "fn(l: [Any], v: Any) -> [Any]") + let xs = native_list_append(xs, "list_join") let xs = native_list_append(xs, "fn(l: [String], sep: String) -> String") + let xs = native_list_append(xs, "list_range") let xs = native_list_append(xs, "fn(start: Int, end: Int) -> [Int]") + let xs = native_list_append(xs, "el_map_get") let xs = native_list_append(xs, "fn(m: Map, k: String) -> Any") + let xs = native_list_append(xs, "el_map_set") let xs = native_list_append(xs, "fn(m: Map, k: String, v: Any) -> Map") + let xs = native_list_append(xs, "json_parse") let xs = native_list_append(xs, "fn(s: String) -> Any") + let xs = native_list_append(xs, "json_stringify") let xs = native_list_append(xs, "fn(v: Any) -> String") + let xs = native_list_append(xs, "json_get_string") let xs = native_list_append(xs, "fn(j: String, k: String) -> String") + let xs = native_list_append(xs, "json_get_int") let xs = native_list_append(xs, "fn(j: String, k: String) -> Int") + let xs = native_list_append(xs, "json_get_bool") let xs = native_list_append(xs, "fn(j: String, k: String) -> Bool") + let xs = native_list_append(xs, "http_get") let xs = native_list_append(xs, "fn(url: String) -> String") + let xs = native_list_append(xs, "http_post") let xs = native_list_append(xs, "fn(url: String, body: String) -> String") + let xs = native_list_append(xs, "fs_read") let xs = native_list_append(xs, "fn(path: String) -> String") + let xs = native_list_append(xs, "fs_write") let xs = native_list_append(xs, "fn(path: String, content: String) -> Bool") + let xs = native_list_append(xs, "fs_list") let xs = native_list_append(xs, "fn(path: String) -> [String]") + let xs = native_list_append(xs, "env") let xs = native_list_append(xs, "fn(key: String) -> String") + let xs = native_list_append(xs, "uuid_new") let xs = native_list_append(xs, "fn() -> String") + let xs = native_list_append(xs, "time_now") let xs = native_list_append(xs, "fn() -> Int") + let xs = native_list_append(xs, "time_now_utc") let xs = native_list_append(xs, "fn() -> Int") + let xs = native_list_append(xs, "sleep_ms") let xs = native_list_append(xs, "fn(ms: Int) -> Void") + let xs = native_list_append(xs, "engram_node") let xs = native_list_append(xs, "fn(content: String, kind: String, salience: Float) -> String") + let xs = native_list_append(xs, "engram_search") let xs = native_list_append(xs, "fn(q: String, limit: Int) -> [Map]") + let xs = native_list_append(xs, "engram_activate") let xs = native_list_append(xs, "fn(q: String, depth: Int) -> [Map]") + let xs = native_list_append(xs, "engram_neighbors") let xs = native_list_append(xs, "fn(id: String) -> [Map]") + let xs = native_list_append(xs, "engram_connect") let xs = native_list_append(xs, "fn(from: String, to: String, w: Float, rel: String) -> Void") + let xs = native_list_append(xs, "dharma_connect") let xs = native_list_append(xs, "fn(cgi: String) -> String") + let xs = native_list_append(xs, "dharma_send") let xs = native_list_append(xs, "fn(channel: String, content: String) -> String") + let xs = native_list_append(xs, "dharma_emit") let xs = native_list_append(xs, "fn(event: String, payload: String) -> Void") + let xs = native_list_append(xs, "dharma_field") let xs = native_list_append(xs, "fn(event: String) -> Map") + let xs = native_list_append(xs, "llm_call") let xs = native_list_append(xs, "fn(model: String, prompt: String) -> String") + xs +} + +fn keyword_doc(kw: String) -> String { + if str_eq(kw, "let") { return "let name: Type = expr — declare an immutable binding." } + if str_eq(kw, "fn") { return "fn name(params) -> Ret { body } — define a function." } + if str_eq(kw, "type") { return "type Name { field: Type } — struct definition." } + if str_eq(kw, "enum") { return "enum Name { Variant1, Variant2(Type) } — enum definition." } + if str_eq(kw, "match") { return "match expr { Pattern => body } — pattern match." } + if str_eq(kw, "return") { return "return value — exit the enclosing function." } + if str_eq(kw, "if") { return "if cond { ... } else { ... } — conditional." } + if str_eq(kw, "for") { return "for item in collection { ... } — iterate." } + if str_eq(kw, "while") { return "while cond { ... } — loop while cond is truthy." } + if str_eq(kw, "import") { return "import \"path.el\" — pull in another module." } + if str_eq(kw, "from") { return "from module import { Name } — selective import." } + if str_eq(kw, "vessel") { return "vessel \"name\" { ... } — manifest declaration." } + if str_eq(kw, "cgi") { return "cgi \"name\" { dharma_id: ... } — CGI identity." } + if str_eq(kw, "activate") { return "activate Type where \"query\" — spreading-activation retrieval." } + if str_eq(kw, "sealed") { return "sealed { ... } — capability-restricted block." } + if str_eq(kw, "protocol") { return "protocol Name { fn method(self) -> R } — trait-like." } + if str_eq(kw, "impl") { return "impl Protocol for Type { ... } — protocol implementation." } + if str_eq(kw, "with") { return "with retry/fallback clause." } + if str_eq(kw, "retry") { return "retry times N — retry policy." } + if str_eq(kw, "fallback") { return "fallback { default } — fallback on failure." } + if str_eq(kw, "true") { return "Boolean literal." } + if str_eq(kw, "false") { return "Boolean literal." } + "" +} + +// ── Source-driven type/fn discovery (no full parser) ───────────────────────── +// +// The Rust crate calls into el-types::TypeChecker for a real env. That +// module isn't available in El yet, so we approximate by line-scan: find +// every `type X {`, `enum X {`, `protocol X {`, and `fn X(` declaration. +// Returns four parallel lists merged into one map for easy access. + +fn first_ident_in(rest: String) -> String { + let trimmed: String = str_trim(rest) + let n: Int = str_len(trimmed) + if n == 0 { return "" } + let chars: [String] = native_string_chars(trimmed) + let i: Int = 0 + let total: Int = native_list_len(chars) + while i < total { + let ch: String = native_list_get(chars, i) + if !is_ident_ch(ch) { + return str_slice(trimmed, 0, i) + } + let i = i + 1 + } + trimmed +} + +// scan_decls — produce a Map with parallel lists: +// "types" : [String] user-defined struct/enum/protocol names +// "type_kind" : [String] "type" | "enum" | "protocol" matching above +// "fns" : [String] user-defined function names +// "type_lines" : [Int] 1-based line numbers +// "fn_lines" : [Int] +fn scan_decls(source: String) -> Map { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let types: [String] = native_list_empty() + let kinds: [String] = native_list_empty() + let fns: [String] = native_list_empty() + let tlines: [Int] = native_list_empty() + let flines: [Int] = native_list_empty() + let i: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + let lineno: Int = i + 1 + if str_starts_with(trimmed, "fn ") { + let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))) + if !str_eq(name, "") { + let fns = native_list_append(fns, name) + let flines = native_list_append(flines, lineno) + } + } else { if str_starts_with(trimmed, "type ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let types = native_list_append(types, name) + let kinds = native_list_append(kinds, "type") + let tlines = native_list_append(tlines, lineno) + } + } else { if str_starts_with(trimmed, "enum ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let types = native_list_append(types, name) + let kinds = native_list_append(kinds, "enum") + let tlines = native_list_append(tlines, lineno) + } + } else { if str_starts_with(trimmed, "protocol ") { + let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))) + if !str_eq(name, "") { + let types = native_list_append(types, name) + let kinds = native_list_append(kinds, "protocol") + let tlines = native_list_append(tlines, lineno) + } + } } } } + let i = i + 1 + } + { + "types": types, + "type_kind": kinds, + "fns": fns, + "type_lines": tlines, + "fn_lines": flines + } +} + +// list_contains — case-sensitive membership test on [String]. +fn list_contains_str(xs: [String], needle: String) -> Bool { + let n: Int = native_list_len(xs) + let i: Int = 0 + while i < n { + if str_eq(native_list_get(xs, i), needle) { return true } + let i = i + 1 + } + false +} + +// Score a label against a prefix: 0 if no prefix match, else (prefix_len/label_len). +fn prefix_score(label: String, prefix: String) -> Float { + if str_eq(prefix, "") { return 0.5 } + let lower_label: String = str_to_lower(label) + let lower_prefix: String = str_to_lower(prefix) + if str_starts_with(lower_label, lower_prefix) { + let lp: Int = str_len(lower_prefix) + let ll: Int = str_len(lower_label) + if ll == 0 { return 0.0 } + return int_to_float(lp) / int_to_float(ll) + } + 0.0 +} + +// Render a single completion item to JSON. +fn completion_json(label: String, kind: String, detail: String, doc: String, score: Float) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("label", label)) + let parts = native_list_append(parts, json_kv_str("kind", kind)) + let parts = native_list_append(parts, json_kv_str("detail", detail)) + if str_eq(doc, "") { + let parts = native_list_append(parts, json_kv_raw("documentation", "null")) + } else { + let parts = native_list_append(parts, json_kv_str("documentation", doc)) + } + let parts = native_list_append(parts, json_kv_float("score", score)) + "{" + list_join(parts, ",") + "}" +} + +// ── Public: completion ─────────────────────────────────────────────────────── + +fn lsp_complete(source: String, pos: Int) -> String { + let prefix: String = extract_prefix(source, pos) + let lower_prefix: String = str_to_lower(prefix) + let prefix_empty: Bool = str_eq(prefix, "") + let items: [String] = native_list_empty() + + // Keywords + let kws: [String] = keywords_list() + let nk: Int = native_list_len(kws) + let i: Int = 0 + while i < nk { + let kw: String = native_list_get(kws, i) + if prefix_empty { + let s: Float = prefix_score(kw, prefix) + let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s)) + } else { if str_starts_with(kw, prefix) { + let s: Float = prefix_score(kw, prefix) + let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s)) + } } + let i = i + 1 + } + + // Built-in types + let bts: [String] = builtin_type_names() + let nb: Int = native_list_len(bts) + let i: Int = 0 + while i < nb { + let name: String = native_list_get(bts, i) + let lname: String = str_to_lower(name) + if prefix_empty { + let s: Float = 0.5 + 0.1 + let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s)) + } else { if str_starts_with(lname, lower_prefix) { + let s: Float = prefix_score(name, prefix) + 0.1 + let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s)) + } } + let i = i + 1 + } + + // User-defined types + let decls: Map = scan_decls(source) + let utypes: [String] = decls["types"] + let ukinds: [String] = decls["type_kind"] + let nu: Int = native_list_len(utypes) + let i: Int = 0 + while i < nu { + let name: String = native_list_get(utypes, i) + let kind: String = native_list_get(ukinds, i) + let lname: String = str_to_lower(name) + let include: Bool = false + if prefix_empty { let include = true } + if str_starts_with(lname, lower_prefix) { let include = true } + if include { + let s: Float = prefix_score(name, prefix) + 0.15 + let items = native_list_append(items, completion_json(name, "type", kind + " " + name, "User-defined " + kind + " " + name, s)) + } + let i = i + 1 + } + + // Built-in functions + let bfns: [String] = builtin_fns() + let nf: Int = native_list_len(bfns) + let i: Int = 0 + while i < nf { + let name: String = native_list_get(bfns, i) + let sig: String = native_list_get(bfns, i + 1) + let lname: String = str_to_lower(name) + let include: Bool = false + if prefix_empty { let include = true } + if str_starts_with(lname, lower_prefix) { let include = true } + if include { + let s: Float = prefix_score(name, prefix) + 0.11 + let items = native_list_append(items, completion_json(name, "function", sig, "Built-in: " + name + " :: " + sig, s)) + } + let i = i + 2 + } + + // User-defined functions + let ufns: [String] = decls["fns"] + let nuf: Int = native_list_len(ufns) + let i: Int = 0 + while i < nuf { + let name: String = native_list_get(ufns, i) + let lname: String = str_to_lower(name) + let include: Bool = false + if prefix_empty { let include = true } + if str_starts_with(lname, lower_prefix) { let include = true } + if include { + let s: Float = prefix_score(name, prefix) + 0.12 + let items = native_list_append(items, completion_json(name, "function", "fn " + name, "User function: " + name, s)) + } + let i = i + 1 + } + + // Output is a list of pre-encoded JSON objects. Sorting by score is a + // stretch goal — el's runtime has no sort-by-key primitive. Items are + // already ordered by category (keywords first), which matches the most + // common editor expectation. + json_array_of(items) +} + +// ── Public: hover ──────────────────────────────────────────────────────────── + +fn lsp_hover(source: String, pos: Int) -> String { + let token: String = extract_token(source, pos) + if str_eq(token, "") { return "null" } + + // Built-in primitives + let doc: String = builtin_type_doc(token) + if !str_eq(doc, "") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", doc)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + + // User-declared types + let decls: Map = scan_decls(source) + let utypes: [String] = decls["types"] + let ukinds: [String] = decls["type_kind"] + let nu: Int = native_list_len(utypes) + let i: Int = 0 + while i < nu { + let name: String = native_list_get(utypes, i) + if str_eq(name, token) { + let kind: String = native_list_get(ukinds, i) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", kind + " " + token)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + let i = i + 1 + } + + // User-declared functions + let ufns: [String] = decls["fns"] + let nuf: Int = native_list_len(ufns) + let i: Int = 0 + while i < nuf { + let name: String = native_list_get(ufns, i) + if str_eq(name, token) { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + let i = i + 1 + } + + // Built-in functions + let bfns: [String] = builtin_fns() + let nf: Int = native_list_len(bfns) + let i: Int = 0 + while i < nf { + let name: String = native_list_get(bfns, i) + if str_eq(name, token) { + let sig: String = native_list_get(bfns, i + 1) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token + " :: " + sig)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + let i = i + 2 + } + + // Keyword + let kdoc: String = keyword_doc(token) + if !str_eq(kdoc, "") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("type_name", token)) + let parts = native_list_append(parts, json_kv_str("documentation", kdoc)) + let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null")) + return "{" + list_join(parts, ",") + "}" + } + + "null" +} + +// ── Public: diagnostics ────────────────────────────────────────────────────── +// +// Without the type checker port we surface only structural diagnostics: +// * unterminated string literal +// * unbalanced braces / parens / brackets +// * unknown type referenced after `:` in a let binding (unknown to source + +// builtins) — best-effort, conservative. + +fn diag_json(message: String, severity: String, line: Int, col: Int) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("message", message)) + let parts = native_list_append(parts, json_kv_str("severity", severity)) + if line > 0 { + let parts = native_list_append(parts, json_kv_int("line", line)) + let parts = native_list_append(parts, json_kv_int("col", col)) + } else { + let parts = native_list_append(parts, json_kv_raw("line", "null")) + let parts = native_list_append(parts, json_kv_raw("col", "null")) + } + "{" + list_join(parts, ",") + "}" +} + +fn lsp_diagnostics(source: String) -> String { + let chars: [String] = native_string_chars(source) + let n: Int = native_list_len(chars) + let diags: [String] = native_list_empty() + + // Bracket balance + unterminated string scan + let depth_brace: Int = 0 + let depth_paren: Int = 0 + let depth_brack: Int = 0 + let i: Int = 0 + let line: Int = 1 + let col: Int = 1 + let in_string: Bool = false + let string_start_line: Int = 0 + let string_start_col: Int = 0 + let in_comment: Bool = false + while i < n { + let ch: String = native_list_get(chars, i) + if in_comment { + if ch == "\n" { + let in_comment = false + let line = line + 1 + let col = 1 + } else { + let col = col + 1 + } + let i = i + 1 + } else { + if in_string { + if ch == "\\" { + let i = i + 1 + let col = col + 1 + if i < n { + let esc: String = native_list_get(chars, i) + if esc == "\n" { let line = line + 1 let col = 1 } else { let col = col + 1 } + let i = i + 1 + } + } else { if ch == "\"" { + let in_string = false + let i = i + 1 + let col = col + 1 + } else { if ch == "\n" { + let line = line + 1 + let col = 1 + let i = i + 1 + } else { + let col = col + 1 + let i = i + 1 + } } } + } else { + if ch == "\"" { + let in_string = true + let string_start_line = line + let string_start_col = col + let i = i + 1 + let col = col + 1 + } else { if ch == "/" { + let next_i: Int = i + 1 + let is_line_comment: Bool = false + if next_i < n { + let next_ch: String = native_list_get(chars, next_i) + if next_ch == "/" { let is_line_comment = true } + } + if is_line_comment { + let in_comment = true + let i = i + 2 + let col = col + 2 + } else { + let i = i + 1 + let col = col + 1 + } + } else { if ch == "{" { + let depth_brace = depth_brace + 1 + let i = i + 1 let col = col + 1 + } else { if ch == "}" { + let depth_brace = depth_brace - 1 + if depth_brace < 0 { + let diags = native_list_append(diags, diag_json("unmatched `}`", "error", line, col)) + let depth_brace = 0 + } + let i = i + 1 let col = col + 1 + } else { if ch == "(" { + let depth_paren = depth_paren + 1 + let i = i + 1 let col = col + 1 + } else { if ch == ")" { + let depth_paren = depth_paren - 1 + if depth_paren < 0 { + let diags = native_list_append(diags, diag_json("unmatched `)`", "error", line, col)) + let depth_paren = 0 + } + let i = i + 1 let col = col + 1 + } else { if ch == "[" { + let depth_brack = depth_brack + 1 + let i = i + 1 let col = col + 1 + } else { if ch == "]" { + let depth_brack = depth_brack - 1 + if depth_brack < 0 { + let diags = native_list_append(diags, diag_json("unmatched `]`", "error", line, col)) + let depth_brack = 0 + } + let i = i + 1 let col = col + 1 + } else { if ch == "\n" { + let line = line + 1 + let col = 1 + let i = i + 1 + } else { + let i = i + 1 + let col = col + 1 + } } } } } } } } } + } + } + } + + if in_string { + let diags = native_list_append(diags, diag_json("unterminated string literal", "error", string_start_line, string_start_col)) + } + if depth_brace > 0 { let diags = native_list_append(diags, diag_json("unclosed `{` (missing `}`)", "error", 0, 0)) } + if depth_paren > 0 { let diags = native_list_append(diags, diag_json("unclosed `(` (missing `)`)", "error", 0, 0)) } + if depth_brack > 0 { let diags = native_list_append(diags, diag_json("unclosed `[` (missing `]`)", "error", 0, 0)) } + + json_array_of(diags) +} + +// ── Public: outline ────────────────────────────────────────────────────────── + +fn outline_item_json(kind: String, name: String, line: Int) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("kind", kind)) + let parts = native_list_append(parts, json_kv_str("name", name)) + let parts = native_list_append(parts, json_kv_int("line", line)) + "{" + list_join(parts, ",") + "}" +} + +fn lsp_outline(source: String) -> String { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let items: [String] = native_list_empty() + let i: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + let lineno: Int = i + 1 + if str_starts_with(trimmed, "fn ") { + let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("fn", name, lineno)) + } + } else { if str_starts_with(trimmed, "type ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("type", name, lineno)) + } + } else { if str_starts_with(trimmed, "enum ") { + let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("enum", name, lineno)) + } + } else { if str_starts_with(trimmed, "protocol ") { + let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed))) + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("protocol", name, lineno)) + } + } else { if str_starts_with(trimmed, "impl ") { + let rest: String = str_trim(str_slice(trimmed, 5, str_len(trimmed))) + let brace: Int = str_index_of(rest, "{") + let name: String = rest + if brace > 0 { let name = str_trim(str_slice(rest, 0, brace)) } + if !str_eq(name, "") { + let items = native_list_append(items, outline_item_json("impl", name, lineno)) + } + } } } } } + let i = i + 1 + } + json_array_of(items) +} + +// ── Public: format ─────────────────────────────────────────────────────────── +// +// Idiomatic-El formatter. Until `el fmt` is a real thing, we apply a minimal +// canonicalisation pass: +// * trim trailing whitespace on every line +// * collapse 3+ blank lines to 2 +// * ensure file ends with exactly one newline +// This is safe for round-tripping and a noticeable quality-of-life win in +// the editor. + +fn format_strip_trailing(line: String) -> String { + // str_trim trims both ends; we want only trailing whitespace stripped. + let n: Int = str_len(line) + if n == 0 { return line } + let chars: [String] = native_string_chars(line) + let i: Int = n - 1 + let trimming: Bool = true + while trimming { + if i < 0 { + let trimming = false + } else { + let ch: String = native_list_get(chars, i) + if ch == " " { let i = i - 1 } else { if ch == "\t" { let i = i - 1 } else { let trimming = false } } + } + } + str_slice(line, 0, i + 1) +} + +fn lsp_format(source: String) -> String { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let cleaned: [String] = native_list_empty() + let i: Int = 0 + let blank_run: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let stripped: String = format_strip_trailing(line) + if str_eq(stripped, "") { + let blank_run = blank_run + 1 + if blank_run <= 2 { + let cleaned = native_list_append(cleaned, "") + } + } else { + let blank_run = 0 + let cleaned = native_list_append(cleaned, stripped) + } + let i = i + 1 + } + let joined: String = list_join(cleaned, "\n") + // Ensure trailing newline. + let final: String = joined + if !str_ends_with(final, "\n") { + let final = final + "\n" + } + let changed: String = "false" + if !str_eq(final, source) { let changed = "true" } + "{" + json_kv_str("content", final) + "," + json_kv_raw("changed", changed) + "}" +} + +// ── Public: type graph ─────────────────────────────────────────────────────── + +fn lsp_type_graph(source: String) -> String { + let nodes: [String] = native_list_empty() + let edges: [String] = native_list_empty() + + // Built-in nodes + let bts: [String] = builtin_type_names() + let nb: Int = native_list_len(bts) + let i: Int = 0 + while i < nb { + let name: String = native_list_get(bts, i) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("id", name)) + let parts = native_list_append(parts, json_kv_str("name", name)) + let parts = native_list_append(parts, json_kv_str("kind", "builtin")) + let parts = native_list_append(parts, json_kv_raw("fields", "[]")) + let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}") + let i = i + 1 + } + + // User-declared types + let decls: Map = scan_decls(source) + let utypes: [String] = decls["types"] + let ukinds: [String] = decls["type_kind"] + let tlines: [Int] = decls["type_lines"] + let nu: Int = native_list_len(utypes) + let i: Int = 0 + let bts_dup: [String] = builtin_type_names() + while i < nu { + let name: String = native_list_get(utypes, i) + let kind: String = native_list_get(ukinds, i) + // Skip if already a builtin (paranoia) + if !list_contains_str(bts_dup, name) { + let fields: [String] = collect_struct_fields(source, name) + let field_jsons: [String] = native_list_empty() + let fn2: Int = native_list_len(fields) + let j: Int = 0 + while j < fn2 { + let field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j))) + let j = j + 1 + } + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_str("id", name)) + let parts = native_list_append(parts, json_kv_str("name", name)) + let parts = native_list_append(parts, json_kv_str("kind", kind)) + let parts = native_list_append(parts, json_kv_raw("fields", json_array_of(field_jsons))) + let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}") + + // Edges: scan each field for a known type reference. + let j: Int = 0 + while j < fn2 { + let field_str: String = native_list_get(fields, j) + let target: String = referenced_type(field_str, utypes) + if !str_eq(target, "") { + let eparts: [String] = native_list_empty() + let eparts = native_list_append(eparts, json_kv_str("from", name)) + let eparts = native_list_append(eparts, json_kv_str("to", target)) + let eparts = native_list_append(eparts, json_kv_str("label", "field")) + let edges = native_list_append(edges, "{" + list_join(eparts, ",") + "}") + } + let j = j + 1 + } + } + let i = i + 1 + } + + "{" + json_kv_raw("nodes", json_array_of(nodes)) + "," + json_kv_raw("edges", json_array_of(edges)) + "}" +} + +// Extract the field list of a `type Name { ... }` block as ["field: Type", ...] +// or for an enum, the variant labels. Best-effort line-by-line scan. +fn collect_struct_fields(source: String, type_name: String) -> [String] { + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + let fields: [String] = native_list_empty() + let inside: Bool = false + let i: Int = 0 + let header_a: String = "type " + type_name + let header_b: String = "enum " + type_name + let header_c: String = "protocol " + type_name + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + if !inside { + if str_starts_with(trimmed, header_a) { let inside = true } + if str_starts_with(trimmed, header_b) { let inside = true } + if str_starts_with(trimmed, header_c) { let inside = true } + } else { + if str_starts_with(trimmed, "}") { + return fields + } + // skip the opening "{" line + if !str_eq(trimmed, "{") { + if !str_eq(trimmed, "") { + if !str_starts_with(trimmed, "//") { + // strip a trailing comma if present + let entry: String = trimmed + if str_ends_with(entry, ",") { + let entry = str_slice(entry, 0, str_len(entry) - 1) + } + let fields = native_list_append(fields, str_trim(entry)) + } + } + } + } + let i = i + 1 + } + fields +} + +// Pick the first known type name out of a `field: Type` string. Stops at non-ident. +fn referenced_type(field_str: String, known_types: [String]) -> String { + // Find ":" — type follows. + let colon: Int = str_index_of(field_str, ":") + if colon < 0 { return "" } + let after: String = str_trim(str_slice(field_str, colon + 1, str_len(field_str))) + // Strip array brackets and optional marker. + let trimmed_l: String = after + if str_starts_with(trimmed_l, "[") { + let end: Int = str_index_of(trimmed_l, "]") + if end > 1 { + let trimmed_l = str_trim(str_slice(trimmed_l, 1, end)) + } + } + // First identifier. + let ident: String = first_ident_in(trimmed_l) + if str_eq(ident, "") { return "" } + let bts: [String] = builtin_type_names() + if list_contains_str(bts, ident) { return ident } + if list_contains_str(known_types, ident) { return ident } + "" +} + +// ── Public: definition / references ────────────────────────────────────────── + +fn lsp_definition(source: String, pos: Int) -> String { + let word: String = extract_token(source, pos) + if str_eq(word, "") { return "null" } + let patterns: [String] = native_list_empty() + let patterns = native_list_append(patterns, "fn " + word) + let patterns = native_list_append(patterns, "type " + word) + let patterns = native_list_append(patterns, "enum " + word) + let patterns = native_list_append(patterns, "protocol " + word) + let np: Int = native_list_len(patterns) + let lines: [String] = str_split(source, "\n") + let nl: Int = native_list_len(lines) + let i: Int = 0 + while i < nl { + let line: String = native_list_get(lines, i) + let j: Int = 0 + while j < np { + let pat: String = native_list_get(patterns, j) + let idx: Int = str_index_of(line, pat) + if idx >= 0 { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_int("line", i + 1)) + let parts = native_list_append(parts, json_kv_int("col", idx + 1)) + let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line))) + return "{" + list_join(parts, ",") + "}" + } + let j = j + 1 + } + let i = i + 1 + } + "null" +} + +fn lsp_references(source: String, pos: Int) -> String { + let word: String = extract_token(source, pos) + if str_eq(word, "") { return "[]" } + let lines: [String] = str_split(source, "\n") + let nl: Int = native_list_len(lines) + let refs: [String] = native_list_empty() + let i: Int = 0 + while i < nl { + let line: String = native_list_get(lines, i) + let line_chars: [String] = native_string_chars(line) + let llen: Int = native_list_len(line_chars) + let wlen: Int = str_len(word) + let off: Int = 0 + let scanning: Bool = true + while scanning { + let idx: Int = str_index_of(str_slice(line, off, str_len(line)), word) + if idx < 0 { + let scanning = false + } else { + let abs: Int = off + idx + // Word-boundary check + let before_ok: Bool = true + if abs > 0 { + let prev: String = native_list_get(line_chars, abs - 1) + if is_ident_ch(prev) { let before_ok = false } + } + let after_ok: Bool = true + let after_pos: Int = abs + wlen + if after_pos < llen { + let nxt: String = native_list_get(line_chars, after_pos) + if is_ident_ch(nxt) { let after_ok = false } + } + if before_ok { + if after_ok { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, json_kv_int("line", i + 1)) + let parts = native_list_append(parts, json_kv_int("col", abs + 1)) + let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line))) + let refs = native_list_append(refs, "{" + list_join(parts, ",") + "}") + } + } + let off = abs + wlen + if off >= str_len(line) { let scanning = false } + } + } + let i = i + 1 + } + json_array_of(refs) +} + +// ── JSON-RPC dispatch ──────────────────────────────────────────────────────── +// +// Method names follow LSP-ish conventions but stay aligned with the Rust crate's +// HTTP surface. Params are a JSON string the dispatcher pulls fields out of via +// json_get_string / json_get_int. +// +// "textDocument/completion" → params { source, pos } +// "textDocument/hover" → params { source, pos } +// "textDocument/diagnostic" → params { source } +// "textDocument/formatting" → params { source } +// "textDocument/documentSymbol"→ params { source } +// "textDocument/definition" → params { source, pos } +// "textDocument/references" → params { source, pos } +// "el/typeGraph" → params { source } + +fn lsp_jsonrpc(method: String, params_json: String) -> String { + let source: String = json_get_string(params_json, "source") + let pos: Int = json_get_int(params_json, "pos") + if str_eq(method, "textDocument/completion") { return lsp_complete(source, pos) } + if str_eq(method, "textDocument/hover") { return lsp_hover(source, pos) } + if str_eq(method, "textDocument/diagnostic") { return lsp_diagnostics(source) } + if str_eq(method, "textDocument/formatting") { return lsp_format(source) } + if str_eq(method, "textDocument/documentSymbol") { return lsp_outline(source) } + if str_eq(method, "textDocument/definition") { return lsp_definition(source, pos) } + if str_eq(method, "textDocument/references") { return lsp_references(source, pos) } + if str_eq(method, "el/typeGraph") { return lsp_type_graph(source) } + "{\"error\":\"unknown method: " + json_escape(method) + "\"}" +} + +// ── stdio loop (best-effort; see runtime gap note in the report) ───────────── +// +// LSP framing is `Content-Length: N\r\n\r\n` then exactly N bytes. The El +// runtime exposes only line-buffered `readline()`, which strips `\n` and is +// hostile to the binary body read. We approximate by reading lines until a +// blank line ends the headers, then reading exactly one more line as the +// body (works for compact, no-newline payloads — i.e. minified JSON-RPC). +// A dedicated `read_n(bytes)` runtime primitive would unblock the strict +// implementation. See report. + +fn read_headers() -> Map { + let content_len: Int = 0 + let scanning: Bool = true + while scanning { + let line: String = readline() + if str_eq(line, "") { + let scanning = false + } else { + let cl_marker: String = "Content-Length:" + if str_starts_with(line, cl_marker) { + let val: String = str_trim(str_slice(line, str_len(cl_marker), str_len(line))) + let content_len = str_to_int(val) + } + } + } + { "content_length": content_len } +} + +fn lsp_stdio_loop() -> Void { + let running: Bool = true + while running { + let headers: Map = read_headers() + let body: String = readline() + if str_eq(body, "") { + let running = false + } else { + let method: String = json_get_string(body, "method") + let id_val: String = json_get_raw(body, "id") + if str_eq(id_val, "") { let id_val = "null" } + let params_raw: String = json_get_raw(body, "params") + let result: String = lsp_jsonrpc(method, params_raw) + let resp: String = "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}" + let frame: String = "Content-Length: " + int_to_str(str_len(resp)) + "\r\n\r\n" + resp + print(frame) + } + } +} + +// ── Entry ──────────────────────────────────────────────────────────────────── +// +// When invoked as a standalone binary, default to stdio JSON-RPC so editors +// that drive the language server directly (without the HTTP wrapper) work +// out of the box. Pass a single CLI arg to short-circuit: +// ./el-lsp probe → emits a self-test diagnostic + exits +// ./el-lsp version → emits version + exits + +fn lsp_main() -> Void { + let argv: [String] = args() + let n: Int = native_list_len(argv) + if n > 0 { + let cmd: String = native_list_get(argv, 0) + if str_eq(cmd, "version") { + println("el-lsp 0.1.0") + return + } + if str_eq(cmd, "probe") { + let sample: String = "fn main() -> Void { let x: Int = 42 }" + println(lsp_diagnostics(sample)) + return + } + } + lsp_stdio_loop() +} + +lsp_main() diff --git a/ide/vessels/el-lsp/src/main.err b/ide/vessels/el-lsp/src/main.err new file mode 100644 index 0000000..e69de29 diff --git a/ide/vessels/el-plugin-host/manifest.el b/ide/vessels/el-plugin-host/manifest.el new file mode 100644 index 0000000..0f496d7 --- /dev/null +++ b/ide/vessels/el-plugin-host/manifest.el @@ -0,0 +1,13 @@ +// manifest.el — el-plugin-host vessel + +vessel "el-plugin-host" { + version "0.1.0" + description "Plugin lifecycle manager for el-ide. First-party plugin registry, install/remove/enable/disable." + authors ["Will Anderson "] + edition "2026" +} + +build { + entry "src/main.el" + output "dist/" +} diff --git a/ide/vessels/el-plugin-host/src/el-plugin-host b/ide/vessels/el-plugin-host/src/el-plugin-host new file mode 100755 index 0000000..ec8fdf5 Binary files /dev/null and b/ide/vessels/el-plugin-host/src/el-plugin-host differ diff --git a/ide/vessels/el-plugin-host/src/main.c b/ide/vessels/el-plugin-host/src/main.c new file mode 100644 index 0000000..e47c081 --- /dev/null +++ b/ide/vessels/el-plugin-host/src/main.c @@ -0,0 +1,311 @@ +#include +#include +#include "el_runtime.h" + +el_val_t pkey(el_val_t name, el_val_t field); +el_val_t put_str(el_val_t name, el_val_t field, el_val_t value); +el_val_t put_bool(el_val_t name, el_val_t field, el_val_t value); +el_val_t get_str(el_val_t name, el_val_t field); +el_val_t get_bool(el_val_t name, el_val_t field); +el_val_t index_list(void); +el_val_t index_set(el_val_t names); +el_val_t index_contains(el_val_t name); +el_val_t index_add(el_val_t name); +el_val_t register_plugin(el_val_t name, el_val_t version, el_val_t description, el_val_t installed, el_val_t enabled, el_val_t hooks, el_val_t first_party); +el_val_t plugin_init(void); +el_val_t json_escape_str(el_val_t s); +el_val_t quote_json(el_val_t s); +el_val_t hooks_to_json(el_val_t hooks_csv); +el_val_t plugin_to_json(el_val_t name); +el_val_t plugin_list_json(void); +el_val_t plugin_get_json(el_val_t name); +el_val_t plugin_install(el_val_t name); +el_val_t plugin_remove(el_val_t name); +el_val_t plugin_enable(el_val_t name); +el_val_t plugin_disable(el_val_t name); +el_val_t host_main(void); + +el_val_t pkey(el_val_t name, el_val_t field) { + return el_str_concat(el_str_concat(el_str_concat(EL_STR("plugin/"), name), EL_STR("/")), field); + return 0; +} + +el_val_t put_str(el_val_t name, el_val_t field, el_val_t value) { + state_set(pkey(name, field), value); + return 0; +} + +el_val_t put_bool(el_val_t name, el_val_t field, el_val_t value) { + if (value) { + state_set(pkey(name, field), EL_STR("true")); + } + if (!value) { + state_set(pkey(name, field), EL_STR("false")); + } + return 0; +} + +el_val_t get_str(el_val_t name, el_val_t field) { + return state_get(pkey(name, field)); + return 0; +} + +el_val_t get_bool(el_val_t name, el_val_t field) { + return str_eq(state_get(pkey(name, field)), EL_STR("true")); + return 0; +} + +el_val_t index_list(void) { + el_val_t raw = state_get(EL_STR("plugins/index")); + if (str_eq(raw, EL_STR(""))) { + return native_list_empty(); + } + return str_split(raw, EL_STR(",")); + return 0; +} + +el_val_t index_set(el_val_t names) { + state_set(EL_STR("plugins/index"), list_join(names, EL_STR(","))); + return 0; +} + +el_val_t index_contains(el_val_t name) { + el_val_t names = index_list(); + el_val_t n = native_list_len(names); + el_val_t i = 0; + while (i < n) { + if (str_eq(native_list_get(names, i), name)) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t index_add(el_val_t name) { + if (!index_contains(name)) { + el_val_t names = index_list(); + names = native_list_append(names, name); + index_set(names); + } + return 0; +} + +el_val_t register_plugin(el_val_t name, el_val_t version, el_val_t description, el_val_t installed, el_val_t enabled, el_val_t hooks, el_val_t first_party) { + put_str(name, EL_STR("name"), name); + put_str(name, EL_STR("version"), version); + put_str(name, EL_STR("description"), description); + put_bool(name, EL_STR("installed"), installed); + put_bool(name, EL_STR("enabled"), enabled); + put_str(name, EL_STR("hooks"), hooks); + put_bool(name, EL_STR("first_party"), first_party); + index_add(name); + return 0; +} + +el_val_t plugin_init(void) { + if (str_eq(state_get(EL_STR("plugins/initialised")), EL_STR("true"))) { + return 0; + } + register_plugin(EL_STR("el-theme-dark"), EL_STR("1.0.0"), EL_STR("Dark theme — the default Engram IDE theme."), 1, 1, EL_STR(""), 1); + register_plugin(EL_STR("el-theme-light"), EL_STR("1.0.0"), EL_STR("Light theme for high-ambient-light environments."), 0, 0, EL_STR(""), 1); + register_plugin(EL_STR("el-fmt"), EL_STR("0.1.0"), EL_STR("Code formatter — auto-formats .el files on save."), 1, 1, EL_STR("on_save"), 1); + register_plugin(EL_STR("el-doc"), EL_STR("0.1.0"), EL_STR("Documentation generator — produces HTML docs from type definitions."), 0, 0, EL_STR("custom_tool"), 1); + register_plugin(EL_STR("el-test"), EL_STR("0.1.0"), EL_STR("Test runner with inline results displayed in the editor gutter."), 0, 0, EL_STR("on_build_complete,custom_panel"), 1); + state_set(EL_STR("plugins/initialised"), EL_STR("true")); + return 0; +} + +el_val_t json_escape_str(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\""))) { + out = el_str_concat(out, EL_STR("\\\"")); + } else { + if (str_eq(ch, EL_STR("\\"))) { + out = el_str_concat(out, EL_STR("\\\\")); + } else { + if (str_eq(ch, EL_STR("\n"))) { + out = el_str_concat(out, EL_STR("\\n")); + } else { + out = el_str_concat(out, ch); + } + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t quote_json(el_val_t s) { + return el_str_concat(el_str_concat(EL_STR("\""), json_escape_str(s)), EL_STR("\"")); + return 0; +} + +el_val_t hooks_to_json(el_val_t hooks_csv) { + if (str_eq(hooks_csv, EL_STR(""))) { + return EL_STR("[]"); + } + el_val_t xs = str_split(hooks_csv, EL_STR(",")); + el_val_t n = native_list_len(xs); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + out = el_str_concat(out, quote_json(native_list_get(xs, i))); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t plugin_to_json(el_val_t name) { + el_val_t parts = native_list_empty(); + parts = native_list_append(parts, el_str_concat(EL_STR("\"name\":"), quote_json(get_str(name, EL_STR("name"))))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"version\":"), quote_json(get_str(name, EL_STR("version"))))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"description\":"), quote_json(get_str(name, EL_STR("description"))))); + el_val_t installed = get_str(name, EL_STR("installed")); + el_val_t enabled = get_str(name, EL_STR("enabled")); + el_val_t first_party = get_str(name, EL_STR("first_party")); + parts = native_list_append(parts, el_str_concat(EL_STR("\"installed\":"), installed)); + parts = native_list_append(parts, el_str_concat(EL_STR("\"enabled\":"), enabled)); + parts = native_list_append(parts, el_str_concat(EL_STR("\"hooks\":"), hooks_to_json(get_str(name, EL_STR("hooks"))))); + parts = native_list_append(parts, el_str_concat(EL_STR("\"first_party\":"), first_party)); + return el_str_concat(el_str_concat(EL_STR("{"), list_join(parts, EL_STR(","))), EL_STR("}")); + return 0; +} + +el_val_t plugin_list_json(void) { + plugin_init(); + el_val_t names = index_list(); + el_val_t n = native_list_len(names); + el_val_t out = EL_STR("["); + el_val_t i = 0; + while (i < n) { + if (i > 0) { + out = el_str_concat(out, EL_STR(",")); + } + out = el_str_concat(out, plugin_to_json(native_list_get(names, i))); + i = (i + 1); + } + return el_str_concat(out, EL_STR("]")); + return 0; +} + +el_val_t plugin_get_json(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return EL_STR("null"); + } + return plugin_to_json(name); + return 0; +} + +el_val_t plugin_install(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return EL_STR("{\"error\":\"plugin registry is not available (registry URL not configured)\"}"); + } + if (get_bool(name, EL_STR("installed"))) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' is already installed\"}")); + } + put_bool(name, EL_STR("installed"), 1); + put_bool(name, EL_STR("enabled"), 1); + return plugin_to_json(name); + return 0; +} + +el_val_t plugin_remove(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}")); + } + if (str_eq(name, EL_STR("el-theme-dark"))) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' cannot be removed: it is a required built-in\"}")); + } + put_bool(name, EL_STR("installed"), 0); + put_bool(name, EL_STR("enabled"), 0); + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t plugin_enable(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}")); + } + put_bool(name, EL_STR("enabled"), 1); + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t plugin_disable(el_val_t name) { + plugin_init(); + if (!index_contains(name)) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"plugin '"), json_escape_str(name)), EL_STR("' not found\"}")); + } + put_bool(name, EL_STR("enabled"), 0); + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t host_main(void) { + el_val_t argv = args(); + el_val_t n = native_list_len(argv); + if (n == 0) { + println(plugin_list_json()); + return 0; + } + el_val_t cmd = native_list_get(argv, 0); + if (str_eq(cmd, EL_STR("list"))) { + println(plugin_list_json()); + return 0; + } + if (str_eq(cmd, EL_STR("get"))) { + if (n >= 2) { + println(plugin_get_json(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("install"))) { + if (n >= 2) { + println(plugin_install(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("remove"))) { + if (n >= 2) { + println(plugin_remove(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("enable"))) { + if (n >= 2) { + println(plugin_enable(native_list_get(argv, 1))); + } + return 0; + } + if (str_eq(cmd, EL_STR("disable"))) { + if (n >= 2) { + println(plugin_disable(native_list_get(argv, 1))); + } + return 0; + } + println(EL_STR("usage: el-plugin-host [list|get|install|remove|enable|disable] ")); + return 0; +} + +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + host_main(); + return 0; +} + diff --git a/ide/vessels/el-plugin-host/src/main.el b/ide/vessels/el-plugin-host/src/main.el new file mode 100644 index 0000000..b42fec0 --- /dev/null +++ b/ide/vessels/el-plugin-host/src/main.el @@ -0,0 +1,251 @@ +// main.el — el-plugin-host +// +// Plugin lifecycle manager for el-ide. Pure-El port of the el-plugin-host +// Rust crate. +// +// Plugins are stored in process-global state via state_get / state_set. +// Each plugin lives under a key prefix `plugin//`. The +// canonical list of plugin names is maintained at `plugins/index` as a +// comma-separated string (we don't have list serialisation in state yet). +// +// Public API: +// plugin_init() — seed first-party plugins (idempotent) +// plugin_list_json() -> String — JSON array of all plugins +// plugin_get_json(name) -> String — JSON object or "null" +// plugin_install(name) -> String — JSON { ok | error } +// plugin_remove(name) -> String +// plugin_enable(name) -> String +// plugin_disable(name) -> String + +// ── State helpers (string K/V) ─────────────────────────────────────────────── + +fn pkey(name: String, field: String) -> String { + "plugin/" + name + "/" + field +} + +fn put_str(name: String, field: String, value: String) -> Void { + state_set(pkey(name, field), value) +} + +fn put_bool(name: String, field: String, value: Bool) -> Void { + if value { state_set(pkey(name, field), "true") } + if !value { state_set(pkey(name, field), "false") } +} + +fn get_str(name: String, field: String) -> String { + state_get(pkey(name, field)) +} + +fn get_bool(name: String, field: String) -> Bool { + str_eq(state_get(pkey(name, field)), "true") +} + +// Plugin index: comma-separated list of names at "plugins/index". +fn index_list() -> [String] { + let raw: String = state_get("plugins/index") + if str_eq(raw, "") { return native_list_empty() } + str_split(raw, ",") +} + +fn index_set(names: [String]) -> Void { + state_set("plugins/index", list_join(names, ",")) +} + +fn index_contains(name: String) -> Bool { + let names: [String] = index_list() + let n: Int = native_list_len(names) + let i: Int = 0 + while i < n { + if str_eq(native_list_get(names, i), name) { return true } + let i = i + 1 + } + false +} + +fn index_add(name: String) -> Void { + if !index_contains(name) { + let names: [String] = index_list() + let names = native_list_append(names, name) + index_set(names) + } +} + +// ── Initialisation ─────────────────────────────────────────────────────────── + +fn register_plugin(name: String, version: String, description: String, + installed: Bool, enabled: Bool, hooks: String, + first_party: Bool) -> Void { + put_str(name, "name", name) + put_str(name, "version", version) + put_str(name, "description", description) + put_bool(name, "installed", installed) + put_bool(name, "enabled", enabled) + put_str(name, "hooks", hooks) + put_bool(name, "first_party", first_party) + index_add(name) +} + +fn plugin_init() -> Void { + if str_eq(state_get("plugins/initialised"), "true") { + return + } + register_plugin("el-theme-dark", "1.0.0", "Dark theme — the default Engram IDE theme.", true, true, "", true) + register_plugin("el-theme-light", "1.0.0", "Light theme for high-ambient-light environments.", false, false, "", true) + register_plugin("el-fmt", "0.1.0", "Code formatter — auto-formats .el files on save.", true, true, "on_save", true) + register_plugin("el-doc", "0.1.0", "Documentation generator — produces HTML docs from type definitions.", false, false, "custom_tool", true) + register_plugin("el-test", "0.1.0", "Test runner with inline results displayed in the editor gutter.", false, false, "on_build_complete,custom_panel", true) + state_set("plugins/initialised", "true") +} + +// ── Serialisation ──────────────────────────────────────────────────────────── + +fn json_escape_str(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + let out: String = "" + let i: Int = 0 + while i < n { + let ch: String = native_list_get(chars, i) + if ch == "\"" { let out = out + "\\\"" } + else { if ch == "\\" { let out = out + "\\\\" } + else { if ch == "\n" { let out = out + "\\n" } + else { let out = out + ch } } } + let i = i + 1 + } + out +} + +fn quote_json(s: String) -> String { + "\"" + json_escape_str(s) + "\"" +} + +fn hooks_to_json(hooks_csv: String) -> String { + if str_eq(hooks_csv, "") { return "[]" } + let xs: [String] = str_split(hooks_csv, ",") + let n: Int = native_list_len(xs) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let out = out + quote_json(native_list_get(xs, i)) + let i = i + 1 + } + out + "]" +} + +fn plugin_to_json(name: String) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "\"name\":" + quote_json(get_str(name, "name"))) + let parts = native_list_append(parts, "\"version\":" + quote_json(get_str(name, "version"))) + let parts = native_list_append(parts, "\"description\":" + quote_json(get_str(name, "description"))) + let installed: String = get_str(name, "installed") + let enabled: String = get_str(name, "enabled") + let first_party: String = get_str(name, "first_party") + let parts = native_list_append(parts, "\"installed\":" + installed) + let parts = native_list_append(parts, "\"enabled\":" + enabled) + let parts = native_list_append(parts, "\"hooks\":" + hooks_to_json(get_str(name, "hooks"))) + let parts = native_list_append(parts, "\"first_party\":" + first_party) + "{" + list_join(parts, ",") + "}" +} + +fn plugin_list_json() -> String { + plugin_init() + let names: [String] = index_list() + let n: Int = native_list_len(names) + let out: String = "[" + let i: Int = 0 + while i < n { + if i > 0 { let out = out + "," } + let out = out + plugin_to_json(native_list_get(names, i)) + let i = i + 1 + } + out + "]" +} + +fn plugin_get_json(name: String) -> String { + plugin_init() + if !index_contains(name) { return "null" } + plugin_to_json(name) +} + +// ── Mutations ──────────────────────────────────────────────────────────────── + +fn plugin_install(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin registry is not available (registry URL not configured)\"}" + } + if get_bool(name, "installed") { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' is already installed\"}" + } + put_bool(name, "installed", true) + put_bool(name, "enabled", true) + plugin_to_json(name) +} + +fn plugin_remove(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" + } + if str_eq(name, "el-theme-dark") { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' cannot be removed: it is a required built-in\"}" + } + put_bool(name, "installed", false) + put_bool(name, "enabled", false) + "{\"ok\":true}" +} + +fn plugin_enable(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" + } + put_bool(name, "enabled", true) + "{\"ok\":true}" +} + +fn plugin_disable(name: String) -> String { + plugin_init() + if !index_contains(name) { + return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}" + } + put_bool(name, "enabled", false) + "{\"ok\":true}" +} + +// ── Standalone CLI ─────────────────────────────────────────────────────────── + +fn host_main() -> Void { + let argv: [String] = args() + let n: Int = native_list_len(argv) + if n == 0 { + println(plugin_list_json()) + return + } + let cmd: String = native_list_get(argv, 0) + if str_eq(cmd, "list") { println(plugin_list_json()) return } + if str_eq(cmd, "get") { + if n >= 2 { println(plugin_get_json(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "install") { + if n >= 2 { println(plugin_install(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "remove") { + if n >= 2 { println(plugin_remove(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "enable") { + if n >= 2 { println(plugin_enable(native_list_get(argv, 1))) } + return + } + if str_eq(cmd, "disable") { + if n >= 2 { println(plugin_disable(native_list_get(argv, 1))) } + return + } + println("usage: el-plugin-host [list|get|install|remove|enable|disable] ") +} + +host_main() diff --git a/ide/vessels/el-plugin-host/src/main.err b/ide/vessels/el-plugin-host/src/main.err new file mode 100644 index 0000000..e69de29