169 lines
8.0 KiB
TypeScript
169 lines
8.0 KiB
TypeScript
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<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)
|
||
}
|
||
|
||
/** 实参表(一行译文下面那张、值那一格里套着的,都总是展开的)里有格子满足 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<string, string>): 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)
|
||
}
|