fix(editor): honor configured accept-completion shortcut

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-15 14:35:52 +08:00
1 parent 1aec3ae000
commit 057b24b5e8
2 files changed
+62 -4

No files matched your search

@@ -748,7 +748,25 @@ function editorIndentUnit(): string {
} }
function handleTab(view: EditorViewType): boolean { function handleTab(view: EditorViewType): boolean {
return acceptCompletionOrNextSnippetField(view) || performNormalTab(view); if (tabKeyAcceptsCompletion()) {
return acceptCompletionOrNextSnippetField(view) || performNormalTab(view);
}
return handleTabWithoutAcceptingCompletion(view) || performNormalTab(view);
}
// The Tab key is always wired up for indentation and snippet-field navigation,
// but it must only accept an open completion popup when the user's configured
// "accept completion" shortcut is actually Tab — otherwise a user who remapped
// that shortcut (e.g. to Enter) would find Tab silently accepting completions
// anyway, ignoring their setting (dbx#6236).
function tabKeyAcceptsCompletion(): boolean {
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
return shortcutToCodeMirrorKey(shortcuts.acceptCompletion) === "Tab";
}
function handleTabWithoutAcceptingCompletion(view: EditorViewType): boolean {
if (codeMirrorCompletionStatus?.(view.state)) return false;
return codeMirrorNextSnippetField?.(view) ?? false;
} }
function performNormalTab(view: EditorViewType): boolean { function performNormalTab(view: EditorViewType): boolean {
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import ts from "typescript"; import ts from "typescript";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_SHORTCUT_SETTINGS, normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8"); const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
@@ -59,6 +60,7 @@ function createHarness(options: {
insertNewlineKeepIndent?: (view: MockView) => boolean; insertNewlineKeepIndent?: (view: MockView) => boolean;
nextSnippetField?: (view: MockView) => boolean; nextSnippetField?: (view: MockView) => boolean;
indentMore?: (view: MockView) => boolean; indentMore?: (view: MockView) => boolean;
acceptCompletionShortcut?: string;
}): TabHarness { }): TabHarness {
const source = [ const source = [
extractDeclaration(/const COMPLETION_REMOTE_LATENCY_BUDGET_MS = \d+;/, "remote completion latency budget"), extractDeclaration(/const COMPLETION_REMOTE_LATENCY_BUDGET_MS = \d+;/, "remote completion latency budget"),
@@ -69,6 +71,8 @@ function createHarness(options: {
"let suppressNextSqlCompletionAutoStartUntil = 0;", "let suppressNextSqlCompletionAutoStartUntil = 0;",
extractFunction("editorIndentUnit"), extractFunction("editorIndentUnit"),
extractFunction("handleTab"), extractFunction("handleTab"),
extractFunction("tabKeyAcceptsCompletion"),
extractFunction("handleTabWithoutAcceptingCompletion"),
extractFunction("performNormalTab"), extractFunction("performNormalTab"),
extractFunction("insertNewlineWithoutCompletion"), extractFunction("insertNewlineWithoutCompletion"),
extractFunction("acceptCompletionOrNextSnippetField"), extractFunction("acceptCompletionOrNextSnippetField"),
@@ -87,11 +91,26 @@ function createHarness(options: {
"codeMirrorNextSnippetField", "codeMirrorNextSnippetField",
"codeMirrorIndentMore", "codeMirrorIndentMore",
"settingsStore", "settingsStore",
"normalizeShortcutSettings",
"shortcutToCodeMirrorKey",
`${javascript}\nreturn { handleTab, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`, `${javascript}\nreturn { handleTab, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`,
); );
return factory(options.completionStatus, options.acceptCompletion ?? (() => false), options.closeCompletion ?? (() => false), options.insertNewlineKeepIndent ?? (() => false), options.nextSnippetField ?? (() => false), options.indentMore ?? (() => false), { return factory(
editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 } }, options.completionStatus,
}) as TabHarness; options.acceptCompletion ?? (() => false),
options.closeCompletion ?? (() => false),
options.insertNewlineKeepIndent ?? (() => false),
options.nextSnippetField ?? (() => false),
options.indentMore ?? (() => false),
{
editorSettings: {
sqlFormatter: { useTabs: false, tabWidth: 2 },
shortcuts: { ...DEFAULT_SHORTCUT_SETTINGS, acceptCompletion: options.acceptCompletionShortcut ?? DEFAULT_SHORTCUT_SETTINGS.acceptCompletion },
},
},
normalizeShortcutSettings,
shortcutToCodeMirrorKey,
) as TabHarness;
} }
function createView(text = "SELECT", position = text.length): MockView { function createView(text = "SELECT", position = text.length): MockView {
@@ -237,6 +256,27 @@ describe("QueryEditor completion Tab keymap", () => {
expect(view.dispatch).not.toHaveBeenCalled(); expect(view.dispatch).not.toHaveBeenCalled();
}); });
it("does not accept an open completion popup on Tab when the configured accept-completion shortcut is Enter (dbx#6236)", () => {
const acceptCompletion = vi.fn(() => true);
const nextSnippetField = vi.fn(() => true);
const harness = createHarness({ completionStatus: () => "active", acceptCompletion, nextSnippetField, acceptCompletionShortcut: "Enter" });
const view = createView();
expect(harness.handleTab(view)).toBe(true);
expect(acceptCompletion).not.toHaveBeenCalled();
expect(nextSnippetField).not.toHaveBeenCalled();
expect(view.state.replaceSelection).toHaveBeenCalledWith(" ");
});
it("still accepts an open completion popup on Tab when the configured shortcut is left at its Tab default", () => {
const acceptCompletion = vi.fn(() => true);
const harness = createHarness({ completionStatus: () => "active", acceptCompletion, acceptCompletionShortcut: "Tab" });
const view = createView();
expect(harness.handleTab(view)).toBe(true);
expect(acceptCompletion).toHaveBeenCalledWith(view);
});
it("falls back to normal Tab when pending completion has no candidate", async () => { it("falls back to normal Tab when pending completion has no candidate", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
let status: "active" | "pending" | null = "pending"; let status: "active" | "pending" | null = "pending";