import type { ScrollAnchor } from './scrollPosition' import type { BlockView, OpenedFrom } from './types' /** * 页面重新加载(webview 进程崩了重建、开发者重载 webview)时靠它恢复(标签页切走时页面一直留着,用不上它):哪些块停在「编辑」、 * 用户把哪些块、属性行和子行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行、子行 id 见 [propertyRowId]、[propertyItemId]),滚到了哪, * 以及开着哪一块的调用树弹框、选中第几个入口。 * VS Code 重启后恢复标签页时也带着它,这个标签页是从哪儿打开的靠它报回扩展端。 */ export interface ViewState { editing: string[] /** 用户自己点过折叠箭头的块、属性行和子行:id → 折叠着没有。没点过的按默认,见 collapsedIds */ folds: Record scroll?: ScrollAnchor /** 这个标签页是从哪儿打开的,扩展端告诉的;新开的页面还没记 */ openedFrom?: OpenedFrom /** 开着的调用树弹框;关着是没有 */ callTree?: CallTreeView } /** 开着的调用树弹框:哪一块(anchor),选中第几个入口(从 0 起)。 */ export interface CallTreeView { anchor: string entry: number } export interface ViewStateStorage { /** 存下来的东西可能是旧版本的形状,读的时候逐项核对 */ getState: () => unknown setState: (state: ViewState) => void } export interface ViewStateStore { get: () => ViewState /** 只改给出的那几项,其余照旧,整份写回 */ update: (patch: Partial) => void } /** 一个 webview 只有一份状态:块的状态和滚动位置由不同地方各自更新,存的时候合在一起。 */ export function createViewStateStore(storage: ViewStateStorage): ViewStateStore { let current = readViewState(storage.getState()) return { get: () => current, update: (patch) => { current = { ...current, ...patch } storage.setState(current) }, } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function isScrollAnchor(value: unknown): value is ScrollAnchor { return isRecord(value) && [value.line, value.offset, value.scrollTop].every(field => typeof field === 'number') } /** 存下来的状态逐项核对,形状不对的项(包括旧版本存的)当没存过。 */ function readViewState(saved: unknown): ViewState { const state = isRecord(saved) ? saved : {} const editing = Array.isArray(state.editing) ? state.editing.filter((id): id is string => typeof id === 'string') : [] const folds = isRecord(state.folds) ? Object.fromEntries(Object.entries(state.folds).filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean')) : {} return { editing, folds, ...(isScrollAnchor(state.scroll) && { scroll: state.scroll }), ...((state.openedFrom === 'uncommitted' || state.openedFrom === 'other') && { openedFrom: state.openedFrom }), ...(isCallTreeView(state.callTree) && { callTree: state.callTree }), } } function isCallTreeView(value: unknown): value is CallTreeView { return isRecord(value) && typeof value.anchor === 'string' && Number.isInteger(value.entry) && (value.entry as number) >= 0 } /** * 块默认折叠吗:函数、类、枚举、异常块和库规则的卡片收起子块(属性表照常显示),其余块展开。属性行和带下一层的子行默认都展开。 * 挂在父卡片子行上、点子行才展开的子块(BlockSlot)默认收着:它折叠着就是整张不画。 */ export function foldedByDefault(block: Pick): boolean { return block.slot !== undefined || block.badge !== undefined || ['function', 'class', 'enum', 'exception'].includes(block.kind) } /** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault;opened 里的块(标了改动的)默认展开。 */ export function collapsedIds(blocks: readonly BlockView[], folds: Readonly>, opened: ReadonlySet = new Set()): Set { const collapsed = new Set(Object.keys(folds).filter(id => folds[id])) const visit = (list: readonly BlockView[]): void => { for (const block of list) { if (folds[block.id] === undefined && foldedByDefault(block) && !opened.has(block.id)) collapsed.add(block.id) visit(block.children) } } visit(blocks) return collapsed } /** * 折叠着的 id 按卡片分好,卡片只订阅自己那份:属性行、子行的 id(`<块 id>/…`)归它那张卡片;块 id 归它自己, * 也归它的父卡片(挂在父卡片子行上的子块由父卡片的属性表画、点子行开关)。 * 和 previous 里同一张卡片的那份一样时沿用那一份:别的卡片折叠、展开时,这张卡片订阅的还是原来那个对象,不重画。 */ export function foldsByCard(collapsed: ReadonlySet, previous?: ReadonlyMap>): ReadonlyMap> { const groups = new Map>() const add = (card: string, id: string): void => { const group = groups.get(card) if (group) group.add(id) else groups.set(card, new Set([id])) } for (const id of collapsed) { const slash = id.indexOf('/') if (slash >= 0) { add(id.slice(0, slash), id) continue } add(id, id) const dot = id.lastIndexOf('.') if (dot >= 0) add(id.slice(0, dot), id) } if (!previous) return groups return new Map([...groups].map(([card, ids]) => { const before = previous.get(card) return [card, before && before.size === ids.size && [...ids].every(id => before.has(id)) ? before : ids] })) } /** * 属性表里带子行的那一行(输入参数、导入项、字典 / 对象的值)的折叠 id,和块 id 放在同一份 folds 里: * `<块 id>/p<行序号>`,例如 '6.1.0/p3'。块 id 只由数字和点组成,带 '/' 的不会和任何块 id 撞上。 */ export function propertyRowId(blockId: string, rowIndex: number): string { return `${blockId}/p${rowIndex}` } /** * 一行或一个子行(parentId)下面展开的第几个子行:`<上一层的 id>/<子行序号>`,一层套一层往后接, * 例如 '6.1.0/p3/1' 是第 3 行的第 1 个子行,'6.1.0/p3/1/0' 是它下面的第 0 个。跳到定义时拿它在页面上找到要闪的子行; * 带下一层子行的子行(字典 / 对象的值又是字典 / 对象)也拿它当折叠 id,和属性行一样记进 folds、默认展开。 * 只有行那一节带 'p',后面各节都是纯数字,不同位置的 id 不会撞上,也不会和块 id、属性行 id 撞上。 */ export function propertyItemId(parentId: string, itemIndex: number): string { return `${parentId}/${itemIndex}` } /** * 实参表的 id:一行译文下面那张(owner 是块 id),或实参表里一行的值那一格里套着的那张(owner 是那一行的 id); * 表里各行的 id 是 propertyItemId(它, 行号)。这两种表总是展开的。 */ export function tableId(ownerId: string): string { return `${ownerId}/t` } /** * 一行译文下面拆开的各项(跨几行写的列表、字典)的上一层 id:`<块 id>/i`,各项的 id 是 propertyItemId(它, 项号)。 * 这一层总是展开的,项下面的下一层照常折叠在项的 id 上。 */ export function listId(blockId: string): string { return `${blockId}/i` } /** * 卡片上一段字的记号,画在显示它的元素上(data-find):`<所在块、属性行、子行或表的 id>#<哪一段>`, * 例如 '6.1.0#text' 是这块的一行译文,'6.1.0/p3#label' 是第 3 行的属性名,'6.1.0/p3/1#0' 是它第 1 个子行的第 0 格,'6.1.0/p3#h0' 是那张表的第 0 个列名。 * 查找栏按它把算好的命中落到页面上的字上。 */ export function textKey(ownerId: string, part: string): string { return `${ownerId}#${part}` } /** * 父块看得见时,要让它的一个子块也看得见得展开的(块 id、属性行 id):一般的子块是父块自己(收起子块时不画); * 挂在父卡片子行上的子块(BlockSlot)是它所在的那一行和它自己。 */ export function unfoldToShow(parentId: string, child: Pick): string[] { if (!child.slot) return [parentId] return [propertyRowId(parentId, child.slot.row), child.id] }