✨ 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

+63 -15
View File
@@ -1,27 +1,33 @@
import type { BlockAttributes, BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceDocument, SourceRange, TokenRole, Translator } from './contract'
import type { BlockAttributes, CodeBlock, KindBadge, LanguageAdapter, LibraryRule, Node, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TokenRole, Translator } from './contract'
import type { TreeSitterRuntime } from './runtime'
import { blockAttributes } from './attributes'
import { commonIndent, splitLines } from './editText'
import { absorbedRange, activeRules, claimBlock, importsIn } from './libraryRules'
interface BlockViewBase {
/** 在树里的位置,例如 '6.1.0'。 */
id: string
kind: BlockKind
/** header 的起止行,0 起。 */
/** BlockKind,或库规则认领的块的种类 id(RuleBlock.kind,带点,例如 sqlalchemy.table)。 */
kind: string
/** 编辑范围的起止行,0 起:一般是 header;库规则认领的块到最后一个并进来的子块为止(见 RuleBlock.absorbs)。 */
startLine: number
endLine: number
/** header 所在的整行源码,去掉共同缩进。 */
/** 编辑范围的整行源码,去掉共同缩进。 */
code: string
/** 源码着色(拼起来等于 code)。 */
codeTokens: Segment[]
/** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */
defines?: SourceRange
/** 筛选属性(含 kind),见 blockAttributes。 */
/** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */
attributes: BlockAttributes
/** 库规则认领的块才有:种类标签的文字和颜色(RuleBlock.badge)。预览同类块一样默认收起它的子块。 */
badge?: KindBadge
children: BlockView[]
}
/** 发给 webview 的一块(可序列化):函数、类、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
/**
* 发给 webview 的一块(可序列化):函数、类、导入、常量、变量块和库规则认领的块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
*/
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
interface Line { start: number, end: number }
@@ -102,23 +108,64 @@ function ownName(block: CodeBlock): Node | undefined {
}
}
/** 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。 */
export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined): BlockView[] {
/** 用哪些库规则(某 languageId 注册的,按注册顺序;这份源码导入了规则关心的库才启用),规则出错时怎么记。 */
export interface RuleRun {
rules: readonly LibraryRule[]
report: (message: string) => void
}
const NO_RULES: RuleRun = { rules: [], report: () => {} }
/**
* 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock),并进去的子块不再单独成块。
*/
export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined, rules: RuleRun = NO_RULES): BlockView[] {
const lines = lineRanges(source)
const texts = splitLines(source)
const document: SourceDocument = { source, language: adapter }
const build = (block: CodeBlock, id: string): BlockView => {
const { startRow, endRow } = block.header
const imports = importsIn(blocks)
const active = activeRules(rules.rules, imports)
const context: RuleContext = {
document,
imports,
...(translator && { displayLanguage: translator.displayLanguage }),
translate: node => translator ? translator.expression(node, document) : [{ text: node.text, role: 'plain' }],
doc: node => translator?.doc(node, document),
}
/** startRow..endRow 这几行整行源码去共同缩进,和它的着色。 */
const sourceRows = (root: Node, startRow: number, endRow: number): Pick<BlockView, 'startLine' | 'endLine' | 'code' | 'codeTokens'> => {
const rows = texts.slice(startRow, endRow + 1)
const indent = commonIndent(rows).length
return {
startLine: startRow,
endLine: endRow,
code: rows.map(row => row.slice(Math.min(indent, row.length))).join('\n'),
codeTokens: colorLines(root, source, lines, startRow, endRow, indent, adapter.tokenRole),
}
}
const build = (block: CodeBlock, id: string): BlockView => {
const root = block.node.tree.rootNode
const claim = active.length > 0 ? claimBlock(block, active, context, rules.report) : undefined
if (claim) {
const { kind, badge, rows, defines, attributes } = claim.block
const { endRow, kept } = absorbedRange(block, claim.block.absorbs)
return {
id,
kind,
...sourceRows(root, block.header.startRow, endRow),
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
attributes: { ...blockAttributes(block), ...attributes, kind },
badge,
children: kept.map((child, i) => build(child, `${id}.${i}`)),
properties: rows,
}
}
const name = ownName(block)
return {
id,
kind: block.kind,
startLine: startRow,
endLine: endRow,
code: rows.map(row => row.slice(Math.min(indent, row.length))).join('\n'),
codeTokens: colorLines(block.node.tree.rootNode, source, lines, startRow, endRow, indent, adapter.tokenRole),
...sourceRows(root, block.header.startRow, block.header.endRow),
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
attributes: blockAttributes(block),
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
@@ -137,10 +184,11 @@ export async function readBlockViews(
translator: Translator | undefined,
languageId: string,
source: string,
rules: RuleRun = NO_RULES,
): Promise<BlockView[]> {
const tree = await runtime.parse(adapter, languageId, source)
try {
return toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), source, adapter, translator)
return toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), source, adapter, translator, rules)
}
finally {
tree.delete()