fix(editor): stabilize batch column selection

This commit is contained in:
skyler authored and GitHub committed 2026-08-30 10:35:34 +08:00
1 parent f664918e95
commit 143be57d41
5 files changed
+110 -15

No files matched your search

@@ -118,7 +118,7 @@ import { buildQueryEditorLineNumbersExtension, createQueryEditorLineNumberAlignm
import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap";
import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultKeymap";
import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion";
import { batchColumnSelectionColumnList, batchColumnSelectionReplaceTo, shouldResolveSqlColumnCompletion } from "@/lib/editor/batchColumnSelection";
import { batchColumnSelectionColumnList, batchColumnSelectionInsertReplacement, batchColumnSelectionReplaceTo, isBatchColumnSelectionCompletionActive, shouldResolveSqlColumnCompletion } from "@/lib/editor/batchColumnSelection";
import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
import { clampEditorFontSize, createEditorWheelZoomGestureGuard, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
import { enabledSqlShortcutActions, resolveSqlShortcutTemplate } from "@/lib/sql/sqlShortcutActions";
@@ -2085,7 +2085,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
function handleEnter(view: EditorViewType): boolean {
clearPendingCompletionEnter();
if (applySelectedBatchColumnSelection(view)) return true;
if (isBatchColumnSelectionCompletionActive(codeMirrorCompletionStatus?.(view.state) ?? null) && applySelectedBatchColumnSelection(view)) return true;
// CodeMirror's default completion keymap is disabled so batch selection can
// take precedence. Preserve its normal single-item acceptance here.
if (codeMirrorAcceptCompletion?.(view)) return true;
@@ -2139,8 +2139,8 @@ function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
// not word completion. A completion popup can still appear as a side effect
// of the indent edit itself, so it must never hijack this or a following Tab.
if (view.state.selection.ranges.every((range) => range.empty)) {
if (applySelectedBatchColumnSelection(view)) return true;
const completionStatus = codeMirrorCompletionStatus?.(view.state) ?? null;
if (isBatchColumnSelectionCompletionActive(completionStatus) && applySelectedBatchColumnSelection(view)) return true;
if (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion)) return true;
if (completionStatus) return waitForCompletionTab(view);
}
@@ -3513,6 +3513,10 @@ type QueryCompletionOption = Completion & {
let batchColumnSelectionSession: BatchColumnSelectionSession | null = null;
function clearBatchColumnSelectionSession() {
batchColumnSelectionSession = null;
}
function isBatchColumnSelectionAction(item: QueryCompletionItem | BatchColumnSelectionActionItem): item is BatchColumnSelectionActionItem {
return "batchColumnSelectionAction" in item && item.batchColumnSelectionAction === true;
}
@@ -3627,20 +3631,35 @@ function applyBatchColumnSelection(view: EditorViewType, item: BatchColumnSelect
return;
}
const replaceTo = batchColumnSelectionReplaceTo({
to,
mode: session.mode,
nextCharacter: view.state.sliceDoc(to, to + 1),
replaceClosingQuote: session.replaceClosingQuote,
});
const columns = batchColumnSelectionColumnList(
selected.map((candidate) => candidate.apply),
session.mode,
session.qualifier,
);
const insert = session.mode === "insert" ? `${columns}) ${settingsStore.editorSettings.sqlFormatter.keywordCase === "lower" ? "values" : "VALUES"} (${selected.map((_, index) => `\${${index + 1}:value}`).join(", ")})` : columns;
let replaceTo = batchColumnSelectionReplaceTo({
to,
mode: session.mode,
nextCharacter: view.state.sliceDoc(to, to + 1),
replaceClosingQuote: session.replaceClosingQuote,
});
let insert = columns;
if (session.mode === "insert") {
const replacement = batchColumnSelectionInsertReplacement({
document: view.state.doc.toString(),
to,
columns,
valuesKeyword: settingsStore.editorSettings.sqlFormatter.keywordCase === "lower" ? "values" : "VALUES",
valueCount: selected.length,
});
replaceTo = replacement.replaceTo;
insert = replacement.insert;
}
batchColumnSelectionSession = null;
if (session.mode === "insert" && !codeMirrorSnippetCompletion) {
clearBatchColumnSelectionSession();
return;
}
clearBatchColumnSelectionSession();
markCompletionAccepted(item);
if (session.mode === "insert") {
const snippet = codeMirrorSnippetCompletion(insert, { label: item.label });
@@ -3658,7 +3677,7 @@ function applyBatchColumnSelection(view: EditorViewType, item: BatchColumnSelect
function applySelectedBatchColumnSelection(view: EditorViewType): boolean {
const session = batchColumnSelectionSession;
if (!session || session.document !== view.state.doc.toString() || session.selectedKeys.size === 0) return false;
if (!session || !isBatchColumnSelectionCompletionActive(codeMirrorCompletionStatus?.(view.state) ?? null) || session.document !== view.state.doc.toString() || session.selectedKeys.size === 0) return false;
applyBatchColumnSelection(
view,
{
@@ -5659,6 +5678,7 @@ onMounted(async () => {
{
key: "Escape",
run: () => {
clearBatchColumnSelectionSession();
return searchPanelRef.value?.closeSearch() ?? false;
},
},
@@ -5718,6 +5738,7 @@ onMounted(async () => {
const status = codeMirrorCompletionStatus(update.state) ?? null;
if (status === null) {
activeCompletionOrigin = null;
clearBatchColumnSelectionSession();
}
}
}),
@@ -120,9 +120,9 @@ describe("QueryEditor batch column selection", () => {
const tabEnd = source.indexOf("\n}\n\nfunction clearPendingCompletionTab", tabStart);
expect(source).toContain("function applySelectedBatchColumnSelection");
expect(source.slice(handleEnterStart, handleEnterEnd)).toContain("if (applySelectedBatchColumnSelection(view)) return true;");
expect(source.slice(handleEnterStart, handleEnterEnd)).toContain("if (isBatchColumnSelectionCompletionActive(codeMirrorCompletionStatus?.(view.state) ?? null) && applySelectedBatchColumnSelection(view)) return true;");
expect(source.slice(handleEnterStart, handleEnterEnd)).toContain("if (codeMirrorAcceptCompletion?.(view)) return true;");
expect(source.slice(tabStart, tabEnd)).toContain("if (applySelectedBatchColumnSelection(view)) return true;");
expect(source.slice(tabStart, tabEnd)).toContain("if (isBatchColumnSelectionCompletionActive(completionStatus) && applySelectedBatchColumnSelection(view)) return true;");
expect(source).toContain("defaultKeymap: false");
expect(source).toContain('{ key: "ArrowDown", run: (view) => moveCompletion(view, true) }');
expect(source).toContain('{ key: "ArrowUp", run: (view) => moveCompletion(view, false) }');