✨ 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

@@ -12,7 +12,7 @@ const py = createPythonAdapter(WASM_DIR)
const SNAPSHOT = readFixture('celeryApp.py')
function views(source: string): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, source, py, pythonZhTranslator, { rules: [celeryRule], report: () => {} }))
return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [celeryRule], report: () => {} }))
}
/** 记下问了哪边:规则(引用的键)还是语言扩展(下标)。 */
@@ -49,7 +49,7 @@ function doc(languageId: string, text: string, version = 3): { languageId: strin
describe('预览的 document 消息', () => {
it('受支持的语言:带文档版本、代码块、可选翻译和选中的翻译', async () => {
const message = await documentMessage(doc('typescript', 'if (!folder)\n return []\n'), registryWithTypeScript(), runtime, ignore)
const { message } = await documentMessage(doc('typescript', 'if (!folder)\n return []\n'), registryWithTypeScript(), runtime, ignore)
expect(message).toMatchObject({
type: 'document',
version: 3,
@@ -66,7 +66,7 @@ describe('预览的 document 消息', () => {
})
it('没有语言插件的语言:带 error,不解析', async () => {
const message = await documentMessage(doc('rust', 'fn main() {}'), registryWithTypeScript(), runtime, ignore)
const { message } = await documentMessage(doc('rust', 'fn main() {}'), registryWithTypeScript(), runtime, ignore)
expect(message.blocks).toEqual([])
expect(message.attributes).toEqual([])
expect(message.translators).toEqual([])
@@ -74,7 +74,7 @@ describe('预览的 document 消息', () => {
})
it('库规则认领的块:种类是规则的种类 id,筛选栏里叫规则给的名字', async () => {
const message = await documentMessage(doc('python', MODEL), registryWithPython(sqlalchemyRule), runtime, ignore)
const { message } = await documentMessage(doc('python', MODEL), registryWithPython(sqlalchemyRule), runtime, ignore)
expect(message.blocks.map(b => b.kind)).toEqual(['import', 'sqlalchemy.table'])
expect(message.attributes.find(attribute => attribute.id === 'kind')?.values).toEqual([
{ id: 'import', name: '导入', count: 1 },
@@ -93,7 +93,7 @@ describe('预览的 document 消息', () => {
},
}
const reports: string[] = []
const message = await documentMessage(doc('python', MODEL), registryWithPython(broken), runtime, text => reports.push(text))
const { message } = await documentMessage(doc('python', MODEL), registryWithPython(broken), runtime, text => reports.push(text))
expect(message.error).toBeUndefined()
expect(message.blocks.map(b => b.kind)).toEqual(['import', 'class'])
expect(reports).toEqual([1, 3, 4, 5].map((line, i) =>
@@ -102,7 +102,7 @@ describe('预览的 document 消息', () => {
it('解析失败:把原因放进 error', async () => {
const failing = { parse: async () => Promise.reject(new Error('语法包加载失败')) }
const message = await documentMessage(doc('typescript', 'const a = 1'), registryWithTypeScript(), failing, ignore)
const { message } = await documentMessage(doc('typescript', 'const a = 1'), registryWithTypeScript(), failing, ignore)
expect(message.blocks).toEqual([])
expect(message.error).toBe('语法包加载失败')
})
@@ -1,3 +1,4 @@
import type { DisplayName } from '../contract'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
import type { DocumentMessage } from './messages'
@@ -11,13 +12,19 @@ export interface DocumentSnapshot {
getText: () => string
}
/** 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)。 */
export interface DocumentRead {
message: DocumentMessage
displayNames: DisplayName[]
}
/** 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下。 */
export async function documentMessage(
document: DocumentSnapshot,
registry: Pick<CodeReaderRegistry, 'languageFor' | 'translatorsFor' | 'selectedTranslator' | 'rulesFor'>,
runtime: Pick<TreeSitterRuntime, 'parse'>,
report: (message: string) => void,
): Promise<DocumentMessage> {
): Promise<DocumentRead> {
const { languageId, version } = document
const source = document.getText()
const translator = registry.selectedTranslator(languageId)
@@ -29,15 +36,16 @@ export async function documentMessage(
attributes: [],
translators: registry.translatorsFor(languageId).map(({ id, label }) => ({ id, label })),
translatorId: translator?.id ?? '',
names: [],
}
const adapter = registry.languageFor(languageId)
if (!adapter)
return { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [] }
try {
const blocks = await readBlockViews(runtime, adapter, translator, languageId, source, { rules: registry.rulesFor(languageId), report })
return { ...message, blocks, attributes: fileAttributes(blocks, translator?.attributeNames) }
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules: registry.rulesFor(languageId), report })
return { message: { ...message, blocks, attributes: fileAttributes(blocks, translator?.attributeNames) }, displayNames }
}
catch (err) {
return { ...message, error: err instanceof Error ? err.message : String(err) }
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [] }
}
}
+6 -1
View File
@@ -5,11 +5,12 @@
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { FileAttribute } from '../attributes'
import type { BlockView } from '../blockView'
import type { ShownName } from '../nameResolution'
import type { ProfileChoice } from '../qa/askProfiles'
import type { RunningAsk } from '../qa/asks'
import type { Excluded } from './blockFilter'
export type { Excluded }
export type { Excluded, ShownName }
export interface TranslatorOption { id: string, label: string }
@@ -45,6 +46,8 @@ export interface DocumentMessage {
error?: string
/** 新预览只在第一份 document 里带:从文本编辑器切过来时是它顶部那一行,跳到定义时是定义处 */
reveal?: Reveal
/** 这个版本眼下已知的中文名:查过的,或上一版查到的跟着改动挪了位置的;查完再发 displayNames 换掉 */
names: ShownName[]
}
/** 问过的一次(给预览看的):问题、回答(Markdown)、什么时候问的(ISO 时间)、用哪个 profile 答的。 */
@@ -86,6 +89,7 @@ export interface QaMessage {
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
* answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现,由这两条命令发给活动的预览
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
*/
export type ExtensionToWebview
= | DocumentMessage
@@ -93,6 +97,7 @@ export type ExtensionToWebview
| { type: 'definitionProbed', id: number, found: boolean }
| { type: 'occurrencesFound', id: number, occurrences: Occurrence[] }
| { type: 'moveOccurrence', step: 1 | -1 }
| { type: 'displayNames', version: number, names: ShownName[] }
| { type: 'filter', excluded: Excluded }
| QaMessage
| ({ type: 'askProfiles' } & ProfileChoice)
+32 -4
View File
@@ -2,6 +2,7 @@ import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentSh
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { BlockView } from '../blockView'
import type { RuleLocation, RuleReference } from '../contract'
import type { IndexedName, NameSources } from '../nameResolution'
import type { CodeQa } from '../qa/codeQa'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
@@ -13,6 +14,7 @@ import { logger } from '../../logging/logger'
import { webviewHtml, webviewUiRoot } from '../../webview/html'
import { blockReplacement } from '../editText'
import { locate } from '../folderStore'
import { DocumentNames, resolveNames } from '../nameResolution'
import { askPrompt, followUpPrompt } from '../qa/prompt'
import { definitionAction } from './definitionAction'
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
@@ -46,6 +48,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
readonly resolveReference: (reference: RuleReference) => Promise<RuleLocation[]>,
/** 问 AI:各预览共用,同一份文件开在几处时问答、正在答的问题都一样 */
readonly qa: CodeQa,
/** 工作区里别的文件定义的、原名叫 name 的中文名(languageId 是查的这份文档的语言) */
readonly indexedNames: (languageId: string, name: string) => Promise<IndexedName[]>,
) {}
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
@@ -118,6 +122,8 @@ class Preview {
visibleLine: number | undefined
/** 最近一次发出去的块树和它的源码:问 AI 时按块的记号在里面找那块代码 */
private sent: { blocks: BlockView[], source: string } | undefined
/** 这份文档各版本显示成中文名的名字 */
private readonly names: DocumentNames
constructor(
private readonly document: TextDocument,
@@ -136,6 +142,10 @@ class Preview {
rule: async reference => ruleDefinitions(await owner.resolveReference(reference)),
language: offset => languageDefinitions(document.uri, document.positionAt(offset)),
})
this.names = new DocumentNames((version, names) => {
if (this.ready && version === document.version)
this.post({ type: 'displayNames', version, names })
})
this.disposables = [
panel.webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)),
panel.onDidChangeViewState(() => {
@@ -146,8 +156,10 @@ class Preview {
owner.navigation.update(this, false)
}),
workspace.onDidChangeTextDocument((e) => {
if (e.document === document && e.contentChanges.length > 0)
this.schedulePush()
if (e.document !== document || e.contentChanges.length === 0)
return
this.names.edited(document.version, e.contentChanges.map(change => ({ offset: change.rangeOffset, length: change.rangeLength, text: change.text })))
this.schedulePush()
}),
// 页面没 ready 时收不到,等它 ready 再随第一份文档之前补发
filter.subscribe((excluded) => {
@@ -202,7 +214,7 @@ class Preview {
const request = ++this.latest
const { languageId, version } = this.document
const source = this.document.getText()
const message = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
const { message, displayNames } = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
})
if (request !== this.latest || !this.ready)
@@ -211,7 +223,22 @@ class Preview {
this.sent = { blocks: message.blocks, source }
const reveal = this.pendingReveal
this.pendingReveal = undefined
this.post(reveal === undefined ? message : { ...message, reveal })
this.post({ ...message, names: this.names.known(version), ...(reveal && { reveal }) })
// 文档在解析期间又改了的,等下一次发文档再查
if (version === this.document.version)
this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(), report))
}
/** 找中文名落在哪些名字上时问的:这份文档的语言扩展、工作区里的中文名索引。 */
private nameSources(): NameSources {
const { document } = this
return {
uri: document.uri.toString(),
occurrences: async offset => (await occurrencesOf(document, offset)).map(({ start, end }) => ({ start, end })),
definitions: async offset => (await languageDefinitions(document.uri, document.positionAt(offset)))
.map(({ uri, range }) => ({ uri: uri.toString(), line: range.start.line, character: range.start.character })),
indexed: name => this.owner.indexedNames(document.languageId, name),
}
}
/** 跳到高亮着的名字的下一处 / 上一处出现;页面不在就算了(页面重新加载时高亮已经没了)。 */
@@ -230,6 +257,7 @@ class Preview {
dispose(): void {
clearTimeout(this.timer)
this.names.dispose()
for (const disposable of this.disposables)
disposable.dispose()
}