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 { const found = new Map() 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 /** offset 处名字的定义(定义提供者) */ definitions: (offset: number) => Promise /** 工作区里定义的、原名叫 name 的中文名 */ indexed: (name: string) => Promise /** 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 { const rendered = renderedNames(blocks) const shown = new Map() 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() for (const [offset, original] of rendered) { if (!shown.has(offset)) byName.set(original, [...(byName.get(original) ?? []), offset]) } const bindingAt = new Map() for (const binding of bindings) { for (const { startIndex, endIndex } of binding.occurrences) bindingAt.set(startIndex, { binding, end: endIndex }) } // 同一个绑定的各处问一处、答案共用;这份文档自己定义的(参数、赋值)定义就在这份文档里,对不上别的文件的中文名,不问 const asks = new Map() 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 => 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[0] = [] const far: Parameters[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 | 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: (index: WorkspaceIndexOf, key: string) => Promise[]>, adapter: LanguageAdapter, name: string, ): Promise { 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 | 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 = 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): 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 } }