From cbb21e75130c8a24c1519c8f736bf54b4b12b67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Wed, 30 Sep 2026 15:31:09 +0200 Subject: [PATCH] Move diff annotation helpers into lib The snippet and the code thread had their own copies. --- src/components/PierreDiff.svelte | 79 +++------------------ src/components/PierreSnippet.svelte | 11 ++- src/components/ReviewCodeThread.svelte | 17 +---- src/lib/pierreComments.ts | 95 +++++++++++++++++++++++-- tests/unit/pierreComments.test.ts | 96 ++++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 96 deletions(-) diff --git a/src/components/PierreDiff.svelte b/src/components/PierreDiff.svelte index 48993bc2a..18013d72e 100644 --- a/src/components/PierreDiff.svelte +++ b/src/components/PierreDiff.svelte @@ -32,8 +32,13 @@ } from "@app/lib/pierreComments"; import { anchorOf, + annotationSignature, + commentedLines, + composerLines, + entryStatus, fileAnnotations, isCommentableStatus, + itemIds, } from "@app/lib/pierreComments"; import { parsePatch } from "@app/lib/pierreParse"; import { @@ -504,7 +509,7 @@ if (!instance) { return; } - for (const id of itemIds(parsedFiles)) { + for (const id of itemIds(parsedFiles.map(file => file.name))) { const item = instance.getItem(id); if (item) { instance.updateItem({ @@ -558,18 +563,8 @@ mountedHeaders.clear(); } - // How this entry changed. `fileStatuses` is keyed by path, so the two entries - // of a type change would both read as whichever half the structured diff - // recorded last; Pierre parsed each entry on its own and knows which this is. function statusOf(fileDiff: FileDiffMetadata): FileStatus | undefined { - const status = fileStatuses?.get(fileDiff.name); - if (status === "added" && fileDiff.type === "deleted") { - return "deleted"; - } - if (status === "deleted" && fileDiff.type === "new") { - return "added"; - } - return status; + return entryStatus(fileStatuses?.get(fileDiff.name), fileDiff.type); } // Collapse or expand a single file, by CodeView item. Bumps `version`: @@ -986,25 +981,10 @@ }; } - // The item id for each file, in the order they were given. A type change, a - // symlink replaced by a regular file for instance, is one path arriving as two - // entries, and CodeView throws on a duplicate id, so the repeats are numbered. - // The first keeps the bare path, which is what scroll targets and collapsed - // paths resolve by. NUL is the separator because a path cannot contain one. - function itemIds(files: FileDiffMetadata[]): string[] { - // eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local map, never reactive - const occurrences = new Map(); - return files.map(file => { - const seen = occurrences.get(file.name) ?? 0; - occurrences.set(file.name, seen + 1); - return seen === 0 ? file.name : `${file.name}\u0000${seen + 1}`; - }); - } - function buildItems( files: FileDiffMetadata[], ): CodeViewItem[] { - const ids = itemIds(files); + const ids = itemIds(files.map(file => file.name)); return files.map((fileDiff, index) => ({ id: ids[index], type: "diff", @@ -1046,17 +1026,6 @@ tintStyle = undefined; } - function commentedLines(location: CodeLocation): number[] { - const range = location.new ?? location.old; - if (!range) return []; - if (range.type === "chars") return [range.line]; - const lines: number[] = []; - for (let line = range.range.start; line < range.range.end; line++) { - lines.push(line); - } - return lines; - } - function tintSelectors( side: AnnotationSide, line: number, @@ -1109,23 +1078,6 @@ tintStyle = style; } - // Which slots a file's annotations occupy — and only that. Replacing a file's - // annotation array makes Pierre drop every measured annotation height in that - // file and lay it out as if the comments had no height at all, which yanks the - // scroll position; so it is only told when the set of slots really changes. - // What goes *in* a slot is pushed straight into the mounted component instead - // (see `syncAnnotationSlots`), which is all a reply, an edit or a resolve - // touches. - function annotationSignature( - annotations: DiffLineAnnotation[] | undefined, - ): string { - if (!annotations || annotations.length === 0) return ""; - return annotations - .map(({ side, lineNumber }) => `${side}:${lineNumber}`) - .sort() - .join("|"); - } - // Open the new-comment composer on what the gutter marker covers. Pierre // reports the line range it was dragged over, on the side it started from. function openComposer( @@ -1140,18 +1092,7 @@ if (!isCommentableStatus(fileStatuses?.get(path))) { return; } - const side = range.side ?? "additions"; - // A drag in a split diff can end on the other side, where the end line - // counts a different file. There is no range that means both, so keep the - // line it started from. - const crossSide = range.endSide !== undefined && range.endSide !== side; - const end = crossSide ? range.start : range.end; - composer = { - path, - side, - firstLine: Math.min(range.start, end), - lastLine: Math.max(range.start, end), - }; + composer = { path, ...composerLines(range) }; composerBody = ""; } @@ -1172,7 +1113,7 @@ } untrack(() => { let changed = false; - for (const id of itemIds(parsedFiles)) { + for (const id of itemIds(parsedFiles.map(file => file.name))) { const item = instance.getItem(id); if (item?.type !== "diff") { continue; diff --git a/src/components/PierreSnippet.svelte b/src/components/PierreSnippet.svelte index 175934020..df250805d 100644 --- a/src/components/PierreSnippet.svelte +++ b/src/components/PierreSnippet.svelte @@ -10,7 +10,10 @@ import type { CodeComments } from "@app/lib/codeComments"; import { forwardPatchActivityContext } from "@app/lib/patchActivityContext"; import type { LineAnnotation } from "@app/lib/pierreComments"; - import { fileAnnotations } from "@app/lib/pierreComments"; + import { + annotationSignature, + fileAnnotations, + } from "@app/lib/pierreComments"; import { codeLineHeight, getWorkerPool, @@ -90,11 +93,7 @@ const annotationList = $derived.by(() => fileAnnotations(path, threads, undefined), ); - const annotationKey = $derived( - annotationList - .map(({ side, lineNumber }) => `${side}:${lineNumber}`) - .join("|"), - ); + const annotationKey = $derived(annotationSignature(annotationList)); let stableAnnotations = $state.raw[]>([]); let stableKey: string | undefined; $effect(() => { diff --git a/src/components/ReviewCodeThread.svelte b/src/components/ReviewCodeThread.svelte index f5d76679a..136894fe3 100644 --- a/src/components/ReviewCodeThread.svelte +++ b/src/components/ReviewCodeThread.svelte @@ -10,6 +10,7 @@ import { diffOptions } from "@app/lib/diffOptions.svelte"; import { cachedGetDiffText } from "@app/lib/invoke"; import { sliceForRange } from "@app/lib/patchSlice"; + import { lineRangeOf } from "@app/lib/pierreComments"; import PierreSnippet from "@app/components/PierreSnippet.svelte"; @@ -86,20 +87,6 @@ resolvedBy, }; } - - /// Which lines of which side a comment is about, in the numbering - /// `sliceForRange` matches on. - function rangeOf( - location: CodeLocation, - ): { side: "old" | "new"; start: number; end: number } | undefined { - const range = location.new ?? location.old; - if (!range) return undefined; - const side = location.new ? "new" : "old"; - if (range.type === "chars") { - return { side, start: range.line, end: range.line + 1 }; - } - return { side, start: range.range.start, end: range.range.end }; - }