♻️ refactor(vscode): 代码阅读的翻译插件只经契约拿语言插件的词性和推导式结构

This commit is contained in:
cruldra committed 2026-09-26 16:24:36 +08:00
1 parent 07eaf96a6b
commit 202fa954ba
12 files changed
+185 -76

No files matched your search

+36 -37
View File
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -20,12 +20,14 @@ export interface ContextualWord {
export type TemplatePart = string | { field: string } | { namedChild: number }
/** 一种语言的中文直译表。引擎替换表里的关键字、内置类型、运算符,按节点模板重排少数语法结构,其余原样照抄。 */
/**
* 一种语言的中文词表:只有叫法和语法节点类型,语言知道的结构(词性、遍历)翻译时从 SourceDocument.language 取——
* 未翻译的词、关键字译文按它给的词性着色,它认出的遍历整个换成一个框,框里按读的顺序重排(见 iterationGroup)。
*/
export interface ZhTable {
id: string
label: string
languageIds: string[]
/** 未翻译的词按源码着色的词性上色;关键字译文的词性也取它。 */
tokenRole: (leaf: Node) => TokenRole
/** 关键字:按语法树里匿名节点的类型查(Python 的 `not in` 这类组合关键字本身也是一个匿名节点);随所在语法节点换译法的写成 ContextualWord。 */
keywords: Record<string, string | ContextualWord>
/** 字面量关键字:具名的叶子节点(true / None …),按节点类型查。 */
@@ -49,11 +51,6 @@ export interface ZhTable {
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/
nodeTemplates: Record<string, TemplatePart[]>
/**
* 写在表达式里的遍历(推导式、生成器表达式)的结构,语言插件给;没有这种写法的语言不填。
* 认出来的节点整个换成一个框,框里按读的顺序重排,见 iterationGroup。
*/
iteration?: (node: Node) => IterationFacts | undefined
}
interface Token {
@@ -73,7 +70,7 @@ interface Token {
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
interface Collector {
source: string
document: SourceDocument
tokens: Token[]
cursor: number
gap: boolean
@@ -184,13 +181,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/** 照抄这个节点时带不带源码下标:名字(按名字着色的具名叶子)和整段字符串带,标点、数字、关键字不带。 */
function offsetOf(node: Node): number | undefined {
const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(table.tokenRole(node)))
function offsetOf(node: Node, language: LanguageAdapter): number | undefined {
const linkable = stringNodes.has(node.type) || (node.childCount === 0 && node.isNamed && NAME_ROLES.has(language.tokenRole(node)))
return linkable ? node.startIndex : undefined
}
/** 能整个换成中文的节点 → 译文和词性。 */
function translateNode(node: Node): { text: string, role: TokenRole } | undefined {
function translateNode(node: Node, language: LanguageAdapter): { text: string, role: TokenRole } | undefined {
if (node.childCount === 0 && table.isTypePosition(node)) {
const type = table.builtinTypes[node.text]
if (type)
@@ -202,11 +199,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return { text: table.operators[node.type], role: 'operator' }
const word = table.keywords[node.type]
if (word !== undefined)
return { text: keywordText(word, node), role: table.tokenRole(firstLeaf(node)) }
return { text: keywordText(word, node), role: language.tokenRole(firstLeaf(node)) }
return undefined
}
if (node.childCount === 0 && table.literals[node.type])
return { text: table.literals[node.type], role: table.tokenRole(node) }
return { text: table.literals[node.type], role: language.tokenRole(node) }
if (table.emptyLiterals[node.type] && node.namedChildCount === 0)
return { text: table.emptyLiterals[node.type], role: 'literal' }
return undefined
@@ -223,7 +220,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */
function advance(out: Collector, index: number): void {
out.gap ||= /\s/.test(out.source.slice(out.cursor, index))
out.gap ||= /\s/.test(out.document.source.slice(out.cursor, index))
out.cursor = index
}
@@ -239,10 +236,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
function iterationIn(node: Node, span: SourceSpan): IterationFacts | undefined {
if (!table.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
function iterationIn(node: Node, span: SourceSpan, language: LanguageAdapter): IterationFacts | undefined {
if (!language.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
return undefined
return table.iteration(node)
return language.iteration(node)
}
/**
@@ -250,11 +247,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
* 同步、异步换了就另起一句),条件在它所在的位置写「,仅当 <条件>」,最后「,取 <值>」,字典是「<键>: <值>」。
* 来源、变量、条件、值照常翻,里面的名字带源码下标,套着的遍历是框里的框。
*/
function iterationGroup(facts: IterationFacts, span: SourceSpan, source: string): SegmentGroup {
function iterationGroup(facts: IterationFacts, span: SourceSpan, document: SourceDocument): SegmentGroup {
const segments: Inline[] = []
const plain = (text: string): void => pushSegment(segments, text, 'plain')
const word = (text: string): void => pushSegment(segments, text, 'keyword')
const say = (node: Node): void => appendInline(segments, joinTokens(tokensIn(node, span, source)))
const say = (node: Node): void => appendInline(segments, joinTokens(tokensIn(node, span, document)))
let previous: IterationClause | undefined
for (const clause of facts.clauses) {
if (clause.kind === 'if') {
@@ -295,12 +292,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/** 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 */
function emitIteration(out: Collector, node: Node, facts: IterationFacts, span: SourceSpan): void {
const shared = facts.sharedParentheses
const bracket = (paren: Node): Omit<Token, 'gap' | 'raw'> => ({ text: paren.text, role: table.tokenRole(paren), translated: false })
const bracket = (paren: Node): Omit<Token, 'gap' | 'raw'> => ({ text: paren.text, role: out.document.language.tokenRole(paren), translated: false })
if (shared)
emitSource(out, shared.open, bracket(shared.open))
else
advance(out, node.startIndex)
emit(out, { text: '', role: 'plain', translated: true, raw: '', group: iterationGroup(facts, span, out.source) })
emit(out, { text: '', role: 'plain', translated: true, raw: '', group: iterationGroup(facts, span, out.document) })
if (shared) {
out.cursor = shared.close.startIndex
emitSource(out, shared.close, bracket(shared.close))
@@ -315,7 +312,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
emitSource(out, node, { text: '', role: 'comment', translated: false })
return
}
const iteration = iterationIn(node, span)
const { language } = out.document
const iteration = iterationIn(node, span, language)
if (iteration) {
emitIteration(out, node, iteration, span)
return
@@ -335,14 +333,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
out.cursor = node.endIndex
return
}
const translated = translateNode(node)
const translated = translateNode(node, language)
if (translated) {
emitSource(out, node, { ...translated, translated: true })
return
}
if (node.childCount === 0 || stringNodes.has(node.type)) {
const role = stringNodes.has(node.type) ? 'string' : table.tokenRole(node)
emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node) })
const role = stringNodes.has(node.type) ? 'string' : language.tokenRole(node)
emitSource(out, node, { text: oneLine(node.text), role, translated: false, offset: offsetOf(node, language) })
return
}
for (const child of node.children) {
@@ -352,14 +350,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/** 按译文顺序收集 node 落在 span 里的词。 */
function tokensIn(node: Node, span: SourceSpan, source: string): Token[] {
const out: Collector = { source, tokens: [], cursor: span.startIndex, gap: false }
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument): Token[] {
const out: Collector = { document, tokens: [], cursor: span.startIndex, gap: false }
collect(node, span, out)
return out.tokens
}
/** node 的源码原样照抄:叶子按源码着色(给了 role 就都按它),叶子之间的空白、标点照抄。 */
function verbatim(node: Node, source: string, role?: TokenRole): Segment[] {
function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole): Segment[] {
const segments: Segment[] = []
let cursor = node.startIndex
const visit = (current: Node): void => {
@@ -373,7 +371,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return
}
pushSegment(segments, source.slice(cursor, current.startIndex), 'plain')
pushSegment(segments, current.text, role ?? table.tokenRole(current), offsetOf(current))
pushSegment(segments, current.text, role ?? language.tokenRole(current), offsetOf(current, language))
cursor = current.endIndex
}
visit(node)
@@ -388,10 +386,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return { markdown: docText(nodes.map(table.commentText)) }
}
function translate(block: TextBlock, source: string): PreviewText {
function translate(block: TextBlock, document: SourceDocument): PreviewText {
if (block.kind === 'comment')
return translateComment(block)
const tokens = tokensIn(block.node, block.header, source)
const tokens = tokensIn(block.node, block.header, document)
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))
@@ -399,12 +397,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return joinTokens(tokens)
}
function describe(block: FactBlock, source: string): PropertyRow[] {
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
const { header } = block
const { language } = document
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
const within = (node: Node, part: SourceSpan): Inline[] => {
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
return joinTokens(tokensIn(node, span, source))
return joinTokens(tokensIn(node, span, document))
}
const phrases: Phrases = {
translated: node => within(node, { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }),
@@ -413,10 +412,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
const name = table.mapLiterals[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
},
verbatim: node => verbatim(node, source),
verbatim: node => verbatim(node, document),
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'),
name: (node, role) => [segment(node.text, role, offsetOf(node))],
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node, language))] : verbatim(node, document, 'type'),
name: (node, role) => [segment(node.text, role, offsetOf(node, language))],
doc: (node) => {
const markdown = docText([table.commentText(node)])
return markdown ? { markdown } : undefined