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 -