El IDE: 10-round pass — syntax highlighting, file browser, runner, completion, split panes, find/replace, settings, minimap

Round 1: Fix dependency paths (../el/crates → ../el/engrams), verify build
Round 2: Enhanced syntax highlighting — function call detection, all El keywords (activate, sealed, parallel, deploy, etc.)
Round 3: Full El keyword set in CodeMirror tokenizer and completions; 50+ builtin function completions with type signatures
Round 4: File system integration — mkdir, rename, delete, file tree search; git status badges
Round 5: Runner integration — Ctrl+R shortcut, SSE streaming output, clickable error lines with jump-to-line
Round 6: Error highlighting with accurate line/col from lexer/parser spans; diagnostic dedup
Round 7: Find/replace panel; Ctrl+G go-to-line; toggle line comment; word-wrap compartment fix
Round 8: Code completion — 50+ builtins, keyword completions, snippet completions, server snippet integration
Round 9: Resizable panels — file tree drag-resize + collapse (Ctrl+B), type-graph drag-resize, bottom panel toggle (Ctrl+J), width persistence
Round 10: Settings API (GET/POST/DELETE /api/settings, ~/.el-ide/settings.json); frontend wired to API with debounced save; theme persistence
Round 11: Minimap click-to-jump and drag-to-scroll
Round 12: Command palette — added Go To Line, Toggle Word Wrap/Minimap/File Tree/Bottom Panel, font size commands, New File, Select Next Occurrence
Round 13: Multi-cursor — Ctrl+D select next occurrence, EditorSelection exposed for multi-range selection
This commit is contained in:
Will Anderson
2026-04-29 04:34:08 -05:00
parent f898683b19
commit 376fbb41b3
25 changed files with 1847 additions and 43 deletions
+108 -3
View File
@@ -29,7 +29,71 @@ pub enum CompletionKind {
const KEYWORDS: &[&str] = &[
"let", "fn", "type", "enum", "match", "return", "activate", "where",
"sealed", "if", "else", "for", "in", "true", "false",
"sealed", "if", "else", "for", "in", "while", "true", "false",
"test", "seed", "assert", "target", "protocol", "impl",
"import", "from", "as", "with", "retry", "times", "fallback",
"reason", "parallel", "trace", "requires", "deploy", "to", "via",
];
// ── Builtin functions ─────────────────────────────────────────────────────────
const BUILTIN_FUNCTIONS: &[(&str, &str)] = &[
("println", "fn(value: String) -> Void"),
("print", "fn(value: String) -> Void"),
("int_to_str", "fn(n: Int) -> String"),
("str_to_int", "fn(s: String) -> Int"),
("string_len", "fn(s: String) -> Int"),
("str_slice", "fn(s: String, start: Int, end: Int) -> String"),
("str_concat", "fn(a: String, b: String) -> String"),
("str_contains", "fn(s: String, sub: String) -> Bool"),
("str_starts_with", "fn(s: String, prefix: String) -> Bool"),
("str_ends_with", "fn(s: String, suffix: String) -> Bool"),
("str_to_upper", "fn(s: String) -> String"),
("str_to_lower", "fn(s: String) -> String"),
("str_split", "fn(s: String, sep: String) -> [String]"),
("str_trim", "fn(s: String) -> String"),
("str_replace", "fn(s: String, from: String, to: String) -> String"),
("str_index_of", "fn(s: String, sub: String) -> Int"),
("float_to_str", "fn(f: Float) -> String"),
("str_to_float", "fn(s: String) -> Float"),
("array_push", "fn(arr: [T], val: T) -> [T]"),
("array_len", "fn(arr: [T]) -> Int"),
("array_get", "fn(arr: [T], i: Int) -> T"),
("list_len", "fn(list: [T]) -> Int"),
("list_get", "fn(list: [T], i: Int) -> T"),
("list_map", "fn(list: [T], f: fn(T) -> U) -> [U]"),
("list_filter", "fn(list: [T], f: fn(T) -> Bool) -> [T]"),
("list_reduce", "fn(list: [T], init: U, f: fn(U, T) -> U) -> U"),
("map_create", "fn() -> Map"),
("map_set", "fn(m: Map, key: String, val: T) -> Map"),
("map_get", "fn(m: Map, key: String) -> T"),
("map_has", "fn(m: Map, key: String) -> Bool"),
("math_abs", "fn(n: Float) -> Float"),
("math_sqrt", "fn(n: Float) -> Float"),
("math_floor", "fn(n: Float) -> Int"),
("math_ceil", "fn(n: Float) -> Int"),
("math_round", "fn(n: Float) -> Int"),
("math_min", "fn(a: Float, b: Float) -> Float"),
("math_max", "fn(a: Float, b: Float) -> Float"),
("math_pow", "fn(base: Float, exp: Float) -> Float"),
("now_millis", "fn() -> Int"),
("time_now_utc", "fn() -> String"),
("time_to_parts", "fn(ts: String) -> Map"),
("time_format", "fn(ts: String, fmt: String) -> String"),
("llm_call", "fn(prompt: String) -> String"),
("llm_parallel", "fn(prompts: [String]) -> [String]"),
("random_int", "fn(min: Int, max: Int) -> Int"),
("random_float", "fn() -> Float"),
("parse_json", "fn(s: String) -> Map"),
("to_json", "fn(v: T) -> String"),
("http_get", "fn(url: String) -> String"),
("http_post", "fn(url: String, body: String) -> String"),
("read_file", "fn(path: String) -> String"),
("write_file", "fn(path: String, content: String) -> Void"),
("env_get", "fn(key: String) -> String"),
("sleep_ms", "fn(ms: Int) -> Void"),
("uuid_new", "fn() -> String"),
("hash_sha256", "fn(s: String) -> String"),
];
const BUILTIN_TYPES: &[(&str, &str)] = &[
@@ -107,6 +171,12 @@ pub fn completions_at(env: &TypeEnv, source: &str, cursor_pos: usize) -> Vec<Com
TypeDef::Primitive(_) => {
("primitive type".into(), None, 0.05)
}
TypeDef::Protocol { methods, .. } => {
let m_list = methods.iter().map(|m| m.name.clone()).collect::<Vec<_>>().join(", ");
(format!("protocol {{ {m_list} }}"),
Some(format!("Protocol methods: {m_list}")),
0.15)
}
};
results.push(Completion {
label: type_name.clone(),
@@ -118,7 +188,20 @@ pub fn completions_at(env: &TypeEnv, source: &str, cursor_pos: usize) -> Vec<Com
}
}
// Functions from env
// Builtin functions
for &(name, sig) in BUILTIN_FUNCTIONS {
if name.to_lowercase().starts_with(&prefix.to_lowercase()) || prefix.is_empty() {
results.push(Completion {
label: name.to_string(),
kind: CompletionKind::Function,
detail: sig.into(),
documentation: Some(format!("Built-in function: {name}\n{sig}")),
score: prefix_score(name, &prefix) + 0.11,
});
}
}
// Functions from env (user-defined)
for (fn_name, fn_type) in &env.functions {
if fn_name.to_lowercase().starts_with(&prefix.to_lowercase()) || prefix.is_empty() {
results.push(Completion {
@@ -131,8 +214,9 @@ pub fn completions_at(env: &TypeEnv, source: &str, cursor_pos: usize) -> Vec<Com
}
}
// Sort by score descending
// Sort by score descending, deduplicate by label
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.dedup_by(|a, b| a.label == b.label);
results
}
@@ -179,8 +263,29 @@ fn keyword_doc(kw: &str) -> Option<String> {
"if" => "Conditional: `if cond { then } else { else }`",
"else" => "Else branch of an if expression.",
"for" => "For loop: `for item in collection { body }`",
"while" => "While loop: `while cond { body }`",
"in" => "Used in `for item in collection`",
"true" | "false" => "Boolean literal",
"protocol" => "Define a protocol (trait): `protocol Name { fn method(self) -> Ret; }`",
"impl" => "Implement a protocol: `impl Protocol for Type { fn method(self) -> Ret { ... } }`",
"import" => "Import from a module: `import { Name } from \"module\"`",
"from" => "Used in import: `import { Name } from \"module\"`",
"as" => "Alias in import: `import { Name as Alias } from \"module\"`",
"with" => "With clause for retry/fallback: `with retry times 3`",
"retry" => "Retry policy: `retry times N`",
"times" => "Used in retry: `retry times N`",
"fallback" => "Fallback value on failure: `fallback { default_expr }`",
"reason" => "Reason clause: provides context to Engram reasoning engine.",
"parallel" => "Parallel execution: `parallel { task1; task2 }`",
"trace" => "Emit a trace event: `trace \"message\"`",
"requires" => "Dependency declaration: `requires Module`",
"deploy" => "Deploy declaration: `deploy service to target via method`",
"to" => "Used in deploy: `deploy X to Y`",
"via" => "Used in deploy: `deploy X via method`",
"test" => "Test declaration: `test \"name\" { assertions }`",
"seed" => "Seed data block: `seed { ... }`",
"assert" => "Assertion: `assert condition, \"message\"`",
"target" => "Target annotation: `target { ... }`",
_ => return None,
};
Some(doc.to_string())
+12 -2
View File
@@ -31,7 +31,12 @@ pub fn check(source: &str) -> Vec<Diagnostic> {
let tokens = match el_lexer::tokenize(source) {
Ok(t) => t,
Err(e) => {
out.push(Diagnostic::error(format!("Lex error: {e}")));
out.push(Diagnostic {
message: format!("Lex error: {}", e.kind),
severity: "error".into(),
line: Some(e.span.line),
col: Some(e.span.col),
});
return out;
}
};
@@ -39,7 +44,12 @@ pub fn check(source: &str) -> Vec<Diagnostic> {
let program = match el_parser::parse(tokens, source.to_string()) {
Ok(p) => p,
Err(e) => {
out.push(Diagnostic::error(format!("Parse error: {e}")));
out.push(Diagnostic {
message: format!("Parse error: {}", e.kind),
severity: "error".into(),
line: Some(e.span.line),
col: Some(e.span.col),
});
return out;
}
};
+8
View File
@@ -127,5 +127,13 @@ fn format_typedef_doc(name: &str, def: &TypeDef) -> String {
format!("enum {name} {{\n{variants_str}\n}}")
}
TypeDef::Primitive(t) => format!("primitive type {name} = {t}"),
TypeDef::Protocol { methods, .. } => {
let methods_str = methods
.iter()
.map(|m| format!(" fn {}()", m.name))
.collect::<Vec<_>>()
.join("\n");
format!("protocol {name} {{\n{methods_str}\n}}")
}
}
}
+12
View File
@@ -119,6 +119,18 @@ pub fn build(env: &TypeEnv) -> TypeGraph {
fields: vec![],
});
}
TypeDef::Protocol { methods, .. } => {
let method_strs: Vec<String> = methods
.iter()
.map(|m| m.name.clone())
.collect();
nodes.push(TypeNode {
id: type_name.clone(),
name: type_name.clone(),
kind: "protocol".into(),
fields: method_strs,
});
}
}
}