mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
* fix(editor): keep selections whole over line backgrounds and indent guides Multi-line selections could show a 1px seam between their stacked rectangles, indent guides showed as short dashes inside a selection, and a breakpoint or execution line hid the selection past its text. - Snap selection rectangle edges to device pixels so stacked pieces share one edge. - Paint caret-row, breakpoint and execution line backgrounds on a pseudo-element below the selection layer. - Draw indent guides on a pseudo-element above the text. * test(e2e): check the visible selection layer in the theme flow
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
import { EditorView, RectangleMarker, layer } from "@codemirror/view";
|
|
import type { Extension } from "@codemirror/state";
|
|
|
|
/** Rounds a CSS length to the nearest device pixel. */
|
|
export function snapToDevicePixel(v: number, dpr: number): number {
|
|
return Math.round(v * dpr) / dpr;
|
|
}
|
|
|
|
/**
|
|
* Re-emits selection rectangles with top and bottom edges snapped to device pixels. A multi-line
|
|
* selection is drawn as several stacked rectangles; their shared edge comes from separate float
|
|
* sums, and when it lands on a fractional pixel each side rounds differently and a 1px seam shows.
|
|
*/
|
|
export function snapMarkers(markers: readonly RectangleMarker[], className: string, dpr: number): RectangleMarker[] {
|
|
return markers.map((m) => {
|
|
const top = snapToDevicePixel(m.top, dpr);
|
|
const bottom = snapToDevicePixel(m.top + m.height, dpr);
|
|
return new RectangleMarker(className, m.left, top, m.width, bottom - top);
|
|
});
|
|
}
|
|
|
|
const CLASS = "cm-selectionBackground";
|
|
|
|
const snappedSelectionLayer = layer({
|
|
above: false,
|
|
class: "cm-selectionLayer",
|
|
mount: (dom) => dom.classList.add("cm-openidea-selectionLayer"),
|
|
markers(view) {
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const out: RectangleMarker[] = [];
|
|
for (const r of view.state.selection.ranges)
|
|
if (!r.empty) out.push(...snapMarkers(RectangleMarker.forRange(view, CLASS, r), CLASS, dpr));
|
|
return out;
|
|
},
|
|
update: (u) => u.docChanged || u.selectionSet || u.viewportChanged,
|
|
});
|
|
|
|
/** Use with `drawSelection()`: replaces its selection layer, which is hidden. */
|
|
export function seamlessSelection(): Extension {
|
|
return [
|
|
snappedSelectionLayer,
|
|
EditorView.theme({
|
|
".cm-selectionLayer:not(.cm-openidea-selectionLayer)": {
|
|
display: "none",
|
|
},
|
|
}),
|
|
];
|
|
}
|