From 45c73ec8ba33cdd3c2a9489bf445c358a467c666 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:31:19 +0000 Subject: [PATCH] perf: avoid expensive doc stringification for autocomplete Refactored the autocomplete variable matching logic in `src/ui/editor.ts` to iterate through document lines using CodeMirror's `iterLines()` instead of allocating the entire document string in memory with `toString()`. Also pulled the regex up and introduced a quick `.includes("=")` bailout to skip the regex engine completely for most lines. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> --- benchmark_editor.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++ src/ui/editor.ts | 13 +++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 benchmark_editor.ts diff --git a/benchmark_editor.ts b/benchmark_editor.ts new file mode 100644 index 0000000..53f5d53 --- /dev/null +++ b/benchmark_editor.ts @@ -0,0 +1,51 @@ +import { EditorState } from "@codemirror/state"; + +const lines = []; +for (let i = 0; i < 500000; i++) { + lines.push(`some_var_${i} = ${i * 10} + 5`); +} +const text = lines.join('\n'); +const state = EditorState.create({ doc: text }); + +function benchmarkOld() { + const start = performance.now(); + const options = []; + const docText = state.doc.toString(); + for (const m of docText.matchAll(/^\s*([\p{L}_][\p{L}\d_]*)\s*=/gmu)) { + options.push({ label: m[1], type: "variable" }); + } + const end = performance.now(); + return end - start; +} + +const pattern = /^\s*([\p{L}_][\p{L}\d_]*)\s*=/u; +function benchmarkRegexPerLine() { + const start = performance.now(); + const options = []; + const iter = state.doc.iterLines(); + for (let next = iter.next(); !next.done; next = iter.next()) { + const line = next.value; + // Optimization: checking '=' first is significantly faster than firing regex blindly + if (line.includes('=')) { + const m = pattern.exec(line); + if (m) { + options.push({ label: m[1], type: "variable" }); + } + } + } + const end = performance.now(); + return end - start; +} + +benchmarkOld(); +benchmarkRegexPerLine(); + +let oldTotal = 0; +let newTotal = 0; +for (let i = 0; i < 10; i++) { + oldTotal += benchmarkOld(); + newTotal += benchmarkRegexPerLine(); +} + +console.log(`Old avg: ${oldTotal / 10} ms`); +console.log(`Regex per line with '=' check avg: ${newTotal / 10} ms`); diff --git a/src/ui/editor.ts b/src/ui/editor.ts index 8d5ca57..dea37c8 100644 --- a/src/ui/editor.ts +++ b/src/ui/editor.ts @@ -170,6 +170,7 @@ export class SumEditor { 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; @@ -182,9 +183,15 @@ export class SumEditor { })); const options = [...engineOptions]; // document variables - const text = ctx.state.doc.toString(); - for (const m of text.matchAll(/^\s*([\p{L}_][\p{L}\d_]*)\s*=/gmu)) { - options.push({ label: m[1], type: "variable" }); + 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 }; };