import type { BlockView, PreviewText, PropertyItem, SourceRange } from './types' 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) && text.some(segment => segment.offset === offset) } /** * 块里哪儿是 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) ? { 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, 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 下面一层层的子行里,哪个显示着 offset 处的名字:要展开的各层(parentId 到那个子行所在的那一层)。 */ function showingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): string[] | undefined { for (const [index, item] of (items ?? []).entries()) { if (item.cells.some(cell => shows(cell, offset))) return [parentId] const inner = showingItem(item.items, propertyItemId(parentId, index), offset) 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 } } /** 让一块露出来要滚到的 scrollTop,和 VS Code 跳到定义一样:整块已经在视口里就不滚;否则放到视口正中,比视口还高就顶部对齐。 */ 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) }