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

122 lines
5.4 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, 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)
}