feat(editor): batch-select completion fields via drag
This commit is contained in:
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 codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | 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 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 codeMirrorSelectFirstCompletion: import("@codemirror/view").Command | null = null;
|
||||
let codeMirrorStartCompletion: typeof import("@codemirror/autocomplete").startCompletion | null = null;
|
||||
@@ -3605,11 +3607,158 @@ interface BatchColumnSelectionActionItem {
|
||||
|
||||
type QueryCompletionOption = Completion & {
|
||||
dbxBatchColumnSelection?: { sessionKey: string; candidateKey: string };
|
||||
dbxBatchColumnSelectionAction?: { sessionKey: string };
|
||||
};
|
||||
|
||||
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() {
|
||||
finishBatchColumnSelectionDrag(false);
|
||||
cancelBatchColumnSelectionRefresh();
|
||||
setBatchColumnSelectionExpandedRendering(false);
|
||||
batchColumnSelectionSession = null;
|
||||
}
|
||||
|
||||
@@ -3624,7 +3773,7 @@ function batchColumnSelectionCandidateKey(item: SqlCompletionItem): string {
|
||||
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;
|
||||
clearBatchColumnSelectionSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3644,9 +3793,11 @@ function prepareBatchColumnSelectionSession(items: SqlCompletionItem[], document
|
||||
selectedKeys: new Set(),
|
||||
completionOptions: new Map(),
|
||||
};
|
||||
setBatchColumnSelectionExpandedRendering(true);
|
||||
return batchColumnSelectionSession;
|
||||
}
|
||||
|
||||
setBatchColumnSelectionExpandedRendering(true);
|
||||
batchColumnSelectionSession.candidates = candidates;
|
||||
batchColumnSelectionSession.qualifier = selectableItems[0]!.batchSelectionQualifier;
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
setBatchColumnSelectionValue(sessionKey, candidateKey, !session.selectedKeys.has(candidateKey));
|
||||
updateBatchColumnSelectionActionLabel(view, sessionKey);
|
||||
// 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 {
|
||||
@@ -3692,6 +3840,143 @@ function toggleSelectedBatchColumnSelection(view: EditorViewType): boolean {
|
||||
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 {
|
||||
const marker = (completion as QueryCompletionOption).dbxBatchColumnSelection;
|
||||
const session = batchColumnSelectionSession;
|
||||
@@ -3703,12 +3988,17 @@ function renderBatchColumnSelectionCheckbox(completion: Completion, _state: impo
|
||||
checkbox.checked = session.selectedKeys.has(marker.candidateKey);
|
||||
checkbox.tabIndex = -1;
|
||||
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) => {
|
||||
// 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();
|
||||
@@ -3717,6 +4007,16 @@ function renderBatchColumnSelectionCheckbox(completion: Completion, _state: impo
|
||||
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) {
|
||||
const session = batchColumnSelectionSession;
|
||||
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) {
|
||||
batchColumnSelectionSession = null;
|
||||
clearBatchColumnSelectionSession();
|
||||
const shouldContinueCompletion = shouldChainSqlCompletionAfterAccept(item) || (props.databaseType === "sqlserver" && item.type === "keyword" && item.label.toUpperCase() === "USE");
|
||||
suppressNextSqlCompletionAutoStartUntil = shouldContinueCompletion ? 0 : Date.now() + 750;
|
||||
completionEpoch++;
|
||||
@@ -3910,6 +4210,7 @@ function completionOptionForItem(item: QueryCompletionItem | BatchColumnSelectio
|
||||
if (isBatchColumnSelectionAction(item)) {
|
||||
return {
|
||||
...labelPresentation,
|
||||
dbxBatchColumnSelectionAction: { sessionKey: item.sessionKey },
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
@@ -5206,7 +5507,24 @@ onMounted(async () => {
|
||||
},
|
||||
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
|
||||
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 },
|
||||
{ bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap, toggleFold, ensureSyntaxTree, highlightingFor, syntaxTree },
|
||||
{ searchKeymap },
|
||||
@@ -5243,8 +5561,10 @@ onMounted(async () => {
|
||||
setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>();
|
||||
codeMirrorCompletionStatus = completionStatus;
|
||||
codeMirrorAcceptCompletion = acceptCompletion;
|
||||
codeMirrorCurrentCompletions = currentCompletions;
|
||||
codeMirrorSelectedCompletion = selectedCompletion;
|
||||
codeMirrorSelectedCompletionIndex = selectedCompletionIndex;
|
||||
codeMirrorSetSelectedCompletion = setSelectedCompletion;
|
||||
codeMirrorMoveCompletionSelection = moveCompletionSelection;
|
||||
codeMirrorSelectFirstCompletion = moveCompletionSelection(true);
|
||||
codeMirrorCloseCompletion = closeCompletion;
|
||||
@@ -5483,7 +5803,13 @@ onMounted(async () => {
|
||||
defaultKeymap: false,
|
||||
selectOnOpen: settingsStore.editorSettings.selectFirstCompletionOnOpen,
|
||||
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)],
|
||||
});
|
||||
|
||||
@@ -6156,6 +6482,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
view.value = new EditorView({ state, parent: editorElement });
|
||||
batchColumnSelectionTooltipParents.set(view.value, tooltipParent);
|
||||
postCompositionKeyGuardCleanup = postCompositionKeyGuard.attach(view.value.contentDOM);
|
||||
registerEditorScrollbarPointerGuard(view.value);
|
||||
view.value.scrollDOM.addEventListener("scroll", scheduleEditorViewportEmit, {
|
||||
@@ -6450,6 +6777,8 @@ watch(
|
||||
);
|
||||
|
||||
function pauseQueryEditorBackgroundWork() {
|
||||
finishBatchColumnSelectionDrag(false);
|
||||
cancelBatchColumnSelectionRefresh();
|
||||
flushEditorViewport();
|
||||
flushEditorSelection();
|
||||
clearTableNavigationHover();
|
||||
|
||||
@@ -127,4 +127,41 @@ describe("QueryEditor batch column selection", () => {
|
||||
expect(source).toContain('{ key: "ArrowDown", run: (view) => moveCompletion(view, true) }');
|
||||
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,");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user