⚡ perf(vscode): 认调用分新建还是调用时,定义所在的文件按版本记着解析结果,同一版不再整份重解析;定义就在本文件时用发文档时读出的

This commit is contained in:
cruldra committed 2026-10-13 14:15:33 +11:00
1 parent 80d9962856
commit e9c1c67d6f
8 files changed
+194 -45

No files matched your search

+10 -8
View File
@@ -1,7 +1,7 @@
import type { BlockView } from './blockView'
import type { LanguageAdapter, LibraryRule, Translator } from './contract'
import { describe, expect, it } from 'vitest'
import { readBlockViews, readDefinitionKind, toBlockViews } from './blockView'
import { kindAt, readBlockViews, readNamedKinds, toBlockViews } from './blockView'
import { replaceLines } from './editText'
import { createBashAdapter } from './languages/bash'
import { createDockerfileAdapter } from './languages/dockerfile'
@@ -405,14 +405,16 @@ describe('blockView', () => {
expect(view?.attributes.kind).toBe('demo.thing')
})
it('readDefinitionKind:offset 落在哪块自己的名字上就是那块的种类,里层的块优先;不在名字上没有', async () => {
it('kindAt:offset 落在哪块自己的名字上就是那块的种类,里层的块优先;不在名字上没有。readBlockViews 顺带交出同样的一份', async () => {
const source = 'class UserService:\n def update_user(self):\n pass\n\nclass NotFound(Exception):\n pass\n\nlimit = 3\n'
const kind = (text: string): Promise<string | undefined> => readDefinitionKind(runtime, py, 'python', source, source.indexOf(text))
expect(await kind('UserService')).toBe('class')
expect(await kind('update_user')).toBe('function')
expect(await kind('NotFound')).toBe('exception')
expect(await kind('limit')).toBe('variable')
expect(await kind('pass')).toBeUndefined()
const kinds = await readNamedKinds(runtime, py, 'python', source)
const kind = (text: string): string | undefined => kindAt(kinds, source.indexOf(text))
expect(kind('UserService')).toBe('class')
expect(kind('update_user')).toBe('function')
expect(kind('NotFound')).toBe('exception')
expect(kind('limit')).toBe('variable')
expect(kind('pass')).toBeUndefined()
expect((await readBlockViews(runtime, py, pythonZhTranslator, 'python', source)).kinds).toEqual(kinds)
})
})
+23 -18
View File
@@ -507,13 +507,14 @@ export function toBlockViews(
/**
* 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)、
* 语言插件自己认得出的名字(LanguageAdapter.symbols,语言扩展查不到定义、各处出现时用)、
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)。
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义,见 symbolLinks.ts)、各块自己的名字和种类(调用的定义就在这份源码里时,分新建还是调用用它,见 NamedKind)。
*/
export interface DocumentViews {
blocks: BlockView[]
displayNames: DisplayName[]
symbols: LocalSymbol[]
imports: ImportedName[]
kinds: NamedKind[]
}
/**
@@ -586,7 +587,7 @@ export async function readBlockViews(
...adapter.symbols?.(tree.rootNode, source, languageId) ?? [],
...[...parts.values()].flatMap(part => part.document.language.symbols?.(part.root, source, part.code.languageId) ?? []),
]
return { blocks, displayNames: names, symbols, imports: importedNames(importsIn(codeBlocks)) }
return { blocks, displayNames: names, symbols, imports: importedNames(importsIn(codeBlocks)), kinds: namedKinds(codeBlocks) }
}
finally {
for (const parsed of trees)
@@ -594,30 +595,34 @@ export async function readBlockViews(
}
}
/** 块树里自己的名字(见 ownName)盖住 offset 的最里层那块的种类;offset 不在哪块的名字上时没有。 */
export function blockKindAt(blocks: readonly CodeBlock[], offset: number): BlockKind | undefined {
for (const block of blocks) {
const inner = blockKindAt(block.children, offset)
if (inner)
return inner
const name = ownName(block)
if (name && name.startIndex <= offset && offset < name.endIndex)
return block.kind
}
return undefined
/** 块树里各块自己的名字(见 ownName)在源码里的范围和块的种类,子块排在父块前面(后序):盖住某个下标的第一个就是最里层那块。 */
export interface NamedKind extends SourceRange {
kind: BlockKind
}
/** 解析一份源码,offset 处的名字定义的是哪种块(见 blockKindAt);语法树用完即释放。 */
export async function readDefinitionKind(
/** 块树里有自己名字的块,按后序排(见 NamedKind)。 */
export function namedKinds(blocks: readonly CodeBlock[]): NamedKind[] {
return blocks.flatMap((block) => {
const name = ownName(block)
return [...namedKinds(block.children), ...(name ? [{ startIndex: name.startIndex, endIndex: name.endIndex, kind: block.kind }] : [])]
})
}
/** 自己的名字盖住 offset 的最里层那块的种类;offset 不在哪块的名字上时没有。 */
export function kindAt(kinds: readonly NamedKind[], offset: number): BlockKind | undefined {
return kinds.find(name => name.startIndex <= offset && offset < name.endIndex)?.kind
}
/** 解析一份源码,读出各块自己的名字和种类(见 NamedKind);语法树用完即释放。 */
export async function readNamedKinds(
runtime: Pick<TreeSitterRuntime, 'parse'>,
adapter: LanguageAdapter,
languageId: string,
source: string,
offset: number,
): Promise<BlockKind | undefined> {
): Promise<NamedKind[]> {
const tree = await runtime.parse(adapter, languageId, source)
try {
return blockKindAt(adapter.toBlocks(tree.rootNode, source, languageId), offset)
return namedKinds(adapter.toBlocks(tree.rootNode, source, languageId))
}
finally {
tree.delete()
@@ -0,0 +1,66 @@
import type { KindSource } from './definitionKinds'
import type { TreeSitterRuntime } from './runtime'
import { describe, expect, it } from 'vitest'
import { readNamedKinds } from './blockView'
import { DefinitionKinds } from './definitionKinds'
import { createPythonAdapter } from './languages/python'
import { runtime, WASM_DIR } from './testSupport'
const py = createPythonAdapter(WASM_DIR)
const SOURCE = 'class UserService:\n def update_user(self):\n pass\n'
/** 数着解析了几次的解析运行时。 */
function counting(): { runtime: Pick<TreeSitterRuntime, 'parse'>, parses: () => number } {
let parses = 0
return {
runtime: {
parse: (...args) => {
parses++
return runtime.parse(...args)
},
},
parses: () => parses,
}
}
function file(name: string, version: number, text = SOURCE): KindSource {
return { uri: `file:///${name}.py`, version, languageId: 'python', getText: () => text }
}
const at = (text: string): number => SOURCE.indexOf(text)
describe('定义处是哪种块(DefinitionKinds)', () => {
it('同一份文件同一版只解析一次,几处同时问也只解析一次;文件改了(版本变了)再解析', async () => {
const { runtime: parser, parses } = counting()
const kinds = new DefinitionKinds(languageId => languageId === 'python' ? py : undefined, parser)
expect(await Promise.all([kinds.kindAt(file('a', 1), at('UserService')), kinds.kindAt(file('a', 1), at('update_user'))])).toEqual(['class', 'function'])
expect(await kinds.kindAt(file('a', 1), at('pass'))).toBeUndefined()
expect(parses()).toBe(1)
expect(await kinds.kindAt(file('a', 2, 'def UserService():\n pass\n'), 4)).toBe('function')
expect(parses()).toBe(2)
})
it('预览发文档时交进来的那份(remember)直接用,不再解析;没有语言插件的文件没有', async () => {
const { runtime: parser, parses } = counting()
const kinds = new DefinitionKinds(languageId => languageId === 'python' ? py : undefined, parser)
kinds.remember('file:///a.py', 3, await readNamedKinds(runtime, py, 'python', SOURCE))
expect(await kinds.kindAt(file('a', 3), at('update_user'))).toBe('function')
expect(parses()).toBe(0)
expect(await kinds.kindAt({ uri: 'file:///b.txt', version: 1, languageId: 'plaintext', getText: () => 'x' }, 0)).toBeUndefined()
expect(parses()).toBe(0)
})
it('最多记 limit 份文件,最久没用过的先丢', async () => {
const { runtime: parser, parses } = counting()
const kinds = new DefinitionKinds(languageId => languageId === 'python' ? py : undefined, parser, 2)
await kinds.kindAt(file('a', 1), 0)
await kinds.kindAt(file('b', 1), 0)
await kinds.kindAt(file('a', 1), 0)
await kinds.kindAt(file('c', 1), 0)
expect(parses()).toBe(3)
await kinds.kindAt(file('a', 1), 0)
expect(parses()).toBe(3)
await kinds.kindAt(file('b', 1), 0)
expect(parses()).toBe(4)
})
})
+71
View File
@@ -0,0 +1,71 @@
import type { NamedKind } from './blockView'
import type { BlockKind, LanguageAdapter } from './contract'
import type { TreeSitterRuntime } from './runtime'
import { kindAt, readNamedKinds } from './blockView'
/** 定义所在的那份文件(TextDocument 的子集):uri、版本、语言、内容。 */
export interface KindSource {
uri: string
version: number
languageId: string
getText: () => string
}
/** 默认记几份文件 */
const LIMIT = 64
/**
* 调用的名字的定义是哪种块(分新建还是调用):定义所在的文件解析一遍,按 uri 记着这一版各块的名字和种类(见 NamedKind),
* 同一版再问不再解析,几处同时问同一版也只解析一次;预览发文档时把读出的那份交进来(remember),定义就在这份文档里时直接用。
* 最近用过的留 limit 份。这里不碰 VS Code。
*/
export class DefinitionKinds {
private readonly files = new Map<string, { version: number, kinds: Promise<NamedKind[] | undefined> }>()
constructor(
private readonly languageFor: (languageId: string) => LanguageAdapter | undefined,
private readonly runtime: Pick<TreeSitterRuntime, 'parse'>,
private readonly limit = LIMIT,
) {}
/** uri 这份文件 version 这一版各块的名字和种类(预览发文档时读出的)。 */
remember(uri: string, version: number, kinds: NamedKind[]): void {
this.keep(uri, version, Promise.resolve(kinds))
}
/** file 里 offset 处的名字定义的是哪种块;没有语言插件、不在哪块的名字上时没有。 */
async kindAt(file: KindSource, offset: number): Promise<BlockKind | undefined> {
const kinds = await this.kindsOf(file)
return kinds && kindAt(kinds, offset)
}
private kindsOf(file: KindSource): Promise<NamedKind[] | undefined> {
const known = this.files.get(file.uri)
if (known?.version === file.version) {
this.keep(file.uri, known.version, known.kinds)
return known.kinds
}
const adapter = this.languageFor(file.languageId)
if (!adapter)
return Promise.resolve(undefined)
const kinds = readNamedKinds(this.runtime, adapter, file.languageId, file.getText())
this.keep(file.uri, file.version, kinds)
// 解析失败的不记,下次重来
kinds.catch(() => {
if (this.files.get(file.uri)?.kinds === kinds)
this.files.delete(file.uri)
})
return kinds
}
/** 记下(或用到了)一份:挪到最近用过的那头,多了丢最久没用的。 */
private keep(uri: string, version: number, kinds: Promise<NamedKind[] | undefined>): void {
this.files.delete(uri)
this.files.set(uri, { version, kinds })
for (const oldest of this.files.keys()) {
if (this.files.size <= this.limit)
break
this.files.delete(oldest)
}
}
}
+6 -11
View File
@@ -1,10 +1,9 @@
import type { Location, LocationLink, Position } from 'vscode'
import type { BlockKind, LanguageAdapter, RuleLocation } from '../contract'
import type { TreeSitterRuntime } from '../runtime'
import type { BlockKind, RuleLocation } from '../contract'
import type { DefinitionKinds } from '../definitionKinds'
import type { DefinitionTarget } from './definitionAction'
import { commands, Range, Uri, window, workspace } from 'vscode'
import { logger } from '../../logging/logger'
import { readDefinitionKind } from '../blockView'
/** 查到的一处定义:文件和定义处那个名字的范围。 */
export interface FoundLocation { uri: Uri, range: Range }
@@ -131,16 +130,12 @@ export function ruleDefinitions(locations: readonly RuleLocation[]): FoundLocati
return locations.map(({ uri, start, end }) => ({ uri: Uri.parse(uri), range: new Range(start.line, start.character, end.line, end.character) }))
}
/** 定义处那个名字定义的是哪种块:打开目标文件,用它那种语言的语言插件分块认;目标打不开、语言不支持、不在哪块的名字上时没有。 */
export async function definitionKind(
location: FoundLocation,
languageFor: (languageId: string) => LanguageAdapter | undefined,
runtime: Pick<TreeSitterRuntime, 'parse'>,
): Promise<BlockKind | undefined> {
/** 定义处那个名字定义的是哪种块:打开目标文件,按它这一版各块的名字认(见 DefinitionKinds);目标打不开、语言不支持、不在哪块的名字上时没有。 */
export async function definitionKind(location: FoundLocation, kinds: DefinitionKinds): Promise<BlockKind | undefined> {
try {
const document = await workspace.openTextDocument(location.uri)
const adapter = languageFor(document.languageId)
return adapter && await readDefinitionKind(runtime, adapter, document.languageId, document.getText(), document.offsetAt(location.range.start))
const file = { uri: document.uri.toString(), version: document.version, languageId: document.languageId, getText: () => document.getText() }
return await kinds.kindAt(file, document.offsetAt(location.range.start))
}
catch (err) {
logger.add({ level: 'warn', source: 'codeReader', message: `认不出 ${location.uri.toString()} 里定义的是哪种块:${err instanceof Error ? err.message : String(err)}` })
@@ -1,3 +1,4 @@
import type { NamedKind } from '../blockView'
import type { DisplayName, LocalSymbol, RuleContext, SourceDocument } from '../contract'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
@@ -22,13 +23,14 @@ export interface DocumentSnapshot {
/**
* 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)、
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)。
* 各条导入引进来的名字(注释里不带路径的引用按它找导入的定义)、各块自己的名字和种类(调用的定义就在这份文档里时认种类用,见 NamedKind)。
*/
export interface DocumentRead {
message: DocumentMessage
displayNames: DisplayName[]
symbols: LocalSymbol[]
imports: ImportedName[]
kinds: NamedKind[]
}
/**
@@ -60,10 +62,10 @@ export async function documentMessage(
}
const adapter = registry.languageFor(languageId)
if (!adapter)
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [] }
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [], imports: [], kinds: [] }
try {
const rules = registry.rulesFor(languageId)
const { blocks, displayNames, symbols, imports } = await readBlockViews(runtime, adapter, translator, languageId, source, {
const { blocks, displayNames, symbols, imports, kinds } = await readBlockViews(runtime, adapter, translator, languageId, source, {
...(document.fileName && { fileName: document.fileName }),
rules: { rules, report, lookup },
calleeKind,
@@ -78,9 +80,9 @@ export async function documentMessage(
},
})
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols, imports }
return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols, imports, kinds }
}
catch (err) {
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [], imports: [] }
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [], imports: [], kinds: [] }
}
}
@@ -44,6 +44,7 @@ vi.mock('./documentMessage', () => ({
displayNames: [],
symbols: [],
imports: [],
kinds: [],
})),
}))
/** 文档符号由各个测试给 */
+10 -3
View File
@@ -26,6 +26,7 @@ import { DocumentCallees } from '../calleeKinds'
import { loadBlockCalls } from '../callTree/query'
import { CallTreeSession } from '../callTree/session'
import { commentFollowUp, commentJob, commentRewritePrompt, findBlock, writeCommentBody } from '../commentRewrite'
import { DefinitionKinds } from '../definitionKinds'
import { PreviewDiff } from '../diff/previewDiff'
import { blockReplacement } from '../editText'
import { locate } from '../folderStore'
@@ -77,6 +78,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
private readonly requests = new Map<string, OpenRequest>()
/** 哪个预览眼下能用键盘在高亮的各处出现之间跳,跟着它设快捷键 when 子句用的上下文键 */
readonly navigation = new NavigationContext<Preview>(value => void commands.executeCommand('setContext', OCCURRENCES_CONTEXT, value))
/** 调用的名字的定义是哪种块:各预览共用,定义所在的文件同一版只解析一次 */
readonly definitionKinds: DefinitionKinds
constructor(
private readonly extensionUri: Uri,
@@ -101,7 +104,9 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
readonly revealInTree: (uri: Uri) => Promise<boolean>,
/** 「了解一块代码」的对话页里接着说(subject 是那个功能,见 featureSubjectUri);说不了时返回给人看的原因 */
readonly askFeature: (subject: Uri, request: TopRequest) => Promise<string | undefined>,
) {}
) {
this.definitionKinds = new DefinitionKinds(languageId => registry.languageFor(languageId), runtime)
}
/**
* 注册成预览编辑器。标签页切走时页面留着(`retainContextWhenHidden`):切回来不用重新生成、重发文档(切走期间改过的才补一次),页面也不用整份重画,
@@ -404,12 +409,14 @@ class Preview {
const source = this.document.getText()
const session = this.lookups.session()
const callees = this.callees.session()
const { message, displayNames, symbols, imports } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
const { message, displayNames, symbols, imports, kinds } = await documentMessage({ languageId, version, getText: () => source, fileName: this.document.fileName, entryNames: this.owner.entries.namesOf(this.document.uri) }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
}, session.lookup, callees.calleeKind)
if (request !== this.latest)
return
this.definitions.remember(message.version, message.blocks)
// 调用的定义就在这份文档里时,认种类直接用这一遍读出的,不再解析一遍
this.owner.definitionKinds.remember(this.document.uri.toString(), version, kinds)
// 解析失败的文档没有块:问 AI 找不到块,也不拿它比差异
this.sent = message.error === undefined ? { version: message.version, blocks: message.blocks, source } : undefined
this.symbols = { version, list: symbols }
@@ -452,7 +459,7 @@ class Preview {
if (found.length === 0)
return undefined
for (const location of found) {
const kind = await definitionKind(location, languageId => this.registry.languageFor(languageId), this.runtime)
const kind = await definitionKind(location, this.owner.definitionKinds)
if (kind)
return kind
}