fix: fix value editor scrolling and add selection highlight
- Fix value editor not scrollable when content exceeds container (overflow-hidden -> overflow-auto) - Extract vscodeSelectionLayer from QueryEditor into shared module and apply to value editor for consistent selection highlighting
This commit is contained in:
parent
84e2093b82
commit
68c8a5cc7b
|
|
@ -44,6 +44,7 @@ import {
|
|||
} from "@/lib/sqlSemanticDiagnostics";
|
||||
import type { SqlCompletionColumn } from "@/lib/sqlCompletion";
|
||||
import type { SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
|
|
@ -934,8 +935,6 @@ onMounted(async () => {
|
|||
dropCursor,
|
||||
crosshairCursor,
|
||||
ViewPlugin,
|
||||
layer,
|
||||
RectangleMarker,
|
||||
},
|
||||
{ EditorState, Compartment, Prec, StateEffect, StateField },
|
||||
{ sql, MSSQL, MySQL, PostgreSQL, SQLDialect },
|
||||
|
|
@ -1069,91 +1068,6 @@ onMounted(async () => {
|
|||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
|
||||
function getLineElement(view: import("@codemirror/view").EditorView, pos: number): HTMLElement | null {
|
||||
try {
|
||||
const domAt = view.domAtPos(pos);
|
||||
let node: Node | null = domAt.node;
|
||||
while (node) {
|
||||
if (node instanceof HTMLElement && node.classList.contains("cm-line")) return node;
|
||||
node = node.parentElement;
|
||||
}
|
||||
} catch {
|
||||
// domAtPos may throw for positions outside the viewport
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const vscodeSelectionLayer = layer({
|
||||
above: false,
|
||||
class: "cm-vscodeSelectionLayer",
|
||||
markers(view) {
|
||||
const markers: InstanceType<typeof RectangleMarker>[] = [];
|
||||
const contentRect = view.contentDOM.getBoundingClientRect();
|
||||
const baseRect = view.scrollDOM.getBoundingClientRect();
|
||||
const base = {
|
||||
left: baseRect.left - view.scrollDOM.scrollLeft * view.scaleX,
|
||||
top: baseRect.top - view.scrollDOM.scrollTop * view.scaleY,
|
||||
};
|
||||
const lineElt = view.contentDOM.querySelector(".cm-line");
|
||||
const lineStyle = lineElt && window.getComputedStyle(lineElt);
|
||||
const contentLeft =
|
||||
contentRect.left +
|
||||
(lineStyle ? parseInt(lineStyle.paddingLeft) + Math.min(0, parseInt(lineStyle.textIndent)) : 0);
|
||||
// single character width for empty-line fallback
|
||||
const sampleCoords = view.coordsAtPos(view.viewport.from);
|
||||
const charWidth = sampleCoords ? sampleCoords.right - sampleCoords.left : 8;
|
||||
for (const r of view.state.selection.ranges) {
|
||||
if (r.empty) continue;
|
||||
const fromLine = view.lineBlockAt(r.from);
|
||||
const toLine = view.lineBlockAt(r.to);
|
||||
for (let pos = fromLine.from; pos <= toLine.from; ) {
|
||||
const line = view.lineBlockAt(pos);
|
||||
const lineEl = getLineElement(view, line.from);
|
||||
if (!lineEl) break;
|
||||
const lineRect = lineEl.getBoundingClientRect();
|
||||
let left = contentLeft - base.left;
|
||||
let right: number;
|
||||
const isFirst = line.from === fromLine.from;
|
||||
const isLast = line.from === toLine.from;
|
||||
if (isFirst) {
|
||||
const c = view.coordsAtPos(r.from);
|
||||
if (c) left = c.left - base.left;
|
||||
}
|
||||
if (isLast) {
|
||||
const c = view.coordsAtPos(r.to);
|
||||
right = c ? c.left - base.left : contentLeft - base.left;
|
||||
} else {
|
||||
// not the last line: measure text content width from DOM
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(lineEl);
|
||||
const textRect = range.getBoundingClientRect();
|
||||
const textRight = textRect.right - base.left;
|
||||
right = textRight > left ? textRight : left + charWidth;
|
||||
}
|
||||
const w = right! - left;
|
||||
if (w > 0) {
|
||||
markers.push(
|
||||
new RectangleMarker(
|
||||
"cm-vscodeSelection",
|
||||
left,
|
||||
lineRect.top - base.top,
|
||||
w,
|
||||
lineRect.bottom - lineRect.top,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isLast) break;
|
||||
pos = line.to + 1;
|
||||
if (pos > view.state.doc.length) break;
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
},
|
||||
update(update, _dom) {
|
||||
return update.docChanged || update.selectionSet || update.viewportChanged;
|
||||
},
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions: [
|
||||
|
|
@ -1171,7 +1085,7 @@ onMounted(async () => {
|
|||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
vscodeSelectionLayer,
|
||||
vscodeSelectionLayer(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
|
|
|
|||
|
|
@ -5299,7 +5299,7 @@ defineExpose({
|
|||
v-else
|
||||
ref="valueEditorContainer"
|
||||
data-cell-detail-editor-root
|
||||
class="min-h-0 flex-1 w-full rounded border overflow-hidden"
|
||||
class="min-h-0 flex-1 w-full rounded border overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-1 mt-2 shrink-0">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
highlightSpecialChars,
|
||||
highlightActiveLine,
|
||||
} from "@codemirror/view";
|
||||
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
|
|
@ -81,6 +82,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
|||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
vscodeSelectionLayer(),
|
||||
dropCursor(),
|
||||
highlightActiveLine(),
|
||||
EditorView.theme({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { layer, RectangleMarker, type EditorView } from "@codemirror/view";
|
||||
|
||||
function getLineElement(view: EditorView, pos: number): HTMLElement | null {
|
||||
try {
|
||||
const domAt = view.domAtPos(pos);
|
||||
let node: Node | null = domAt.node;
|
||||
while (node) {
|
||||
if (node instanceof HTMLElement && node.classList.contains("cm-line")) return node;
|
||||
node = node.parentElement;
|
||||
}
|
||||
} catch {
|
||||
// domAtPos may throw for positions outside the viewport
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function vscodeSelectionLayer() {
|
||||
return layer({
|
||||
above: false,
|
||||
class: "cm-vscodeSelectionLayer",
|
||||
markers(view) {
|
||||
const markers: InstanceType<typeof RectangleMarker>[] = [];
|
||||
const contentRect = view.contentDOM.getBoundingClientRect();
|
||||
const baseRect = view.scrollDOM.getBoundingClientRect();
|
||||
const base = {
|
||||
left: baseRect.left - view.scrollDOM.scrollLeft * view.scaleX,
|
||||
top: baseRect.top - view.scrollDOM.scrollTop * view.scaleY,
|
||||
};
|
||||
const lineElt = view.contentDOM.querySelector(".cm-line");
|
||||
const lineStyle = lineElt && window.getComputedStyle(lineElt);
|
||||
const contentLeft =
|
||||
contentRect.left +
|
||||
(lineStyle ? parseInt(lineStyle.paddingLeft) + Math.min(0, parseInt(lineStyle.textIndent)) : 0);
|
||||
// single character width for empty-line fallback
|
||||
const sampleCoords = view.coordsAtPos(view.viewport.from);
|
||||
const charWidth = sampleCoords ? sampleCoords.right - sampleCoords.left : 8;
|
||||
for (const r of view.state.selection.ranges) {
|
||||
if (r.empty) continue;
|
||||
const fromLine = view.lineBlockAt(r.from);
|
||||
const toLine = view.lineBlockAt(r.to);
|
||||
for (let pos = fromLine.from; pos <= toLine.from; ) {
|
||||
const line = view.lineBlockAt(pos);
|
||||
const lineEl = getLineElement(view, line.from);
|
||||
if (!lineEl) break;
|
||||
const lineRect = lineEl.getBoundingClientRect();
|
||||
let left = contentLeft - base.left;
|
||||
let right: number;
|
||||
const isFirst = line.from === fromLine.from;
|
||||
const isLast = line.from === toLine.from;
|
||||
if (isFirst) {
|
||||
const c = view.coordsAtPos(r.from);
|
||||
if (c) left = c.left - base.left;
|
||||
}
|
||||
if (isLast) {
|
||||
const c = view.coordsAtPos(r.to);
|
||||
right = c ? c.left - base.left : contentLeft - base.left;
|
||||
} else {
|
||||
// not the last line: measure text content width from DOM
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(lineEl);
|
||||
const textRect = range.getBoundingClientRect();
|
||||
const textRight = textRect.right - base.left;
|
||||
right = textRight > left ? textRight : left + charWidth;
|
||||
}
|
||||
const w = right! - left;
|
||||
if (w > 0) {
|
||||
markers.push(
|
||||
new RectangleMarker(
|
||||
"cm-vscodeSelection",
|
||||
left,
|
||||
lineRect.top - base.top,
|
||||
w,
|
||||
lineRect.bottom - lineRect.top,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isLast) break;
|
||||
pos = line.to + 1;
|
||||
if (pos > view.state.doc.length) break;
|
||||
}
|
||||
}
|
||||
return markers;
|
||||
},
|
||||
update(update, _dom) {
|
||||
return update.docChanged || update.selectionSet || update.viewportChanged;
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue