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:
@@ -14,7 +14,9 @@ import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
|
||||
import { numberToCol, cycleReferenceLocks } from '../engine';
|
||||
import { useDocument } from '../store/document';
|
||||
import { DEFAULT_ROW_H } from '../store/types';
|
||||
import { useFormulaAutocomplete } from '../composables/useFormulaAutocomplete';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
import FormulaSuggest from './FormulaSuggest.vue';
|
||||
|
||||
const props = defineProps<{ tableId: string }>();
|
||||
|
||||
@@ -630,6 +632,7 @@ const editMirrorEl = ref<HTMLDivElement | null>(null);
|
||||
const setEditRef = (el: unknown) => {
|
||||
editInput.value = (el as HTMLTextAreaElement) ?? null;
|
||||
};
|
||||
const autocomplete = useFormulaAutocomplete();
|
||||
let committing = false;
|
||||
|
||||
const editStyle = computed(() => {
|
||||
@@ -711,15 +714,55 @@ function commitEdit() {
|
||||
store.setCellInput(props.tableId, col, row, editValue.value);
|
||||
editing.value = null;
|
||||
store.registerFormulaEditor(null);
|
||||
autocomplete.close();
|
||||
focusRoot();
|
||||
setTimeout(() => (committing = false), 0);
|
||||
}
|
||||
function cancelEdit() {
|
||||
editing.value = null;
|
||||
store.registerFormulaEditor(null);
|
||||
autocomplete.close();
|
||||
focusRoot();
|
||||
}
|
||||
/** Accept the highlighted autocomplete suggestion (name + "(") via the same
|
||||
* replace() mechanism point-mode uses. */
|
||||
function applySuggestion(el: HTMLTextAreaElement) {
|
||||
const caret = el.selectionStart ?? editValue.value.length;
|
||||
const rep = autocomplete.accept(editValue.value, caret);
|
||||
if (!rep) return false;
|
||||
editValue.value = editValue.value.slice(0, rep.start) + rep.value + editValue.value.slice(rep.end);
|
||||
autocomplete.close();
|
||||
nextTick(() => {
|
||||
el.setSelectionRange(rep.start + rep.value.length, rep.start + rep.value.length);
|
||||
resizeEditor();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
function onEditKeydown(e: KeyboardEvent) {
|
||||
// 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();
|
||||
e.stopPropagation();
|
||||
autocomplete.move(e.key === 'ArrowDown' ? 1 : -1);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Tab' || e.key === 'Enter') {
|
||||
const el = editInput.value;
|
||||
if (el && applySuggestion(el)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
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, e.g. for Control+Option+Return, the
|
||||
// traditional Mac Excel shortcut, or some layouts/browsers, so `altKey`
|
||||
@@ -778,12 +821,22 @@ function toggleReferenceLock() {
|
||||
}
|
||||
function onEditBlur() {
|
||||
if (!committing && editing.value) commitEdit();
|
||||
autocomplete.close();
|
||||
}
|
||||
/** Actual typing (not a point-mode click inserting a reference programmatically)
|
||||
* ends the "next click replaces the last reference" window. */
|
||||
function onEditInput() {
|
||||
resizeEditor();
|
||||
store.noteFormulaTyped();
|
||||
const el = editInput.value;
|
||||
autocomplete.update(editValue.value, el?.selectionStart ?? editValue.value.length);
|
||||
}
|
||||
/** A mouse click on a dropdown entry (see FormulaSuggest's `@mousedown.prevent`
|
||||
* keeping focus in the textarea, so `editInput` is still the live editor). */
|
||||
function onPickSuggestion(i: number) {
|
||||
autocomplete.activeIndex.value = i;
|
||||
const el = editInput.value;
|
||||
if (el) applySuggestion(el);
|
||||
}
|
||||
/** Clicking inside the field to reposition the caret should also end that
|
||||
* window (mousedown so it fires before any selection change is read). */
|
||||
@@ -1072,19 +1125,25 @@ defineExpose({
|
||||
title="Ziehen zum Ausfüllen/Kopieren"
|
||||
@mousedown="onFillHandleDown"
|
||||
/>
|
||||
<textarea
|
||||
v-if="editing"
|
||||
:ref="setEditRef"
|
||||
v-model="editValue"
|
||||
class="cell-input"
|
||||
:style="editStyle"
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
@keydown="onEditKeydown"
|
||||
@input="onEditInput"
|
||||
@blur="onEditBlur"
|
||||
@mousedown="onEditMousedown"
|
||||
/>
|
||||
<div v-if="editing" class="cell-input-wrap" :style="editStyle">
|
||||
<textarea
|
||||
:ref="setEditRef"
|
||||
v-model="editValue"
|
||||
class="cell-input"
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
@keydown="onEditKeydown"
|
||||
@input="onEditInput"
|
||||
@blur="onEditBlur"
|
||||
@mousedown="onEditMousedown"
|
||||
/>
|
||||
<FormulaSuggest
|
||||
v-if="autocomplete.suggestions.value.length"
|
||||
:items="autocomplete.suggestions.value"
|
||||
:active-index="autocomplete.activeIndex.value"
|
||||
@pick="onPickSuggestion"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="editing" ref="editMirrorEl" class="cell-input-mirror" :style="editMirrorStyle" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
@@ -1266,9 +1325,14 @@ defineExpose({
|
||||
child of its <td>, so it's free to grow past the cell (request: a longer
|
||||
formula gets a box sized to its content / the display's max width, not
|
||||
squeezed into the cell). Sized imperatively by resizeEditor(); left/top and
|
||||
the starting min-width/min-height come from editStyle. */
|
||||
.cell-input {
|
||||
the starting min-width/min-height come from editStyle. Positioning lives
|
||||
on this wrapper (not .cell-input itself) so FormulaSuggest's dropdown,
|
||||
`position: absolute; top: 100%`, lands right under the textarea. */
|
||||
.cell-input-wrap {
|
||||
position: absolute;
|
||||
z-index: 12;
|
||||
}
|
||||
.cell-input {
|
||||
resize: none;
|
||||
border: 2px solid var(--accent);
|
||||
padding: 0 5px;
|
||||
@@ -1281,7 +1345,6 @@ defineExpose({
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
box-sizing: border-box;
|
||||
z-index: 12;
|
||||
}
|
||||
/* hidden clone of .cell-input's text box, measured to size the real one --
|
||||
same font/line-height/padding/border, capped at EDIT_MAX_W so it wraps at
|
||||
|
||||
Reference in New Issue
Block a user