🧹 [code health improvement] Refactor SumEditor constructor in src/ui/editor.ts

Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-10-07 04:03:07 +00:00
parent bede889ced
commit a5a6412c69

View file

@ -1,7 +1,7 @@
// CodeMirror editor wired to the engine: evaluates on change, highlights // CodeMirror editor wired to the engine: evaluates on change, highlights
// engine tokens, renders the results overlay, autocompletes phrases. // engine tokens, renders the results overlay, autocompletes phrases.
import { EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, keymap, drawSelection } from "@codemirror/view"; import { EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, keymap, drawSelection } from "@codemirror/view";
import { EditorState, StateEffect, StateField, RangeSetBuilder } from "@codemirror/state"; import { EditorState, StateEffect, StateField, RangeSetBuilder, Extension } from "@codemirror/state";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { autocompletion, CompletionContext, CompletionResult, acceptCompletion } from "@codemirror/autocomplete"; import { autocompletion, CompletionContext, CompletionResult, acceptCompletion } from "@codemirror/autocomplete";
import { SumEngine, LineResult } from "../engine"; import { SumEngine, LineResult } from "../engine";
@ -158,6 +158,7 @@ export class SumEditor {
view: EditorView; view: EditorView;
private resultsEl: HTMLElement; private resultsEl: HTMLElement;
private results: LineResult[] = []; private results: LineResult[] = [];
private engineOptions: { label: string; type: string; detail?: string }[] | null = null;
constructor( constructor(
parent: HTMLElement, parent: HTMLElement,
@ -169,41 +170,25 @@ export class SumEditor {
) { ) {
this.resultsEl = resultsEl; this.resultsEl = resultsEl;
let engineOptions: { label: string; type: string; detail?: string }[] | null = null;
const varPattern = /^\s*([\p{L}_][\p{L}\d_]*)\s*=/u;
const completionSource = (ctx: CompletionContext): CompletionResult | null => {
const word = ctx.matchBefore(/[\p{L}_]+$/u);
if (!word || (word.from === word.to && !ctx.explicit)) return null;
if (word.to - word.from < 2 && !ctx.explicit) return null;
// the engine's phrase list is fixed after boot — map it once
engineOptions ??= this.engine.completions().map((c) => ({
label: c.label,
type: c.type === "function" ? "function" : c.type === "keyword" ? "keyword" : "constant",
detail: c.detail,
}));
const options = [...engineOptions];
// document variables
const iter = ctx.state.doc.iterLines();
for (let next = iter.next(); !next.done; next = iter.next()) {
const line = next.value;
if (line.includes("=")) {
const m = varPattern.exec(line);
if (m) {
options.push({ label: m[1], type: "variable" });
}
}
}
return { from: word.from, options, validFor: /^[\p{L}_]*$/u };
};
this.view = new EditorView({ this.view = new EditorView({
parent, parent,
state: EditorState.create({ state: EditorState.create({
doc: initialText, doc: initialText,
extensions: [ extensions: this.buildExtensions(),
}),
});
this.evaluate(initialText);
requestAnimationFrame(() => this.renderResults());
this.setupEventListeners();
}
private buildExtensions(): Extension[] {
return [
history(), history(),
keymap.of([...defaultKeymap, ...historyKeymap]), keymap.of([...defaultKeymap, ...historyKeymap]),
autocompletion({ override: [completionSource], activateOnTyping: true, defaultKeymap: false }), autocompletion({ override: [this.completionSource.bind(this)], activateOnTyping: true, defaultKeymap: false }),
keymap.of([{ key: "Tab", run: acceptCompletion }]), keymap.of([{ key: "Tab", run: acceptCompletion }]),
resultsField, resultsField,
highlightField, highlightField,
@ -224,7 +209,37 @@ export class SumEditor {
borderLeft: "2px solid var(--caret)", borderLeft: "2px solid var(--caret)",
}, },
}), }),
EditorView.updateListener.of((u: ViewUpdate) => { EditorView.updateListener.of((u: ViewUpdate) => this.onUpdate(u)),
];
}
private completionSource(ctx: CompletionContext): CompletionResult | null {
const varPattern = /^\s*([\p{L}_][\p{L}\d_]*)\s*=/u;
const word = ctx.matchBefore(/[\p{L}_]+$/u);
if (!word || (word.from === word.to && !ctx.explicit)) return null;
if (word.to - word.from < 2 && !ctx.explicit) return null;
// the engine's phrase list is fixed after boot — map it once
this.engineOptions ??= this.engine.completions().map((c) => ({
label: c.label,
type: c.type === "function" ? "function" : c.type === "keyword" ? "keyword" : "constant",
detail: c.detail,
}));
const options = [...this.engineOptions];
// document variables
const iter = ctx.state.doc.iterLines();
for (let next = iter.next(); !next.done; next = iter.next()) {
const line = next.value;
if (line.includes("=")) {
const m = varPattern.exec(line);
if (m) {
options.push({ label: m[1], type: "variable" });
}
}
}
return { from: word.from, options, validFor: /^[\p{L}_]*$/u };
}
private onUpdate(u: ViewUpdate) {
if (u.docChanged) { if (u.docChanged) {
const text = u.state.doc.toString(); const text = u.state.doc.toString();
this.evaluate(text); this.evaluate(text);
@ -243,13 +258,9 @@ export class SumEditor {
this.cb.onSelection([from, to]); this.cb.onSelection([from, to]);
} }
} }
}), }
],
}),
});
this.evaluate(initialText); private setupEventListeners() {
requestAnimationFrame(() => this.renderResults());
this.view.scrollDOM.addEventListener("scroll", () => this.renderResults()); this.view.scrollDOM.addEventListener("scroll", () => this.renderResults());
// copying whole lines also copies their results: "5+5 = 10"; // copying whole lines also copies their results: "5+5 = 10";