diff --git a/vscode/src/codeReader/blockView.test.ts b/vscode/src/codeReader/blockView.test.ts index 5ab180c..be20e8d 100644 --- a/vscode/src/codeReader/blockView.test.ts +++ b/vscode/src/codeReader/blockView.test.ts @@ -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 => 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) }) }) diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index 49b0a42..130ee71 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -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, adapter: LanguageAdapter, languageId: string, source: string, - offset: number, -): Promise { +): Promise { 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() diff --git a/vscode/src/codeReader/definitionKinds.test.ts b/vscode/src/codeReader/definitionKinds.test.ts new file mode 100644 index 0000000..185e9dd --- /dev/null +++ b/vscode/src/codeReader/definitionKinds.test.ts @@ -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, 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) + }) +}) diff --git a/vscode/src/codeReader/definitionKinds.ts b/vscode/src/codeReader/definitionKinds.ts new file mode 100644 index 0000000..97f4bbe --- /dev/null +++ b/vscode/src/codeReader/definitionKinds.ts @@ -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 }>() + + constructor( + private readonly languageFor: (languageId: string) => LanguageAdapter | undefined, + private readonly runtime: Pick, + 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 { + const kinds = await this.kindsOf(file) + return kinds && kindAt(kinds, offset) + } + + private kindsOf(file: KindSource): Promise { + 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): 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) + } + } +} diff --git a/vscode/src/codeReader/editor/definitions.ts b/vscode/src/codeReader/editor/definitions.ts index 6c98d20..5f7a562 100644 --- a/vscode/src/codeReader/editor/definitions.ts +++ b/vscode/src/codeReader/editor/definitions.ts @@ -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, -): Promise { +/** 定义处那个名字定义的是哪种块:打开目标文件,按它这一版各块的名字认(见 DefinitionKinds);目标打不开、语言不支持、不在哪块的名字上时没有。 */ +export async function definitionKind(location: FoundLocation, kinds: DefinitionKinds): Promise { 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)}` }) diff --git a/vscode/src/codeReader/editor/documentMessage.ts b/vscode/src/codeReader/editor/documentMessage.ts index 5e56fe1..37cf637 100644 --- a/vscode/src/codeReader/editor/documentMessage.ts +++ b/vscode/src/codeReader/editor/documentMessage.ts @@ -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: [] } } } diff --git a/vscode/src/codeReader/editor/previewEditor.test.ts b/vscode/src/codeReader/editor/previewEditor.test.ts index 9da09f6..3af1a34 100644 --- a/vscode/src/codeReader/editor/previewEditor.test.ts +++ b/vscode/src/codeReader/editor/previewEditor.test.ts @@ -44,6 +44,7 @@ vi.mock('./documentMessage', () => ({ displayNames: [], symbols: [], imports: [], + kinds: [], })), })) /** 文档符号由各个测试给 */ diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index 2df310d..477ad63 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -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() /** 哪个预览眼下能用键盘在高亮的各处出现之间跳,跟着它设快捷键 when 子句用的上下文键 */ readonly navigation = new NavigationContext(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, /** 「了解一块代码」的对话页里接着说(subject 是那个功能,见 featureSubjectUri);说不了时返回给人看的原因 */ readonly askFeature: (subject: Uri, request: TopRequest) => Promise, - ) {} + ) { + 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 }