✨ feat(vscode): 代码阅读预览标出新增 / 修改 / 删除的卡片(左侧改动色条、新增淡绿底挂小标、删掉的块原位收成一行可展开),筛选栏第二行写对比基准和改动统计,新文件顶部横幅
This commit is contained in:
1 parent
2a5806c6b5
commit
3b7ce69842
10 files changed
+487
-16
No files matched your search
@@ -1,15 +1,17 @@
|
|||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import type { BlockTreeState } from './BlockTree'
|
import type { BlockTreeState } from './BlockTree'
|
||||||
import type { NameMap, VersionNames } from './displayNames'
|
import type { NameMap, VersionNames } from './displayNames'
|
||||||
import type { DocumentMessage, Excluded, Reveal } from './types'
|
import type { DocumentDiff, DocumentMessage, Excluded, Reveal } from './types'
|
||||||
import type { OccurrencePage } from './useOccurrences'
|
import type { OccurrencePage } from './useOccurrences'
|
||||||
import type { ViewState } from './viewState'
|
import type { ViewState } from './viewState'
|
||||||
import { TriangleAlert } from 'lucide-react'
|
import { TriangleAlert } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BlockLevel } from './BlockTree'
|
import { BlockLevel } from './BlockTree'
|
||||||
|
import { DiffBar, NewFileBanner } from './DiffBar'
|
||||||
|
import { diffTree, openedByDiff } from './diffView'
|
||||||
import { acceptNames, DisplayNameContext, documentNames } from './displayNames'
|
import { acceptNames, DisplayNameContext, documentNames } from './displayNames'
|
||||||
import { coveredByEditing, startEditing } from './editing'
|
import { coveredByEditing, startEditing } from './editing'
|
||||||
import { applyFilter, keptWhileEditing } from './filter'
|
import { applyFilter, excludedBy, keptWhileEditing } from './filter'
|
||||||
import { FilterBar } from './FilterBar'
|
import { FilterBar } from './FilterBar'
|
||||||
import { useDefinitionLinks } from './useDefinitionLinks'
|
import { useDefinitionLinks } from './useDefinitionLinks'
|
||||||
import { HighlightContext, useOccurrences } from './useOccurrences'
|
import { HighlightContext, useOccurrences } from './useOccurrences'
|
||||||
@@ -43,9 +45,15 @@ export function App(): ReactElement | null {
|
|||||||
const [keep, setKeep] = useState<ReadonlySet<string>>(NONE)
|
const [keep, setKeep] = useState<ReadonlySet<string>>(NONE)
|
||||||
/** 页面上这个文档版本的中文名:文档带来的,扩展端查到了再换 */
|
/** 页面上这个文档版本的中文名:文档带来的,扩展端查到了再换 */
|
||||||
const [names, setNames] = useState<VersionNames | undefined>()
|
const [names, setNames] = useState<VersionNames | undefined>()
|
||||||
|
/** 这份文件相比对比基准改了什么:侧栏开着「未提交的更改」或审查工单时扩展端算好了补来,不比时是 null */
|
||||||
|
const [diff, setDiff] = useState<DocumentDiff | null>(null)
|
||||||
/** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */
|
/** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */
|
||||||
const editing = useRef(view.editing)
|
const editing = useRef(view.editing)
|
||||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds])
|
// 解析失败的文档没有块,不标差异;删掉的块按块筛选同样藏
|
||||||
|
const changes = useMemo(() => doc && doc.error === undefined
|
||||||
|
? diffTree(doc.blocks, diff, doc.version, removed => !excludedBy(removed.block, excluded))
|
||||||
|
: undefined, [doc, diff, excluded])
|
||||||
|
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds, openedByDiff(changes)), [doc, view.folds, changes])
|
||||||
const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep])
|
const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep])
|
||||||
const covered = useMemo(() => coveredByEditing(doc?.blocks ?? [], new Set(view.editing)), [doc, view.editing])
|
const covered = useMemo(() => coveredByEditing(doc?.blocks ?? [], new Set(view.editing)), [doc, view.editing])
|
||||||
/** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */
|
/** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */
|
||||||
@@ -88,6 +96,9 @@ export function App(): ReactElement | null {
|
|||||||
case 'filter':
|
case 'filter':
|
||||||
setExcluded(msg.excluded)
|
setExcluded(msg.excluded)
|
||||||
setKeep(NONE)
|
setKeep(NONE)
|
||||||
|
return
|
||||||
|
case 'diff':
|
||||||
|
setDiff(msg.diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
postMessage({ type: 'ready' })
|
postMessage({ type: 'ready' })
|
||||||
@@ -121,7 +132,8 @@ export function App(): ReactElement | null {
|
|||||||
},
|
},
|
||||||
onToggleFold: id => setFolded([id], !collapsed.has(id)),
|
onToggleFold: id => setFolded([id], !collapsed.has(id)),
|
||||||
...(qa.state.available && { qa }),
|
...(qa.state.available && { qa }),
|
||||||
}, [doc, view, collapsed, filter, covered, kindName, focusId, onFocused, setFolded, qa])
|
...(changes && { diff: changes }),
|
||||||
|
}, [doc, view, collapsed, filter, covered, kindName, focusId, onFocused, setFolded, qa, changes])
|
||||||
|
|
||||||
if (!doc || !tree)
|
if (!doc || !tree)
|
||||||
return null
|
return null
|
||||||
@@ -138,8 +150,10 @@ export function App(): ReactElement | null {
|
|||||||
translators={doc.translators}
|
translators={doc.translators}
|
||||||
translatorId={doc.translatorId}
|
translatorId={doc.translatorId}
|
||||||
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
||||||
|
below={diff && doc.error === undefined && <DiffBar diff={diff} />}
|
||||||
/>
|
/>
|
||||||
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
|
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||||
|
{diff?.newFile !== undefined && doc.error === undefined && <NewFileBanner detail={diff.newFile} />}
|
||||||
{doc.error !== undefined && (
|
{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">
|
<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)]" />
|
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { CSSProperties, ReactElement } from 'react'
|
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||||||
|
import type { DiffTree, PlacedRemoved } from './diffView'
|
||||||
import type { QaState } from './qa'
|
import type { QaState } from './qa'
|
||||||
import type { PendingImage } from './qaImages'
|
import type { PendingImage } from './qaImages'
|
||||||
import type { BlockView } from './types'
|
import type { BlockView } from './types'
|
||||||
import { ChevronDown, ChevronRight, EyeOff } from 'lucide-react'
|
import { ChevronDown, ChevronRight, EyeOff } from 'lucide-react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { AskThread } from './AskThread'
|
import { AskThread } from './AskThread'
|
||||||
import { BlockTools } from './BlockTools'
|
import { BlockTools } from './BlockTools'
|
||||||
import { CodeEditor } from './CodeEditor'
|
import { CodeEditor } from './CodeEditor'
|
||||||
|
import { removedTitle } from './diffView'
|
||||||
import { KindIcon } from './kindIcons'
|
import { KindIcon } from './kindIcons'
|
||||||
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
||||||
import { asksOf, historyOf } from './qa'
|
import { asksOf, historyOf } from './qa'
|
||||||
@@ -33,6 +35,8 @@ export interface BlockTreeState {
|
|||||||
onToggleFold: (id: string) => void
|
onToggleFold: (id: string) => void
|
||||||
/** 问 AI;文件不在工作区文件夹里(问答没地方存)时没有 */
|
/** 问 AI;文件不在工作区文件夹里(问答没地方存)时没有 */
|
||||||
qa?: QaTree
|
qa?: QaTree
|
||||||
|
/** 代码差异:新增、修改的块和删掉的块;侧栏开着「未提交的更改」或审查工单、基准里有这个文件时才有 */
|
||||||
|
diff?: DiffTree
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 问 AI 的状态和动作,块按自己的记号(anchor)取 */
|
/** 问 AI 的状态和动作,块按自己的记号(anchor)取 */
|
||||||
@@ -48,9 +52,23 @@ export interface QaTree {
|
|||||||
onSelectProfile: (id: string) => void
|
onSelectProfile: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 块下面有没有画出来的子块:子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。 */
|
/** 块下面有没有画出来的子块(删掉的子块也算):子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。 */
|
||||||
function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean {
|
function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean {
|
||||||
return block.children.some(child => shown(child, tree))
|
return block.children.some(child => shown(child, tree)) || (!tree.covered.has(block.id) && tree.diff?.removed.has(block.id) === true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGE_NAMES = { add: '新增', mod: '修改', del: '删除' } as const
|
||||||
|
|
||||||
|
/** 标差异时卡片左边外侧的改动色条(没变的留一条透明的,同一层照样对齐);不标差异时原样。 */
|
||||||
|
function ChangeRow({ tree, change, children }: { tree: BlockTreeState, change: keyof typeof CHANGE_NAMES | undefined, children: ReactNode }): ReactElement {
|
||||||
|
if (!tree.diff)
|
||||||
|
return <>{children}</>
|
||||||
|
return (
|
||||||
|
<div className="cr-change-row flex gap-[5px]">
|
||||||
|
<span className="cr-change-bar" data-change={change} data-tooltip={change && CHANGE_NAMES[change]} />
|
||||||
|
<div className="min-w-0 flex-1">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 块画不画:被筛掉的、被正在编辑的祖先盖住的都不画。 */
|
/** 块画不画:被筛掉的、被正在编辑的祖先盖住的都不画。 */
|
||||||
@@ -74,21 +92,35 @@ function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data-
|
|||||||
/**
|
/**
|
||||||
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉、被盖住的块不画,不留空位,竖线跟着变短。
|
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉、被盖住的块不画,不留空位,竖线跟着变短。
|
||||||
* owner 是这一层所属的块(顶层没有),悬停它时竖线换成它的种类色。
|
* owner 是这一层所属的块(顶层没有),悬停它时竖线换成它的种类色。
|
||||||
|
* 标差异时每张卡片左边外侧一条改动色条,删掉的块收成一行放回原来的位置(见 RemovedCard)。
|
||||||
*/
|
*/
|
||||||
export function BlockLevel({ blocks, owner, tree }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState }): ReactElement {
|
export function BlockLevel({ blocks, owner, tree }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState }): ReactElement {
|
||||||
|
const removed = tree.diff?.removed.get(owner?.id ?? '') ?? []
|
||||||
|
// 删掉的块排在它原来前面那块后面;那块被筛掉了也照排,不跟着消失
|
||||||
|
const removedAfter = (id: string | undefined): ReactElement[] => removed
|
||||||
|
.filter(item => item.after === id)
|
||||||
|
.map(item => <RemovedCard key={item.id} placed={item} tree={tree} />)
|
||||||
return (
|
return (
|
||||||
<div className="cr-level flex gap-2" {...kindProps(owner)}>
|
<div className="cr-level flex gap-2" {...kindProps(owner)}>
|
||||||
<div className="flex w-3 shrink-0 justify-center">
|
<div className="flex w-3 shrink-0 justify-center">
|
||||||
<div className="cr-guide w-px" />
|
<div className="cr-guide w-px" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||||
{blocks.filter(block => shown(block, tree)).map(block => (
|
{removedAfter(undefined)}
|
||||||
|
{blocks.map(block => (
|
||||||
<Fragment key={block.id}>
|
<Fragment key={block.id}>
|
||||||
|
{shown(block, tree) && (
|
||||||
|
<>
|
||||||
|
<ChangeRow tree={tree} change={tree.diff?.changes.get(block.id)?.change}>
|
||||||
<BlockCard block={block} tree={tree} />
|
<BlockCard block={block} tree={tree} />
|
||||||
|
</ChangeRow>
|
||||||
{tree.qa && <BlockQa key={block.anchor} anchor={block.anchor} qa={tree.qa} />}
|
{tree.qa && <BlockQa key={block.anchor} anchor={block.anchor} qa={tree.qa} />}
|
||||||
{hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && (
|
{hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && (
|
||||||
<BlockLevel blocks={block.children} owner={block} tree={tree} />
|
<BlockLevel blocks={block.children} owner={block} tree={tree} />
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{removedAfter(block.id)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -139,14 +171,17 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
|||||||
const collapsed = tree.collapsed.has(block.id)
|
const collapsed = tree.collapsed.has(block.id)
|
||||||
const hasChildren = hasShownChildren(block, tree)
|
const hasChildren = hasShownChildren(block, tree)
|
||||||
const Chevron = collapsed ? ChevronRight : ChevronDown
|
const Chevron = collapsed ? ChevronRight : ChevronDown
|
||||||
|
const change = tree.diff?.changes.get(block.id)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="cr-block flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
|
className="cr-block relative flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
|
||||||
{...kindProps(block)}
|
{...kindProps(block)}
|
||||||
data-id={block.id}
|
data-id={block.id}
|
||||||
data-line={block.startLine}
|
data-line={block.startLine}
|
||||||
data-editing={editing ? '' : undefined}
|
data-editing={editing ? '' : undefined}
|
||||||
|
data-change={change?.change}
|
||||||
>
|
>
|
||||||
|
{change?.outermost && <span className="cr-added-badge">新增</span>}
|
||||||
{hasChildren
|
{hasChildren
|
||||||
? (
|
? (
|
||||||
<button
|
<button
|
||||||
@@ -189,3 +224,44 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删掉的一块,放在原来的位置:红色虚线边、半透明,没有编辑、问 AI 按钮。默认收成一行「已删除 · 种类 名字」,
|
||||||
|
* 点箭头或这一行展开,下面是旧内容(整张划掉,名字不能跳定义);子块不画。
|
||||||
|
*/
|
||||||
|
function RemovedCard({ placed, tree }: { placed: PlacedRemoved, tree: BlockTreeState }): ReactElement {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { block, kindName } = placed.removed
|
||||||
|
const title = removedTitle(block)
|
||||||
|
const Chevron = open ? ChevronDown : ChevronRight
|
||||||
|
const toggle = (): void => setOpen(value => !value)
|
||||||
|
return (
|
||||||
|
<ChangeRow tree={tree} change="del">
|
||||||
|
<div className="cr-block cr-removed flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2" {...kindProps(block)} data-id={placed.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex h-5 w-3.5 shrink-0 items-center justify-center rounded-sm text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)]"
|
||||||
|
aria-label={open ? '收起删掉的内容' : '展开删掉的内容'}
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<Chevron size={14} strokeWidth={1.8} />
|
||||||
|
</button>
|
||||||
|
<KindIcon block={block} name={kindName} />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="cr-removed-head cursor-pointer truncate text-[12.5px] leading-5" onClick={toggle}>
|
||||||
|
<span className="cr-removed-label">已删除 · </span>
|
||||||
|
{kindName}
|
||||||
|
{title && <span className="cr-removed-title">{` ${title}`}</span>}
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div className="cr-removed-old mt-1">
|
||||||
|
{/* 旧块的 id 是旧版本里的位置,换成页面上认删掉的块用的 id,属性行的 id 才不和现在的块撞上 */}
|
||||||
|
<BlockPreview block={{ ...block, id: placed.id }} tree={tree} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ChangeRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
|
import type { DocumentDiff } from './types'
|
||||||
|
import { FilePlus, GitCompare } from 'lucide-react'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { statsParts } from './diffView'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 筛选栏第二行(标差异时才有):对比基准(「对比:上一次提交」),改动统计(「新增 2 · 修改 3 · 删除 1」,各按改动色,
|
||||||
|
* 没有的那种不写,都没有写「没有改动」)。基准里没有这个文件时不写统计,看横幅。
|
||||||
|
*/
|
||||||
|
export function DiffBar({ diff }: { diff: DocumentDiff }): ReactElement {
|
||||||
|
const parts = statsParts(diff.stats)
|
||||||
|
return (
|
||||||
|
<div className="mx-auto box-content flex h-[30px] max-w-[880px] items-center gap-2 px-6 text-xs whitespace-nowrap text-[var(--vscode-descriptionForeground)]">
|
||||||
|
<GitCompare size={14} strokeWidth={1.8} className="shrink-0" aria-hidden />
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
对比:
|
||||||
|
<span className="text-[var(--vscode-foreground)]">{diff.base}</span>
|
||||||
|
</span>
|
||||||
|
{diff.newFile === undefined && (
|
||||||
|
<>
|
||||||
|
<span className="opacity-60" aria-hidden>·</span>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
{parts.length === 0
|
||||||
|
? '没有改动'
|
||||||
|
: parts.map((part, index) => (
|
||||||
|
<Fragment key={part.change}>
|
||||||
|
{index > 0 && <span aria-hidden>·</span>}
|
||||||
|
<span className="cr-diff-stat" data-change={part.change}>{part.text}</span>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 基准里没有这个文件(新文件、未跟踪的):预览顶部一条横幅,卡片不再逐张标绿。 */
|
||||||
|
export function NewFileBanner({ detail }: { detail: string }): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="cr-new-file mb-3 flex items-center gap-2 rounded-md px-3 py-[7px]" role="status">
|
||||||
|
<FilePlus size={16} strokeWidth={1.8} className="shrink-0" aria-hidden />
|
||||||
|
<span className="shrink-0 text-[12.5px] leading-[18px] font-medium text-[var(--vscode-foreground)]">新文件,全部是新增</span>
|
||||||
|
<span className="min-w-0 text-xs leading-[18px] text-[var(--vscode-descriptionForeground)]">{detail}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FocusEvent, KeyboardEvent, ReactElement } from 'react'
|
import type { FocusEvent, KeyboardEvent, ReactElement, ReactNode } from 'react'
|
||||||
import type { Facet, FilterView } from './filter'
|
import type { Facet, FilterView } from './filter'
|
||||||
import type { Highlight } from './occurrences'
|
import type { Highlight } from './occurrences'
|
||||||
import type { Excluded, FileAttribute, TranslatorOption } from './types'
|
import type { Excluded, FileAttribute, TranslatorOption } from './types'
|
||||||
@@ -18,13 +18,15 @@ export interface FilterBarProps {
|
|||||||
translators: TranslatorOption[]
|
translators: TranslatorOption[]
|
||||||
translatorId: string
|
translatorId: string
|
||||||
onSelectTranslator: (id: string) => void
|
onSelectTranslator: (id: string) => void
|
||||||
|
/** 栏里接在这一行下面的(标差异时的第二行,见 DiffBar) */
|
||||||
|
below?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预览顶部钉住的一条:「筛选」、这份文件里每个能筛的属性一个下拉框、右边汇总(高亮着名字时先是「名字 · 2/5」,再是共 N 块 / 已隐藏 N 块 · 清除),
|
* 预览顶部钉住的一条:「筛选」、这份文件里每个能筛的属性一个下拉框、右边汇总(高亮着名字时先是「名字 · 2/5」,再是共 N 块 / 已隐藏 N 块 · 清除),
|
||||||
* 多于一种翻译时最右边是翻译下拉框。内容和代码块同宽对齐,滚动时块从它下面过去。
|
* 多于一种翻译时最右边是翻译下拉框;标差异时下面再接一行(below)。内容和代码块同宽对齐,滚动时块从它下面过去。
|
||||||
*/
|
*/
|
||||||
export function FilterBar({ attributes, excluded, view, highlight, onChange, translators, translatorId, onSelectTranslator }: FilterBarProps): ReactElement {
|
export function FilterBar({ attributes, excluded, view, highlight, onChange, translators, translatorId, onSelectTranslator, below }: FilterBarProps): ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="cr-filter-bar">
|
<div className="cr-filter-bar">
|
||||||
<div className="mx-auto box-content flex max-w-[880px] flex-wrap items-center gap-2 px-6 py-1.5">
|
<div className="mx-auto box-content flex max-w-[880px] flex-wrap items-center gap-2 px-6 py-1.5">
|
||||||
@@ -72,6 +74,7 @@ export function FilterBar({ attributes, excluded, view, highlight, onChange, tra
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{below}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -839,3 +839,95 @@ body:is(.vscode-light, .vscode-high-contrast-light) .cr-colored { color: var(--c
|
|||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 代码差异:配色取 VS Code 编辑器行号旁的改动色带(新增绿、修改蓝、删除红),主题没给时用 Dark Modern 的默认值;
|
||||||
|
* 行内的字增删用 diff 编辑器的插入 / 删除文字底色
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
--cr-diff-add: var(--vscode-editorGutter-addedBackground, #2ea043);
|
||||||
|
--cr-diff-mod: var(--vscode-editorGutter-modifiedBackground, #0078d4);
|
||||||
|
--cr-diff-del: var(--vscode-editorGutter-deletedBackground, #f85149);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片左边外侧的改动色条:3px 宽、和卡片等高;没变的卡片留一条透明的,同一层的卡片照样对齐 */
|
||||||
|
.cr-change-bar {
|
||||||
|
width: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 1.5px;
|
||||||
|
}
|
||||||
|
.cr-change-bar[data-change="add"] { background: var(--cr-diff-add); }
|
||||||
|
.cr-change-bar[data-change="mod"] { background: var(--cr-diff-mod); }
|
||||||
|
.cr-change-bar[data-change="del"] { background: var(--cr-diff-del); }
|
||||||
|
/* 卡片包了一层色条以后不再和子层相邻:悬停卡片时子层竖线换色改按外层认;问答区往右缩到和卡片对齐 */
|
||||||
|
.cr-change-row:has(> div > .cr-block:hover) + .cr-level > * > .cr-guide,
|
||||||
|
.cr-change-row:has(> div > .cr-block:hover) + .cr-qa + .cr-level > * > .cr-guide {
|
||||||
|
background: var(--kind);
|
||||||
|
}
|
||||||
|
.cr-change-row + .cr-qa {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增的卡片(连同它的子卡片):底色淡绿;最外面那张左上角压着「新增」小标 */
|
||||||
|
.cr-block[data-change="add"] {
|
||||||
|
background: color-mix(in srgb, var(--cr-diff-add) 10%, transparent);
|
||||||
|
}
|
||||||
|
.cr-block[data-change="add"]:hover {
|
||||||
|
background: color-mix(in srgb, var(--cr-diff-add) 16%, transparent);
|
||||||
|
}
|
||||||
|
.cr-added-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 6px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--cr-diff-add);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删掉的块:红色虚线边、淡红底、半透明,没有编辑、问 AI 按钮;展开后下面是旧内容,整张划掉 */
|
||||||
|
.cr-removed {
|
||||||
|
border: 1px dashed color-mix(in srgb, var(--cr-diff-del) 80%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--cr-diff-del) 6%, transparent);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.cr-removed:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.cr-removed-head {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
.cr-removed-head:focus-visible {
|
||||||
|
outline: 1px solid var(--vscode-focusBorder);
|
||||||
|
outline-offset: 1px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.cr-removed-label {
|
||||||
|
color: var(--cr-diff-del);
|
||||||
|
}
|
||||||
|
.cr-removed-title {
|
||||||
|
color: var(--cr-function);
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace);
|
||||||
|
}
|
||||||
|
.cr-removed-old,
|
||||||
|
.cr-removed-old * {
|
||||||
|
text-decoration-line: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选栏第二行:对比基准、改动统计、「只看改动」、上一处 / 下一处 */
|
||||||
|
.cr-diff-stat[data-change="add"] { color: var(--cr-diff-add); }
|
||||||
|
.cr-diff-stat[data-change="mod"] { color: var(--cr-diff-mod); }
|
||||||
|
.cr-diff-stat[data-change="del"] { color: var(--cr-diff-del); }
|
||||||
|
|
||||||
|
/* 基准里没有这个文件:顶部一条横幅,卡片不再逐张标绿 */
|
||||||
|
.cr-new-file {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--cr-diff-add) 50%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--cr-diff-add) 14%, transparent);
|
||||||
|
}
|
||||||
|
.cr-new-file > svg {
|
||||||
|
color: var(--cr-diff-add);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { BlockView, DocumentDiff, RemovedView } from './types'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { diffTree, openedByDiff, removedTitle, statsParts } from './diffView'
|
||||||
|
|
||||||
|
function block(id: string, anchor: string, children: BlockView[] = [], extra: Partial<BlockView> = {}): BlockView {
|
||||||
|
return { id, anchor, kind: 'function', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'function' }, children, translation: [], ...extra } as BlockView
|
||||||
|
}
|
||||||
|
|
||||||
|
function removed(parent: string, after: string | undefined, kind = 'function'): RemovedView {
|
||||||
|
return { parent, ...(after !== undefined && { after }), block: block('9', `${kind} gone`, [], { kind }), line: 3, kindName: '函数' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function diff(parts: Partial<DocumentDiff>): DocumentDiff {
|
||||||
|
return { base: '上一次提交', version: 2, blocks: {}, removed: [], stats: { add: 0, mod: 0, del: 0 }, lineCount: 10, ...parts }
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOCKS = [
|
||||||
|
block('0', 'function a', [block('0.0', 'function a > return @1')]),
|
||||||
|
block('1', 'function b'),
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('diffTree', () => {
|
||||||
|
it('块按记号认成页面上的 id;页面上找不到的记号(文档刚改)不标', () => {
|
||||||
|
const tree = diffTree(BLOCKS, diff({ blocks: { 'function a': { change: 'mod' }, 'function a > return @1': { change: 'mod', own: true }, 'function gone': { change: 'add' } } }), 2, () => true)
|
||||||
|
expect([...tree!.changes]).toEqual([['0', { change: 'mod' }], ['0.0', { change: 'mod', own: true }]])
|
||||||
|
expect(tree!.current).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('页面上不是差异算的那一版时,卡片里的标记不画', () => {
|
||||||
|
expect(diffTree(BLOCKS, diff({}), 3, () => true)?.current).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('删掉的块按层挂:父块、排在后面的那块换成页面上的 id,找不到的不画,被筛掉的不画', () => {
|
||||||
|
const tree = diffTree(BLOCKS, diff({ removed: [
|
||||||
|
removed('', 'function b'),
|
||||||
|
removed('function a', undefined),
|
||||||
|
removed('function missing', undefined),
|
||||||
|
removed('', 'function missing'),
|
||||||
|
removed('', undefined, 'import'),
|
||||||
|
] }), 2, item => item.block.kind !== 'import')
|
||||||
|
expect(tree!.removed.get('')).toEqual([{ id: 'removed-0', after: '1', removed: removed('', 'function b') }])
|
||||||
|
expect(tree!.removed.get('0')).toEqual([{ id: 'removed-1', removed: removed('function a', undefined) }])
|
||||||
|
expect(tree!.removed.size).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不比、基准里没有这个文件时不逐块标', () => {
|
||||||
|
expect(diffTree(BLOCKS, null, 2, () => true)).toBeUndefined()
|
||||||
|
expect(diffTree(BLOCKS, diff({ newFile: '上一次提交里没有这个文件' }), 2, () => true)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('标了改动的块默认展开', () => {
|
||||||
|
const tree = diffTree(BLOCKS, diff({ blocks: { 'function b': { change: 'add', outermost: true } } }), 2, () => true)
|
||||||
|
expect([...openedByDiff(tree)]).toEqual(['1'])
|
||||||
|
expect(openedByDiff(undefined).size).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('statsParts', () => {
|
||||||
|
it('没有的那种不写', () => {
|
||||||
|
expect(statsParts({ add: 2, mod: 0, del: 1 })).toEqual([{ change: 'add', text: '新增 2' }, { change: 'del', text: '删除 1' }])
|
||||||
|
expect(statsParts({ add: 0, mod: 0, del: 0 })).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('removedTitle', () => {
|
||||||
|
it('属性表取第一行的值,一行译文取译文,只取第一行', () => {
|
||||||
|
expect(removedTitle(block('0', 'x', [], { properties: [{ label: '名称', value: [{ text: '_is_owner', role: 'function' }] }] } as Partial<BlockView>))).toBe('_is_owner')
|
||||||
|
expect(removedTitle(block('0', 'x', [], { translation: [{ text: '调用 f\n接着', role: 'plain' }] }))).toBe('调用 f')
|
||||||
|
expect(removedTitle(block('0', 'x', [], { kind: 'comment', translation: { markdown: '说明第一行\n第二行' } }))).toBe('说明第一行')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type { BlockView, ChangedBlock, DiffStats, DocumentDiff, RemovedView } from './types'
|
||||||
|
import { inlineText } from './segments'
|
||||||
|
|
||||||
|
/** 页面上怎么标差异:按块 id 取改动,按层取删掉的块。基准里没有这个文件(整份新增)时不逐块标,没有它。 */
|
||||||
|
export interface DiffTree {
|
||||||
|
/** 按块 id:新增、修改的块 */
|
||||||
|
changes: ReadonlyMap<string, ChangedBlock>
|
||||||
|
/** 按父块 id(顶层 ''):这一层删掉的块,照差异里的先后 */
|
||||||
|
removed: ReadonlyMap<string, PlacedRemoved[]>
|
||||||
|
/** 页面上正是差异算的那一版:卡片里的标记按下标认,只在这时画 */
|
||||||
|
current: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 放在页面上的一块删掉的块:id 是页面上认它用的(`removed-<序号>`,不会和块 id、属性行 id 撞上),after 是排在哪块(id)后面,最前面时没有。 */
|
||||||
|
export interface PlacedRemoved {
|
||||||
|
id: string
|
||||||
|
after?: string
|
||||||
|
removed: RemovedView
|
||||||
|
}
|
||||||
|
|
||||||
|
function* allBlocks(blocks: readonly BlockView[]): Generator<BlockView> {
|
||||||
|
for (const block of blocks) {
|
||||||
|
yield block
|
||||||
|
yield* allBlocks(block.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 差异(块按记号认)→ 页面上的块 id:记号在页面上找不到的(文档刚改、差异还没重算)不标;删掉的块挂的父块、排在后面的那块找不到的也不画。
|
||||||
|
* shown 挑删掉的块画不画(被筛选藏掉的种类不画)。
|
||||||
|
*/
|
||||||
|
export function diffTree(blocks: readonly BlockView[], diff: DocumentDiff | null, version: number, shown: (removed: RemovedView) => boolean): DiffTree | undefined {
|
||||||
|
if (!diff || diff.newFile !== undefined)
|
||||||
|
return undefined
|
||||||
|
const ids = new Map([...allBlocks(blocks)].map(block => [block.anchor, block.id]))
|
||||||
|
const changes = new Map<string, ChangedBlock>()
|
||||||
|
for (const [anchor, change] of Object.entries(diff.blocks)) {
|
||||||
|
const id = ids.get(anchor)
|
||||||
|
if (id !== undefined)
|
||||||
|
changes.set(id, change)
|
||||||
|
}
|
||||||
|
const removed = new Map<string, PlacedRemoved[]>()
|
||||||
|
diff.removed.forEach((item, index) => {
|
||||||
|
const parent = item.parent === '' ? '' : ids.get(item.parent)
|
||||||
|
const after = item.after === undefined ? undefined : ids.get(item.after)
|
||||||
|
if (parent === undefined || (item.after !== undefined && after === undefined) || !shown(item))
|
||||||
|
return
|
||||||
|
removed.set(parent, [...removed.get(parent) ?? [], { id: `removed-${index}`, ...(after !== undefined && { after }), removed: item }])
|
||||||
|
})
|
||||||
|
return { changes, removed, current: diff.version === version }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标了改动的块默认展开(函数、类本来默认收起子块):改了什么一眼能看到。 */
|
||||||
|
export function openedByDiff(tree: DiffTree | undefined): ReadonlySet<string> {
|
||||||
|
return new Set(tree?.changes.keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「新增 2 · 修改 3 · 删除 1」的各段:没有的那种不写。 */
|
||||||
|
export function statsParts(stats: DiffStats): { change: keyof DiffStats, text: string }[] {
|
||||||
|
const labels: Record<keyof DiffStats, string> = { add: '新增', mod: '修改', del: '删除' }
|
||||||
|
return (['add', 'mod', 'del'] as const).filter(change => stats[change] > 0).map(change => ({ change, text: `${labels[change]} ${stats[change]}` }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删掉的块收起时写的名字:属性表是第一行的值(函数名、变量名、导入的来源),一行译文是译文;只取第一行。 */
|
||||||
|
export function removedTitle(block: BlockView): string {
|
||||||
|
const text = 'properties' in block
|
||||||
|
? block.properties[0] && ('markdown' in block.properties[0].value ? block.properties[0].value.markdown : inlineText(block.properties[0].value))
|
||||||
|
: 'markdown' in block.translation ? block.translation.markdown : inlineText(block.translation)
|
||||||
|
return (text ?? '').split('\n', 1)[0].trim()
|
||||||
|
}
|
||||||
@@ -258,6 +258,96 @@ export interface AnswerEnd {
|
|||||||
entryId?: string
|
entryId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 一截是新加的还是删掉的;没变的不写 */
|
||||||
|
export type PartChange = 'add' | 'del'
|
||||||
|
|
||||||
|
/** 着色的词里的一截:没变的、新加的(带源码下标),或删掉的(取自旧版本,不带下标) */
|
||||||
|
export interface InlinePart { segments: Inline[], change?: PartChange }
|
||||||
|
|
||||||
|
/** Markdown 说明里的一截:按词切,中文按字 */
|
||||||
|
export interface MarkdownPart { text: string, change?: PartChange }
|
||||||
|
|
||||||
|
/** 一段预览文字改成了什么样:着色的词逐截标(改得多就是整段旧值 → 新值),Markdown 说明逐词 / 字标 */
|
||||||
|
export type TextDiff = { inline: InlinePart[] } | { markdown: MarkdownPart[] }
|
||||||
|
|
||||||
|
/** 删掉的一行 / 子行:旧的那行,和它排在新的第几行后面(-1 是最前面) */
|
||||||
|
export interface Removed<T> { after: number, value: T }
|
||||||
|
|
||||||
|
/** 一层子行(参数表、导入项、字典的各项、摊开实参的表)的改动:changed 按新子行的下标,没变的不写 */
|
||||||
|
export interface ItemsDiff {
|
||||||
|
changed: Record<number, ItemDiff>
|
||||||
|
removed: Removed<PropertyItem>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一个子行的改动:整行新加的(它下面各层也都是),或按列标的格子、下一层、套着的表 */
|
||||||
|
export interface ItemDiff {
|
||||||
|
added?: true
|
||||||
|
cells?: Record<number, InlinePart[]>
|
||||||
|
items?: ItemsDiff
|
||||||
|
table?: ItemsDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 属性表各行的改动:changed 按新属性表的行下标,没变的不写 */
|
||||||
|
export interface RowsDiff {
|
||||||
|
changed: Record<number, RowDiff>
|
||||||
|
removed: Removed<PropertyRow>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 属性表一行的改动:整行新加的(它下面的子行也都是),或值、子行改了 */
|
||||||
|
export interface RowDiff {
|
||||||
|
added?: true
|
||||||
|
value?: TextDiff
|
||||||
|
items?: ItemsDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一块自己的内容(不算子块)改了哪些:属性表逐行,一行译文逐截,摊开实参的表逐行,说明逐词 */
|
||||||
|
export interface BlockMarks {
|
||||||
|
rows?: RowsDiff
|
||||||
|
translation?: TextDiff
|
||||||
|
table?: ItemsDiff
|
||||||
|
doc?: TextDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一块的改动:add 新增(outermost 是新增的最外面那块,挂「新增」小标;子孙跟着算新增),mod 修改(own 是这块自己的内容或头部变了,
|
||||||
|
* 不只是子块变了;marks 是卡片里要标的地方)
|
||||||
|
*/
|
||||||
|
export interface ChangedBlock {
|
||||||
|
change: 'add' | 'mod'
|
||||||
|
outermost?: true
|
||||||
|
own?: true
|
||||||
|
marks?: BlockMarks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删掉的一块,放回原来的位置:parent 是父块的记号(顶层 ''),after 是排在这一层哪块后面(新块的记号,最前面时没有);
|
||||||
|
* block 是旧版本里的这块(只带它自己的内容,词不带源码下标),line 是它在新文件里的位置(0 起),kindName 是种类的名字
|
||||||
|
*/
|
||||||
|
export interface RemovedView {
|
||||||
|
parent: string
|
||||||
|
after?: string
|
||||||
|
block: BlockView
|
||||||
|
line: number
|
||||||
|
kindName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「新增 N · 修改 N · 删除 N」:只数最外层带标记的块 */
|
||||||
|
export interface DiffStats { add: number, mod: number, del: number }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这份文件相比对比基准改了什么:base 是「对比:」后面的字;块按记号(anchor)认,卡片里的标记只在页面上正是 version 这一版时画;
|
||||||
|
* newFile 是基准里没有这个文件时横幅上的说明(这时不逐块标);lineCount 是新文件一共几行,总览条按它排刻度
|
||||||
|
*/
|
||||||
|
export interface DocumentDiff {
|
||||||
|
base: string
|
||||||
|
version: number
|
||||||
|
newFile?: string
|
||||||
|
blocks: Record<string, ChangedBlock>
|
||||||
|
removed: RemovedView[]
|
||||||
|
stats: DiffStats
|
||||||
|
lineCount: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* reveal:已经开着的预览滚到某处(同 DocumentMessage.reveal)
|
* reveal:已经开着的预览滚到某处(同 DocumentMessage.reveal)
|
||||||
* definitionProbed:id 那次 probeDefinition 的回答,found 表示查得到定义
|
* definitionProbed:id 那次 probeDefinition 的回答,found 表示查得到定义
|
||||||
@@ -269,6 +359,7 @@ export interface AnswerEnd {
|
|||||||
* answerEnd:askId 那个问题答完了,见 AnswerEnd
|
* answerEnd:askId 那个问题答完了,见 AnswerEnd
|
||||||
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现
|
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现
|
||||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
|
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
|
||||||
|
* diff:这份文件相比对比基准改了什么(侧栏开着「未提交的更改」或审查工单时才有);每来一份文档之后补来,基准变了再来;null 是不比了
|
||||||
*/
|
*/
|
||||||
export type ExtensionToWebview
|
export type ExtensionToWebview
|
||||||
= | DocumentMessage
|
= | DocumentMessage
|
||||||
@@ -282,6 +373,7 @@ export type ExtensionToWebview
|
|||||||
| ({ type: 'askProfiles' } & ProfileChoice)
|
| ({ type: 'askProfiles' } & ProfileChoice)
|
||||||
| { type: 'answer', askId: string, text: string, activity?: string }
|
| { type: 'answer', askId: string, text: string, activity?: string }
|
||||||
| AnswerEnd
|
| AnswerEnd
|
||||||
|
| { type: 'diff', diff: DocumentDiff | null }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来第一行的缩进)
|
* edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来第一行的缩进)
|
||||||
|
|||||||
@@ -119,6 +119,11 @@ describe('眼下折叠着哪些', () => {
|
|||||||
expect([...collapsedIds([block('0', 'exception', [block('0.0', 'function')]), block('1', 'if')], {})]).toEqual(['0', '0.0'])
|
expect([...collapsedIds([block('0', 'exception', [block('0.0', 'function')]), block('1', 'if')], {})]).toEqual(['0', '0.0'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('标了改动的块默认展开,用户折叠过的照样折叠', () => {
|
||||||
|
expect([...collapsedIds(BLOCKS, {}, new Set(['1', '2']))].sort()).toEqual(['1.0.0', '2.0'])
|
||||||
|
expect([...collapsedIds(BLOCKS, { 1: true }, new Set(['1']))].sort()).toEqual(['1', '1.0.0', '2', '2.0'])
|
||||||
|
})
|
||||||
|
|
||||||
it('点过的按用户的:展开的函数、类保持展开,折叠的其他块、属性行保持折叠', () => {
|
it('点过的按用户的:展开的函数、类保持展开,折叠的其他块、属性行保持折叠', () => {
|
||||||
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
||||||
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
|
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
|
||||||
|
|||||||
@@ -59,12 +59,12 @@ export function foldedByDefault(block: Pick<BlockView, 'kind' | 'badge'>): boole
|
|||||||
return block.badge !== undefined || ['function', 'class', 'enum', 'exception'].includes(block.kind)
|
return block.badge !== undefined || ['function', 'class', 'enum', 'exception'].includes(block.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault。 */
|
/** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault;opened 里的块(标了改动的)默认展开。 */
|
||||||
export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Record<string, boolean>>): Set<string> {
|
export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Record<string, boolean>>, opened: ReadonlySet<string> = new Set()): Set<string> {
|
||||||
const collapsed = new Set(Object.keys(folds).filter(id => folds[id]))
|
const collapsed = new Set(Object.keys(folds).filter(id => folds[id]))
|
||||||
const visit = (list: readonly BlockView[]): void => {
|
const visit = (list: readonly BlockView[]): void => {
|
||||||
for (const block of list) {
|
for (const block of list) {
|
||||||
if (folds[block.id] === undefined && foldedByDefault(block))
|
if (folds[block.id] === undefined && foldedByDefault(block) && !opened.has(block.id))
|
||||||
collapsed.add(block.id)
|
collapsed.add(block.id)
|
||||||
visit(block.children)
|
visit(block.children)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user