diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index f0969184b3..34ba6f1ca4 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -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; diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts index 1b25676f88..74368fc6a1 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorCompletionKeymap.spec.ts @@ -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(); });