Files
superwork/vscode/webview-ui/src/codeReader/StickyLines.tsx
T

105 lines
4.8 KiB
TypeScript

import type { ReactElement, RefObject } from 'react'
import type { BlockView, StickyScrollSettings } from './types'
import { useContext, useLayoutEffect, useMemo } from 'react'
import { KindIcon } from './kindIcons'
import { SegmentText, TextMarksContext } from './SegmentText'
import { selectStore } from './store'
import { flash } from './useReveal'
import { pinnedHeight, usePinnedHeight } from './useScrollPosition'
import { STICKY_LINE_HEIGHT, stickyInset, useStickyLines } from './useStickyScroll'
/** 块 id → 块,连同各层子块 */
function blocksById(blocks: readonly BlockView[]): Map<string, BlockView> {
const byId = new Map<string, BlockView>()
const visit = (list: readonly BlockView[]): void => {
for (const block of list) {
byId.set(block.id, block)
visit(block.children)
}
}
visit(blocks)
return byId
}
/**
* 钉住的一行里卡片的头:折叠箭头那格留空、概念图标、标题,各自的位置同卡片上的,一行放不下省略。
* 标题同卡片上的写法(中文名照换):属性表的第一行(名称)的值,或一行译文;Markdown 只取第一行。
*/
export function PinnedHead({ block }: { block: BlockView }): ReactElement {
const title = 'properties' in block ? block.properties[0]?.value : block.translation
return (
<div className="flex h-full min-w-0 items-center gap-2.5 pr-2.5 pl-[9px]">
<span className="w-3.5 shrink-0" />
<KindIcon concepts={block.concepts} />
<p className="cr-code cr-sticky-title min-w-0 flex-1">
{title && ('markdown' in title
? <span className="cr-role-comment">{title.markdown.split('\n', 1)[0].trim()}</span>
: <SegmentText segments={title} />)}
</p>
</div>
)
}
/**
* 钉在顶上的卡片(同 VS Code 编辑器的 Sticky Scroll,设置也用它的,一张卡片就是一层):卡片的开头已经滚到筛选栏后面、卡片(连同子块)
* 还有一部分看得见时,筛选栏下面钉一行这张卡片的头(PinnedHead),外层的在上、里层的往下摞,每行和那张卡片左右对齐(越里层越往右);
* 这张卡片快滚完时,最里那行被下面的内容顶上去、从上一行下面滑走。哪几张见 pinnedLines,只按页面上画出来的算:
* 折叠着的卡片自己照样钉(它的子块没画出来,不算);被筛掉的不在页面上,正在编辑的不钉。点一行把那张卡片滚到顶上(外层还钉着的几行下面)、闪几下。
* 钉着的名字不画页面上的高亮(页面上找当前那一处只认卡片里的);跳到定义、悬停看卡片照旧。
* content 是块树所在的页面内容,blocks 是这份文档的块。
*/
export function StickyLines({ content, blocks, settings }: { content: RefObject<HTMLElement | null>, blocks: readonly BlockView[], settings: StickyScrollSettings }): ReactElement | null {
const top = usePinnedHeight()
const lines = useStickyLines(content, settings.maxLineCount)
const byId = useMemo(() => blocksById(blocks), [blocks])
// 钉着的名字照换中文名,不画页面上的高亮
const marks = useContext(TextMarksContext)
const unhighlighted = useMemo(() => selectStore(marks, ({ names }) => ({ names })), [marks])
const count = lines.scopes.length
const height = count * STICKY_LINE_HEIGHT + lines.offset
// 浏览器自己滚的时候(输入框拿到焦点、打字跟着光标走)也让开筛选栏和钉着的几行:问 AI 的输入框、块的编辑框不被盖住
useLayoutEffect(() => {
const page = document.documentElement
page.style.scrollPaddingTop = `${top + height}px`
return () => {
page.style.scrollPaddingTop = ''
}
}, [top, height])
if (count === 0)
return null
const jump = (id: string): void => {
const card = content.current?.querySelector<HTMLElement>(`.cr-block[data-id="${id}"]`)
if (!card)
return
window.scrollTo(0, card.getBoundingClientRect().top + window.scrollY - pinnedHeight() - stickyInset(card, settings))
flash(card)
}
return (
<TextMarksContext value={unhighlighted}>
<div className="cr-sticky" style={{ top, height }}>
{lines.scopes.map((scope, index) => {
const block = byId.get(scope.id)
const last = index === count - 1
return block && (
<div
key={scope.id}
className="cr-sticky-line"
data-last={last ? '' : undefined}
style={{ top: index * STICKY_LINE_HEIGHT + (last ? lines.offset : 0), height: STICKY_LINE_HEIGHT }}
onClick={() => jump(scope.id)}
>
<div className="h-full" style={{ marginLeft: scope.left, width: scope.right - scope.left }}>
<PinnedHead block={block} />
</div>
</div>
)
})}
</div>
</TextMarksContext>
)
}