From 393065771ff72bc1ea9547f7fe9da0dcc6bbff44 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Fri, 23 Oct 2026 09:50:05 +0100 Subject: [PATCH] web/pulls: quote diff lines into a comment from the gutter just like go appview, this can be removed when we have a more stable review lexicon Signed-off-by: oppiliappan --- .../lib/components/repo/DiffCodeView.svelte | 26 ++++++++- web/src/lib/components/repo/DiffFiles.svelte | 10 +++- web/src/lib/components/repo/DiffView.svelte | 6 +- web/src/lib/components/repo/pierreDiff.css | 57 +++++++++++++++++++ .../repo/pulls/PullReviewCommentForm.svelte | 29 +++++++--- .../[version]/[[range]]/PullViewPage.svelte | 33 +++++++++-- 6 files changed, 146 insertions(+), 15 deletions(-) diff --git a/web/src/lib/components/repo/DiffCodeView.svelte b/web/src/lib/components/repo/DiffCodeView.svelte index 96d205871..f3ae5d65d 100644 --- a/web/src/lib/components/repo/DiffCodeView.svelte +++ b/web/src/lib/components/repo/DiffCodeView.svelte @@ -58,6 +58,7 @@ onScrollExtent?: (max: number) => void; onLineSelected?: (key: string, range: SelectedLineRange | null) => void; onWantWhole?: (key: string) => boolean; + onQuoteLines?: (key: string, range: SelectedLineRange) => void; } let { @@ -74,7 +75,8 @@ dockTop = () => 0, onScrollExtent, onLineSelected, - onWantWhole + onWantWhole, + onQuoteLines }: Props = $props(); const HEADER_HEIGHT = 38; @@ -140,14 +142,29 @@ onLineSelectionStart: trackSelection, onLineSelectionChange: trackSelection, onLineSelected: (range: SelectedLineRange | null, context: ItemContext) => { + if (quoting) { + quoting = false; + restoreSelection(); + return; + } trackSelection(range, context); + committed = selected; onLineSelected?.(context.item.id, range); }, + enableGutterUtility: onQuoteLines !== undefined, + onGutterUtilityClick: + onQuoteLines && + ((range: SelectedLineRange, context: ItemContext) => { + quoting = true; + onQuoteLines(context.item.id, range); + }), __devOnlyValidateItemHeights: PIERRE_VALIDATE_ITEM_HEIGHTS, layout: { paddingTop: 0, paddingBottom: 8, gap: 8 } }); let selected: CodeViewLineSelection | null = null; + let committed: CodeViewLineSelection | null = null; + let quoting = false; type ItemContext = { item: { id: string } }; @@ -168,6 +185,12 @@ } }; + const restoreSelection = () => { + selected = committed; + view?.setSelectedLines(committed, { notify: false }); + paintSelection(); + }; + const trackSelection = (range: SelectedLineRange | null, context: ItemContext): void => { selected = range ? { id: context.item.id, range } : null; paintSelection(); @@ -452,6 +475,7 @@ return top; } selected = { id: key, range }; + committed = selected; instance.setSelectedLines(selected, { notify: false }); paintSelection(); if (isOpen && !isOpen(key)) { diff --git a/web/src/lib/components/repo/DiffFiles.svelte b/web/src/lib/components/repo/DiffFiles.svelte index bf030709e..00350e535 100644 --- a/web/src/lib/components/repo/DiffFiles.svelte +++ b/web/src/lib/components/repo/DiffFiles.svelte @@ -30,6 +30,7 @@ dockTop?: () => number; onScrollExtent?: (max: number) => void; linkLines?: boolean; + onQuoteLines?: (path: string, range: SelectedLineRange) => void; } let { @@ -44,7 +45,8 @@ patch, dockTop, onScrollExtent, - linkLines = false + linkLines = false, + onQuoteLines }: Props = $props(); // parsed metadata only: raw text is dropped after parsing. a map, because @@ -172,6 +174,11 @@ }); }; + const quoteLines = (key: string, range: SelectedLineRange): void => { + const file = fileByKey(key); + if (file) onQuoteLines?.(file.name, range); + }; + export const resync = () => view?.resync(); jumpToAnchoredFile(jumpToFile); @@ -197,5 +204,6 @@ onFocus={(key) => queue.focus(key)} onLineSelected={linkLines ? selectLines : undefined} onWantWhole={(key) => queue.whole(key)} + onQuoteLines={onQuoteLines && quoteLines} /> diff --git a/web/src/lib/components/repo/DiffView.svelte b/web/src/lib/components/repo/DiffView.svelte index 12e3e2e88..4bd96c864 100644 --- a/web/src/lib/components/repo/DiffView.svelte +++ b/web/src/lib/components/repo/DiffView.svelte @@ -31,6 +31,7 @@ import DiffFileTree from "#lib/components/repo/DiffFileTree.svelte"; import DiffStatPill from "#lib/components/repo/DiffStatPill.svelte"; import DiffFiles from "#lib/components/repo/DiffFiles.svelte"; + import type { SelectedLineRange } from "@pierre/diffs"; import type { CodeViewFile } from "#lib/components/repo/DiffCodeView.svelte"; import type { DiffStyle } from "#lib/components/repo/pierre.ts"; import type { PatchStream } from "#lib/api/patchStream.ts"; @@ -64,6 +65,7 @@ patch?: PatchStream; treeCollapsed?: boolean; linkLines?: boolean; + onQuoteLines?: (path: string, range: SelectedLineRange) => void; } let { @@ -79,7 +81,8 @@ patchUrl, patch, treeCollapsed = false, - linkLines = false + linkLines = false, + onQuoteLines }: Props = $props(); let treeOpen = $state(untrack(() => !treeCollapsed)); @@ -374,6 +377,7 @@ onSetOpen={(key, open) => (openStates[key] = open)} patchUrl={patchUrl ?? downloadUrls?.diff} {linkLines} + {onQuoteLines} /> {/key} diff --git a/web/src/lib/components/repo/pierreDiff.css b/web/src/lib/components/repo/pierreDiff.css index 2a16418e8..b149b4166 100644 --- a/web/src/lib/components/repo/pierreDiff.css +++ b/web/src/lib/components/repo/pierreDiff.css @@ -184,3 +184,60 @@ [data-line-type="change-deletion"]::before { background-size: 2px 3px; } + +[data-gutter-utility-slot] { + right: unset; + left: 4px; + align-items: center; + justify-content: flex-start; +} + +[data-utility-button] { + width: 18px; + height: 18px; + margin-right: 0; + border: 1px solid var(--color-border-strong); + background-color: var(--color-background-default); + color: var(--color-foreground-subtle); + box-shadow: var(--shadow-small); + transition: + color 150ms ease, + background-color 150ms ease, + transform 160ms var(--ease-out-strong); + + & > svg[data-icon] { + display: none; + } + + &::after { + content: ""; + width: 14px; + height: 14px; + background-color: currentColor; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M14 14a2 2 0 0 0 2-2V8h-2'/%3E%3Cpath d='M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z'/%3E%3Cpath d='M8 14a2 2 0 0 0 2-2V8H8'/%3E%3C/svg%3E") + center / contain no-repeat; + } + + &:active { + transform: scale(0.97); + } +} + +@media (hover: hover) and (pointer: fine) { + [data-utility-button]:hover { + background-color: var(--color-background-subtle); + color: var(--color-foreground-default); + } +} + +@media (prefers-reduced-motion: reduce) { + [data-utility-button] { + transition: + color 150ms ease, + background-color 150ms ease; + + &:active { + transform: none; + } + } +} diff --git a/web/src/lib/components/repo/pulls/PullReviewCommentForm.svelte b/web/src/lib/components/repo/pulls/PullReviewCommentForm.svelte index bc21706ff..cbda30fbe 100644 --- a/web/src/lib/components/repo/pulls/PullReviewCommentForm.svelte +++ b/web/src/lib/components/repo/pulls/PullReviewCommentForm.svelte @@ -7,6 +7,10 @@ lines?: { start: number; end?: number }; } + export interface CommentFormApi { + insert: (text: string) => void; + } + const referenceRange = (lines: CodeReference["lines"]) => lines && lineNumbers({ start: lines.start, end: lines.end ?? lines.start }); @@ -16,6 +20,7 @@ import { onMount } from "svelte"; import Code from "$icon/code"; import X from "$icon/x"; + import { applyEdit, insertInline } from "#lib/components/ui/markdownFormat.ts"; import { createAction } from "#lib/action.svelte.ts"; import Button from "#lib/components/ui/Button.svelte"; import Checkbox from "#lib/components/ui/Checkbox.svelte"; @@ -79,6 +84,13 @@ mac = isMac(); }); + export const insert: CommentFormApi["insert"] = (text) => { + const el = field; + if (!el || el.disabled) return; + const selection = { value: el.value, start: el.selectionStart, end: el.selectionEnd }; + applyEdit(el, insertInline(selection, text)); + }; + const inert = $derived(disabled || publish.loading || value.trim() === ""); // ctrl/cmd+enter submits, mirroring MarkdownEditor's shortcut @@ -107,18 +119,21 @@

- {keyLabel("enter", mac)} - Newline - - - {keyLabel("mod", mac)} + {keyLabel("enter", mac)} + Newline + + + + {keyLabel("mod", mac)} + {keyLabel("enter", mac)} + + Send - Send