✨ feat(vscode): 代码阅读认出代码里起的中文名(Annotated[…, Comment("…")]、@Comment 装饰器、行尾 # @Comment、docstring / JSDoc 的参数说明),预览里定义和用到它的各处都写中文名,悬停看原名,定义处后面淡淡写原名
This commit is contained in:
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: [] }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user