A couple of autocomplete improvements (#1024)

* fix(tui): keep file suggestions open when typing in Tab-triggered mode

Previously, pressing Tab on an empty prompt or after a space would show
file suggestions, but typing a letter would dismiss them because the
text didn't look like a path pattern. Now the editor tracks whether
autocomplete was triggered via Tab (force mode) or naturally (regular
mode), and uses the appropriate suggestion method when updating.

* fix(tui): hide autocomplete when backspacing slash command to empty

Previously, typing / showed slash command suggestions, but pressing
Backspace to delete it showed file suggestions instead of hiding all
suggestions.
This commit is contained in:
Sviatoslav Abakumov 2026-01-29 05:48:09 +04:00 committed by GitHub
parent d57a26c88b
commit b54d689ec1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 152 additions and 56 deletions

View file

@ -174,7 +174,7 @@ export class Editor implements Component, Focusable {
// Autocomplete support
private autocompleteProvider?: AutocompleteProvider;
private autocompleteList?: SelectList;
private isAutocompleting: boolean = false;
private autocompleteState: "regular" | "force" | null = null;
private autocompletePrefix: string = "";
// Paste tracking for large pastes
@ -351,7 +351,7 @@ export class Editor implements Component, Focusable {
// Render each visible layout line
// Emit hardware cursor marker only when focused and not showing autocomplete
const emitCursorMarker = this.focused && !this.isAutocompleting;
const emitCursorMarker = this.focused && !this.autocompleteState;
for (const layoutLine of visibleLines) {
let displayText = layoutLine.text;
@ -406,7 +406,7 @@ export class Editor implements Component, Focusable {
}
// Add autocomplete list if active
if (this.isAutocompleting && this.autocompleteList) {
if (this.autocompleteState && this.autocompleteList) {
const autocompleteResult = this.autocompleteList.render(contentWidth);
for (const line of autocompleteResult) {
const lineWidth = visibleWidth(line);
@ -459,7 +459,7 @@ export class Editor implements Component, Focusable {
}
// Handle autocomplete mode
if (this.isAutocompleting && this.autocompleteList) {
if (this.autocompleteState && this.autocompleteList) {
if (kb.matches(data, "selectCancel")) {
this.cancelAutocomplete();
return;
@ -520,7 +520,7 @@ export class Editor implements Component, Focusable {
}
// Tab - trigger completion
if (kb.matches(data, "tab") && !this.isAutocompleting) {
if (kb.matches(data, "tab") && !this.autocompleteState) {
this.handleTabCompletion();
return;
}
@ -893,7 +893,7 @@ export class Editor implements Component, Focusable {
}
// Check if we should trigger or update autocomplete
if (!this.isAutocompleting) {
if (!this.autocompleteState) {
// Auto-trigger for "/" at the start of a line (slash commands)
if (char === "/" && this.isAtStartOfMessage()) {
this.tryTriggerAutocomplete();
@ -1050,7 +1050,7 @@ export class Editor implements Component, Focusable {
}
// Update or re-trigger autocomplete after backspace
if (this.isAutocompleting) {
if (this.autocompleteState) {
this.updateAutocomplete();
} else {
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
@ -1270,7 +1270,7 @@ export class Editor implements Component, Focusable {
}
// Update or re-trigger autocomplete after forward delete
if (this.isAutocompleting) {
if (this.autocompleteState) {
this.updateAutocomplete();
} else {
const currentLine = this.state.lines[this.state.cursorLine] || "";
@ -1735,7 +1735,7 @@ export class Editor implements Component, Focusable {
if (suggestions && suggestions.items.length > 0) {
this.autocompletePrefix = suggestions.prefix;
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
this.isAutocompleting = true;
this.autocompleteState = "regular";
} else {
this.cancelAutocomplete();
}
@ -1751,7 +1751,7 @@ export class Editor implements Component, Focusable {
if (this.isInSlashCommandContext(beforeCursor) && !beforeCursor.trimStart().includes(" ")) {
this.handleSlashCommandCompletion();
} else {
this.forceFileAutocomplete();
this.forceFileAutocomplete(true);
}
}
@ -1764,7 +1764,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
536643416/job/55932288317 havea look at .gi
*/
private forceFileAutocomplete(): void {
private forceFileAutocomplete(explicitTab: boolean = false): void {
if (!this.autocompleteProvider) return;
// Check if provider supports force file suggestions via runtime check
@ -1784,7 +1784,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
if (suggestions && suggestions.items.length > 0) {
// If there's exactly one suggestion, apply it immediately
if (suggestions.items.length === 1) {
if (explicitTab && suggestions.items.length === 1) {
const item = suggestions.items[0]!;
this.pushUndoSnapshot();
this.lastAction = null;
@ -1804,31 +1804,35 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
this.autocompletePrefix = suggestions.prefix;
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
this.isAutocompleting = true;
this.autocompleteState = "force";
} else {
this.cancelAutocomplete();
}
}
private cancelAutocomplete(): void {
this.isAutocompleting = false;
this.autocompleteState = null;
this.autocompleteList = undefined;
this.autocompletePrefix = "";
}
public isShowingAutocomplete(): boolean {
return this.isAutocompleting;
return this.autocompleteState !== null;
}
private updateAutocomplete(): void {
if (!this.isAutocompleting || !this.autocompleteProvider) return;
if (!this.autocompleteState || !this.autocompleteProvider) return;
if (this.autocompleteState === "force") {
this.forceFileAutocomplete();
return;
}
const suggestions = this.autocompleteProvider.getSuggestions(
this.state.lines,
this.state.cursorLine,
this.state.cursorCol,
);
if (suggestions && suggestions.items.length > 0) {
this.autocompletePrefix = suggestions.prefix;
// Always create new SelectList to ensure update