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:
t8y2 2026-05-27 16:05:30 +08:00
parent 84e2093b82
commit 68c8a5cc7b
4 changed files with 93 additions and 89 deletions

View File

@ -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(),

View File

@ -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">

View File

@ -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({

View File

@ -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;
},
});
}