fix: debounce @ file autocomplete to prevent TUI freeze on large codebases (#448)
The synchronous fuzzyFind() native call blocks the event loop during @ file autocomplete. On large codebases (e.g. Java projects with deep directory trees), each call can take seconds. Since updateAutocomplete() was called on every keystroke while autocomplete was active, rapid typing would cascade into dozens of blocking searches — freezing the TUI for minutes. This made it appear that arrow keys caused the freeze, when the actual cause was accumulated backlog from processing buffered input. Debounce all @ file reference autocomplete paths (character input, backspace, forward delete, and re-trigger after cancellation) with a 150ms delay so only the final keystroke triggers the expensive search. Slash command autocomplete remains synchronous since it's cheap.
This commit is contained in:
parent
16364c7dba
commit
8b7b8bc651
1 changed files with 71 additions and 6 deletions
|
|
@ -150,6 +150,12 @@ export class Editor implements Component, Focusable {
|
|||
private autocompletePrefix: string = "";
|
||||
private autocompleteMaxVisible: number = 5;
|
||||
|
||||
// Debounce for @ file autocomplete to prevent blocking the event loop
|
||||
// with synchronous fuzzyFind calls on every keystroke
|
||||
private autocompleteDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private lastAutocompleteLookupPrefix: string | null = null;
|
||||
private static readonly AUTOCOMPLETE_DEBOUNCE_MS = 150;
|
||||
|
||||
// Paste tracking for large pastes
|
||||
private pastes: Map<number, string> = new Map();
|
||||
private pasteCounter: number = 0;
|
||||
|
|
@ -965,9 +971,10 @@ export class Editor implements Component, Focusable {
|
|||
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
||||
this.tryTriggerAutocomplete();
|
||||
}
|
||||
// Check if we're in an @ file reference context
|
||||
// Check if we're in an @ file reference context (debounce to avoid
|
||||
// blocking the event loop with synchronous fuzzyFind on every keystroke)
|
||||
else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
||||
this.tryTriggerAutocomplete();
|
||||
this.debouncedTriggerAutocomplete();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -975,6 +982,23 @@ export class Editor implements Component, Focusable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced version of tryTriggerAutocomplete for @ file reference context.
|
||||
* Prevents synchronous fuzzyFind calls from blocking the event loop on every keystroke.
|
||||
*/
|
||||
private debouncedTriggerAutocomplete(): void {
|
||||
if (this.autocompleteDebounceTimer) {
|
||||
clearTimeout(this.autocompleteDebounceTimer);
|
||||
this.autocompleteDebounceTimer = null;
|
||||
}
|
||||
|
||||
this.autocompleteDebounceTimer = setTimeout(() => {
|
||||
this.autocompleteDebounceTimer = null;
|
||||
this.tryTriggerAutocomplete();
|
||||
this.tui.requestRender();
|
||||
}, Editor.AUTOCOMPLETE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
private handlePaste(pastedText: string): void {
|
||||
this.historyIndex = -1; // Exit history browsing mode
|
||||
this.lastAction = null;
|
||||
|
|
@ -1133,9 +1157,9 @@ export class Editor implements Component, Focusable {
|
|||
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
||||
this.tryTriggerAutocomplete();
|
||||
}
|
||||
// @ file reference context
|
||||
// @ file reference context (debounced to avoid blocking event loop)
|
||||
else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
||||
this.tryTriggerAutocomplete();
|
||||
this.debouncedTriggerAutocomplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1440,9 +1464,9 @@ export class Editor implements Component, Focusable {
|
|||
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
||||
this.tryTriggerAutocomplete();
|
||||
}
|
||||
// @ file reference context
|
||||
// @ file reference context (debounced to avoid blocking event loop)
|
||||
else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
||||
this.tryTriggerAutocomplete();
|
||||
this.debouncedTriggerAutocomplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2020,6 +2044,15 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|||
this.autocompleteState = null;
|
||||
this.autocompleteList = undefined;
|
||||
this.autocompletePrefix = "";
|
||||
this.clearAutocompleteDebounce();
|
||||
}
|
||||
|
||||
private clearAutocompleteDebounce(): void {
|
||||
if (this.autocompleteDebounceTimer) {
|
||||
clearTimeout(this.autocompleteDebounceTimer);
|
||||
this.autocompleteDebounceTimer = null;
|
||||
}
|
||||
this.lastAutocompleteLookupPrefix = null;
|
||||
}
|
||||
|
||||
public isShowingAutocomplete(): boolean {
|
||||
|
|
@ -2034,6 +2067,38 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if we're in an @ file reference context — these trigger expensive
|
||||
// synchronous fuzzyFind calls that block the event loop. Debounce them so
|
||||
// rapid typing doesn't cascade into dozens of blocking searches.
|
||||
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
||||
if (this.autocompletePrefix.startsWith("@") || textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
||||
this.debouncedUpdateAutocompleteSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyAutocompleteSuggestions();
|
||||
}
|
||||
|
||||
private debouncedUpdateAutocompleteSuggestions(): void {
|
||||
// Clear any pending debounce
|
||||
if (this.autocompleteDebounceTimer) {
|
||||
clearTimeout(this.autocompleteDebounceTimer);
|
||||
this.autocompleteDebounceTimer = null;
|
||||
}
|
||||
|
||||
this.autocompleteDebounceTimer = setTimeout(() => {
|
||||
this.autocompleteDebounceTimer = null;
|
||||
// Guard: autocomplete may have been cancelled during debounce wait
|
||||
if (!this.autocompleteState || !this.autocompleteProvider) return;
|
||||
this.applyAutocompleteSuggestions();
|
||||
this.tui.requestRender();
|
||||
}, Editor.AUTOCOMPLETE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
private applyAutocompleteSuggestions(): void {
|
||||
if (!this.autocompleteProvider) return;
|
||||
|
||||
const suggestions = this.autocompleteProvider.getSuggestions(
|
||||
this.state.lines,
|
||||
this.state.cursorLine,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue