链的各步排在值下面的竖轨道上,方法一列对齐、轨道线是函数色淡到 35%;实参是链的缩进成空心点小链,注释挂在步骤下;收起的闭包是能点的小框,点开那一步才画挂着的子块(折叠 id 记在属性行下面);跳到定义、查找、高度估算、只看改动都算上链里的字和步骤下的子块
197 lines
9.4 KiB
TypeScript
197 lines
9.4 KiB
TypeScript
import type { Placement } from './placement'
|
|
import type { BlockView, CardView, ChangedBlock, DiffStats, DocumentDiff, RemovedView } from './types'
|
|
import { inlineText } from './segments'
|
|
import { unfoldToShow } from './viewState'
|
|
|
|
/** 页面上怎么标差异:按块 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: CardView): 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()
|
|
}
|
|
|
|
/** 「只看改动」时折起来的:各段连着没变的块(按第一块的 id 记这段几块),和折进去的所有块(连同子孙)。 */
|
|
export interface FoldedRuns {
|
|
runs: ReadonlyMap<string, number>
|
|
folded: ReadonlySet<string>
|
|
}
|
|
|
|
/**
|
|
* 「只看改动」:每一层连着没变的块折成一段(被筛掉的块不算、不打断),改过的块照常、它下面的子块同样按层折;
|
|
* 删掉的块排在哪块后面,那一段就在哪儿断开。opened 里的段(按第一块的 id,点开过的)不折;keep 里的块(跳到定义要露出来的)
|
|
* 和通向它的块不折。
|
|
*/
|
|
export function foldUnchanged(blocks: readonly BlockView[], tree: DiffTree, shown: (block: BlockView) => boolean, opened: ReadonlySet<string>, keep: ReadonlySet<string>): FoldedRuns {
|
|
const runs = new Map<string, number>()
|
|
const folded = new Set<string>()
|
|
const fold = (block: BlockView): void => {
|
|
folded.add(block.id)
|
|
block.children.forEach(fold)
|
|
}
|
|
const kept = (block: BlockView): boolean => keep.has(block.id) || block.children.some(kept)
|
|
/** 画在同一处的一串兄弟块:连着没变的折成一段 */
|
|
function visitRun(list: readonly BlockView[], breaks: ReadonlySet<string | undefined>): void {
|
|
let run: BlockView[] = []
|
|
const close = (): void => {
|
|
if (run.length > 0 && !opened.has(run[0].id)) {
|
|
runs.set(run[0].id, run.length)
|
|
run.forEach(fold)
|
|
}
|
|
run = []
|
|
}
|
|
for (const block of list) {
|
|
if (shown(block)) {
|
|
if (tree.changes.has(block.id) || kept(block)) {
|
|
close()
|
|
visit(block.children, block.id)
|
|
}
|
|
else {
|
|
run.push(block)
|
|
}
|
|
}
|
|
if (breaks.has(block.id))
|
|
close()
|
|
}
|
|
close()
|
|
}
|
|
/** 一层子块:画在父卡片里的(挂在子行上、链式调用某一步下面,点开才展开)不折、只往里看改动,别的照常折 */
|
|
function visit(list: readonly BlockView[], owner: string): void {
|
|
const breaks = new Set((tree.removed.get(owner) ?? []).map(item => item.after))
|
|
for (const block of list) {
|
|
if ((block.slot || block.step) && (tree.changes.has(block.id) || kept(block)))
|
|
visit(block.children, block.id)
|
|
}
|
|
visitRun(list.filter(block => !block.slot && !block.step), breaks)
|
|
}
|
|
visit(blocks, '')
|
|
return { runs, folded }
|
|
}
|
|
|
|
/**
|
|
* 「上一处 / 下一处」和总览条上的一处改动:新增的最外面那块、自己改了的修改块、删掉的块,照页面上的先后。
|
|
* id 是页面上的块 id(删掉的块是 PlacedRemoved.id);from / to 是它在新文件里的行(0 起),修改块只算它自己那几行(到第一个子块之前);
|
|
* ancestors 是要展开才看得见它的各层块和属性行(挂在父卡片子行上的子块要展开它所在的那一行和它自己,见 unfoldToShow)。
|
|
*/
|
|
export interface ChangeStop {
|
|
id: string
|
|
change: 'add' | 'mod' | 'del'
|
|
from: number
|
|
to: number
|
|
ancestors: string[]
|
|
}
|
|
|
|
/** 页面上的各处改动(见 ChangeStop,行号是文件里的,按 placement 换算);hidden 里的块(被筛掉的)连同子孙不算。 */
|
|
export function changeStops(blocks: readonly BlockView[], placement: Placement, tree: DiffTree, hidden: ReadonlySet<string>): ChangeStop[] {
|
|
const stops: ChangeStop[] = []
|
|
/** 一层块:owner 是父块 id(顶层 ''),ancestorsOf 给出要让某个子块看得见得展开的,removedAncestors 是这一层删掉的块要展开的 */
|
|
const visit = (list: readonly BlockView[], owner: string, ancestorsOf: (block: BlockView) => string[], removedAncestors: string[]): void => {
|
|
const removed = tree.removed.get(owner) ?? []
|
|
const removedAfter = (id: string | undefined): void => {
|
|
for (const item of removed.filter(entry => entry.after === id))
|
|
stops.push({ id: item.id, change: 'del', from: item.removed.line, to: item.removed.line, ancestors: removedAncestors })
|
|
}
|
|
removedAfter(undefined)
|
|
for (const block of list) {
|
|
const ancestors = ancestorsOf(block)
|
|
const change = hidden.has(block.id) ? undefined : tree.changes.get(block.id)
|
|
const place = placement.of(block.id)
|
|
if (place && change?.change === 'add' && change.outermost)
|
|
stops.push({ id: block.id, change: 'add', from: place.line, to: place.endLine, ancestors })
|
|
if (place && change?.change === 'mod' && change.own) {
|
|
const first = block.children[0] && placement.of(block.children[0].id)
|
|
const own = first ? Math.max(place.line, first.line - 1) : place.endLine
|
|
stops.push({ id: block.id, change: 'mod', from: place.line, to: own, ancestors })
|
|
}
|
|
if (change?.change === 'mod')
|
|
visit(block.children, block.id, child => [...ancestors, ...unfoldToShow(block.id, child)], [...ancestors, block.id])
|
|
removedAfter(block.id)
|
|
}
|
|
}
|
|
visit(blocks, '', () => [], [])
|
|
return stops
|
|
}
|
|
|
|
/**
|
|
* 「上一处 / 下一处」跳到第几处:at.index 是接着走的那处(刚跳到的那处还在视口顶上时),否则按视口顶上那块的行 at.line 找:
|
|
* 下一处是起始行在它后面的第一处,上一处是在它前面的最后一处;走到头绕回另一头。
|
|
*/
|
|
export function nextStop(stops: readonly ChangeStop[], at: { index?: number, line?: number }, step: 1 | -1): number {
|
|
const count = stops.length
|
|
if (at.index !== undefined)
|
|
return (at.index + step + count) % count
|
|
const line = at.line
|
|
if (line === undefined)
|
|
return step === 1 ? 0 : count - 1
|
|
if (step === 1) {
|
|
const index = stops.findIndex(stop => stop.from > line)
|
|
return index >= 0 ? index : 0
|
|
}
|
|
let last = -1
|
|
stops.forEach((stop, index) => {
|
|
if (stop.from < line)
|
|
last = index
|
|
})
|
|
return last >= 0 ? last : count - 1
|
|
}
|