diff --git a/apps/desktop/src/lib/codemirrorTrimmedSelectionLayer.ts b/apps/desktop/src/lib/codemirrorTrimmedSelectionLayer.ts index 4fc7a9d369..77e16fe55c 100644 --- a/apps/desktop/src/lib/codemirrorTrimmedSelectionLayer.ts +++ b/apps/desktop/src/lib/codemirrorTrimmedSelectionLayer.ts @@ -1,6 +1,6 @@ import { layer, RectangleMarker, type EditorView } from "@codemirror/view"; -const MIN_EMPTY_LINE_WIDTH = 14; +const FALLBACK_EMPTY_SELECTION_WIDTH = 8; const END_OF_LINE_PADDING = 3; const CONTIGUOUS_LINE_GAP = 1.5; const CORNER_COVERAGE_GAP = 1; @@ -14,6 +14,33 @@ type TrimmedSelectionRect = { type PositionRect = ReturnType; +export function trimmedSelectionHorizontalBounds(options: { + from: number; + to: number; + lineFrom: number; + lineTo: number; + includesLineBreak: boolean; + startLeft: number; + startRight: number; + endLeft: number; + endRight: number; + lineStartLeft: number; + lineEndRight: number; + emptySelectionWidth?: number; +}): { + left: number; + right: number; +} { + const lineBreakOnlyAtContentEnd = options.from >= options.to && options.includesLineBreak && options.from >= options.lineTo && options.lineTo > options.lineFrom; + const left = options.from < options.to ? Math.min(options.startLeft, options.endLeft) : lineBreakOnlyAtContentEnd ? options.lineEndRight : options.lineStartLeft; + const emptySelectionWidth = Math.max(1, options.emptySelectionWidth ?? FALLBACK_EMPTY_SELECTION_WIDTH); + let right = options.from < options.to ? Math.max(options.startRight, options.endRight) : left + emptySelectionWidth; + if (options.includesLineBreak && options.to >= options.lineTo) { + right = Math.max(right, options.lineEndRight + END_OF_LINE_PADDING); + } + return { left, right }; +} + function visualLineVerticalBounds(view: EditorView, start: NonNullable, end: NonNullable): { top: number; bottom: number } { const top = Math.min(start.top, end.top); const bottom = Math.max(start.bottom, end.bottom); @@ -40,11 +67,20 @@ function markerForLineRange(view: EditorView, from: number, to: number, lineFrom 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 { left, right } = trimmedSelectionHorizontalBounds({ + from, + to, + lineFrom, + lineTo, + includesLineBreak, + startLeft: start.left, + startRight: start.right, + endLeft: end.left, + endRight: end.right, + lineStartLeft: lineStart.left, + lineEndRight: lineEnd.right, + emptySelectionWidth: view.defaultCharacterWidth, + }); const { top, bottom } = visualLineVerticalBounds(view, start, end); diff --git a/packages/app-tests/codemirrorTrimmedSelectionLayer.test.ts b/packages/app-tests/codemirrorTrimmedSelectionLayer.test.ts new file mode 100644 index 0000000000..91e88f8415 --- /dev/null +++ b/packages/app-tests/codemirrorTrimmedSelectionLayer.test.ts @@ -0,0 +1,43 @@ +import { strict as assert } from "node:assert"; +import { test } from "vitest"; +import { trimmedSelectionHorizontalBounds } from "../../apps/desktop/src/lib/codemirrorTrimmedSelectionLayer.ts"; + +test("line-break-only selection starts at the content end", () => { + const bounds = trimmedSelectionHorizontalBounds({ + from: 42, + to: 42, + lineFrom: 0, + lineTo: 42, + includesLineBreak: true, + startLeft: 260, + startRight: 260, + endLeft: 260, + endRight: 260, + lineStartLeft: 80, + lineEndRight: 260, + emptySelectionWidth: 7, + }); + + assert.equal(bounds.left, 260); + assert.equal(bounds.right, 267); +}); + +test("empty selected line still uses the line start", () => { + const bounds = trimmedSelectionHorizontalBounds({ + from: 12, + to: 12, + lineFrom: 12, + lineTo: 12, + includesLineBreak: true, + startLeft: 80, + startRight: 80, + endLeft: 80, + endRight: 80, + lineStartLeft: 80, + lineEndRight: 80, + emptySelectionWidth: 7, + }); + + assert.equal(bounds.left, 80); + assert.equal(bounds.right, 87); +});