Split from #22. Phase 2b of the textarea editor enhancement track.
Dependency: do not start until Phase 2a (#25 — reference data loader) is merged.
Goal
Show a completion dropdown driven entirely by in-memory data. No SQL executes on typing; no network call is made from the autocomplete path.
Data sources (all in-memory)
| Source |
Provided by |
| Keywords |
app.state.editorReference.keywords (#25) |
| Built-in functions |
app.state.editorReference.funcs (#25) |
system.completions entries |
app.state.editorReference.completions (may be null) |
| Databases and tables |
app.state.schema |
| Columns |
Already-loaded columns only (table.columns !== null). No on-demand column load from the autocomplete path. |
Triggers
Ctrl+Space — show completions for the token at the caret, or a context list if the caret follows ..
- Typing
. after a bare identifier — show child names: database → its tables; table name → its already-loaded columns.
- No as-you-type popup after N characters. Leave for a future iteration.
If app.state.editorReference is null (#25 not yet loaded), Ctrl+Space falls back to the hardcoded SQL_KEYWORDS set.
Pure module src/core/editor-autocomplete.js
completionContext(sql, caretPos, schema, refData)
// refData may be null → falls back to SQL_KEYWORDS / SQL_FUNCS
// → { kind: 'keyword'|'function'|'completion'|'table'|'column'|'db',
// prefix: string,
// candidates: string[] }
completionInsert(prefix, chosen)
// → string — the text to insert, replacing the prefix
// For most completions: returns chosen.
// For a function name followed by nothing: may append '(' as a hint.
Both functions are pure — no DOM, no globals.
UI insertion contract: the UI (in editor.js) is responsible for calling applyEdit correctly:
const { prefix, candidates } = completionContext(ta.value, ta.selectionStart, schema, refData);
// ... user picks `chosen` from the dropdown ...
const insert = completionInsert(prefix, chosen);
ta.setSelectionRange(ta.selectionStart - prefix.length, ta.selectionStart); // select the prefix
applyEdit(ta, insert); // replaces the prefix with the completion; fires input listener
applyCompletion does not return a full replacement SQL string. applyEdit inserts/replaces the selection only.
Dropdown component (mounted in src/ui/editor.js)
One <div class="autocomplete-dropdown"> appended to .sql-area. Hidden (display: none) when not active.
Positioning: absolutely positioned below the caret. Use <pre> metrics (same font/line-height as the textarea) to estimate the caret's pixel position. If the dropdown would overflow the viewport bottom, flip it above.
Keyboard while dropdown is open:
ArrowDown / ArrowUp → move selection, wrap around.
Enter or Tab → accept: select prefix range, applyEdit(ta, insert), close dropdown.
Esc → close without inserting.
- Any other key → close dropdown (the character is typed normally); re-trigger context on
..
Dismiss: click outside the dropdown closes it.
Acceptance criteria
Non-goals
Split from #22. Phase 2b of the textarea editor enhancement track.
Dependency: do not start until Phase 2a (#25 — reference data loader) is merged.
Goal
Show a completion dropdown driven entirely by in-memory data. No SQL executes on typing; no network call is made from the autocomplete path.
Data sources (all in-memory)
app.state.editorReference.keywords(#25)app.state.editorReference.funcs(#25)system.completionsentriesapp.state.editorReference.completions(may be null)app.state.schematable.columns !== null). No on-demand column load from the autocomplete path.Triggers
Ctrl+Space— show completions for the token at the caret, or a context list if the caret follows...after a bare identifier — show child names: database → its tables; table name → its already-loaded columns.If
app.state.editorReferenceis null (#25 not yet loaded),Ctrl+Spacefalls back to the hardcodedSQL_KEYWORDSset.Pure module
src/core/editor-autocomplete.jsBoth functions are pure — no DOM, no globals.
UI insertion contract: the UI (in
editor.js) is responsible for callingapplyEditcorrectly:applyCompletiondoes not return a full replacement SQL string.applyEditinserts/replaces the selection only.Dropdown component (mounted in
src/ui/editor.js)One
<div class="autocomplete-dropdown">appended to.sql-area. Hidden (display: none) when not active.Positioning: absolutely positioned below the caret. Use
<pre>metrics (same font/line-height as the textarea) to estimate the caret's pixel position. If the dropdown would overflow the viewport bottom, flip it above.Keyboard while dropdown is open:
ArrowDown/ArrowUp→ move selection, wrap around.EnterorTab→ accept: select prefix range,applyEdit(ta, insert), close dropdown.Esc→ close without inserting...Dismiss: click outside the dropdown closes it.
Acceptance criteria
Ctrl+Spaceshows completions for the token at the caret..after a db/table name shows table/column names (already-loaded only).ArrowDown/Up/Enter/Tab/Esckeyboard navigation works.Ctrl+Space.app.state.editorReferencebeing null shows keywords only without error.src/core/editor-autocomplete.jsat 100% coverage.src/ui/editor.jschanges maintain existing coverage threshold.Non-goals