feat(editor): batch-select completion fields via drag

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-02 18:07:25 +08:00
1 parent 72dcd8355a
commit fdea607efd
4 files changed
+396 -11

No files matched your search

@@ -571,8 +571,10 @@ let buildSqlSemanticHighlightExtension: (() => import("@codemirror/state").Exten
let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion; let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion;
let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null; let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null;
let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null; let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null;
let codeMirrorCurrentCompletions: typeof import("@codemirror/autocomplete").currentCompletions | null = null;
let codeMirrorSelectedCompletionIndex: typeof import("@codemirror/autocomplete").selectedCompletionIndex | null = null; let codeMirrorSelectedCompletionIndex: typeof import("@codemirror/autocomplete").selectedCompletionIndex | null = null;
let codeMirrorSelectedCompletion: typeof import("@codemirror/autocomplete").selectedCompletion | null = null; let codeMirrorSelectedCompletion: typeof import("@codemirror/autocomplete").selectedCompletion | null = null;
let codeMirrorSetSelectedCompletion: typeof import("@codemirror/autocomplete").setSelectedCompletion | null = null;
let codeMirrorMoveCompletionSelection: typeof import("@codemirror/autocomplete").moveCompletionSelection | null = null; let codeMirrorMoveCompletionSelection: typeof import("@codemirror/autocomplete").moveCompletionSelection | null = null;
let codeMirrorSelectFirstCompletion: import("@codemirror/view").Command | null = null; let codeMirrorSelectFirstCompletion: import("@codemirror/view").Command | null = null;
let codeMirrorStartCompletion: typeof import("@codemirror/autocomplete").startCompletion | null = null; let codeMirrorStartCompletion: typeof import("@codemirror/autocomplete").startCompletion | null = null;
@@ -3605,11 +3607,158 @@ interface BatchColumnSelectionActionItem {
type QueryCompletionOption = Completion & { type QueryCompletionOption = Completion & {
dbxBatchColumnSelection?: { sessionKey: string; candidateKey: string }; dbxBatchColumnSelection?: { sessionKey: string; candidateKey: string };
dbxBatchColumnSelectionAction?: { sessionKey: string };
}; };
let batchColumnSelectionSession: BatchColumnSelectionSession | null = null; let batchColumnSelectionSession: BatchColumnSelectionSession | null = null;
type BatchColumnSelectionCheckboxMarker = NonNullable<QueryCompletionOption["dbxBatchColumnSelection"]>;
interface BatchColumnSelectionDragState {
view: EditorViewType;
sessionKey: string;
pointerId: number;
anchorCandidateKey: string;
baseSelectedKeys: Set<string>;
selected: boolean;
focusCandidateKey: string;
previousUserSelect: string;
scrollElement: HTMLElement | null;
pointerClientX: number;
pointerClientY: number;
autoScrollFrame: number;
}
const batchColumnSelectionCheckboxMarkers = new WeakMap<HTMLElement, BatchColumnSelectionCheckboxMarker>();
const batchColumnSelectionActionMarkers = new WeakMap<HTMLElement, string>();
const batchColumnSelectionTooltipParents = new WeakMap<EditorViewType, HTMLElement>();
let batchColumnSelectionDragState: BatchColumnSelectionDragState | null = null;
let batchColumnSelectionRefreshCleanup: (() => void) | null = null;
let batchColumnSelectionExpandedRendering = false;
let batchColumnSelectionRenderLimitTimer: number | null = null;
function cancelBatchColumnSelectionRefresh() {
batchColumnSelectionRefreshCleanup?.();
batchColumnSelectionRefreshCleanup = null;
}
function setBatchColumnSelectionExpandedRendering(expanded: boolean) {
if (batchColumnSelectionExpandedRendering === expanded) return;
batchColumnSelectionExpandedRendering = expanded;
if (batchColumnSelectionRenderLimitTimer !== null) window.clearTimeout(batchColumnSelectionRenderLimitTimer);
const currentView = view.value;
if (!currentView || !completionComp || !buildSqlCompletionExtension) return;
batchColumnSelectionRenderLimitTimer = window.setTimeout(() => {
batchColumnSelectionRenderLimitTimer = null;
if (view.value !== currentView || !completionComp || !buildSqlCompletionExtension) return;
currentView.dispatch({ effects: completionComp.reconfigure(buildSqlCompletionExtension()) });
if (expanded && codeMirrorCompletionStatus?.(currentView.state) === "active") codeMirrorStartCompletion?.(currentView);
}, 0);
}
function setBatchColumnSelectionValue(sessionKey: string, candidateKey: string, selected: boolean, checkbox?: HTMLInputElement) {
const session = batchColumnSelectionSession;
if (!session || session.key !== sessionKey || !session.candidates.some((candidate) => candidate.key === candidateKey)) return;
if (selected) session.selectedKeys.add(candidateKey);
else session.selectedKeys.delete(candidateKey);
if (checkbox) checkbox.checked = selected;
}
function updateBatchColumnSelectionActionLabel(view: EditorViewType, sessionKey: string) {
const session = batchColumnSelectionSession;
if (!session || session.key !== sessionKey) return;
const label = t("editor.completion.insertSelectedColumns", { count: session.selectedKeys.size });
const tooltipParent = batchColumnSelectionTooltipParents.get(view) ?? view.dom;
tooltipParent.querySelectorAll<HTMLElement>(".cm-batch-column-selection-action-marker").forEach((marker) => {
if (batchColumnSelectionActionMarkers.get(marker) !== sessionKey) return;
const element = marker.closest("li")?.querySelector<HTMLElement>(".cm-completionLabel");
if (!element) return;
if (element.textContent !== label) element.textContent = label;
});
}
function scheduleBatchColumnSelectionRefresh(view: EditorViewType, sessionKey: string, focusCandidateKey: string, scrollState?: { element: HTMLElement | null; top: number; left: number }) {
cancelBatchColumnSelectionRefresh();
const preservedScroll =
scrollState ??
(() => {
const element =
visibleBatchColumnSelectionCheckboxes(sessionKey)
.map(({ checkbox }) => batchColumnSelectionScrollElement(checkbox))
.find((candidate): candidate is HTMLElement => !!candidate) ?? null;
return { element, top: element?.scrollTop ?? 0, left: element?.scrollLeft ?? 0 };
})();
const restoreScroll = () => {
const currentElement =
visibleBatchColumnSelectionCheckboxes(sessionKey)
.map(({ checkbox }) => batchColumnSelectionScrollElement(checkbox))
.find((candidate): candidate is HTMLElement => !!candidate) ?? preservedScroll.element;
if (!currentElement) return;
currentElement.scrollTop = Math.min(preservedScroll.top, Math.max(0, currentElement.scrollHeight - currentElement.clientHeight));
currentElement.scrollLeft = Math.min(preservedScroll.left, Math.max(0, currentElement.scrollWidth - currentElement.clientWidth));
};
let restoreAttempts = 0;
let restoreFrame = 0;
let restoreTimer = 0;
let restoreStartTimer = 0;
let stopped = false;
const restoreObserver = typeof MutationObserver === "undefined" ? null : new MutationObserver(restoreScroll);
const stopRestore = () => {
if (stopped) return;
stopped = true;
restoreObserver?.disconnect();
if (restoreFrame) window.cancelAnimationFrame(restoreFrame);
if (restoreTimer) window.clearTimeout(restoreTimer);
if (restoreStartTimer) window.clearTimeout(restoreStartTimer);
if (batchColumnSelectionRefreshCleanup === stopRestore) batchColumnSelectionRefreshCleanup = null;
};
batchColumnSelectionRefreshCleanup = stopRestore;
const restoreNextFrame = () => {
if (batchColumnSelectionSession?.key !== sessionKey) {
stopRestore();
return;
}
restoreScroll();
restoreAttempts += 1;
if (restoreAttempts < 60) restoreFrame = window.requestAnimationFrame(restoreNextFrame);
else stopRestore();
};
restoreStartTimer = window.setTimeout(() => {
if (stopped || batchColumnSelectionSession?.key !== sessionKey) {
stopRestore();
return;
}
restoreObserver?.observe(document.body, { childList: true, subtree: true });
// Keep CodeMirror's virtualized range anchored to the item where the drag ended.
const focusIndex =
codeMirrorCurrentCompletions?.(view.state).findIndex((completion) => {
const marker = (completion as QueryCompletionOption).dbxBatchColumnSelection;
return marker?.sessionKey === sessionKey && marker.candidateKey === focusCandidateKey;
}) ?? -1;
if (focusIndex >= 0 && codeMirrorSetSelectedCompletion) view.dispatch({ effects: codeMirrorSetSelectedCompletion(focusIndex) });
codeMirrorStartCompletion?.(view);
restoreNextFrame();
restoreTimer = window.setTimeout(stopRestore, 1500);
}, 0);
}
function finishBatchColumnSelectionDrag(refresh = true) {
const state = batchColumnSelectionDragState;
if (!state) return;
const scrollState = state.scrollElement ? { element: state.scrollElement, top: state.scrollElement.scrollTop, left: state.scrollElement.scrollLeft } : undefined;
batchColumnSelectionDragState = null;
if (state.autoScrollFrame) window.cancelAnimationFrame(state.autoScrollFrame);
window.removeEventListener("pointermove", onBatchColumnSelectionPointerMove, true);
window.removeEventListener("pointerup", onBatchColumnSelectionPointerUp, true);
window.removeEventListener("pointercancel", onBatchColumnSelectionPointerCancel, true);
window.removeEventListener("blur", onBatchColumnSelectionPointerCancel, true);
document.body.style.userSelect = state.previousUserSelect;
if (refresh) scheduleBatchColumnSelectionRefresh(state.view, state.sessionKey, state.focusCandidateKey, scrollState);
}
function clearBatchColumnSelectionSession() { function clearBatchColumnSelectionSession() {
finishBatchColumnSelectionDrag(false);
cancelBatchColumnSelectionRefresh();
setBatchColumnSelectionExpandedRendering(false);
batchColumnSelectionSession = null; batchColumnSelectionSession = null;
} }
@@ -3624,7 +3773,7 @@ function batchColumnSelectionCandidateKey(item: SqlCompletionItem): string {
function prepareBatchColumnSelectionSession(items: SqlCompletionItem[], document: string, from: number, to: number): BatchColumnSelectionSession | null { function prepareBatchColumnSelectionSession(items: SqlCompletionItem[], document: string, from: number, to: number): BatchColumnSelectionSession | null {
const selectableItems = items.filter((item) => item.type === "column" && item.batchSelectionMode && item.apply); const selectableItems = items.filter((item) => item.type === "column" && item.batchSelectionMode && item.apply);
if (selectableItems.length === 0) { if (selectableItems.length === 0) {
batchColumnSelectionSession = null; clearBatchColumnSelectionSession();
return null; return null;
} }
@@ -3644,9 +3793,11 @@ function prepareBatchColumnSelectionSession(items: SqlCompletionItem[], document
selectedKeys: new Set(), selectedKeys: new Set(),
completionOptions: new Map(), completionOptions: new Map(),
}; };
setBatchColumnSelectionExpandedRendering(true);
return batchColumnSelectionSession; return batchColumnSelectionSession;
} }
setBatchColumnSelectionExpandedRendering(true);
batchColumnSelectionSession.candidates = candidates; batchColumnSelectionSession.candidates = candidates;
batchColumnSelectionSession.qualifier = selectableItems[0]!.batchSelectionQualifier; batchColumnSelectionSession.qualifier = selectableItems[0]!.batchSelectionQualifier;
const candidateKeys = new Set(candidates.map((candidate) => candidate.key)); const candidateKeys = new Set(candidates.map((candidate) => candidate.key));
@@ -3675,13 +3826,10 @@ function cacheBatchColumnSelectionOption(marker: QueryCompletionOption["dbxBatch
function toggleBatchColumnSelection(view: EditorViewType, sessionKey: string, candidateKey: string) { function toggleBatchColumnSelection(view: EditorViewType, sessionKey: string, candidateKey: string) {
const session = batchColumnSelectionSession; const session = batchColumnSelectionSession;
if (!session || session.key !== sessionKey || !session.candidates.some((candidate) => candidate.key === candidateKey)) return; if (!session || session.key !== sessionKey || !session.candidates.some((candidate) => candidate.key === candidateKey)) return;
if (session.selectedKeys.has(candidateKey)) { setBatchColumnSelectionValue(sessionKey, candidateKey, !session.selectedKeys.has(candidateKey));
session.selectedKeys.delete(candidateKey); updateBatchColumnSelectionActionLabel(view, sessionKey);
} else {
session.selectedKeys.add(candidateKey);
}
// Reopen the list so the action row and all virtualized checkboxes reflect the new state. // Reopen the list so the action row and all virtualized checkboxes reflect the new state.
window.setTimeout(() => codeMirrorStartCompletion?.(view), 0); scheduleBatchColumnSelectionRefresh(view, sessionKey, candidateKey);
} }
function toggleSelectedBatchColumnSelection(view: EditorViewType): boolean { function toggleSelectedBatchColumnSelection(view: EditorViewType): boolean {
@@ -3692,6 +3840,143 @@ function toggleSelectedBatchColumnSelection(view: EditorViewType): boolean {
return true; return true;
} }
function batchColumnSelectionMarkerAtPoint(clientX: number, clientY: number): { checkbox: HTMLInputElement; marker: BatchColumnSelectionCheckboxMarker } | null {
const target = document.elementFromPoint(clientX, clientY);
if (!(target instanceof HTMLElement)) return null;
const checkbox = target.closest<HTMLInputElement>("input.cm-batch-column-selection-checkbox") ?? target.closest<HTMLElement>("[role='option']")?.querySelector<HTMLInputElement>("input.cm-batch-column-selection-checkbox") ?? null;
if (!checkbox) return null;
const marker = batchColumnSelectionCheckboxMarkers.get(checkbox);
return marker ? { checkbox, marker } : null;
}
function batchColumnSelectionScrollElement(checkbox: HTMLElement): HTMLElement | null {
let current = checkbox.parentElement;
while (current && current !== document.body) {
const style = window.getComputedStyle(current);
if (current.scrollHeight > current.clientHeight && /(auto|scroll|overlay)/.test(style.overflowY)) return current;
current = current.parentElement;
}
return null;
}
function visibleBatchColumnSelectionCheckboxes(sessionKey: string): Array<{ checkbox: HTMLInputElement; marker: BatchColumnSelectionCheckboxMarker }> {
return Array.from(document.querySelectorAll<HTMLInputElement>("input.cm-batch-column-selection-checkbox")).flatMap((checkbox) => {
const marker = batchColumnSelectionCheckboxMarkers.get(checkbox);
return marker?.sessionKey === sessionKey ? [{ checkbox, marker }] : [];
});
}
function updateBatchColumnSelectionAtPoint(state: BatchColumnSelectionDragState, clientX: number, clientY: number) {
const hit = batchColumnSelectionMarkerAtPoint(clientX, clientY);
if (!hit || hit.marker.sessionKey !== state.sessionKey) return;
state.focusCandidateKey = hit.marker.candidateKey;
const session = batchColumnSelectionSession;
if (!session) return;
const visibleCheckboxes = visibleBatchColumnSelectionCheckboxes(state.sessionKey);
const anchorIndex = visibleCheckboxes.findIndex(({ marker }) => marker.candidateKey === state.anchorCandidateKey);
const focusIndex = visibleCheckboxes.findIndex(({ marker }) => marker.candidateKey === hit.marker.candidateKey);
if (anchorIndex < 0 || focusIndex < 0) return;
const nextSelectedKeys = new Set(state.baseSelectedKeys);
const rangeStart = Math.min(anchorIndex, focusIndex);
const rangeEnd = Math.max(anchorIndex, focusIndex);
for (let index = rangeStart; index <= rangeEnd; index++) {
const candidate = visibleCheckboxes[index]?.marker;
if (!candidate) continue;
if (state.selected) nextSelectedKeys.add(candidate.candidateKey);
else nextSelectedKeys.delete(candidate.candidateKey);
}
session.selectedKeys = nextSelectedKeys;
visibleCheckboxes.forEach(({ checkbox, marker }) => {
checkbox.checked = nextSelectedKeys.has(marker.candidateKey);
});
updateBatchColumnSelectionActionLabel(state.view, state.sessionKey);
}
const BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX = 36;
const BATCH_COLUMN_SELECTION_AUTO_SCROLL_MAX_PX = 20;
function runBatchColumnSelectionAutoScroll() {
const state = batchColumnSelectionDragState;
if (!state) return;
state.autoScrollFrame = 0;
const scroller = state.scrollElement;
if (!scroller) return;
const rect = scroller.getBoundingClientRect();
if (state.pointerClientX < rect.left || state.pointerClientX > rect.right) return;
let delta = 0;
if (state.pointerClientY < rect.top + BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX) {
delta = -BATCH_COLUMN_SELECTION_AUTO_SCROLL_MAX_PX * Math.min(1, (rect.top + BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX - state.pointerClientY) / BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX);
} else if (state.pointerClientY > rect.bottom - BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX) {
delta = BATCH_COLUMN_SELECTION_AUTO_SCROLL_MAX_PX * Math.min(1, (state.pointerClientY - (rect.bottom - BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX)) / BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX);
}
if (!delta) return;
const previousScrollTop = scroller.scrollTop;
scroller.scrollTop = Math.max(0, Math.min(scroller.scrollHeight - scroller.clientHeight, scroller.scrollTop + delta));
if (scroller.scrollTop === previousScrollTop) return;
updateBatchColumnSelectionAtPoint(state, state.pointerClientX, state.pointerClientY);
state.autoScrollFrame = window.requestAnimationFrame(runBatchColumnSelectionAutoScroll);
}
function scheduleBatchColumnSelectionAutoScroll(state: BatchColumnSelectionDragState) {
if (!state.scrollElement || state.autoScrollFrame) return;
state.autoScrollFrame = window.requestAnimationFrame(runBatchColumnSelectionAutoScroll);
}
function onBatchColumnSelectionPointerMove(event: PointerEvent) {
const state = batchColumnSelectionDragState;
if (!state || event.pointerId !== state.pointerId) return;
if ((event.buttons & 1) === 0) {
finishBatchColumnSelectionDrag();
return;
}
state.pointerClientX = event.clientX;
state.pointerClientY = event.clientY;
updateBatchColumnSelectionAtPoint(state, event.clientX, event.clientY);
scheduleBatchColumnSelectionAutoScroll(state);
}
function onBatchColumnSelectionPointerUp(event: PointerEvent) {
const state = batchColumnSelectionDragState;
if (!state || event.pointerId !== state.pointerId) return;
finishBatchColumnSelectionDrag();
}
function onBatchColumnSelectionPointerCancel() {
finishBatchColumnSelectionDrag();
}
function startBatchColumnSelectionDrag(view: EditorViewType, marker: BatchColumnSelectionCheckboxMarker, checkbox: HTMLInputElement, event: PointerEvent) {
if (event.button !== 0 || !event.isPrimary) return;
finishBatchColumnSelectionDrag(false);
cancelBatchColumnSelectionRefresh();
const session = batchColumnSelectionSession;
if (!session || session.key !== marker.sessionKey) return;
if (!session.candidates.some((candidate) => candidate.key === marker.candidateKey)) return;
const selected = !session.selectedKeys.has(marker.candidateKey);
const baseSelectedKeys = new Set(session.selectedKeys);
setBatchColumnSelectionValue(marker.sessionKey, marker.candidateKey, selected, checkbox);
batchColumnSelectionDragState = {
view,
sessionKey: marker.sessionKey,
pointerId: event.pointerId,
anchorCandidateKey: marker.candidateKey,
baseSelectedKeys,
selected,
focusCandidateKey: marker.candidateKey,
previousUserSelect: document.body.style.userSelect,
scrollElement: batchColumnSelectionScrollElement(checkbox),
pointerClientX: event.clientX,
pointerClientY: event.clientY,
autoScrollFrame: 0,
};
updateBatchColumnSelectionActionLabel(view, marker.sessionKey);
document.body.style.userSelect = "none";
window.addEventListener("pointermove", onBatchColumnSelectionPointerMove, true);
window.addEventListener("pointerup", onBatchColumnSelectionPointerUp, true);
window.addEventListener("pointercancel", onBatchColumnSelectionPointerCancel, true);
window.addEventListener("blur", onBatchColumnSelectionPointerCancel, true);
}
function renderBatchColumnSelectionCheckbox(completion: Completion, _state: import("@codemirror/state").EditorState, currentView: EditorViewType): Node | null { function renderBatchColumnSelectionCheckbox(completion: Completion, _state: import("@codemirror/state").EditorState, currentView: EditorViewType): Node | null {
const marker = (completion as QueryCompletionOption).dbxBatchColumnSelection; const marker = (completion as QueryCompletionOption).dbxBatchColumnSelection;
const session = batchColumnSelectionSession; const session = batchColumnSelectionSession;
@@ -3703,12 +3988,17 @@ function renderBatchColumnSelectionCheckbox(completion: Completion, _state: impo
checkbox.checked = session.selectedKeys.has(marker.candidateKey); checkbox.checked = session.selectedKeys.has(marker.candidateKey);
checkbox.tabIndex = -1; checkbox.tabIndex = -1;
checkbox.setAttribute("aria-label", completion.displayLabel ?? completion.label); checkbox.setAttribute("aria-label", completion.displayLabel ?? completion.label);
batchColumnSelectionCheckboxMarkers.set(checkbox, marker);
checkbox.addEventListener("pointerdown", (event) => {
event.preventDefault();
event.stopPropagation();
startBatchColumnSelectionDrag(currentView, marker, checkbox, event);
});
checkbox.addEventListener("mousedown", (event) => { checkbox.addEventListener("mousedown", (event) => {
// CodeMirror accepts the completion on a list-item mousedown. Keep this // CodeMirror accepts the completion on a list-item mousedown. Keep this
// interaction local to the checkbox so a field can be toggled repeatedly. // interaction local to the checkbox so a field can be toggled repeatedly.
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
toggleBatchColumnSelection(currentView, marker.sessionKey, marker.candidateKey);
}); });
checkbox.addEventListener("click", (event) => { checkbox.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
@@ -3717,6 +4007,16 @@ function renderBatchColumnSelectionCheckbox(completion: Completion, _state: impo
return checkbox; return checkbox;
} }
function renderBatchColumnSelectionActionMarker(completion: Completion): Node | null {
const action = (completion as QueryCompletionOption).dbxBatchColumnSelectionAction;
if (!action) return null;
const marker = document.createElement("span");
marker.className = "cm-batch-column-selection-action-marker";
marker.hidden = true;
batchColumnSelectionActionMarkers.set(marker, action.sessionKey);
return marker;
}
function applyBatchColumnSelection(view: EditorViewType, item: BatchColumnSelectionActionItem, from: number, to: number) { function applyBatchColumnSelection(view: EditorViewType, item: BatchColumnSelectionActionItem, from: number, to: number) {
const session = batchColumnSelectionSession; const session = batchColumnSelectionSession;
if (!session || session.key !== item.sessionKey || session.document !== view.state.doc.toString()) return; if (!session || session.key !== item.sessionKey || session.document !== view.state.doc.toString()) return;
@@ -3799,7 +4099,7 @@ function isTypedCompletionActivation(explicit: boolean) {
} }
function markCompletionAccepted(item: QueryCompletionItem | BatchColumnSelectionActionItem) { function markCompletionAccepted(item: QueryCompletionItem | BatchColumnSelectionActionItem) {
batchColumnSelectionSession = null; clearBatchColumnSelectionSession();
const shouldContinueCompletion = shouldChainSqlCompletionAfterAccept(item) || (props.databaseType === "sqlserver" && item.type === "keyword" && item.label.toUpperCase() === "USE"); const shouldContinueCompletion = shouldChainSqlCompletionAfterAccept(item) || (props.databaseType === "sqlserver" && item.type === "keyword" && item.label.toUpperCase() === "USE");
suppressNextSqlCompletionAutoStartUntil = shouldContinueCompletion ? 0 : Date.now() + 750; suppressNextSqlCompletionAutoStartUntil = shouldContinueCompletion ? 0 : Date.now() + 750;
completionEpoch++; completionEpoch++;
@@ -3910,6 +4210,7 @@ function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectio
if (isBatchColumnSelectionAction(item)) { if (isBatchColumnSelectionAction(item)) {
return { return {
...labelPresentation, ...labelPresentation,
dbxBatchColumnSelectionAction: { sessionKey: item.sessionKey },
type: item.type, type: item.type,
detail: item.detail, detail: item.detail,
boost: item.boost, boost: item.boost,
@@ -5206,7 +5507,24 @@ onMounted(async () => {
}, },
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField }, { EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
langSql, langSql,
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion, moveCompletionSelection, selectedCompletion, selectedCompletionIndex }, {
autocompletion,
startCompletion,
acceptCompletion,
closeBrackets,
closeBracketsKeymap,
snippetCompletion,
completionStatus,
completionKeymap,
insertCompletionText,
nextSnippetField,
closeCompletion,
moveCompletionSelection,
selectedCompletion,
selectedCompletionIndex,
currentCompletions,
setSelectedCompletion,
},
{ copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, toggleBlockComment, history, defaultKeymap, historyKeymap }, { 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 }, { bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap, toggleFold, ensureSyntaxTree, highlightingFor, syntaxTree },
{ searchKeymap }, { searchKeymap },
@@ -5243,8 +5561,10 @@ onMounted(async () => {
setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>(); setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>();
codeMirrorCompletionStatus = completionStatus; codeMirrorCompletionStatus = completionStatus;
codeMirrorAcceptCompletion = acceptCompletion; codeMirrorAcceptCompletion = acceptCompletion;
codeMirrorCurrentCompletions = currentCompletions;
codeMirrorSelectedCompletion = selectedCompletion; codeMirrorSelectedCompletion = selectedCompletion;
codeMirrorSelectedCompletionIndex = selectedCompletionIndex; codeMirrorSelectedCompletionIndex = selectedCompletionIndex;
codeMirrorSetSelectedCompletion = setSelectedCompletion;
codeMirrorMoveCompletionSelection = moveCompletionSelection; codeMirrorMoveCompletionSelection = moveCompletionSelection;
codeMirrorSelectFirstCompletion = moveCompletionSelection(true); codeMirrorSelectFirstCompletion = moveCompletionSelection(true);
codeMirrorCloseCompletion = closeCompletion; codeMirrorCloseCompletion = closeCompletion;
@@ -5483,7 +5803,13 @@ onMounted(async () => {
defaultKeymap: false, defaultKeymap: false,
selectOnOpen: settingsStore.editorSettings.selectFirstCompletionOnOpen, selectOnOpen: settingsStore.editorSettings.selectFirstCompletionOnOpen,
compareCompletions: (a, b) => compareSqlCompletions(a, b, settingsStore.editorSettings.sortCompletionColumnsAlphabetically), compareCompletions: (a, b) => compareSqlCompletions(a, b, settingsStore.editorSettings.sortCompletionColumnsAlphabetically),
addToOptions: [{ position: 10, render: renderBatchColumnSelectionCheckbox }], // Keep normal completion lists virtualized; batch-field selection needs every row in the DOM.
maxRenderedOptions: batchColumnSelectionExpandedRendering ? Number.MAX_SAFE_INTEGER : 100,
optionClass: (completion) => ((completion as QueryCompletionOption).dbxBatchColumnSelectionAction ? "cm-batch-column-selection-action" : ""),
addToOptions: [
{ position: 5, render: renderBatchColumnSelectionActionMarker },
{ position: 10, render: renderBatchColumnSelectionCheckbox },
],
override: [async (context: CompletionContext) => provideSqlCompletions(context)], override: [async (context: CompletionContext) => provideSqlCompletions(context)],
}); });
@@ -6156,6 +6482,7 @@ onMounted(async () => {
}); });
view.value = new EditorView({ state, parent: editorElement }); view.value = new EditorView({ state, parent: editorElement });
batchColumnSelectionTooltipParents.set(view.value, tooltipParent);
postCompositionKeyGuardCleanup = postCompositionKeyGuard.attach(view.value.contentDOM); postCompositionKeyGuardCleanup = postCompositionKeyGuard.attach(view.value.contentDOM);
registerEditorScrollbarPointerGuard(view.value); registerEditorScrollbarPointerGuard(view.value);
view.value.scrollDOM.addEventListener("scroll", scheduleEditorViewportEmit, { view.value.scrollDOM.addEventListener("scroll", scheduleEditorViewportEmit, {
@@ -6450,6 +6777,8 @@ watch(
); );
function pauseQueryEditorBackgroundWork() { function pauseQueryEditorBackgroundWork() {
finishBatchColumnSelectionDrag(false);
cancelBatchColumnSelectionRefresh();
flushEditorViewport(); flushEditorViewport();
flushEditorSelection(); flushEditorSelection();
clearTableNavigationHover(); clearTableNavigationHover();
@@ -127,4 +127,41 @@ describe("QueryEditor batch column selection", () => {
expect(source).toContain('{ key: "ArrowDown", run: (view) => moveCompletion(view, true) }'); expect(source).toContain('{ key: "ArrowDown", run: (view) => moveCompletion(view, true) }');
expect(source).toContain('{ key: "ArrowUp", run: (view) => moveCompletion(view, false) }'); expect(source).toContain('{ key: "ArrowUp", run: (view) => moveCompletion(view, false) }');
}); });
it("marks the insertion action so the completion menu can keep it sticky", () => {
expect(source).toContain("dbxBatchColumnSelectionAction: { sessionKey: item.sessionKey }");
expect(source).toContain('optionClass: (completion) => ((completion as QueryCompletionOption).dbxBatchColumnSelectionAction ? "cm-batch-column-selection-action" : "")');
});
it("updates the insertion count while a mouse selection is still in progress", () => {
const dragUpdateStart = source.indexOf("function updateBatchColumnSelectionAtPoint");
const dragUpdateEnd = source.indexOf("\n}\n\nconst BATCH_COLUMN_SELECTION_AUTO_SCROLL_EDGE_PX", dragUpdateStart);
expect(source).toContain("function updateBatchColumnSelectionActionLabel");
expect(source.slice(dragUpdateStart, dragUpdateEnd)).toContain("updateBatchColumnSelectionActionLabel(state.view, state.sessionKey);");
expect(source).toContain("const batchColumnSelectionTooltipParents = new WeakMap<EditorViewType, HTMLElement>();");
expect(source).toContain("batchColumnSelectionTooltipParents.set(view.value, tooltipParent);");
expect(source).toContain("const batchColumnSelectionActionMarkers = new WeakMap<HTMLElement, string>();");
expect(source).toContain("batchColumnSelectionActionMarkers.get(marker) !== sessionKey");
expect(source).toContain("renderBatchColumnSelectionActionMarker");
});
it("cancels stale scroll restoration before a new drag can begin", () => {
const refreshStart = source.indexOf("function scheduleBatchColumnSelectionRefresh");
const refreshEnd = source.indexOf("\n}\n\nfunction finishBatchColumnSelectionDrag", refreshStart);
const dragStart = source.indexOf("function startBatchColumnSelectionDrag");
const dragEnd = source.indexOf("\n}\n\nfunction renderBatchColumnSelectionCheckbox", dragStart);
expect(source).toContain("let batchColumnSelectionRefreshCleanup: (() => void) | null = null;");
expect(source.slice(refreshStart, refreshEnd)).toContain("cancelBatchColumnSelectionRefresh();");
expect(source.slice(dragStart, dragEnd)).toContain("cancelBatchColumnSelectionRefresh();");
expect(source).toContain("if (restoreStartTimer) window.clearTimeout(restoreStartTimer);");
});
it("only expands rendering for batch field selection", () => {
expect(source).toContain("function setBatchColumnSelectionExpandedRendering");
expect(source).toContain("setBatchColumnSelectionExpandedRendering(true);");
expect(source).toContain("setBatchColumnSelectionExpandedRendering(false);");
expect(source).toContain("maxRenderedOptions: batchColumnSelectionExpandedRendering ? Number.MAX_SAFE_INTEGER : 100,");
});
}); });
+10
View File
@@ -162,6 +162,16 @@ describe("SQL completion theme", () => {
expect(rules[".cm-completionLabel"]).toMatchObject({ flex: "0 1 auto" }); expect(rules[".cm-completionLabel"]).toMatchObject({ flex: "0 1 auto" });
expect(rules[".cm-completionDetail"]).toMatchObject({ flex: "1 1 0", minWidth: "0", textOverflow: "ellipsis" }); expect(rules[".cm-completionDetail"]).toMatchObject({ flex: "1 1 0", minWidth: "0", textOverflow: "ellipsis" });
}); });
it("keeps the batch column insertion action fixed at the bottom of its menu", () => {
const rules = buildSqlCompletionThemeRules();
expect(rules[".cm-tooltip.cm-tooltip-autocomplete > ul > li.cm-batch-column-selection-action"]).toMatchObject({
bottom: "0",
position: "sticky",
zIndex: "1",
});
});
}); });
describe("editor gutters", () => { describe("editor gutters", () => {
@@ -973,6 +973,15 @@ export function buildSqlCompletionThemeRules(): CodeMirrorStyleSpec {
color: "var(--popover-foreground) !important", color: "var(--popover-foreground) !important",
outline: colorMixValue("1px solid var(--border)", "1px solid color-mix(in oklch, var(--primary) 22%, transparent)"), outline: colorMixValue("1px solid var(--border)", "1px solid color-mix(in oklch, var(--primary) 22%, transparent)"),
}, },
".cm-tooltip.cm-tooltip-autocomplete > ul > li.cm-batch-column-selection-action": {
background: "var(--popover)",
borderRadius: "0",
borderTop: colorMixValue("1px solid var(--border)", "1px solid color-mix(in oklch, var(--border) 82%, var(--foreground) 18%)"),
bottom: "0",
boxShadow: "0 -6px 12px rgb(0 0 0 / 0.06)",
position: "sticky",
zIndex: "1",
},
".cm-completionIcon": { ".cm-completionIcon": {
alignItems: "center", alignItems: "center",
display: "inline-flex", display: "inline-flex",