✨ feat(vscode): 代码阅读函数、导入、常量、变量块预览改成属性表
This commit is contained in:
1 parent
0c4c9b0fba
commit
dd9b39ac5d
19 files changed
+1458
-213
No files matched your search
@@ -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('无')] },
|
||||
])
|
||||
}
|
||||
Reference in new issue
Block a user