fix(editor): improve wrapped selection highlights

This commit is contained in:
t8y2 2026-06-09 20:53:47 +08:00
parent 699281c7a6
commit ae483efb98
1 changed files with 89 additions and 7 deletions

View File

@ -1,4 +1,4 @@
import { layer, RectangleMarker, type BlockInfo, type EditorView } from "@codemirror/view";
import { layer, RectangleMarker, type EditorView } from "@codemirror/view";
const MIN_EMPTY_LINE_WIDTH = 14;
const END_OF_LINE_PADDING = 3;
@ -12,6 +12,23 @@ type TrimmedSelectionRect = {
height: number;
};
type PositionRect = ReturnType<EditorView["coordsAtPos"]>;
function visualLineVerticalBounds(
view: EditorView,
start: NonNullable<PositionRect>,
end: NonNullable<PositionRect>,
): { top: number; bottom: number } {
const top = Math.min(start.top, end.top);
const bottom = Math.max(start.bottom, end.bottom);
const height = Math.max(bottom - top, view.defaultLineHeight);
const center = (top + bottom) / 2;
return {
top: center - height / 2,
bottom: center + height / 2,
};
}
function layerBase(view: EditorView) {
const rect = view.scrollDOM.getBoundingClientRect();
return {
@ -26,7 +43,6 @@ function markerForLineRange(
to: number,
lineFrom: number,
lineTo: number,
lineBlock: BlockInfo,
includesLineBreak: boolean,
base: { left: number; top: number },
): TrimmedSelectionRect | null {
@ -42,8 +58,7 @@ function markerForLineRange(
right = Math.max(right, lineEnd.right + END_OF_LINE_PADDING);
}
const top = view.documentTop + lineBlock.top;
const bottom = top + lineBlock.height;
const { top, bottom } = visualLineVerticalBounds(view, start, end);
return {
left: left - base.left,
@ -53,6 +68,75 @@ function markerForLineRange(
};
}
function sameVisualLine(a: NonNullable<PositionRect>, b: NonNullable<PositionRect>): boolean {
const aMid = (a.top + a.bottom) / 2;
const bMid = (b.top + b.bottom) / 2;
return Math.abs(aMid - bMid) <= Math.max(2, Math.min(a.bottom - a.top, b.bottom - b.top) / 2);
}
function lastPositionOnVisualLine(
view: EditorView,
from: number,
to: number,
start: NonNullable<PositionRect>,
): number {
let low = from + 1;
let high = to;
let best = from;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const coords = view.coordsAtPos(mid, -1) ?? view.coordsAtPos(mid, 1);
if (coords && sameVisualLine(start, coords)) {
best = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
return best > from ? best : Math.min(to, from + 1);
}
function markerRectsForLineRange(
view: EditorView,
from: number,
to: number,
lineFrom: number,
lineTo: number,
includesLineBreak: boolean,
base: { left: number; top: number },
): TrimmedSelectionRect[] {
const start = view.coordsAtPos(from, 1);
const end = from < to ? view.coordsAtPos(to, -1) : start;
if (!start || !end) return [];
if (from >= to || sameVisualLine(start, end)) {
const rect = markerForLineRange(view, from, to, lineFrom, lineTo, includesLineBreak, base);
return rect ? [rect] : [];
}
const rects: TrimmedSelectionRect[] = [];
let segmentFrom = from;
while (segmentFrom < to) {
const segmentStart = view.coordsAtPos(segmentFrom, 1);
if (!segmentStart) break;
const segmentTo = lastPositionOnVisualLine(view, segmentFrom, to, segmentStart);
const rect = markerForLineRange(
view,
segmentFrom,
segmentTo,
lineFrom,
lineTo,
includesLineBreak && segmentTo >= to,
base,
);
if (rect) rects.push(rect);
if (segmentTo <= segmentFrom) break;
segmentFrom = segmentTo;
}
return rects;
}
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;
@ -96,10 +180,8 @@ export function trimmedSelectionLayer() {
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);
rects.push(...markerRectsForLineRange(view, from, to, line.from, line.to, includesLineBreak, base));
const next = line.to + 1;
if (next <= pos) break;