186 lines
8.4 KiB
TypeScript
186 lines
8.4 KiB
TypeScript
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<string, boolean>
|
||
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<ViewState>) => 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<string, unknown> {
|
||
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<BlockView, 'kind' | 'badge' | 'slot'>): 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<Record<string, boolean>>, opened: ReadonlySet<string> = new Set()): Set<string> {
|
||
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<string>, previous?: ReadonlyMap<string, ReadonlySet<string>>): ReadonlyMap<string, ReadonlySet<string>> {
|
||
const groups = new Map<string, Set<string>>()
|
||
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<BlockView, 'id' | 'slot'>): string[] {
|
||
if (!child.slot)
|
||
return [parentId]
|
||
return [propertyRowId(parentId, child.slot.row), child.id]
|
||
}
|