✨ feat(vscode): 代码阅读函数、导入、常量、变量块预览改成属性表

This commit is contained in:
cruldra committed 2026-09-25 04:56:22 +08:00
1 parent 0c4c9b0fba
commit dd9b39ac5d
19 files changed
+1458 -213

No files matched your search

+189 -31
View File
@@ -1,4 +1,4 @@
import type { CodeBlock, Node, Segment, SourceSpan, TokenRole, Translator } from '../contract'
import type { Access, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
export interface ContextualWord {
@@ -210,7 +210,37 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
function translateComment(block: CodeBlock): Segment[] {
/** 按译文顺序收集 node 落在 span 里的词。 */
function tokensIn(node: Node, span: SourceSpan, source: string): Token[] {
const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false }
collect(node, span, out)
return out.tokens
}
/** node 的源码原样照抄:叶子按源码着色,叶子之间的空白、标点照抄。 */
function verbatim(node: Node, source: string): Segment[] {
const segments: Segment[] = []
let cursor = node.startIndex
const visit = (current: Node): void => {
if (current.startIndex === current.endIndex)
return
if (current.childCount > 0) {
for (const child of current.children) {
if (child)
visit(child)
}
return
}
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain')
pushSegment(segments, current.text, table.tokenRole(current))
cursor = current.endIndex
}
visit(node)
pushSegment(segments, source.slice(cursor, node.endIndex), 'plain')
return segments
}
function translateComment(block: TextBlock): Segment[] {
const { startIndex, endIndex } = block.header
const root = block.node.tree.rootNode
const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node]
@@ -218,41 +248,169 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return text ? [{ text, role: 'comment' }] : []
}
function translate(block: CodeBlock, source: string): Segment[] {
function translate(block: TextBlock, source: string): Segment[] {
if (block.kind === 'comment')
return translateComment(block)
const out: Collector = { source, tokens: [], cursor: block.header.startIndex, gap: false }
collect(block.node, block.header, out)
const { tokens } = out
const tokens = tokensIn(block.node, block.header, source)
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
const last = tokens.at(-1)
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
tokens.pop()
const segments: Segment[] = []
const push = (text: string, role: TokenRole): void => {
const prev = segments.at(-1)
if (role === 'plain' && prev?.role === 'plain')
prev.text += text
else
segments.push({ text, role })
}
let gap = false
tokens.forEach((token, i) => {
gap ||= token.gap
const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i))
if (token.text === '' || trailingComma) {
gap ||= token.role === 'comment'
return
}
if (gap && segments.length > 0)
push(' ', 'plain')
push(token.text, token.role)
// 「非」是个词,后面补一个空格,不和操作数粘在一起
gap = token.translated && token.raw === '!'
})
return segments
return joinTokens(tokens)
}
return { id: table.id, label: table.label, languageIds: table.languageIds, translate }
function describe(block: FactBlock, source: string): PropertyRow[] {
const { header } = block
const phrases: Phrases = {
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
translated: (node) => {
const clipped = node.endIndex > header.endIndex
const span = {
startIndex: node.startIndex,
endIndex: clipped ? header.endIndex : node.endIndex,
startRow: node.startPosition.row,
endRow: clipped ? header.endRow : node.endPosition.row,
}
return joinTokens(tokensIn(node, span, source))
},
verbatim: node => verbatim(node, source),
module: node => [{ text: node.text, role: stringNodes.has(node.type) ? 'string' : 'type' }],
}
switch (block.kind) {
case 'function':
return functionRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
default:
return variableRows(block, phrases)
}
}
return { id: table.id, label: table.label, languageIds: table.languageIds, translate, describe }
}
function pushSegment(segments: Segment[], text: string, role: TokenRole): void {
if (!text)
return
const prev = segments.at(-1)
if (role === 'plain' && prev?.role === 'plain')
prev.text += text
else
segments.push({ text, role })
}
/** 词连成译文:源码里隔着空白的词之间空一格,注释不进译文,右括号前的尾逗号去掉。 */
function joinTokens(tokens: Token[]): Segment[] {
const segments: Segment[] = []
let gap = false
tokens.forEach((token, i) => {
gap ||= token.gap
const trailingComma = token.raw === ',' && !token.translated && CLOSING.has(nextRaw(tokens, i))
if (token.text === '' || trailingComma) {
gap ||= token.role === 'comment'
return
}
if (gap && segments.length > 0)
pushSegment(segments, ' ', 'plain')
pushSegment(segments, token.text, token.role)
// 「非」是个词,后面补一个空格,不和操作数粘在一起
gap = token.translated && token.raw === '!'
})
return segments
}
// ─── 属性表 ───────────────────────────────────────────────────────────────────
/** 属性表里值的三种来源。 */
interface Phrases {
/** 按表翻译,只翻落在块头部里的那一段。 */
translated: (node: Node) => Segment[]
/** 源码原样照抄、按源码着色(装饰器)。 */
verbatim: (node: Node) => Segment[]
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
module: (node: Node) => Segment[]
}
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
function hint(text: string): Segment {
return { text, role: 'hint' }
}
function yesNo(value: boolean): Segment[] {
return [{ text: value ? '是' : '否', role: 'storage' }]
}
function count(n: number): Segment[] {
return [hint(n > 0 ? `${n} 个` : '无')]
}
function joined(parts: Segment[][], separator: string): Segment[] {
return parts.flatMap((part, i) => i > 0 ? [{ text: separator, role: 'plain' as const }, ...part] : part)
}
function accessRows(access: Access | undefined): PropertyRow[] {
return access ? [{ label: '访问级别', value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : []
}
function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segment[] {
if (!alias)
return item
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
}
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
const decorators = facts.decorators.map(say.verbatim)
return [
{ label: '名称', value: facts.name ? [{ text: facts.name.text, role: 'function' }] : [hint('匿名')] },
...accessRows(facts.access),
{ label: '异步', value: yesNo(facts.async) },
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
{
label: '输入参数',
value: count(facts.parameters.length),
items: facts.parameters.map(param => ({
cells: [
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
param.type ? say.translated(param.type) : [hint('未标注')],
param.default ? [hint('默认 '), ...say.translated(param.default)] : [],
],
})),
},
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
]
}
/** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */
function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
const itemText = (item: ImportItem): Segment[] => {
switch (item.kind) {
case 'named':
return aliased(say.translated(item.name), item.alias, say)
case 'default':
return aliased([hint('默认导出')], item.alias, say)
case 'namespace':
return aliased([hint('全部')], item.alias, say)
case 'module':
// 一条语句导入多个模块时(Python 的 import a, b)要写出是哪个模块
return aliased(facts.sources.length > 1 ? say.module(item.module) : [hint('整个模块')], item.alias, say)
}
}
return [
{ label: '来源', value: facts.sources.length > 0 ? joined(facts.sources.map(say.module), ', ') : [hint('无')] },
...(facts.typeOnly === undefined ? [] : [{ label: '仅类型', value: yesNo(facts.typeOnly) }]),
{ label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)] })) },
]
}
/** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值)。 */
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
const valueLabel = block.kind === 'const' ? '值' : '初始值'
return block.facts.declarators.flatMap(declarator => [
{ label: '名称', value: say.translated(declarator.name) },
...accessRows(block.facts.access),
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
{ label: valueLabel, value: declarator.value ? say.translated(declarator.value) : [hint('无')] },
])
}