From 0b7602b874655b7721602e4534ceda03e13bc705 Mon Sep 17 00:00:00 2001 From: Wild Karl-Heinz Date: Thu, 3 Sep 2026 08:43:57 +0200 Subject: [PATCH] =?UTF-8?q?Funktions-Autovervollst=C3=A4ndigung=20in=20der?= =?UTF-8?q?=20Formelbearbeitung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 37 +++++++-- src/components/FormulaBar.vue | 88 +++++++++++++++++---- src/components/FormulaSuggest.vue | 70 +++++++++++++++++ src/components/TableGrid.vue | 95 +++++++++++++++++++---- src/composables/useFormulaAutocomplete.ts | 49 ++++++++++++ src/engine/README.md | 14 +++- src/engine/__tests__/functions.spec.ts | 18 +++++ src/engine/__tests__/refs.spec.ts | 37 ++++++++- src/engine/functions.ts | 21 +++++ src/engine/index.ts | 3 +- src/engine/refs.ts | 43 ++++++++++ src/store/__tests__/document.spec.ts | 30 +++++++ src/store/document.ts | 20 +++++ src/store/types.ts | 9 +++ 14 files changed, 493 insertions(+), 41 deletions(-) create mode 100644 src/components/FormulaSuggest.vue create mode 100644 src/composables/useFormulaAutocomplete.ts create mode 100644 src/engine/__tests__/functions.spec.ts diff --git a/README.md b/README.md index 59f94e7..704fbe3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt: ```bash npm install npm run dev # Vite-Devserver -npm run test # Vitest-Suite (Engine + Store + IO), 318 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 331 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -102,6 +102,7 @@ umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt | **`Strg`/`Cmd`+`Z`** · **`Strg`/`Cmd`+`Y`** / **`Strg`/`Cmd`+`Shift`+`Z`** | Rückgängig · Wiederholen (auch als Symbolleisten-Buttons ↶ ↷) | | **`F4`** (im Editor/in der Formelleiste) | Bezug am Cursor sperren/zyklisch umschalten: `A1` → `$A$1` → `A$1` → `$A1` → `A1`; bei markiertem Text werden **alle** darin gefundenen Bezüge auf einmal umgeschaltet, jeder ausgehend vom eigenen Zustand | | **`Alt`+`Enter`** bzw. **`Cmd`+`Enter`** (im Editor) | Zeilenumbruch in der Zelle einfügen, statt zu committen — `Cmd`+`Enter` als Mac-Fallback, da `⌥` (Option) dort eine Compose-Taste ist und nicht überall zuverlässig als reines Modifier ankommt | +| `↑`/`↓`/`Tab`/`Enter`/`Esc` (bei offenem Funktions-Dropdown) | navigieren / Vorschlag übernehmen / nur das Dropdown schließen — hat Vorrang vor der sonstigen Bedeutung dieser Tasten (s. u. „Funktions-Autovervollständigung") | Bei **festen (scrollbaren) Tabellen** wird die aktive Zelle bei jeder Navigation automatisch in den sichtbaren Bereich gescrollt (unter Beachtung fixierter @@ -400,6 +401,15 @@ verschiebt sich die referenzierte Zelle selbst, also folgt jede Referenz darauf, `$` hin oder her. `F4` schaltet den Bezug unter dem Cursor durch die vier Kombinationen. +**Funktions-Autovervollständigung**: sobald in der Formelleiste oder im +In-Zellen-Editor ein Funktionsname zu tippen beginnt (nach `=`, einem +Operator, `(` oder `;` — nicht bei einem Zellbezug wie `A1`), erscheint ein +Dropdown mit den passenden eingebauten **und** eigenen Funktionen samt +Kurzsignatur (z. B. `XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; …)`). +`↑`/`↓` navigieren, `Tab`/`Enter` oder ein Klick übernehmen den markierten +Vorschlag (fügt `NAME(` ein, bereit für die Argumente), `Esc` schließt nur +das Dropdown, ohne die Bearbeitung selbst abzubrechen. + **Funktionen** (case-insensitiv, Trenner `;`): `SUM`, `AVG` (Alias `AVERAGE`), `MIN`, `MAX`, `COUNT`, `IF(Bedingung; Dann; Sonst?)`, `DATE(Jahr; Monat; Tag)`, `TIME(Std; Min; Sek)`, `TODAY()`, `NOW()`, `XLOOKUP(Suchwert; @@ -593,7 +603,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 318 Tests: +`npm run test` — Vitest, 331 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -679,6 +689,14 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, hängt er einen neuen an; Ziehen fügt/erweitert einen Bereichsbezug; ein Klick in eine andere Tabelle qualifiziert mit deren Namen (Quoting bei Leerzeichen); ohne Formel im Puffer greift nichts. +- **Funktions-Autovervollständigung** — Engine (`refs.spec.ts`): + `currentIdentifierAtCaret` erkennt den getippten Namen nach `=`/Operator/`(`/ + `;`, ignoriert Zellbezüge (`A1`) und bereits abgeschlossene Aufrufe (`SUM(`), + schaut nie über den Cursor hinaus, verkraftet einen kaputten Formel-Präfix + ohne zu werfen. Store (`document.spec.ts`): `functionSuggestions` filtert + case-insensitiv, mischt eingebaute und eigene Funktionen sortiert. + `functions.spec.ts`: die Signatur-Tabelle deckt sich exakt mit den + registrierten eingebauten Funktionen (kein künftiges Vergessen möglich). - **Benutzerdefinierte Funktionen** — Engine-Ebene (`userFunctions.spec.ts`): Parametersubstitution, verschachtelte Aufrufe (eingebaut + benutzerdefiniert, auch bei kollidierenden Parameternamen zwischen den Scopes), Arity-Mismatch → @@ -783,10 +801,17 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, Argument (`;;`) ist eine allgemeine Grammatik-Erweiterung, nicht XLOOKUP-spezifisch — funktioniert in jeder Funktion. - **Benutzerdefinierte Funktionen** (Phase 1, siehe oben) bewusst ohne: - Autovervollständigen des Funktionsnamens im Editor, Live-Vorschau des - Ergebnisses im Bearbeiten-Dialog, variadische Parameter/Default-Werte, und - automatisches Nachführen bestehender Aufrufe bei Umbenennung (Verhalten - entspricht bewusst dem einer Tabellen-Umbenennung, s. o.). + Live-Vorschau des Ergebnisses im Bearbeiten-Dialog, variadische Parameter/ + Default-Werte, und automatisches Nachführen bestehender Aufrufe bei + Umbenennung (Verhalten entspricht bewusst dem einer Tabellen-Umbenennung, + s. o.) — der Funktionsname selbst wird beim Formeltippen aber inzwischen + vervollständigt (s. o. „Funktions-Autovervollständigung"), auch für eigene + Funktionen. +- **Funktions-Autovervollständigung** bewusst ohne: Zell-/Tabellennamen- + Vorschläge (dafür existiert der Point-Mode, s. o.), Vervollständigung, die + auch Text *nach* dem Cursor berücksichtigt (nur der Text bis zum Cursor + zählt — wie in den meisten Editoren), Live-Update der Vorschlagsliste bei + reiner Cursor-Bewegung ohne Tippen. - **„Mit Schleife"-Code** bewusst ohne: verkettete Vergleiche (`a < b < c`, nur ein Vergleich pro Ausdruck), Text-Vergleich (Vergleichsoperatoren bleiben numerisch), `AND`/`OR`/`NOT` für zusammengesetzte Bedingungen, diff --git a/src/components/FormulaBar.vue b/src/components/FormulaBar.vue index eead335..f673c6e 100644 --- a/src/components/FormulaBar.vue +++ b/src/components/FormulaBar.vue @@ -2,11 +2,14 @@ import { nextTick, ref, watch } from 'vue'; import { useDocument } from '../store/document'; import { cycleReferenceLocks } from '../engine'; +import { useFormulaAutocomplete } from '../composables/useFormulaAutocomplete'; +import FormulaSuggest from './FormulaSuggest.vue'; const store = useDocument(); const draft = ref(''); const focused = ref(false); const fieldEl = ref(null); +const autocomplete = useFormulaAutocomplete(); // keep the field in sync with the active cell unless the user is editing it watch( @@ -54,6 +57,7 @@ function onBlur() { focused.value = false; commit(); store.registerFormulaEditor(null); + autocomplete.close(); } function onEscape() { draft.value = store.activeCellInput.value; @@ -61,8 +65,28 @@ function onEscape() { } /** Actual typing (not a point-mode click inserting a reference programmatically) * ends the "next click replaces the last reference" window. */ -function onInput() { +function onInput(e: Event) { store.noteFormulaTyped(); + const el = e.target as HTMLTextAreaElement; + autocomplete.update(draft.value, el.selectionStart ?? draft.value.length); +} +/** Accept the highlighted autocomplete suggestion (name + "(") via the same + * replace() mechanism point-mode uses. */ +function applySuggestion(el: HTMLTextAreaElement) { + const caret = el.selectionStart ?? draft.value.length; + const rep = autocomplete.accept(draft.value, caret); + if (!rep) return false; + draft.value = draft.value.slice(0, rep.start) + rep.value + draft.value.slice(rep.end); + autocomplete.close(); + nextTick(() => el.setSelectionRange(rep.start + rep.value.length, rep.start + rep.value.length)); + return true; +} +/** A mouse click on a dropdown entry (see FormulaSuggest's `@mousedown.prevent` + * keeping focus in the textarea, so `fieldEl` is still the live editor). */ +function onPickSuggestion(i: number) { + autocomplete.activeIndex.value = i; + const el = fieldEl.value; + if (el) applySuggestion(el); } /** Clicking inside the field to reposition the caret should also end that window. */ function onMousedown() { @@ -81,6 +105,25 @@ function onF4(el: HTMLTextAreaElement) { } function onKeydown(e: KeyboardEvent) { const el = e.target as HTMLTextAreaElement; + // Function-name autocomplete takes priority over every other key below + // while its dropdown is open (see useFormulaAutocomplete.ts). + if (autocomplete.suggestions.value.length) { + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault(); + autocomplete.move(e.key === 'ArrowDown' ? 1 : -1); + return; + } + if (e.key === 'Tab' || e.key === 'Enter') { + e.preventDefault(); + if (applySuggestion(el)) return; + } + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + autocomplete.close(); + return; + } + } // Alt+Enter (Windows/Excel-365-Mac) or Cmd+Enter (a macOS-safe fallback -- // Option is a compose key there, so `altKey` alone isn't fully reliable; // `e.code` sidesteps `key` being altered by that composition). @@ -112,20 +155,28 @@ function onKeydown(e: KeyboardEvent) { {{ store.selectionRangeName.value }} fx -