feat(editor): batch-select and insert SQL completion columns
This commit is contained in:
15 files changed
+440
-14
No files matched your search
@@ -2,7 +2,7 @@
|
||||
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
|
||||
import { AlignLeft, Camera, CaseLower, CaseUpper, ClipboardPaste, Code2, Download, FileCode, MessageSquareText, Minimize2, Pencil, PencilRuler, Play, Copy, List, Scissors, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
||||
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
|
||||
import { Transaction, StateEffect } from "@codemirror/state";
|
||||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import { search as cmSearch } from "@codemirror/search";
|
||||
@@ -118,6 +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 { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
import { clampEditorFontSize, createEditorWheelZoomGestureGuard, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
|
||||
import { enabledSqlShortcutActions, resolveSqlShortcutTemplate } from "@/lib/sql/sqlShortcutActions";
|
||||
@@ -566,6 +567,8 @@ let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snipp
|
||||
let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null;
|
||||
let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null;
|
||||
let codeMirrorSelectedCompletionIndex: typeof import("@codemirror/autocomplete").selectedCompletionIndex | null = null;
|
||||
let codeMirrorSelectedCompletion: typeof import("@codemirror/autocomplete").selectedCompletion | null = null;
|
||||
let codeMirrorMoveCompletionSelection: typeof import("@codemirror/autocomplete").moveCompletionSelection | null = null;
|
||||
let codeMirrorSelectFirstCompletion: import("@codemirror/view").Command | null = null;
|
||||
let codeMirrorStartCompletion: typeof import("@codemirror/autocomplete").startCompletion | null = null;
|
||||
let codeMirrorCloseCompletion: typeof import("@codemirror/autocomplete").closeCompletion | null = null;
|
||||
@@ -1972,6 +1975,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
if (replaceShortcutHandler(event)) return true;
|
||||
return sqlShortcutDomHandler(event, view);
|
||||
};
|
||||
const moveCompletion = (view: EditorViewType, forward: boolean, by?: "page") => codeMirrorMoveCompletionSelection?.(forward, by)?.(view) ?? false;
|
||||
return [
|
||||
editorViewModule
|
||||
? (Prec?.high(
|
||||
@@ -1980,12 +1984,24 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
}),
|
||||
) ?? [])
|
||||
: [],
|
||||
Prec?.highest(
|
||||
codeMirrorKeymap.of([
|
||||
{ key: "ArrowDown", run: (view) => moveCompletion(view, true) },
|
||||
{ key: "ArrowUp", run: (view) => moveCompletion(view, false) },
|
||||
{ key: "PageDown", run: (view) => moveCompletion(view, true, "page") },
|
||||
{ key: "PageUp", run: (view) => moveCompletion(view, false, "page") },
|
||||
]),
|
||||
) ?? [],
|
||||
Prec?.high(
|
||||
codeMirrorKeymap.of([
|
||||
{
|
||||
key: "Enter",
|
||||
run: handleEnter,
|
||||
},
|
||||
{
|
||||
key: "Space",
|
||||
run: toggleSelectedBatchColumnSelection,
|
||||
},
|
||||
...binding(shortcuts.find, openSearch),
|
||||
...replaceShortcutBindings,
|
||||
...executeInNewResultTabBindings,
|
||||
@@ -2057,6 +2073,10 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
|
||||
function handleEnter(view: EditorViewType): boolean {
|
||||
clearPendingCompletionEnter();
|
||||
if (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;
|
||||
if (settingsStore.editorSettings.selectFirstCompletionOnOpen && codeMirrorCompletionStatus) {
|
||||
let cancelRetry: (() => void) | null = null;
|
||||
const result = acceptSelectedCompletionWithRetry(view, {
|
||||
@@ -2107,6 +2127,7 @@ 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 (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion)) return true;
|
||||
if (completionStatus) return waitForCompletionTab(view);
|
||||
@@ -3446,6 +3467,202 @@ let activeCompletionOrigin: SqlCompletionTriggerOrigin | null = null;
|
||||
|
||||
type QueryCompletionItem = SqlCompletionItem | ElasticsearchCompletionItem | RedisCompletionItem | MongoCompletionItem;
|
||||
|
||||
interface BatchColumnSelectionCandidate {
|
||||
key: string;
|
||||
apply: string;
|
||||
}
|
||||
|
||||
interface BatchColumnSelectionSession {
|
||||
key: string;
|
||||
mode: "select" | "insert";
|
||||
qualifier?: string;
|
||||
document: string;
|
||||
from: number;
|
||||
to: number;
|
||||
replaceClosingQuote?: SqlCompletionItem["replaceClosingQuote"];
|
||||
candidates: BatchColumnSelectionCandidate[];
|
||||
selectedKeys: Set<string>;
|
||||
completionOptions: Map<string, QueryCompletionOption>;
|
||||
}
|
||||
|
||||
interface BatchColumnSelectionActionItem {
|
||||
label: string;
|
||||
filterText?: string;
|
||||
type: "text";
|
||||
detail: string;
|
||||
boost: number;
|
||||
batchColumnSelectionAction: true;
|
||||
sessionKey: string;
|
||||
}
|
||||
|
||||
type QueryCompletionOption = Completion & {
|
||||
dbxBatchColumnSelection?: { sessionKey: string; candidateKey: string };
|
||||
};
|
||||
|
||||
let batchColumnSelectionSession: BatchColumnSelectionSession | null = null;
|
||||
|
||||
function isBatchColumnSelectionAction(item: QueryCompletionItem | BatchColumnSelectionActionItem): item is BatchColumnSelectionActionItem {
|
||||
return "batchColumnSelectionAction" in item && item.batchColumnSelectionAction === true;
|
||||
}
|
||||
|
||||
function batchColumnSelectionCandidateKey(item: SqlCompletionItem): string {
|
||||
return `${item.label}\u0000${item.apply ?? item.label}`;
|
||||
}
|
||||
|
||||
function prepareBatchColumnSelectionSession(items: SqlCompletionItem[], document: string, from: number, to: number): BatchColumnSelectionSession | null {
|
||||
const selectableItems = items.filter((item) => item.type === "column" && item.batchSelectionMode && item.apply);
|
||||
if (selectableItems.length === 0) {
|
||||
batchColumnSelectionSession = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const mode = selectableItems[0]!.batchSelectionMode!;
|
||||
const candidates = selectableItems.filter((item) => item.batchSelectionMode === mode).map((item) => ({ key: batchColumnSelectionCandidateKey(item), apply: item.apply! }));
|
||||
const key = `${mode}\u0000${from}\u0000${to}\u0000${document}`;
|
||||
if (!batchColumnSelectionSession || batchColumnSelectionSession.key !== key) {
|
||||
batchColumnSelectionSession = {
|
||||
key,
|
||||
mode,
|
||||
qualifier: selectableItems[0]!.batchSelectionQualifier,
|
||||
document,
|
||||
from,
|
||||
to,
|
||||
replaceClosingQuote: selectableItems[0]!.replaceClosingQuote,
|
||||
candidates,
|
||||
selectedKeys: new Set(),
|
||||
completionOptions: new Map(),
|
||||
};
|
||||
return batchColumnSelectionSession;
|
||||
}
|
||||
|
||||
batchColumnSelectionSession.candidates = candidates;
|
||||
batchColumnSelectionSession.qualifier = selectableItems[0]!.batchSelectionQualifier;
|
||||
const candidateKeys = new Set(candidates.map((candidate) => candidate.key));
|
||||
batchColumnSelectionSession.selectedKeys = new Set([...batchColumnSelectionSession.selectedKeys].filter((candidateKey) => candidateKeys.has(candidateKey)));
|
||||
batchColumnSelectionSession.completionOptions = new Map([...batchColumnSelectionSession.completionOptions].filter(([candidateKey]) => candidateKeys.has(candidateKey)));
|
||||
return batchColumnSelectionSession;
|
||||
}
|
||||
|
||||
function batchColumnSelectionMarkerForItem(item: QueryCompletionItem): QueryCompletionOption["dbxBatchColumnSelection"] | undefined {
|
||||
if (!("batchSelectionMode" in item) || item.type !== "column" || !item.batchSelectionMode || !item.apply) return undefined;
|
||||
const session = batchColumnSelectionSession;
|
||||
if (!session || session.mode !== item.batchSelectionMode) return undefined;
|
||||
const candidateKey = batchColumnSelectionCandidateKey(item);
|
||||
if (!session.candidates.some((candidate) => candidate.key === candidateKey)) return undefined;
|
||||
return { sessionKey: session.key, candidateKey };
|
||||
}
|
||||
|
||||
function cacheBatchColumnSelectionOption(marker: QueryCompletionOption["dbxBatchColumnSelection"], option: QueryCompletionOption): QueryCompletionOption {
|
||||
if (!marker || !batchColumnSelectionSession || batchColumnSelectionSession.key !== marker.sessionKey) return option;
|
||||
const cached = batchColumnSelectionSession.completionOptions.get(marker.candidateKey);
|
||||
if (cached) return cached;
|
||||
batchColumnSelectionSession.completionOptions.set(marker.candidateKey, option);
|
||||
return option;
|
||||
}
|
||||
|
||||
function toggleBatchColumnSelection(view: EditorViewType, sessionKey: string, candidateKey: string) {
|
||||
const session = batchColumnSelectionSession;
|
||||
if (!session || session.key !== sessionKey || !session.candidates.some((candidate) => candidate.key === candidateKey)) return;
|
||||
if (session.selectedKeys.has(candidateKey)) {
|
||||
session.selectedKeys.delete(candidateKey);
|
||||
} else {
|
||||
session.selectedKeys.add(candidateKey);
|
||||
}
|
||||
// Reopen the list so the action row and all virtualized checkboxes reflect the new state.
|
||||
window.setTimeout(() => codeMirrorStartCompletion?.(view), 0);
|
||||
}
|
||||
|
||||
function toggleSelectedBatchColumnSelection(view: EditorViewType): boolean {
|
||||
const completion = codeMirrorSelectedCompletion?.(view.state) as QueryCompletionOption | null | undefined;
|
||||
const marker = completion?.dbxBatchColumnSelection;
|
||||
if (!marker) return false;
|
||||
toggleBatchColumnSelection(view, marker.sessionKey, marker.candidateKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderBatchColumnSelectionCheckbox(completion: Completion, _state: import("@codemirror/state").EditorState, currentView: EditorViewType): Node | null {
|
||||
const marker = (completion as QueryCompletionOption).dbxBatchColumnSelection;
|
||||
const session = batchColumnSelectionSession;
|
||||
if (!marker || !session || session.key !== marker.sessionKey) return null;
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "cm-batch-column-selection-checkbox";
|
||||
checkbox.checked = session.selectedKeys.has(marker.candidateKey);
|
||||
checkbox.tabIndex = -1;
|
||||
checkbox.setAttribute("aria-label", completion.displayLabel ?? completion.label);
|
||||
checkbox.addEventListener("mousedown", (event) => {
|
||||
// CodeMirror accepts the completion on a list-item mousedown. Keep this
|
||||
// interaction local to the checkbox so a field can be toggled repeatedly.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleBatchColumnSelection(currentView, marker.sessionKey, marker.candidateKey);
|
||||
});
|
||||
checkbox.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
function applyBatchColumnSelection(view: EditorViewType, item: BatchColumnSelectionActionItem, from: number, to: number) {
|
||||
const session = batchColumnSelectionSession;
|
||||
if (!session || session.key !== item.sessionKey || session.document !== view.state.doc.toString()) return;
|
||||
|
||||
const selected = session.candidates.filter((candidate) => session.selectedKeys.has(candidate.key));
|
||||
if (selected.length === 0) {
|
||||
toast(t("editor.completion.selectColumnsBeforeInsert"), 3000);
|
||||
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;
|
||||
|
||||
batchColumnSelectionSession = null;
|
||||
markCompletionAccepted(item);
|
||||
if (session.mode === "insert") {
|
||||
const snippet = codeMirrorSnippetCompletion(insert, { label: item.label });
|
||||
if (typeof snippet.apply === "function") {
|
||||
snippet.apply(view, snippet, from, replaceTo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
view.dispatch({
|
||||
changes: { from, to: replaceTo, insert },
|
||||
selection: { anchor: from + insert.length },
|
||||
scrollIntoView: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applySelectedBatchColumnSelection(view: EditorViewType): boolean {
|
||||
const session = batchColumnSelectionSession;
|
||||
if (!session || session.document !== view.state.doc.toString() || session.selectedKeys.size === 0) return false;
|
||||
applyBatchColumnSelection(
|
||||
view,
|
||||
{
|
||||
label: t("editor.completion.insertSelectedColumns", { count: session.selectedKeys.size }),
|
||||
type: "text",
|
||||
detail: t("editor.completion.insertSelectedColumnsDetail"),
|
||||
boost: -1000,
|
||||
batchColumnSelectionAction: true,
|
||||
sessionKey: session.key,
|
||||
},
|
||||
session.from,
|
||||
session.to,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function markTypedCompletionActivation() {
|
||||
typedCompletionActivationUntil = Date.now() + 500;
|
||||
}
|
||||
@@ -3454,7 +3671,8 @@ function isTypedCompletionActivation(explicit: boolean) {
|
||||
return explicit && typedCompletionActivationUntil >= Date.now();
|
||||
}
|
||||
|
||||
function markCompletionAccepted(item: QueryCompletionItem) {
|
||||
function markCompletionAccepted(item: QueryCompletionItem | BatchColumnSelectionActionItem) {
|
||||
batchColumnSelectionSession = null;
|
||||
const shouldContinueCompletion = shouldChainSqlCompletionAfterAccept(item) || (props.databaseType === "sqlserver" && item.type === "keyword" && item.label.toUpperCase() === "USE");
|
||||
suppressNextSqlCompletionAutoStartUntil = shouldContinueCompletion ? 0 : Date.now() + 750;
|
||||
completionEpoch++;
|
||||
@@ -3469,7 +3687,7 @@ function consumeSqlCompletionAutoStartSuppression() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildCompletionResult(items: QueryCompletionItem[], from: number, validFor?: RegExp, prefix?: string) {
|
||||
function buildCompletionResult(items: Array<QueryCompletionItem | BatchColumnSelectionActionItem>, from: number, validFor?: RegExp, prefix?: string) {
|
||||
if (items.length === 0) return null;
|
||||
const bypassFilter = !!prefix && shouldBypassCompletionFilter(prefix, items);
|
||||
const resultItems = bypassFilter && prefix ? completionItemsForBypassedFilter(prefix, items) : items;
|
||||
@@ -3487,23 +3705,39 @@ function buildCompletionResult(items: QueryCompletionItem[], from: number, valid
|
||||
|
||||
function buildSqlCompletionResult(items: SqlCompletionItem[], completionContext: SqlCompletionContext, fullDoc: string, position: number) {
|
||||
const replacement = prepareSqlCompletionReplacement(fullDoc, position, completionContext, items);
|
||||
return buildCompletionResult(replacement.items, replacement.from, getSqlCompletionResultValidFor(fullDoc, position), completionContext.prefix);
|
||||
const session = prepareBatchColumnSelectionSession(replacement.items, fullDoc, replacement.from, position);
|
||||
const action: BatchColumnSelectionActionItem | undefined = session
|
||||
? {
|
||||
label: t("editor.completion.insertSelectedColumns", { count: session.selectedKeys.size }),
|
||||
filterText: completionContext.prefix,
|
||||
type: "text",
|
||||
detail: t("editor.completion.insertSelectedColumnsDetail"),
|
||||
// Keep ordinary single-field completion as the default Enter target.
|
||||
boost: -1000,
|
||||
batchColumnSelectionAction: true,
|
||||
sessionKey: session.key,
|
||||
}
|
||||
: undefined;
|
||||
return buildCompletionResult(action ? [...replacement.items, action] : replacement.items, replacement.from, getSqlCompletionResultValidFor(fullDoc, position), completionContext.prefix);
|
||||
}
|
||||
|
||||
// CodeMirror's built-in matcher only matches single-character queries against
|
||||
// the label start, and cannot match pinyin initials against Han labels. Our
|
||||
// provider already filters and ranks items itself (substring + pinyin), so
|
||||
// skip the second-stage filter exactly in the cases it would break.
|
||||
function shouldBypassCompletionFilter(prefix: string, items: QueryCompletionItem[]): boolean {
|
||||
function shouldBypassCompletionFilter(prefix: string, items: Array<QueryCompletionItem | BatchColumnSelectionActionItem>): boolean {
|
||||
if (!prefix) return false;
|
||||
if (/\p{Script=Han}/u.test(prefix)) return true;
|
||||
return /^[a-z0-9]+$/i.test(prefix) && items.some((item) => /\p{Script=Han}/u.test(item.label));
|
||||
}
|
||||
|
||||
function completionItemsForBypassedFilter(prefix: string, items: QueryCompletionItem[]): QueryCompletionItem[] {
|
||||
function completionItemsForBypassedFilter(prefix: string, items: Array<QueryCompletionItem | BatchColumnSelectionActionItem>): Array<QueryCompletionItem | BatchColumnSelectionActionItem> {
|
||||
if ([...prefix].length !== 1 || !/^[a-z0-9]$/i.test(prefix)) return items;
|
||||
const normalized = prefix.toLowerCase();
|
||||
return items.filter((item) => {
|
||||
// The confirmation row must remain available while typing a Han/pinyin
|
||||
// prefix, otherwise checked columns cannot be applied from that menu.
|
||||
if (isBatchColumnSelectionAction(item)) return true;
|
||||
const renderedLabel = "displayLabel" in item && typeof item.displayLabel === "string" ? item.displayLabel : item.label;
|
||||
return /\p{Script=Han}/u.test(item.label) || renderedLabel.toLowerCase().startsWith(normalized);
|
||||
});
|
||||
@@ -3536,12 +3770,24 @@ function shouldInsertSqlCompletionSpace(): boolean {
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
function completionOptionForItem(item: QueryCompletionItem) {
|
||||
function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectionActionItem) {
|
||||
const filterText = "filterText" in item && typeof item.filterText === "string" ? item.filterText : undefined;
|
||||
const labelPresentation = completionLabelPresentation(item.label, filterText);
|
||||
if (isBatchColumnSelectionAction(item)) {
|
||||
return {
|
||||
...labelPresentation,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
apply(view: EditorViewType, _completionItem: unknown, from: number, to: number) {
|
||||
applyBatchColumnSelection(view, item, from, to);
|
||||
},
|
||||
};
|
||||
}
|
||||
const record = () => {
|
||||
recordCompletionSelection(item.label, item.type);
|
||||
};
|
||||
const batchColumnSelection = batchColumnSelectionMarkerForItem(item);
|
||||
if ((item.type === "snippet" || item.type === "function") && item.apply) {
|
||||
const completion = codeMirrorSnippetCompletion(item.apply, {
|
||||
...labelPresentation,
|
||||
@@ -3551,8 +3797,9 @@ function completionOptionForItem(item: QueryCompletionItem) {
|
||||
boost: item.boost,
|
||||
});
|
||||
const originalApply = completion.apply;
|
||||
return {
|
||||
return cacheBatchColumnSelectionOption(batchColumnSelection, {
|
||||
...completion,
|
||||
...(batchColumnSelection ? { dbxBatchColumnSelection: batchColumnSelection } : {}),
|
||||
apply(view: EditorViewType, completionItem: unknown, from: number, to: number) {
|
||||
record();
|
||||
markCompletionAccepted(item);
|
||||
@@ -3573,10 +3820,11 @@ function completionOptionForItem(item: QueryCompletionItem) {
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
return cacheBatchColumnSelectionOption(batchColumnSelection, {
|
||||
...labelPresentation,
|
||||
...(batchColumnSelection ? { dbxBatchColumnSelection: batchColumnSelection } : {}),
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
info: item.info,
|
||||
@@ -3599,7 +3847,7 @@ function completionOptionForItem(item: QueryCompletionItem) {
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function provideElasticsearchCompletions(currentState: import("@codemirror/state").EditorState, position: number, explicit: boolean) {
|
||||
@@ -3907,7 +4155,13 @@ async function provideSqlCompletions(context: CompletionContext) {
|
||||
}
|
||||
|
||||
const tableNameCompletion = isTableNameCompletionContext(completionContext);
|
||||
const shouldResolveColumnCompletion = completionContext.suggestColumns && completionContext.referencedTables.length > 0 && (completionContext.prefix.length > 0 || typedActivation);
|
||||
const shouldResolveColumnCompletion = shouldResolveSqlColumnCompletion({
|
||||
suggestColumns: completionContext.suggestColumns,
|
||||
hasReferencedTables: completionContext.referencedTables.length > 0,
|
||||
prefix: completionContext.prefix,
|
||||
typedActivation,
|
||||
selectListColumnContext: completionContext.selectListColumnContext,
|
||||
});
|
||||
const shouldResolveAsyncCompletion = tableNameCompletion || shouldResolveColumnCompletion;
|
||||
const localResult = buildLocalSqlCompletionResult(completionContext, fullDoc, position, completionScope);
|
||||
if (localResult) {
|
||||
@@ -4818,7 +5072,7 @@ onMounted(async () => {
|
||||
},
|
||||
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
|
||||
langSql,
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion, moveCompletionSelection, selectedCompletionIndex },
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion, moveCompletionSelection, selectedCompletion, selectedCompletionIndex },
|
||||
{ copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, toggleBlockComment, history, defaultKeymap, historyKeymap },
|
||||
{ bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap, toggleFold, ensureSyntaxTree, highlightingFor, syntaxTree },
|
||||
{ searchKeymap },
|
||||
@@ -4855,7 +5109,9 @@ onMounted(async () => {
|
||||
setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>();
|
||||
codeMirrorCompletionStatus = completionStatus;
|
||||
codeMirrorAcceptCompletion = acceptCompletion;
|
||||
codeMirrorSelectedCompletion = selectedCompletion;
|
||||
codeMirrorSelectedCompletionIndex = selectedCompletionIndex;
|
||||
codeMirrorMoveCompletionSelection = moveCompletionSelection;
|
||||
codeMirrorSelectFirstCompletion = moveCompletionSelection(true);
|
||||
codeMirrorCloseCompletion = closeCompletion;
|
||||
codeMirrorStartCompletion = startCompletion;
|
||||
@@ -5090,8 +5346,10 @@ onMounted(async () => {
|
||||
buildSqlCompletionExtension = () =>
|
||||
autocompletion({
|
||||
activateOnTyping: true,
|
||||
defaultKeymap: false,
|
||||
selectOnOpen: settingsStore.editorSettings.selectFirstCompletionOnOpen,
|
||||
compareCompletions: (a, b) => compareSqlCompletions(a, b, settingsStore.editorSettings.sortCompletionColumnsAlphabetically),
|
||||
addToOptions: [{ position: 10, render: renderBatchColumnSelectionCheckbox }],
|
||||
override: [async (context: CompletionContext) => provideSqlCompletions(context)],
|
||||
});
|
||||
|
||||
@@ -6632,4 +6890,13 @@ defineExpose({
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cm-batch-column-selection-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0 2px 0 0;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -98,3 +98,33 @@ describe("QueryEditor context menu lifecycle", () => {
|
||||
container.remove();
|
||||
});
|
||||
});
|
||||
|
||||
describe("QueryEditor batch column selection", () => {
|
||||
it("keeps the confirmation action visible during pinyin filtering and supports keyboard toggling", () => {
|
||||
const bypassFilterStart = source.indexOf("function completionItemsForBypassedFilter");
|
||||
const bypassFilterEnd = source.indexOf("\n}\n\nfunction localCompletionDatabaseNames", bypassFilterStart);
|
||||
const bypassFilterSource = source.slice(bypassFilterStart, bypassFilterEnd);
|
||||
|
||||
expect(bypassFilterStart).toBeGreaterThanOrEqual(0);
|
||||
expect(bypassFilterEnd).toBeGreaterThan(bypassFilterStart);
|
||||
expect(bypassFilterSource).toContain("if (isBatchColumnSelectionAction(item)) return true;");
|
||||
expect(source).toContain('key: "Space"');
|
||||
expect(source).toContain("run: toggleSelectedBatchColumnSelection");
|
||||
expect(source).toContain("codeMirrorSelectedCompletion?.(view.state)");
|
||||
});
|
||||
|
||||
it("applies checked columns directly through Enter and the completion Tab shortcut", () => {
|
||||
const handleEnterStart = source.indexOf("function handleEnter");
|
||||
const handleEnterEnd = source.indexOf("\n}\n\nfunction clearPendingCompletionEnter", handleEnterStart);
|
||||
const tabStart = source.indexOf("function acceptCompletionOrNextSnippetField");
|
||||
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 (codeMirrorAcceptCompletion?.(view)) return true;");
|
||||
expect(source.slice(tabStart, tabEnd)).toContain("if (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) }');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user