Skip to content

Commit 99e15ca

Browse files
committed
wip(app): line selection
1 parent 1e1872a commit 99e15ca

3 files changed

Lines changed: 115 additions & 31 deletions

File tree

packages/ui/src/components/code.tsx

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
2-
import { ComponentProps, createEffect, createMemo, onCleanup, splitProps } from "solid-js"
2+
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, splitProps } from "solid-js"
33
import { createDefaultOptions, styleVariables } from "../pierre"
44
import { getWorkerPool } from "../pierre/worker"
55

@@ -9,7 +9,9 @@ export type CodeProps<T = {}> = FileOptions<T> & {
99
file: FileContents
1010
annotations?: LineAnnotation<T>[]
1111
selectedLines?: SelectedLineRange | null
12+
commentedLines?: SelectedLineRange[]
1213
onRendered?: () => void
14+
onLineSelectionEnd?: (selection: SelectedLineRange | null) => void
1315
class?: string
1416
classList?: ComponentProps<"div">["classList"]
1517
}
@@ -53,16 +55,22 @@ export function Code<T>(props: CodeProps<T>) {
5355
let dragStart: number | undefined
5456
let dragEnd: number | undefined
5557
let dragMoved = false
58+
let lastSelection: SelectedLineRange | null = null
59+
let pendingSelectionEnd = false
5660

5761
const [local, others] = splitProps(props, [
5862
"file",
5963
"class",
6064
"classList",
6165
"annotations",
6266
"selectedLines",
67+
"commentedLines",
6368
"onRendered",
69+
"onLineSelectionEnd",
6470
])
6571

72+
const [rendered, setRendered] = createSignal(0)
73+
6674
const handleLineClick: FileOptions<T>["onLineClick"] = (info) => {
6775
props.onLineClick?.(info)
6876

@@ -95,6 +103,30 @@ export function Code<T>(props: CodeProps<T>) {
95103
return root
96104
}
97105

106+
const applyCommentedLines = (ranges: SelectedLineRange[]) => {
107+
const root = getRoot()
108+
if (!root) return
109+
110+
const existing = Array.from(root.querySelectorAll("[data-comment-selected]"))
111+
for (const node of existing) {
112+
if (!(node instanceof HTMLElement)) continue
113+
node.removeAttribute("data-comment-selected")
114+
}
115+
116+
for (const range of ranges) {
117+
const start = Math.max(1, Math.min(range.start, range.end))
118+
const end = Math.max(range.start, range.end)
119+
120+
for (let line = start; line <= end; line++) {
121+
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"]`))
122+
for (const node of nodes) {
123+
if (!(node instanceof HTMLElement)) continue
124+
node.setAttribute("data-comment-selected", "")
125+
}
126+
}
127+
}
128+
}
129+
98130
const notifyRendered = () => {
99131
if (!local.onRendered) return
100132

@@ -203,7 +235,12 @@ export function Code<T>(props: CodeProps<T>) {
203235
if (side) selected.side = side
204236
if (endSide && side && endSide !== side) selected.endSide = endSide
205237

206-
file().setSelectedLines(selected)
238+
setSelectedLines(selected)
239+
}
240+
241+
const setSelectedLines = (range: SelectedLineRange | null) => {
242+
lastSelection = range
243+
file().setSelectedLines(range)
207244
}
208245

209246
const scheduleSelectionUpdate = () => {
@@ -212,6 +249,10 @@ export function Code<T>(props: CodeProps<T>) {
212249
selectionFrame = requestAnimationFrame(() => {
213250
selectionFrame = undefined
214251
updateSelection()
252+
253+
if (!pendingSelectionEnd) return
254+
pendingSelectionEnd = false
255+
props.onLineSelectionEnd?.(lastSelection)
215256
})
216257
}
217258

@@ -221,7 +262,7 @@ export function Code<T>(props: CodeProps<T>) {
221262
const start = Math.min(dragStart, dragEnd)
222263
const end = Math.max(dragStart, dragEnd)
223264

224-
file().setSelectedLines({ start, end })
265+
setSelectedLines({ start, end })
225266
}
226267

227268
const scheduleDragUpdate = () => {
@@ -289,19 +330,22 @@ export function Code<T>(props: CodeProps<T>) {
289330

290331
const handleMouseUp = () => {
291332
if (props.enableLineSelection !== true) return
333+
if (dragStart === undefined) return
292334

293-
if (dragStart !== undefined) {
294-
if (dragMoved) scheduleDragUpdate()
295-
dragStart = undefined
296-
dragEnd = undefined
297-
dragMoved = false
335+
if (dragMoved) {
336+
pendingSelectionEnd = true
337+
scheduleDragUpdate()
338+
scheduleSelectionUpdate()
298339
}
299340

300-
scheduleSelectionUpdate()
341+
dragStart = undefined
342+
dragEnd = undefined
343+
dragMoved = false
301344
}
302345

303346
const handleSelectionChange = () => {
304347
if (props.enableLineSelection !== true) return
348+
if (dragStart === undefined) return
305349

306350
const selection = window.getSelection()
307351
if (!selection || selection.isCollapsed) return
@@ -328,11 +372,18 @@ export function Code<T>(props: CodeProps<T>) {
328372
containerWrapper: container,
329373
})
330374

375+
setRendered((value) => value + 1)
331376
notifyRendered()
332377
})
333378

334379
createEffect(() => {
335-
file().setSelectedLines(local.selectedLines ?? null)
380+
rendered()
381+
const ranges = local.commentedLines ?? []
382+
requestAnimationFrame(() => applyCommentedLines(ranges))
383+
})
384+
385+
createEffect(() => {
386+
setSelectedLines(local.selectedLines ?? null)
336387
})
337388

338389
createEffect(() => {
@@ -367,6 +418,8 @@ export function Code<T>(props: CodeProps<T>) {
367418
dragStart = undefined
368419
dragEnd = undefined
369420
dragMoved = false
421+
lastSelection = null
422+
pendingSelectionEnd = false
370423
})
371424

372425
return (

packages/ui/src/components/session-review.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@
7070
user-select: text;
7171
}
7272

73+
[data-slot="session-review-accordion-content"] {
74+
position: relative;
75+
overflow: hidden;
76+
}
77+
78+
[data-component="popover-content"] {
79+
position: absolute !important;
80+
}
81+
82+
.session-review-comment-popover-content {
83+
left: auto !important;
84+
right: calc(100% + 12px) !important;
85+
}
86+
7387
[data-slot="session-review-trigger-content"] {
7488
display: flex;
7589
align-items: center;

packages/ui/src/components/session-review.tsx

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Accordion } from "./accordion"
22
import { Button } from "./button"
3-
import { HoverCard } from "./hover-card"
43
import { Popover } from "./popover"
54
import { RadioGroup } from "./radio-group"
65
import { DiffChanges } from "./diff-changes"
@@ -151,6 +150,7 @@ function markerTop(wrapper: HTMLElement, marker: HTMLElement) {
151150
}
152151

153152
export const SessionReview = (props: SessionReviewProps) => {
153+
let scroll: HTMLDivElement | undefined
154154
const i18n = useI18n()
155155
const diffComponent = useDiffComponent()
156156
const anchors = new Map<string, HTMLElement>()
@@ -212,7 +212,29 @@ export const SessionReview = (props: SessionReviewProps) => {
212212
}
213213

214214
requestAnimationFrame(() => {
215-
anchors.get(focus.file)?.scrollIntoView({ block: "center" })
215+
requestAnimationFrame(() => {
216+
const root = scroll
217+
if (!root) return
218+
219+
const anchor = root.querySelector(`[data-comment-id="${focus.id}"]`)
220+
if (anchor instanceof HTMLElement) {
221+
const rootRect = root.getBoundingClientRect()
222+
const anchorRect = anchor.getBoundingClientRect()
223+
const offset = anchorRect.top - rootRect.top
224+
const next = root.scrollTop + offset - rootRect.height / 2 + anchorRect.height / 2
225+
root.scrollTop = Math.max(0, next)
226+
return
227+
}
228+
229+
const target = anchors.get(focus.file)
230+
if (!target) return
231+
232+
const rootRect = root.getBoundingClientRect()
233+
const targetRect = target.getBoundingClientRect()
234+
const offset = targetRect.top - rootRect.top
235+
const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2
236+
root.scrollTop = Math.max(0, next)
237+
})
216238
})
217239

218240
requestAnimationFrame(() => props.onFocusedCommentChange?.(null))
@@ -221,7 +243,10 @@ export const SessionReview = (props: SessionReviewProps) => {
221243
return (
222244
<div
223245
data-component="session-review"
224-
ref={props.scrollRef}
246+
ref={(el) => {
247+
scroll = el
248+
props.scrollRef?.(el)
249+
}}
225250
onScroll={props.onScroll}
226251
classList={{
227252
...(props.classList ?? {}),
@@ -574,6 +599,7 @@ export const SessionReview = (props: SessionReviewProps) => {
574599
{(comment) => (
575600
<div
576601
data-slot="session-review-comment-anchor"
602+
data-comment-id={comment.id}
577603
style={{
578604
top: `${positions()[comment.id] ?? 0}px`,
579605
opacity: positions()[comment.id] === undefined ? 0 : 1,
@@ -583,6 +609,7 @@ export const SessionReview = (props: SessionReviewProps) => {
583609
<Popover
584610
portal={false}
585611
open={isCommentOpen(comment)}
612+
class="session-review-comment-popover-content"
586613
onOpenChange={(open) => {
587614
if (open) {
588615
openComment(comment)
@@ -592,26 +619,15 @@ export const SessionReview = (props: SessionReviewProps) => {
592619
setOpened(null)
593620
}}
594621
trigger={
595-
<HoverCard
596-
trigger={
597-
<button
598-
type="button"
599-
data-slot="session-review-comment-button"
600-
onMouseEnter={() =>
601-
setSelection({ file: comment.file, range: comment.selection })
602-
}
603-
>
604-
<Icon name="speech-bubble" size="small" />
605-
</button>
622+
<button
623+
type="button"
624+
data-slot="session-review-comment-button"
625+
onMouseEnter={() =>
626+
setSelection({ file: comment.file, range: comment.selection })
606627
}
607628
>
608-
<div data-slot="session-review-comment-hover">
609-
<div data-slot="session-review-comment-hover-label">
610-
{getFilename(comment.file)}:{selectionLabel(comment.selection)}
611-
</div>
612-
<div data-slot="session-review-comment-hover-text">{comment.comment}</div>
613-
</div>
614-
</HoverCard>
629+
<Icon name="speech-bubble" size="small" />
630+
</button>
615631
}
616632
>
617633
<div data-slot="session-review-comment-popover">
@@ -635,6 +651,7 @@ export const SessionReview = (props: SessionReviewProps) => {
635651
<Popover
636652
portal={false}
637653
open={true}
654+
class="session-review-comment-popover-content"
638655
onOpenChange={(open) => {
639656
if (open) return
640657
setCommenting(null)

0 commit comments

Comments
 (0)