feat: Type-Ahead-Folge tippen (A-M-Y springt zu Amy Spring)

Buchstaben akkumulieren sich jetzt statt neu zu starten: 'A'+'M'+'Y'
findet 'Amy Spring'. Präfix-Treffer haben Vorrang, danach Teiltreffer.
Doppelter Buchstabe (AA) springt zum nächsten a-Eintrag wie Explorer.
Folge wird verworfen, wenn sie keinen Treffer mehr hat, und nach >1s
Pause neu gestartet. Build auf 2026.09.12.r002.
This commit is contained in:
2026-09-12 12:29:37 +02:00
parent 5b929a9ee8
commit 4afacca2fb
4 changed files with 36 additions and 15 deletions
+33 -12
View File
@@ -32,6 +32,7 @@ const state = {
settingsPanelVisible: false,
typeAhead: {
key: '',
sequence: '',
windowId: null,
at: 0
},
@@ -2360,15 +2361,27 @@ function handleTypeAheadKey(event) {
if (!win || !win.data.entries?.length) return;
const now = Date.now();
const repeated = state.typeAhead.windowId === win.id && state.typeAhead.key === key && now - state.typeAhead.at < 1000;
const sameWindow = state.typeAhead.windowId === win.id;
const withinWindow = now - state.typeAhead.at < 1000;
const sequence = sameWindow && withinWindow ? (state.typeAhead.sequence || '') : '';
// Gleicher Buchstabe kurz nach einander = nächstes Vorkommen dieses Buchstabens
// (z. B. "AA" springt zum zweiten A-Eintrag), statt die Folge zu verdoppeln.
const repeated = sameWindow && withinWindow && state.typeAhead.key === key && sequence.length <= 1;
let next = repeated ? key : sequence + key;
if (!repeated && next.length > 16) next = key;
const entries = win.data.entries;
const selectedIndex = entries.findIndex((entry) => entry.path === win.data.selectedPath);
const startIndex = repeated ? selectedIndex + 1 : Math.max(0, selectedIndex + 1);
const match = findTypeAheadMatch(entries, key, startIndex);
if (!match) return;
const startIndex = Math.max(0, selectedIndex + 1);
const match = findTypeAheadMatch(entries, next, startIndex, repeated);
if (!match) {
// Unvollständiger Pfad ohne Treffer: Auswahl bleibt, Folge wird verworfen
if (sequence) state.typeAhead = { key, sequence: '', windowId: win.id, at: now };
return;
}
event.preventDefault();
state.typeAhead = { key, windowId: win.id, at: now };
state.typeAhead = { key, sequence: next, windowId: win.id, at: now };
win.data.selectedPath = match.path;
win.data.selectedPaths = [match.path];
win.data.selectionAnchorPath = match.path;
@@ -2377,15 +2390,23 @@ function handleTypeAheadKey(event) {
updateSelectedRows();
updatePropertiesWindows();
scrollEntryIntoView(win, match.path);
recordDebug('typeahead.match', { key, path: match.path });
recordDebug('typeahead.match', { sequence: next, path: match.path });
}
function findTypeAheadMatch(entries, key, startIndex) {
for (let offset = 0; offset < entries.length; offset += 1) {
const index = (startIndex + offset) % entries.length;
const entry = entries[index];
if (entry.name?.toLocaleLowerCase().startsWith(key)) {
return entry;
function findTypeAheadMatch(entries, sequence, startIndex, preferNextOccurrence = false) {
// Erst: Präfix-Treffer ab der aktuellen Position (mit Wraparound).
// Zweit: Teiltreffer (z. B. "amy" findet "Amy Spring"), damit Tippfolgen auch bei
// Präfix-Konkurrenz zum richtigen Eintrag springen.
for (let pass = 0; pass < 2; pass += 1) {
for (let offset = 0; offset < entries.length; offset += 1) {
const index = (startIndex + offset) % entries.length;
const entry = entries[index];
const name = entry.name?.toLocaleLowerCase();
if (!name) continue;
const isMatch = pass === 0
? name.startsWith(sequence)
: name.includes(sequence) && !(preferNextOccurrence && sequence.length <= 1);
if (isMatch) return entry;
}
}
return null;