From ebd157850bfbb80792d7617fb73786658360c5e8 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Fri, 23 Oct 2026 09:50:05 +0100 Subject: [PATCH] web/repo: write and read diff line ranges as L/R anchors a diff line needs its side as well as its number, so R12-20 names new lines 12 to 20, L4 an old line, and L5-R9 a range that starts on the old side and ends on the new. the blob anchor shares the number formatting. Signed-off-by: oppiliappan --- .../lib/components/repo/lineAnchor.test.ts | 83 ++++++++++++++++++- web/src/lib/components/repo/lineAnchor.ts | 56 ++++++++++++- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/web/src/lib/components/repo/lineAnchor.test.ts b/web/src/lib/components/repo/lineAnchor.test.ts index 26d839a00..788962755 100644 --- a/web/src/lib/components/repo/lineAnchor.test.ts +++ b/web/src/lib/components/repo/lineAnchor.test.ts @@ -1,5 +1,12 @@ import { describe, expect, it } from "vitest"; -import { lineAnchor, parseLineAnchor } from "#lib/components/repo/lineAnchor.ts"; +import { + diffLineAnchor, + diffLineSpan, + lineAnchor, + lineNumbers, + parseDiffLineAnchor, + parseLineAnchor +} from "#lib/components/repo/lineAnchor.ts"; describe("parseLineAnchor", () => { it("reads one line and a range", () => { @@ -27,3 +34,77 @@ describe("lineAnchor", () => { expect(lineAnchor(null)).toBe(""); }); }); + +describe("parseDiffLineAnchor", () => { + it("reads either side", () => { + expect(parseDiffLineAnchor("R12")).toEqual({ start: 12, end: 12, side: "additions" }); + expect(parseDiffLineAnchor("L4-9")).toEqual({ start: 4, end: 9, side: "deletions" }); + expect(parseDiffLineAnchor("R20-R12")).toEqual({ start: 12, end: 20, side: "additions" }); + }); + + it("keeps a range across sides in order", () => { + expect(parseDiffLineAnchor("L5-R9")).toEqual({ + start: 5, + side: "deletions", + end: 9, + endSide: "additions" + }); + }); + + it("ignores anything else", () => { + expect(parseDiffLineAnchor("")).toBeNull(); + expect(parseDiffLineAnchor("R0")).toBeNull(); + expect(parseDiffLineAnchor("12")).toBeNull(); + expect(parseDiffLineAnchor("X3")).toBeNull(); + }); +}); + +describe("diffLineAnchor", () => { + it("writes one side", () => { + expect(diffLineAnchor({ start: 7, end: 7, side: "additions" })).toBe("R7"); + expect(diffLineAnchor({ start: 7, end: 7 })).toBe("R7"); + expect(diffLineAnchor({ start: 9, end: 4, side: "deletions" })).toBe("L4-9"); + }); + + it("writes across sides", () => { + expect(diffLineAnchor({ start: 5, side: "deletions", end: 9, endSide: "additions" })).toBe( + "L5-R9" + ); + }); + + it("round trips", () => { + for (const anchor of ["R1", "L3-8", "L5-R9", "R9-L5"]) { + const range = parseDiffLineAnchor(anchor); + expect(range && diffLineAnchor(range)).toBe(anchor); + } + }); +}); + +describe("diffLineSpan", () => { + it("names both sides and orders one side", () => { + expect(diffLineSpan({ start: 9, end: 4 })).toEqual({ + start: 4, + end: 9, + side: "additions", + endSide: "additions" + }); + }); + + it("keeps a range across sides as made", () => { + expect(diffLineSpan({ start: 9, side: "additions", end: 5, endSide: "deletions" })).toEqual( + { + start: 9, + end: 5, + side: "additions", + endSide: "deletions" + } + ); + }); +}); + +describe("lineNumbers", () => { + it("writes one line or a range", () => { + expect(lineNumbers({ start: 3, end: 3 })).toBe("3"); + expect(lineNumbers({ start: 3, end: 8 })).toBe("3-8"); + }); +}); diff --git a/web/src/lib/components/repo/lineAnchor.ts b/web/src/lib/components/repo/lineAnchor.ts index a1bf1a397..ddb062e3a 100644 --- a/web/src/lib/components/repo/lineAnchor.ts +++ b/web/src/lib/components/repo/lineAnchor.ts @@ -1,4 +1,4 @@ -import type { SelectedLineRange } from "@pierre/diffs"; +import type { SelectedLineRange, SelectionSide } from "@pierre/diffs"; const LINE_ANCHOR_RE = /^#L(\d+)(?:-L?(\d+))?$/; @@ -11,9 +11,57 @@ export const parseLineAnchor = (hash: string): SelectedLineRange | null => { return { start: Math.min(first, last), end: Math.max(first, last) }; }; +export const lineNumbers = ({ start, end }: { start: number; end: number }): string => + start === end ? `${start}` : `${start}-${end}`; + export const lineAnchor = (range: SelectedLineRange | null): string => { if (!range) return ""; - const start = Math.min(range.start, range.end); - const end = Math.max(range.start, range.end); - return start === end ? `#L${start}` : `#L${start}-${end}`; + return `#L${lineNumbers({ start: Math.min(range.start, range.end), end: Math.max(range.start, range.end) })}`; +}; + +export interface DiffLineSpan { + start: number; + end: number; + side: SelectionSide; + endSide: SelectionSide; +} + +export const diffLineSpan = (range: SelectedLineRange): DiffLineSpan => { + const side = range.side ?? "additions"; + const endSide = range.endSide ?? side; + if (endSide !== side) return { start: range.start, end: range.end, side, endSide }; + return { + start: Math.min(range.start, range.end), + end: Math.max(range.start, range.end), + side, + endSide + }; +}; + +type SideLetter = "L" | "R"; + +const DIFF_LINE_ANCHOR_RE = /^([LR])(\d+)(?:-([LR])?(\d+))?$/; + +const sideOf = (letter: string): SelectionSide => (letter === "L" ? "deletions" : "additions"); + +const letterOf = (side: SelectionSide): SideLetter => (side === "deletions" ? "L" : "R"); + +export const parseDiffLineAnchor = (anchor: string): SelectedLineRange | null => { + const match = DIFF_LINE_ANCHOR_RE.exec(anchor); + if (!match) return null; + const side = sideOf(match[1]); + const endSide = match[3] === undefined ? side : sideOf(match[3]); + const first = Number(match[2]); + const last = match[4] === undefined ? first : Number(match[4]); + if (first < 1 || last < 1) return null; + if (endSide !== side) return { start: first, side, end: last, endSide }; + return { start: Math.min(first, last), end: Math.max(first, last), side }; +}; + +export const diffLineAnchor = (range: SelectedLineRange): string => { + const span = diffLineSpan(range); + if (span.endSide !== span.side) { + return `${letterOf(span.side)}${span.start}-${letterOf(span.endSide)}${span.end}`; + } + return `${letterOf(span.side)}${lineNumbers(span)}`; }; -- 2.54.0