fix(editor): improve selection rendering
This commit is contained in:
parent
e3087e0954
commit
15b792a424
|
|
@ -66,6 +66,7 @@ import {
|
|||
fontSizeFromWheelDelta,
|
||||
} from "@/lib/editorZoom";
|
||||
import { shortcutToCodeMirrorKey } from "@/lib/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/codemirrorTrimmedSelectionLayer";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
areSqlSemanticDiagnosticsEqual,
|
||||
|
|
@ -87,7 +88,6 @@ import type {
|
|||
SqlTableReference,
|
||||
SqlTextSpan,
|
||||
} from "@/types/database";
|
||||
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
|
|
@ -1486,7 +1486,7 @@ onMounted(async () => {
|
|||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
vscodeSelectionLayer(),
|
||||
trimmedSelectionLayer(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import {
|
|||
highlightSpecialChars,
|
||||
highlightActiveLine,
|
||||
} from "@codemirror/view";
|
||||
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { search as cmSearch } from "@codemirror/search";
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
import { trimmedSelectionLayer } from "@/lib/codemirrorTrimmedSelectionLayer";
|
||||
import {
|
||||
EDITOR_FONT_FAMILY_CSS_VAR,
|
||||
EDITOR_FONT_SIZE_CSS_VAR,
|
||||
|
|
@ -192,7 +192,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
|||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
vscodeSelectionLayer(),
|
||||
trimmedSelectionLayer(),
|
||||
dropCursor(),
|
||||
highlightActiveLine(),
|
||||
EditorView.theme({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import { layer, RectangleMarker, type BlockInfo, type EditorView } from "@codemirror/view";
|
||||
|
||||
const MIN_EMPTY_LINE_WIDTH = 14;
|
||||
const END_OF_LINE_PADDING = 3;
|
||||
const CONTIGUOUS_LINE_GAP = 1.5;
|
||||
const CORNER_COVERAGE_GAP = 1;
|
||||
|
||||
type TrimmedSelectionRect = {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function layerBase(view: EditorView) {
|
||||
const rect = view.scrollDOM.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left - view.scrollDOM.scrollLeft * view.scaleX,
|
||||
top: rect.top - view.scrollDOM.scrollTop * view.scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function markerForLineRange(
|
||||
view: EditorView,
|
||||
from: number,
|
||||
to: number,
|
||||
lineFrom: number,
|
||||
lineTo: number,
|
||||
lineBlock: BlockInfo,
|
||||
includesLineBreak: boolean,
|
||||
base: { left: number; top: number },
|
||||
): TrimmedSelectionRect | null {
|
||||
const start = view.coordsAtPos(from, 1);
|
||||
const end = from < to ? view.coordsAtPos(to, -1) : start;
|
||||
const lineStart = view.coordsAtPos(lineFrom, 1) ?? start;
|
||||
const lineEnd = view.coordsAtPos(lineTo, -1) ?? end;
|
||||
if (!start || !end || !lineStart || !lineEnd) return null;
|
||||
|
||||
const left = from < to ? Math.min(start.left, end.left) : lineStart.left;
|
||||
let right = from < to ? Math.max(start.right, end.right) : left + MIN_EMPTY_LINE_WIDTH;
|
||||
if (includesLineBreak && to >= lineTo) {
|
||||
right = Math.max(right, lineEnd.right + END_OF_LINE_PADDING);
|
||||
}
|
||||
|
||||
const top = view.documentTop + lineBlock.top;
|
||||
const bottom = top + lineBlock.height;
|
||||
|
||||
return {
|
||||
left: left - base.left,
|
||||
top: top - base.top,
|
||||
width: Math.max(1, right - left),
|
||||
height: Math.max(1, bottom - top),
|
||||
};
|
||||
}
|
||||
|
||||
function coversX(rect: TrimmedSelectionRect | undefined, x: number): boolean {
|
||||
if (!rect) return false;
|
||||
return rect.left <= x + CORNER_COVERAGE_GAP && rect.left + rect.width >= x - CORNER_COVERAGE_GAP;
|
||||
}
|
||||
|
||||
function markerClass(rects: TrimmedSelectionRect[], index: number): string {
|
||||
const prev = rects[index - 1];
|
||||
const current = rects[index];
|
||||
const next = rects[index + 1];
|
||||
const touchesPrev = !!prev && Math.abs(prev.top + prev.height - current.top) <= CONTIGUOUS_LINE_GAP;
|
||||
const touchesNext = !!next && Math.abs(current.top + current.height - next.top) <= CONTIGUOUS_LINE_GAP;
|
||||
const left = current.left;
|
||||
const right = current.left + current.width;
|
||||
|
||||
const classes = ["cm-trimmedSelection"];
|
||||
if (!touchesPrev || !coversX(prev, left)) classes.push("cm-trimmedSelection-topLeft");
|
||||
if (!touchesPrev || !coversX(prev, right)) classes.push("cm-trimmedSelection-topRight");
|
||||
if (!touchesNext || !coversX(next, left)) classes.push("cm-trimmedSelection-bottomLeft");
|
||||
if (!touchesNext || !coversX(next, right)) classes.push("cm-trimmedSelection-bottomRight");
|
||||
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
export function trimmedSelectionLayer() {
|
||||
return layer({
|
||||
above: false,
|
||||
class: "cm-trimmedSelectionLayer",
|
||||
markers(view) {
|
||||
const markers: InstanceType<typeof RectangleMarker>[] = [];
|
||||
const base = layerBase(view);
|
||||
|
||||
for (const range of view.state.selection.ranges) {
|
||||
if (range.empty) continue;
|
||||
const rects: TrimmedSelectionRect[] = [];
|
||||
|
||||
for (const visible of view.visibleRanges) {
|
||||
let pos = Math.max(range.from, visible.from, view.viewport.from);
|
||||
const endPos = Math.min(range.to, visible.to, view.viewport.to);
|
||||
|
||||
while (pos < endPos) {
|
||||
const line = view.state.doc.lineAt(pos);
|
||||
const from = Math.max(pos, line.from);
|
||||
const to = Math.min(endPos, line.to);
|
||||
const lineBlock = view.lineBlockAt(line.from);
|
||||
const includesLineBreak = endPos > line.to && range.to > line.to;
|
||||
const rect = markerForLineRange(view, from, to, line.from, line.to, lineBlock, includesLineBreak, base);
|
||||
if (rect) rects.push(rect);
|
||||
|
||||
const next = line.to + 1;
|
||||
if (next <= pos) break;
|
||||
pos = next;
|
||||
}
|
||||
}
|
||||
|
||||
rects.sort((a, b) => a.top - b.top || a.left - b.left);
|
||||
rects.forEach((rect, index) => {
|
||||
markers.push(new RectangleMarker(markerClass(rects, index), rect.left, rect.top, rect.width, rect.height));
|
||||
});
|
||||
}
|
||||
|
||||
return markers;
|
||||
},
|
||||
update(update) {
|
||||
return update.docChanged || update.selectionSet || update.viewportChanged || update.geometryChanged;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { layer, RectangleMarker, type EditorView } from "@codemirror/view";
|
||||
|
||||
function domRangeForSelection(view: EditorView, from: number, to: number): Range | null {
|
||||
try {
|
||||
const start = view.domAtPos(from);
|
||||
const end = view.domAtPos(to, -1);
|
||||
const range = document.createRange();
|
||||
range.setStart(start.node, start.offset);
|
||||
range.setEnd(end.node, end.offset);
|
||||
return range;
|
||||
} 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 baseRect = view.scrollDOM.getBoundingClientRect();
|
||||
const base = {
|
||||
left: baseRect.left - view.scrollDOM.scrollLeft * view.scaleX,
|
||||
top: baseRect.top - view.scrollDOM.scrollTop * view.scaleY,
|
||||
};
|
||||
for (const r of view.state.selection.ranges) {
|
||||
if (r.empty) continue;
|
||||
const range = domRangeForSelection(view, r.from, r.to);
|
||||
if (!range) continue;
|
||||
const rects = Array.from(range.getClientRects());
|
||||
for (const rect of rects) {
|
||||
if (rect.width <= 0 || rect.height <= 0) continue;
|
||||
markers.push(
|
||||
new RectangleMarker(
|
||||
"cm-vscodeSelection",
|
||||
rect.left - base.left,
|
||||
rect.top - base.top,
|
||||
rect.width,
|
||||
rect.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
range.detach();
|
||||
}
|
||||
return markers;
|
||||
},
|
||||
update(update, _dom) {
|
||||
return update.docChanged || update.selectionSet || update.viewportChanged || update.geometryChanged;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ type LucideIconNode = Array<[string, Record<string, string>]>;
|
|||
|
||||
export const EDITOR_FONT_SIZE_CSS_VAR = "--dbx-editor-font-size";
|
||||
export const EDITOR_FONT_FAMILY_CSS_VAR = "--dbx-editor-font-family";
|
||||
const EDITOR_SELECTION_BACKGROUND_CSS_VAR = "--dbx-editor-selection-background";
|
||||
|
||||
const SUPPORTS_COLOR_MIX =
|
||||
typeof CSS !== "undefined" &&
|
||||
|
|
@ -82,6 +83,7 @@ function createCustomTheme(
|
|||
"&": {
|
||||
backgroundColor: c.background,
|
||||
color: c.foreground,
|
||||
[EDITOR_SELECTION_BACKGROUND_CSS_VAR]: c.selection,
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: c.cursor,
|
||||
|
|
@ -317,9 +319,21 @@ export function buildEditorFontThemeRules(
|
|||
height: "1.6em !important",
|
||||
transform: "translateY(-0.3em)",
|
||||
},
|
||||
".cm-vscodeSelection": {
|
||||
opacity: "0.38",
|
||||
background: "rgb(148, 163, 184)",
|
||||
".cm-trimmedSelection": {
|
||||
backgroundColor: `var(${EDITOR_SELECTION_BACKGROUND_CSS_VAR}, rgb(148 163 184 / 38%))`,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-trimmedSelection-topLeft": {
|
||||
borderTopLeftRadius: "3px",
|
||||
},
|
||||
".cm-trimmedSelection-topRight": {
|
||||
borderTopRightRadius: "3px",
|
||||
},
|
||||
".cm-trimmedSelection-bottomLeft": {
|
||||
borderBottomLeftRadius: "3px",
|
||||
},
|
||||
".cm-trimmedSelection-bottomRight": {
|
||||
borderBottomRightRadius: "3px",
|
||||
},
|
||||
".cm-gutters": {
|
||||
borderRight: "0 !important",
|
||||
|
|
|
|||
Loading…
Reference in New Issue