✨ feat(vscode): 代码阅读把推导式换成按读的顺序重排的遍历小框
This commit is contained in:
1 parent
677fb7da1f
commit
6416cdf714
16 files changed
+641
-91
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
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 { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -49,18 +49,26 @@ export interface ZhTable {
|
||||
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
|
||||
*/
|
||||
nodeTemplates: Record<string, TemplatePart[]>
|
||||
/**
|
||||
* 写在表达式里的遍历(推导式、生成器表达式)的结构,语言插件给;没有这种写法的语言不填。
|
||||
* 认出来的节点整个换成一个框,框里按读的顺序重排,见 iterationGroup。
|
||||
*/
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
}
|
||||
|
||||
interface Token {
|
||||
/** 注释是空文字:不进译文,只在前后留一个空格。 */
|
||||
text: string
|
||||
role: TokenRole
|
||||
translated: boolean
|
||||
/** 源码里的原文,用来认出逗号、括号;模板插入的译词没有原文。 */
|
||||
/** 源码里的原文,用来认出逗号、括号;模板插入的译词、框没有原文。 */
|
||||
raw: string
|
||||
/** 前面要空一格:源码里它前面有空白,或者模板在它前面插了别的项。 */
|
||||
gap: boolean
|
||||
/** 照抄的名字、字符串在源码里的起始下标,见 Segment.offset。 */
|
||||
offset?: number
|
||||
/** 整个换成一个框的节点(写在表达式里的遍历):输出这个框,text 为空。 */
|
||||
group?: SegmentGroup
|
||||
}
|
||||
|
||||
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
|
||||
@@ -90,10 +98,14 @@ function commentsWithin(node: Node, span: SourceSpan): Node[] {
|
||||
return node.children.flatMap(child => child ? commentsWithin(child, span) : [])
|
||||
}
|
||||
|
||||
function isComment(token: Token): boolean {
|
||||
return token.text === '' && !token.group
|
||||
}
|
||||
|
||||
/** 下一个要输出的词的原文(跳过注释)。 */
|
||||
function nextRaw(tokens: Token[], i: number): string {
|
||||
for (let j = i + 1; j < tokens.length; j++) {
|
||||
if (tokens[j].text !== '')
|
||||
if (!isComment(tokens[j]))
|
||||
return tokens[j].raw
|
||||
}
|
||||
return ''
|
||||
@@ -142,6 +154,7 @@ const KIND_ZH: Record<BlockKind, string> = {
|
||||
comment: '注释',
|
||||
}
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
|
||||
const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' }
|
||||
|
||||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||||
@@ -225,6 +238,76 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
|
||||
function iterationIn(node: Node, span: SourceSpan): IterationFacts | undefined {
|
||||
if (!table.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
return undefined
|
||||
return table.iteration(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历按读的顺序重排进一个框:「遍历 <来源> 的每个 <变量>」,紧挨着的几节 for 用顿号接着写(异步的写「异步遍历」,
|
||||
* 同步、异步换了就另起一句),条件在它所在的位置写「,仅当 <条件>」,最后「,取 <值>」,字典是「<键>: <值>」。
|
||||
* 来源、变量、条件、值照常翻,里面的名字带源码下标,套着的遍历是框里的框。
|
||||
*/
|
||||
function iterationGroup(facts: IterationFacts, span: SourceSpan, source: string): 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)))
|
||||
let previous: IterationClause | undefined
|
||||
for (const clause of facts.clauses) {
|
||||
if (clause.kind === 'if') {
|
||||
plain(',')
|
||||
word('仅当')
|
||||
plain(' ')
|
||||
say(clause.condition)
|
||||
}
|
||||
else {
|
||||
if (previous?.kind === 'for' && previous.async === clause.async) {
|
||||
plain('、')
|
||||
}
|
||||
else {
|
||||
if (previous)
|
||||
plain(',')
|
||||
word(clause.async ? '异步遍历' : '遍历')
|
||||
plain(' ')
|
||||
}
|
||||
say(clause.source)
|
||||
plain(' ')
|
||||
word('的每个')
|
||||
plain(' ')
|
||||
say(clause.variables)
|
||||
}
|
||||
previous = clause
|
||||
}
|
||||
plain(',')
|
||||
word('取')
|
||||
plain(' ')
|
||||
if (facts.key) {
|
||||
say(facts.key)
|
||||
plain(': ')
|
||||
}
|
||||
say(facts.value)
|
||||
return { style: 'iteration', label: ITERATION_ZH[facts.result], segments }
|
||||
}
|
||||
|
||||
/** 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 */
|
||||
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 })
|
||||
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) })
|
||||
if (shared) {
|
||||
out.cursor = shared.close.startIndex
|
||||
emitSource(out, shared.close, bracket(shared.close))
|
||||
}
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Collector): void {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
|
||||
return
|
||||
@@ -232,6 +315,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
emitSource(out, node, { text: '', role: 'comment', translated: false })
|
||||
return
|
||||
}
|
||||
const iteration = iterationIn(node, span)
|
||||
if (iteration) {
|
||||
emitIteration(out, node, iteration, span)
|
||||
return
|
||||
}
|
||||
const parts = templateParts(node, span)
|
||||
if (parts) {
|
||||
advance(out, node.startIndex)
|
||||
@@ -314,7 +402,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
function describe(block: FactBlock, source: string): PropertyRow[] {
|
||||
const { header } = block
|
||||
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
|
||||
const within = (node: Node, part: SourceSpan): Segment[] => {
|
||||
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))
|
||||
}
|
||||
@@ -353,30 +441,43 @@ function segment(text: string, role: TokenRole, offset: number | undefined): Seg
|
||||
return offset === undefined ? { text, role } : { text, role, offset }
|
||||
}
|
||||
|
||||
function pushSegment(segments: Segment[], text: string, role: TokenRole, offset?: number): void {
|
||||
/** 相邻的两段不带下标的普通文字并成一段;框不和别的并。 */
|
||||
function pushSegment(segments: Inline[], text: string, role: TokenRole, offset?: number): void {
|
||||
if (!text)
|
||||
return
|
||||
const prev = segments.at(-1)
|
||||
if (role === 'plain' && prev?.role === 'plain')
|
||||
if (role === 'plain' && prev && 'role' in prev && prev.role === 'plain')
|
||||
prev.text += text
|
||||
else
|
||||
segments.push(segment(text, role, offset))
|
||||
}
|
||||
|
||||
function appendInline(segments: Inline[], pieces: Inline[]): void {
|
||||
for (const piece of pieces) {
|
||||
if ('role' in piece)
|
||||
pushSegment(segments, piece.text, piece.role, piece.offset)
|
||||
else
|
||||
segments.push(piece)
|
||||
}
|
||||
}
|
||||
|
||||
/** 词连成译文:源码里隔着空白的词之间空一格,注释不进译文,右括号前的尾逗号去掉。 */
|
||||
function joinTokens(tokens: Token[]): Segment[] {
|
||||
const segments: Segment[] = []
|
||||
function joinTokens(tokens: Token[]): Inline[] {
|
||||
const segments: Inline[] = []
|
||||
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) {
|
||||
if (isComment(token) || trailingComma) {
|
||||
gap ||= token.role === 'comment'
|
||||
return
|
||||
}
|
||||
if (gap && segments.length > 0)
|
||||
pushSegment(segments, ' ', 'plain')
|
||||
pushSegment(segments, token.text, token.role, token.offset)
|
||||
if (token.group)
|
||||
segments.push(token.group)
|
||||
else
|
||||
pushSegment(segments, token.text, token.role, token.offset)
|
||||
// 「非」是个词,后面补一个空格,不和操作数粘在一起
|
||||
gap = token.translated && token.raw === '!'
|
||||
})
|
||||
@@ -388,9 +489,9 @@ function joinTokens(tokens: Token[]): Segment[] {
|
||||
/** 属性表里的值怎么写。 */
|
||||
interface Phrases {
|
||||
/** 按表翻译,只翻落在块头部里的那一段。 */
|
||||
translated: (node: Node) => Segment[]
|
||||
translated: (node: Node) => Inline[]
|
||||
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
|
||||
within: (node: Node, span: SourceSpan) => Segment[]
|
||||
within: (node: Node, span: SourceSpan) => Inline[]
|
||||
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
|
||||
entryCount: (literal: Node, size: number) => Segment[]
|
||||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||||
@@ -416,7 +517,7 @@ function count(n: number): Segment[] {
|
||||
return [hint(n > 0 ? `${n} 个` : '无')]
|
||||
}
|
||||
|
||||
function joined(parts: Segment[][], separator: string): Segment[] {
|
||||
function joined(parts: Inline[][], separator: string): Inline[] {
|
||||
return parts.flatMap((part, i) => i > 0 ? [{ text: separator, role: 'plain' as const }, ...part] : part)
|
||||
}
|
||||
|
||||
@@ -443,7 +544,7 @@ function importNodes(item: ImportItem): (Node | undefined)[] {
|
||||
}
|
||||
}
|
||||
|
||||
function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segment[] {
|
||||
function aliased(item: Inline[], alias: Node | undefined, say: Phrases): Inline[] {
|
||||
if (!alias)
|
||||
return item
|
||||
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
|
||||
@@ -478,7 +579,7 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
|
||||
*/
|
||||
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
||||
const listRow = (label: string, parts: Segment[][], defines: Defines = {}): PropertyRow[] =>
|
||||
const listRow = (label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] =>
|
||||
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||||
@@ -505,7 +606,7 @@ function decoratorRows(decorators: Node[], say: Phrases): PropertyRow[] {
|
||||
|
||||
/** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */
|
||||
function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
const itemText = (item: ImportItem): Segment[] => {
|
||||
const itemText = (item: ImportItem): Inline[] => {
|
||||
switch (item.kind) {
|
||||
case 'named':
|
||||
return aliased(say.translated(item.name), item.alias, say)
|
||||
@@ -540,7 +641,7 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
|
||||
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
|
||||
*/
|
||||
function literalValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): { value: Segment[], items?: PropertyItem[] } {
|
||||
function literalValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): { value: Inline[], items?: PropertyItem[] } {
|
||||
if (!entries?.length)
|
||||
return { value: say.translated(value) }
|
||||
return { value: say.entryCount(value, entries.length), items: entries.map(entry => entryItem(entry, say)) }
|
||||
|
||||
Reference in new issue
Block a user