@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,485 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
|
|
2
|
+
import { makeEventListener } from "@solid-primitives/event-listener"
|
|
3
|
+
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
4
|
+
import { createStore } from "solid-js/store"
|
|
5
|
+
|
|
6
|
+
export type FindHost = {
|
|
7
|
+
element: () => HTMLElement | undefined
|
|
8
|
+
open: () => void
|
|
9
|
+
close: () => void
|
|
10
|
+
next: (dir: 1 | -1) => void
|
|
11
|
+
isOpen: () => boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hosts = new Set<FindHost>()
|
|
15
|
+
let target: FindHost | undefined
|
|
16
|
+
let current: FindHost | undefined
|
|
17
|
+
let installed = false
|
|
18
|
+
|
|
19
|
+
function isEditable(node: unknown): boolean {
|
|
20
|
+
if (!(node instanceof HTMLElement)) return false
|
|
21
|
+
if (node.closest("[data-prevent-autofocus]")) return true
|
|
22
|
+
if (node.isContentEditable) return true
|
|
23
|
+
return /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(node.tagName)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hostForNode(node: unknown) {
|
|
27
|
+
if (!(node instanceof Node)) return
|
|
28
|
+
for (const host of hosts) {
|
|
29
|
+
const el = host.element()
|
|
30
|
+
if (el && el.isConnected && el.contains(node)) return host
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function installShortcuts() {
|
|
35
|
+
if (installed) return
|
|
36
|
+
if (typeof window === "undefined") return
|
|
37
|
+
installed = true
|
|
38
|
+
|
|
39
|
+
window.addEventListener(
|
|
40
|
+
"keydown",
|
|
41
|
+
(event) => {
|
|
42
|
+
if (event.defaultPrevented) return
|
|
43
|
+
if (isEditable(event.target)) return
|
|
44
|
+
|
|
45
|
+
const mod = event.metaKey || event.ctrlKey
|
|
46
|
+
if (!mod) return
|
|
47
|
+
|
|
48
|
+
const key = event.key.toLowerCase()
|
|
49
|
+
if (key === "g") {
|
|
50
|
+
const host = current
|
|
51
|
+
if (!host || !host.isOpen()) return
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
event.stopPropagation()
|
|
54
|
+
host.next(event.shiftKey ? -1 : 1)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (key !== "f") return
|
|
59
|
+
|
|
60
|
+
const active = current
|
|
61
|
+
if (active && active.isOpen()) {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
event.stopPropagation()
|
|
64
|
+
active.open()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const host = hostForNode(document.activeElement) ?? hostForNode(event.target) ?? target ?? Array.from(hosts)[0]
|
|
69
|
+
if (!host) return
|
|
70
|
+
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
event.stopPropagation()
|
|
73
|
+
host.open()
|
|
74
|
+
},
|
|
75
|
+
{ capture: true },
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clearHighlightFind() {
|
|
80
|
+
const api = (globalThis as { CSS?: { highlights?: { delete: (name: string) => void } } }).CSS?.highlights
|
|
81
|
+
if (!api) return
|
|
82
|
+
api.delete("opencode-find")
|
|
83
|
+
api.delete("opencode-find-current")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function supportsHighlights() {
|
|
87
|
+
const g = globalThis as unknown as { CSS?: { highlights?: unknown }; Highlight?: unknown }
|
|
88
|
+
return typeof g.Highlight === "function" && g.CSS?.highlights != null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scrollParent(el: HTMLElement): HTMLElement | undefined {
|
|
92
|
+
let parent = el.parentElement
|
|
93
|
+
while (parent) {
|
|
94
|
+
const style = getComputedStyle(parent)
|
|
95
|
+
if (style.overflowY === "auto" || style.overflowY === "scroll") return parent
|
|
96
|
+
parent = parent.parentElement
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type CreateFileFindOptions = {
|
|
101
|
+
wrapper: () => HTMLElement | undefined
|
|
102
|
+
overlay: () => HTMLDivElement | undefined
|
|
103
|
+
getRoot: () => ShadowRoot | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createFileFind(opts: CreateFileFindOptions) {
|
|
107
|
+
let input: HTMLInputElement | undefined
|
|
108
|
+
let overlayFrame: number | undefined
|
|
109
|
+
let mode: "highlights" | "overlay" = "overlay"
|
|
110
|
+
let hits: Range[] = []
|
|
111
|
+
const [overlayScroll, setOverlayScroll] = createSignal<HTMLElement[]>([])
|
|
112
|
+
|
|
113
|
+
const [state, setState] = createStore({
|
|
114
|
+
open: false,
|
|
115
|
+
query: "",
|
|
116
|
+
index: 0,
|
|
117
|
+
count: 0,
|
|
118
|
+
pos: { top: 8, right: 8 },
|
|
119
|
+
})
|
|
120
|
+
const open = () => state.open
|
|
121
|
+
const query = () => state.query
|
|
122
|
+
const index = () => state.index
|
|
123
|
+
const count = () => state.count
|
|
124
|
+
const pos = () => state.pos
|
|
125
|
+
|
|
126
|
+
const clearOverlayScroll = () => {
|
|
127
|
+
setOverlayScroll([])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const clearOverlay = () => {
|
|
131
|
+
const el = opts.overlay()
|
|
132
|
+
if (!el) return
|
|
133
|
+
if (overlayFrame !== undefined) {
|
|
134
|
+
cancelAnimationFrame(overlayFrame)
|
|
135
|
+
overlayFrame = undefined
|
|
136
|
+
}
|
|
137
|
+
el.innerHTML = ""
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const renderOverlay = () => {
|
|
141
|
+
if (mode !== "overlay") {
|
|
142
|
+
clearOverlay()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const wrapper = opts.wrapper()
|
|
147
|
+
const overlay = opts.overlay()
|
|
148
|
+
if (!wrapper || !overlay) return
|
|
149
|
+
|
|
150
|
+
clearOverlay()
|
|
151
|
+
if (hits.length === 0) return
|
|
152
|
+
|
|
153
|
+
const base = wrapper.getBoundingClientRect()
|
|
154
|
+
const currentIndex = index()
|
|
155
|
+
const frag = document.createDocumentFragment()
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < hits.length; i++) {
|
|
158
|
+
const range = hits[i]
|
|
159
|
+
const active = i === currentIndex
|
|
160
|
+
for (const rect of Array.from(range.getClientRects())) {
|
|
161
|
+
if (!rect.width || !rect.height) continue
|
|
162
|
+
|
|
163
|
+
const mark = document.createElement("div")
|
|
164
|
+
mark.style.position = "absolute"
|
|
165
|
+
mark.style.left = `${Math.round(rect.left - base.left)}px`
|
|
166
|
+
mark.style.top = `${Math.round(rect.top - base.top)}px`
|
|
167
|
+
mark.style.width = `${Math.round(rect.width)}px`
|
|
168
|
+
mark.style.height = `${Math.round(rect.height)}px`
|
|
169
|
+
mark.style.borderRadius = "2px"
|
|
170
|
+
mark.style.backgroundColor = active ? "var(--surface-warning-strong)" : "var(--surface-warning-base)"
|
|
171
|
+
mark.style.opacity = active ? "0.55" : "0.35"
|
|
172
|
+
if (active) mark.style.boxShadow = "inset 0 0 0 1px var(--border-warning-base)"
|
|
173
|
+
frag.appendChild(mark)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
overlay.appendChild(frag)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scheduleOverlay() {
|
|
181
|
+
if (mode !== "overlay") return
|
|
182
|
+
if (!open()) return
|
|
183
|
+
if (overlayFrame !== undefined) return
|
|
184
|
+
|
|
185
|
+
overlayFrame = requestAnimationFrame(() => {
|
|
186
|
+
overlayFrame = undefined
|
|
187
|
+
renderOverlay()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const syncOverlayScroll = () => {
|
|
192
|
+
if (mode !== "overlay") return
|
|
193
|
+
const root = opts.getRoot()
|
|
194
|
+
|
|
195
|
+
const next = root
|
|
196
|
+
? Array.from(root.querySelectorAll("[data-code]")).filter(
|
|
197
|
+
(node): node is HTMLElement => node instanceof HTMLElement,
|
|
198
|
+
)
|
|
199
|
+
: []
|
|
200
|
+
const current = overlayScroll()
|
|
201
|
+
if (next.length === current.length && next.every((el, i) => el === current[i])) return
|
|
202
|
+
|
|
203
|
+
clearOverlayScroll()
|
|
204
|
+
setOverlayScroll(next)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const clearFind = () => {
|
|
208
|
+
clearHighlightFind()
|
|
209
|
+
clearOverlay()
|
|
210
|
+
clearOverlayScroll()
|
|
211
|
+
hits = []
|
|
212
|
+
setState("count", 0)
|
|
213
|
+
setState("index", 0)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const positionBar = () => {
|
|
217
|
+
if (typeof window === "undefined") return
|
|
218
|
+
const wrapper = opts.wrapper()
|
|
219
|
+
if (!wrapper) return
|
|
220
|
+
|
|
221
|
+
const root = scrollParent(wrapper) ?? wrapper
|
|
222
|
+
const rect = root.getBoundingClientRect()
|
|
223
|
+
const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
|
|
224
|
+
const header = Number.isNaN(title) ? 0 : title
|
|
225
|
+
|
|
226
|
+
setState("pos", {
|
|
227
|
+
top: Math.round(rect.top) + header - 4,
|
|
228
|
+
right: Math.round(window.innerWidth - rect.right) + 8,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const scan = (root: ShadowRoot, value: string) => {
|
|
233
|
+
const needle = value.toLowerCase()
|
|
234
|
+
const ranges: Range[] = []
|
|
235
|
+
const cols = Array.from(root.querySelectorAll("[data-content] [data-line], [data-column-content]")).filter(
|
|
236
|
+
(node): node is HTMLElement => node instanceof HTMLElement,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
for (const col of cols) {
|
|
240
|
+
const text = col.textContent
|
|
241
|
+
if (!text) continue
|
|
242
|
+
|
|
243
|
+
const hay = text.toLowerCase()
|
|
244
|
+
let at = hay.indexOf(needle)
|
|
245
|
+
if (at === -1) continue
|
|
246
|
+
|
|
247
|
+
const nodes: Text[] = []
|
|
248
|
+
const ends: number[] = []
|
|
249
|
+
const walker = document.createTreeWalker(col, NodeFilter.SHOW_TEXT)
|
|
250
|
+
let node = walker.nextNode()
|
|
251
|
+
let pos = 0
|
|
252
|
+
while (node) {
|
|
253
|
+
if (node instanceof Text) {
|
|
254
|
+
pos += node.data.length
|
|
255
|
+
nodes.push(node)
|
|
256
|
+
ends.push(pos)
|
|
257
|
+
}
|
|
258
|
+
node = walker.nextNode()
|
|
259
|
+
}
|
|
260
|
+
if (nodes.length === 0) continue
|
|
261
|
+
|
|
262
|
+
const locate = (offset: number) => {
|
|
263
|
+
let lo = 0
|
|
264
|
+
let hi = ends.length - 1
|
|
265
|
+
while (lo < hi) {
|
|
266
|
+
const mid = (lo + hi) >> 1
|
|
267
|
+
if (ends[mid] >= offset) hi = mid
|
|
268
|
+
else lo = mid + 1
|
|
269
|
+
}
|
|
270
|
+
const prev = lo === 0 ? 0 : ends[lo - 1]
|
|
271
|
+
return { node: nodes[lo], offset: offset - prev }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
while (at !== -1) {
|
|
275
|
+
const start = locate(at)
|
|
276
|
+
const end = locate(at + value.length)
|
|
277
|
+
const range = document.createRange()
|
|
278
|
+
range.setStart(start.node, start.offset)
|
|
279
|
+
range.setEnd(end.node, end.offset)
|
|
280
|
+
ranges.push(range)
|
|
281
|
+
at = hay.indexOf(needle, at + value.length)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return ranges
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const scrollToRange = (range: Range) => {
|
|
289
|
+
const start = range.startContainer
|
|
290
|
+
const el = start instanceof Element ? start : start.parentElement
|
|
291
|
+
el?.scrollIntoView({ block: "center", inline: "center" })
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const setHighlights = (ranges: Range[], currentIndex: number) => {
|
|
295
|
+
const api = (globalThis as unknown as { CSS?: { highlights?: any }; Highlight?: any }).CSS?.highlights
|
|
296
|
+
const Highlight = (globalThis as unknown as { Highlight?: any }).Highlight
|
|
297
|
+
if (!api || typeof Highlight !== "function") return false
|
|
298
|
+
|
|
299
|
+
api.delete("opencode-find")
|
|
300
|
+
api.delete("opencode-find-current")
|
|
301
|
+
|
|
302
|
+
const active = ranges[currentIndex]
|
|
303
|
+
if (active) api.set("opencode-find-current", new Highlight(active))
|
|
304
|
+
|
|
305
|
+
const rest = ranges.filter((_, i) => i !== currentIndex)
|
|
306
|
+
if (rest.length > 0) api.set("opencode-find", new Highlight(...rest))
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const apply = (args?: { reset?: boolean; scroll?: boolean }) => {
|
|
311
|
+
if (!open()) return
|
|
312
|
+
|
|
313
|
+
const value = query().trim()
|
|
314
|
+
if (!value) {
|
|
315
|
+
clearFind()
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const root = opts.getRoot()
|
|
320
|
+
if (!root) return
|
|
321
|
+
|
|
322
|
+
mode = supportsHighlights() ? "highlights" : "overlay"
|
|
323
|
+
|
|
324
|
+
const ranges = scan(root, value)
|
|
325
|
+
const total = ranges.length
|
|
326
|
+
const desired = args?.reset ? 0 : index()
|
|
327
|
+
const currentIndex = total ? Math.min(desired, total - 1) : 0
|
|
328
|
+
|
|
329
|
+
hits = ranges
|
|
330
|
+
setState("count", total)
|
|
331
|
+
setState("index", currentIndex)
|
|
332
|
+
|
|
333
|
+
const active = ranges[currentIndex]
|
|
334
|
+
if (mode === "highlights") {
|
|
335
|
+
clearOverlay()
|
|
336
|
+
clearOverlayScroll()
|
|
337
|
+
if (!setHighlights(ranges, currentIndex)) {
|
|
338
|
+
mode = "overlay"
|
|
339
|
+
clearHighlightFind()
|
|
340
|
+
syncOverlayScroll()
|
|
341
|
+
scheduleOverlay()
|
|
342
|
+
}
|
|
343
|
+
if (args?.scroll && active) scrollToRange(active)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
clearHighlightFind()
|
|
348
|
+
syncOverlayScroll()
|
|
349
|
+
if (args?.scroll && active) scrollToRange(active)
|
|
350
|
+
scheduleOverlay()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const close = () => {
|
|
354
|
+
setState("open", false)
|
|
355
|
+
setState("query", "")
|
|
356
|
+
clearFind()
|
|
357
|
+
if (current === host) current = undefined
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const focus = () => {
|
|
361
|
+
if (current && current !== host) current.close()
|
|
362
|
+
current = host
|
|
363
|
+
target = host
|
|
364
|
+
if (!open()) setState("open", true)
|
|
365
|
+
requestAnimationFrame(() => {
|
|
366
|
+
apply({ scroll: true })
|
|
367
|
+
input?.focus()
|
|
368
|
+
input?.select()
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const next = (dir: 1 | -1) => {
|
|
373
|
+
if (!open()) return
|
|
374
|
+
const total = count()
|
|
375
|
+
if (total <= 0) return
|
|
376
|
+
|
|
377
|
+
const currentIndex = (index() + dir + total) % total
|
|
378
|
+
setState("index", currentIndex)
|
|
379
|
+
|
|
380
|
+
const active = hits[currentIndex]
|
|
381
|
+
if (!active) return
|
|
382
|
+
|
|
383
|
+
if (mode === "highlights") {
|
|
384
|
+
if (!setHighlights(hits, currentIndex)) {
|
|
385
|
+
mode = "overlay"
|
|
386
|
+
apply({ reset: true, scroll: true })
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
scrollToRange(active)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
clearHighlightFind()
|
|
394
|
+
syncOverlayScroll()
|
|
395
|
+
scrollToRange(active)
|
|
396
|
+
scheduleOverlay()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const host: FindHost = {
|
|
400
|
+
element: opts.wrapper,
|
|
401
|
+
isOpen: () => open(),
|
|
402
|
+
next,
|
|
403
|
+
open: focus,
|
|
404
|
+
close,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
createEffect(() => {
|
|
408
|
+
for (const el of overlayScroll()) makeEventListener(el, "scroll", scheduleOverlay, { passive: true })
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
onMount(() => {
|
|
412
|
+
mode = supportsHighlights() ? "highlights" : "overlay"
|
|
413
|
+
installShortcuts()
|
|
414
|
+
hosts.add(host)
|
|
415
|
+
if (!target) target = host
|
|
416
|
+
|
|
417
|
+
onCleanup(() => {
|
|
418
|
+
hosts.delete(host)
|
|
419
|
+
if (current === host) {
|
|
420
|
+
current = undefined
|
|
421
|
+
clearHighlightFind()
|
|
422
|
+
}
|
|
423
|
+
if (target === host) target = undefined
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
createEffect(() => {
|
|
428
|
+
if (!open()) return
|
|
429
|
+
|
|
430
|
+
const update = () => positionBar()
|
|
431
|
+
requestAnimationFrame(update)
|
|
432
|
+
makeEventListener(window, "resize", update, { passive: true })
|
|
433
|
+
|
|
434
|
+
const wrapper = opts.wrapper()
|
|
435
|
+
if (!wrapper) return
|
|
436
|
+
const root = scrollParent(wrapper) ?? wrapper
|
|
437
|
+
createResizeObserver(root, update)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
onCleanup(() => {
|
|
441
|
+
clearOverlayScroll()
|
|
442
|
+
clearOverlay()
|
|
443
|
+
if (current === host) {
|
|
444
|
+
current = undefined
|
|
445
|
+
clearHighlightFind()
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
open,
|
|
451
|
+
query,
|
|
452
|
+
count,
|
|
453
|
+
index,
|
|
454
|
+
pos,
|
|
455
|
+
setInput: (el: HTMLInputElement) => {
|
|
456
|
+
input = el
|
|
457
|
+
},
|
|
458
|
+
setQuery: (value: string) => {
|
|
459
|
+
setState("query", value)
|
|
460
|
+
setState("index", 0)
|
|
461
|
+
apply({ reset: true, scroll: true })
|
|
462
|
+
},
|
|
463
|
+
focus,
|
|
464
|
+
close,
|
|
465
|
+
next,
|
|
466
|
+
refresh: (args?: { reset?: boolean; scroll?: boolean }) => apply(args),
|
|
467
|
+
onPointerDown: () => {
|
|
468
|
+
target = host
|
|
469
|
+
opts.wrapper()?.focus({ preventScroll: true })
|
|
470
|
+
},
|
|
471
|
+
onFocus: () => {
|
|
472
|
+
target = host
|
|
473
|
+
},
|
|
474
|
+
onInputKeyDown: (event: KeyboardEvent) => {
|
|
475
|
+
if (event.key === "Escape") {
|
|
476
|
+
event.preventDefault()
|
|
477
|
+
close()
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
if (event.key !== "Enter") return
|
|
481
|
+
event.preventDefault()
|
|
482
|
+
next(event.shiftKey ? -1 : 1)
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
type ReadyWatcher = {
|
|
2
|
+
observer?: MutationObserver
|
|
3
|
+
token: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createReadyWatcher(): ReadyWatcher {
|
|
7
|
+
return { token: 0 }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function clearReadyWatcher(state: ReadyWatcher) {
|
|
11
|
+
state.observer?.disconnect()
|
|
12
|
+
state.observer = undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getViewerHost(container: HTMLElement | undefined) {
|
|
16
|
+
if (!container) return
|
|
17
|
+
const host = container.querySelector("diffs-container")
|
|
18
|
+
if (!(host instanceof HTMLElement)) return
|
|
19
|
+
return host
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getViewerRoot(container: HTMLElement | undefined) {
|
|
23
|
+
return getViewerHost(container)?.shadowRoot ?? undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyViewerScheme(host: HTMLElement | undefined) {
|
|
27
|
+
if (!host) return
|
|
28
|
+
if (typeof document === "undefined") return
|
|
29
|
+
|
|
30
|
+
const scheme = document.documentElement.dataset.colorScheme
|
|
31
|
+
if (scheme === "dark" || scheme === "light") {
|
|
32
|
+
host.dataset.colorScheme = scheme
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
host.removeAttribute("data-color-scheme")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function observeViewerScheme(getHost: () => HTMLElement | undefined) {
|
|
40
|
+
if (typeof document === "undefined") return () => {}
|
|
41
|
+
|
|
42
|
+
applyViewerScheme(getHost())
|
|
43
|
+
if (typeof MutationObserver === "undefined") return () => {}
|
|
44
|
+
|
|
45
|
+
const root = document.documentElement
|
|
46
|
+
const monitor = new MutationObserver(() => applyViewerScheme(getHost()))
|
|
47
|
+
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
|
|
48
|
+
return () => monitor.disconnect()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function notifyShadowReady(opts: {
|
|
52
|
+
state: ReadyWatcher
|
|
53
|
+
container: HTMLElement
|
|
54
|
+
getRoot: () => ShadowRoot | undefined
|
|
55
|
+
isReady: (root: ShadowRoot) => boolean
|
|
56
|
+
onReady: () => void
|
|
57
|
+
settleFrames?: number
|
|
58
|
+
}) {
|
|
59
|
+
clearReadyWatcher(opts.state)
|
|
60
|
+
opts.state.token += 1
|
|
61
|
+
|
|
62
|
+
const token = opts.state.token
|
|
63
|
+
const settle = Math.max(0, opts.settleFrames ?? 0)
|
|
64
|
+
|
|
65
|
+
const runReady = () => {
|
|
66
|
+
const step = (left: number) => {
|
|
67
|
+
if (token !== opts.state.token) return
|
|
68
|
+
if (left <= 0) {
|
|
69
|
+
opts.onReady()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
requestAnimationFrame(() => step(left - 1))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
requestAnimationFrame(() => step(settle))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const observeRoot = (root: ShadowRoot) => {
|
|
79
|
+
if (opts.isReady(root)) {
|
|
80
|
+
runReady()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof MutationObserver === "undefined") return
|
|
85
|
+
|
|
86
|
+
clearReadyWatcher(opts.state)
|
|
87
|
+
opts.state.observer = new MutationObserver(() => {
|
|
88
|
+
if (token !== opts.state.token) return
|
|
89
|
+
if (!opts.isReady(root)) return
|
|
90
|
+
|
|
91
|
+
clearReadyWatcher(opts.state)
|
|
92
|
+
runReady()
|
|
93
|
+
})
|
|
94
|
+
opts.state.observer.observe(root, { childList: true, subtree: true })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const root = opts.getRoot()
|
|
98
|
+
if (!root) {
|
|
99
|
+
if (typeof MutationObserver === "undefined") return
|
|
100
|
+
|
|
101
|
+
opts.state.observer = new MutationObserver(() => {
|
|
102
|
+
if (token !== opts.state.token) return
|
|
103
|
+
|
|
104
|
+
const next = opts.getRoot()
|
|
105
|
+
if (!next) return
|
|
106
|
+
|
|
107
|
+
observeRoot(next)
|
|
108
|
+
})
|
|
109
|
+
opts.state.observer.observe(opts.container, { childList: true, subtree: true })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
observeRoot(root)
|
|
114
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type SelectedLineRange } from "@pierre/diffs"
|
|
2
|
+
import { toRange } from "./selection-bridge"
|
|
3
|
+
|
|
4
|
+
export function findElement(node: Node | null): HTMLElement | undefined {
|
|
5
|
+
if (!node) return
|
|
6
|
+
if (node instanceof HTMLElement) return node
|
|
7
|
+
return node.parentElement ?? undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function findFileLineNumber(node: Node | null): number | undefined {
|
|
11
|
+
const el = findElement(node)
|
|
12
|
+
if (!el) return
|
|
13
|
+
|
|
14
|
+
const line = el.closest("[data-line]")
|
|
15
|
+
if (!(line instanceof HTMLElement)) return
|
|
16
|
+
|
|
17
|
+
const value = parseInt(line.dataset.line ?? "", 10)
|
|
18
|
+
if (Number.isNaN(value)) return
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function findDiffLineNumber(node: Node | null): number | undefined {
|
|
23
|
+
const el = findElement(node)
|
|
24
|
+
if (!el) return
|
|
25
|
+
|
|
26
|
+
const line = el.closest("[data-line], [data-alt-line]")
|
|
27
|
+
if (!(line instanceof HTMLElement)) return
|
|
28
|
+
|
|
29
|
+
const primary = parseInt(line.dataset.line ?? "", 10)
|
|
30
|
+
if (!Number.isNaN(primary)) return primary
|
|
31
|
+
|
|
32
|
+
const alt = parseInt(line.dataset.altLine ?? "", 10)
|
|
33
|
+
if (!Number.isNaN(alt)) return alt
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function findCodeSelectionSide(node: Node | null): SelectedLineRange["side"] {
|
|
37
|
+
const el = findElement(node)
|
|
38
|
+
if (!el) return
|
|
39
|
+
|
|
40
|
+
const code = el.closest("[data-code]")
|
|
41
|
+
if (!(code instanceof HTMLElement)) return
|
|
42
|
+
if (code.hasAttribute("data-deletions")) return "deletions"
|
|
43
|
+
return "additions"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readShadowLineSelection(opts: {
|
|
47
|
+
root: ShadowRoot
|
|
48
|
+
lineForNode: (node: Node | null) => number | undefined
|
|
49
|
+
sideForNode?: (node: Node | null) => SelectedLineRange["side"]
|
|
50
|
+
preserveTextSelection?: boolean
|
|
51
|
+
}) {
|
|
52
|
+
const selection =
|
|
53
|
+
(opts.root as unknown as { getSelection?: () => Selection | null }).getSelection?.() ?? window.getSelection()
|
|
54
|
+
if (!selection || selection.isCollapsed) return
|
|
55
|
+
|
|
56
|
+
const domRange =
|
|
57
|
+
(
|
|
58
|
+
selection as unknown as {
|
|
59
|
+
getComposedRanges?: (options?: { shadowRoots?: ShadowRoot[] }) => StaticRange[]
|
|
60
|
+
}
|
|
61
|
+
).getComposedRanges?.({ shadowRoots: [opts.root] })?.[0] ??
|
|
62
|
+
(selection.rangeCount > 0 ? selection.getRangeAt(0) : undefined)
|
|
63
|
+
|
|
64
|
+
const startNode = domRange?.startContainer ?? selection.anchorNode
|
|
65
|
+
const endNode = domRange?.endContainer ?? selection.focusNode
|
|
66
|
+
if (!startNode || !endNode) return
|
|
67
|
+
if (!opts.root.contains(startNode) || !opts.root.contains(endNode)) return
|
|
68
|
+
|
|
69
|
+
const start = opts.lineForNode(startNode)
|
|
70
|
+
const end = opts.lineForNode(endNode)
|
|
71
|
+
if (start === undefined || end === undefined) return
|
|
72
|
+
|
|
73
|
+
const startSide = opts.sideForNode?.(startNode)
|
|
74
|
+
const endSide = opts.sideForNode?.(endNode)
|
|
75
|
+
const side = startSide ?? endSide
|
|
76
|
+
|
|
77
|
+
const range: SelectedLineRange = { start, end }
|
|
78
|
+
if (side) range.side = side
|
|
79
|
+
if (endSide && side && endSide !== side) range.endSide = endSide
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
range,
|
|
83
|
+
text: opts.preserveTextSelection && domRange ? toRange(domRange).cloneRange() : undefined,
|
|
84
|
+
}
|
|
85
|
+
}
|