import type { BlockBox } from './scrollPosition' import type { DocumentMessage } from './types' import { useEffect, useLayoutEffect, useRef } from 'react' import { anchorAt, scrollTopFor } from './scrollPosition' import { postMessage, viewState } from './vscode' /** 滚动时最多这么久记一次位置 */ const RECORD_INTERVAL_MS = 100 /** 页面上每块(BlockCard 带 data-line)的位置,按页面上的先后。 */ export function measureBlocks(): BlockBox[] { const y = window.scrollY return Array.from(document.querySelectorAll('.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 状态,页面因标签页切走被重新加载后照着滚回去; * ② 同时把那块的起始行报给扩展端,切回文本编辑器时把这行放到顶部。 * 第一份文档带 reveal(刚从文本编辑器切过来、跳到定义)时不恢复,交给 useReveal 滚。 */ export function useScrollPosition(doc: DocumentMessage | null): void { const restored = useRef(false) const reportedLine = useRef(undefined) // 第一份文档渲染进 DOM 后、浏览器绘制前滚好,不会先闪一下顶部 useLayoutEffect(() => { if (!doc || restored.current) return restored.current = true const saved = viewState.get().scroll const top = doc.reveal === undefined && saved ? scrollTopFor(measureBlocks(), saved) : undefined 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) } }, []) }