@bojackduy/opencode-telescope 0.1.17 → 0.1.18
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/components/preview.tsx +112 -38
- package/package.json +1 -1
- package/search.ts +28 -1
- package/telescope.tsx +143 -17
- package/ui/render-target.ts +29 -3
package/components/preview.tsx
CHANGED
|
@@ -143,8 +143,8 @@ const PreviewToolPart = (props: { part: ConversationPreviewPart; item: SearchRes
|
|
|
143
143
|
<Show when={props.part.target}>
|
|
144
144
|
<TargetMarker part={props.part} item={props.item} role={props.part.tool ?? "tool"} time={props.part.timeCreated} theme={props.theme} />
|
|
145
145
|
</Show>
|
|
146
|
-
<Show when={codeTool()} fallback={<CompactToolRow part={props.part} color={color()} failed={failed()} theme={props.theme} />}>
|
|
147
|
-
<CodeToolPreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
146
|
+
<Show when={codeTool() && props.part.target} fallback={<CompactToolRow part={props.part} color={color()} failed={failed()} theme={props.theme} />}>
|
|
147
|
+
<CodeToolPreview part={props.part} item={props.item} syntax={props.syntax} theme={props.theme} />
|
|
148
148
|
</Show>
|
|
149
149
|
<Show when={props.part.state?.error}>
|
|
150
150
|
{(error) => <text fg={props.theme.error}>{error()}</text>}
|
|
@@ -167,29 +167,37 @@ const CompactToolRow = (props: { part: ConversationPreviewPart; color: any; fail
|
|
|
167
167
|
</>
|
|
168
168
|
)
|
|
169
169
|
|
|
170
|
-
const CodeToolPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
171
|
-
if (props.part.tool === "apply_patch") return <ApplyPatchPreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
172
|
-
if (props.part.tool === "edit") return <EditPreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
173
|
-
if (props.part.tool === "write") return <WritePreview part={props.part} syntax={props.syntax} theme={props.theme} />
|
|
170
|
+
const CodeToolPreview = (props: { part: ConversationPreviewPart; item: SearchResult; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
171
|
+
if (props.part.tool === "apply_patch") return <ApplyPatchPreview part={props.part} item={props.item} syntax={props.syntax} theme={props.theme} />
|
|
172
|
+
if (props.part.tool === "edit") return <EditPreview part={props.part} item={props.item} syntax={props.syntax} theme={props.theme} />
|
|
173
|
+
if (props.part.tool === "write") return <WritePreview part={props.part} item={props.item} syntax={props.syntax} theme={props.theme} />
|
|
174
174
|
return <CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
const ApplyPatchPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
177
|
+
const ApplyPatchPreview = (props: { part: ConversationPreviewPart; item: SearchResult; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
178
178
|
const files = createMemo(() => parseApplyPatchFiles(props.part.state?.metadata))
|
|
179
|
+
const matched = createMemo(() => {
|
|
180
|
+
const query = props.item.match || props.item.previewMatch
|
|
181
|
+
return files().find((file) => containsOrderedTokens(file.patch, query)) ?? files()[0]
|
|
182
|
+
})
|
|
179
183
|
return (
|
|
180
|
-
<Show when={
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
<Show when={matched()} fallback={<CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />}>
|
|
185
|
+
{(file) => {
|
|
186
|
+
const view = createMemo(() => clippedText(file().patch, props.item.match || props.item.previewMatch, 24))
|
|
187
|
+
return (
|
|
188
|
+
<ToolBlock title={patchTitle(file())} theme={props.theme}>
|
|
189
|
+
<Show when={view().clipped}>
|
|
190
|
+
<text fg={props.theme.textMuted}>showing matched excerpt from large patch</text>
|
|
191
|
+
</Show>
|
|
192
|
+
<DiffBlock diff={view().text} filePath={file().filePath} syntax={props.syntax} theme={props.theme} clipped={view().clipped} />
|
|
185
193
|
</ToolBlock>
|
|
186
|
-
)
|
|
187
|
-
|
|
194
|
+
)
|
|
195
|
+
}}
|
|
188
196
|
</Show>
|
|
189
197
|
)
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
const EditPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
200
|
+
const EditPreview = (props: { part: ConversationPreviewPart; item: SearchResult; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
193
201
|
const input = createMemo(() => recordValue(props.part.state?.input))
|
|
194
202
|
const metadata = createMemo(() => recordValue(props.part.state?.metadata))
|
|
195
203
|
const diff = createMemo(() => stringValue(metadata()?.diff) ?? stringValue(recordValue(metadata()?.filediff)?.patch) ?? "")
|
|
@@ -198,14 +206,24 @@ const EditPreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle
|
|
|
198
206
|
<Show when={diff()} fallback={<CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />}>
|
|
199
207
|
{(value) => (
|
|
200
208
|
<ToolBlock title={`← Edit ${shortPath(filePath())}`} theme={props.theme}>
|
|
201
|
-
|
|
209
|
+
{(() => {
|
|
210
|
+
const view = createMemo(() => clippedText(value(), props.item.match || props.item.previewMatch, 24))
|
|
211
|
+
return (
|
|
212
|
+
<>
|
|
213
|
+
<Show when={view().clipped}>
|
|
214
|
+
<text fg={props.theme.textMuted}>showing matched excerpt from large diff</text>
|
|
215
|
+
</Show>
|
|
216
|
+
<DiffBlock diff={view().text} filePath={filePath()} syntax={props.syntax} theme={props.theme} clipped={view().clipped} />
|
|
217
|
+
</>
|
|
218
|
+
)
|
|
219
|
+
})()}
|
|
202
220
|
</ToolBlock>
|
|
203
221
|
)}
|
|
204
222
|
</Show>
|
|
205
223
|
)
|
|
206
224
|
}
|
|
207
225
|
|
|
208
|
-
const WritePreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
226
|
+
const WritePreview = (props: { part: ConversationPreviewPart; item: SearchResult; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => {
|
|
209
227
|
const input = createMemo(() => recordValue(props.part.state?.input))
|
|
210
228
|
const filePath = createMemo(() => stringValue(input()?.filePath) ?? "")
|
|
211
229
|
const content = createMemo(() => stringValue(input()?.content) ?? "")
|
|
@@ -213,15 +231,25 @@ const WritePreview = (props: { part: ConversationPreviewPart; syntax: SyntaxStyl
|
|
|
213
231
|
<Show when={content()} fallback={<CompactToolRow part={props.part} color={props.theme.textMuted} failed={false} theme={props.theme} />}>
|
|
214
232
|
{(value) => (
|
|
215
233
|
<ToolBlock title={`# Wrote ${shortPath(filePath())}`} theme={props.theme}>
|
|
234
|
+
{(() => {
|
|
235
|
+
const view = createMemo(() => clippedText(value(), props.item.match || props.item.previewMatch, 32))
|
|
236
|
+
return (
|
|
237
|
+
<>
|
|
238
|
+
<Show when={view().clipped}>
|
|
239
|
+
<text fg={props.theme.textMuted}>showing matched excerpt from large file</text>
|
|
240
|
+
</Show>
|
|
216
241
|
<line_number fg={props.theme.textMuted} minWidth={3} paddingRight={1}>
|
|
217
242
|
<code
|
|
218
243
|
conceal={false}
|
|
219
244
|
fg={props.theme.text}
|
|
220
245
|
filetype={filetype(filePath())}
|
|
221
246
|
syntaxStyle={props.syntax}
|
|
222
|
-
content={
|
|
247
|
+
content={view().text}
|
|
223
248
|
/>
|
|
224
249
|
</line_number>
|
|
250
|
+
</>
|
|
251
|
+
)
|
|
252
|
+
})()}
|
|
225
253
|
</ToolBlock>
|
|
226
254
|
)}
|
|
227
255
|
</Show>
|
|
@@ -246,27 +274,29 @@ const ToolBlock = (props: { title: string; children: any; theme: TuiThemeCurrent
|
|
|
246
274
|
</box>
|
|
247
275
|
)
|
|
248
276
|
|
|
249
|
-
const DiffBlock = (props: { diff: string; filePath: string; syntax: SyntaxStyle; theme: TuiThemeCurrent }) => (
|
|
277
|
+
const DiffBlock = (props: { diff: string; filePath: string; syntax: SyntaxStyle; theme: TuiThemeCurrent; clipped?: boolean }) => (
|
|
250
278
|
<box paddingLeft={1}>
|
|
251
|
-
<diff
|
|
252
|
-
diff
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
<Show when={!props.clipped} fallback={<code filetype="diff" syntaxStyle={props.syntax} content={props.diff} fg={props.theme.text} />}>
|
|
280
|
+
<diff
|
|
281
|
+
diff={props.diff}
|
|
282
|
+
view="unified"
|
|
283
|
+
filetype={filetype(props.filePath)}
|
|
284
|
+
syntaxStyle={props.syntax}
|
|
285
|
+
showLineNumbers={true}
|
|
286
|
+
width="100%"
|
|
287
|
+
wrapMode="word"
|
|
288
|
+
fg={props.theme.text}
|
|
289
|
+
addedBg={props.theme.diffAddedBg}
|
|
290
|
+
removedBg={props.theme.diffRemovedBg}
|
|
291
|
+
contextBg={props.theme.diffContextBg}
|
|
292
|
+
addedSignColor={props.theme.diffHighlightAdded}
|
|
293
|
+
removedSignColor={props.theme.diffHighlightRemoved}
|
|
294
|
+
lineNumberFg={props.theme.diffLineNumber}
|
|
295
|
+
lineNumberBg={props.theme.diffContextBg}
|
|
296
|
+
addedLineNumberBg={props.theme.diffAddedLineNumberBg}
|
|
297
|
+
removedLineNumberBg={props.theme.diffRemovedLineNumberBg}
|
|
298
|
+
/>
|
|
299
|
+
</Show>
|
|
270
300
|
</box>
|
|
271
301
|
)
|
|
272
302
|
|
|
@@ -386,6 +416,50 @@ function matchExcerpt(text: string, query: string, radius = 80) {
|
|
|
386
416
|
}
|
|
387
417
|
}
|
|
388
418
|
|
|
419
|
+
function clippedText(text: string, query: string, radiusLines: number) {
|
|
420
|
+
const lines = text.split("\n")
|
|
421
|
+
const tooLarge = text.length > 30000 || lines.length > 420
|
|
422
|
+
if (!tooLarge) return { text, clipped: false }
|
|
423
|
+
|
|
424
|
+
const matchLine = findOrderedTokenLine(lines, query)
|
|
425
|
+
if (matchLine === -1) {
|
|
426
|
+
return { text: lines.slice(0, radiusLines * 2).join("\n"), clipped: true }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const start = Math.max(0, matchLine - radiusLines)
|
|
430
|
+
const end = Math.min(lines.length, matchLine + radiusLines + 1)
|
|
431
|
+
return {
|
|
432
|
+
text: [
|
|
433
|
+
start > 0 ? `... ${start} lines omitted ...` : undefined,
|
|
434
|
+
...lines.slice(start, end),
|
|
435
|
+
end < lines.length ? `... ${lines.length - end} lines omitted ...` : undefined,
|
|
436
|
+
].filter(Boolean).join("\n"),
|
|
437
|
+
clipped: true,
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function findOrderedTokenLine(lines: string[], query: string) {
|
|
442
|
+
const tokens = query.trim().split(/\s+/).filter(Boolean)
|
|
443
|
+
if (tokens.length === 0) return -1
|
|
444
|
+
for (let index = 0; index < lines.length; index++) {
|
|
445
|
+
if (containsOrderedTokens(lines[index]!, query)) return index
|
|
446
|
+
}
|
|
447
|
+
return -1
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function containsOrderedTokens(text: string, query: string) {
|
|
451
|
+
const tokens = query.trim().split(/\s+/).filter(Boolean)
|
|
452
|
+
if (tokens.length === 0) return false
|
|
453
|
+
const lower = text.toLowerCase()
|
|
454
|
+
let searchPos = 0
|
|
455
|
+
for (const token of tokens) {
|
|
456
|
+
const index = lower.indexOf(token.toLowerCase(), searchPos)
|
|
457
|
+
if (index === -1) return false
|
|
458
|
+
searchPos = index + token.length
|
|
459
|
+
}
|
|
460
|
+
return true
|
|
461
|
+
}
|
|
462
|
+
|
|
389
463
|
function parseApplyPatchFiles(metadata: unknown) {
|
|
390
464
|
const files = recordValue(metadata)?.files
|
|
391
465
|
if (!Array.isArray(files)) return []
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-telescope",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.18",
|
|
5
5
|
"description": "Fuzzy search across all OpenCode conversations — grep session and chat history, find code snippets, and jump to any chat instantly",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/search.ts
CHANGED
|
@@ -231,7 +231,11 @@ export function loadConversationAround(result: SearchResult, options?: { before?
|
|
|
231
231
|
fetchBefore,
|
|
232
232
|
fetchAfter,
|
|
233
233
|
beforeRows: beforeRows.length,
|
|
234
|
+
validBefore: validBefore.length,
|
|
235
|
+
invalidBefore: previewRowBreakdown(beforeRows, validBefore),
|
|
234
236
|
afterRows: afterRows.length,
|
|
237
|
+
validAfter: validAfter.length,
|
|
238
|
+
invalidAfter: previewRowBreakdown(afterRows, validAfter),
|
|
235
239
|
parts: parts.length,
|
|
236
240
|
hasMoreBefore: page.hasMoreBefore,
|
|
237
241
|
hasMoreAfter: page.hasMoreAfter,
|
|
@@ -262,6 +266,7 @@ export function loadConversationBefore(result: SearchResult, cursor: Conversatio
|
|
|
262
266
|
`).all(result.sessionID, cursor.timeCreated, cursor.timeCreated, cursor.id, fetchLimit)
|
|
263
267
|
const valid = rows.filter(isPreviewRow)
|
|
264
268
|
const parts = valid.slice(0, limit).reverse().flatMap((row) => parseConversationPart(row, false) ?? [])
|
|
269
|
+
const parsedIds = new Set(parts.map((part) => part.id))
|
|
265
270
|
const page = { parts, hasMoreBefore: valid.length > limit }
|
|
266
271
|
debug.log("preview:window", {
|
|
267
272
|
item: result.id,
|
|
@@ -270,7 +275,10 @@ export function loadConversationBefore(result: SearchResult, cursor: Conversatio
|
|
|
270
275
|
cursor,
|
|
271
276
|
limit,
|
|
272
277
|
rows: rows.length,
|
|
278
|
+
valid: valid.length,
|
|
279
|
+
invalid: previewRowBreakdown(rows, valid),
|
|
273
280
|
parts: parts.length,
|
|
281
|
+
unparsedValid: valid.slice(0, limit).filter((row) => !parsedIds.has(row.id)).map((row) => ({ id: row.id, type: row.type, role: row.role })),
|
|
274
282
|
hasMoreBefore: page.hasMoreBefore,
|
|
275
283
|
first: parts[0]?.id,
|
|
276
284
|
last: parts.at(-1)?.id,
|
|
@@ -298,6 +306,7 @@ export function loadConversationAfter(result: SearchResult, cursor: Conversation
|
|
|
298
306
|
`).all(result.sessionID, cursor.timeCreated, cursor.timeCreated, cursor.id, fetchLimit)
|
|
299
307
|
const valid = rows.filter(isPreviewRow)
|
|
300
308
|
const parts = valid.slice(0, limit).flatMap((row) => parseConversationPart(row, false) ?? [])
|
|
309
|
+
const parsedIds = new Set(parts.map((part) => part.id))
|
|
301
310
|
const page = { parts, hasMoreAfter: valid.length > limit }
|
|
302
311
|
debug.log("preview:window", {
|
|
303
312
|
item: result.id,
|
|
@@ -306,7 +315,10 @@ export function loadConversationAfter(result: SearchResult, cursor: Conversation
|
|
|
306
315
|
cursor,
|
|
307
316
|
limit,
|
|
308
317
|
rows: rows.length,
|
|
318
|
+
valid: valid.length,
|
|
319
|
+
invalid: previewRowBreakdown(rows, valid),
|
|
309
320
|
parts: parts.length,
|
|
321
|
+
unparsedValid: valid.slice(0, limit).filter((row) => !parsedIds.has(row.id)).map((row) => ({ id: row.id, type: row.type, role: row.role })),
|
|
310
322
|
hasMoreAfter: page.hasMoreAfter,
|
|
311
323
|
first: parts[0]?.id,
|
|
312
324
|
last: parts.at(-1)?.id,
|
|
@@ -501,7 +513,7 @@ function parseConversationPart(row: ConversationRow, target: boolean): Conversat
|
|
|
501
513
|
role: row.role,
|
|
502
514
|
type: row.type,
|
|
503
515
|
timeCreated: row.time_created,
|
|
504
|
-
text: extractToolIndexText(data).trim(),
|
|
516
|
+
text: target ? extractToolIndexText(data).trim() : "",
|
|
505
517
|
tool: typeof data.tool === "string" ? data.tool : "tool",
|
|
506
518
|
state: parseToolState(data.state),
|
|
507
519
|
target,
|
|
@@ -526,6 +538,21 @@ function isPreviewRow(row: ConversationRow) {
|
|
|
526
538
|
(row.type === "text" || row.type === "reasoning" || row.type === "tool")
|
|
527
539
|
}
|
|
528
540
|
|
|
541
|
+
function previewRowBreakdown(rows: ConversationRow[], validRows: ConversationRow[]) {
|
|
542
|
+
const valid = new Set(validRows.map((row) => row.id))
|
|
543
|
+
const invalidRows = rows.filter((row) => !valid.has(row.id))
|
|
544
|
+
if (invalidRows.length === 0) return undefined
|
|
545
|
+
const byRole = countBy(invalidRows, (row) => String(row.role ?? "unknown"))
|
|
546
|
+
const byType = countBy(invalidRows, (row) => String(row.type ?? "unknown"))
|
|
547
|
+
return { count: invalidRows.length, byRole, byType }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function countBy<T>(items: T[], key: (item: T) => string) {
|
|
551
|
+
const counts: Record<string, number> = {}
|
|
552
|
+
for (const item of items) counts[key(item)] = (counts[key(item)] ?? 0) + 1
|
|
553
|
+
return counts
|
|
554
|
+
}
|
|
555
|
+
|
|
529
556
|
function parsePartData(data: string) {
|
|
530
557
|
try {
|
|
531
558
|
const value = JSON.parse(data) as unknown
|
package/telescope.tsx
CHANGED
|
@@ -432,18 +432,55 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
432
432
|
})
|
|
433
433
|
|
|
434
434
|
const previewContentHeight = () => {
|
|
435
|
-
|
|
435
|
+
return previewScroll?.scrollHeight ?? 0
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const previewScrollState = () => {
|
|
439
|
+
const scroll = previewScroll
|
|
440
|
+
const children = scroll?.getChildren()
|
|
436
441
|
const lastChild = children?.[children.length - 1] as { y: number; height: number } | undefined
|
|
437
|
-
return
|
|
442
|
+
return {
|
|
443
|
+
y: scroll?.y,
|
|
444
|
+
height: scroll?.height,
|
|
445
|
+
scrollTop: scroll?.scrollTop,
|
|
446
|
+
scrollHeight: scroll?.scrollHeight,
|
|
447
|
+
childContentHeight: lastChild ? lastChild.y + lastChild.height : 0,
|
|
448
|
+
children: children?.length ?? 0,
|
|
449
|
+
hasMoreBefore: hasMorePreviewBefore(),
|
|
450
|
+
hasMoreAfter: hasMorePreviewAfter(),
|
|
451
|
+
loadingMore: loadingPreviewMore(),
|
|
452
|
+
prefetchingBefore: prefetchingPreviewBefore(),
|
|
453
|
+
prefetchingAfter: prefetchingPreviewAfter(),
|
|
454
|
+
timerBefore: Boolean(previewBeforeTimer),
|
|
455
|
+
timerAfter: Boolean(previewAfterTimer),
|
|
456
|
+
}
|
|
438
457
|
}
|
|
439
458
|
|
|
440
459
|
const loadPreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
441
460
|
const item = selectedResult()
|
|
442
461
|
const first = previewParts()[0]
|
|
443
|
-
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore())
|
|
462
|
+
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) {
|
|
463
|
+
debug.log("preview:load-before:skip", {
|
|
464
|
+
reason: !item ? "no-item" : !first ? "no-first-part" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-before",
|
|
465
|
+
previousContentHeight,
|
|
466
|
+
preserveScroll,
|
|
467
|
+
visibleLoad,
|
|
468
|
+
state: previewScrollState(),
|
|
469
|
+
})
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
const beforeState = previewScrollState()
|
|
444
473
|
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
445
474
|
debug.time("preview:load-before")
|
|
446
475
|
try {
|
|
476
|
+
debug.log("preview:load-before:start", {
|
|
477
|
+
item: item.id,
|
|
478
|
+
cursor: { id: first.id, timeCreated: first.timeCreated },
|
|
479
|
+
previousContentHeight,
|
|
480
|
+
preserveScroll,
|
|
481
|
+
visibleLoad,
|
|
482
|
+
state: beforeState,
|
|
483
|
+
})
|
|
447
484
|
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
448
485
|
debug.log("preview:load-before", {
|
|
449
486
|
item: item.id,
|
|
@@ -458,8 +495,25 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
458
495
|
setPreviewParts((prev) => [...page.parts, ...prev])
|
|
459
496
|
if (preserveScroll) {
|
|
460
497
|
setTimeout(() => {
|
|
498
|
+
const beforeAdjust = previewScrollState()
|
|
461
499
|
const delta = previewContentHeight() - previousContentHeight
|
|
462
500
|
if (delta > 0) previewScroll?.scrollBy(delta)
|
|
501
|
+
debug.log("preview:load-before:adjust", {
|
|
502
|
+
delta,
|
|
503
|
+
previousContentHeight,
|
|
504
|
+
newContentHeight: previewContentHeight(),
|
|
505
|
+
scrolled: delta > 0,
|
|
506
|
+
beforeAdjust,
|
|
507
|
+
afterAdjust: previewScrollState(),
|
|
508
|
+
})
|
|
509
|
+
}, 1)
|
|
510
|
+
} else {
|
|
511
|
+
setTimeout(() => {
|
|
512
|
+
debug.log("preview:load-before:no-adjust", {
|
|
513
|
+
previousContentHeight,
|
|
514
|
+
newContentHeight: previewContentHeight(),
|
|
515
|
+
state: previewScrollState(),
|
|
516
|
+
})
|
|
463
517
|
}, 1)
|
|
464
518
|
}
|
|
465
519
|
}
|
|
@@ -475,7 +529,20 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
475
529
|
const loadPreviewAfter = (visibleLoad = false) => {
|
|
476
530
|
const item = selectedResult()
|
|
477
531
|
const last = previewParts().at(-1)
|
|
478
|
-
if (!item || !last || loadingPreviewMore() || prefetchingPreviewAfter())
|
|
532
|
+
if (!item || !last || loadingPreviewMore() || prefetchingPreviewAfter()) {
|
|
533
|
+
debug.log("preview:load-after:skip", {
|
|
534
|
+
reason: !item ? "no-item" : !last ? "no-last-part" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-after",
|
|
535
|
+
visibleLoad,
|
|
536
|
+
state: previewScrollState(),
|
|
537
|
+
})
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
debug.log("preview:load-after:start", {
|
|
541
|
+
item: item.id,
|
|
542
|
+
cursor: { id: last.id, timeCreated: last.timeCreated },
|
|
543
|
+
visibleLoad,
|
|
544
|
+
state: previewScrollState(),
|
|
545
|
+
})
|
|
479
546
|
visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewAfter(true)
|
|
480
547
|
debug.time("preview:load-after")
|
|
481
548
|
try {
|
|
@@ -499,16 +566,40 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
499
566
|
}
|
|
500
567
|
|
|
501
568
|
const schedulePreviewBefore = (previousContentHeight = previewContentHeight(), preserveScroll = true, visibleLoad = false) => {
|
|
502
|
-
if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore())
|
|
569
|
+
if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) {
|
|
570
|
+
debug.log("preview:prefetch-before:skip", {
|
|
571
|
+
reason: !hasMorePreviewBefore() ? "no-more-before" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-before",
|
|
572
|
+
previousContentHeight,
|
|
573
|
+
preserveScroll,
|
|
574
|
+
visibleLoad,
|
|
575
|
+
state: previewScrollState(),
|
|
576
|
+
})
|
|
577
|
+
return
|
|
578
|
+
}
|
|
503
579
|
if (previewBeforeTimer) {
|
|
504
580
|
if (pendingPreviewBefore) {
|
|
581
|
+
const previousPending = { ...pendingPreviewBefore }
|
|
505
582
|
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
506
583
|
pendingPreviewBefore.visibleLoad = pendingPreviewBefore.visibleLoad || visibleLoad
|
|
584
|
+
debug.log("preview:prefetch-before:merge", {
|
|
585
|
+
previousPending,
|
|
586
|
+
nextPending: pendingPreviewBefore,
|
|
587
|
+
requested: { previousContentHeight, preserveScroll, visibleLoad },
|
|
588
|
+
state: previewScrollState(),
|
|
589
|
+
})
|
|
590
|
+
} else {
|
|
591
|
+
debug.log("preview:prefetch-before:skip", {
|
|
592
|
+
reason: "timer-already-set",
|
|
593
|
+
previousContentHeight,
|
|
594
|
+
preserveScroll,
|
|
595
|
+
visibleLoad,
|
|
596
|
+
state: previewScrollState(),
|
|
597
|
+
})
|
|
507
598
|
}
|
|
508
599
|
return
|
|
509
600
|
}
|
|
510
601
|
pendingPreviewBefore = { previousContentHeight, preserveScroll, visibleLoad }
|
|
511
|
-
debug.log("preview:prefetch-before-scheduled", { preserveScroll, visibleLoad })
|
|
602
|
+
debug.log("preview:prefetch-before-scheduled", { previousContentHeight, preserveScroll, visibleLoad, state: previewScrollState() })
|
|
512
603
|
previewBeforeTimer = setTimeout(() => {
|
|
513
604
|
const pending = pendingPreviewBefore
|
|
514
605
|
previewBeforeTimer = undefined
|
|
@@ -518,10 +609,20 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
518
609
|
}
|
|
519
610
|
|
|
520
611
|
const schedulePreviewAfter = (visibleLoad = false) => {
|
|
521
|
-
if (!hasMorePreviewAfter() || loadingPreviewMore() || prefetchingPreviewAfter())
|
|
612
|
+
if (!hasMorePreviewAfter() || loadingPreviewMore() || prefetchingPreviewAfter()) {
|
|
613
|
+
debug.log("preview:prefetch-after:skip", {
|
|
614
|
+
reason: !hasMorePreviewAfter() ? "no-more-after" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-after",
|
|
615
|
+
visibleLoad,
|
|
616
|
+
state: previewScrollState(),
|
|
617
|
+
})
|
|
618
|
+
return
|
|
619
|
+
}
|
|
522
620
|
pendingPreviewAfterVisible = pendingPreviewAfterVisible || visibleLoad
|
|
523
|
-
if (previewAfterTimer)
|
|
524
|
-
|
|
621
|
+
if (previewAfterTimer) {
|
|
622
|
+
debug.log("preview:prefetch-after:skip", { reason: "timer-already-set", visibleLoad, pendingVisibleLoad: pendingPreviewAfterVisible, state: previewScrollState() })
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
debug.log("preview:prefetch-after-scheduled", { visibleLoad, state: previewScrollState() })
|
|
525
626
|
previewAfterTimer = setTimeout(() => {
|
|
526
627
|
const pendingVisibleLoad = pendingPreviewAfterVisible
|
|
527
628
|
previewAfterTimer = undefined
|
|
@@ -573,18 +674,19 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
573
674
|
const scroll = previewScroll
|
|
574
675
|
const children = scroll?.getChildren()
|
|
575
676
|
if (!scroll || !children || children.length === 0) return
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
const atTop = scroll.y <= 0
|
|
677
|
+
const totalContentHeight = scroll.scrollHeight
|
|
678
|
+
const atTop = scroll.scrollTop <= 0
|
|
579
679
|
const prefetchDistance = Math.max(2, Math.floor(scroll.height * PREVIEW_PREFETCH_VIEWPORTS))
|
|
580
|
-
const nearTop = scroll.
|
|
581
|
-
const atBottom = scroll.
|
|
582
|
-
const nearBottom = scroll.
|
|
680
|
+
const nearTop = scroll.scrollTop <= prefetchDistance
|
|
681
|
+
const atBottom = scroll.scrollTop + scroll.height >= totalContentHeight - 1
|
|
682
|
+
const nearBottom = scroll.scrollTop + scroll.height >= totalContentHeight - prefetchDistance
|
|
583
683
|
if (nearTop || nearBottom) {
|
|
584
684
|
debug.log("preview:scroll-edge", {
|
|
585
685
|
y: scroll.y,
|
|
686
|
+
scrollTop: scroll.scrollTop,
|
|
586
687
|
height: scroll.height,
|
|
587
688
|
contentHeight: totalContentHeight,
|
|
689
|
+
childContentHeight: previewScrollState().childContentHeight,
|
|
588
690
|
prefetchDistance,
|
|
589
691
|
atTop,
|
|
590
692
|
nearTop,
|
|
@@ -597,7 +699,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
597
699
|
children: children.length,
|
|
598
700
|
})
|
|
599
701
|
}
|
|
600
|
-
if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight,
|
|
702
|
+
if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight, true, false)
|
|
601
703
|
if (nearBottom && hasMorePreviewAfter()) schedulePreviewAfter(atBottom)
|
|
602
704
|
}, 400)
|
|
603
705
|
onCleanup(() => clearInterval(interval))
|
|
@@ -625,7 +727,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
625
727
|
|
|
626
728
|
const scrollPreview = (direction: 1 | -1, evt: ParsedKey) => {
|
|
627
729
|
prevent(evt)
|
|
628
|
-
|
|
730
|
+
const scroll = previewScroll
|
|
731
|
+
if (!scroll) {
|
|
732
|
+
debug.log("preview:scroll-key:skip", { reason: "no-scroll", direction })
|
|
733
|
+
return
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const beforeState = previewScrollState()
|
|
737
|
+
debug.log("preview:scroll-key", { direction, state: beforeState })
|
|
738
|
+
|
|
739
|
+
if (direction < 0 && scroll.scrollTop <= 0 && hasMorePreviewBefore()) {
|
|
740
|
+
debug.log("preview:scroll-key:load-before", { direction, state: beforeState })
|
|
741
|
+
schedulePreviewBefore(previewContentHeight(), true, true)
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const totalContentHeight = scroll.scrollHeight
|
|
746
|
+
if (direction > 0 && scroll.scrollTop + scroll.height >= totalContentHeight - 1 && hasMorePreviewAfter()) {
|
|
747
|
+
debug.log("preview:scroll-key:load-after", { direction, totalContentHeight, state: beforeState })
|
|
748
|
+
schedulePreviewAfter(true)
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const amount = direction * previewScrollAmount(scroll)
|
|
753
|
+
scroll.scrollBy(amount)
|
|
754
|
+
debug.log("preview:scroll-key:scroll", { direction, amount, before: beforeState, after: previewScrollState() })
|
|
629
755
|
}
|
|
630
756
|
|
|
631
757
|
const focusInput = () => {
|
package/ui/render-target.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import type { SearchResult } from "../search.ts"
|
|
3
|
+
import { debug } from "./debug.ts"
|
|
3
4
|
|
|
4
5
|
export function previewScrollAmount(scroll: ScrollBoxRenderable | undefined) {
|
|
5
6
|
return Math.max(1, Math.floor((scroll?.height || 10) / 8))
|
|
@@ -13,10 +14,35 @@ export function messageTargetID(item: SearchResult) {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function scrollPreviewToTarget(scroll: ScrollBoxRenderable | undefined, targetID: string) {
|
|
16
|
-
if (!scroll)
|
|
17
|
+
if (!scroll) {
|
|
18
|
+
debug.log("preview:target-scroll:skip", { reason: "no-scroll", targetID })
|
|
19
|
+
return
|
|
20
|
+
}
|
|
17
21
|
const target = findRenderableByID(scroll, targetID)
|
|
18
|
-
if (!target)
|
|
19
|
-
|
|
22
|
+
if (!target) {
|
|
23
|
+
debug.log("preview:target-scroll:skip", {
|
|
24
|
+
reason: "target-not-found",
|
|
25
|
+
targetID,
|
|
26
|
+
y: scroll.y,
|
|
27
|
+
scrollTop: scroll.scrollTop,
|
|
28
|
+
scrollHeight: scroll.scrollHeight,
|
|
29
|
+
height: scroll.height,
|
|
30
|
+
children: scroll.getChildren().length,
|
|
31
|
+
})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const delta = target.y - scroll.y - Math.max(1, Math.floor(scroll.height / 3))
|
|
35
|
+
debug.log("preview:target-scroll", {
|
|
36
|
+
targetID,
|
|
37
|
+
targetY: target.y,
|
|
38
|
+
scrollY: scroll.y,
|
|
39
|
+
scrollTop: scroll.scrollTop,
|
|
40
|
+
scrollHeight: scroll.height,
|
|
41
|
+
contentHeight: scroll.scrollHeight,
|
|
42
|
+
delta,
|
|
43
|
+
})
|
|
44
|
+
scroll.scrollBy(delta)
|
|
45
|
+
debug.log("preview:target-scroll:after", { targetID, scrollY: scroll.y, scrollTop: scroll.scrollTop })
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
export function jumpToRenderedTarget(root: unknown, targetID: string) {
|