// 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()