🐛 fix(vscode): 代码阅读预览记住浏览位置,编辑 / 预览互切位置对齐

This commit is contained in:
cruldra committed 2026-09-25 03:21:14 +08:00
1 parent 6897954644
commit 37306bd23e
13 files changed
+409 -37

No files matched your search

@@ -0,0 +1,70 @@
import type { BlockBox } from './scrollPosition'
import type { DocumentMessage } from './types'
import { useEffect, useLayoutEffect, useRef } from 'react'
import { anchorAt, blockForLine, scrollTopFor } from './scrollPosition'
import { postMessage, viewState } from './vscode'
/** 滚动时最多这么久记一次位置 */
const RECORD_INTERVAL_MS = 100
/** 页面上每块(BlockCard 带 data-line)的位置,按页面上的先后。 */
function measureBlocks(): BlockBox[] {
const y = window.scrollY
return Array.from(document.querySelectorAll<HTMLElement>('.cr-block[data-line]'), (el) => {
const rect = el.getBoundingClientRect()
return { line: Number(el.dataset.line), top: rect.top + y, bottom: rect.bottom + y }
})
}
/** 记下视口顶部那块;它的起始行变了才报给扩展端。 */
function record(reportedLine: { current: number | undefined }): void {
const anchor = anchorAt(measureBlocks(), window.scrollY)
if (!anchor)
return
viewState.update({ scroll: anchor })
if (anchor.line !== reportedLine.current) {
reportedLine.current = anchor.line
postMessage({ type: 'visibleLine', line: anchor.line })
}
}
/**
* 记住并恢复预览看到的位置:
* ① 滚动时把视口顶部那块记进 webview 状态,页面因标签页切走被重新加载后照着滚回去;
* ② 同时把那块的起始行报给扩展端,切回文本编辑器时把这行放到顶部;
* ③ 第一份文档带 revealLine(刚从文本编辑器切过来)时优先滚到包含这行的块。
*/
export function useScrollPosition(doc: DocumentMessage | null): void {
const restored = useRef(false)
const reportedLine = useRef<number | undefined>(undefined)
// 第一份文档渲染进 DOM 后、浏览器绘制前滚好,不会先闪一下顶部
useLayoutEffect(() => {
if (!doc || restored.current)
return
restored.current = true
const boxes = measureBlocks()
const saved = viewState.get().scroll
const top = doc.revealLine !== undefined
? blockForLine(boxes, doc.revealLine)?.top
: saved && scrollTopFor(boxes, saved)
if (top !== undefined)
window.scrollTo(0, top)
record(reportedLine)
}, [doc])
useEffect(() => {
let timer: number | undefined
const onScroll = (): void => {
timer ??= window.setTimeout(() => {
timer = undefined
record(reportedLine)
}, RECORD_INTERVAL_MS)
}
window.addEventListener('scroll', onScroll, { passive: true })
return () => {
window.removeEventListener('scroll', onScroll)
window.clearTimeout(timer)
}
}, [])
}