Files
superwork/vscode/webview-ui/src/codeReader/viewState.ts
T

186 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
}