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:
@@ -2423,6 +2423,109 @@ describe('formula "point mode" (click/drag a cell to insert a reference)', () =>
|
||||
});
|
||||
});
|
||||
|
||||
describe('Name Box (jumpToNameInput / nameBoxSuggestions)', () => {
|
||||
it('a bare cell/range navigates on the currently active table', () => {
|
||||
const s = fresh();
|
||||
s.setSelection({ tableId: tableId(s, 'Umsatz'), anchor: coords('A1'), focus: coords('A1') });
|
||||
expect(s.jumpToNameInput('B3')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({
|
||||
tableId: tableId(s, 'Umsatz'),
|
||||
anchor: coords('B3'),
|
||||
focus: coords('B3'),
|
||||
});
|
||||
expect(s.jumpToNameInput('B2:D4')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({ anchor: coords('B2'), focus: coords('D4') });
|
||||
});
|
||||
|
||||
it('a qualified reference ("Tabelle::A1") jumps to a different table, switching sheet/object', () => {
|
||||
const s = fresh();
|
||||
expect(s.jumpToNameInput('Kosten::B2')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({
|
||||
tableId: tableId(s, 'Kosten'),
|
||||
anchor: coords('B2'),
|
||||
focus: coords('B2'),
|
||||
});
|
||||
expect(s.doc.activeObjectId).toBe(tableId(s, 'Kosten'));
|
||||
});
|
||||
|
||||
it('a named range name jumps to and selects its target range', () => {
|
||||
const s = fresh();
|
||||
expect(s.jumpToNameInput('Umsatz_Q1')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({
|
||||
tableId: tableId(s, 'Umsatz'),
|
||||
anchor: coords('B2'),
|
||||
focus: coords('B6'),
|
||||
});
|
||||
});
|
||||
|
||||
it('a qualified reference works when the table name has non-ASCII letters', () => {
|
||||
// regression: the lexer used to reject "Ü" outright (an unquoted
|
||||
// qualifier is tokenized the same as any other identifier), so
|
||||
// "Übersicht::B4" threw instead of parsing -- see engine/lexer.ts.
|
||||
const s = fresh();
|
||||
expect(s.jumpToNameInput('Übersicht::B4')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({
|
||||
tableId: tableId(s, 'Übersicht'),
|
||||
anchor: coords('B4'),
|
||||
focus: coords('B4'),
|
||||
});
|
||||
});
|
||||
|
||||
it('a bare table name selects the whole table', () => {
|
||||
const s = fresh();
|
||||
const id = tableId(s, 'Kosten');
|
||||
const t = s.tableById(id)!;
|
||||
expect(s.jumpToNameInput('Kosten')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({
|
||||
tableId: id,
|
||||
anchor: { col: 0, row: 0 },
|
||||
focus: { col: t.cols - 1, row: t.rows - 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('a bare sheet name switches to that sheet', () => {
|
||||
const s = fresh();
|
||||
expect(s.doc.activeSheetId).not.toBe(s.doc.sheets[1].id);
|
||||
expect(s.jumpToNameInput('Blatt 2')).toBe(true);
|
||||
expect(s.doc.activeSheetId).toBe(s.doc.sheets[1].id);
|
||||
});
|
||||
|
||||
it('garbage input, an unknown name, or a non-reference formula (e.g. a function call) resolves to nothing', () => {
|
||||
const s = fresh();
|
||||
const before = s.doc.selection;
|
||||
expect(s.jumpToNameInput('')).toBe(false);
|
||||
expect(s.jumpToNameInput(' ')).toBe(false);
|
||||
expect(s.jumpToNameInput('NichtVorhanden')).toBe(false);
|
||||
expect(s.jumpToNameInput('SUM(A1;A2)')).toBe(false); // navigates, doesn't evaluate
|
||||
expect(s.jumpToNameInput('1+1')).toBe(false);
|
||||
expect(s.doc.selection).toEqual(before); // no partial/incidental navigation on failure
|
||||
});
|
||||
|
||||
it('out-of-bounds coordinates clamp into the target table instead of erroring', () => {
|
||||
const s = fresh();
|
||||
const id = tableId(s, 'Umsatz');
|
||||
const t = s.tableById(id)!;
|
||||
expect(s.jumpToNameInput('Z999')).toBe(true);
|
||||
expect(s.doc.selection).toMatchObject({
|
||||
tableId: id,
|
||||
anchor: { col: t.cols - 1, row: t.rows - 1 },
|
||||
focus: { col: t.cols - 1, row: t.rows - 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('nameBoxSuggestions offers named ranges, then tables, then sheets, prefix-matched', () => {
|
||||
const s = fresh();
|
||||
expect(s.nameBoxSuggestions('')).toEqual([]); // empty prefix -> no suggestions
|
||||
const hits = s.nameBoxSuggestions('u');
|
||||
expect(hits.map((h) => h.name)).toEqual(['Umsatz_Q1', 'Umsatz']); // namedrange before table
|
||||
expect(hits[0]).toMatchObject({ kind: 'namedrange', hint: 'Umsatz!B2:B6' });
|
||||
expect(hits[1]).toMatchObject({ kind: 'table' });
|
||||
|
||||
const blattHits = s.nameBoxSuggestions('blatt');
|
||||
expect(blattHits).toEqual([{ name: 'Blatt 2', hint: 'Blatt', kind: 'sheet' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-sheet formulas: sheet qualifier + per-sheet table name uniqueness', () => {
|
||||
it('renaming a table to a name already used on a DIFFERENT sheet is now allowed', () => {
|
||||
const s = fresh();
|
||||
|
||||
Reference in New Issue
Block a user