122 lines
5.4 KiB
TypeScript
122 lines
5.4 KiB
TypeScript
import type { BlockView, PreviewText, PropertyItem, SourceRange } from './types'
|
||
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<RevealPlan, 'id'>
|
||
|
||
interface Placed {
|
||
block: BlockView
|
||
/** 从顶层到父块的块 id */
|
||
ancestors: string[]
|
||
}
|
||
|
||
/** 按页面上的先后(先父块、再子块)列出所有块。 */
|
||
function* placed(blocks: BlockView[], ancestors: string[] = []): Generator<Placed> {
|
||
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)
|
||
}
|
||
|
||
/**
|
||
* 块里哪儿是 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 }
|
||
}
|
||
|
||
/**
|
||
* 让一块露出来时视口顶部要在的位置,和 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)
|
||
}
|