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

169 lines
8.0 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 { 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)
}