fix(editor): suppress completion after newline

This commit is contained in:
zipg
2026-08-13 19:28:13 +08:00
committed by GitHub
parent fbf240efbc
commit 847539397c
2 changed files with 55 additions and 4 deletions
@@ -1778,7 +1778,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
codeMirrorKeymap.of([
{
key: "Enter",
run: codeMirrorInsertNewlineKeepIndent ?? undefined,
run: insertNewlineWithoutCompletion,
},
...binding(shortcuts.find, openSearch),
...binding(shortcuts.replace, openReplace),
@@ -1842,6 +1842,14 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
];
}
function insertNewlineWithoutCompletion(view: EditorViewType): boolean {
codeMirrorCloseCompletion?.(view);
suppressNextSqlCompletionAutoStartUntil = Date.now() + 750;
const handled = codeMirrorInsertNewlineKeepIndent?.(view) ?? false;
if (!handled) suppressNextSqlCompletionAutoStartUntil = 0;
return handled;
}
function extendQueryEditorSelectionForView(currentView: EditorViewType): boolean {
const databaseType = props.databaseType;
const language = databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "victoriametrics" ? "text" : "sql";
@@ -46,29 +46,52 @@ interface MockView {
interface TabHarness {
handleTab: (view: MockView) => boolean;
insertNewlineWithoutCompletion: (view: MockView) => boolean;
acceptCompletionOrNextSnippetField: (view: MockView) => boolean;
clearPendingCompletionTab: () => void;
consumeSqlCompletionAutoStartSuppression: () => boolean;
}
function createHarness(options: { completionStatus: (state: MockState) => "active" | "pending" | null; acceptCompletion?: (view: MockView) => boolean; nextSnippetField?: (view: MockView) => boolean; indentMore?: (view: MockView) => boolean }): TabHarness {
function createHarness(options: {
completionStatus: (state: MockState) => "active" | "pending" | null;
acceptCompletion?: (view: MockView) => boolean;
closeCompletion?: (view: MockView) => boolean;
insertNewlineKeepIndent?: (view: MockView) => boolean;
nextSnippetField?: (view: MockView) => boolean;
indentMore?: (view: MockView) => boolean;
}): TabHarness {
const source = [
extractDeclaration(/const COMPLETION_REMOTE_LATENCY_BUDGET_MS = \d+;/, "remote completion latency budget"),
extractDeclaration(/const COMPLETION_DEBOUNCE_DELAY_MS = \d+;/, "completion debounce delay"),
extractDeclaration(/const COMPLETION_TAB_RETRY_DELAY_MS = \d+;/, "completion retry delay"),
extractDeclaration(/const COMPLETION_TAB_MAX_WAIT_MS = [^;]+;/, "completion wait timeout"),
"let pendingCompletionTabTimer: ReturnType<typeof setTimeout> | null = null;",
"let suppressNextSqlCompletionAutoStartUntil = 0;",
extractFunction("editorIndentUnit"),
extractFunction("handleTab"),
extractFunction("performNormalTab"),
extractFunction("insertNewlineWithoutCompletion"),
extractFunction("acceptCompletionOrNextSnippetField"),
extractFunction("clearPendingCompletionTab"),
extractFunction("waitForCompletionTab"),
extractFunction("consumeSqlCompletionAutoStartSuppression"),
].join("\n");
const javascript = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 },
}).outputText;
const factory = new Function("codeMirrorCompletionStatus", "codeMirrorAcceptCompletion", "codeMirrorNextSnippetField", "codeMirrorIndentMore", "settingsStore", `${javascript}\nreturn { handleTab, acceptCompletionOrNextSnippetField, clearPendingCompletionTab };`);
return factory(options.completionStatus, options.acceptCompletion ?? (() => false), options.nextSnippetField ?? (() => false), options.indentMore ?? (() => false), { editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 } } }) as TabHarness;
const factory = new Function(
"codeMirrorCompletionStatus",
"codeMirrorAcceptCompletion",
"codeMirrorCloseCompletion",
"codeMirrorInsertNewlineKeepIndent",
"codeMirrorNextSnippetField",
"codeMirrorIndentMore",
"settingsStore",
`${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), {
editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 } },
}) as TabHarness;
}
function createView(text = "SELECT", position = text.length): MockView {
@@ -89,6 +112,26 @@ afterEach(() => {
});
describe("QueryEditor completion Tab keymap", () => {
it("closes completion and suppresses its restart for the Enter newline", () => {
const closeCompletion = vi.fn(() => true);
let harness: TabHarness;
const insertNewlineKeepIndent = vi.fn(() => harness.consumeSqlCompletionAutoStartSuppression());
harness = createHarness({ completionStatus: () => "active", closeCompletion, insertNewlineKeepIndent });
const view = createView("SELECT * FROM demo WHERE id IN ()", 32);
expect(harness.insertNewlineWithoutCompletion(view)).toBe(true);
expect(closeCompletion).toHaveBeenCalledWith(view);
expect(insertNewlineKeepIndent).toHaveBeenCalledWith(view);
expect(harness.consumeSqlCompletionAutoStartSuppression()).toBe(false);
});
it("does not suppress later completion when Enter cannot insert a newline", () => {
const harness = createHarness({ completionStatus: () => null, insertNewlineKeepIndent: () => false });
expect(harness.insertNewlineWithoutCompletion(createView())).toBe(false);
expect(harness.consumeSqlCompletionAutoStartSuppression()).toBe(false);
});
it("keeps CodeMirror prefix filtering enabled for SQL completion results", () => {
const javascript = ts.transpileModule(extractFunction("buildCompletionResult"), {
compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 },