✨ feat(vscode): 代码阅读预览顶部加块筛选栏
语言插件按和语言无关的 id 报块属性(通用的由宿主从 facts 推),翻译插件起名,webview 只按 id 筛; 筛选整个工作区共用、记在 workspaceState;JavaScript 不再显示「抽象:否」「仅类型:否」
This commit is contained in:
1 parent
4eaf2ae11d
commit
1c7e31c51a
29 files changed
+1364
-319
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BlockTreeState } from './BlockTree'
|
||||
import type { DocumentMessage, Reveal } from './types'
|
||||
import type { DocumentMessage, Excluded, Reveal } from './types'
|
||||
import type { ViewState } from './viewState'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { SelectMenu } from '@/components/ui/select-menu'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { BlockLevel } from './BlockTree'
|
||||
import { applyFilter, keptWhileEditing } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
import { useDefinitionLinks } from './useDefinitionLinks'
|
||||
import { useReveal } from './useReveal'
|
||||
import { useScrollPosition } from './useScrollPosition'
|
||||
@@ -14,6 +15,8 @@ import { onMessage, postMessage, viewState } from './vscode'
|
||||
|
||||
type BlockModes = Pick<ViewState, 'editing' | 'folds'>
|
||||
|
||||
const NONE: ReadonlySet<string> = new Set()
|
||||
|
||||
function toggled(ids: string[], id: string): string[] {
|
||||
return ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id]
|
||||
}
|
||||
@@ -29,16 +32,24 @@ export function App(): ReactElement | null {
|
||||
const [focusId, setFocusId] = useState<string | null>(null)
|
||||
/** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */
|
||||
const [reveal, setReveal] = useState<Reveal | null>(null)
|
||||
/** 扩展端发来的块筛选(整个工作区共用);本地不改,改了发给扩展端、等它发回来 */
|
||||
const [excluded, setExcluded] = useState<Excluded>({})
|
||||
/** 跳到定义时临时露出来的块(目标和它的各层祖先),筛选一变、文档一刷新就回到筛选状态 */
|
||||
const [keep, setKeep] = useState<ReadonlySet<string>>(NONE)
|
||||
/** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */
|
||||
const editing = useRef(view.editing)
|
||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds])
|
||||
const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep])
|
||||
/** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */
|
||||
const setFolded = useCallback((ids: string[], folded: boolean) => setView(prev => ({
|
||||
...prev,
|
||||
folds: { ...prev.folds, ...Object.fromEntries(ids.map(id => [id, folded])) },
|
||||
})), [])
|
||||
const expand = useCallback((ids: string[]) => setFolded(ids, false), [setFolded])
|
||||
const show = useCallback((ids: string[]) => setKeep(prev => new Set([...prev, ...ids])), [])
|
||||
const revealed = useCallback(() => setReveal(null), [])
|
||||
useScrollPosition(doc)
|
||||
useReveal(doc, reveal, collapsed, expand, revealed)
|
||||
useReveal(doc, reveal, collapsed, filter.hidden, show, expand, revealed)
|
||||
useDefinitionLinks(doc?.version)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,11 +57,16 @@ export function App(): ReactElement | null {
|
||||
switch (msg.type) {
|
||||
case 'document':
|
||||
setDoc(msg)
|
||||
setKeep(prev => keptWhileEditing(prev, editing.current))
|
||||
if (msg.reveal)
|
||||
setReveal(msg.reveal)
|
||||
return
|
||||
case 'reveal':
|
||||
setReveal({ line: msg.line, definition: msg.definition })
|
||||
return
|
||||
case 'filter':
|
||||
setExcluded(msg.excluded)
|
||||
setKeep(NONE)
|
||||
}
|
||||
})
|
||||
postMessage({ type: 'ready' })
|
||||
@@ -59,13 +75,18 @@ export function App(): ReactElement | null {
|
||||
|
||||
useEffect(() => {
|
||||
viewState.update(view)
|
||||
editing.current = view.editing
|
||||
}, [view])
|
||||
|
||||
const onFocused = useCallback(() => setFocusId(null), [])
|
||||
const kindNames = useMemo(() => new Map(doc?.attributes.find(attribute => attribute.id === 'kind')?.values.map(value => [value.id, value.name])), [doc])
|
||||
const tree = useMemo((): BlockTreeState | null => doc && {
|
||||
version: doc.version,
|
||||
editing: new Set(view.editing),
|
||||
collapsed,
|
||||
hidden: filter.hidden,
|
||||
temporary: filter.temporary,
|
||||
kindName: kind => kindNames.get(kind) ?? kind,
|
||||
focusId,
|
||||
onFocused,
|
||||
onToggleEdit: (id) => {
|
||||
@@ -73,40 +94,41 @@ export function App(): ReactElement | null {
|
||||
setView(prev => ({ ...prev, editing: toggled(prev.editing, id) }))
|
||||
},
|
||||
onToggleFold: id => setFolded([id], !collapsed.has(id)),
|
||||
}, [doc, view, collapsed, focusId, onFocused, setFolded])
|
||||
}, [doc, view, collapsed, filter, kindNames, focusId, onFocused, setFolded])
|
||||
|
||||
if (!doc || !tree)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||
{doc.translators.length > 1 && (
|
||||
<div className="mb-3 flex items-center justify-end gap-2">
|
||||
<span className="text-xs text-[var(--vscode-descriptionForeground)]">翻译</span>
|
||||
<SelectMenu
|
||||
value={doc.translatorId}
|
||||
options={doc.translators.map(t => ({ value: t.id, label: t.label }))}
|
||||
onChange={id => postMessage({ type: 'selectTranslator', id })}
|
||||
title="选择翻译方式"
|
||||
className="w-40 text-left"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{doc.error !== undefined && (
|
||||
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
|
||||
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||
<div className="min-w-0">
|
||||
<div className="break-words">
|
||||
解析失败:
|
||||
{doc.error}
|
||||
<>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
view={filter}
|
||||
onChange={next => postMessage({ type: 'setFilter', excluded: next })}
|
||||
translators={doc.translators}
|
||||
translatorId={doc.translatorId}
|
||||
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
||||
/>
|
||||
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||
{doc.error !== undefined && (
|
||||
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
|
||||
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||
<div className="min-w-0">
|
||||
<div className="break-words">
|
||||
解析失败:
|
||||
{doc.error}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{doc.blocks.length > 0
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: doc.error === undefined && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>}
|
||||
</div>
|
||||
)}
|
||||
{doc.blocks.length === 0
|
||||
? doc.error === undefined && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>
|
||||
: doc.blocks.some(block => !filter.hidden.has(block.id))
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user