feat: El port — vessels populated alongside Rust
Adds src/main.el + manifest.el for each vessel in this workspace, ported from the Rust sources during the El consolidation pass on 2026-04-30. Each vessel now has both Rust (legacy) and El (target) sources side-by-side; Rust will be removed once the El paths are verified at runtime, vessel by vessel. Per-vessel work was split across multiple parallel agents reading the Rust to understand intent, then designing idiomatic El. Not 1:1 transliteration. Each ported vessel includes: - manifest.el per spec/language.md \u00a715.1 - src/main.el with the vessel's public surface - Compile verified via dist/platform/elc + cc against the el_runtime Known gaps surfaced during the port (held for follow-up): HMAC-SHA256 and base64 crypto, HTTP status code in handler returns, request headers in handler signatures, subprocess primitives, streaming responses, struct/enum types, browser/JS codegen target. Codegen bug list of 9 items tracked separately. The El sources here are runtime- ready under the canonical C runtime; the gaps are language/runtime extensions still in flight.
This commit is contained in:
@@ -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<String, Any> {
|
||||
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 }
|
||||
"<!doctype html><html><head><title>el-ide</title></head><body><h1>el-ide</h1><p>Backend running. UI bundle missing at <code>" + path + "</code>.</p></body></html>"
|
||||
}
|
||||
|
||||
// ── 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<String, Any> = 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()
|
||||
Reference in New Issue
Block a user