diff --git a/bun.lock b/bun.lock
index d3c647a..92c2dc3 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,8 +10,11 @@
"dependencies": {
"bits-ui": "^2.4.1",
"chroma-js": "^3.1.2",
+ "date-fns": "^4.1.0",
"diff": "^8.0.2",
+ "dompurify": "^3.2.6",
"luxon": "^3.6.1",
+ "marked": "^16.0.0",
"runed": "^0.28.0",
"shiki": "^3.4.2",
"svelte-toolbelt": "^0.9.1",
@@ -29,7 +32,9 @@
"@sveltejs/vite-plugin-svelte": "^5.1.0",
"@tailwindcss/vite": "^4.1.8",
"@types/chroma-js": "^3.1.1",
+ "@types/dompurify": "^3.2.0",
"@types/luxon": "^3.6.2",
+ "@types/marked": "^6.0.0",
"@types/wicg-file-system-access": "^2023.10.6",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
@@ -364,6 +369,8 @@
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+ "@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="],
+
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
@@ -372,10 +379,14 @@
"@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
+ "@types/marked": ["@types/marked@6.0.0", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
+
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="],
+ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/webextension-polyfill": ["@types/webextension-polyfill@0.12.3", "", {}, "sha512-F58aDVSeN/MjUGazXo/cPsmR76EvqQhQ1v4x23hFjUX0cfAJYE+JBWwiOGW36/VJGGxoH74sVlRIF3z7SJCKyg=="],
@@ -500,6 +511,8 @@
"data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
+ "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
+
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
@@ -520,6 +533,8 @@
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
+ "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
+
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
@@ -688,6 +703,8 @@
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+ "marked": ["marked@16.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA=="],
+
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
diff --git a/web/package.json b/web/package.json
index 10b772e..7464a11 100644
--- a/web/package.json
+++ b/web/package.json
@@ -27,7 +27,9 @@
"@sveltejs/vite-plugin-svelte": "^5.1.0",
"@tailwindcss/vite": "^4.1.8",
"@types/chroma-js": "^3.1.1",
+ "@types/dompurify": "^3.2.0",
"@types/luxon": "^3.6.2",
+ "@types/marked": "^6.0.0",
"@types/wicg-file-system-access": "^2023.10.6",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
@@ -48,8 +50,11 @@
"dependencies": {
"bits-ui": "^2.4.1",
"chroma-js": "^3.1.2",
+ "date-fns": "^4.1.0",
"diff": "^8.0.2",
+ "dompurify": "^3.2.6",
"luxon": "^3.6.1",
+ "marked": "^16.0.0",
"runed": "^0.28.0",
"shiki": "^3.4.2",
"svelte-toolbelt": "^0.9.1",
diff --git a/web/src/lib/components/diff/CommentDisplay.svelte b/web/src/lib/components/diff/CommentDisplay.svelte
new file mode 100644
index 0000000..16f36fb
--- /dev/null
+++ b/web/src/lib/components/diff/CommentDisplay.svelte
@@ -0,0 +1,349 @@
+
+
+
+
+
diff --git a/web/src/lib/components/diff/CommentForm.svelte b/web/src/lib/components/diff/CommentForm.svelte
new file mode 100644
index 0000000..05ce948
--- /dev/null
+++ b/web/src/lib/components/diff/CommentForm.svelte
@@ -0,0 +1,407 @@
+
+
+
+
+
diff --git a/web/src/lib/components/diff/CommentThread.svelte b/web/src/lib/components/diff/CommentThread.svelte
new file mode 100644
index 0000000..779f842
--- /dev/null
+++ b/web/src/lib/components/diff/CommentThread.svelte
@@ -0,0 +1,207 @@
+
+
+
+
+
diff --git a/web/src/lib/components/diff/ConciseDiffView.svelte b/web/src/lib/components/diff/ConciseDiffView.svelte
index a0fe7eb..ba6ba98 100644
--- a/web/src/lib/components/diff/ConciseDiffView.svelte
+++ b/web/src/lib/components/diff/ConciseDiffView.svelte
@@ -11,10 +11,16 @@
type PatchLineTypeProps,
patchLineTypeProps,
type SearchSegment,
+ type DiffViewerPatch,
+ type DiffViewerPatchHunk,
} from "$lib/components/diff/concise-diff-view.svelte";
import Spinner from "$lib/components/Spinner.svelte";
+ import CommentThread from "./CommentThread.svelte";
+ import CommentForm from "./CommentForm.svelte";
+ import type { CommentThread as CommentThreadType } from "$lib/diff-viewer-multi-file.svelte";
+ import { getGithubToken, type GithubPRComment } from "$lib/github.svelte";
import { type StructuredPatch } from "diff";
- import { onDestroy } from "svelte";
+ import { onDestroy, setContext } from "svelte";
import { type MutableValue } from "$lib/util";
import { box } from "svelte-toolbelt";
@@ -31,7 +37,33 @@
activeSearchResult = -1,
cache,
cacheKey,
- }: ConciseDiffViewProps = $props();
+ showComments = false,
+ commentsForLine,
+ onCommentAdded,
+ onCommentUpdated,
+ onCommentDeleted,
+ filePath,
+ owner,
+ repo,
+ prNumber,
+ }: ConciseDiffViewProps & {
+ showComments?: boolean;
+ commentsForLine?: (line: number, side: "LEFT" | "RIGHT") => CommentThreadType[];
+ onCommentAdded?: (comment: GithubPRComment) => void;
+ onCommentUpdated?: (comment: GithubPRComment) => void;
+ onCommentDeleted?: (commentId: number) => void;
+ filePath?: string;
+ owner?: string;
+ repo?: string;
+ prNumber?: string;
+ } = $props();
+
+ setContext("diff-settings", {
+ syntaxHighlighting,
+ syntaxHighlightingTheme,
+ wordDiffs,
+ lineWrap,
+ });
const parsedPatch: Promise = $derived.by(async () => {
if (rawPatchContent !== undefined) {
@@ -53,6 +85,21 @@
cacheKey: box.with(() => cacheKey),
});
+ let hoveredLineKey = $state(null);
+ let showNewCommentForm = $state(null); // "line:side" format
+
+ // Multiline comment selection state
+ // Note: GitHub API requires multiline comments to be within the same hunk, so we track hunkIndex
+ let isSelecting = $state(false);
+ let selectionStart = $state<{ line: PatchLine; side: "LEFT" | "RIGHT"; hunkIndex: number } | null>(null);
+ let selectionEnd = $state<{ line: PatchLine; side: "LEFT" | "RIGHT"; hunkIndex: number } | null>(null);
+ let isDragging = $state(false);
+ let dragThreshold = 5; // pixels
+ let dragStartPosition = $state<{ x: number; y: number } | null>(null);
+
+ const hasToken = $derived(!!getGithubToken());
+ const canAddComments = $derived(showComments && hasToken && owner && repo && prNumber && filePath);
+
function getDisplayLineNo(line: PatchLine, num: number | undefined) {
if (line.type == PatchLineType.HEADER) {
return "...";
@@ -61,6 +108,182 @@
}
}
+ function getLineKey(line: PatchLine, side: "LEFT" | "RIGHT"): string {
+ const lineNum = side === "LEFT" ? line.oldLineNo : line.newLineNo;
+ return `${lineNum}:${side}`;
+ }
+
+ function getLineNumber(line: PatchLine, side: "LEFT" | "RIGHT"): number | undefined {
+ return side === "LEFT" ? line.oldLineNo : line.newLineNo;
+ }
+
+ function getOriginalContentForThread(thread: CommentThreadType, hunk: DiffViewerPatchHunk): string {
+ const firstComment = thread.comments[0];
+ if (!firstComment) return "";
+
+ const isMultiline = firstComment.start_line !== undefined && firstComment.start_line !== null && firstComment.start_line !== firstComment.line;
+
+ const startLine = isMultiline ? firstComment.start_line! : thread.position.line;
+ const endLine = thread.position.line;
+
+ const selectedLinesContent: string[] = [];
+
+ for (const line of hunk.lines) {
+ if (line.type === PatchLineType.ADD || line.type === PatchLineType.CONTEXT) {
+ const lineNum = line.newLineNo;
+ if (lineNum !== undefined && lineNum >= startLine && lineNum <= endLine) {
+ const content = extractLineContent(line);
+ selectedLinesContent.push(content);
+ }
+ }
+ }
+
+ return selectedLinesContent.join("\n");
+ }
+
+ function extractLineContent(line: PatchLine): string {
+ return line.content.map((segment) => segment.text || "").join("");
+ }
+
+ function handleAddComment(line: PatchLine, side: "LEFT" | "RIGHT") {
+ const lineKey = getLineKey(line, side);
+
+ // If there's already a comment form open for a different line, close it and start new
+ if (showNewCommentForm && showNewCommentForm !== lineKey) {
+ showNewCommentForm = lineKey;
+ clearSelection();
+ return;
+ }
+
+ // Check if this is a multiline selection
+ if (selectionStart && selectionEnd && (selectionStart.line !== selectionEnd.line || selectionStart.side !== selectionEnd.side)) {
+ // Create multiline comment
+ const startLineNum = getLineNumber(selectionStart.line, selectionStart.side);
+ const endLineNum = getLineNumber(selectionEnd.line, selectionEnd.side);
+
+ if (startLineNum !== undefined && endLineNum !== undefined) {
+ // Ensure proper order (start should be before end)
+ let actualStart = selectionStart;
+ let actualEnd = selectionEnd;
+
+ if (startLineNum > endLineNum || (startLineNum === endLineNum && selectionStart.side === "RIGHT" && selectionEnd.side === "LEFT")) {
+ actualStart = selectionEnd;
+ actualEnd = selectionStart;
+ }
+
+ // Update the selection state with the correct order
+ selectionStart = actualStart;
+ selectionEnd = actualEnd;
+
+ showNewCommentForm = getLineKey(actualEnd.line, actualEnd.side);
+ clearSelection();
+ return;
+ }
+ }
+
+ showNewCommentForm = lineKey;
+ clearSelection();
+ }
+
+ function handleCommentFormCancel() {
+ showNewCommentForm = null;
+ clearSelection();
+ }
+
+ function handleCommentSubmitted(comment: GithubPRComment) {
+ showNewCommentForm = null;
+ clearSelection();
+ onCommentAdded?.(comment);
+ }
+
+ function handleCommentUpdated(comment: GithubPRComment) {
+ onCommentUpdated?.(comment);
+ }
+
+ function handleCommentDeleted(commentId: number) {
+ onCommentDeleted?.(commentId);
+ }
+
+ // Mouse event handlers for drag selection
+ function handleMouseDown(event: MouseEvent, line: PatchLine, side: "LEFT" | "RIGHT", hunkIndex: number) {
+ if (!canAddComments || line.type === PatchLineType.HEADER) return;
+
+ const lineNum = getLineNumber(line, side);
+ if (lineNum === undefined) return;
+
+ // Store initial position for drag threshold
+ dragStartPosition = { x: event.clientX, y: event.clientY };
+
+ isSelecting = true;
+ isDragging = false; // Don't set to true until we actually start dragging
+ selectionStart = { line, side, hunkIndex };
+ selectionEnd = { line, side, hunkIndex };
+ }
+
+ function handleMouseMove(event: MouseEvent, line: PatchLine, side: "LEFT" | "RIGHT", hunkIndex: number) {
+ if (!isSelecting || !canAddComments || line.type === PatchLineType.HEADER) return;
+
+ const lineNum = getLineNumber(line, side);
+ if (lineNum === undefined) return;
+
+ // Don't allow selection across different hunks
+ if (selectionStart && selectionStart.hunkIndex !== hunkIndex) {
+ return;
+ }
+
+ // Check if we've moved far enough to start dragging
+ if (!isDragging && dragStartPosition) {
+ const dx = event.clientX - dragStartPosition.x;
+ const dy = event.clientY - dragStartPosition.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance > dragThreshold) {
+ isDragging = true;
+ event.preventDefault();
+ }
+ }
+
+ if (isDragging) {
+ selectionEnd = { line, side, hunkIndex };
+ }
+ }
+
+ function handleMouseUp() {
+ if (!isSelecting) return;
+
+ // If we were dragging and have a multiline selection, open comment form
+ if (isDragging && selectionStart && selectionEnd && (selectionStart.line !== selectionEnd.line || selectionStart.side !== selectionEnd.side)) {
+ // Open comment form for multiline selection
+ const startLineNum = getLineNumber(selectionStart.line, selectionStart.side);
+ const endLineNum = getLineNumber(selectionEnd.line, selectionEnd.side);
+
+ if (startLineNum !== undefined && endLineNum !== undefined) {
+ // Ensure proper order (start should be before end)
+ let actualStart = selectionStart;
+ let actualEnd = selectionEnd;
+
+ // If dragging from bottom to top, swap the selection
+ if (startLineNum > endLineNum || (startLineNum === endLineNum && selectionStart.side === "RIGHT" && selectionEnd.side === "LEFT")) {
+ actualStart = selectionEnd;
+ actualEnd = selectionStart;
+ }
+
+ // Update the selection state with the correct order
+ selectionStart = actualStart;
+ selectionEnd = actualEnd;
+
+ showNewCommentForm = getLineKey(actualEnd.line, actualEnd.side);
+ isSelecting = false;
+ isDragging = false;
+ dragStartPosition = null;
+ return;
+ }
+ }
+
+ // Otherwise clear selection
+ clearSelection();
+ }
+
let searchResultElements: HTMLSpanElement[] = $state([]);
let didInitialJump = $state(false);
let scheduledJump: ReturnType | undefined = undefined;
@@ -135,8 +358,117 @@
}
return segments;
});
+
+ function isLineInSelection(line: PatchLine, side: "LEFT" | "RIGHT", hunkIndex: number): boolean {
+ if (!selectionStart || !selectionEnd) return false;
+
+ // Only lines in the same hunk as the selection can be selected
+ if (selectionStart.hunkIndex !== hunkIndex) return false;
+
+ const lineNum = getLineNumber(line, side);
+ if (lineNum === undefined) return false;
+
+ const startLineNum = getLineNumber(selectionStart.line, selectionStart.side);
+ const endLineNum = getLineNumber(selectionEnd.line, selectionEnd.side);
+
+ if (startLineNum === undefined || endLineNum === undefined) return false;
+
+ // For single line selection (same line and side)
+ if (selectionStart.line === selectionEnd.line && selectionStart.side === selectionEnd.side) {
+ return line === selectionStart.line && side === selectionStart.side;
+ }
+
+ // Determine actual start and end (handle both directions)
+ let actualStartLineNum = startLineNum;
+ let actualEndLineNum = endLineNum;
+ let actualStartSide = selectionStart.side;
+ let actualEndSide = selectionEnd.side;
+
+ // If dragging from bottom to top, swap the order
+ if (startLineNum > endLineNum || (startLineNum === endLineNum && selectionStart.side === "RIGHT" && selectionEnd.side === "LEFT")) {
+ actualStartLineNum = endLineNum;
+ actualEndLineNum = startLineNum;
+ actualStartSide = selectionEnd.side;
+ actualEndSide = selectionStart.side;
+ }
+
+ // For same side selection
+ if (actualStartSide === actualEndSide && side === actualStartSide) {
+ return lineNum >= actualStartLineNum && lineNum <= actualEndLineNum;
+ }
+
+ // For cross-side selection, only include the exact start and end lines
+ if (actualStartSide !== actualEndSide) {
+ if (side === actualStartSide && line === selectionStart.line) {
+ return true;
+ } else if (side === actualEndSide && line === selectionEnd.line) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function clearSelection() {
+ isSelecting = false;
+ selectionStart = null;
+ selectionEnd = null;
+ isDragging = false;
+ dragStartPosition = null;
+ }
+
+ function handleClick(event: MouseEvent, line: PatchLine, side: "LEFT" | "RIGHT") {
+ // Don't handle click if we just finished dragging
+ if (isDragging) {
+ event.preventDefault();
+ return;
+ }
+
+ // Don't handle click if there's a multiline selection (from drag)
+ if (selectionStart && selectionEnd && (selectionStart.line !== selectionEnd.line || selectionStart.side !== selectionEnd.side)) {
+ event.preventDefault();
+ return;
+ }
+
+ handleAddComment(line, side);
+ }
+
+ function getSelectionInfo(diffViewerPatch: DiffViewerPatch): { content: string; containsDeletedLines: boolean } {
+ if (!selectionStart || !selectionEnd) return { content: "", containsDeletedLines: false };
+
+ let containsDeletedLines = false;
+ const selectedLines: string[] = [];
+ const hunk = diffViewerPatch.hunks[selectionStart.hunkIndex];
+
+ if (hunk) {
+ for (let lineIndex = 0; lineIndex < hunk.lines.length; lineIndex++) {
+ const line = hunk.lines[lineIndex];
+
+ // Skip header and spacer lines
+ if (line.type === PatchLineType.HEADER || line.type === PatchLineType.SPACER) continue;
+
+ // Check if this line is in the selection using the same logic as isLineInSelection
+ if (isLineInSelection(line, "LEFT", selectionStart.hunkIndex) || isLineInSelection(line, "RIGHT", selectionStart.hunkIndex)) {
+ if (line.type === PatchLineType.REMOVE) {
+ containsDeletedLines = true;
+ }
+ const content = extractLineContent(line);
+ // Remove diff prefix if present
+ let cleanContent = content;
+ if (content.startsWith("+") || content.startsWith("-") || content.startsWith(" ")) {
+ cleanContent = content.substring(1);
+ }
+ selectedLines.push(cleanContent);
+ }
+ }
+ }
+
+ return { content: selectedLines.join("\n"), containsDeletedLines };
+ }
+
+
{#snippet lineContent(line: PatchLine, lineType: PatchLineTypeProps, innerLineType: InnerPatchLineTypeProps)}
{#if line.lineBreak}
{:else}
@@ -187,19 +519,189 @@
{/await}
{/snippet}
+{#snippet commentButton(line: PatchLine, hunkIndex: number, isRightColumn: boolean)}
+ {@const leftLineKey = getLineKey(line, "LEFT")}
+ {@const rightLineKey = getLineKey(line, "RIGHT")}
+ {@const leftLineNum = getLineNumber(line, "LEFT")}
+ {@const rightLineNum = getLineNumber(line, "RIGHT")}
+ {@const leftSelected = isLineInSelection(line, "LEFT", hunkIndex)}
+ {@const rightSelected = isLineInSelection(line, "RIGHT", hunkIndex)}
+ {@const isThisLineHovered = hoveredLineKey === leftLineKey || hoveredLineKey === rightLineKey}
+
+ {#if canAddComments && (leftLineNum !== undefined || rightLineNum !== undefined) && line.type !== PatchLineType.HEADER}
+ {#if line.type === PatchLineType.REMOVE && isRightColumn}
+
+
+ {:else if line.type === PatchLineType.ADD && isRightColumn}
+
+
+ {:else if line.type === PatchLineType.CONTEXT && isRightColumn}
+
+
+ {/if}
+ {/if}
+{/snippet}
+
{#snippet renderLine(line: PatchLine, hunkIndex: number, lineIndex: number)}
{@const lineType = patchLineTypeProps[line.type]}
-