✨ feat(vscode): 代码阅读认出代码里起的中文名(Annotated[…, Comment("…")]、@Comment 装饰器、行尾 # @Comment、docstring / JSDoc 的参数说明),预览里定义和用到它的各处都写中文名,悬停看原名,定义处后面淡淡写原名

This commit is contained in:
cruldra committed 2026-09-27 00:56:39 +08:00
1 parent 1b1299dc1f
commit 44c221ea2b
45 files changed
+1685 -128

No files matched your search

+250
View File
@@ -0,0 +1,250 @@
import type { BlockView } from './blockView'
import type { DisplayName, IndexEntry, Inline, LanguageAdapter, PreviewText, PropertyItem, 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(inline)
items(item.items)
}
}
const visit = (list: readonly BlockView[]): void => {
for (const block of list) {
if ('properties' in block) {
for (const row of block.properties) {
text(row.value)
items(row.items)
}
}
else {
text(block.translation)
}
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[]>
}
/**
* 找这份文档里显示成中文名的各处,分两步交出(先交本文件的,不等工作区索引建好;最后一次 final):
* 1. 本文件定义的:每个定义问一次它的各处出现,落在视图数据里带下标的名字上的显示成中文名,定义处本身标 definition;
* 2. 别的文件定义的:原名和工作区索引里某个中文名一样的名字才问定义提供者,定义处正是索引里那个定义才算。
* 返回语言扩展答没答(settled):定义处自己都查不到出现,或要问的定义一个都没答,是语言扩展还没起来,过一会儿再查。
*/
export async function resolveNames(
names: readonly DisplayName[],
blocks: readonly BlockView[],
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])
}
let asked = 0
let answered = 0
await Promise.all([...byName].map(async ([original, offsets]) => {
const elsewhere = (await sources.indexed(original)).filter(entry => entry.location.uri !== sources.uri)
if (elsewhere.length === 0)
return
await Promise.all(offsets.map(async (offset) => {
asked++
const found = await sources.definitions(offset)
if (found.length > 0)
answered++
const entry = elsewhere.find(({ location }) => found.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character))
if (entry)
shown.set(offset, { offset, length: original.length, name: entry.text })
}))
}))
if (asked > 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
}
/** 语言扩展还没起来时隔多久再查(毫秒),查到最后一次还没答就算了 */
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 && this.names.length > 0))
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
}
}