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

91 lines
3.7 KiB
TypeScript

import type { ReactElement } from 'react'
import type { BlockTreeState } from './BlockTree'
import type { ShownPeek } from './definitionPeek'
import type { DefinitionPeek } from './types'
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { BlockLevel } from './BlockTree'
import { PEEK_ATTRIBUTE, placePeek } from './definitionPeek'
import { DisplayNameContext, nameMap } from './displayNames'
import { MarkdownKindContext } from './TextPreview'
import { HighlightContext } from './useOccurrences'
import { collapsedIds } from './viewState'
const NONE: ReadonlySet<string> = new Set()
const NOT_COVERED: ReadonlyMap<string, string> = new Map()
/**
* 浮窗里的那张卡片:只读的块树(不编辑、不问 AI、不重写注释、不标差异、不筛选),折叠展开只记在浮窗里,默认同页面(类、函数收起子块)。
* 名字的下标是那个文件的:中文名换成卡片带来的,不画页面上的高亮;Markdown 不认不带路径的引用。
*/
export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement {
const [folds, setFolds] = useState<Record<string, boolean>>({})
const collapsed = useMemo(() => collapsedIds([peek.block], folds), [peek, folds])
const names = useMemo(() => nameMap(peek.names), [peek])
const tree: BlockTreeState = {
collapsed,
hidden: NONE,
temporary: NONE,
covered: NOT_COVERED,
kindName: kind => kind,
onToggleFold: id => setFolds(prev => ({ ...prev, [id]: !collapsed.has(id) })),
onSetFolds: next => setFolds(prev => ({ ...prev, ...next })),
// 不标差异就没有「只看改动」折起来的块
onOpenRun: () => {},
}
return (
<HighlightContext value={undefined}>
<DisplayNameContext value={names}>
<MarkdownKindContext value="peek">
<BlockLevel blocks={[peek.block]} tree={tree} />
</MarkdownKindContext>
</DisplayNameContext>
</HighlightContext>
)
}
/**
* 定义卡片浮窗:标题一行「相对路径:行号」,点了跳到定义(同 ⌘ / Ctrl + 点击);下面是卡片,太大时在浮窗里滚。
* 外框用 VS Code 悬停框的颜色;portal 到 body,先在左上角量出大小,再落到名字下方或上方(placePeek)。
*/
export function DefinitionPeekPopup({ shown, onJump }: { shown: ShownPeek<HTMLElement> | undefined, onJump: () => void }): ReactElement | null {
const popupRef = useRef<HTMLDivElement | null>(null)
useLayoutEffect(() => {
const popup = popupRef.current
if (!shown || !popup)
return
Object.assign(popup.style, { top: '0px', left: '0px', bottom: '', maxWidth: '', maxHeight: '', visibility: 'hidden' })
const place = placePeek(shown.anchor.getBoundingClientRect(), popup.getBoundingClientRect(), { width: window.innerWidth, height: window.innerHeight })
Object.assign(popup.style, {
top: 'top' in place ? `${place.top}px` : '',
bottom: 'bottom' in place ? `${place.bottom}px` : '',
left: `${place.left}px`,
maxWidth: `${place.maxWidth}px`,
maxHeight: `${place.maxHeight}px`,
visibility: 'visible',
})
}, [shown])
if (!shown)
return null
const { peek } = shown
const where = `${peek.path}:${peek.line + 1}`
return createPortal(
<div
ref={popupRef}
{...{ [PEEK_ATTRIBUTE]: '' }}
role="dialog"
aria-label={`定义 ${where}`}
className="cr-peek"
style={{ top: 0, left: 0, visibility: 'hidden' }}
>
<button type="button" className="cr-peek-head" data-tooltip="跳到定义" onClick={onJump}>{where}</button>
<div className="cr-peek-body">
<PeekTree key={`${where}#${peek.block.id}`} peek={peek} />
</div>
</div>,
document.body,
)
}