fix(editor): restore snippet Tab navigation after completion

Closes #5892
This commit is contained in:
t8y2
2026-08-11 16:52:08 +08:00
parent 691f24b286
commit 20b992ea02
2 changed files with 15 additions and 12 deletions
@@ -742,8 +742,7 @@ function editorIndentUnit(): string {
}
function handleTab(view: EditorViewType): boolean {
if (codeMirrorCompletionStatus?.(view.state)) return false;
return performNormalTab(view);
return acceptCompletionOrNextSnippetField(view) || performNormalTab(view);
}
function performNormalTab(view: EditorViewType): boolean {
@@ -1853,11 +1852,8 @@ function extendQueryEditorSelectionForView(currentView: EditorViewType): boolean
function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
const completionStatus = codeMirrorCompletionStatus?.(view.state) ?? null;
if (completionStatus === "active" && (codeMirrorAcceptCompletion?.(view) ?? false)) return true;
// Snippet fields keep their normal immediate Tab priority when completion
// is pending or still inside CodeMirror's interaction delay.
if (codeMirrorNextSnippetField?.(view)) return true;
if (completionStatus) return waitForCompletionTab(view);
return false;
return codeMirrorNextSnippetField?.(view) ?? false;
}
function clearPendingCompletionTab() {
@@ -1879,7 +1875,6 @@ function waitForCompletionTab(view: EditorViewType): boolean {
const completionStatus = codeMirrorCompletionStatus?.(view.state) ?? null;
if (completionStatus === "active" && (codeMirrorAcceptCompletion?.(view) ?? false)) return;
if (codeMirrorNextSnippetField?.(view)) return;
if (completionStatus && Date.now() - startedAt < COMPLETION_TAB_MAX_WAIT_MS) {
pendingCompletionTabTimer = setTimeout(retry, COMPLETION_TAB_RETRY_DELAY_MS);
return;
@@ -141,21 +141,29 @@ describe("QueryEditor completion Tab keymap", () => {
const harness = createHarness({ completionStatus: () => null, nextSnippetField });
const view = createView();
expect(harness.acceptCompletionOrNextSnippetField(view)).toBe(true);
expect(harness.handleTab(view)).toBe(true);
expect(nextSnippetField).toHaveBeenCalledWith(view);
expect(view.dispatch).not.toHaveBeenCalled();
});
it("advances an available snippet field immediately while completion is pending", () => {
it("waits for pending completion before advancing a snippet field", async () => {
vi.useFakeTimers();
let status: "active" | "pending" | null = "pending";
const acceptCompletion = vi.fn(() => true);
const nextSnippetField = vi.fn(() => true);
const indentMore = vi.fn(() => true);
const harness = createHarness({ completionStatus: () => "pending", nextSnippetField, indentMore });
const harness = createHarness({ completionStatus: () => status, acceptCompletion, nextSnippetField, indentMore });
const view = createView();
expect(harness.acceptCompletionOrNextSnippetField(view)).toBe(true);
expect(nextSnippetField).toHaveBeenCalledWith(view);
expect(vi.getTimerCount()).toBe(0);
expect(nextSnippetField).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(1);
status = "active";
await vi.advanceTimersByTimeAsync(16);
expect(acceptCompletion).toHaveBeenCalledWith(view);
expect(nextSnippetField).not.toHaveBeenCalled();
expect(indentMore).not.toHaveBeenCalled();
expect(view.dispatch).not.toHaveBeenCalled();
});