✨ 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

+28 -11
View File
@@ -322,10 +322,23 @@ export interface ImportFacts {
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
export type EntryFacts
= | { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TypeScript 的简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries
= | { kind: 'pair', key: Node, value: Node } // 'a': 1、a: 1、[k]: v;TypeScript 的简写 { a } 键和值是同一个节点
| { kind: 'spread', value: Node } // **other、...other:value 是展开的那个表达式
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象字面量里的方法、getter、setter:node 是整个方法,header 同 BlockShape.header,只到主体开始之前
/** 列表 / 元组 / 集合 / 数组字面量里的一项,照源码顺序;注释不算。 */
export type ElementFacts
= | { kind: 'item', value: Node } // 写出来的一项
| { kind: 'spread', value: Node } // *other、...other:value 是展开的那个表达式
/**
* 写出各项的字面量:语言插件从语法树读出结构(LanguageAdapter.collection),翻译插件把各项拆成子行。
* map 是字典 / 对象,sequence 是列表 / 元组 / 集合 / 数组;空字面量的各项是空数组。节点都指向源码,项的值里还能再套字面量。
*/
export type CollectionFacts
= | { kind: 'map', entries: EntryFacts[] }
| { kind: 'sequence', elements: ElementFacts[] }
/** 写在表达式里的遍历得到什么:逐个产出的生成器,或一次建好的列表、集合、字典。 */
export type IterationResult = 'generator' | 'list' | 'set' | 'dict'
@@ -361,8 +374,8 @@ export type CallArgument
| { kind: 'keywordSpread', value: Node } // 展开成按名字传的几个:Python 的 **kwargs
/**
* 一次调用(含新建对象):语言插件从语法树读出结构(LanguageAdapter.call),翻译插件把跨几行写的调用排成
* 「新建 / 调用 被调用的名字 · N 个参数」一行,实参摊成一张表。节点都指向源码。
* 一次调用(含新建对象):语言插件从语法树读出结构(LanguageAdapter.call),翻译插件把跨几行写的调用(拆开的列表里整项是调用的,
* 写成一行也算)排成「新建 / 调用 被调用的名字 · N 个参数」一行,实参摊成一张表。节点都指向源码。
*/
export interface CallFacts {
/** 被调用的表达式:`user_service.update_user`、`UserDetailResponse`;TypeScript 的 new 不算在里面。 */
@@ -450,8 +463,6 @@ export interface DeclaratorFacts {
name: Node
type?: Node
value?: Node
/** 值是字典 / 对象字面量(Python 的 dictionary、TypeScript 的 object)时是它的各项,空字面量是空数组;别的值没有。 */
entries?: EntryFacts[]
}
export interface VariableFacts {
@@ -667,23 +678,27 @@ export type PreviewText = Inline[] | Markdown
export interface Defines { defines?: SourceRange }
/**
* 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值。
* 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值、列表的第 N 项 / 值。
* 格子是着色的词,或一段 Markdown(参数的说明、异常的触发条件)。
*/
export interface PropertyItem extends Defines {
cells: PreviewText[]
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
/**
* 最后一格的值拆开的下一层子行(值是字典 / 对象、跨几行写的列表时,它的各项):排成列表的子行把它展开在自己下面,能折叠;
* 表格的行把它排在最后一格里、那格文字下面,总是展开。一层层可以一直往下套。
*/
items?: PropertyItem[]
/**
* 排在这一行最后一格里、那格文字下面的表:实参表里一个实参的值里有跨几行写的调用时,是那个调用的实参表,
* 表里的行又可以带 table,一层层往下套。总是展开的。
* 最后一格的值里那次调用的实参表(跨几行写的调用;列表里整项是调用的,写成一行也算):表格的行把它排在最后一格里、
* 那格文字下面,排成列表的子行把它排在自己下面。总是展开的;表里的行又可以带 items、table,一层层往下套。
*/
table?: PreviewTable
}
/**
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;表格只有一层,子行不再往下套。
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象、列表的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;
* 两种子行都能再带下一层(PropertyItem.items、table)。
*/
export interface PropertyRow extends Defines {
label: string
@@ -735,6 +750,8 @@ export interface LanguageAdapter {
iteration?: (node: Node) => IterationFacts | undefined
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
call?: (node: Node) => CallFacts | undefined
/** node 是写出各项的字面量(字典、列表……)时给出它的各项,别的节点没有;没给的语言不拆字面量,照常一行译文。 */
collection?: (node: Node) => CollectionFacts | undefined
/** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;别的节点没有,没有命令的语言不填。 */
command?: (node: Node) => CommandFacts | undefined
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
+51 -18
View File
@@ -304,33 +304,66 @@ describe('python 属性表信息(facts)', () => {
expect(found[10]).not.toHaveProperty('access')
})
it('字典字面量的各项:字符串键、名字键、嵌套字典连它的各项、**展开;空字典是空数组,别的值没有;类属性也有', async () => {
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
const found = await facts('EMPTY = {}\nITEMS = [1]\nclass K:\n labels = {"x": 1}')
expect(found[1]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}' }] })
expect(found[2]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[4]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}' }] })
})
})
describe('python 字面量的各项', () => {
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.collection?.(node))
it('字典:字符串键、名字键、**展开,注释不算一项;值里的字典另有自己的各项,空字典没有项', async () => {
const source = [
'_KEYS = {',
' "published": "contests:publish",',
' key: 1,',
' # 注释不算一项',
' "nested": {"a": {}, "b": [1]},',
' "nested": {"a": {}},',
' **other,',
'}',
'EMPTY = {}',
'ITEMS = [1]',
'class K:',
' labels = {"x": 1}',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private', declarators: [{ name: '_KEYS', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}, "b": [1]}', entries: [
{ kind: 'pair', key: '"a"', value: '{}', entries: [] },
{ kind: 'pair', key: '"b"', value: '[1]' },
expect(await collections(source)).toEqual([
{ kind: 'map', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}}' },
{ kind: 'spread', value: 'other' },
] },
{ kind: 'spread', value: 'other' },
] }] })
expect(found[8]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}', entries: [] }] })
expect(found[9]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[11]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}', entries: [{ kind: 'pair', key: '"x"', value: '1' }] }] })
{ kind: 'map', entries: [{ kind: 'pair', key: '"a"', value: '{}' }] },
{ kind: 'map', entries: [] },
])
})
it('列表、元组、集合:各项照源码顺序,*x 是展开,注释不算一项;值里的字面量另有自己的各项,空列表没有项', async () => {
const source = [
'filters = [',
' Filter(field="user_id"),',
' # 注释不算一项',
' *rest,',
' (a, b),',
' {1},',
' [],',
']',
].join('\n')
expect(await collections(source)).toEqual([
{ kind: 'sequence', elements: [
{ kind: 'item', value: 'Filter(field="user_id")' },
{ kind: 'spread', value: 'rest' },
{ kind: 'item', value: '(a, b)' },
{ kind: 'item', value: '{1}' },
{ kind: 'item', value: '[]' },
] },
{ kind: 'sequence', elements: [{ kind: 'item', value: 'a' }, { kind: 'item', value: 'b' }] },
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }] },
{ kind: 'sequence', elements: [] },
])
})
it('推导式、括号里的一个式子、调用的实参列表、别的值不是', async () => {
expect(await collections('x = [a for a in b]\ny = (a)\nf(a, b)\nz = a, b')).toEqual([])
})
})
+29 -15
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, ElementFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { pythonNameReader } from './pythonNames'
@@ -303,7 +303,7 @@ function expressionBlock(node: Node, scope: Scope, doc: Node | undefined): CodeB
facts: {
access: scopeAccess(left, scope),
doc,
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined, entries: dictionaryEntries(value) }],
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined }],
},
}
}
@@ -314,21 +314,34 @@ function assignmentIn(statement: Node): Node | undefined {
return expression?.type === 'assignment' ? expression : undefined
}
/** 字典字面量的各项:`键: 值`(值又是字典时连它的各项)、`**other`;值不是字典字面量时没有。 */
function dictionaryEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'dictionary')
/** 列表、元组、集合字面量(各项是 ElementFacts)的节点类型。 */
const SEQUENCES = new Set(['list', 'tuple', 'set'])
/** 字面量的各项:字典的 `键: 值`、`**other`,列表、元组、集合的各项、`*other`;别的节点没有。 */
function pythonCollection(node: Node): CollectionFacts | undefined {
if (node.type === 'dictionary')
return { kind: 'map', entries: syntaxChildren(node).flatMap(dictionaryEntry) }
if (!SEQUENCES.has(node.type))
return undefined
return syntaxChildren(value).flatMap((item): EntryFacts[] => {
if (item.type === 'dictionary_splat') {
const [spread] = syntaxChildren(item)
return {
kind: 'sequence',
elements: syntaxChildren(node).flatMap((element): ElementFacts[] => {
if (element.type !== 'list_splat')
return [{ kind: 'item', value: element }]
const [spread] = syntaxChildren(element)
return spread ? [{ kind: 'spread', value: spread }] : []
}
const key = item.childForFieldName('key')
const itemValue = item.childForFieldName('value')
if (item.type !== 'pair' || !key || !itemValue)
return []
return [{ kind: 'pair', key, value: itemValue, entries: dictionaryEntries(itemValue) }]
})
}),
}
}
function dictionaryEntry(item: Node): EntryFacts[] {
if (item.type === 'dictionary_splat') {
const [spread] = syntaxChildren(item)
return spread ? [{ kind: 'spread', value: spread }] : []
}
const key = item.childForFieldName('key')
const value = item.childForFieldName('value')
return item.type === 'pair' && key && value ? [{ kind: 'pair', key, value }] : []
}
const ITERATION_RESULTS: Record<string, IterationResult> = {
@@ -671,6 +684,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
freshDoc,
iteration: pythonIteration,
call: pythonCall,
collection: pythonCollection,
displayNames: pythonNameReader,
}
}
+24 -1
View File
@@ -5,7 +5,7 @@ import { toBlockViews } from '../blockView'
import { blockMarks } from '../diff/viewDiff'
import { rustImplRule } from '../rules/rustImpl'
import { isCrateRoot } from '../rules/rustTree'
import { commentText, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { commentText, nodeFacts, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { createRustAdapter } from './rust'
@@ -188,6 +188,29 @@ describe('rust 语言卡片', () => {
})
})
describe('rust 字面量的各项', () => {
it('数组、元组的各项照源码顺序,注释不算一项;[值; 个数]、别的值不是', async () => {
const source = 'const A: [&str; 2] = [\n "a", // 第一个\n "b",\n];\nconst Z: [u8; 4] = [0; 4];\nconst T: (u8, u8) = (1, 2);\nfn f() { g(a, b); }'
expect(await nodeFacts(adapter, 'rust', source, node => adapter.collection?.(node))).toEqual([
{ kind: 'sequence', elements: [{ kind: 'item', value: '"a"' }, { kind: 'item', value: '"b"' }] },
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }, { kind: 'item', value: '2' }] },
])
})
it('常量的值是跨几行写的数组、元组时按项拆开:「数组 N 项」,每项一行「第 N 项 | 值」;写在一行里的照旧', async () => {
const source = 'pub const NAMES: [&str; 2] = [\n "a",\n "b",\n];\npub const PAIR: (u8, u8) = (\n 1,\n 2,\n);\npub const ONE: [u8; 2] = [1, 2];\n'
expect(viewLines(await views(source)).filter(line => /[值第|]/.test(line))).toEqual([
' 值: 数组 2 项',
' 第 1 项 | "a"',
' 第 2 项 | "b"',
' 值: 元组 2 项',
' 第 1 项 | 1',
' 第 2 项 | 2',
' 值: [1, 2]',
])
})
})
describe('rust 注释', () => {
it('连着写的 // 合成一块注释,空的 // 是分段,预览是去掉 // 的 Markdown;后面的 use 照常是导入', async () => {
const list = await views(readFixture('rustComments.rs'))
+11 -2
View File
@@ -1,8 +1,8 @@
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named, syntaxChildren } from './shared'
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
@@ -15,9 +15,18 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter {
tokenRole: rustTokenRole,
lineComment: '//',
freshDoc,
collection: rustCollection,
}
}
/** 数组 `[a, b]`、元组 `(a, b)` 的各项;`[值; 个数]` 不是一项项写出来的,别的节点也没有。项前面的 `#[…]` 不算一项。 */
function rustCollection(node: Node): CollectionFacts | undefined {
const listed = node.type === 'tuple_expression' || (node.type === 'array_expression' && !node.childForFieldName('length'))
if (!listed)
return undefined
return { kind: 'sequence', elements: syntaxChildren(node).filter(child => child.type !== 'attribute_item').map(value => ({ kind: 'item', value })) }
}
/** extern 块本身不成一块源码,里面的签名跟所在这一层的别的项走同一条路。 */
function unwrapExtern(item: Node): Node[] {
if (item.type !== 'foreign_mod_item')
@@ -338,54 +338,13 @@ describe('typeScript 属性表信息(facts)', () => {
name: 'result',
type: 'EnvLockResult',
value: '{ total: files.length, ok: [], failed: [] }',
entries: [
{ kind: 'pair', key: 'total', value: 'files.length' },
{ kind: 'pair', key: 'ok', value: '[]' },
{ kind: 'pair', key: 'failed', value: '[]' },
],
}] })
})
it('对象字面量的各项:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前)、嵌套对象连它的各项;空对象是空数组,别的值没有', async () => {
const source = [
'const o = {',
' a: 1,',
' \'b-c\': x,',
' [k]: 2,',
' s,',
' ...rest,',
' // 注释不算一项',
' async load(id: string): Promise<void> {',
' await run(id)',
' },',
' get size() { return 1 },',
' n: { d: 4, e: {} },',
'}',
'const empty = {}',
'const list = [1]',
].join('\n')
const at = (text: string): number => source.indexOf(text)
const found = await facts(source)
expect(found[1]).toMatchObject({ declarators: [{ entries: [
{ kind: 'pair', key: 'a', value: '1' },
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
{ kind: 'pair', key: '[k]', value: '2' },
{ kind: 'pair', key: 's', value: 's' },
{ kind: 'spread', value: 'rest' },
{
kind: 'method',
name: 'load',
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
},
{ kind: 'method', name: 'size', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }', entries: [
{ kind: 'pair', key: 'd', value: '4' },
{ kind: 'pair', key: 'e', value: '{}', entries: [] },
] },
] }] })
expect(found[14]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}', entries: [] }] })
expect(found[15]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
const found = await facts('const empty = {}\nconst list = [1]')
expect(found[1]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}' }] })
expect(found[2]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
})
it('导入:默认导入、* as、仅类型、副作用导入、require', async () => {
@@ -485,6 +444,72 @@ describe('typeScript 属性表信息(facts)', () => {
})
})
describe('typeScript 字面量的各项', () => {
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.collection?.(node))
it('对象:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前),注释不算一项;值里的对象另有自己的各项,空对象没有项', async () => {
const source = [
'const o = {',
' a: 1,',
' \'b-c\': x,',
' [k]: 2,',
' s,',
' ...rest,',
' // 注释不算一项',
' async load(id: string): Promise<void> {',
' await run(id)',
' },',
' get size() { return 1 },',
' n: { d: 4, e: {} },',
'}',
].join('\n')
const at = (text: string): number => source.indexOf(text)
expect(await collections(source)).toEqual([
{ kind: 'map', entries: [
{ kind: 'pair', key: 'a', value: '1' },
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
{ kind: 'pair', key: '[k]', value: '2' },
{ kind: 'pair', key: 's', value: 's' },
{ kind: 'spread', value: 'rest' },
{
kind: 'method',
name: 'load',
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
},
{ kind: 'method', name: 'size', node: 'get size() { return 1 }', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }' },
] },
{ kind: 'map', entries: [{ kind: 'pair', key: 'd', value: '4' }, { kind: 'pair', key: 'e', value: '{}' }] },
{ kind: 'map', entries: [] },
])
})
it('数组:各项照源码顺序,...x 是展开,注释不算一项;值里的字面量另有自己的各项,空数组没有项', async () => {
const source = [
'const filters = [',
' new Filter({ field: \'user_id\' }),',
' // 注释不算一项',
' ...rest,',
' [],',
']',
].join('\n')
expect(await collections(source)).toEqual([
{ kind: 'sequence', elements: [
{ kind: 'item', value: 'new Filter({ field: \'user_id\' })' },
{ kind: 'spread', value: 'rest' },
{ kind: 'item', value: '[]' },
] },
{ kind: 'map', entries: [{ kind: 'pair', key: 'field', value: '\'user_id\'' }] },
{ kind: 'sequence', elements: [] },
])
})
it('数组形式的解构、类型里的元组、别的值不是', async () => {
expect(await collections('const [a, b] = pair\nlet t: [string, number]\nf(a, b)')).toEqual([])
})
})
describe('typeScript 类', () => {
it('classes.ts:紧贴的 JSDoc(隔着装饰器也算)算进类头部;类表达式赋给变量、类字段也是类块;函数里的类照常成块', async () => {
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
+38 -26
View File
@@ -1,4 +1,4 @@
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeclaratorFacts, ElementFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { attachComments, children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
@@ -153,36 +153,47 @@ function declaratorFacts(declarator: Node): DeclaratorFacts {
name: declarator.childForFieldName('name') ?? declarator,
type: annotatedType(declarator.childForFieldName('type')),
value: value ?? undefined,
entries: objectEntries(value),
}
}
/** 对象字面量的各项:`键: 值`(值又是对象时连它的各项)、简写 `{ a }`、`...other`、方法;值不是对象字面量时没有。 */
function objectEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'object')
/** 字面量的各项:对象的 `键: 值`、简写 `{ a }`、`...other`、方法,数组的各项、`...other`;别的节点没有。 */
function typescriptCollection(node: Node): CollectionFacts | undefined {
if (node.type === 'object')
return { kind: 'map', entries: syntaxChildren(node).flatMap(objectEntry) }
if (node.type !== 'array')
return undefined
return syntaxChildren(value).flatMap((member): EntryFacts[] => {
switch (member.type) {
case 'pair': {
const key = member.childForFieldName('key')
const memberValue = member.childForFieldName('value')
return key && memberValue ? [{ kind: 'pair', key, value: memberValue, entries: objectEntries(memberValue) }] : []
}
case 'shorthand_property_identifier':
return [{ kind: 'pair', key: member, value: member }]
case 'spread_element': {
const [spread] = syntaxChildren(member)
return spread ? [{ kind: 'spread', value: spread }] : []
}
case 'method_definition': {
const name = member.childForFieldName('name')
const body = member.childForFieldName('body')
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
}
default:
return []
return {
kind: 'sequence',
elements: syntaxChildren(node).flatMap((element): ElementFacts[] => {
if (element.type !== 'spread_element')
return [{ kind: 'item', value: element }]
const [spread] = syntaxChildren(element)
return spread ? [{ kind: 'spread', value: spread }] : []
}),
}
}
function objectEntry(member: Node): EntryFacts[] {
switch (member.type) {
case 'pair': {
const key = member.childForFieldName('key')
const value = member.childForFieldName('value')
return key && value ? [{ kind: 'pair', key, value }] : []
}
})
case 'shorthand_property_identifier':
return [{ kind: 'pair', key: member, value: member }]
case 'spread_element': {
const [spread] = syntaxChildren(member)
return spread ? [{ kind: 'spread', value: spread }] : []
}
case 'method_definition': {
const name = member.childForFieldName('name')
const body = member.childForFieldName('body')
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
}
default:
return []
}
}
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;TypeScript(typed)的 `import type` 只导入类型。 */
@@ -658,6 +669,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
lineComment: '//',
freshDoc,
call: typescriptCall,
collection: typescriptCollection,
displayNames: typescriptNameReader,
}
}
+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 }
}