Skip to content

Editor intelligence: autocomplete dropdown (Phase 2b) #26

Description

@BorisTyshkevich

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

  • Ctrl+Space shows completions for the token at the caret.
  • Typing . after a db/table name shows table/column names (already-loaded only).
  • ArrowDown/Up/Enter/Tab/Esc keyboard navigation works.
  • Click outside the dropdown closes it.
  • Accepting a completion replaces the prefix and is undoable with ⌘Z.
  • No SQL executes on typing or on Ctrl+Space.
  • Only already-loaded columns appear; no on-demand fetch triggered.
  • app.state.editorReference being null shows keywords only without error.
  • src/core/editor-autocomplete.js at 100% coverage.
  • src/ui/editor.js changes maintain existing coverage threshold.

Non-goals

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions