diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index c6f702fb5..16a915d9d 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -1,5 +1,5 @@ import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs" -import { ComponentProps, createEffect, createMemo, onCleanup, splitProps } from "solid-js" +import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js" import { createDefaultOptions, styleVariables } from "../pierre" import { getWorkerPool } from "../pierre/worker" @@ -9,7 +9,9 @@ export type CodeProps = FileOptions & { file: FileContents annotations?: LineAnnotation[] selectedLines?: SelectedLineRange | null + commentedLines?: SelectedLineRange[] onRendered?: () => void + onLineSelectionEnd?: (selection: SelectedLineRange | null) => void class?: string classList?: ComponentProps<"div">["classList"] } @@ -53,6 +55,8 @@ export function Code(props: CodeProps) { let dragStart: number | undefined let dragEnd: number | undefined let dragMoved = false + let lastSelection: SelectedLineRange | null = null + let pendingSelectionEnd = false const [local, others] = splitProps(props, [ "file", @@ -60,9 +64,13 @@ export function Code(props: CodeProps) { "classList", "annotations", "selectedLines", + "commentedLines", "onRendered", + "onLineSelectionEnd", ]) + const [rendered, setRendered] = createSignal(0) + const handleLineClick: FileOptions["onLineClick"] = (info) => { props.onLineClick?.(info) @@ -95,6 +103,30 @@ export function Code(props: CodeProps) { return root } + const applyCommentedLines = (ranges: SelectedLineRange[]) => { + const root = getRoot() + if (!root) return + + const existing = Array.from(root.querySelectorAll("[data-comment-selected]")) + for (const node of existing) { + if (!(node instanceof HTMLElement)) continue + node.removeAttribute("data-comment-selected") + } + + for (const range of ranges) { + const start = Math.max(1, Math.min(range.start, range.end)) + const end = Math.max(range.start, range.end) + + for (let line = start; line <= end; line++) { + const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"]`)) + for (const node of nodes) { + if (!(node instanceof HTMLElement)) continue + node.setAttribute("data-comment-selected", "") + } + } + } + } + const notifyRendered = () => { if (!local.onRendered) return @@ -203,7 +235,12 @@ export function Code(props: CodeProps) { if (side) selected.side = side if (endSide && side && endSide !== side) selected.endSide = endSide - file().setSelectedLines(selected) + setSelectedLines(selected) + } + + const setSelectedLines = (range: SelectedLineRange | null) => { + lastSelection = range + file().setSelectedLines(range) } const scheduleSelectionUpdate = () => { @@ -212,6 +249,10 @@ export function Code(props: CodeProps) { selectionFrame = requestAnimationFrame(() => { selectionFrame = undefined updateSelection() + + if (!pendingSelectionEnd) return + pendingSelectionEnd = false + props.onLineSelectionEnd?.(lastSelection) }) } @@ -221,7 +262,7 @@ export function Code(props: CodeProps) { const start = Math.min(dragStart, dragEnd) const end = Math.max(dragStart, dragEnd) - file().setSelectedLines({ start, end }) + setSelectedLines({ start, end }) } const scheduleDragUpdate = () => { @@ -289,19 +330,22 @@ export function Code(props: CodeProps) { const handleMouseUp = () => { if (props.enableLineSelection !== true) return + if (dragStart === undefined) return - if (dragStart !== undefined) { - if (dragMoved) scheduleDragUpdate() - dragStart = undefined - dragEnd = undefined - dragMoved = false + if (dragMoved) { + pendingSelectionEnd = true + scheduleDragUpdate() + scheduleSelectionUpdate() } - scheduleSelectionUpdate() + dragStart = undefined + dragEnd = undefined + dragMoved = false } const handleSelectionChange = () => { if (props.enableLineSelection !== true) return + if (dragStart === undefined) return const selection = window.getSelection() if (!selection || selection.isCollapsed) return @@ -328,11 +372,18 @@ export function Code(props: CodeProps) { containerWrapper: container, }) + setRendered((value) => value + 1) notifyRendered() }) createEffect(() => { - file().setSelectedLines(local.selectedLines ?? null) + rendered() + const ranges = local.commentedLines ?? [] + requestAnimationFrame(() => applyCommentedLines(ranges)) + }) + + createEffect(() => { + setSelectedLines(local.selectedLines ?? null) }) createEffect(() => { @@ -367,6 +418,8 @@ export function Code(props: CodeProps) { dragStart = undefined dragEnd = undefined dragMoved = false + lastSelection = null + pendingSelectionEnd = false }) return ( diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index d271da5f9..26ca73265 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -70,6 +70,20 @@ user-select: text; } + [data-slot="session-review-accordion-content"] { + position: relative; + overflow: hidden; + } + + [data-component="popover-content"] { + position: absolute !important; + } + + .session-review-comment-popover-content { + left: auto !important; + right: calc(100% + 12px) !important; + } + [data-slot="session-review-trigger-content"] { display: flex; align-items: center; diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index f3e7736f8..4096f341b 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -1,6 +1,5 @@ import { Accordion } from "./accordion" import { Button } from "./button" -import { HoverCard } from "./hover-card" import { Popover } from "./popover" import { RadioGroup } from "./radio-group" import { DiffChanges } from "./diff-changes" @@ -151,6 +150,7 @@ function markerTop(wrapper: HTMLElement, marker: HTMLElement) { } export const SessionReview = (props: SessionReviewProps) => { + let scroll: HTMLDivElement | undefined const i18n = useI18n() const diffComponent = useDiffComponent() const anchors = new Map() @@ -212,7 +212,29 @@ export const SessionReview = (props: SessionReviewProps) => { } requestAnimationFrame(() => { - anchors.get(focus.file)?.scrollIntoView({ block: "center" }) + requestAnimationFrame(() => { + const root = scroll + if (!root) return + + const anchor = root.querySelector(`[data-comment-id="${focus.id}"]`) + if (anchor instanceof HTMLElement) { + const rootRect = root.getBoundingClientRect() + const anchorRect = anchor.getBoundingClientRect() + const offset = anchorRect.top - rootRect.top + const next = root.scrollTop + offset - rootRect.height / 2 + anchorRect.height / 2 + root.scrollTop = Math.max(0, next) + return + } + + const target = anchors.get(focus.file) + if (!target) return + + const rootRect = root.getBoundingClientRect() + const targetRect = target.getBoundingClientRect() + const offset = targetRect.top - rootRect.top + const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2 + root.scrollTop = Math.max(0, next) + }) }) requestAnimationFrame(() => props.onFocusedCommentChange?.(null)) @@ -221,7 +243,10 @@ export const SessionReview = (props: SessionReviewProps) => { return (
{ + scroll = el + props.scrollRef?.(el) + }} onScroll={props.onScroll} classList={{ ...(props.classList ?? {}), @@ -574,6 +599,7 @@ export const SessionReview = (props: SessionReviewProps) => { {(comment) => (
{ { if (open) { openComment(comment) @@ -592,26 +619,15 @@ export const SessionReview = (props: SessionReviewProps) => { setOpened(null) }} trigger={ - - setSelection({ file: comment.file, range: comment.selection }) - } - > - - + } >
@@ -635,6 +651,7 @@ export const SessionReview = (props: SessionReviewProps) => { { if (open) return setCommenting(null)