@bojackduy/opencode-telescope 0.1.20 → 0.1.22
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/README.md +17 -1
- package/package.json +9 -5
- package/telescope.tsx +395 -154
- package/ui/render-target.ts +6 -4
package/README.md
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# opencode-telescope
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenCode TUI plugin for fuzzy-searching local conversation history, session transcripts, and past AI coding chats.
|
|
4
|
+
|
|
5
|
+
Install the npm package `@bojackduy/opencode-telescope` to grep through OpenCode chat history, find old code snippets, and jump back to any session instantly.
|
|
4
6
|
|
|
5
7
|
> Inspired by [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) — a fuzzy finder for your conversation history.
|
|
6
8
|
|
|
9
|
+
## Links
|
|
10
|
+
|
|
11
|
+
- Documentation: https://bojackduy.github.io/opencode-telescope/
|
|
12
|
+
- npm: https://www.npmjs.com/package/@bojackduy/opencode-telescope
|
|
13
|
+
- GitHub: https://github.com/bojackduy/opencode-telescope
|
|
14
|
+
- Issues: https://github.com/bojackduy/opencode-telescope/issues
|
|
15
|
+
|
|
7
16
|

|
|
8
17
|
|
|
9
18
|
## Use cases
|
|
@@ -19,9 +28,12 @@ Fuzzy search across all your OpenCode conversations — grep through session and
|
|
|
19
28
|
- **Live preview** — preview the matched conversation result before opening
|
|
20
29
|
- **Find & jump** — select any result and jump straight to that session
|
|
21
30
|
- **Neovim Telescope-style UX** — familiar `<leader>f` keybind and `/telescope` command
|
|
31
|
+
- **Local-first search** — reads your OpenCode session database without sending chat history to a remote service
|
|
22
32
|
|
|
23
33
|
## Installation
|
|
24
34
|
|
|
35
|
+
Install from npm as `@bojackduy/opencode-telescope`.
|
|
36
|
+
|
|
25
37
|
Add the plugin to your `tui.json`:
|
|
26
38
|
|
|
27
39
|
```jsonc
|
|
@@ -88,4 +100,8 @@ Key strings support simple names like `j`, `k`, `down`, `up`, `enter`, `return`,
|
|
|
88
100
|
|
|
89
101
|
Reads the OpenCode local SQLite session database in read-only mode, parses conversations into searchable text, and opens the selected session through the existing TUI route.
|
|
90
102
|
|
|
103
|
+
## Keywords
|
|
104
|
+
|
|
105
|
+
OpenCode plugin, OpenCode TUI, fuzzy finder, conversation search, chat history search, AI coding session search, LLM history, local session search, Telescope-style picker.
|
|
106
|
+
|
|
91
107
|

|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-telescope",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.1.22",
|
|
5
|
+
"description": "OpenCode TUI plugin for fuzzy-searching local conversation history, session transcripts, and past AI coding chats",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/bojackduy/opencode-telescope.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://github.
|
|
12
|
+
"homepage": "https://bojackduy.github.io/opencode-telescope/",
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/bojackduy/opencode-telescope/issues"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"opencode",
|
|
@@ -21,11 +21,15 @@
|
|
|
21
21
|
"search",
|
|
22
22
|
"session",
|
|
23
23
|
"conversation",
|
|
24
|
+
"conversation-search",
|
|
24
25
|
"history",
|
|
26
|
+
"ai-chat-history",
|
|
27
|
+
"llm-history",
|
|
25
28
|
"fuzzy",
|
|
26
29
|
"fuzzy-finder",
|
|
27
30
|
"grep",
|
|
28
31
|
"finder",
|
|
32
|
+
"opencode-tui",
|
|
29
33
|
"neovim",
|
|
30
34
|
"chat-history"
|
|
31
35
|
],
|
package/telescope.tsx
CHANGED
|
@@ -53,7 +53,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
53
53
|
const MIN_RECENT_BATCH_SIZE = 15
|
|
54
54
|
const RESULT_OVERSCAN_MULTIPLIER = 2
|
|
55
55
|
const RESULT_BATCH_VIEWPORTS = 4
|
|
56
|
-
const RESULT_PREFETCH_VIEWPORTS =
|
|
56
|
+
const RESULT_PREFETCH_VIEWPORTS = 5
|
|
57
57
|
const RESULT_CACHE_BEHIND_VIEWPORTS = 6
|
|
58
58
|
const INITIAL_PREVIEW_BEFORE = 20
|
|
59
59
|
const INITIAL_PREVIEW_AFTER = 30
|
|
@@ -93,10 +93,26 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
93
93
|
let resultNavigationStarted = false
|
|
94
94
|
let resultPrefetchTimer: ReturnType<typeof setTimeout> | undefined
|
|
95
95
|
let resultPreviousTimer: ReturnType<typeof setTimeout> | undefined
|
|
96
|
+
let searchTimer: ReturnType<typeof setTimeout> | undefined
|
|
97
|
+
let lastFiredQuery = ""
|
|
98
|
+
const SEARCH_DEBOUNCE_MS = 200
|
|
99
|
+
type PreviewBeforeIntent = "passive-prefetch" | "explicit-scroll"
|
|
100
|
+
type PendingPreviewBefore = {
|
|
101
|
+
id: number
|
|
102
|
+
previousContentHeight: number
|
|
103
|
+
previousScrollTop: number
|
|
104
|
+
requestedAmount: number
|
|
105
|
+
intent: PreviewBeforeIntent
|
|
106
|
+
visibleLoad: boolean
|
|
107
|
+
}
|
|
108
|
+
let previewBeforeLoadID = 0
|
|
96
109
|
let previewBeforeTimer: ReturnType<typeof setTimeout> | undefined
|
|
97
110
|
let previewAfterTimer: ReturnType<typeof setTimeout> | undefined
|
|
98
|
-
let pendingPreviewBefore:
|
|
111
|
+
let pendingPreviewBefore: PendingPreviewBefore | undefined
|
|
99
112
|
let pendingPreviewAfterVisible = false
|
|
113
|
+
let previewBeforeAdjusting = false
|
|
114
|
+
let pendingCoalescedExplicit: { requestedAmount: number } | undefined
|
|
115
|
+
let pendingPreviewAnchorCorrection: { anchorPartID: string; visualOffset: number; createdAt: number; expiresAt: number } | undefined
|
|
100
116
|
const previewMeasuredHeights = new Map<string, number>()
|
|
101
117
|
const cancelPreviewPrefetch = () => {
|
|
102
118
|
if (previewBeforeTimer) clearTimeout(previewBeforeTimer)
|
|
@@ -105,6 +121,9 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
105
121
|
previewAfterTimer = undefined
|
|
106
122
|
pendingPreviewBefore = undefined
|
|
107
123
|
pendingPreviewAfterVisible = false
|
|
124
|
+
previewBeforeAdjusting = false
|
|
125
|
+
pendingCoalescedExplicit = undefined
|
|
126
|
+
pendingPreviewAnchorCorrection = undefined
|
|
108
127
|
setPrefetchingPreviewBefore(false)
|
|
109
128
|
setPrefetchingPreviewAfter(false)
|
|
110
129
|
setLoadingPreviewMore(false)
|
|
@@ -151,6 +170,12 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
151
170
|
return { start: base + cachedStart, end: base + cachedEnd, items: cached.slice(cachedStart, cachedEnd) }
|
|
152
171
|
})
|
|
153
172
|
|
|
173
|
+
const resultTopSpacerHeight = () =>
|
|
174
|
+
Math.max(0, resultRenderWindow().start - resultBaseOffset()) * resultRowHeight()
|
|
175
|
+
|
|
176
|
+
const resultBottomSpacerHeight = () =>
|
|
177
|
+
Math.max(0, resultBaseOffset() + results().length - resultRenderWindow().end) * resultRowHeight()
|
|
178
|
+
|
|
154
179
|
let lastResultRenderWindow = ""
|
|
155
180
|
createEffect(() => {
|
|
156
181
|
const window = resultRenderWindow()
|
|
@@ -171,6 +196,51 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
171
196
|
})
|
|
172
197
|
})
|
|
173
198
|
|
|
199
|
+
const executeSearch = (q: string, role: SearchRole | undefined, db: string, dir: string) => {
|
|
200
|
+
lastFiredQuery = q
|
|
201
|
+
const limit = q ? Math.min(searchBatchSize(), 80) : recentBatchSize()
|
|
202
|
+
setLoading(true)
|
|
203
|
+
if (q) setBusy(true)
|
|
204
|
+
debug.time("query:search")
|
|
205
|
+
try {
|
|
206
|
+
debug.log("bootstrap:search:start", { limit, directory: dir, role, query: q || "(all recent)" })
|
|
207
|
+
const batch = q
|
|
208
|
+
? searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir, role })
|
|
209
|
+
: recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir, role })
|
|
210
|
+
debug.log("bootstrap:search:done", { rows: batch.length, limit })
|
|
211
|
+
solidBatch(() => {
|
|
212
|
+
setResults(batch)
|
|
213
|
+
setResultBaseOffset(0)
|
|
214
|
+
setNextResultOffset(batch.length)
|
|
215
|
+
setHasMore(batch.length >= limit)
|
|
216
|
+
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
217
|
+
setSelected(0)
|
|
218
|
+
})
|
|
219
|
+
} catch (err) {
|
|
220
|
+
debug.log("bootstrap:search:error", err instanceof Error ? err.message : String(err))
|
|
221
|
+
solidBatch(() => {
|
|
222
|
+
setResults([])
|
|
223
|
+
setResultBaseOffset(0)
|
|
224
|
+
setNextResultOffset(0)
|
|
225
|
+
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
226
|
+
})
|
|
227
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
228
|
+
} finally {
|
|
229
|
+
debug.timeEnd("query:search")
|
|
230
|
+
setBusy(false)
|
|
231
|
+
setLoading(false)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const scheduleSearch = (q: string, role: SearchRole | undefined, db: string, dir: string) => {
|
|
236
|
+
if (searchTimer) clearTimeout(searchTimer)
|
|
237
|
+
if (q === lastFiredQuery && results().length > 0) return
|
|
238
|
+
searchTimer = setTimeout(() => {
|
|
239
|
+
searchTimer = undefined
|
|
240
|
+
executeSearch(q, role, db, dir)
|
|
241
|
+
}, q ? SEARCH_DEBOUNCE_MS : 1)
|
|
242
|
+
}
|
|
243
|
+
|
|
174
244
|
createEffect(() => {
|
|
175
245
|
const q = query().trim()
|
|
176
246
|
const role = ownerRole()
|
|
@@ -189,73 +259,11 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
189
259
|
const db = dbPath()
|
|
190
260
|
const dir = directory
|
|
191
261
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
debug.time("query:recent")
|
|
198
|
-
try {
|
|
199
|
-
const batch = recentSessionMessages({ limit, offset: 0, dbPath: db, directory: dir, role })
|
|
200
|
-
debug.log("bootstrap:recent:done", { rows: batch.length, limit })
|
|
201
|
-
solidBatch(() => {
|
|
202
|
-
setResults(batch)
|
|
203
|
-
setResultBaseOffset(0)
|
|
204
|
-
setNextResultOffset(batch.length)
|
|
205
|
-
setHasMore(batch.length >= limit)
|
|
206
|
-
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
207
|
-
setSelected(0)
|
|
208
|
-
})
|
|
209
|
-
} catch (err) {
|
|
210
|
-
debug.log("bootstrap:recent:error", err instanceof Error ? err.message : String(err))
|
|
211
|
-
solidBatch(() => {
|
|
212
|
-
setResults([])
|
|
213
|
-
setResultBaseOffset(0)
|
|
214
|
-
setNextResultOffset(0)
|
|
215
|
-
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
216
|
-
})
|
|
217
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
218
|
-
}
|
|
219
|
-
debug.timeEnd("query:recent")
|
|
220
|
-
setLoading(false)
|
|
221
|
-
debug.log("component:interactive")
|
|
222
|
-
}, 1)
|
|
223
|
-
onCleanup(() => clearTimeout(timer))
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
setLoading(true)
|
|
228
|
-
setBusy(true)
|
|
229
|
-
const limit = searchBatchSize()
|
|
230
|
-
const timer = setTimeout(() => {
|
|
231
|
-
debug.time("query:search")
|
|
232
|
-
try {
|
|
233
|
-
debug.log("bootstrap:search:start", { limit, directory: dir, role, query: q })
|
|
234
|
-
const batch = searchSessionMessages(q, { limit, offset: 0, dbPath: db, directory: dir, role })
|
|
235
|
-
debug.log("bootstrap:search:done", { rows: batch.length, limit })
|
|
236
|
-
solidBatch(() => {
|
|
237
|
-
setResults(batch)
|
|
238
|
-
setResultBaseOffset(0)
|
|
239
|
-
setNextResultOffset(batch.length)
|
|
240
|
-
setHasMore(batch.length >= limit)
|
|
241
|
-
setResultPageInfo({ loadedUntil: batch.length, hasMore: batch.length >= limit, pageSize: limit, lastOffset: 0, lastAdded: batch.length })
|
|
242
|
-
setSelected(0)
|
|
243
|
-
})
|
|
244
|
-
} catch (err) {
|
|
245
|
-
solidBatch(() => {
|
|
246
|
-
setResults([])
|
|
247
|
-
setResultBaseOffset(0)
|
|
248
|
-
setNextResultOffset(0)
|
|
249
|
-
setResultPageInfo({ loadedUntil: 0, hasMore: false, pageSize: limit, lastOffset: 0, lastAdded: 0 })
|
|
250
|
-
})
|
|
251
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
252
|
-
} finally {
|
|
253
|
-
debug.timeEnd("query:search")
|
|
254
|
-
setBusy(false)
|
|
255
|
-
setLoading(false)
|
|
256
|
-
}
|
|
257
|
-
}, 180)
|
|
258
|
-
onCleanup(() => clearTimeout(timer))
|
|
262
|
+
scheduleSearch(q, role, db, dir)
|
|
263
|
+
onCleanup(() => {
|
|
264
|
+
if (searchTimer) clearTimeout(searchTimer)
|
|
265
|
+
searchTimer = undefined
|
|
266
|
+
})
|
|
259
267
|
})
|
|
260
268
|
|
|
261
269
|
const loadMoreResults = (advance = false) => {
|
|
@@ -446,6 +454,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
446
454
|
onCleanup(() => clearTimeout(timer))
|
|
447
455
|
})
|
|
448
456
|
|
|
457
|
+
createEffect(() => {
|
|
458
|
+
if (!resultNavigationStarted || !hasMore()) return
|
|
459
|
+
const interval = setInterval(() => {
|
|
460
|
+
if (!hasMore() || loadingMore() || loadingPreviousResults() || prefetchingResults() || busy() || loading()) return
|
|
461
|
+
const scroll = resultScroll
|
|
462
|
+
if (!scroll) return
|
|
463
|
+
const children = scroll.getChildren()
|
|
464
|
+
if (!children || children.length === 0) return
|
|
465
|
+
const lastChild = children[children.length - 1] as { y: number; height: number }
|
|
466
|
+
const contentHeight = lastChild.y + lastChild.height
|
|
467
|
+
const scrollThreshold = Math.max(2, Math.floor(scroll.height * RESULT_PREFETCH_VIEWPORTS))
|
|
468
|
+
if (scroll.scrollTop + scroll.height >= contentHeight - scrollThreshold) {
|
|
469
|
+
debug.log("results:scroll-prefetch", {
|
|
470
|
+
scrollTop: scroll.scrollTop,
|
|
471
|
+
height: scroll.height,
|
|
472
|
+
contentHeight,
|
|
473
|
+
scrollThreshold,
|
|
474
|
+
rowsAhead: resultPrefetchState().rowsAhead,
|
|
475
|
+
})
|
|
476
|
+
scheduleResultPrefetch(false)
|
|
477
|
+
}
|
|
478
|
+
}, 200)
|
|
479
|
+
onCleanup(() => clearInterval(interval))
|
|
480
|
+
})
|
|
481
|
+
|
|
449
482
|
const estimatePreviewPartHeight = (part: ConversationPreviewPart) => {
|
|
450
483
|
if (part.type === "tool") {
|
|
451
484
|
if (!part.target) return 2
|
|
@@ -459,15 +492,31 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
459
492
|
|
|
460
493
|
const previewPartHeight = (part: ConversationPreviewPart) => previewMeasuredHeights.get(part.id) ?? estimatePreviewPartHeight(part)
|
|
461
494
|
|
|
462
|
-
const
|
|
463
|
-
previewHeightVersion()
|
|
495
|
+
const buildPreviewLayout = (parts: ConversationPreviewPart[]) => {
|
|
464
496
|
let offset = 0
|
|
465
|
-
return
|
|
497
|
+
return parts.map((part) => {
|
|
466
498
|
const height = previewPartHeight(part)
|
|
467
499
|
const row = { part, top: offset, bottom: offset + height, height }
|
|
468
500
|
offset += height
|
|
469
501
|
return row
|
|
470
502
|
})
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const previewWindowForLayout = (layout: ReturnType<typeof buildPreviewLayout>, scrollTop: number, viewportHeight: number) => {
|
|
506
|
+
if (layout.length === 0) return { start: 0, end: 0 }
|
|
507
|
+
const overscan = Math.max(viewportHeight * 2, 40)
|
|
508
|
+
const from = Math.max(0, scrollTop - overscan)
|
|
509
|
+
const to = scrollTop + viewportHeight + overscan
|
|
510
|
+
const foundStart = layout.findIndex((row) => row.bottom >= from)
|
|
511
|
+
const start = foundStart === -1 ? Math.max(0, layout.length - 1) : Math.max(0, foundStart)
|
|
512
|
+
const foundEnd = layout.findIndex((row) => row.top > to)
|
|
513
|
+
const end = foundEnd === -1 ? layout.length : foundEnd
|
|
514
|
+
return { start, end: Math.min(layout.length, Math.max(start + 1, end)) }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const previewVirtualLayout = createMemo(() => {
|
|
518
|
+
previewHeightVersion()
|
|
519
|
+
return buildPreviewLayout(previewParts())
|
|
471
520
|
})
|
|
472
521
|
|
|
473
522
|
const previewContentHeight = () => previewVirtualLayout().at(-1)?.bottom ?? 0
|
|
@@ -486,14 +535,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
486
535
|
return
|
|
487
536
|
}
|
|
488
537
|
|
|
489
|
-
const
|
|
490
|
-
const from = Math.max(0, scroll.scrollTop - overscan)
|
|
491
|
-
const to = scroll.scrollTop + scroll.height + overscan
|
|
492
|
-
const foundStart = layout.findIndex((row) => row.bottom >= from)
|
|
493
|
-
const start = foundStart === -1 ? Math.max(0, layout.length - 1) : Math.max(0, foundStart)
|
|
494
|
-
const foundEnd = layout.findIndex((row) => row.top > to)
|
|
495
|
-
const end = foundEnd === -1 ? layout.length : foundEnd
|
|
496
|
-
const next = { start, end: Math.min(layout.length, Math.max(start + 1, end)) }
|
|
538
|
+
const next = previewWindowForLayout(layout, scroll.scrollTop, scroll.height)
|
|
497
539
|
const current = previewWindow()
|
|
498
540
|
if (current.start !== next.start || current.end !== next.end) setPreviewWindow(next)
|
|
499
541
|
}
|
|
@@ -558,6 +600,29 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
558
600
|
totalMeasured: previewMeasuredHeights.size,
|
|
559
601
|
window: previewWindow(),
|
|
560
602
|
})
|
|
603
|
+
const anchorCorrection = pendingPreviewAnchorCorrection
|
|
604
|
+
if (anchorCorrection && scroll) {
|
|
605
|
+
const now = Date.now()
|
|
606
|
+
const shouldCorrect = now <= anchorCorrection.expiresAt && lastPreviewScrollKeyMs <= anchorCorrection.createdAt
|
|
607
|
+
if (shouldCorrect) {
|
|
608
|
+
const nextLayout = buildPreviewLayout(previewParts())
|
|
609
|
+
const anchor = nextLayout.find((row) => row.part.id === anchorCorrection.anchorPartID)
|
|
610
|
+
if (anchor) {
|
|
611
|
+
const correctedScrollTop = Math.max(0, anchor.top + anchorCorrection.visualOffset)
|
|
612
|
+
const correctedWindow = previewWindowForLayout(nextLayout, correctedScrollTop, scroll.height)
|
|
613
|
+
setPreviewWindow(correctedWindow)
|
|
614
|
+
scroll.scrollTo(correctedScrollTop)
|
|
615
|
+
debug.log("preview:measure:anchor-correct", {
|
|
616
|
+
anchorPartID: anchorCorrection.anchorPartID,
|
|
617
|
+
visualOffset: anchorCorrection.visualOffset,
|
|
618
|
+
correctedScrollTop,
|
|
619
|
+
window: correctedWindow,
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
pendingPreviewAnchorCorrection = undefined
|
|
624
|
+
}
|
|
625
|
+
}
|
|
561
626
|
setPreviewHeightVersion((value) => value + 1)
|
|
562
627
|
setTimeout(updatePreviewWindow, 1)
|
|
563
628
|
}
|
|
@@ -576,75 +641,166 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
576
641
|
onCleanup(() => clearTimeout(timer))
|
|
577
642
|
})
|
|
578
643
|
|
|
579
|
-
const loadPreviewBefore = (
|
|
644
|
+
const loadPreviewBefore = (pending: PendingPreviewBefore) => {
|
|
580
645
|
const item = selectedResult()
|
|
581
646
|
const first = previewParts()[0]
|
|
582
647
|
if (!item || !first || loadingPreviewMore() || prefetchingPreviewBefore()) {
|
|
583
648
|
debug.log("preview:load-before:skip", {
|
|
649
|
+
loadID: pending.id,
|
|
584
650
|
reason: !item ? "no-item" : !first ? "no-first-part" : loadingPreviewMore() ? "loading-preview-more" : "prefetching-before",
|
|
585
|
-
|
|
586
|
-
preserveScroll,
|
|
587
|
-
visibleLoad,
|
|
651
|
+
pending,
|
|
588
652
|
state: previewScrollState(),
|
|
589
653
|
})
|
|
590
654
|
return
|
|
591
655
|
}
|
|
592
656
|
const beforeState = previewScrollState()
|
|
593
|
-
|
|
657
|
+
const beforeLayout = previewVirtualLayout()
|
|
658
|
+
const scrollTop = previewScroll?.scrollTop ?? pending.previousScrollTop
|
|
659
|
+
const anchor = beforeLayout.find((row) => row.bottom >= scrollTop) ?? beforeLayout[0]
|
|
660
|
+
const anchorPartID = anchor?.part.id ?? first.id
|
|
661
|
+
const offsetInAnchor = anchor ? scrollTop - anchor.top : 0
|
|
662
|
+
pending.visibleLoad ? setLoadingPreviewMore(true) : setPrefetchingPreviewBefore(true)
|
|
594
663
|
debug.time("preview:load-before")
|
|
595
664
|
try {
|
|
596
665
|
debug.log("preview:load-before:start", {
|
|
597
|
-
|
|
666
|
+
loadID: pending.id,
|
|
667
|
+
intent: pending.intent,
|
|
598
668
|
cursor: { id: first.id, timeCreated: first.timeCreated },
|
|
599
|
-
previousContentHeight,
|
|
600
|
-
|
|
601
|
-
|
|
669
|
+
previousContentHeight: pending.previousContentHeight,
|
|
670
|
+
previousScrollTop: pending.previousScrollTop,
|
|
671
|
+
anchorPartID,
|
|
672
|
+
offsetInAnchor,
|
|
673
|
+
requestedAmount: pending.requestedAmount,
|
|
674
|
+
visibleLoad: pending.visibleLoad,
|
|
602
675
|
state: beforeState,
|
|
603
676
|
})
|
|
604
677
|
const page = loadConversationBefore(item, { id: first.id, timeCreated: first.timeCreated }, { limit: PREVIEW_PAGE_SIZE, dbPath: dbPath() })
|
|
605
|
-
debug.log("preview:load-before", {
|
|
606
|
-
|
|
678
|
+
debug.log("preview:load-before:query-done", {
|
|
679
|
+
loadID: pending.id,
|
|
607
680
|
added: page.parts.length,
|
|
608
681
|
hasMoreBefore: page.hasMoreBefore,
|
|
609
|
-
preserveScroll,
|
|
610
|
-
visibleLoad,
|
|
611
682
|
first: page.parts[0]?.id,
|
|
612
683
|
last: page.parts.at(-1)?.id,
|
|
613
684
|
})
|
|
614
685
|
if (page.parts.length > 0) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
686
|
+
const currentParts = previewParts()
|
|
687
|
+
const nextParts = [...page.parts, ...currentParts]
|
|
688
|
+
const nextLayout = buildPreviewLayout(nextParts)
|
|
689
|
+
const anchorPredicted = nextLayout.find((row) => row.part.id === anchorPartID)
|
|
690
|
+
const anchorPredictedTop = anchorPredicted?.top ?? 0
|
|
691
|
+
const visualOffset = offsetInAnchor - (pending.intent === "explicit-scroll" ? pending.requestedAmount : 0)
|
|
692
|
+
const targetScrollTop = pending.intent === "explicit-scroll"
|
|
693
|
+
? Math.max(0, anchorPredictedTop + offsetInAnchor - pending.requestedAmount)
|
|
694
|
+
: anchorPredictedTop + offsetInAnchor
|
|
695
|
+
const nextWindow = previewWindowForLayout(nextLayout, targetScrollTop, previewScroll?.height ?? Math.max(8, height() - 7))
|
|
696
|
+
|
|
697
|
+
solidBatch(() => {
|
|
698
|
+
setPreviewParts(nextParts)
|
|
699
|
+
setPreviewWindow(nextWindow)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
previewScroll?.scrollTo(targetScrollTop)
|
|
703
|
+
const anchorCorrectionCreatedAt = Date.now()
|
|
704
|
+
pendingPreviewAnchorCorrection = {
|
|
705
|
+
anchorPartID,
|
|
706
|
+
visualOffset,
|
|
707
|
+
createdAt: anchorCorrectionCreatedAt,
|
|
708
|
+
expiresAt: anchorCorrectionCreatedAt + 300,
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
debug.log("preview:load-before:commit", {
|
|
712
|
+
loadID: pending.id,
|
|
713
|
+
anchorPartID,
|
|
714
|
+
anchorPredictedTop,
|
|
715
|
+
offsetInAnchor,
|
|
716
|
+
visualOffset,
|
|
717
|
+
targetScrollTop,
|
|
718
|
+
window: nextWindow,
|
|
719
|
+
intent: pending.intent,
|
|
720
|
+
newParts: page.parts.length,
|
|
721
|
+
totalParts: nextParts.length,
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
previewBeforeAdjusting = true
|
|
725
|
+
setTimeout(() => {
|
|
726
|
+
const newLayout = previewVirtualLayout()
|
|
727
|
+
const anchorNew = newLayout.find(e => e.part.id === anchorPartID)
|
|
728
|
+
const actualAnchorTop = anchorNew?.top ?? anchorPredictedTop
|
|
729
|
+
const drift = actualAnchorTop - anchorPredictedTop
|
|
730
|
+
|
|
731
|
+
if (Math.abs(drift) >= 1 && previewScroll) {
|
|
732
|
+
const correctedScrollTop = targetScrollTop + drift
|
|
733
|
+
const correctedWindow = previewWindowForLayout(newLayout, correctedScrollTop, previewScroll.height)
|
|
734
|
+
setPreviewWindow(correctedWindow)
|
|
735
|
+
previewScroll.scrollTo(correctedScrollTop)
|
|
736
|
+
debug.log("preview:load-before:drift-correct", {
|
|
737
|
+
loadID: pending.id,
|
|
738
|
+
drift,
|
|
739
|
+
anchorPredictedTop,
|
|
740
|
+
actualAnchorTop,
|
|
741
|
+
correctedScrollTop,
|
|
742
|
+
window: correctedWindow,
|
|
629
743
|
})
|
|
630
|
-
}
|
|
631
|
-
} else {
|
|
632
|
-
setTimeout(() => {
|
|
744
|
+
} else {
|
|
633
745
|
updatePreviewWindow()
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
previewBeforeAdjusting = false
|
|
749
|
+
|
|
750
|
+
const coalesced = pendingCoalescedExplicit
|
|
751
|
+
if (coalesced) {
|
|
752
|
+
pendingCoalescedExplicit = undefined
|
|
753
|
+
const scroll = previewScroll
|
|
754
|
+
if (scroll) {
|
|
755
|
+
debug.log("preview:before:coalesce-replay", {
|
|
756
|
+
requestedAmount: coalesced.requestedAmount,
|
|
757
|
+
state: previewScrollState(),
|
|
758
|
+
})
|
|
759
|
+
schedulePreviewBefore({
|
|
760
|
+
intent: "explicit-scroll",
|
|
761
|
+
visibleLoad: true,
|
|
762
|
+
requestedAmount: coalesced.requestedAmount,
|
|
763
|
+
previousScrollTop: scroll.scrollTop,
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (pending.intent === "passive-prefetch") {
|
|
769
|
+
const matchItem = selectedResult()
|
|
770
|
+
const scroll = previewScroll
|
|
771
|
+
if (matchItem && scroll) {
|
|
772
|
+
const layout = previewVirtualLayout()
|
|
773
|
+
const matchRow = layout.find((r) => r.part.id === matchItem.id)
|
|
774
|
+
if (matchRow) {
|
|
775
|
+
const viewportHeight = scroll.height
|
|
776
|
+
const desiredScrollTop = Math.max(0, matchRow.top - Math.floor(viewportHeight / 3))
|
|
777
|
+
const msSinceManual = Date.now() - lastPreviewScrollKeyMs
|
|
778
|
+
if (Math.abs(scroll.scrollTop - desiredScrollTop) >= 2 && msSinceManual > 700) {
|
|
779
|
+
const correctedWindow = previewWindowForLayout(layout, desiredScrollTop, viewportHeight)
|
|
780
|
+
setPreviewWindow(correctedWindow)
|
|
781
|
+
scroll.scrollTo(desiredScrollTop)
|
|
782
|
+
debug.log("preview:load-before:recenter", {
|
|
783
|
+
matchPartID: matchItem.id,
|
|
784
|
+
matchTop: matchRow.top,
|
|
785
|
+
scrollTop: scroll.scrollTop,
|
|
786
|
+
desiredScrollTop,
|
|
787
|
+
viewportHeight,
|
|
788
|
+
window: correctedWindow,
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}, 1)
|
|
641
795
|
}
|
|
642
796
|
setHasMorePreviewBefore(page.hasMoreBefore)
|
|
643
797
|
} catch (err) {
|
|
644
798
|
debug.log("preview:load-before:error", err instanceof Error ? err.message : String(err))
|
|
799
|
+
previewBeforeAdjusting = false
|
|
645
800
|
} finally {
|
|
646
801
|
debug.timeEnd("preview:load-before")
|
|
647
|
-
|
|
802
|
+
lastBeforeLoadMs = Date.now()
|
|
803
|
+
pending.visibleLoad ? setLoadingPreviewMore(false) : setPrefetchingPreviewBefore(false)
|
|
648
804
|
}
|
|
649
805
|
}
|
|
650
806
|
|
|
@@ -690,46 +846,104 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
690
846
|
}
|
|
691
847
|
}
|
|
692
848
|
|
|
693
|
-
const schedulePreviewBefore = (
|
|
849
|
+
const schedulePreviewBefore = (opts: {
|
|
850
|
+
intent: PreviewBeforeIntent
|
|
851
|
+
visibleLoad?: boolean
|
|
852
|
+
previousContentHeight?: number
|
|
853
|
+
requestedAmount?: number
|
|
854
|
+
previousScrollTop?: number
|
|
855
|
+
}) => {
|
|
856
|
+
const id = ++previewBeforeLoadID
|
|
857
|
+
const previousContentHeight = opts.previousContentHeight ?? previewContentHeight()
|
|
858
|
+
const previousScrollTop = opts.previousScrollTop ?? previewScroll?.scrollTop ?? 0
|
|
859
|
+
const requestedAmount = opts.requestedAmount ?? 0
|
|
860
|
+
const visibleLoad = opts.visibleLoad ?? false
|
|
861
|
+
const source = opts.intent === "explicit-scroll" ? "key" : "edge"
|
|
694
862
|
if (!hasMorePreviewBefore() || loadingPreviewMore() || prefetchingPreviewBefore()) {
|
|
695
|
-
debug.log("preview:
|
|
696
|
-
|
|
863
|
+
debug.log("preview:before:schedule-skip", {
|
|
864
|
+
loadID: id,
|
|
865
|
+
reason: !hasMorePreviewBefore() ? "no-more-before" : loadingPreviewMore() ? "loading" : "prefetching",
|
|
866
|
+
source,
|
|
867
|
+
intent: opts.intent,
|
|
697
868
|
previousContentHeight,
|
|
698
|
-
|
|
869
|
+
previousScrollTop,
|
|
870
|
+
requestedAmount,
|
|
699
871
|
visibleLoad,
|
|
700
872
|
state: previewScrollState(),
|
|
701
873
|
})
|
|
702
874
|
return
|
|
703
875
|
}
|
|
876
|
+
if (previewBeforeAdjusting) {
|
|
877
|
+
if (opts.intent === "explicit-scroll") {
|
|
878
|
+
pendingCoalescedExplicit = { requestedAmount }
|
|
879
|
+
debug.log("preview:before:coalesce-explicit", {
|
|
880
|
+
loadID: id,
|
|
881
|
+
requestedAmount,
|
|
882
|
+
state: previewScrollState(),
|
|
883
|
+
})
|
|
884
|
+
} else {
|
|
885
|
+
debug.log("preview:before:schedule-skip", {
|
|
886
|
+
loadID: id,
|
|
887
|
+
reason: "adjusting",
|
|
888
|
+
source,
|
|
889
|
+
intent: opts.intent,
|
|
890
|
+
previousContentHeight,
|
|
891
|
+
previousScrollTop,
|
|
892
|
+
requestedAmount,
|
|
893
|
+
visibleLoad,
|
|
894
|
+
state: previewScrollState(),
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
return
|
|
898
|
+
}
|
|
704
899
|
if (previewBeforeTimer) {
|
|
705
900
|
if (pendingPreviewBefore) {
|
|
706
901
|
const previousPending = { ...pendingPreviewBefore }
|
|
707
|
-
pendingPreviewBefore.preserveScroll = pendingPreviewBefore.preserveScroll && preserveScroll
|
|
708
902
|
pendingPreviewBefore.visibleLoad = pendingPreviewBefore.visibleLoad || visibleLoad
|
|
709
|
-
|
|
903
|
+
if (opts.intent === "explicit-scroll" && pendingPreviewBefore.intent === "passive-prefetch") {
|
|
904
|
+
pendingPreviewBefore.intent = "explicit-scroll"
|
|
905
|
+
pendingPreviewBefore.requestedAmount = requestedAmount
|
|
906
|
+
pendingPreviewBefore.previousScrollTop = previousScrollTop
|
|
907
|
+
}
|
|
908
|
+
debug.log("preview:before:schedule-merge", {
|
|
909
|
+
loadID: id,
|
|
710
910
|
previousPending,
|
|
711
911
|
nextPending: pendingPreviewBefore,
|
|
712
|
-
requested: { previousContentHeight,
|
|
713
|
-
state: previewScrollState(),
|
|
912
|
+
requested: { source, intent: opts.intent, previousContentHeight, previousScrollTop, requestedAmount, visibleLoad },
|
|
714
913
|
})
|
|
715
914
|
} else {
|
|
716
|
-
debug.log("preview:
|
|
915
|
+
debug.log("preview:before:schedule-skip", {
|
|
916
|
+
loadID: id,
|
|
717
917
|
reason: "timer-already-set",
|
|
918
|
+
source,
|
|
919
|
+
intent: opts.intent,
|
|
718
920
|
previousContentHeight,
|
|
719
|
-
|
|
921
|
+
previousScrollTop,
|
|
922
|
+
requestedAmount,
|
|
720
923
|
visibleLoad,
|
|
721
924
|
state: previewScrollState(),
|
|
722
925
|
})
|
|
723
926
|
}
|
|
724
927
|
return
|
|
725
928
|
}
|
|
726
|
-
pendingPreviewBefore = { previousContentHeight,
|
|
727
|
-
debug.log("preview:
|
|
929
|
+
pendingPreviewBefore = { id, previousContentHeight, previousScrollTop, requestedAmount, intent: opts.intent, visibleLoad }
|
|
930
|
+
debug.log("preview:before:schedule", {
|
|
931
|
+
loadID: id,
|
|
932
|
+
source,
|
|
933
|
+
intent: opts.intent,
|
|
934
|
+
previousContentHeight,
|
|
935
|
+
previousScrollTop,
|
|
936
|
+
requestedAmount,
|
|
937
|
+
visibleLoad,
|
|
938
|
+
firstPart: previewParts()[0]?.id,
|
|
939
|
+
partCount: previewParts().length,
|
|
940
|
+
state: previewScrollState(),
|
|
941
|
+
})
|
|
728
942
|
previewBeforeTimer = setTimeout(() => {
|
|
729
943
|
const pending = pendingPreviewBefore
|
|
730
944
|
previewBeforeTimer = undefined
|
|
731
945
|
pendingPreviewBefore = undefined
|
|
732
|
-
if (pending) loadPreviewBefore(pending
|
|
946
|
+
if (pending) loadPreviewBefore(pending)
|
|
733
947
|
}, 1)
|
|
734
948
|
}
|
|
735
949
|
|
|
@@ -770,7 +984,24 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
770
984
|
debug.log("preview:target-scroll:estimated", { targetID, targetTop: row.top, scrollTop: scroll.scrollTop, nextScrollTop: top, window: previewWindow() })
|
|
771
985
|
scroll.scrollTo(top)
|
|
772
986
|
updatePreviewWindow()
|
|
773
|
-
|
|
987
|
+
const scrollTopBeforeReal = scroll.scrollTop
|
|
988
|
+
setTimeout(() => {
|
|
989
|
+
scrollPreviewToTarget(scroll, targetID)
|
|
990
|
+
const now = Date.now()
|
|
991
|
+
const adjustmentMs = lastPreviewScrollKeyMs
|
|
992
|
+
if (adjustmentMs > 0 && now - adjustmentMs > 300) {
|
|
993
|
+
const targetTopReal = row.top
|
|
994
|
+
const expectedScrollTop = Math.max(0, targetTopReal - Math.max(1, Math.floor(scroll.height / 3)))
|
|
995
|
+
const drift = scroll.scrollTop - expectedScrollTop
|
|
996
|
+
if (Math.abs(drift) >= 2) {
|
|
997
|
+
const correctedScrollTop = scroll.scrollTop - drift
|
|
998
|
+
const correctedWindow = previewWindowForLayout(previewVirtualLayout(), correctedScrollTop, scroll.height)
|
|
999
|
+
setPreviewWindow(correctedWindow)
|
|
1000
|
+
scroll.scrollTo(correctedScrollTop)
|
|
1001
|
+
debug.log("preview:target-scroll:real-correct", { targetID, drift, scrollTopAfterReal: scroll.scrollTop, correctedScrollTop, window: correctedWindow })
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}, 1)
|
|
774
1005
|
}
|
|
775
1006
|
|
|
776
1007
|
let lastPreviewItemId = ""
|
|
@@ -815,11 +1046,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
815
1046
|
debug.timeEnd("preview:load")
|
|
816
1047
|
})
|
|
817
1048
|
|
|
1049
|
+
let lastBeforeLoadMs = 0
|
|
1050
|
+
let lastPreviewScrollKeyMs = 0
|
|
818
1051
|
createEffect(() => {
|
|
819
1052
|
const item = selectedResult()
|
|
820
1053
|
if (!item) return
|
|
821
1054
|
const interval = setInterval(() => {
|
|
822
1055
|
if (loadingPreviewMore() || prefetchingPreviewBefore() || prefetchingPreviewAfter()) return
|
|
1056
|
+
if (previewBeforeAdjusting) return
|
|
823
1057
|
const scroll = previewScroll
|
|
824
1058
|
const children = scroll?.getChildren()
|
|
825
1059
|
if (!scroll || !children || children.length === 0) return
|
|
@@ -829,26 +1063,28 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
829
1063
|
const nearTop = scroll.scrollTop <= prefetchDistance
|
|
830
1064
|
const atBottom = scroll.scrollTop + scroll.height >= totalContentHeight - 1
|
|
831
1065
|
const nearBottom = scroll.scrollTop + scroll.height >= totalContentHeight - prefetchDistance
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1066
|
+
const msSinceManualScroll = Date.now() - lastPreviewScrollKeyMs
|
|
1067
|
+
const willLoadBefore = atTop && hasMorePreviewBefore() && (Date.now() - lastBeforeLoadMs) > 100 && msSinceManualScroll > 700
|
|
1068
|
+
debug.log("preview:edge:decision", {
|
|
1069
|
+
scrollTop: scroll.scrollTop,
|
|
1070
|
+
height: scroll.height,
|
|
1071
|
+
contentHeight: totalContentHeight,
|
|
1072
|
+
prefetchDistance,
|
|
1073
|
+
atTop,
|
|
1074
|
+
nearTop,
|
|
1075
|
+
atBottom,
|
|
1076
|
+
nearBottom,
|
|
1077
|
+
hasMoreBefore: hasMorePreviewBefore(),
|
|
1078
|
+
hasMoreAfter: hasMorePreviewAfter(),
|
|
1079
|
+
loadingBefore: loadingPreviewMore(),
|
|
1080
|
+
pendingBefore: Boolean(previewBeforeTimer),
|
|
1081
|
+
willLoadBefore,
|
|
1082
|
+
msSinceLastLoad: Date.now() - lastBeforeLoadMs,
|
|
1083
|
+
msSinceManualScroll,
|
|
1084
|
+
})
|
|
1085
|
+
if (willLoadBefore) {
|
|
1086
|
+
schedulePreviewBefore({ intent: "passive-prefetch", previousContentHeight: totalContentHeight, previousScrollTop: scroll.scrollTop })
|
|
850
1087
|
}
|
|
851
|
-
if (atTop && hasMorePreviewBefore()) schedulePreviewBefore(totalContentHeight, true, false)
|
|
852
1088
|
if (nearBottom && hasMorePreviewAfter()) schedulePreviewAfter(atBottom)
|
|
853
1089
|
}, 400)
|
|
854
1090
|
onCleanup(() => clearInterval(interval))
|
|
@@ -883,11 +1119,14 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
883
1119
|
}
|
|
884
1120
|
|
|
885
1121
|
const beforeState = previewScrollState()
|
|
1122
|
+
lastPreviewScrollKeyMs = Date.now()
|
|
886
1123
|
debug.log("preview:scroll-key", { direction, state: beforeState })
|
|
887
1124
|
|
|
1125
|
+
const requestedAmount = previewScrollAmount(scroll)
|
|
1126
|
+
|
|
888
1127
|
if (direction < 0 && scroll.scrollTop <= 0 && hasMorePreviewBefore()) {
|
|
889
|
-
debug.log("preview:scroll-key:load-before", { direction, state: beforeState })
|
|
890
|
-
schedulePreviewBefore(
|
|
1128
|
+
debug.log("preview:scroll-key:load-before", { direction, requestedAmount, state: beforeState })
|
|
1129
|
+
schedulePreviewBefore({ intent: "explicit-scroll", visibleLoad: true, requestedAmount, previousScrollTop: scroll.scrollTop })
|
|
891
1130
|
return
|
|
892
1131
|
}
|
|
893
1132
|
|
|
@@ -898,7 +1137,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
898
1137
|
return
|
|
899
1138
|
}
|
|
900
1139
|
|
|
901
|
-
const amount = direction *
|
|
1140
|
+
const amount = direction * requestedAmount
|
|
902
1141
|
scroll.scrollBy(amount)
|
|
903
1142
|
updatePreviewWindow()
|
|
904
1143
|
debug.log("preview:scroll-key:scroll", { direction, amount, before: beforeState, after: previewScrollState() })
|
|
@@ -1027,6 +1266,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
1027
1266
|
>
|
|
1028
1267
|
<Show when={!loading()}>
|
|
1029
1268
|
<Show when={results().length > 0} fallback={<EmptyState query={query()} owner={ownerLabel()} theme={theme()} />}>
|
|
1269
|
+
<box height={resultTopSpacerHeight()} flexShrink={0} />
|
|
1030
1270
|
<For each={resultRenderWindow().items}>
|
|
1031
1271
|
{(item, index) => {
|
|
1032
1272
|
const absoluteIndex = () => resultRenderWindow().start + index()
|
|
@@ -1043,6 +1283,7 @@ export const Telescope = (props: { api: TuiPluginApi; config: TelescopeConfig; o
|
|
|
1043
1283
|
)
|
|
1044
1284
|
}}
|
|
1045
1285
|
</For>
|
|
1286
|
+
<box height={resultBottomSpacerHeight()} flexShrink={0} />
|
|
1046
1287
|
<Show when={loadingMore()}>
|
|
1047
1288
|
<SkeletonRow theme={theme()} />
|
|
1048
1289
|
</Show>
|
package/ui/render-target.ts
CHANGED
|
@@ -31,18 +31,20 @@ export function scrollPreviewToTarget(scroll: ScrollBoxRenderable | undefined, t
|
|
|
31
31
|
})
|
|
32
32
|
return
|
|
33
33
|
}
|
|
34
|
-
const
|
|
34
|
+
const contentY = target.y + scroll.scrollTop - scroll.y
|
|
35
|
+
const desiredScrollTop = Math.max(0, contentY - Math.max(1, Math.floor(scroll.height / 3)))
|
|
35
36
|
debug.log("preview:target-scroll", {
|
|
36
37
|
targetID,
|
|
37
38
|
targetY: target.y,
|
|
38
39
|
scrollY: scroll.y,
|
|
39
40
|
scrollTop: scroll.scrollTop,
|
|
41
|
+
contentY,
|
|
42
|
+
desiredScrollTop,
|
|
40
43
|
scrollHeight: scroll.height,
|
|
41
44
|
contentHeight: scroll.scrollHeight,
|
|
42
|
-
delta,
|
|
43
45
|
})
|
|
44
|
-
scroll.
|
|
45
|
-
debug.log("preview:target-scroll:after", { targetID, scrollY: scroll.y, scrollTop: scroll.scrollTop })
|
|
46
|
+
scroll.scrollTo(desiredScrollTop)
|
|
47
|
+
debug.log("preview:target-scroll:after", { targetID, scrollY: scroll.y, scrollTop: scroll.scrollTop, desiredScrollTop })
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export function jumpToRenderedTarget(root: unknown, targetID: string) {
|