✨ feat(vscode): 代码阅读预览里跨几行写的列表按项拆开,列表里整项是调用的带着自己的实参表,一层层能往下套

This commit is contained in:
cruldra committed 2026-10-13 13:32:45 +11:00
1 parent 7d6e7592c9
commit 2c490cac2d
22 files changed
+848 -314

No files matched your search

+1 -1
View File
@@ -551,7 +551,7 @@ export const bashZhTranslator: Translator = createZhTemplateTranslator({
operators: {},
operatorParents: [],
emptyLiterals: {},
mapLiterals: {},
collections: {},
stringNodes: [],
commentText,
nodeTemplates: {},
@@ -45,7 +45,7 @@ const translator = createZhTemplateTranslator({
operators: {},
operatorParents: [],
emptyLiterals: {},
mapLiterals: {},
collections: {},
stringNodes: [],
commentText: node => node.text,
nodeTemplates: {},
@@ -392,7 +392,7 @@ export const dockerfileZhTranslator = createZhTemplateTranslator({
operators: {},
operatorParents: [],
emptyLiterals: {},
mapLiterals: {},
collections: {},
stringNodes: [],
commentText: (_node, text) => text.replace(/^#[ \t]?/, ''),
nodeTemplates: {},
@@ -89,7 +89,7 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
operators: { '==': '等于', '!=': '不等于' },
operatorParents: ['comparison_operator'],
emptyLiterals: { list: '空列表', dictionary: '空字典' },
mapLiterals: { dictionary: '字典' },
collections: { dictionary: '字典', list: '列表', tuple: '元组', set: '集合' },
stringNodes: ['string'],
commentText,
// `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名
+1 -1
View File
@@ -50,7 +50,7 @@ const template = createZhTemplateTranslator({
operators: {},
operatorParents: [],
emptyLiterals: {},
mapLiterals: {},
collections: { array_expression: '数组', tuple_expression: '元组' },
stringNodes: ['string_literal'],
commentText,
docNodes,
@@ -83,7 +83,7 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者', '!': '非' },
operatorParents: ['binary_expression', 'unary_expression'],
emptyLiterals: { array: '空数组', object: '空对象' },
mapLiterals: { object: '对象' },
collections: { object: '对象', array: '数组' },
stringNodes: ['string', 'template_string', 'regex'],
commentText,
nodeTemplates: {
@@ -1336,7 +1336,8 @@ describe('跨几行写的调用', () => {
expect(value(1)).toBe('初始值: (调用 make · 1 个参数)[0]')
expect(value(4)).toBe('初始值: (调用 make · 1 个参数) 或者 fallback')
expect(value(7)).toBe('初始值: fallback 或者 调用 make · 1 个参数')
expect(value(10)).toBe('初始值: [ 调用 make · 1 个参数, b ]')
// 跨几行写的列表按项拆开(见「跨几行写的列表」),整项是调用的不括
expect(propertyLines(values.get(10) ?? []).slice(2)).toEqual(['初始值: 列表 2 项', ' 第 1 项 | 调用 make · 1 个参数', ' 第 1 个 | a', ' 第 2 项 | b'])
const cell = (await translateAll(py, 'python', pythonZhTranslator, source)).get(16)
expect(tableLines(cell?.table)).toEqual(['key | (调用 Model · 1 个参数).model_dump()', ' a | 1'])
})
@@ -1398,6 +1399,183 @@ describe('跨几行写的调用', () => {
})
})
describe('跨几行写的列表', () => {
/** Filter 的定义是类,别的名字还没查到。 */
const calleeKind = (call: CallFacts): BlockKind | undefined => call.callee.text === 'Filter' ? 'class' : undefined
const FILTERS = [
'async def list_sessions(archived: bool, current_user: User):',
' filters = [',
' Filter(field="user_id", operator=FilterOperator.EQ, value=current_user.id),',
' Filter(',
' field="archived_at",',
' operator=FilterOperator.NE if archived else FilterOperator.EQ,',
' value=None,',
' ),',
' ]',
].join('\n')
it('初始值写「列表 N 项」,每项一个子行「第 N 项 | 值」;整项是调用的写成一行还是几行都排成「新建 / 调用 …」,实参表跟着这一项', async () => {
const rows = (await describeAll(py, 'python', pythonZhTranslator, FILTERS, calleeKind)).get(2) ?? []
expect(propertyLines(rows)).toEqual([
'名称: filters',
'初始值: 列表 2 项',
' 第 1 项 | 新建 Filter · 3 个参数',
' field | "user_id"',
' operator | FilterOperator.EQ',
' value | current_user.id',
' 第 2 项 | 新建 Filter · 3 个参数',
' field | "archived_at"',
' operator | 如果 archived 则 FilterOperator.NE 否则 FilterOperator.EQ',
' value | 空',
])
const value = rows.find(row => row.label === '初始值')
expect(value?.columns).toBeUndefined()
expect(colors(value?.value)).toEqual([{ text: '列表 2 项', role: 'hint' }])
const [first, second] = value?.items ?? []
expect(first?.cells[0]).toEqual([{ text: '第 1 项', role: 'hint' }])
expect(first).not.toHaveProperty('items')
expect(first?.table?.columns).toEqual(['参数', '值'])
expect(second?.table?.columns).toEqual(['参数', '值'])
// 拆开以后名字照样带源码下标(跳到定义、悬停、中文名、出现处高亮都靠它):被调用的名字、按名字传的名字、值里的名字
const multiline = FILTERS.indexOf('Filter(\n')
expect(inlineOf(second?.cells[1] ?? []).find(piece => 'role' in piece && piece.text === 'Filter')).toEqual({ text: 'Filter', role: 'type', offset: multiline })
expect(second?.table?.items[0].cells[0]).toEqual([{ text: 'field', role: 'variable', offset: FILTERS.indexOf('field="archived_at"') }])
const user = FILTERS.indexOf('current_user.id')
expect(linked(first?.table?.items[2].cells[1])).toEqual([['current_user', user], ['id', user + 13]])
})
it('写在一行里的列表照旧一行译文,里面的调用也照旧;空列表写「空列表」', async () => {
const source = 'ids = [a, Filter(field="x")]\nempty = []'
const rows = await describeAll(py, 'python', pythonZhTranslator, source, calleeKind)
expect(propertyLines(rows.get(1) ?? [])).toEqual(['名称: ids', '访问级别: 公共', '初始值: [a, Filter(field="x")]'])
expect(propertyLines(rows.get(2) ?? [])).toEqual(['名称: empty', '访问级别: 公共', '初始值: 空列表'])
})
it('元组、集合各用自己的叫法,*x 写「展开」、「第 N 项」只数不是展开的;项是字典照字典拆,是跨几行的列表接着拆,一层层往下套', async () => {
const source = [
'PAIR = (',
' first,',
' *rest,',
' second,',
')',
'TAGS = {',
' "a",',
' "b",',
'}',
'CONFIG = [',
' {"name": "a", "tags": [',
' "x",',
' "y",',
' ]},',
' [1, 2],',
' [',
' 3,',
' 4,',
' ],',
']',
].join('\n')
const rows = await describeAll(py, 'python', pythonZhTranslator, source)
expect(propertyLines(rows.get(1) ?? []).slice(2)).toEqual(['值: 元组 3 项', ' 第 1 项 | first', ' 展开 | rest', ' 第 2 项 | second'])
expect(propertyLines(rows.get(6) ?? []).slice(2)).toEqual(['值: 集合 2 项', ' 第 1 项 | "a"', ' 第 2 项 | "b"'])
expect(propertyLines(rows.get(10) ?? []).slice(2)).toEqual([
'值: 列表 3 项',
' 第 1 项 | 字典 2 项',
' "name" | "a"',
' "tags" | 列表 2 项',
' 第 1 项 | "x"',
' 第 2 项 | "y"',
' 第 2 项 | [1, 2]',
' 第 3 项 | 列表 2 项',
' 第 1 项 | 3',
' 第 2 项 | 4',
])
// 字典的项照旧标出它讲的定义(键),列表的项不讲哪个定义
const config = rows.get(10)?.find(row => row.label === '值')
expect(config?.items?.[0]).not.toHaveProperty('defines')
const tags = source.indexOf('"tags"')
expect(config?.items?.[0].items?.[1].defines).toEqual({ startIndex: tags, endIndex: tags + 6 })
})
it('字典一项的值是跨几行写的列表时也按项拆', async () => {
const source = 'ROUTES = {"admin": [\n "users",\n "roles",\n]}'
expect(propertyLines((await describeAll(py, 'python', pythonZhTranslator, source)).get(1) ?? []).slice(2)).toEqual([
'值: 字典 1 项',
' "admin" | 列表 2 项',
' 第 1 项 | "users"',
' 第 2 项 | "roles"',
])
})
it('实参的值是跨几行写的列表时也按项拆,嵌在那一格下面;写在一行里的列表、实参里的字典照旧一行', async () => {
const source = [
'session.query(',
' filters=[',
' Filter(field="a"),',
' other,',
' ],',
' ids=[1, 2],',
' meta={"a": 1},',
')',
].join('\n')
const line = (await translateAll(py, 'python', pythonZhTranslator, source, calleeKind)).get(1)
expect(text(line?.segments)).toBe('调用 session.query · 3 个参数')
expect(tableLines(line?.table)).toEqual([
'filters | 列表 2 项',
' 第 1 项 | 新建 Filter · 1 个参数',
' field | "a"',
' 第 2 项 | other',
'ids | [1, 2]',
'meta | {"a": 1}',
])
expect(line?.table?.items[0].items?.[0].table?.columns).toEqual(['参数', '值'])
})
it('typeScript:数组写「数组 N 项」,new 是新建、别的调用是调用,整项是调用的都排开;...x 写「展开」,对象照对象拆;实参的值是跨几行的数组也拆', async () => {
const source = [
'const filters = [',
' new Filter({ field: \'user_id\' }),',
' makeFilter(',
' \'archived_at\',',
' archived,',
' ),',
' ...rest,',
' [1, 2],',
' {',
' a: 1,',
' },',
']',
'const ids = [a, f(b)]',
'register(',
' \'x\',',
' [',
' a,',
' b,',
' ],',
')',
].join('\n')
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
expect(propertyLines(rows.get(1) ?? [])).toEqual([
'名称: filters',
'访问级别: 私有',
'值: 数组 5 项',
' 第 1 项 | 新建 Filter · 1 个参数',
' 第 1 个 | { field: \'user_id\' }',
' 第 2 项 | 调用 makeFilter · 2 个参数',
' 第 1 个 | \'archived_at\'',
' 第 2 个 | archived',
' 展开 | rest',
' 第 3 项 | [1, 2]',
' 第 4 项 | 对象 1 项',
' a | 1',
])
expect(propertyLines(rows.get(13) ?? []).at(-1)).toBe('值: [a, f(b)]')
const line = (await translateAll(ts, 'typescript', typescriptZhTranslator, source)).get(14)
expect(text(line?.segments)).toBe('调用 register · 2 个参数')
expect(tableLines(line?.table)).toEqual(['第 1 个 | \'x\'', '第 2 个 | 数组 2 项', ' 第 1 项 | a', ' 第 2 项 | b'])
})
})
describe('中文模板引擎', () => {
function testTranslator(table: Partial<ZhTable>): Translator {
return createZhTemplateTranslator({
@@ -1411,7 +1589,7 @@ describe('中文模板引擎', () => {
operators: {},
operatorParents: [],
emptyLiterals: {},
mapLiterals: {},
collections: {},
stringNodes: [],
commentText: node => node.text,
nodeTemplates: {},
+114 -73
View File
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, Doc, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, Doc, ElementFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { ParamDocs } from '../rules/docRows'
import { readDoc } from '../docTags'
import { commonIndent, splitLines } from '../editText'
@@ -43,8 +43,8 @@ export interface ZhTable {
operatorParents: string[]
/** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */
emptyLiterals: Record<string, string>
/** 字典 / 对象字面量(语言插件给了 entries 的值)的叫法,按节点类型查:属性表里的值写成「字典 3 项」,各项展开成子行。 */
mapLiterals: Record<string, string>
/** 写出各项的字面量(语言插件给了各项,见 LanguageAdapter.collection)的叫法,按节点类型查:属性表里拆开的值写成「字典 3 项」「列表 2 项」。 */
collections: Record<string, string>
/** 整段原样照抄、按字符串着色的节点。 */
stringNodes: string[]
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
@@ -310,7 +310,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/** 节点命中模板时,模板各项换成译词或子节点;不该重排时返回 undefined。 */
function templateParts(node: Node, span: SourceSpan): (string | Node)[] | undefined {
const template = table.nodeTemplates[node.type]
if (!template || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
if (!template || !inside(node, span))
return undefined
const parts = template.map(part => typeof part === 'string' ? part : slotChild(node, part))
return parts.every((part): part is string | Node => part != null) ? parts : undefined
@@ -340,7 +340,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
function iterationIn(node: Node, span: SourceSpan, language: LanguageAdapter): IterationFacts | undefined {
if (!language.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
if (!language.iteration || !inside(node, span))
return undefined
return language.iteration(node)
}
@@ -414,26 +414,86 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
function expandedCall(node: Node, span: SourceSpan, slot: TableSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
if (slot.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
if (slot.table || !call || !inside(node, span))
return undefined
const facts = call(node)
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
}
/**
* node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。
* 一行译文的块、常量 / 变量的值、实参表里每个值各翻各的,各自排自己里面的那个调用。
* node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall);
* 给了 call(node 整个就是这次调用)时不管写成几行都这样排。一行译文的块、属性表里每一格的值各翻各的,各自排自己里面的那个调用。
*/
function expandedText(node: Node, span: SourceSpan, document: SourceDocument): { text: Inline[], table?: PreviewTable } {
function expandedText(node: Node, span: SourceSpan, document: SourceDocument, call?: CallFacts): CellValue {
const slot: TableSlot = {}
const text = joinTokens(tokensIn(node, span, document, slot))
return { text, ...(slot.table && { table: slot.table }) }
const out = collector(node, span, document, slot)
if (call)
emitCall(out, slot, node, call, span)
else
collect(node, span, out)
return { text: joinTokens(out.tokens), ...(slot.table && { table: slot.table }) }
}
/**
* 属性表里一格的值(常量 / 变量的值、字典一项的值、列表的一项、实参的值),只翻落在 span 里的部分:
* 语言插件给了各项的字面量(LanguageAdapter.collection)写叫法和项数,各项拆成下一层子行(见 entryItem、elementItems)——
* 字典 / 对象一直拆(maps 为假时照旧一行:实参的值),列表 / 元组 / 集合 / 数组跨几行写才拆;
* 空字面量和别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,实参表跟着这一格(见 expandedText)。
*/
function cellValue(node: Node, span: SourceSpan, document: SourceDocument, maps = true): CellValue {
const collection = document.language.collection?.(node)
if (collection?.kind === 'map' && maps && collection.entries.length > 0)
return { text: itemCount(node, collection.entries.length), items: collection.entries.map(entry => entryItem(entry, span, document)) }
if (collection?.kind === 'sequence' && collection.elements.length > 0 && node.startPosition.row !== node.endPosition.row)
return { text: itemCount(node, collection.elements.length), items: elementItems(collection.elements, span, document) }
return expandedText(node, span, document)
}
/** 拆开的字面量写叫法和项数(展开的也算一项):「字典 3 项」,叫法按字面量的节点类型查表(ZhTable.collections)。 */
function itemCount(literal: Node, size: number): Segment[] {
const name = table.collections[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
}
/**
* 字典 / 对象的一项是一个子行,两列:键 | 值。键照源码写法(字符串带引号,计算属性名 `[k]` 照常翻),值见 cellValue;
* `**x`、`...x` 的键写「展开」、值是展开的表达式,方法的值是它的头部。讲的定义是键(对象的属性名)。
*/
function entryItem(entry: EntryFacts, span: SourceSpan, document: SourceDocument): PropertyItem {
const say = (node: Node, part = span): Inline[] => joinTokens(tokensIn(node, part, document))
switch (entry.kind) {
case 'pair': {
const { text, ...nested } = cellValue(entry.value, span, document)
return { cells: [say(entry.key), text], ...nested, ...defining(entry.key) }
}
case 'spread':
return { cells: [[hint('展开')], say(entry.value)] }
case 'method':
return { cells: [say(entry.name), say(entry.node, clip(entry.header, span))], ...defining(entry.name) }
}
}
/**
* 列表的各项各是一个子行,两列:第 N 项 | 值(只数不是展开的项),`*x`、`...x` 写「展开」、值是展开的表达式。
* 整项是一次调用的不管写成几行都排成「新建 / 调用 …」,实参表跟着这一项,同一个列表里样子一致;别的值见 cellValue。
*/
function elementItems(elements: ElementFacts[], span: SourceSpan, document: SourceDocument): PropertyItem[] {
let position = 0
return elements.map((element): PropertyItem => {
if (element.kind === 'spread')
return { cells: [[hint('展开')], joinTokens(tokensIn(element.value, span, document))] }
position++
const { value } = element
const call = inside(value, span) ? document.language.call?.(value) : undefined
const { text, ...nested } = call ? expandedText(value, span, document, call) : cellValue(value, span, document)
return { cells: [[hint(`第 ${position} 项`)], text], ...nested }
})
}
/**
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值照常翻;值里有跨几行写的调用时同样排成一行,
* 它的实参表排在值那一格里(PropertyItem.table),一层层往下套。
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值见 cellValue(字典 / 对象照旧一行):值里有跨几行写的调用时
* 同样排成一行,它的实参表排在值那一格里(PropertyItem.table);值是跨几行写的列表时各项拆在那一格里(PropertyItem.items)。一层层往下套。
*/
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
let position = 0
@@ -451,8 +511,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
const row = (arg: CallArgument): PropertyItem => {
const { text, table } = expandedText(arg.value, span, document)
return { cells: [label(arg), text], ...(table && { table }) }
const { text, ...nested } = cellValue(arg.value, span, document, false)
return { cells: [label(arg), text], ...nested }
}
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(row) }
}
@@ -492,7 +552,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return {
document,
say: node => joinTokens(tokensIn(node, span, document)),
whole: node => node.startIndex >= span.startIndex && node.endIndex <= span.endIndex,
whole: node => inside(node, span),
name: (node, role) => segment(node.text, role ?? document.language.tokenRole(node), node.startIndex),
command: (node) => {
const facts = document.language.command?.(node)
@@ -605,9 +665,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
/** 按译文顺序收集 node 落在 span 里的词;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
/** 翻 node 落在 span 里的那一段用的收集器;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
function collector(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Collector {
return { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
}
/** 按译文顺序收集 node 落在 span 里的词;slot 见 collector。 */
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Token[] {
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
const out = collector(node, span, document, slot)
collect(node, span, out)
return out.tokens
}
@@ -652,10 +717,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
if (block.kind === 'comment')
return { text: translateComment(block, document) }
const group = block.group && table.groups?.[block.node.type]
if (block.group && group) {
const out: Collector = { document, root: block.node, tokens: [], cursor: block.header.startIndex, gap: false }
return group(block.group, readerContext(out, block.header))
}
if (block.group && group)
return group(block.group, readerContext(collector(block.node, block.header, document), block.header))
const slot: TableSlot = {}
const tokens = tokensIn(block.node, block.header, document, slot)
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
@@ -677,22 +740,15 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
const { header } = block
const { language } = document
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
const clipped = (part: SourceSpan): SourceSpan => part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
const within = (node: Node, part: SourceSpan): Inline[] => joinTokens(tokensIn(node, clipped(part), document))
const clipped = (part: SourceSpan): SourceSpan => clip(part, header)
const phrases: Phrases = {
translated: node => within(node, nodeSpan(node)),
within,
expanded: node => expandedText(node, clipped(nodeSpan(node)), document),
translated: node => joinTokens(tokensIn(node, clipped(nodeSpan(node)), document)),
value: node => cellValue(node, clipped(nodeSpan(node)), document),
body: node => expression(node, document),
returned: (node) => {
const [value] = language.command?.(node)?.arguments ?? []
return value ? expression(value.node, document) : []
},
entryCount: (literal, size) => {
const name = table.mapLiterals[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
},
verbatim: node => verbatim(node, document),
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node, language))] : verbatim(node, document, 'type'),
@@ -756,6 +812,16 @@ function nodeSpan(node: Node): SourceSpan {
return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }
}
/** node 整个落在 span 里。 */
function inside(node: Node, span: SourceSpan): boolean {
return node.startIndex >= span.startIndex && node.endIndex <= span.endIndex
}
/** part 超出 limit 结尾的部分截掉:值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致。 */
function clip(part: SourceSpan, limit: SourceSpan): SourceSpan {
return part.endIndex > limit.endIndex ? { ...part, endIndex: limit.endIndex, endRow: limit.endRow } : part
}
function segment(text: string, role: TokenRole, offset: number | undefined): Segment {
return offset === undefined ? { text, role } : { text, role, offset }
}
@@ -805,20 +871,23 @@ function joinTokens(tokens: Token[]): Inline[] {
// ─── 属性表 ───────────────────────────────────────────────────────────────────
/** 属性表里一格的值:一行译文,带下一层子行(拆开的字面量的各项)或那次调用的实参表。 */
interface CellValue {
text: Inline[]
items?: PropertyItem[]
table?: PreviewTable
}
/** 属性表里的值怎么写。 */
interface Phrases {
/** 按表翻译,只翻落在块头部里的那一段。 */
translated: (node: Node) => Inline[]
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
within: (node: Node, span: SourceSpan) => Inline[]
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
/** 一格里的值,只翻落在块头部里的那一段:字面量的各项拆成下一层子行,跨几行写的调用排成「新建 / 调用 …」带实参表(见 cellValue)。 */
value: (node: Node) => CellValue
/** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */
body: (node: Node) => Inline[]
/** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */
returned: (node: Node) => Inline[]
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
entryCount: (literal: Node, size: number) => Segment[]
/** 源码原样照抄、按源码着色(装饰器)。 */
verbatim: (node: Node) => Segment[]
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
@@ -1271,44 +1340,16 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
...(i === 0 ? docBodyRows(block.facts.doc && say.doc(block.facts.doc)) : []),
...accessRows(block.facts.access),
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, say) : { value: [hint('无')] }) },
])
}
/**
* 声明的值:字典 / 对象字面量见 literalValue;别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,
* 实参摊成表(参数 | 值)展开在这一行下面。
* 声明的值(见 Phrases.value):拆开的字面量的各项、跨几行写的调用的实参表(参数 | 值,带列名)都展开在这一行下面。
*/
function declaredValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
if (entries?.length)
return literalValue(value, entries, say)
const { text, table } = say.expanded(value)
return table ? { value: text, ...table } : { value: text }
}
/**
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
*/
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)) }
}
/**
* 字典 / 对象字面量的一项是一个子行,两列:键 | 值。键照源码写法(字符串带引号,计算属性名 `[k]` 照常翻),
* `**x`、`...x` 的键写「展开」、值是展开的表达式,方法的值是它的头部。讲的定义是键(对象的属性名)。
*/
function entryItem(entry: EntryFacts, say: Phrases): PropertyItem {
switch (entry.kind) {
case 'pair': {
const { value, ...nested } = literalValue(entry.value, entry.entries, say)
return { cells: [say.translated(entry.key), value], ...nested, ...defining(entry.key) }
}
case 'spread':
return { cells: [[hint('展开')], say.translated(entry.value)] }
case 'method':
return { cells: [say.translated(entry.name), say.within(entry.node, entry.header)], ...defining(entry.name) }
}
function declaredValue(value: Node, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
const { text, items, table } = say.value(value)
if (table)
return { value: text, ...table }
return items ? { value: text, items } : { value: text }
}