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 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,");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user