✨ feat(vscode): 代码阅读标出项目写明的入口文件和入口函数

This commit is contained in:
cruldra committed 2026-10-08 07:17:25 +08:00
1 parent 78d5748b81
commit 718bbb7f6c
19 files changed
+761 -83

No files matched your search

@@ -1,4 +1,5 @@
import type { Memento } from 'vscode'
import type { BlockView } from '../blockView'
import type { LibraryRule } from '../contract'
import { describe, expect, it } from 'vitest'
import { packageCommandRules } from '../commands/packageCommands'
@@ -6,14 +7,16 @@ import { shellCommandRules } from '../commands/shellCommands'
import { createBashAdapter } from '../languages/bash'
import { createDockerfileAdapter } from '../languages/dockerfile'
import { createPythonAdapter } from '../languages/python'
import { createRustAdapter } from '../languages/rust'
import { createTypeScriptAdapter } from '../languages/typescript'
import { CodeReaderRegistry } from '../registry'
import { fastapiRule } from '../rules/fastapi'
import { sqlalchemyRule } from '../rules/sqlalchemy'
import { DOCKERFILE_WASM_DIR, runtime, WASM_DIR } from '../testSupport'
import { DOCKERFILE_WASM_DIR, propertyLines, runtime, WASM_DIR } from '../testSupport'
import { bashZhTranslator } from '../translators/bashZh'
import { dockerfileZhTranslator } from '../translators/dockerfileZh'
import { pythonZhTranslator } from '../translators/pythonZh'
import { rustZhTranslator } from '../translators/rustZh'
import { typescriptZhTranslator } from '../translators/typescriptZh'
import { documentMessage } from './documentMessage'
@@ -196,3 +199,44 @@ describe('预览的 document 消息', () => {
expect(message.error).toBe('语法包加载失败')
})
})
describe('入口文件的预览', () => {
const CLI = [
'import typer',
'',
'app = typer.Typer()',
'',
'',
'def main():',
' pass',
'',
'',
'def helper():',
' pass',
'',
].join('\n')
const nameRow = (block: BlockView): string => 'properties' in block ? propertyLines(block.properties)[0] : ''
it('名字对上的块(变量、函数都算),名称那一行名字后面加浅色的「入口」', async () => {
const { message } = await documentMessage({ ...doc('python', CLI), entryNames: ['app', 'main'] }, registryWithPython(), runtime, ignore)
expect(message.blocks.slice(1).map(nameRow)).toEqual(['名称: app 入口', '名称: main 入口', '名称: helper'])
const [, app] = message.blocks
expect('properties' in app && app.properties[0].value).toContainEqual({ text: ' 入口', role: 'hint' })
})
it('对不上名字、不是入口:什么都不加', async () => {
const plain = await documentMessage(doc('python', CLI), registryWithPython(), runtime, ignore)
const unmatched = await documentMessage({ ...doc('python', CLI), entryNames: ['run'] }, registryWithPython(), runtime, ignore)
expect(unmatched.message.blocks).toEqual(plain.message.blocks)
expect(plain.message.blocks.map(nameRow).join('\n')).not.toContain('入口')
})
it('rust:fn main 的名称后面加「入口」', async () => {
const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick<Memento, 'get' | 'update'>)
registry.registerLanguage(createRustAdapter(WASM_DIR))
registry.registerTranslator(rustZhTranslator)
const { message } = await documentMessage({ ...doc('rust', 'fn main() {\n run();\n}\n\nfn run() {}\n'), entryNames: ['main'] }, registry, runtime, ignore)
expect(message.blocks.map(nameRow)).toEqual(['名称: main 入口', '名称: run'])
})
})
@@ -5,6 +5,7 @@ import type { DocumentMessage } from './messages'
import { fileAttributes, mergeAttributeNames } from '../attributes'
import { readBlockViews } from '../blockView'
import { commandRuleLookup } from '../commandRules'
import { markEntryNames } from '../entries/entryMarks'
import { ruleAttributeNames } from '../libraryRules'
/** 生成预览数据要读的那点文档信息(TextDocument 的子集)。 */
@@ -12,6 +13,8 @@ export interface DocumentSnapshot {
languageId: string
version: number
getText: () => string
/** 这份文件是项目写明的入口时,预览里要标「入口」的名字(见 entries/entryPoints.ts) */
entryNames?: readonly string[]
}
/** 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)。 */
@@ -25,6 +28,7 @@ export interface DocumentRead {
* 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下;
* 规则查名字的定义时问 lookup(RuleContext.lookup),翻译插件问调用的名字定义成哪种块时问 calleeKind(SourceDocument.calleeKind)。
* 筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
* 入口文件里名字对上的块,名称那一行在翻译好的结果上加「入口」(markEntryNames)。
*/
export async function documentMessage(
document: DocumentSnapshot,
@@ -66,7 +70,7 @@ export async function documentMessage(
},
})
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames, symbols }
return { message: { ...message, blocks: markEntryNames(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols }
}
catch (err) {
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [] }
+11 -3
View File
@@ -3,6 +3,7 @@ import type { ClaudeImage } from '../../cc/spawnClaude'
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
import type { DiffBase } from '../diff/diffBase'
import type { OldDocument, SentDocument } from '../diff/previewDiff'
import type { WorkspaceEntries } from '../entries/workspaceEntries'
import type { DefinitionStart, IndexedName, NameSources } from '../nameResolution'
import type { CodeQa } from '../qa/codeQa'
import type { CodeReaderRegistry } from '../registry'
@@ -76,6 +77,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
readonly indexed: (index: WorkspaceIndex, key: string) => Promise<IndexedEntry[]>,
/** 预览标差异时对比什么 */
readonly diffBases: DiffBases,
/** 哪些文件是项目写明的入口、预览里标哪些名字 */
readonly entries: Pick<WorkspaceEntries, 'namesOf' | 'onDidChange'>,
) {}
/** 侧栏「问 AI」打开预览,并把光标放进顶部输入框。已经开着的就揭到前面再聚焦。 */
@@ -253,6 +256,11 @@ class Preview {
if (this.ready)
this.post({ type: 'filter', ...state })
}),
// 项目的入口变了:重新发文档,名称那一行的「入口」跟着变
owner.entries.onDidChange((uris) => {
if (uris.some(uri => uri.toString() === document.uri.toString()))
this.schedulePush()
}),
owner.qa.onDidChange((uris) => {
if (uris.some(uri => uri.toString() === this.subject().uri.toString()))
this.postQa()
@@ -320,7 +328,7 @@ class Preview {
const source = this.document.getText()
const session = this.lookups.session()
const callees = this.callees.session()
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source, 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 || !this.ready)
@@ -344,11 +352,11 @@ class Preview {
/**
* 对比基准里的旧版本排成视图数据:和这份文档同一种语言、同一个翻译插件和库规则;规则查定义、调用的种类用这份文档眼下查到的答案,
* 两边同样的写法翻得一样,不会因为答案晚到被当成改了。旧版本里规则出的错不记(记的是现在这份的)。
* 两边同样的写法翻得一样,不会因为答案晚到被当成改了;项目入口也按眼下的标,「入口」两边都有。旧版本里规则出的错不记(记的是现在这份的)。
*/
private async readOld(source: string): Promise<OldDocument | undefined> {
const { message } = await documentMessage(
{ languageId: this.document.languageId, version: this.document.version, getText: () => source },
{ languageId: this.document.languageId, version: this.document.version, getText: () => source, entryNames: this.owner.entries.namesOf(this.document.uri) },
this.registry,
this.runtime,
() => {},