91 lines
3.7 KiB
TypeScript
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,
|
|
)
|
|
}
|