import type { DocumentMessage } from './types' import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { pageLayout } from './pageLayout' import { scrollOwner } from './scrollOwner' import { postMessage, viewState } from './vscode' /** 滚动时最多这么久记一次位置 */ const RECORD_INTERVAL_MS = 100 /** * 筛选栏有多高,跟着它变(换行、多出第二行时):钉在它下面的东西(总览条、钉住的卡片)从这儿开始。 * 大小变了从 ResizeObserver 报来的尺寸里取,不回头读它的 offsetHeight(那要先排版)。 */ export function usePinnedHeight(): number { const [height, setHeight] = useState(0) useLayoutEffect(() => { const bar = document.querySelector('.cr-filter-bar') if (!bar) return setHeight(bar.offsetHeight) const observer = new ResizeObserver(([entry]) => setHeight(entry.borderBoxSize[0]?.blockSize ?? bar.offsetHeight)) observer.observe(bar) return () => observer.disconnect() }, []) return height } /** 记下视口顶部那块;它的起始行变了才报给扩展端。 */ function record(reportedLine: { current: number | undefined }): void { const anchor = pageLayout.anchorAt(window.scrollY + pageLayout.pinnedHeight()) if (!anchor) return viewState.update({ scroll: anchor }) if (anchor.line !== reportedLine.current) { reportedLine.current = anchor.line postMessage({ type: 'visibleLine', line: anchor.line }) } } /** * 记住并恢复预览看到的位置: * ① 滚动时([scrollOwner] 转来的滚动事件)把视口顶部那块记进 webview 状态,页面重新加载后(VS Code 重启后恢复标签页、webview 进程崩了重建) * 交给 [scrollOwner] 滚回去、钉住那块,之后注释里的图画出来、中文名到了也不跑; * ② 同时把那块的起始行报给扩展端,切回文本编辑器时把这行放到顶部。 * 第一份文档带 reveal(刚从文本编辑器切过来、跳到定义)时不恢复,交给 useReveal 滚。 * 块的位置从 pageLayout 取:滚动时用量过的,不每次量全部卡片。 */ 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 if (doc.reveal === undefined && saved) scrollOwner.align(saved) record(reportedLine) }, [doc]) useEffect(() => { let timer: number | undefined const off = scrollOwner.onScroll(() => { timer ??= window.setTimeout(() => { timer = undefined record(reportedLine) }, RECORD_INTERVAL_MS) }) return () => { off() window.clearTimeout(timer) } }, []) }