契约加 LanguageAdapter.chain、Translator.chain、RuleContext.chain、PropertyRow.chain、TextTranslation.rows;跨行写或实参里有闭包的链排成卡片,连着同名调用合成一步列出每次实参,注释挂在步骤上,闭包收起、子块挂到那一步(BlockView.step);Rust 插件读链、标准库常见方法写中文名;挤成一行时另起一行的 . 贴着前面
308 lines
13 KiB
TypeScript
308 lines
13 KiB
TypeScript
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
|
||
}
|
||
}
|