Download packages/session-ui/src/components/line-comment-annotations.tsx from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/session-ui/src/components/line-comment-annotations.tsx
- Command line
-
hf download hf://SaylorTwift/opencode/packages/session-ui/src/components/line-comment-annotations.tsx
-
curl -L -o line-comment-annotations.tsx https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/session-ui/src/components/line-comment-annotations.tsx
19.6 kB
| import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs" | |
| import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js" | |
| import { createStore } from "solid-js/store" | |
| import { render as renderSolid } from "solid-js/web" | |
| import { useI18n } from "@opencode-ai/ui/context/i18n" | |
| import { createHoverCommentUtility } from "../pierre/comment-hover" | |
| import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge" | |
| import { LineComment, LineCommentEditor, type LineCommentEditorProps } from "./line-comment" | |
| export type LineCommentAnnotationMeta<T> = | |
| | { kind: "comment"; key: string; comment: T } | |
| | { kind: "draft"; key: string; range: SelectedLineRange } | |
| export type LineCommentAnnotation<T> = { | |
| lineNumber: number | |
| side?: "additions" | "deletions" | |
| metadata: LineCommentAnnotationMeta<T> | |
| } | |
| type LineCommentAnnotationsProps<T> = { | |
| comments: Accessor<T[]> | |
| getCommentId: (comment: T) => string | |
| getCommentSelection: (comment: T) => SelectedLineRange | |
| draftRange: Accessor<SelectedLineRange | null> | |
| draftKey: Accessor<string> | |
| } | |
| type LineCommentAnnotationsWithSideProps<T> = LineCommentAnnotationsProps<T> & { | |
| getSide: (range: SelectedLineRange) => "additions" | "deletions" | |
| } | |
| type HoverCommentLine = { | |
| lineNumber: number | |
| side?: "additions" | "deletions" | |
| } | |
| export type LineCommentStateProps<T> = { | |
| opened: Accessor<T | null> | |
| setOpened: (id: T | null) => void | |
| selected: Accessor<SelectedLineRange | null> | |
| setSelected: (range: SelectedLineRange | null) => void | |
| commenting: Accessor<SelectedLineRange | null> | |
| setCommenting: (range: SelectedLineRange | null) => void | |
| syncSelected?: (range: SelectedLineRange | null) => void | |
| hoverSelected?: (range: SelectedLineRange) => void | |
| } | |
| export type LineCommentShape = { | |
| id: string | |
| selection: SelectedLineRange | |
| comment: string | |
| } | |
| type LineCommentControllerProps<T extends LineCommentShape> = { | |
| comments: Accessor<T[]> | |
| draftKey: Accessor<string> | |
| label: string | |
| mention?: LineCommentEditorProps["mention"] | |
| state: LineCommentStateProps<string> | |
| onSubmit: (input: { comment: string; selection: SelectedLineRange }) => void | |
| onUpdate?: (input: { id: string; comment: string; selection: SelectedLineRange }) => void | |
| onDelete?: (comment: T) => void | |
| renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element | |
| editSubmitLabel?: string | |
| onDraftPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent> | |
| getHoverSelectedRange?: Accessor<SelectedLineRange | null> | |
| cancelDraftOnCommentToggle?: boolean | |
| clearSelectionOnSelectionEndNull?: boolean | |
| } | |
| type LineCommentControllerWithSideProps<T extends LineCommentShape> = LineCommentControllerProps<T> & { | |
| getSide: (range: SelectedLineRange) => "additions" | "deletions" | |
| } | |
| type CommentProps = { | |
| id?: string | |
| open: boolean | |
| comment: JSX.Element | |
| selection: JSX.Element | |
| actions?: JSX.Element | |
| editor?: DraftProps | |
| onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> | |
| onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> | |
| } | |
| type DraftProps = { | |
| value: string | |
| selection: JSX.Element | |
| mention?: LineCommentEditorProps["mention"] | |
| onInput: (value: string) => void | |
| onCancel: VoidFunction | |
| onSubmit: (value: string) => void | |
| onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent> | |
| cancelLabel?: string | |
| submitLabel?: string | |
| } | |
| // Generic host machinery shared by the v1 and v2 annotation renderers: each | |
| // annotation key gets a detached DOM host with its own Solid root, updated in | |
| // place through a signal so Pierre can reparent the host without re-rendering. | |
| export function createLineCommentAnnotationRenderer<T, C, D>(props: { | |
| renderComment: (comment: T) => C | |
| renderDraft: (range: SelectedLineRange) => D | |
| commentElement: (view: Accessor<C>) => JSX.Element | |
| draftElement: (view: Accessor<D>) => JSX.Element | |
| }) { | |
| const nodes = new Map< | |
| string, | |
| { | |
| host: HTMLDivElement | |
| dispose: VoidFunction | |
| setMeta: (meta: LineCommentAnnotationMeta<T>) => void | |
| } | |
| >() | |
| const mount = (meta: LineCommentAnnotationMeta<T>) => { | |
| if (typeof document === "undefined") return | |
| const host = document.createElement("div") | |
| host.setAttribute("data-prevent-autofocus", "") | |
| const [current, setCurrent] = createSignal(meta) | |
| const dispose = renderSolid(() => { | |
| const active = current() | |
| if (active.kind === "comment") { | |
| const view = createMemo(() => { | |
| const next = current() | |
| if (next.kind !== "comment") return props.renderComment(active.comment) | |
| return props.renderComment(next.comment) | |
| }) | |
| return props.commentElement(view) | |
| } | |
| const view = createMemo(() => { | |
| const next = current() | |
| if (next.kind !== "draft") return props.renderDraft(active.range) | |
| return props.renderDraft(next.range) | |
| }) | |
| return props.draftElement(view) | |
| }, host) | |
| const node = { host, dispose, setMeta: setCurrent } | |
| nodes.set(meta.key, node) | |
| return node | |
| } | |
| const render = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotation: A) => { | |
| const meta = annotation.metadata | |
| const node = nodes.get(meta.key) ?? mount(meta) | |
| if (!node) return | |
| node.setMeta(meta) | |
| return node.host | |
| } | |
| const reconcile = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotations: A[]) => { | |
| const next = new Set(annotations.map((annotation) => annotation.metadata.key)) | |
| for (const [key, node] of nodes) { | |
| if (next.has(key)) continue | |
| node.dispose() | |
| nodes.delete(key) | |
| } | |
| } | |
| const cleanup = () => { | |
| for (const [, node] of nodes) node.dispose() | |
| nodes.clear() | |
| } | |
| return { render, reconcile, cleanup } | |
| } | |
| function lineCommentElement(view: Accessor<CommentProps>) { | |
| return ( | |
| <Show | |
| when={view().editor} | |
| fallback={ | |
| <LineComment | |
| inline | |
| id={view().id} | |
| open={view().open} | |
| comment={view().comment} | |
| selection={view().selection} | |
| actions={view().actions} | |
| onClick={view().onClick} | |
| onMouseEnter={view().onMouseEnter} | |
| /> | |
| } | |
| > | |
| <LineCommentEditor | |
| inline | |
| id={view().id} | |
| value={view().editor!.value} | |
| selection={view().editor!.selection} | |
| onInput={view().editor!.onInput} | |
| onCancel={view().editor!.onCancel} | |
| onSubmit={view().editor!.onSubmit} | |
| onPopoverFocusOut={view().editor!.onPopoverFocusOut} | |
| cancelLabel={view().editor!.cancelLabel} | |
| submitLabel={view().editor!.submitLabel} | |
| mention={view().editor!.mention} | |
| /> | |
| </Show> | |
| ) | |
| } | |
| function lineCommentDraftElement(view: Accessor<DraftProps>) { | |
| return ( | |
| <LineCommentEditor | |
| inline | |
| value={view().value} | |
| selection={view().selection} | |
| onInput={view().onInput} | |
| onCancel={view().onCancel} | |
| onSubmit={view().onSubmit} | |
| onPopoverFocusOut={view().onPopoverFocusOut} | |
| mention={view().mention} | |
| /> | |
| ) | |
| } | |
| export function createLineCommentState<T>(props: LineCommentStateProps<T>) { | |
| const [state, setState] = createStore({ | |
| draft: "", | |
| editing: null as T | null, | |
| }) | |
| const draft = () => state.draft | |
| const setDraft = (value: string) => setState("draft", value) | |
| const editing = () => state.editing | |
| const setEditing = (value: T | null) => setState("editing", typeof value === "function" ? () => value : value) | |
| const toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null) | |
| const setSelected = (range: SelectedLineRange | null) => { | |
| const next = toRange(range) | |
| props.setSelected(next) | |
| props.syncSelected?.(toRange(next)) | |
| return next | |
| } | |
| const setCommenting = (range: SelectedLineRange | null) => { | |
| const next = toRange(range) | |
| props.setCommenting(next) | |
| return next | |
| } | |
| const closeComment = () => { | |
| props.setOpened(null) | |
| } | |
| const cancelDraft = () => { | |
| setDraft("") | |
| setEditing(null) | |
| setCommenting(null) | |
| } | |
| const reset = () => { | |
| setDraft("") | |
| setEditing(null) | |
| props.setOpened(null) | |
| props.setSelected(null) | |
| props.setCommenting(null) | |
| } | |
| const openComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => { | |
| if (options?.cancelDraft) cancelDraft() | |
| props.setOpened(id) | |
| setSelected(range) | |
| } | |
| const toggleComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => { | |
| if (options?.cancelDraft) cancelDraft() | |
| const next = props.opened() === id ? null : id | |
| props.setOpened(next) | |
| setSelected(range) | |
| } | |
| const openDraft = (range: SelectedLineRange) => { | |
| const next = toRange(range) | |
| setDraft("") | |
| setEditing(null) | |
| closeComment() | |
| setSelected(next) | |
| setCommenting(next) | |
| } | |
| const openEditor = (id: T, range: SelectedLineRange, value: string) => { | |
| closeComment() | |
| setSelected(range) | |
| props.setCommenting(null) | |
| setEditing(id) | |
| setDraft(value) | |
| } | |
| const hoverComment = (range: SelectedLineRange) => { | |
| const next = toRange(range) | |
| if (!next) return | |
| if (props.hoverSelected) { | |
| props.hoverSelected(next) | |
| return | |
| } | |
| setSelected(next) | |
| } | |
| return { | |
| draft, | |
| setDraft, | |
| editing, | |
| opened: props.opened, | |
| selected: props.selected, | |
| commenting: props.commenting, | |
| isOpen: (id: T) => props.opened() === id, | |
| isEditing: (id: T) => editing() === id, | |
| closeComment, | |
| openComment, | |
| toggleComment, | |
| openDraft, | |
| openEditor, | |
| hoverComment, | |
| cancelDraft, | |
| select: setSelected, | |
| reset, | |
| } | |
| } | |
| export function createLineCommentController<T extends LineCommentShape>( | |
| props: LineCommentControllerWithSideProps<T>, | |
| ): { | |
| note: ReturnType<typeof createLineCommentState<string>> | |
| annotations: Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]> | |
| renderAnnotation: ReturnType< | |
| typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps> | |
| >["renderAnnotation"] | |
| renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer> | |
| onLineSelected: (range: SelectedLineRange | null) => void | |
| onLineSelectionEnd: (range: SelectedLineRange | null) => void | |
| } | |
| export function createLineCommentController<T extends LineCommentShape>( | |
| props: LineCommentControllerProps<T>, | |
| ): { | |
| note: ReturnType<typeof createLineCommentState<string>> | |
| annotations: Accessor<LineCommentAnnotation<T>[]> | |
| renderAnnotation: ReturnType< | |
| typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps> | |
| >["renderAnnotation"] | |
| renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer> | |
| onLineSelected: (range: SelectedLineRange | null) => void | |
| onLineSelectionEnd: (range: SelectedLineRange | null) => void | |
| } | |
| export function createLineCommentController<T extends LineCommentShape>( | |
| props: LineCommentControllerProps<T> | LineCommentControllerWithSideProps<T>, | |
| ) { | |
| const i18n = useI18n() | |
| const note = createLineCommentState<string>(props.state) | |
| const annotations = | |
| "getSide" in props | |
| ? createLineCommentAnnotations({ | |
| comments: props.comments, | |
| getCommentId: (comment) => comment.id, | |
| getCommentSelection: (comment) => comment.selection, | |
| draftRange: note.commenting, | |
| draftKey: props.draftKey, | |
| getSide: props.getSide, | |
| }) | |
| : createLineCommentAnnotations({ | |
| comments: props.comments, | |
| getCommentId: (comment) => comment.id, | |
| getCommentSelection: (comment) => comment.selection, | |
| draftRange: note.commenting, | |
| draftKey: props.draftKey, | |
| }) | |
| const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>({ | |
| annotations, | |
| commentElement: lineCommentElement, | |
| draftElement: lineCommentDraftElement, | |
| renderComment: (comment) => { | |
| const edit = () => note.openEditor(comment.id, comment.selection, comment.comment) | |
| const remove = () => { | |
| note.reset() | |
| props.onDelete?.(comment) | |
| } | |
| return { | |
| id: comment.id, | |
| get open() { | |
| return note.isOpen(comment.id) || note.isEditing(comment.id) | |
| }, | |
| comment: comment.comment, | |
| selection: formatSelectedLineLabel(comment.selection, i18n.t), | |
| get actions() { | |
| return props.renderCommentActions?.(comment, { edit, remove }) | |
| }, | |
| get editor() { | |
| return note.isEditing(comment.id) | |
| ? { | |
| get value() { | |
| return note.draft() | |
| }, | |
| selection: formatSelectedLineLabel(comment.selection, i18n.t), | |
| mention: props.mention, | |
| onInput: note.setDraft, | |
| onCancel: note.cancelDraft, | |
| onSubmit: (value: string) => { | |
| props.onUpdate?.({ | |
| id: comment.id, | |
| comment: value, | |
| selection: cloneSelectedLineRange(comment.selection), | |
| }) | |
| note.cancelDraft() | |
| }, | |
| submitLabel: props.editSubmitLabel, | |
| } | |
| : undefined | |
| }, | |
| onMouseEnter: () => note.hoverComment(comment.selection), | |
| onClick: () => { | |
| if (note.isEditing(comment.id)) return | |
| note.toggleComment(comment.id, comment.selection, { cancelDraft: props.cancelDraftOnCommentToggle }) | |
| }, | |
| } | |
| }, | |
| renderDraft: (range) => ({ | |
| get value() { | |
| return note.draft() | |
| }, | |
| selection: formatSelectedLineLabel(range, i18n.t), | |
| mention: props.mention, | |
| onInput: note.setDraft, | |
| onCancel: () => { | |
| note.cancelDraft() | |
| note.select(null) | |
| }, | |
| onSubmit: (comment) => { | |
| props.onSubmit({ comment, selection: cloneSelectedLineRange(range) }) | |
| note.cancelDraft() | |
| }, | |
| onPopoverFocusOut: props.onDraftPopoverFocusOut, | |
| }), | |
| }) | |
| const renderGutterUtility = createLineCommentGutterRenderer({ | |
| label: props.label, | |
| getSelectedRange: () => { | |
| if (note.opened()) return null | |
| return props.getHoverSelectedRange?.() ?? note.selected() | |
| }, | |
| onOpenDraft: note.openDraft, | |
| }) | |
| const onLineSelected = (range: SelectedLineRange | null) => { | |
| if (!range) { | |
| note.select(null) | |
| note.cancelDraft() | |
| return | |
| } | |
| note.select(range) | |
| } | |
| const onLineSelectionEnd = (range: SelectedLineRange | null) => { | |
| if (!range) { | |
| if (props.clearSelectionOnSelectionEndNull) note.select(null) | |
| note.cancelDraft() | |
| return | |
| } | |
| note.openDraft(range) | |
| } | |
| return { | |
| note, | |
| annotations, | |
| renderAnnotation, | |
| renderGutterUtility, | |
| onLineSelected, | |
| onLineSelectionEnd, | |
| } | |
| } | |
| export function createLineCommentAnnotations<T>( | |
| props: LineCommentAnnotationsWithSideProps<T>, | |
| ): Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]> | |
| export function createLineCommentAnnotations<T>( | |
| props: LineCommentAnnotationsProps<T>, | |
| ): Accessor<LineCommentAnnotation<T>[]> | |
| export function createLineCommentAnnotations<T>( | |
| props: LineCommentAnnotationsProps<T> | LineCommentAnnotationsWithSideProps<T>, | |
| ) { | |
| const line = (range: SelectedLineRange) => Math.max(range.start, range.end) | |
| if ("getSide" in props) { | |
| return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>( | |
| () => { | |
| const list = props.comments().map((comment) => { | |
| const range = props.getCommentSelection(comment) | |
| return { | |
| side: props.getSide(range), | |
| lineNumber: line(range), | |
| metadata: { | |
| kind: "comment", | |
| key: `comment:${props.getCommentId(comment)}`, | |
| comment, | |
| } satisfies LineCommentAnnotationMeta<T>, | |
| } | |
| }) | |
| const range = props.draftRange() | |
| if (!range) return list | |
| return [ | |
| ...list, | |
| { | |
| side: props.getSide(range), | |
| lineNumber: line(range), | |
| metadata: { | |
| kind: "draft", | |
| key: `draft:${props.draftKey()}`, | |
| range, | |
| } satisfies LineCommentAnnotationMeta<T>, | |
| }, | |
| ] | |
| }, | |
| [], | |
| // Stable identity for unchanged annotations avoids no-op diff rerenders downstream. | |
| { equals: sameAnnotationLists }, | |
| ) | |
| } | |
| return createMemo<LineCommentAnnotation<T>[]>( | |
| () => { | |
| const list = props.comments().map((comment) => { | |
| const range = props.getCommentSelection(comment) | |
| const entry: LineCommentAnnotation<T> = { | |
| lineNumber: line(range), | |
| metadata: { | |
| kind: "comment", | |
| key: `comment:${props.getCommentId(comment)}`, | |
| comment, | |
| }, | |
| } | |
| return entry | |
| }) | |
| const range = props.draftRange() | |
| if (!range) return list | |
| const draft: LineCommentAnnotation<T> = { | |
| lineNumber: line(range), | |
| metadata: { | |
| kind: "draft", | |
| key: `draft:${props.draftKey()}`, | |
| range, | |
| }, | |
| } | |
| return [...list, draft] | |
| }, | |
| [], | |
| { equals: sameAnnotationLists }, | |
| ) | |
| } | |
| type AnnotationListItem = { | |
| lineNumber: number | |
| side?: unknown | |
| metadata: { kind: string; key: string; comment?: unknown; range?: unknown } | |
| } | |
| function sameAnnotationLists(previous: AnnotationListItem[], next: AnnotationListItem[]) { | |
| if (previous.length !== next.length) return false | |
| return previous.every((item, index) => { | |
| const other = next[index]! | |
| return ( | |
| item.lineNumber === other.lineNumber && | |
| item.side === other.side && | |
| item.metadata.kind === other.metadata.kind && | |
| item.metadata.key === other.metadata.key && | |
| item.metadata.comment === other.metadata.comment && | |
| item.metadata.range === other.metadata.range | |
| ) | |
| }) | |
| } | |
| export function createManagedLineCommentAnnotationRenderer<T, C, D>(props: { | |
| annotations: Accessor<LineCommentAnnotation<T>[]> | |
| renderComment: (comment: T) => C | |
| renderDraft: (range: SelectedLineRange) => D | |
| commentElement: (view: Accessor<C>) => JSX.Element | |
| draftElement: (view: Accessor<D>) => JSX.Element | |
| }) { | |
| const renderer = createLineCommentAnnotationRenderer<T, C, D>({ | |
| renderComment: props.renderComment, | |
| renderDraft: props.renderDraft, | |
| commentElement: props.commentElement, | |
| draftElement: props.draftElement, | |
| }) | |
| createEffect(() => { | |
| renderer.reconcile(props.annotations()) | |
| }) | |
| onCleanup(() => { | |
| renderer.cleanup() | |
| }) | |
| return { | |
| renderAnnotation: renderer.render, | |
| } | |
| } | |
| export function createLineCommentGutterRenderer(props: { | |
| label: string | |
| getSelectedRange: Accessor<SelectedLineRange | null> | |
| onOpenDraft: (range: SelectedLineRange) => void | |
| }) { | |
| return (getHoveredLine: () => HoverCommentLine | undefined) => | |
| createHoverCommentUtility({ | |
| label: props.label, | |
| getHoveredLine, | |
| onSelect: (hovered) => { | |
| const current = props.getSelectedRange() | |
| if (current && lineInSelectedRange(current, hovered.lineNumber, hovered.side)) { | |
| props.onOpenDraft(cloneSelectedLineRange(current)) | |
| return | |
| } | |
| const range: SelectedLineRange = { | |
| start: hovered.lineNumber, | |
| end: hovered.lineNumber, | |
| } | |
| if (hovered.side) range.side = hovered.side | |
| props.onOpenDraft(range) | |
| }, | |
| }) | |
| } | |