import type { BlockView, Occurrence, PreviewTable, PreviewText, PropertyItem, SourceRange } from './types' import { blockPath } from './filter' import { showsOccurrence } from './occurrences' import { showsOffset } from './segments' import { propertyItemId, propertyRowId } from './viewState' /** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */ export interface RevealPlan { id: string /** 属性行 id(propertyRowId)或子行 id(propertyItemId);没有时闪整块 */ row?: string expand: string[] } /** 目标在一块里的哪儿:row、expand 同 RevealPlan,expand 不含祖先块。 */ type Spot = Omit interface Placed { block: BlockView /** 从顶层到父块的块 id */ ancestors: string[] } /** 按页面上的先后(先父块、再子块)列出所有块。 */ function* placed(blocks: BlockView[], ancestors: string[] = []): Generator { for (const block of blocks) { yield { block, ancestors } yield* placed(block.children, [...ancestors, block.id]) } } function covers(range: SourceRange | undefined, offset: number): boolean { return range !== undefined && range.startIndex <= offset && offset < range.endIndex } /** 文字里(连框里)显示着 offset 处的名字;Markdown 说明里的名字不带下标。 */ function shows(text: PreviewText, offset: number): boolean { return !('markdown' in text) && showsOffset(text, offset) } /** 实参表(一行译文下面那张、值那一格里套着的,都总是展开的)里有格子满足 test,套着的表一层层都算。 */ function tableShows(table: PreviewTable | undefined, test: (cell: PreviewText) => boolean): boolean { return table?.items.some(item => item.cells.some(test) || tableShows(item.table, test)) ?? false } /** * 块里哪儿是 offset 处的定义,从具体到笼统: * ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块; * ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它, * 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开; * ③ 块里只是显示着这个名字(一行译文和它下面的表里的名字、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。 * 不在这块里是 undefined。 */ function locate(block: BlockView, offset: number): Spot | undefined { if (covers(block.defines, offset)) return { expand: [] } if ('translation' in block) return shows(block.translation, offset) || tableShows(block.table, cell => shows(cell, offset)) ? { expand: [] } : undefined const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) })) for (const { row, id } of rows) { if (covers(row.defines, offset)) return { row: id, expand: [] } const item = definingItem(row.items, id, offset) if (item) return item } for (const { row, id } of rows) { if (shows(row.value, offset)) return { expand: [] } const expand = showingItem(row.items, id, cell => shows(cell, offset)) if (expand) return { expand } } return undefined } /** parentId 下面一层层的子行里,defines 盖住 offset 的那个:闪它,展开它上面的各层(parentId 和它所在的那串上层子行)。 */ function definingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): Spot | undefined { for (const [index, item] of (items ?? []).entries()) { const id = propertyItemId(parentId, index) if (covers(item.defines, offset)) return { row: id, expand: [parentId] } const inner = definingItem(item.items, id, offset) if (inner) return { ...inner, expand: [parentId, ...inner.expand] } } return undefined } /** * parentId 下面一层层的子行里,哪个的格子(连它值那一格里套着的实参表)满足 test(显示着要找的名字): * 要展开的各层(parentId 到那个子行所在的那一层)。 */ function showingItem(items: PropertyItem[] | undefined, parentId: string, test: (cell: PreviewText) => boolean): string[] | undefined { for (const [index, item] of (items ?? []).entries()) { if (item.cells.some(test) || tableShows(item.table, test)) return [parentId] const inner = showingItem(item.items, propertyItemId(parentId, index), test) if (inner) return [parentId, ...inner] } return undefined } /** * 跳到定义:目标见 locate;页面上找不到这处定义(例如定义是整个模块,落在文件开头)就取包含 line 的块,规则同 blockForLine。 * 目标的祖先块都要展开。 */ export function revealPlan(blocks: BlockView[], line: number, definition: number): RevealPlan | undefined { let byLine: Placed | undefined for (const item of placed(blocks)) { const spot = locate(item.block, definition) if (spot) return { id: item.block.id, ...spot, expand: [...item.ancestors, ...spot.expand] } if (item.block.startLine <= line && (!byLine || item.block.startLine > byLine.block.startLine)) byLine = item } const target = byLine ?? (blocks.length > 0 ? { block: blocks[0], ancestors: [] } : undefined) return target && { id: target.block.id, expand: target.ancestors } } /** * 目标块被正在编辑的祖先盖住、不在页面上(库规则的卡片正在编辑,目标是它下面留下的方法):改成闪盖住它的那块, * 展开那块的祖先,不再找目标里的属性行。covered 见 coveredByEditing。 */ export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap): RevealPlan { const owner = covered.get(plan.id) return owner === undefined ? plan : { id: owner, expand: blockPath(owner).slice(0, -1) } } /** 块里哪儿显示着满足 test 的文字(译文和它下面的表、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */ function showingSpot(block: BlockView, test: (text: PreviewText) => boolean): string[] | undefined { if ('translation' in block) return test(block.translation) || tableShows(block.table, test) ? [] : undefined for (const [index, row] of block.properties.entries()) { if (test(row.value)) return [] const expand = showingItem(row.items, propertyRowId(block.id, index), test) if (expand) return expand } return undefined } /** * 用键盘跳到高亮着的名字的一处出现:页面上按先后第一处显示着它(有段和它重叠)的块,以及要展开的块、属性行和子行。 * skip 的块(被筛掉的、正在编辑的、被正在编辑的祖先盖住的)里的不算;Markdown 说明里的名字不带下标,也不算。页面上哪儿都没有时是 undefined。 */ export function occurrencePlan(blocks: BlockView[], occurrence: Occurrence, skip: (id: string) => boolean): RevealPlan | undefined { const test = (text: PreviewText): boolean => !('markdown' in text) && showsOccurrence(text, occurrence) for (const { block, ancestors } of placed(blocks)) { const expand = skip(block.id) ? undefined : showingSpot(block, test) if (expand) return { id: block.id, expand: [...ancestors, ...expand] } } return undefined } /** * 让一块露出来时视口顶部要在的位置,和 VS Code 跳到定义一样:整块已经在视口里就不滚;否则放到视口正中,比视口还高就顶部对齐。 * viewTop / viewHeight 是视口里露出块的那部分(钉住的筛选栏下面)。 */ export function scrollTopToShow(box: { top: number, bottom: number }, viewTop: number, viewHeight: number): number | undefined { if (box.top >= viewTop && box.bottom <= viewTop + viewHeight) return undefined const height = box.bottom - box.top return Math.max(0, height >= viewHeight ? box.top : box.top - (viewHeight - height) / 2) }