Funktions-Autovervollständigung in der Formelbearbeitung
Beim Tippen eines Funktionsnamens (nach "=", einem Operator, "(" oder
";" -- nicht bei einem Zellbezug wie A1) zeigt sowohl die Formelleiste
als auch der In-Zellen-Editor jetzt ein Dropdown mit passenden
eingebauten UND eigenen Funktionen samt Kurzsignatur. ↑/↓ navigieren,
Tab/Enter/Klick übernehmen (fügt "NAME(" ein), Esc schließt nur das
Dropdown, ohne die Bearbeitung abzubrechen.
- engine/refs.ts: neue reine Funktion currentIdentifierAtCaret() --
strukturell wie das schon vorhandene cycleReferenceLocks (=
abstreifen, tokenize, am Cursor nachsehen), aber nur der Text bis
zum Cursor wird tokenisiert und nur das letzte Token zählt; ein
IDENT, das bis an den Cursor reicht und nicht wie ein Zellbezug
aussieht, ist der Funktionsname. tokenize() hängt ein EOF-Token an
-- muss beim Suchen des "letzten Tokens" herausgefiltert werden
(von den eigenen Tests aufgedeckt).
- engine/functions.ts: BUILTIN_SIGNATURES (Name -> Kurzsignatur je
eingebauter Funktion), gegen FUNCTIONS' Keys testabgesichert, damit
eine künftige neue Funktion ohne Eintrag hier nicht still im
Dropdown fehlt.
- store/document.ts: functionSuggestions(prefix) kombiniert eingebaute
+ dokumenteigene Funktionen, case-insensitiv gefiltert, sortiert.
- composables/useFormulaAutocomplete.ts: pro Editor-Instanz eigener
State (suggestions/activeIndex), orchestriert die drei Bausteine
oben; die Übernahme eines Vorschlags läuft über dieselbe
replace(start,end,value)-Mechanik, die Point-Mode für den Bezug-
Einfügeklick schon nutzt -- keine neue Einfüge-Infrastruktur nötig.
- components/FormulaSuggest.vue: geteiltes, rein präsentationelles
Dropdown (position:absolute; top:100% relativ zum Editor-Wrapper,
kein Caret-Pixel-Tracking).
- FormulaBar.vue + TableGrid.vue: gleiches Einbindungsmuster in
beiden Editoren (Wrapper bekommt position:relative, onInput ruft
autocomplete.update(), onKeydown fängt Pfeiltasten/Tab/Enter/Esc
vor der bestehenden Logik ab, wenn Vorschläge offen sind, onBlur
schließt das Dropdown).
Tests: 13 neue (refs.spec.ts: 7 Fälle für currentIdentifierAtCaret;
functions.spec.ts neu: Signatur-Tabelle deckungsgleich mit FUNCTIONS;
document.spec.ts: functionSuggestions Filter/Mischung/Sortierung).
318 -> 331 Tests, alle grün. vue-tsc/eslint/build sauber. Live per
Playwright verifiziert in Formelleiste UND In-Zellen-Editor: Filterung,
Pfeiltasten-Navigation, Tab-Übernahme, Maus-Klick-Übernahme, Esc
schließt nur das Dropdown, eigene Funktion (MARGE) erscheint neben
eingebauten (MAX) korrekt einsortiert.
READMEs aktualisiert (neuer Abschnitt "Funktions-Autovervollständigung",
Tastaturtabelle, Testliste, Bekannte Grenzen -- dabei eine veraltete
Aussage korrigiert, die Autovervollständigung für benutzerdefinierte
Funktionen noch als "bewusst fehlend" auflistete).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FUNCTIONS, BUILTIN_SIGNATURES } from '../functions';
|
||||
|
||||
describe('BUILTIN_SIGNATURES', () => {
|
||||
it('has exactly one entry per registered built-in function, names matching', () => {
|
||||
const registered = Object.keys(FUNCTIONS).sort();
|
||||
const signatures = BUILTIN_SIGNATURES.map((s) => s.name).sort();
|
||||
// A future new built-in without a matching hint entry fails loudly here
|
||||
// instead of just silently being missing from the autocomplete dropdown.
|
||||
expect(signatures).toEqual(registered);
|
||||
});
|
||||
|
||||
it('every hint actually starts with its own name', () => {
|
||||
for (const s of BUILTIN_SIGNATURES) {
|
||||
expect(s.hint.startsWith(s.name)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseA1, toA1, cycleReferenceLocks } from '../refs';
|
||||
import { parseA1, toA1, cycleReferenceLocks, currentIdentifierAtCaret } from '../refs';
|
||||
|
||||
describe('parseA1 / toA1: $-locked (absolute/mixed) references', () => {
|
||||
it('parseA1 reports which axes are $-locked', () => {
|
||||
@@ -86,3 +86,38 @@ describe('cycleReferenceLocks (F4) -- a real text selection spanning several ref
|
||||
expect(cycleReferenceLocks('=SUM(1;2)', 4, 8)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentIdentifierAtCaret -- function-name autocomplete detection', () => {
|
||||
it('right after "=", right after an operator, right after "(" or ";"', () => {
|
||||
expect(currentIdentifierAtCaret('=S', 2)).toEqual({ text: 'S', start: 1 });
|
||||
expect(currentIdentifierAtCaret('=A1+S', 5)).toEqual({ text: 'S', start: 4 });
|
||||
expect(currentIdentifierAtCaret('=SUM(A', 6)).toEqual({ text: 'A', start: 5 });
|
||||
expect(currentIdentifierAtCaret('=SUM(1;A', 8)).toEqual({ text: 'A', start: 7 });
|
||||
});
|
||||
|
||||
it('null outside a formula (no leading "=")', () => {
|
||||
expect(currentIdentifierAtCaret('SUM', 3)).toBeNull();
|
||||
});
|
||||
|
||||
it('null once the word looks like a cell reference (has a digit)', () => {
|
||||
expect(currentIdentifierAtCaret('=SUM(A1', 7)).toBeNull();
|
||||
expect(currentIdentifierAtCaret('=A1', 3)).toBeNull();
|
||||
});
|
||||
|
||||
it('null right after an already-open, complete function call', () => {
|
||||
expect(currentIdentifierAtCaret('=SUM(', 5)).toBeNull();
|
||||
});
|
||||
|
||||
it('null once the caret has moved past the word (trailing whitespace)', () => {
|
||||
expect(currentIdentifierAtCaret('=SUM ', 5)).toBeNull();
|
||||
});
|
||||
|
||||
it('only looks at text up to the caret, ignoring what follows it', () => {
|
||||
// caret between "SUM" and "X" in "SUMX" -- same result as if "X" weren't there
|
||||
expect(currentIdentifierAtCaret('=SUMX', 4)).toEqual({ text: 'SUM', start: 1 });
|
||||
});
|
||||
|
||||
it('null on a malformed prefix (unterminated string) rather than throwing', () => {
|
||||
expect(currentIdentifierAtCaret('=SUM("x', 7)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user