From 68c8a5cc7b25e67dcaa2fa4ebaaf5d352c0e5391 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 27 May 2026 16:05:30 +0800 Subject: [PATCH] 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 --- .../src/components/editor/QueryEditor.vue | 90 +------------------ apps/desktop/src/components/grid/DataGrid.vue | 2 +- .../src/composables/useCellDetailEditor.ts | 2 + .../src/lib/codemirrorVscodeSelectionLayer.ts | 88 ++++++++++++++++++ 4 files changed, 93 insertions(+), 89 deletions(-) create mode 100644 apps/desktop/src/lib/codemirrorVscodeSelectionLayer.ts diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 9cea64115..f317a2415 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -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[] = []; - 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(), diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 1c4dd2fe8..6b8362755 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -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" />
diff --git a/apps/desktop/src/composables/useCellDetailEditor.ts b/apps/desktop/src/composables/useCellDetailEditor.ts index f7f65ab4e..20ee9ed65 100644 --- a/apps/desktop/src/composables/useCellDetailEditor.ts +++ b/apps/desktop/src/composables/useCellDetailEditor.ts @@ -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({ diff --git a/apps/desktop/src/lib/codemirrorVscodeSelectionLayer.ts b/apps/desktop/src/lib/codemirrorVscodeSelectionLayer.ts new file mode 100644 index 000000000..c195998da --- /dev/null +++ b/apps/desktop/src/lib/codemirrorVscodeSelectionLayer.ts @@ -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[] = []; + 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; + }, + }); +}