Files
superwork/vscode/src/codeReader/nameResolution.ts
T
cruldra ee595f3da9 ✨ feat(vscode): 代码阅读的链式调用卡片:语言插件读链、翻译模板排成起点和步骤
契约加 LanguageAdapter.chain、Translator.chain、RuleContext.chain、PropertyRow.chain、TextTranslation.rows;跨行写或实参里有闭包的链排成卡片,连着同名调用合成一步列出每次实参,注释挂在步骤上,闭包收起、子块挂到那一步(BlockView.step);Rust 插件读链、标准库常见方法写中文名;挤成一行时另起一行的 . 贴着前面
2026-10-15 20:54:59 +11:00

308 lines
13 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 } from './blockView'
import type { DisplayName, IndexEntry, Inline, LanguageAdapter, NameBinding, PreviewText, PropertyItem, PropertyRow, RuleLocation } from './contract'
import type { IndexedEntry, WorkspaceIndexOf } from './workspaceIndex'
/**
* 中文名落到预览里的哪些名字上(见「中文名」):语言插件只认出定义和它的中文名,
* 用到它的各处由语言扩展按语义认(同一文件问 document highlights,别的文件定义的问定义提供者),这里不碰 VS Code。
*/
/** 预览里显示成中文名的一个名字:源码下标 [offset, offset + length) 的名字显示成 name;definition 是定义处本身(名称格子、名称行后面淡淡写原名)。 */
export interface ShownName {
offset: number
length: number
name: string
definition?: true
}
/** 视图数据里照抄自源码、带下标的名字(一行译文和它下面拆开的各项、表、几行属性,属性表的值、各层子行的格子、链式调用的各步、框里):下标 → 原文。 */
export function renderedNames(blocks: readonly BlockView[]): Map<number, string> {
const found = new Map<number, string>()
const inline = (pieces: readonly Inline[]): void => {
for (const piece of pieces) {
if (!('role' in piece))
inline(piece.segments)
else if (piece.offset !== undefined)
found.set(piece.offset, piece.text)
}
}
const text = (value: PreviewText): void => {
if (!('markdown' in value))
inline(value)
}
const items = (list: readonly PropertyItem[] | undefined): void => {
for (const item of list ?? []) {
item.cells.forEach(text)
items(item.items)
items(item.table?.items)
}
}
const rows = (list: readonly PropertyRow[] | undefined): void => {
for (const row of list ?? []) {
text(row.value)
items(row.items)
for (const step of row.chain ?? []) {
inline(step.name)
inline(step.value)
step.lines?.forEach(inline)
rows(step.rows)
items(step.table?.items)
}
}
}
const visit = (list: readonly BlockView[]): void => {
for (const block of list) {
if ('properties' in block) {
rows(block.properties)
}
else {
text(block.translation)
items(block.items)
items(block.table?.items)
rows(block.rows)
}
visit(block.children)
}
}
visit(blocks)
return found
}
/** 源码里的一段 [start, end)。 */
export interface TextRange { start: number, end: number }
/** 定义处名字的起点(行列 0 起,列按 UTF-16)。 */
export interface DefinitionStart { uri: string, line: number, character: number }
/** 工作区索引里别的文件定义的一个中文名。 */
export interface IndexedName { location: RuleLocation, text: string }
/** 找各处要问的:这份文档的语言扩展、工作区索引。 */
export interface NameSources {
/** 这份文档的 uri */
uri: string
/** offset 处的名字在这份文档里的各处出现(document highlights) */
occurrences: (offset: number) => Promise<TextRange[]>
/** offset 处名字的定义(定义提供者) */
definitions: (offset: number) => Promise<DefinitionStart[]>
/** 工作区里定义的、原名叫 name 的中文名 */
indexed: (name: string) => Promise<IndexedName[]>
/** offset 处页面眼下看不看得见:别的文件定义的,看得见的先问、问完先交一次 */
visible: (offset: number) => boolean
}
/**
* 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final):
* 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition;
* 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算;
* 同一个绑定(见 NameBinding)的各处只问一处、答案共用,这份文档自己定义的绑定不问;页面看得见的先问,问完先交一次,再问其余的。
* 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。
*/
export async function resolveNames(
names: readonly DisplayName[],
blocks: readonly BlockView[],
bindings: readonly NameBinding[],
sources: NameSources,
report: (shown: ShownName[], final: boolean) => void,
): Promise<boolean> {
const rendered = renderedNames(blocks)
const shown = new Map<number, ShownName>()
let settled = true
await Promise.all(names.map(async ({ name, text }) => {
const ranges = await sources.occurrences(name.startIndex)
if (!ranges.some(range => range.start === name.startIndex))
settled = false
for (const { start, end } of ranges) {
if (!shown.has(start) && rendered.get(start)?.length === end - start)
shown.set(start, { offset: start, length: end - start, name: text, ...(start === name.startIndex && { definition: true as const }) })
}
}))
report([...shown.values()], false)
const byName = new Map<string, number[]>()
for (const [offset, original] of rendered) {
if (!shown.has(offset))
byName.set(original, [...(byName.get(original) ?? []), offset])
}
const bindingAt = new Map<number, { binding: NameBinding, end: number }>()
for (const binding of bindings) {
for (const { startIndex, endIndex } of binding.occurrences)
bindingAt.set(startIndex, { binding, end: endIndex })
}
// 同一个绑定的各处问一处、答案共用;这份文档自己定义的(参数、赋值)定义就在这份文档里,对不上别的文件的中文名,不问
const asks = new Map<NameBinding | number, { offsets: number[], original: string, elsewhere: IndexedName[] }>()
await Promise.all([...byName].map(async ([original, offsets]) => {
const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri)
for (const offset of elsewhere.length > 0 ? offsets : []) {
const at = bindingAt.get(offset)
const binding = at?.end === offset + original.length ? at.binding : undefined
if (binding?.local)
continue
const ask = asks.get(binding ?? offset) ?? { offsets: [], original, elsewhere }
ask.offsets.push(offset)
asks.set(binding ?? offset, ask)
}
}))
let answered = 0
const ask = (list: { at: number, offsets: number[], original: string, elsewhere: IndexedName[] }[]): Promise<unknown> => Promise.all(list.map(async ({ at, offsets, original, elsewhere }) => {
const found = await sources.definitions(at)
if (found.length > 0)
answered++
const entry = elsewhere.find(({ location }) => found.some(start => start.uri === location.uri && start.line === location.start.line && start.character === location.start.character))
if (!entry)
return
for (const offset of offsets)
shown.set(offset, { offset, length: original.length, name: entry.text })
}))
const near: Parameters<typeof ask>[0] = []
const far: Parameters<typeof ask>[0] = []
for (const item of asks.values()) {
const visible = item.offsets.find(offset => sources.visible(offset))
if (visible === undefined)
far.push({ ...item, at: item.offsets[0] })
else
near.push({ ...item, at: visible })
}
if (near.length > 0 && far.length > 0) {
await ask(near)
report([...shown.values()], false)
await ask(far)
}
else {
await ask([...near, ...far])
}
if (asks.size > 0 && answered === 0)
settled = false
report([...shown.values()], true)
return settled
}
/** 中文名索引的一项:键是原名。 */
export interface NameEntry extends IndexEntry { text: string }
/** 一种语言的文件在整个工作区里的中文名索引(同一个 languageId 只建一份);语言插件不认中文名、不管这种文件时没有。 */
export function displayNameIndex(adapter: LanguageAdapter, languageId: string): WorkspaceIndexOf<NameEntry> | undefined {
const reader = adapter.displayNames
const include = reader?.files[languageId]
if (!reader || !include)
return undefined
return {
id: `displayNames:${languageId}`,
languageId,
include,
mentions: reader.mentions,
entries: (root, file) => reader.find(root, file.source, languageId).names.map(({ name, text }) => ({
key: file.source.slice(name.startIndex, name.endIndex),
name,
text,
})),
}
}
/** 这个语言插件管的各种文件里,原名叫 name 的中文名。 */
export async function indexedNames(
find: <E extends IndexEntry>(index: WorkspaceIndexOf<E>, key: string) => Promise<IndexedEntry<E>[]>,
adapter: LanguageAdapter,
name: string,
): Promise<IndexedName[]> {
const indexes = Object.keys(adapter.displayNames?.files ?? {}).flatMap(languageId => displayNameIndex(adapter, languageId) ?? [])
const found = await Promise.all(indexes.map(index => find(index, name)))
return found.flat().map(({ location, entry }) => ({ location, text: entry.text }))
}
/** 文档的一处改动(TextDocumentContentChangeEvent 的下标写法):从 offset 起 length 个字换成 text。 */
export interface TextChange { offset: number, length: number, text: string }
/**
* 中文名跟着改动挪位置:改动整个在名字前面的往后挪,整个在后面的不动,碰到名字的丢掉;一次的几处改动按顺序一个个套。
* 紧贴着名字插的字把名字改长了(`foo` → `xfoo`)也不会认错:页面只把中文名放在长度对得上的名字上。
*/
export function shiftNames(names: readonly ShownName[], changes: readonly TextChange[]): ShownName[] {
let current = [...names]
for (const { offset, length, text } of changes) {
const delta = text.length - length
current = current.flatMap((name) => {
if (offset + length <= name.offset)
return [{ ...name, offset: name.offset + delta }]
return offset >= name.offset + name.length ? [name] : []
})
}
return current
}
/** next 把 current 的每一个都盖住了(一模一样的一项),还多出几个。 */
function adds(next: readonly ShownName[], current: readonly ShownName[]): boolean {
const keyOf = ({ offset, length, name, definition }: ShownName): string => JSON.stringify([offset, length, name, definition])
const keys = new Set(next.map(keyOf))
return next.length > current.length && current.every(name => keys.has(keyOf(name)))
}
/** 语言扩展还没起来时隔多久再查(毫秒),查到最后一次还没答就算了 */
export const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000]
/**
* 一份打开着的文档的中文名:每个版本查一次(语言扩展还没起来时照 RETRY_DELAYS 隔一会儿再查),只交出眼下这个版本的;
* 文档改了,已知的中文名先跟着改动挪位置当新版本的用,新版本查完再换掉,免得每改一下预览里的中文名闪一下。
*/
export class DocumentNames {
private version: number | undefined
private names: ShownName[] = []
/** 眼下这个版本查过了(或正在查) */
private resolving: number | undefined
private timer: ReturnType<typeof setTimeout> | undefined
constructor(
private readonly deliver: (version: number, names: ShownName[]) => void,
private readonly delays: readonly number[] = RETRY_DELAYS,
private readonly schedule: (run: () => void, ms: number) => ReturnType<typeof setTimeout> = setTimeout,
) {}
/** version 这份文档已知的中文名(发文档时带上);不是眼下这个版本的没有。 */
known(version: number): ShownName[] {
return version === this.version ? this.names : []
}
/** 文档改成了 version:已知的中文名跟着改动挪位置,等新版本查完再换。 */
edited(version: number, changes: readonly TextChange[]): void {
clearTimeout(this.timer)
this.names = shiftNames(this.names, changes)
this.version = version
this.resolving = undefined
}
/**
* 发出去了 version 这份文档:查它的中文名;同一个版本只查一次(页面重新加载时照旧用查到的)。
* 查到一步交一步(先交本文件的,再交看得见的);比眼下显示着的(上一版挪过来的、上一步交过的)少了哪个、或者没多出什么的那一步不交,
* 等查完一起换,免得少了的那些闪一下。
* 查的过程中文档又改了的,查到的丢掉。
*/
resolve(version: number, run: (report: (names: ShownName[], final: boolean) => void) => Promise<boolean>): void {
if (this.resolving === version)
return
clearTimeout(this.timer)
if (this.version !== version) {
this.version = version
this.names = []
}
this.resolving = version
const report = (names: ShownName[], final: boolean): void => {
if (this.resolving !== version || (!final && !adds(names, this.names)))
return
this.names = names
this.deliver(version, names)
}
const attempt = (tries: number): void => {
void run(report).then((settled) => {
if (!settled && this.resolving === version && tries < this.delays.length)
this.timer = this.schedule(() => attempt(tries + 1), this.delays[tries])
})
}
attempt(0)
}
dispose(): void {
clearTimeout(this.timer)
this.resolving = undefined
}
}