✨ feat(vscode): 代码阅读支持库规则,内置 SQLAlchemy 模型类显示成数据表卡片

This commit is contained in:
cruldra committed 2026-09-26 18:29:40 +08:00
1 parent 32be7d54b6
commit 5ece24cef9
26 files changed
+1768 -81

No files matched your search

@@ -1,8 +1,12 @@
import type { Memento } from 'vscode'
import type { LibraryRule } from '../contract'
import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from '../languages/python'
import { createTypeScriptAdapter } from '../languages/typescript'
import { CodeReaderRegistry } from '../registry'
import { sqlalchemyRule } from '../rules/sqlalchemy'
import { runtime, WASM_DIR } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { typescriptZhTranslator } from '../translators/typescriptZh'
import { documentMessage } from './documentMessage'
@@ -19,13 +23,33 @@ function registryWithTypeScript(): CodeReaderRegistry {
return registry
}
function registryWithPython(...rules: LibraryRule[]): CodeReaderRegistry {
const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick<Memento, 'get' | 'update'>)
registry.registerLanguage(createPythonAdapter(WASM_DIR))
registry.registerTranslator(pythonZhTranslator)
for (const rule of rules)
registry.registerLibraryRule(rule)
return registry
}
function ignore(): void {}
const MODEL = [
'from sqlalchemy.orm import Mapped, mapped_column',
'',
'class User(Base):',
' __tablename__ = "users"',
' id: Mapped[int] = mapped_column(primary_key=True)',
'',
].join('\n')
function doc(languageId: string, text: string, version = 3): { languageId: string, version: number, getText: () => string } {
return { languageId, version, getText: () => text }
}
describe('预览的 document 消息', () => {
it('受支持的语言:带文档版本、代码块、可选翻译和选中的翻译', async () => {
const message = await documentMessage(doc('typescript', 'if (!folder)\n return []\n'), registryWithTypeScript(), runtime)
const message = await documentMessage(doc('typescript', 'if (!folder)\n return []\n'), registryWithTypeScript(), runtime, ignore)
expect(message).toMatchObject({
type: 'document',
version: 3,
@@ -42,16 +66,43 @@ describe('预览的 document 消息', () => {
})
it('没有语言插件的语言:带 error,不解析', async () => {
const message = await documentMessage(doc('rust', 'fn main() {}'), registryWithTypeScript(), runtime)
const message = await documentMessage(doc('rust', 'fn main() {}'), registryWithTypeScript(), runtime, ignore)
expect(message.blocks).toEqual([])
expect(message.attributes).toEqual([])
expect(message.translators).toEqual([])
expect(message.error).toContain('rust')
})
it('库规则认领的块:种类是规则的种类 id,筛选栏里叫规则给的名字', async () => {
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 },
{ id: 'sqlalchemy.table', name: '数据表', count: 1 },
])
})
it('库规则抛错:不算解析失败,交给 report 记下,这块照常显示', async () => {
const broken: LibraryRule = {
id: 'broken',
label: '坏规则',
languageIds: ['python'],
libraries: ['sqlalchemy'],
claim: () => {
throw new Error('读不懂')
},
}
const reports: string[] = []
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) =>
`库规则 broken 看第 ${line} 行的 ${['import', 'class', 'variable', 'field'][i]} 块时出错,这块照常显示:读不懂`))
})
it('解析失败:把原因放进 error', async () => {
const failing = { parse: async () => Promise.reject(new Error('语法包加载失败')) }
const message = await documentMessage(doc('typescript', 'const a = 1'), registryWithTypeScript(), failing)
const message = await documentMessage(doc('typescript', 'const a = 1'), registryWithTypeScript(), failing, ignore)
expect(message.blocks).toEqual([])
expect(message.error).toBe('语法包加载失败')
})
@@ -11,11 +11,12 @@ export interface DocumentSnapshot {
getText: () => string
}
/** 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。 */
/** 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下。 */
export async function documentMessage(
document: DocumentSnapshot,
registry: Pick<CodeReaderRegistry, 'languageFor' | 'translatorsFor' | 'selectedTranslator'>,
registry: Pick<CodeReaderRegistry, 'languageFor' | 'translatorsFor' | 'selectedTranslator' | 'rulesFor'>,
runtime: Pick<TreeSitterRuntime, 'parse'>,
report: (message: string) => void,
): Promise<DocumentMessage> {
const { languageId, version } = document
const source = document.getText()
@@ -33,7 +34,7 @@ export async function documentMessage(
if (!adapter)
return { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }
try {
const blocks = await readBlockViews(runtime, adapter, translator, languageId, source)
const blocks = await readBlockViews(runtime, adapter, translator, languageId, source, { rules: registry.rulesFor(languageId), report })
return { ...message, blocks, attributes: fileAttributes(blocks, translator?.attributeNames) }
}
catch (err) {
@@ -141,7 +141,9 @@ class Preview {
if (!this.ready)
return
const request = ++this.latest
const message: ExtensionToWebview = await documentMessage(this.document, this.registry, this.runtime)
const message: ExtensionToWebview = await documentMessage(this.document, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
})
if (request !== this.latest || !this.ready)
return
const reveal = this.pendingReveal