Make the Name Box interactive: jump-to-reference + table/sheet/named-range autocomplete

The top-left Name Box was a plain read-only display of the current
selection ("Umsatz::B2:E6"). It's now an editable input that navigates
on Enter, using the same reference syntax a formula would: a bare cell
or range on the active table, a table/sheet-qualified reference
(Tabelle::A1, Blatt!Tabelle::A1:B5), a named range's name, or -- beyond
what's valid formula syntax -- a bare table name (selects the whole
table) or bare sheet name (switches to it).

Autocompletes against named ranges, table names, and sheet names as you
type (Tab accepts the highlighted suggestion, click jumps there
directly); an unresolvable entry reports via a toast and leaves the text
selected for a quick retry rather than silently reverting.

document.ts gains two new functions (jumpToNameInput, nameBoxSuggestions)
built on the existing parseFormula/resolveTableName/resolveNamedRange
machinery already used elsewhere in the store -- no engine changes.
FormulaSuggest.vue's prop type was loosened so the same dropdown
component serves both this and the existing formula function-name
autocomplete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-09 23:54:01 +02:00
co-authored by Claude Sonnet 5
parent d0500fcaf5
commit 06fea34f82
6 changed files with 380 additions and 17 deletions
+113
View File
@@ -68,6 +68,7 @@ import type {
DocumentState,
FnSuggestion,
Macro,
NameSuggestion,
Sheet,
SelectionState,
SheetObject,
@@ -2590,6 +2591,116 @@ function createStore() {
.map((f) => ({ name: f.name, hint: `${f.name}(${f.params.join('; ')})`, kind: 'custom' }));
return [...builtins, ...custom].sort((a, b) => a.name.localeCompare(b.name));
}
/** Name Box autocomplete candidates for a prefix already typed -- named
* ranges, table names (any sheet), then sheet names, each group
* case-insensitively prefix-matched and name-sorted, in that priority
* order (matching `jumpToNameInput`'s own resolution order below). Empty
* prefix -> no suggestions, same convention as `functionSuggestions`. */
function nameBoxSuggestions(prefix: string): NameSuggestion[] {
const p = prefix.trim().toLowerCase();
if (!p) return [];
const namedRanges: NameSuggestion[] = doc.namedRanges
.filter((r) => r.name.toLowerCase().startsWith(p))
.map((r) => {
const t = tableById(r.tableId);
const addr =
r.start.col === r.end.col && r.start.row === r.end.row
? toA1(r.start.col, r.start.row)
: `${toA1(r.start.col, r.start.row)}:${toA1(r.end.col, r.end.row)}`;
return { name: r.name, hint: `${t?.name ?? '?'}!${addr}`, kind: 'namedrange' as const };
})
.sort((a, b) => a.name.localeCompare(b.name));
const tables: NameSuggestion[] = allTables(doc)
.filter((t) => t.name.toLowerCase().startsWith(p))
.map((t) => {
const sheetId = sheetIdOfObject(t.id);
const sheetName = (sheetId && sheetById(sheetId)?.name) || '?';
return { name: t.name, hint: `Tabelle · Blatt „${sheetName}"`, kind: 'table' as const };
})
.sort((a, b) => a.name.localeCompare(b.name));
const sheets: NameSuggestion[] = doc.sheets
.filter((s) => s.name.toLowerCase().startsWith(p))
.map((s) => ({ name: s.name, hint: 'Blatt', kind: 'sheet' as const }))
.sort((a, b) => a.name.localeCompare(b.name));
return [...namedRanges, ...tables, ...sheets];
}
/**
* Resolves free-form Name Box input and navigates to it -- same syntax a
* formula reference would use (`Tabelle::A1`, `Blatt!Tabelle::A1:B5`, a
* bare `A1`/`A1:B5` meaning "on the currently active table", or a named
* range's own name), falling back to a bare **table** name (selects the
* whole table) or a bare **sheet** name (switches to it) -- neither of
* those two is valid formula syntax on its own, so they're only tried
* once the reference parse produced nothing usable. Returns whether it
* found something to jump to; the caller (the Name Box itself) reports a
* failure to the user.
*/
function jumpToNameInput(text: string): boolean {
const trimmed = text.trim();
if (!trimmed) return false;
const home = doc.selection?.tableId ?? activeTable.value?.id ?? null;
let node: ReturnType<typeof parseFormula> | null = null;
try {
node = parseFormula(trimmed);
} catch {
node = null;
}
if (node) {
let qualifier: string | null = null;
let sheetQualifier: string | undefined;
let start: { col: number; row: number } | null = null;
let end: { col: number; row: number } | null = null;
if (node.kind === 'cell') {
qualifier = node.qualifier;
sheetQualifier = node.sheetQualifier;
start = end = { col: node.col, row: node.row };
} else if (node.kind === 'range') {
qualifier = node.qualifier;
sheetQualifier = node.sheetQualifier;
start = node.start;
end = node.end;
} else if (node.kind === 'namedref') {
const r = wb.resolveNamedRange(node.name);
if (r) {
qualifier = r.qualifier;
sheetQualifier = r.sheetQualifier;
start = r.start;
end = r.end;
}
}
if (start && end) {
const tableId = qualifier === null ? home : wb.resolveTableName(qualifier, sheetQualifier ?? null);
const t = tableId ? tableById(tableId) : undefined;
if (t) {
const clampC = (c: number) => Math.max(0, Math.min(t.cols - 1, c));
const clampR = (r: number) => Math.max(0, Math.min(t.rows - 1, r));
setSelection({
tableId: t.id,
anchor: { col: clampC(start.col), row: clampR(start.row) },
focus: { col: clampC(end.col), row: clampR(end.row) },
});
return true;
}
}
}
// Not a (resolvable) reference -- try a bare table or sheet name.
const tableId = wb.resolveTableName(trimmed, null);
const t = tableId ? tableById(tableId) : undefined;
if (t) {
setSelection({ tableId: t.id, anchor: { col: 0, row: 0 }, focus: { col: t.cols - 1, row: t.rows - 1 } });
return true;
}
const sheet = sheetByName(trimmed);
if (sheet) {
setActiveSheet(sheet.id);
return true;
}
return false;
}
function qualifyRef(homeTableId: string, targetTableId: string, body: string): string {
if (targetTableId === homeTableId) return body;
const target = tableById(targetTableId);
@@ -2912,6 +3023,8 @@ function createStore() {
noteFormulaTyped,
formulaPointModeActive,
functionSuggestions,
nameBoxSuggestions,
jumpToNameInput,
pointClick,
pointDrag,
// document