Skip to content

Commit 86ef2a5

Browse files
committed
File viewer: Fix loading very long files
Bumped into a WebKit virtual scrolling issue, went around it
1 parent d15ecde commit 86ef2a5

1 file changed

Lines changed: 33 additions & 27 deletions

File tree

apps/desktop/src/routes/viewer/+page.svelte

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
const LINE_HEIGHT = 18
1010
const BUFFER_LINES = 50
1111
const FETCH_BATCH = 500
12+
// WebKit caps element height at ~2^25 px (33.5M). Stay well below to avoid scroll cutoff.
13+
const MAX_SCROLL_HEIGHT = 30_000_000
1214
const SEARCH_POLL_INTERVAL = 100
1315
// Must match INDEXING_TIMEOUT_SECS in src-tauri/src/file_viewer/session.rs
1416
const INDEXING_TIMEOUT_SECS = 5
@@ -28,7 +30,6 @@
2830
viewerSetupMenu,
2931
viewerSetWordWrap,
3032
type ViewerSearchMatch,
31-
type BackendCapabilities,
3233
} from '$lib/tauri-commands'
3334
import { getCurrentWindow } from '@tauri-apps/api/window'
3435
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
@@ -47,7 +48,6 @@
4748
let loading = $state(true)
4849
let sessionId = $state('')
4950
let backendType = $state<'fullLoad' | 'byteSeek' | 'lineIndex'>('fullLoad')
50-
let capabilities = $state<BackendCapabilities | null>(null)
5151
let isIndexing = $state(false)
5252
5353
// Line cache: lineNumber -> text (sparse - may have gaps)
@@ -68,10 +68,19 @@
6868
let avgWrappedLineHeight = $state(LINE_HEIGHT)
6969
const effectiveLineHeight = $derived(wordWrap ? avgWrappedLineHeight : LINE_HEIGHT)
7070
71+
// Scroll compression: when total content height exceeds browser limits, scale down
72+
// the scroll coordinate system. Each scroll pixel then covers more than one real line.
73+
// scrollLineHeight = pixels per line in scroll space (may be < effectiveLineHeight for huge files).
74+
const scrollScale = $derived.by(() => {
75+
const fullHeight = estimatedTotalLines() * effectiveLineHeight
76+
return fullHeight > MAX_SCROLL_HEIGHT ? MAX_SCROLL_HEIGHT / fullHeight : 1
77+
})
78+
const scrollLineHeight = $derived(effectiveLineHeight * scrollScale)
79+
7180
// Derived: which lines are visible
72-
const visibleFrom = $derived(Math.max(0, Math.floor(scrollTop / effectiveLineHeight) - BUFFER_LINES))
81+
const visibleFrom = $derived(Math.max(0, Math.floor(scrollTop / scrollLineHeight) - BUFFER_LINES))
7382
const visibleTo = $derived(
74-
Math.min(estimatedTotalLines(), Math.ceil((scrollTop + viewportHeight) / effectiveLineHeight) + BUFFER_LINES),
83+
Math.min(estimatedTotalLines(), Math.ceil((scrollTop + viewportHeight) / scrollLineHeight) + BUFFER_LINES),
7584
)
7685
const visibleLines = $derived(getVisibleLines())
7786
const gutterWidth = $derived(String(estimatedTotalLines()).length)
@@ -201,19 +210,19 @@
201210
}
202211
})
203212
204-
// Compensate scroll position when effectiveLineHeight changes (toggle or measurement update).
205-
// Proportional adjustment keeps the same line visible: ratio * scrollTop preserves the line
206-
// at the top of the viewport, and ON ratio * OFF ratio = 1.0 so there's zero cumulative drift.
207-
let prevEffectiveLineHeight = LINE_HEIGHT
213+
// Compensate scroll position when scrollLineHeight changes (word wrap toggle, measurement
214+
// update, or scroll scale change from totalLines update). Ratio adjustment preserves the
215+
// line at the top of the viewport.
216+
let prevScrollLineHeight = LINE_HEIGHT
208217
$effect(() => {
209-
const newHeight = effectiveLineHeight
210-
if (!contentRef || prevEffectiveLineHeight === newHeight) {
211-
prevEffectiveLineHeight = newHeight
218+
const newSLH = scrollLineHeight
219+
if (!contentRef || prevScrollLineHeight === newSLH) {
220+
prevScrollLineHeight = newSLH
212221
return
213222
}
214-
const ratio = newHeight / prevEffectiveLineHeight
223+
const ratio = newSLH / prevScrollLineHeight
215224
contentRef.scrollTop = Math.round(contentRef.scrollTop * ratio)
216-
prevEffectiveLineHeight = newHeight
225+
prevScrollLineHeight = newSLH
217226
})
218227
219228
function scheduleFetch(from: number, to: number) {
@@ -234,8 +243,8 @@
234243
totalLines = newTotal
235244
return
236245
}
237-
// Calculate current scroll fraction before updating
238-
const oldHeight = oldEstimate * effectiveLineHeight
246+
// Calculate current scroll fraction before updating (use capped heights for accuracy)
247+
const oldHeight = Math.min(oldEstimate * effectiveLineHeight, MAX_SCROLL_HEIGHT)
239248
const scrollFraction = contentRef.scrollTop / oldHeight
240249
log.debug('totalLines changed: {oldEstimate} -> {newTotal}, preserving scroll fraction {fraction}', {
241250
oldEstimate,
@@ -244,7 +253,7 @@
244253
})
245254
totalLines = newTotal
246255
// Restore scroll fraction after DOM updates using rAF to run after browser's scroll clamping
247-
const newHeight = newTotal * effectiveLineHeight
256+
const newHeight = Math.min(newTotal * effectiveLineHeight, MAX_SCROLL_HEIGHT)
248257
const newScrollTop = Math.round(scrollFraction * newHeight)
249258
const ref = contentRef // Capture reference for rAF callback
250259
requestAnimationFrame(() => {
@@ -465,9 +474,8 @@
465474
// Convert via byte offset: (byteOffset / totalBytes) * estimatedTotalLines
466475
targetLine = totalBytes > 0 ? (match.byteOffset / totalBytes) * estimatedTotalLines() : match.line
467476
}
468-
const targetScroll = targetLine * effectiveLineHeight - viewportHeight / 2
469-
const finalScroll = Math.max(0, targetScroll)
470-
contentRef.scrollTop = finalScroll
477+
const targetScroll = targetLine * scrollLineHeight - viewportHeight / 2
478+
contentRef.scrollTop = Math.max(0, targetScroll)
471479
}
472480
473481
// Debounce search input
@@ -529,14 +537,15 @@
529537
530538
function scrollByLines(lines: number) {
531539
if (contentRef) {
532-
contentRef.scrollTop = Math.max(0, contentRef.scrollTop + lines * effectiveLineHeight)
540+
contentRef.scrollTop = Math.max(0, contentRef.scrollTop + lines * scrollLineHeight)
533541
}
534542
}
535543
536544
function scrollByPages(pages: number) {
537545
if (contentRef) {
538-
const pageSize = contentRef.clientHeight - effectiveLineHeight // Overlap by 1 line
539-
contentRef.scrollTop = Math.max(0, contentRef.scrollTop + pages * pageSize)
546+
// Advance by one screenful of lines (minus 1 for overlap context)
547+
const linesPerPage = Math.floor(contentRef.clientHeight / effectiveLineHeight) - 1
548+
contentRef.scrollTop = Math.max(0, contentRef.scrollTop + pages * linesPerPage * scrollLineHeight)
540549
}
541550
}
542551
@@ -748,7 +757,6 @@
748757
totalLines = result.totalLines
749758
estimatedLines = result.estimatedTotalLines
750759
backendType = result.backendType
751-
capabilities = result.capabilities
752760
isIndexing = result.isIndexing
753761
754762
log.debug(
@@ -889,14 +897,12 @@
889897
>
890898
<div
891899
class="scroll-spacer"
892-
style="height: {estimatedTotalLines() * effectiveLineHeight}px; min-width: {wordWrap
893-
? 0
894-
: contentWidth}px"
900+
style="height: {estimatedTotalLines() * scrollLineHeight}px; min-width: {wordWrap ? 0 : contentWidth}px"
895901
>
896902
<div
897903
class="lines-container"
898904
bind:this={linesContainerRef}
899-
style="transform: translateY({visibleFrom * effectiveLineHeight}px)"
905+
style="transform: translateY({visibleFrom * scrollLineHeight}px)"
900906
>
901907
{#each visibleLines as { lineNumber, text } (lineNumber)}
902908
<div class="line" data-line={lineNumber}>

0 commit comments

Comments
 (0)