@neurocode-ai/session-ui 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +71 -0
- package/src/components/apply-patch-file.test.ts +43 -0
- package/src/components/apply-patch-file.ts +78 -0
- package/src/components/basic-tool.css +358 -0
- package/src/components/basic-tool.stories.tsx +133 -0
- package/src/components/basic-tool.tsx +343 -0
- package/src/components/dock-prompt.stories.tsx +62 -0
- package/src/components/dock-prompt.tsx +23 -0
- package/src/components/file-media.tsx +291 -0
- package/src/components/file-search.tsx +72 -0
- package/src/components/file-ssr.tsx +197 -0
- package/src/components/file.css +46 -0
- package/src/components/file.tsx +1202 -0
- package/src/components/line-comment-annotations.tsx +637 -0
- package/src/components/line-comment-styles.ts +292 -0
- package/src/components/line-comment.stories.tsx +115 -0
- package/src/components/line-comment.tsx +439 -0
- package/src/components/markdown-cache.tsx +78 -0
- package/src/components/markdown-code-state.test.ts +32 -0
- package/src/components/markdown-code-state.ts +22 -0
- package/src/components/markdown-inline-code-kind.test.ts +46 -0
- package/src/components/markdown-inline-code-kind.ts +1915 -0
- package/src/components/markdown-preload.test.ts +18 -0
- package/src/components/markdown-shiki.worker.ts +104 -0
- package/src/components/markdown-stream.test.ts +194 -0
- package/src/components/markdown-stream.ts +110 -0
- package/src/components/markdown-worker-protocol.test.ts +81 -0
- package/src/components/markdown-worker-protocol.ts +48 -0
- package/src/components/markdown-worker-queue.test.ts +49 -0
- package/src/components/markdown-worker-queue.ts +64 -0
- package/src/components/markdown-worker-transport.test.ts +56 -0
- package/src/components/markdown-worker-transport.ts +41 -0
- package/src/components/markdown-worker.ts +122 -0
- package/src/components/markdown.css +377 -0
- package/src/components/markdown.stories.tsx +53 -0
- package/src/components/markdown.tsx +678 -0
- package/src/components/message-file.test.ts +63 -0
- package/src/components/message-file.ts +34 -0
- package/src/components/message-nav.css +127 -0
- package/src/components/message-nav.stories.tsx +7 -0
- package/src/components/message-nav.tsx +102 -0
- package/src/components/message-part-text.ts +3 -0
- package/src/components/message-part.css +1569 -0
- package/src/components/message-part.stories.tsx +7 -0
- package/src/components/message-part.test.ts +28 -0
- package/src/components/message-part.tsx +2662 -0
- package/src/components/session-diff.test.ts +135 -0
- package/src/components/session-diff.ts +145 -0
- package/src/components/session-retry.tsx +74 -0
- package/src/components/session-review.css +247 -0
- package/src/components/session-review.stories.tsx +7 -0
- package/src/components/session-review.tsx +657 -0
- package/src/components/session-turn.css +241 -0
- package/src/components/session-turn.stories.tsx +7 -0
- package/src/components/session-turn.tsx +543 -0
- package/src/components/shell-submessage-motion.stories.tsx +346 -0
- package/src/components/shell-submessage.css +23 -0
- package/src/components/timeline-playground.stories.tsx +2090 -0
- package/src/components/tool-count-label.css +57 -0
- package/src/components/tool-count-label.tsx +58 -0
- package/src/components/tool-count-summary.css +102 -0
- package/src/components/tool-count-summary.stories.tsx +238 -0
- package/src/components/tool-count-summary.tsx +52 -0
- package/src/components/tool-error-card.css +91 -0
- package/src/components/tool-error-card.stories.tsx +92 -0
- package/src/components/tool-error-card.tsx +157 -0
- package/src/components/tool-status-title.css +89 -0
- package/src/components/tool-status-title.tsx +133 -0
- package/src/context/data.tsx +64 -0
- package/src/context/index.ts +1 -0
- package/src/pierre/comment-hover.ts +83 -0
- package/src/pierre/commented-lines.ts +91 -0
- package/src/pierre/diff-selection.ts +71 -0
- package/src/pierre/file-find.ts +485 -0
- package/src/pierre/file-runtime.ts +114 -0
- package/src/pierre/file-selection.ts +85 -0
- package/src/pierre/index.ts +186 -0
- package/src/pierre/media.ts +110 -0
- package/src/pierre/selection-bridge.ts +132 -0
- package/src/pierre/virtualizer.ts +100 -0
- package/src/pierre/worker.ts +52 -0
- package/src/styles/index.css +14 -0
- package/src/v2/components/attachment-card-v2.css +70 -0
- package/src/v2/components/attachment-card-v2.tsx +33 -0
- package/src/v2/components/basic-tool-v2.css +163 -0
- package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
- package/src/v2/components/basic-tool-v2.tsx +139 -0
- package/src/v2/components/comment-card-v2.tsx +64 -0
- package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
- package/src/v2/components/prompt-input/attachments.ts +266 -0
- package/src/v2/components/prompt-input/index.tsx +706 -0
- package/src/v2/components/prompt-input/interaction.ts +482 -0
- package/src/v2/components/prompt-input/machine.test.ts +164 -0
- package/src/v2/components/prompt-input/machine.ts +261 -0
- package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
- package/src/v2/components/prompt-input/store.test.ts +116 -0
- package/src/v2/components/prompt-input/store.ts +152 -0
- package/src/v2/components/prompt-input/types.ts +106 -0
- package/src/v2/components/session-file-panel-v2.tsx +43 -0
- package/src/v2/components/session-progress-indicator-v2.css +875 -0
- package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
- package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
- package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
- package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
- package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
- package/src/v2/components/session-review-v2.css +458 -0
- package/src/v2/components/session-review-v2.tsx +340 -0
- package/src/v2/components/tool-error-card-v2.css +200 -0
- package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
- package/src/v2/components/tool-error-card-v2.tsx +166 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs"
|
|
2
|
+
import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js"
|
|
3
|
+
import { createStore } from "solid-js/store"
|
|
4
|
+
import { render as renderSolid } from "solid-js/web"
|
|
5
|
+
import { useI18n } from "@neurocode-ai/ui/context/i18n"
|
|
6
|
+
import { createHoverCommentUtility } from "../pierre/comment-hover"
|
|
7
|
+
import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
|
|
8
|
+
import { LineComment, LineCommentEditor, type LineCommentEditorProps } from "./line-comment"
|
|
9
|
+
|
|
10
|
+
export type LineCommentAnnotationMeta<T> =
|
|
11
|
+
| { kind: "comment"; key: string; comment: T }
|
|
12
|
+
| { kind: "draft"; key: string; range: SelectedLineRange }
|
|
13
|
+
|
|
14
|
+
export type LineCommentAnnotation<T> = {
|
|
15
|
+
lineNumber: number
|
|
16
|
+
side?: "additions" | "deletions"
|
|
17
|
+
metadata: LineCommentAnnotationMeta<T>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type LineCommentAnnotationsProps<T> = {
|
|
21
|
+
comments: Accessor<T[]>
|
|
22
|
+
getCommentId: (comment: T) => string
|
|
23
|
+
getCommentSelection: (comment: T) => SelectedLineRange
|
|
24
|
+
draftRange: Accessor<SelectedLineRange | null>
|
|
25
|
+
draftKey: Accessor<string>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type LineCommentAnnotationsWithSideProps<T> = LineCommentAnnotationsProps<T> & {
|
|
29
|
+
getSide: (range: SelectedLineRange) => "additions" | "deletions"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type HoverCommentLine = {
|
|
33
|
+
lineNumber: number
|
|
34
|
+
side?: "additions" | "deletions"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type LineCommentStateProps<T> = {
|
|
38
|
+
opened: Accessor<T | null>
|
|
39
|
+
setOpened: (id: T | null) => void
|
|
40
|
+
selected: Accessor<SelectedLineRange | null>
|
|
41
|
+
setSelected: (range: SelectedLineRange | null) => void
|
|
42
|
+
commenting: Accessor<SelectedLineRange | null>
|
|
43
|
+
setCommenting: (range: SelectedLineRange | null) => void
|
|
44
|
+
syncSelected?: (range: SelectedLineRange | null) => void
|
|
45
|
+
hoverSelected?: (range: SelectedLineRange) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type LineCommentShape = {
|
|
49
|
+
id: string
|
|
50
|
+
selection: SelectedLineRange
|
|
51
|
+
comment: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type LineCommentControllerProps<T extends LineCommentShape> = {
|
|
55
|
+
comments: Accessor<T[]>
|
|
56
|
+
draftKey: Accessor<string>
|
|
57
|
+
label: string
|
|
58
|
+
mention?: LineCommentEditorProps["mention"]
|
|
59
|
+
state: LineCommentStateProps<string>
|
|
60
|
+
onSubmit: (input: { comment: string; selection: SelectedLineRange }) => void
|
|
61
|
+
onUpdate?: (input: { id: string; comment: string; selection: SelectedLineRange }) => void
|
|
62
|
+
onDelete?: (comment: T) => void
|
|
63
|
+
renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
|
|
64
|
+
editSubmitLabel?: string
|
|
65
|
+
onDraftPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
|
|
66
|
+
getHoverSelectedRange?: Accessor<SelectedLineRange | null>
|
|
67
|
+
cancelDraftOnCommentToggle?: boolean
|
|
68
|
+
clearSelectionOnSelectionEndNull?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type LineCommentControllerWithSideProps<T extends LineCommentShape> = LineCommentControllerProps<T> & {
|
|
72
|
+
getSide: (range: SelectedLineRange) => "additions" | "deletions"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type CommentProps = {
|
|
76
|
+
id?: string
|
|
77
|
+
open: boolean
|
|
78
|
+
comment: JSX.Element
|
|
79
|
+
selection: JSX.Element
|
|
80
|
+
actions?: JSX.Element
|
|
81
|
+
editor?: DraftProps
|
|
82
|
+
onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
|
83
|
+
onMouseEnter?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type DraftProps = {
|
|
87
|
+
value: string
|
|
88
|
+
selection: JSX.Element
|
|
89
|
+
mention?: LineCommentEditorProps["mention"]
|
|
90
|
+
onInput: (value: string) => void
|
|
91
|
+
onCancel: VoidFunction
|
|
92
|
+
onSubmit: (value: string) => void
|
|
93
|
+
onPopoverFocusOut?: JSX.EventHandlerUnion<HTMLDivElement, FocusEvent>
|
|
94
|
+
cancelLabel?: string
|
|
95
|
+
submitLabel?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Generic host machinery shared by the v1 and v2 annotation renderers: each
|
|
99
|
+
// annotation key gets a detached DOM host with its own Solid root, updated in
|
|
100
|
+
// place through a signal so Pierre can reparent the host without re-rendering.
|
|
101
|
+
export function createLineCommentAnnotationRenderer<T, C, D>(props: {
|
|
102
|
+
renderComment: (comment: T) => C
|
|
103
|
+
renderDraft: (range: SelectedLineRange) => D
|
|
104
|
+
commentElement: (view: Accessor<C>) => JSX.Element
|
|
105
|
+
draftElement: (view: Accessor<D>) => JSX.Element
|
|
106
|
+
}) {
|
|
107
|
+
const nodes = new Map<
|
|
108
|
+
string,
|
|
109
|
+
{
|
|
110
|
+
host: HTMLDivElement
|
|
111
|
+
dispose: VoidFunction
|
|
112
|
+
setMeta: (meta: LineCommentAnnotationMeta<T>) => void
|
|
113
|
+
}
|
|
114
|
+
>()
|
|
115
|
+
|
|
116
|
+
const mount = (meta: LineCommentAnnotationMeta<T>) => {
|
|
117
|
+
if (typeof document === "undefined") return
|
|
118
|
+
|
|
119
|
+
const host = document.createElement("div")
|
|
120
|
+
host.setAttribute("data-prevent-autofocus", "")
|
|
121
|
+
const [current, setCurrent] = createSignal(meta)
|
|
122
|
+
|
|
123
|
+
const dispose = renderSolid(() => {
|
|
124
|
+
const active = current()
|
|
125
|
+
if (active.kind === "comment") {
|
|
126
|
+
const view = createMemo(() => {
|
|
127
|
+
const next = current()
|
|
128
|
+
if (next.kind !== "comment") return props.renderComment(active.comment)
|
|
129
|
+
return props.renderComment(next.comment)
|
|
130
|
+
})
|
|
131
|
+
return props.commentElement(view)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const view = createMemo(() => {
|
|
135
|
+
const next = current()
|
|
136
|
+
if (next.kind !== "draft") return props.renderDraft(active.range)
|
|
137
|
+
return props.renderDraft(next.range)
|
|
138
|
+
})
|
|
139
|
+
return props.draftElement(view)
|
|
140
|
+
}, host)
|
|
141
|
+
|
|
142
|
+
const node = { host, dispose, setMeta: setCurrent }
|
|
143
|
+
nodes.set(meta.key, node)
|
|
144
|
+
return node
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const render = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotation: A) => {
|
|
148
|
+
const meta = annotation.metadata
|
|
149
|
+
const node = nodes.get(meta.key) ?? mount(meta)
|
|
150
|
+
if (!node) return
|
|
151
|
+
node.setMeta(meta)
|
|
152
|
+
return node.host
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const reconcile = <A extends { metadata: LineCommentAnnotationMeta<T> }>(annotations: A[]) => {
|
|
156
|
+
const next = new Set(annotations.map((annotation) => annotation.metadata.key))
|
|
157
|
+
for (const [key, node] of nodes) {
|
|
158
|
+
if (next.has(key)) continue
|
|
159
|
+
node.dispose()
|
|
160
|
+
nodes.delete(key)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const cleanup = () => {
|
|
165
|
+
for (const [, node] of nodes) node.dispose()
|
|
166
|
+
nodes.clear()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { render, reconcile, cleanup }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function lineCommentElement(view: Accessor<CommentProps>) {
|
|
173
|
+
return (
|
|
174
|
+
<Show
|
|
175
|
+
when={view().editor}
|
|
176
|
+
fallback={
|
|
177
|
+
<LineComment
|
|
178
|
+
inline
|
|
179
|
+
id={view().id}
|
|
180
|
+
open={view().open}
|
|
181
|
+
comment={view().comment}
|
|
182
|
+
selection={view().selection}
|
|
183
|
+
actions={view().actions}
|
|
184
|
+
onClick={view().onClick}
|
|
185
|
+
onMouseEnter={view().onMouseEnter}
|
|
186
|
+
/>
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<LineCommentEditor
|
|
190
|
+
inline
|
|
191
|
+
id={view().id}
|
|
192
|
+
value={view().editor!.value}
|
|
193
|
+
selection={view().editor!.selection}
|
|
194
|
+
onInput={view().editor!.onInput}
|
|
195
|
+
onCancel={view().editor!.onCancel}
|
|
196
|
+
onSubmit={view().editor!.onSubmit}
|
|
197
|
+
onPopoverFocusOut={view().editor!.onPopoverFocusOut}
|
|
198
|
+
cancelLabel={view().editor!.cancelLabel}
|
|
199
|
+
submitLabel={view().editor!.submitLabel}
|
|
200
|
+
mention={view().editor!.mention}
|
|
201
|
+
/>
|
|
202
|
+
</Show>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function lineCommentDraftElement(view: Accessor<DraftProps>) {
|
|
207
|
+
return (
|
|
208
|
+
<LineCommentEditor
|
|
209
|
+
inline
|
|
210
|
+
value={view().value}
|
|
211
|
+
selection={view().selection}
|
|
212
|
+
onInput={view().onInput}
|
|
213
|
+
onCancel={view().onCancel}
|
|
214
|
+
onSubmit={view().onSubmit}
|
|
215
|
+
onPopoverFocusOut={view().onPopoverFocusOut}
|
|
216
|
+
mention={view().mention}
|
|
217
|
+
/>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
|
|
222
|
+
const [state, setState] = createStore({
|
|
223
|
+
draft: "",
|
|
224
|
+
editing: null as T | null,
|
|
225
|
+
})
|
|
226
|
+
const draft = () => state.draft
|
|
227
|
+
const setDraft = (value: string) => setState("draft", value)
|
|
228
|
+
const editing = () => state.editing
|
|
229
|
+
const setEditing = (value: T | null) => setState("editing", typeof value === "function" ? () => value : value)
|
|
230
|
+
|
|
231
|
+
const toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null)
|
|
232
|
+
const setSelected = (range: SelectedLineRange | null) => {
|
|
233
|
+
const next = toRange(range)
|
|
234
|
+
props.setSelected(next)
|
|
235
|
+
props.syncSelected?.(toRange(next))
|
|
236
|
+
return next
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const setCommenting = (range: SelectedLineRange | null) => {
|
|
240
|
+
const next = toRange(range)
|
|
241
|
+
props.setCommenting(next)
|
|
242
|
+
return next
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const closeComment = () => {
|
|
246
|
+
props.setOpened(null)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const cancelDraft = () => {
|
|
250
|
+
setDraft("")
|
|
251
|
+
setEditing(null)
|
|
252
|
+
setCommenting(null)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const reset = () => {
|
|
256
|
+
setDraft("")
|
|
257
|
+
setEditing(null)
|
|
258
|
+
props.setOpened(null)
|
|
259
|
+
props.setSelected(null)
|
|
260
|
+
props.setCommenting(null)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const openComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => {
|
|
264
|
+
if (options?.cancelDraft) cancelDraft()
|
|
265
|
+
props.setOpened(id)
|
|
266
|
+
setSelected(range)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const toggleComment = (id: T, range: SelectedLineRange, options?: { cancelDraft?: boolean }) => {
|
|
270
|
+
if (options?.cancelDraft) cancelDraft()
|
|
271
|
+
const next = props.opened() === id ? null : id
|
|
272
|
+
props.setOpened(next)
|
|
273
|
+
setSelected(range)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const openDraft = (range: SelectedLineRange) => {
|
|
277
|
+
const next = toRange(range)
|
|
278
|
+
setDraft("")
|
|
279
|
+
setEditing(null)
|
|
280
|
+
closeComment()
|
|
281
|
+
setSelected(next)
|
|
282
|
+
setCommenting(next)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const openEditor = (id: T, range: SelectedLineRange, value: string) => {
|
|
286
|
+
closeComment()
|
|
287
|
+
setSelected(range)
|
|
288
|
+
props.setCommenting(null)
|
|
289
|
+
setEditing(id)
|
|
290
|
+
setDraft(value)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const hoverComment = (range: SelectedLineRange) => {
|
|
294
|
+
const next = toRange(range)
|
|
295
|
+
if (!next) return
|
|
296
|
+
if (props.hoverSelected) {
|
|
297
|
+
props.hoverSelected(next)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setSelected(next)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
draft,
|
|
306
|
+
setDraft,
|
|
307
|
+
editing,
|
|
308
|
+
opened: props.opened,
|
|
309
|
+
selected: props.selected,
|
|
310
|
+
commenting: props.commenting,
|
|
311
|
+
isOpen: (id: T) => props.opened() === id,
|
|
312
|
+
isEditing: (id: T) => editing() === id,
|
|
313
|
+
closeComment,
|
|
314
|
+
openComment,
|
|
315
|
+
toggleComment,
|
|
316
|
+
openDraft,
|
|
317
|
+
openEditor,
|
|
318
|
+
hoverComment,
|
|
319
|
+
cancelDraft,
|
|
320
|
+
select: setSelected,
|
|
321
|
+
reset,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function createLineCommentController<T extends LineCommentShape>(
|
|
326
|
+
props: LineCommentControllerWithSideProps<T>,
|
|
327
|
+
): {
|
|
328
|
+
note: ReturnType<typeof createLineCommentState<string>>
|
|
329
|
+
annotations: Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
|
|
330
|
+
renderAnnotation: ReturnType<
|
|
331
|
+
typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
|
|
332
|
+
>["renderAnnotation"]
|
|
333
|
+
renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
|
|
334
|
+
onLineSelected: (range: SelectedLineRange | null) => void
|
|
335
|
+
onLineSelectionEnd: (range: SelectedLineRange | null) => void
|
|
336
|
+
}
|
|
337
|
+
export function createLineCommentController<T extends LineCommentShape>(
|
|
338
|
+
props: LineCommentControllerProps<T>,
|
|
339
|
+
): {
|
|
340
|
+
note: ReturnType<typeof createLineCommentState<string>>
|
|
341
|
+
annotations: Accessor<LineCommentAnnotation<T>[]>
|
|
342
|
+
renderAnnotation: ReturnType<
|
|
343
|
+
typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
|
|
344
|
+
>["renderAnnotation"]
|
|
345
|
+
renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
|
|
346
|
+
onLineSelected: (range: SelectedLineRange | null) => void
|
|
347
|
+
onLineSelectionEnd: (range: SelectedLineRange | null) => void
|
|
348
|
+
}
|
|
349
|
+
export function createLineCommentController<T extends LineCommentShape>(
|
|
350
|
+
props: LineCommentControllerProps<T> | LineCommentControllerWithSideProps<T>,
|
|
351
|
+
) {
|
|
352
|
+
const i18n = useI18n()
|
|
353
|
+
const note = createLineCommentState<string>(props.state)
|
|
354
|
+
|
|
355
|
+
const annotations =
|
|
356
|
+
"getSide" in props
|
|
357
|
+
? createLineCommentAnnotations({
|
|
358
|
+
comments: props.comments,
|
|
359
|
+
getCommentId: (comment) => comment.id,
|
|
360
|
+
getCommentSelection: (comment) => comment.selection,
|
|
361
|
+
draftRange: note.commenting,
|
|
362
|
+
draftKey: props.draftKey,
|
|
363
|
+
getSide: props.getSide,
|
|
364
|
+
})
|
|
365
|
+
: createLineCommentAnnotations({
|
|
366
|
+
comments: props.comments,
|
|
367
|
+
getCommentId: (comment) => comment.id,
|
|
368
|
+
getCommentSelection: (comment) => comment.selection,
|
|
369
|
+
draftRange: note.commenting,
|
|
370
|
+
draftKey: props.draftKey,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>({
|
|
374
|
+
annotations,
|
|
375
|
+
commentElement: lineCommentElement,
|
|
376
|
+
draftElement: lineCommentDraftElement,
|
|
377
|
+
renderComment: (comment) => {
|
|
378
|
+
const edit = () => note.openEditor(comment.id, comment.selection, comment.comment)
|
|
379
|
+
const remove = () => {
|
|
380
|
+
note.reset()
|
|
381
|
+
props.onDelete?.(comment)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
id: comment.id,
|
|
386
|
+
get open() {
|
|
387
|
+
return note.isOpen(comment.id) || note.isEditing(comment.id)
|
|
388
|
+
},
|
|
389
|
+
comment: comment.comment,
|
|
390
|
+
selection: formatSelectedLineLabel(comment.selection, i18n.t),
|
|
391
|
+
get actions() {
|
|
392
|
+
return props.renderCommentActions?.(comment, { edit, remove })
|
|
393
|
+
},
|
|
394
|
+
get editor() {
|
|
395
|
+
return note.isEditing(comment.id)
|
|
396
|
+
? {
|
|
397
|
+
get value() {
|
|
398
|
+
return note.draft()
|
|
399
|
+
},
|
|
400
|
+
selection: formatSelectedLineLabel(comment.selection, i18n.t),
|
|
401
|
+
mention: props.mention,
|
|
402
|
+
onInput: note.setDraft,
|
|
403
|
+
onCancel: note.cancelDraft,
|
|
404
|
+
onSubmit: (value: string) => {
|
|
405
|
+
props.onUpdate?.({
|
|
406
|
+
id: comment.id,
|
|
407
|
+
comment: value,
|
|
408
|
+
selection: cloneSelectedLineRange(comment.selection),
|
|
409
|
+
})
|
|
410
|
+
note.cancelDraft()
|
|
411
|
+
},
|
|
412
|
+
submitLabel: props.editSubmitLabel,
|
|
413
|
+
}
|
|
414
|
+
: undefined
|
|
415
|
+
},
|
|
416
|
+
onMouseEnter: () => note.hoverComment(comment.selection),
|
|
417
|
+
onClick: () => {
|
|
418
|
+
if (note.isEditing(comment.id)) return
|
|
419
|
+
note.toggleComment(comment.id, comment.selection, { cancelDraft: props.cancelDraftOnCommentToggle })
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
renderDraft: (range) => ({
|
|
424
|
+
get value() {
|
|
425
|
+
return note.draft()
|
|
426
|
+
},
|
|
427
|
+
selection: formatSelectedLineLabel(range, i18n.t),
|
|
428
|
+
mention: props.mention,
|
|
429
|
+
onInput: note.setDraft,
|
|
430
|
+
onCancel: () => {
|
|
431
|
+
note.cancelDraft()
|
|
432
|
+
note.select(null)
|
|
433
|
+
},
|
|
434
|
+
onSubmit: (comment) => {
|
|
435
|
+
props.onSubmit({ comment, selection: cloneSelectedLineRange(range) })
|
|
436
|
+
note.cancelDraft()
|
|
437
|
+
},
|
|
438
|
+
onPopoverFocusOut: props.onDraftPopoverFocusOut,
|
|
439
|
+
}),
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
const renderGutterUtility = createLineCommentGutterRenderer({
|
|
443
|
+
label: props.label,
|
|
444
|
+
getSelectedRange: () => {
|
|
445
|
+
if (note.opened()) return null
|
|
446
|
+
return props.getHoverSelectedRange?.() ?? note.selected()
|
|
447
|
+
},
|
|
448
|
+
onOpenDraft: note.openDraft,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const onLineSelected = (range: SelectedLineRange | null) => {
|
|
452
|
+
if (!range) {
|
|
453
|
+
note.select(null)
|
|
454
|
+
note.cancelDraft()
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
note.select(range)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const onLineSelectionEnd = (range: SelectedLineRange | null) => {
|
|
462
|
+
if (!range) {
|
|
463
|
+
if (props.clearSelectionOnSelectionEndNull) note.select(null)
|
|
464
|
+
note.cancelDraft()
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
note.openDraft(range)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
note,
|
|
473
|
+
annotations,
|
|
474
|
+
renderAnnotation,
|
|
475
|
+
renderGutterUtility,
|
|
476
|
+
onLineSelected,
|
|
477
|
+
onLineSelectionEnd,
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function createLineCommentAnnotations<T>(
|
|
482
|
+
props: LineCommentAnnotationsWithSideProps<T>,
|
|
483
|
+
): Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
|
|
484
|
+
export function createLineCommentAnnotations<T>(
|
|
485
|
+
props: LineCommentAnnotationsProps<T>,
|
|
486
|
+
): Accessor<LineCommentAnnotation<T>[]>
|
|
487
|
+
export function createLineCommentAnnotations<T>(
|
|
488
|
+
props: LineCommentAnnotationsProps<T> | LineCommentAnnotationsWithSideProps<T>,
|
|
489
|
+
) {
|
|
490
|
+
const line = (range: SelectedLineRange) => Math.max(range.start, range.end)
|
|
491
|
+
|
|
492
|
+
if ("getSide" in props) {
|
|
493
|
+
return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(
|
|
494
|
+
() => {
|
|
495
|
+
const list = props.comments().map((comment) => {
|
|
496
|
+
const range = props.getCommentSelection(comment)
|
|
497
|
+
return {
|
|
498
|
+
side: props.getSide(range),
|
|
499
|
+
lineNumber: line(range),
|
|
500
|
+
metadata: {
|
|
501
|
+
kind: "comment",
|
|
502
|
+
key: `comment:${props.getCommentId(comment)}`,
|
|
503
|
+
comment,
|
|
504
|
+
} satisfies LineCommentAnnotationMeta<T>,
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const range = props.draftRange()
|
|
509
|
+
if (!range) return list
|
|
510
|
+
|
|
511
|
+
return [
|
|
512
|
+
...list,
|
|
513
|
+
{
|
|
514
|
+
side: props.getSide(range),
|
|
515
|
+
lineNumber: line(range),
|
|
516
|
+
metadata: {
|
|
517
|
+
kind: "draft",
|
|
518
|
+
key: `draft:${props.draftKey()}`,
|
|
519
|
+
range,
|
|
520
|
+
} satisfies LineCommentAnnotationMeta<T>,
|
|
521
|
+
},
|
|
522
|
+
]
|
|
523
|
+
},
|
|
524
|
+
[],
|
|
525
|
+
// Stable identity for unchanged annotations avoids no-op diff rerenders downstream.
|
|
526
|
+
{ equals: sameAnnotationLists },
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return createMemo<LineCommentAnnotation<T>[]>(
|
|
531
|
+
() => {
|
|
532
|
+
const list = props.comments().map((comment) => {
|
|
533
|
+
const range = props.getCommentSelection(comment)
|
|
534
|
+
const entry: LineCommentAnnotation<T> = {
|
|
535
|
+
lineNumber: line(range),
|
|
536
|
+
metadata: {
|
|
537
|
+
kind: "comment",
|
|
538
|
+
key: `comment:${props.getCommentId(comment)}`,
|
|
539
|
+
comment,
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return entry
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
const range = props.draftRange()
|
|
547
|
+
if (!range) return list
|
|
548
|
+
|
|
549
|
+
const draft: LineCommentAnnotation<T> = {
|
|
550
|
+
lineNumber: line(range),
|
|
551
|
+
metadata: {
|
|
552
|
+
kind: "draft",
|
|
553
|
+
key: `draft:${props.draftKey()}`,
|
|
554
|
+
range,
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return [...list, draft]
|
|
559
|
+
},
|
|
560
|
+
[],
|
|
561
|
+
{ equals: sameAnnotationLists },
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
type AnnotationListItem = {
|
|
566
|
+
lineNumber: number
|
|
567
|
+
side?: unknown
|
|
568
|
+
metadata: { kind: string; key: string; comment?: unknown; range?: unknown }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function sameAnnotationLists(previous: AnnotationListItem[], next: AnnotationListItem[]) {
|
|
572
|
+
if (previous.length !== next.length) return false
|
|
573
|
+
return previous.every((item, index) => {
|
|
574
|
+
const other = next[index]!
|
|
575
|
+
return (
|
|
576
|
+
item.lineNumber === other.lineNumber &&
|
|
577
|
+
item.side === other.side &&
|
|
578
|
+
item.metadata.kind === other.metadata.kind &&
|
|
579
|
+
item.metadata.key === other.metadata.key &&
|
|
580
|
+
item.metadata.comment === other.metadata.comment &&
|
|
581
|
+
item.metadata.range === other.metadata.range
|
|
582
|
+
)
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function createManagedLineCommentAnnotationRenderer<T, C, D>(props: {
|
|
587
|
+
annotations: Accessor<LineCommentAnnotation<T>[]>
|
|
588
|
+
renderComment: (comment: T) => C
|
|
589
|
+
renderDraft: (range: SelectedLineRange) => D
|
|
590
|
+
commentElement: (view: Accessor<C>) => JSX.Element
|
|
591
|
+
draftElement: (view: Accessor<D>) => JSX.Element
|
|
592
|
+
}) {
|
|
593
|
+
const renderer = createLineCommentAnnotationRenderer<T, C, D>({
|
|
594
|
+
renderComment: props.renderComment,
|
|
595
|
+
renderDraft: props.renderDraft,
|
|
596
|
+
commentElement: props.commentElement,
|
|
597
|
+
draftElement: props.draftElement,
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
createEffect(() => {
|
|
601
|
+
renderer.reconcile(props.annotations())
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
onCleanup(() => {
|
|
605
|
+
renderer.cleanup()
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
renderAnnotation: renderer.render,
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function createLineCommentGutterRenderer(props: {
|
|
614
|
+
label: string
|
|
615
|
+
getSelectedRange: Accessor<SelectedLineRange | null>
|
|
616
|
+
onOpenDraft: (range: SelectedLineRange) => void
|
|
617
|
+
}) {
|
|
618
|
+
return (getHoveredLine: () => HoverCommentLine | undefined) =>
|
|
619
|
+
createHoverCommentUtility({
|
|
620
|
+
label: props.label,
|
|
621
|
+
getHoveredLine,
|
|
622
|
+
onSelect: (hovered) => {
|
|
623
|
+
const current = props.getSelectedRange()
|
|
624
|
+
if (current && lineInSelectedRange(current, hovered.lineNumber, hovered.side)) {
|
|
625
|
+
props.onOpenDraft(cloneSelectedLineRange(current))
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const range: SelectedLineRange = {
|
|
630
|
+
start: hovered.lineNumber,
|
|
631
|
+
end: hovered.lineNumber,
|
|
632
|
+
}
|
|
633
|
+
if (hovered.side) range.side = hovered.side
|
|
634
|
+
props.onOpenDraft(range)
|
|
635
|
+
},
|
|
636
|
+
})
|
|
637
|
+
}
|