✨ feat(vscode): 代码阅读的链式调用卡片:语言插件读链、翻译模板排成起点和步骤

契约加 LanguageAdapter.chain、Translator.chain、RuleContext.chain、PropertyRow.chain、TextTranslation.rows;跨行写或实参里有闭包的链排成卡片,连着同名调用合成一步列出每次实参,注释挂在步骤上,闭包收起、子块挂到那一步(BlockView.step);Rust 插件读链、标准库常见方法写中文名;挤成一行时另起一行的 . 贴着前面
This commit is contained in:
cruldra committed 2026-10-15 20:54:59 +11:00
1 parent 80eda7c18e
commit ee595f3da9
16 files changed
+1041 -72

No files matched your search

+55 -16
View File
@@ -40,6 +40,8 @@ interface BlockViewBase {
badge?: KindBadge
/** 库规则挂在父卡片子行上的子块才有(见 RuleSlot):展开时画在父卡片属性表第 row 行下面,不排在父卡片的子块里。 */
slot?: BlockSlot
/** 父卡片链式调用里收起来的闭包、async 块的子块才有:挂在那一步下面(见 ChainSlot),不排在父卡片的子块里。 */
step?: ChainSlot
/** 能看调用树的块才有(见 CallTreeTarget):页面按它摆调用树按钮,宿主按它数棵数、查调用者。 */
callTree?: CallTreeTarget
children: BlockView[]
@@ -56,6 +58,15 @@ export interface BlockSlot {
item: number
}
/**
* 子块挂在父卡片链式调用的哪一步:属性表(一行译文的卡片是下面那几行)第 row 行那条链(PropertyRow.chain)的第 step 步。
* 那一步的值里收起来的闭包、async 块点开才展开,这一步下面的子块一起展开、一起收起。
*/
export interface ChainSlot {
row: number
step: number
}
/**
* 「重写注释」写到哪,行号 0 起。
* replace 换掉这几行:块的说明(docstring、JSDoc、`///`),没有说明时是块前面的普通注释,文件开头的说明卡片是它自己。
@@ -301,6 +312,24 @@ function withCommentRow(rows: readonly PropertyRow[], row: PropertyRow): Propert
return [...rows.slice(0, at), row, ...rows.slice(at)]
}
/** 子块挂在父卡片里的哪儿:库规则挂在属性表子行上的(slot),或挂在链式调用某一步下面的(step)。 */
type CardPlace = Pick<BlockViewBase, 'slot' | 'step'>
/**
* 子块挂在卡片链式调用的哪一步(见 ChainSlot):rows 是卡片排好的属性表(连「注释」一行),某一行带着链时,
* 落在那条链某一步源码范围里的子块(实参里闭包、async 块主体的各句)挂在那一步下面。
*/
function chainPlaces(children: readonly CodeBlock[], rows: readonly PropertyRow[] | undefined): Map<CodeBlock, CardPlace> {
const places = new Map<CodeBlock, CardPlace>()
rows?.forEach((row, index) => row.chain?.forEach(({ range }, step) => {
for (const child of children) {
if (child.header.startIndex >= range.startIndex && child.node.endIndex <= range.endIndex)
places.set(child, { step: { row: index, step } })
}
}))
return places
}
/**
* 代码块树 → 视图数据:各块的编辑范围、翻译插件给的译文;块的源码、着色不按块带,页面从整份源码里按编辑范围取(见 SourceColors)。
* 每块先问启用的库规则,认领了就换成规则给的替身(见 RuleBlock):规则的卡片,或认成的属性表块(交给翻译插件排,同语言插件报的这种块),
@@ -337,15 +366,16 @@ export function toBlockViews(
translate: node => layerTranslator ? layerTranslator.expression(node, layerDocument) : [{ text: node.text, role: 'plain' }],
verbatim: (node, role) => layerTranslator ? layerTranslator.verbatim(node, layerDocument, role) : [{ text: node.text, role: role ?? 'plain' }],
doc: node => layerTranslator?.doc(node, layerDocument),
chain: (node, groups) => layerTranslator?.chain?.(node, layerDocument, groups),
lookup: layerRules.lookup ?? (() => undefined),
}
/** 一行译文,连着拆开的各项、摊开实参的表、块的说明(有才有)。 */
const textView = (block: TextBlock): { translation: PreviewText, items?: PropertyItem[], table?: PreviewTable, doc?: Markdown } => {
/** 一行译文,连着拆开的各项、摊开实参的表、块的说明、排在下面的几行属性(链式调用的起点、步骤),有才有。 */
const textView = (block: TextBlock): { translation: PreviewText, items?: PropertyItem[], table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } => {
if (!layerTranslator)
return { translation: [] }
const { text, items, table } = layerTranslator.translate(block, layerDocument)
const { text, items, table, rows } = layerTranslator.translate(block, layerDocument)
const doc = block.doc && layerTranslator.doc(block.doc, layerDocument)?.body
return { translation: text, ...(items && { items }), ...(table && { table }), ...(doc && { doc }) }
return { translation: text, ...(items && { items }), ...(table && { table }), ...(doc && { doc }), ...(rows && { rows }) }
}
/** 块执行的嵌着的代码分成的几步:只有一步时是它的子块,几步时是各步,交给那种语言转(见 TextBlock.embedded)。 */
const embeddedChildren = (block: CodeBlock, id: string, scope: string, from: number): BlockView[] => {
@@ -420,16 +450,19 @@ export function toBlockViews(
const marker = layerDocument.language.lineComment
return marker ? { commentTarget: { kind: 'insert', line: block.header.startRow, shell: `${marker} _` } } : {}
}
/** 一行译文的卡片:译文、实参表、说明,有注释时下面再排「注释」一行。 */
function textCard(block: TextBlock): ReturnType<typeof textView> & { rows?: PropertyRow[] } {
/** 一行译文的卡片:译文、实参表、说明、翻译插件排在下面的几行,有注释时「注释」一行插进这几行(见 withCommentRow),没有这几行时自成一行。 */
function textCard(block: TextBlock): ReturnType<typeof textView> {
const row = commentRow(block)
return { ...textView(block), ...(row && { rows: [row] }) }
const view = textView(block)
const rows = view.rows && row ? withCommentRow(view.rows, row) : view.rows ?? (row && [row])
return { ...view, ...(rows && { rows }) }
}
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):编辑范围是 span,头部是块自己那部分,子块照常交给各规则看。scope 见 BlockAnchors.place */
function plain(block: CodeBlock, span: SourceSpan, id: string, scope: string): BlockView {
const name = ownName(block)
const { anchor, inner } = anchors.place(scope, block.kind, name?.text, rowsText(block.header))
const card = 'facts' in block ? cardRows(block, layerTranslator ? layerTranslator.describe(block, layerDocument) : []) : textCard(block)
return {
id,
anchor,
@@ -442,10 +475,10 @@ export function toBlockViews(
concepts: blockConcepts(block),
...commentTarget(block),
children: [
...list(block.children, `${id}.`, inner),
...list(block.children, `${id}.`, inner, 0, chainPlaces(block.children, 'properties' in card ? card.properties : card.rows)),
...embeddedChildren(block, id, inner, block.children.length),
],
...('facts' in block ? cardRows(block, layerTranslator ? layerTranslator.describe(block, layerDocument) : []) : textCard(block)),
...card,
}
}
/**
@@ -465,7 +498,10 @@ export function toBlockViews(
return plain({ ...replacement.as, node: block.node, header: statement, children: kept, ...(attributes && { attributes }), ...(comments && { comments }) }, span, id, scope)
}
const { kind, badge, rows, defines, attributes, endpoint } = replacement
const slots = new Map((replacement.slots ?? []).map(({ block: child, row, item }): [CodeBlock, BlockSlot] => [child, { row, item }]))
const card = cardRows(block, rows)
const places = chainPlaces(kept, card.properties)
for (const { block: child, row, item } of replacement.slots ?? [])
places.set(child, { slot: { row, item } })
const { anchor, inner } = anchors.place(scope, kind, defines && source.slice(defines.startIndex, defines.endIndex), rowsText(statement))
return {
id,
@@ -479,19 +515,22 @@ export function toBlockViews(
concepts: blockConcepts(block, replacement),
...commentTarget(block),
badge,
children: list(kept, `${id}.`, inner, 0, slots),
...cardRows(block, rows),
children: list(kept, `${id}.`, inner, 0, places),
...card,
}
}
/** 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。slots 是父卡片挂在它属性表子行上的那几块(见 BlockSlot)。 */
function list(siblings: readonly CodeBlock[], prefix: string, scope: string, from = 0, slots?: ReadonlyMap<CodeBlock, BlockSlot>): BlockView[] {
/**
* 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。places 是挂在父卡片里的那几块挂在哪:
* 属性表的子行上(见 BlockSlot),或链式调用的某一步下面(见 ChainSlot)。
*/
function list(siblings: readonly CodeBlock[], prefix: string, scope: string, from = 0, places?: ReadonlyMap<CodeBlock, CardPlace>): BlockView[] {
const spans = blockSpans(siblings, source)
const views: BlockView[] = []
siblings.forEach((block, i) => {
const view = build(block, spans[i], `${prefix}${from + views.length}`, scope)
const slot = slots?.get(block)
const place = places?.get(block)
if (view)
views.push(slot ? { ...view, slot } : view)
views.push(place ? { ...view, ...place } : view)
})
return views
}
+126 -2
View File
@@ -392,6 +392,60 @@ export interface CallFacts {
arguments: CallArgument[]
}
/**
* 链式调用里收起来的一个实参:主体是语句块(或只有一个 match)的闭包、async 块。主体里的各句是这条语句的子块,挂在这一步下面;
* 卡片上它收成一行「闭包 |参数| · N 句」,点开才看见那些子块。
*/
export interface DeferredFacts {
/** closure 是闭包,async 是 async 块 */
kind: 'closure' | 'async'
/** 闭包的参数表(`|app, event|` 整个);async 块没有 */
parameters?: Node
/** 主体里写了几句,注释不算 */
statements: number
/** 主体只有一个 match(`|app, event| match event { … }`)时:整个 match、它匹配的那个值、有几支 */
match?: { node: Node, value: Node, arms: number }
}
/** 链里一步的一个实参,照源码顺序;注释不算。 */
export interface ChainArgument {
/** 整个实参 */
node: Node
/** 实参是闭包、async 块(不管主体怎么写):实参里有它的链写在一行里也排成卡片 */
closure: boolean
/** 收起来的闭包、async 块(见 [DeferredFacts]);别的实参没有 */
deferred?: DeferredFacts
}
/** 链式调用的一步:`.方法(实参)`,连同紧跟在调用后面的 `.await`、`?`。 */
export interface ChainStepFacts {
/** 方法名前面的 `.`:它和上一步的结尾(第一步是起点)不在同一行,这条链就是跨行写的 */
dot: Node
method: Node
/** 方法名后面写的泛型实参(`::<Vec<_>>`);没写没有 */
typeArguments?: Node
/** 这一步的调用,从链的开头算起(`a.b(…).c(…)` 整个) */
call: Node
/** 括号连同里面的实参 */
argumentList: Node
arguments: ChainArgument[]
/** 跟在调用后面的 `.await`、`?` 各套的那一层(Rust 的 await_expression、try_expression),从里往外 */
after: Node[]
/** 写在这一步前面(上一步和 `.` 之间)的注释,照源码顺序 */
comments: Node[]
}
/**
* 链式调用(`tauri::Builder::default().plugin(…).setup(…)`、`rows.iter().filter(…).collect()`):语言插件从语法树读出结构
* ([LanguageAdapter.chain]),翻译插件把它排成链式调用卡片(见 [ChainStep])。节点都指向源码。
*/
export interface ChainFacts {
/** 链最前面的接收者或起始表达式(`tauri::Builder::default()`、`rows`) */
start: Node
/** 照源码顺序,至少两步 */
steps: ChainStepFacts[]
}
/**
* 命令的一个重定向:把输出(1)、错误输出(2)或两样都写到 / 追加到一个文件,并进另一路,或从文件读输入。
* `>&2` 是输出并进错误输出,`2>/dev/null` 是错误输出写到 /dev/null。
@@ -623,9 +677,10 @@ export interface Segment { text: string, role: TokenRole, offset?: number, refer
* - condition:按条件(渲染大纲里 `cond &&` 的「当 … 时」、三元的「如果 …」),图标、颜色同「条件」块;
* - branch:条件的一支(三元的「是」「否」),颜色同「条件」块,不带图标;
* - tag:小标签(名称后面的「具名导出」、属性名后面的「回调」),只有框头的字,颜色是 SegmentGroup.color;
* - classes:收起来的一串类名(「样式 N 个类」),中性色,点开、悬停看 SegmentGroup.detail。
* - classes:收起来的一串类名(「样式 N 个类」),中性色,点开、悬停看 SegmentGroup.detail;
* - fold:链式调用卡片里收起来的闭包、async 块(见 [DeferredFacts]),框头是「闭包」,框里是参数和句数,点它展开、收起这一步下面挂着的子块。
*/
export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'tag' | 'classes'
export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'tag' | 'classes' | 'fold'
/**
* 框起来的一组词:译文里自成一体的一段(写在表达式里的遍历、渲染大纲一行前面的条件),预览把它画成嵌在前后文字里的一个小框,
@@ -717,6 +772,8 @@ export interface PropertyRow extends Defines {
note?: Markdown
items?: PropertyItem[]
columns?: string[]
/** 值是一条链式调用的起点时,链的各步:排成一条竖轨道,接在值下面(见 [ChainStep])。 */
chain?: ChainStep[]
}
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
@@ -725,6 +782,53 @@ export interface PreviewTable {
items: PropertyItem[]
}
/**
* 链式调用卡片的一步,排在竖轨道上:方法那一列、值那一列,下面依次是小链、几行属性、表,再是挂在这一步下面的子块
* (实参里收起来的闭包、async 块的主体,宿主挂,见 BlockView.step)。连着几次调用同一个方法的合成一步;
* 库规则还能把连着的几步合成一步、起叫法、换成表(见 [ChainGroup])。
*/
export interface ChainStep {
/**
* 方法那一列:`.名字`,名字带源码下标。翻译插件、库规则起了叫法的写叫法(遍历、启动时):悬停看原来的方法名,
* ⌘ / Ctrl + 点击跳到方法的定义([Segment] 的 refers)。
*/
name: Inline[]
/**
* 值那一列:实参照抄成一行(名字带源码下标),`.await`、`?` 照常翻、接在后面,收起来的闭包、async 块是一个 fold 框;
* 合成的一步是「N 次」,规则换掉的照规则的(命令名)。
*/
value: Inline[]
/** 值那一列下面缩进的一条小链,一节一行:实参本身是一条链(第一行是它的起点),或规则合成的几步不同的方法(一步一行) */
lines?: Inline[][]
/** 这一步下面的几行属性,属性名在方法那一列:写在这一步上面的注释 */
rows?: PropertyRow[]
/** 合成的一步每次调用的实参(一行一次),或规则换成的表(插件 | 配置) */
table?: PreviewTable
/** 这一步在源码里的范围(`.` 到这一步的结尾,合成的一步到最后一次):宿主按它把实参里闭包体的子块挂到这一步下面。 */
range: SourceRange
}
/** 一条链排成的样子([Translator.chain]):起点照抄成一行,各步见 [ChainStep]。 */
export interface ChainView {
start: Inline[]
steps: ChainStep[]
}
/**
* 库规则给一条链的读法:把第 `from` 到第 `to` 步([ChainFacts] 的 steps 下标,含两头)合成一步,起叫法,可以换掉值、换成表。
* 只起叫法的一步 `from` 等于 `to`。
*/
export interface ChainGroup {
from: number
to: number
/** 这一步的叫法,规则自己的用词 */
label: string
/** 换掉值那一列;不给时一步的照常是实参,几步的空着、每步一行排成小链(见 [ChainStep] 的 lines) */
value?: Inline[]
/** 换成的表,给了就不排小链 */
table?: PreviewTable
}
/**
* 一行译文的块翻出来的:一行译文(注释块是 Markdown 说明);预览排在这一行下面的,跨几行写的字面量是拆开的各项(items,同 PropertyItem.items),
* 跨几行写的调用是摊开各个实参的表(table)。
@@ -733,6 +837,11 @@ export interface TextTranslation {
text: PreviewText
items?: PropertyItem[]
table?: PreviewTable
/**
* 排在这一行下面的几行属性:整句是一条该排成卡片的链式调用时是「起点」「步骤」两行,链的各步在「步骤」那一行上(PropertyRow.chain);
* text 这时只剩链外面的词(「返回」),没有就是空的。
*/
rows?: PropertyRow[]
}
/**
@@ -766,6 +875,11 @@ export interface LanguageAdapter {
call?: (node: Node) => CallFacts | undefined
/** node 是写出各项的字面量(字典、列表……)时给出它的各项,别的节点没有;没给的语言不拆字面量,照常一行译文。 */
collection?: (node: Node) => CollectionFacts | undefined
/**
* node 是两步以上的链式调用(`a.b(…).c(…)`,最外面可以套着 `.await`、`?`)时给出它的结构,别的节点没有;
* 没给的语言不排链式调用卡片,照常一行译文。排不排成卡片归翻译插件(跨行写、实参里有闭包才排)。
*/
chain?: (node: Node) => ChainFacts | undefined
/** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;别的节点没有,没有命令的语言不填。 */
command?: (node: Node) => CommandFacts | undefined
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
@@ -876,6 +990,11 @@ export interface Translator {
* 拆出块标签;正文同属性表的「说明」。几条行文档注释连成一段的(Rust 的 `///`),node 是第一条,翻译插件自己往下连。没有文字时没有。
*/
doc: (node: Node, document: SourceDocument) => Doc | undefined
/**
* 语言插件认得出的链式调用([LanguageAdapter.chain])排成链式调用卡片的样子,不管它值不值得排成卡片:库规则拿它改叫法(`groups`,见 [ChainGroup])。
* node 不是链时没有;不给就不排链式调用卡片,照常一行。
*/
chain?: (node: Node, document: SourceDocument, groups?: readonly ChainGroup[]) => ChainView | undefined
/** 筛选属性和值叫什么,和属性表里的属性名、值用同一套叫法;不给就都显示 id。 */
attributeNames?: AttributeNames
}
@@ -1095,6 +1214,11 @@ export interface RuleContext {
verbatim: (node: Node, role?: TokenRole) => Inline[]
/** 用当前翻译插件读一段说明文字(Translator.doc):正文和块标签;没有翻译插件、没有文字时没有。 */
doc: (node: Node) => Doc | undefined
/**
* 用当前翻译插件把一条链式调用排成链式调用卡片([Translator.chain]),`groups` 是规则自己的读法(见 [ChainGroup]):
* 结构从 `document.language.chain` 读,步的下标同它。不是链、翻译插件不排链时没有。
*/
chain: (node: Node, groups?: readonly ChainGroup[]) => ChainView | undefined
/**
* 源码里 name 这个名字(例如 raise 后面的异常类名)指的定义在 index 里认出的那一项,规则从定义处读出的值在 IndexEntry.value 里:
* 定义由语言扩展查(同 ⌘ + 点击),落在 index 认出的某一项的名字上才算;同一份源码里同名的名字按一处查。
+13 -1
View File
@@ -1,5 +1,5 @@
import type { BlockView } from '../blockView'
import type { Inline, PreviewTable, PreviewText, PropertyItem, PropertyRow } from '../contract'
import type { ChainStep, Inline, PreviewTable, PreviewText, PropertyItem, PropertyRow } from '../contract'
import { diffArrays, diffWordsWithSpace } from 'diff'
/**
@@ -102,6 +102,18 @@ function rowWithoutSource(row: PropertyRow): PropertyRow {
...(row.note && { note: row.note }),
...(row.items && { items: row.items.map(itemWithoutSource) }),
...(row.columns && { columns: row.columns }),
...(row.chain && { chain: row.chain.map(stepWithoutSource) }),
}
}
function stepWithoutSource(step: ChainStep): ChainStep {
return {
name: withoutSource(step.name),
value: withoutSource(step.value),
...(step.lines && { lines: step.lines.map(withoutSource) }),
...(step.rows && { rows: step.rows.map(rowWithoutSource) }),
...(step.table && { table: tableWithoutSource(step.table) }),
range: step.range,
}
}
+2 -1
View File
@@ -4,7 +4,7 @@
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { FileAttribute } from '../attributes'
import type { BlockSlot, CallTreeTarget, CommentTarget, SourceColors } from '../blockView'
import type { BlockSlot, CallTreeTarget, ChainSlot, CommentTarget, SourceColors } from '../blockView'
import type { ContractMiss } from '../callTree/endpoint'
import type { CallTree } from '../callTree/model'
import type { BlockAttributes, BlockConcept, KindBadge, Markdown, PreviewTable, PreviewText, PropertyItem, PropertyRow, SourceRange } from '../contract'
@@ -85,6 +85,7 @@ interface PageCardBase {
commentTarget?: CommentTarget['kind']
badge?: KindBadge
slot?: BlockSlot
step?: ChainSlot
callTree?: CallTreeTarget
}
@@ -3,8 +3,10 @@ import type { PageBlock } from './messages'
import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../blockView'
import { createPythonAdapter } from '../languages/python'
import { createRustAdapter } from '../languages/rust'
import { WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { rustZhTranslator } from '../translators/rustZh'
import { pageBlocks } from './pageBlocks'
const py = createPythonAdapter(WASM_DIR)
@@ -67,3 +69,22 @@ describe('发给页面的块带内容指纹:连子块、在源码里占的地
expect(changed(before, hashes(await page(SOURCE.replace('os.getenv(key)', 'os.getenv(name)'))))).toEqual(['2', '2.0', '2.0.0', '2.0.0.0'])
})
})
describe('链式调用卡片发给页面', () => {
const rust = createRustAdapter(WASM_DIR)
it('起点、各步的叫法和范围里的下标也从块的开头算,挂在链某一步下面的子块带着 step', async () => {
const source = 'fn f() {\n rows.iter()\n .for_each(|r| {\n show(r);\n });\n}\n'
const views = await withBlocks(rust, 'rust', source, blocks => toBlockViews(blocks, { source, language: rust, markers: [] }, rustZhTranslator))
const [fn] = pageBlocks(views, source)
const [statement] = fn?.children ?? []
const base = source.indexOf(' rows')
const rows = statement && 'translation' in statement ? statement.rows ?? [] : []
expect(rows[0]?.value).toEqual([{ text: 'rows', role: 'variable', offset: source.indexOf('rows') - base }])
const [, forEach] = rows[1]?.chain ?? []
const [label] = forEach?.name ?? []
expect(label && 'role' in label ? label.refers : undefined).toEqual({ startIndex: source.indexOf('for_each') - base, endIndex: source.indexOf('for_each') + 'for_each'.length - base })
expect(forEach?.range).toEqual({ startIndex: source.indexOf('.for_each') - base, endIndex: source.indexOf(';\n}') - base })
expect(statement?.children[0]?.step).toEqual({ row: 1, step: 1 })
})
})
+15 -1
View File
@@ -1,5 +1,5 @@
import type { BlockView } from '../blockView'
import type { Inline, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment, SourceRange } from '../contract'
import type { ChainStep, Inline, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment, SourceRange } from '../contract'
import type { BlockMarks, ItemDiff, ItemsDiff, RowDiff, RowsDiff, TextDiff } from '../diff/viewDiff'
import type { BlockSpan, PageBlock, PageCard } from './messages'
import { createHash } from 'node:crypto'
@@ -70,6 +70,19 @@ function shiftRow(row: PropertyRow, base: number): PropertyRow {
value: shiftText(row.value, base),
...(row.defines && { defines: shiftRange(row.defines, base) }),
...(row.items && { items: row.items.map(item => shiftItem(item, base)) }),
...(row.chain && { chain: row.chain.map(step => shiftStep(step, base)) }),
}
}
function shiftStep(step: ChainStep, base: number): ChainStep {
return {
...step,
name: shiftInline(step.name, base),
value: shiftInline(step.value, base),
...(step.lines && { lines: step.lines.map(line => shiftInline(line, base)) }),
...(step.rows && { rows: step.rows.map(row => shiftRow(row, base)) }),
...(step.table && { table: shiftTable(step.table, base) }),
range: shiftRange(step.range, base),
}
}
@@ -85,6 +98,7 @@ export function pageCard(block: BlockView, base: number): PageCard {
...(block.commentTarget && { commentTarget: block.commentTarget.kind }),
...(block.badge && { badge: block.badge }),
...(block.slot && { slot: block.slot }),
...(block.step && { step: block.step }),
...(block.callTree && { callTree: block.callTree }),
}
if ('properties' in block)
@@ -0,0 +1,46 @@
use std::sync::Arc;
use tauri::Manager;
pub fn run() {
let app_state = AppState::default();
tauri::Builder::default()
// 新窗口链接只走主窗口 on_new_window 一条路
.plugin(
tauri_plugin_opener::Builder::new()
.open_js_links_on_click(false)
.build(),
)
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
))
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
let store = app.path().app_data_dir()?.join("tunnels.json");
app.manage(Arc::new(Tunnels::new(store)));
Ok(())
})
.on_window_event(|window, event| {
window.hide();
})
.manage(app_state)
.invoke_handler(tauri::generate_handler![
device_resume,
settings_get,
web::preview_open
])
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app, event| match event {
RunEvent::Exit => app.state::<Arc<Host>>().shutdown(),
_ => {}
});
}
#[tauri::command]
fn device_resume() {}
#[tauri::command]
fn settings_get() {}
+245 -4
View File
@@ -1,5 +1,5 @@
import type { BlockView } from '../blockView'
import type { PreviewText } from '../contract'
import type { ChainArgument, ChainFacts, PreviewText } from '../contract'
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { blockCodes, toBlockViews } from '../blockView'
@@ -440,7 +440,7 @@ describe('rust 函数体拆成子块', () => {
{ at: 11, kind: 'statement', kids: [{ at: 12, kind: 'statement' }] },
{ at: 14, kind: 'statement', kids: [{ at: 15, kind: 'statement' }] },
{ at: 17, kind: 'variable', kids: [{ at: 17, kind: 'return' }] },
{ at: 18, kind: 'statement', kids: [{ at: 19, kind: 'statement' }] },
{ at: [18, 20], kind: 'statement', kids: [{ at: 19, kind: 'statement' }] },
{ at: 21, kind: 'return' },
] },
])
@@ -475,8 +475,12 @@ describe('rust 函数体拆成子块', () => {
' 名称: task',
' 初始值: 异步',
' return 17: 返回 1',
' statement 18: items.iter().for_each(|x|',
' statement 19: println!("{x}");',
' statement 18',
' 起点: items',
' 步骤: 2 个',
' ● 遍历',
' ● 逐个执行 | ⟦闭包||x| · 1 句⟧',
' statement 19 @1: println!("{x}");',
' return 21: 返回 add(1, 2)',
])
})
@@ -851,3 +855,240 @@ describe('rust 注释挂到它讲的项上', () => {
])
})
})
/** 链读出的结构写成文字:每步是 [方法名, 泛型实参, 实参, 套在外面的 .await / ?, 注释],收起来的实参写成「种类:句数」。 */
function chainSummary(facts: ChainFacts): unknown {
const argument = ({ node, closure, deferred }: ChainArgument): string => {
if (deferred)
return `${deferred.kind}:${deferred.statements}${deferred.match ? `:match ${deferred.match.value.text}·${deferred.match.arms}` : ''}`
return closure ? `closure:${node.text}` : node.text
}
return {
start: facts.start.text,
steps: facts.steps.map(step => [
step.method.text,
step.typeArguments?.text ?? '',
step.arguments.map(argument),
step.after.map(node => node.type),
step.comments.map(node => node.text.trim()),
]),
}
}
describe('rust 链式调用', () => {
/** 每一句的式子(let 的值、表达式语句的式子)读出的链,不是链的写 none。 */
const chains = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'rust', source, (node) => {
const value = node.type === 'let_declaration' ? node.childForFieldName('value') : node.type === 'expression_statement' ? node.firstNamedChild : null
if (!value)
return undefined
const facts = adapter.chain?.(value)
return facts ? chainSummary(facts) : 'none'
})
it('两步以上的 .方法(…) 从外往里读:.await、? 归它前面那一步,写在 . 前面的注释归这一步、行尾的归上一步;不是方法调用的式子(连同它的 ?)是起点,取字段、一步的不算链', async () => {
const source = [
'fn f() {',
' let body = client',
' // 先发请求',
' .get::<Page>(URL, 3) // 第一页',
' .send()',
' .await?;',
' let p = app.path().app_data_dir()?.join("x");',
' x.a().b.c();',
' one.only();',
' foo()?.bar().baz();',
'}',
].join('\n')
expect(await chains(source)).toEqual([
{ start: 'client', steps: [
['get', '<Page>', ['URL', '3'], [], ['// 先发请求', '// 第一页']],
['send', '', [], ['await_expression', 'try_expression'], []],
] },
{ start: 'app', steps: [
['path', '', [], [], []],
['app_data_dir', '', [], ['try_expression'], []],
['join', '', ['"x"'], [], []],
] },
'none',
'none',
{ start: 'foo()?', steps: [['bar', '', [], [], []], ['baz', '', [], [], []]] },
])
})
it('实参里的闭包:主体是 {}、或只有一个 match 的闭包和 async 块收起来,记下参数、句数、match 的值和几支;主体是一个式子的闭包只标出是闭包', async () => {
const source = [
'fn f() {',
' rows.iter().filter(|r| r.active).for_each(|r| {',
' show(r);',
' // 注释不算一句',
' log(r);',
' });',
' builder.build().run(|app, event| match event { A => 1, _ => 2 });',
' items.into_iter().for_each(|x| { match x { _ => {} } });',
' task.spawn().then(async { 1 });',
'}',
].join('\n')
expect(await chains(source)).toEqual([
{ start: 'rows', steps: [['iter', '', [], [], []], ['filter', '', ['closure:|r| r.active'], [], []], ['for_each', '', ['closure:2'], [], []]] },
'none',
'none',
{ start: 'builder', steps: [['build', '', [], [], []], ['run', '', ['closure:1:match event·2'], [], []]] },
{ start: 'items', steps: [['into_iter', '', [], [], []], ['for_each', '', ['closure:1:match x·1'], [], []]] },
'none',
{ start: 'task', steps: [['spawn', '', [], [], []], ['then', '', ['async:1'], [], []]] },
])
})
it('带收起来的闭包的链是一整块,头部是整句,子块是各个闭包主体里的各句(闭包主体是 match 时是那一块条件),实参是链的那一步取小链里的闭包', async () => {
expect(await tree(readFixture('rustChain.rs'))).toEqual([
{ at: 1, kind: 'import' },
{ at: 2, kind: 'import' },
{ at: 4, kind: 'function', kids: [
{ at: 5, kind: 'variable' },
{ at: [7, 39], kind: 'statement', kids: [
{ at: 21, kind: 'variable' },
{ at: 22, kind: 'statement' },
{ at: 23, kind: 'return' },
{ at: 26, kind: 'statement' },
{ at: 36, kind: 'if', kids: [
{ at: 37, kind: 'if', kids: [{ at: 37, kind: 'return' }] },
{ at: 38, kind: 'if' },
] },
] },
] },
{ at: [42, 43], kind: 'function' },
{ at: [45, 46], kind: 'function' },
])
expect(await tree('fn f() {\n app.plugin(\n b.new()\n .setup(|x| {\n go(x);\n }),\n ).run();\n}\n')).toEqual([
{ at: 1, kind: 'function', kids: [{ at: [2, 7], kind: 'statement', kids: [{ at: 5, kind: 'statement' }] }] },
])
})
it('跨行写的链排成链式调用卡片:起点、步骤两行,一步一个「方法 | 实参」,连着同名的合成一步列出每次的实参,注释挂在步骤上,闭包收起来,子块挂在各自那一步下面', async () => {
const [run] = (await views(readFixture('rustChain.rs'))).filter(block => block.kind === 'function')
expect(viewLines(run ? [run] : [])).toEqual([
'function 4',
' 名称: run',
' 可见性: 公开',
' 返回: ()',
' variable 5',
' 名称: app_state',
' 初始值: AppState::default()',
' statement 7',
' 起点: tauri::Builder::default()',
' 步骤: 8 个',
' ● .plugin | 4 次',
' 注释: 新窗口链接只走主窗口 on_new_window 一条路',
' 实参',
' tauri_plugin_opener::Builder::new().open_js_links_on_click(false).build()',
' tauri_plugin_dialog::init()',
' tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, None)',
' tauri_plugin_updater::Builder::new().build()',
' ● .setup | ⟦闭包||app| · 3 句⟧',
' ● .on_window_event | ⟦闭包||window, event| · 1 句⟧',
' ● .manage | app_state',
' ● .invoke_handler | tauri::generate_handler![ device_resume, settings_get, web::preview_open ]',
' ● .build | tauri::generate_context!()',
' ● .expect | "error while building tauri application"',
' ● .run | ⟦闭包||app, event| · match event · 2 支⟧',
' variable 21 @1',
' 名称: store',
' 初始值: app.path().app_data_dir()?.join("tunnels.json")',
' statement 22 @1: app.manage(Arc::new(Tunnels::new(store)));',
' return 23 @1: 返回 Ok(())',
' statement 26 @2: window.hide();',
' if 36 @7: 匹配 event',
' if 37: RunEvent::Exit',
' return 37: 返回 app.state::<Arc<Host>>().shutdown()',
' if 38: _',
])
})
it('写在一行里、实参里有闭包的链也排;变量的初始值是起点,链接在下面;标准库常见方法写中文名;.await、? 照常翻、跟在那一步后面;返回值同样排;单行又没闭包的照旧一行', async () => {
const source = [
'async fn load(client: Client, rows: Vec<Row>) -> Result<Vec<u8>, Error> {',
' let names: Vec<String> = rows.iter().filter(|r| r.active).map(|r| r.name.clone()).collect();',
' let body = client',
' .get(URL)',
' .send()',
' .await?;',
' let p = app.path().app_data_dir()?.join("x");',
' body.bytes()',
' .await',
' .map(|b| b.to_vec())',
'}',
].join('\n')
expect(viewLines((await views(source))[0]?.children ?? [])).toEqual([
'variable 2',
' 名称: names',
' 类型: Vec<String>',
' 初始值: rows',
' ● 遍历',
' ● 筛选 | |r| r.active',
' ● 映射 | |r| r.name.clone()',
' ● 收集',
'variable 3',
' 名称: body',
' 初始值: client',
' ● .get | URL',
' ● .send | 等待?',
'variable 7',
' 名称: p',
' 初始值: app.path().app_data_dir()?.join("x")',
'return 8: 返回',
' 起点: body',
' 步骤: 2 个',
' ● .bytes | 等待',
' ● 映射 | |b| b.to_vec()',
])
})
it('实参本身是跨行写的链时缩进成一条小链;连着几次同名调用、每次几个实参时按位置分列', async () => {
const source = [
'fn f() {',
' tauri::Builder::default()',
' .plugin(',
' opener::Builder::new()',
' .open(false)',
' .build(),',
' )',
' .manage(state);',
' Router::new()',
' .route("/a", get(list))',
' .route("/b", post(create))',
' .with_state(state);',
'}',
].join('\n')
expect(viewLines((await views(source))[0]?.children ?? [])).toEqual([
'statement 2',
' 起点: tauri::Builder::default()',
' 步骤: 2 个',
' ● .plugin',
' ○ opener::Builder::new()',
' ○ .open(false)',
' ○ .build()',
' ● .manage | state',
'statement 9',
' 起点: Router::new()',
' 步骤: 2 个',
' ● .route | 2 次',
' 第 1 个 | 第 2 个',
' "/a" | get(list)',
' "/b" | post(create)',
' ● .with_state | state',
])
})
it('方法的中文名悬停看原名,⌘ 点击跳到方法名;照抄的实参、起点、方法名都带自己的下标', async () => {
const source = 'fn f() {\n let n = rows.iter().map(|r| r.id).collect();\n}\n'
const [variable] = (await views(source))[0]?.children ?? []
const row = propertyRow(variable, '初始值')
const [iter, map] = row.chain ?? []
const label = iter?.name[0]
expect(label && 'role' in label ? { text: label.text, refers: label.refers, tooltip: label.tooltip } : undefined)
.toEqual({ text: '遍历', refers: { startIndex: source.indexOf('iter'), endIndex: source.indexOf('iter') + 4 }, tooltip: '.iter' })
expect(wordsOf(row.value, 'variable')).toEqual([['rows', source.indexOf('rows')]])
expect(wordsOf(map?.value, 'variable')).toEqual([['r', source.indexOf('|r|') + 1], ['r', source.indexOf('r.id')], ['id', source.indexOf('r.id') + 2]])
expect(map?.range).toEqual({ startIndex: source.indexOf('.map'), endIndex: source.indexOf('.collect') })
})
})
+147 -6
View File
@@ -1,8 +1,8 @@
import type { Access, AttributeFacts, AttributeItem, BlockShape, CallFacts, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TextKind, TokenRole, TraitBlock } from '../contract'
import type { Access, AttributeFacts, AttributeItem, BlockShape, CallFacts, CfgPredicate, ChainArgument, ChainFacts, ChainStepFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeferredFacts, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TextKind, TokenRole, TraitBlock } from '../contract'
import type { CommentRules, ListBlocks } from './shared'
import * as path from 'node:path'
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, isCommentNode, leafBlock, leafShape, listBlocks, named, syntaxChildren } from './shared'
import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, isCommentNode, leafBlock, leafShape, listBlocks, named, nodeSpan, syntaxChildren } from './shared'
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
@@ -17,9 +17,146 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter {
freshDoc,
call: rustCall,
collection: rustCollection,
chain: rustChain,
}
}
/**
* 两步以上的链式调用 `a.b(…).c(…)`:从最外面往里剥,每层 `.方法(…)` 是一步,套在调用外面的 `.await`、`?` 归它里面那一步,
* 剥到不是方法调用的式子就是起点(连同套在它外面的 `.await`、`?`)。宏调用、取字段(`x.a().b.c()` 的 `.b`)不算一步,链从那里断开。
*/
function rustChain(node: Node): ChainFacts | undefined {
const steps: ChainStepFacts[] = []
let current: Node | null = node
let after: Node[] = []
while (current) {
if (current.type === 'await_expression' || current.type === 'try_expression') {
after = [current, ...after]
current = syntaxChildren(current)[0] ?? null
continue
}
const step = methodStep(current, after)
if (!step)
break
steps.unshift(step.facts)
after = []
current = step.receiver
}
const start = after[after.length - 1] ?? current
if (steps.length < 2 || !start)
return undefined
return { start, steps: withTrailingComments(steps, start) }
}
/** 一层 `.方法(实参)`(方法名后面可以带 `::<…>`):这一步和它的接收者;不是方法调用时没有。after 是套在它外面的 `.await`、`?`,从里往外。 */
function methodStep(call: Node, after: Node[]): { facts: ChainStepFacts, receiver: Node } | undefined {
const fn = call.type === 'call_expression' ? call.childForFieldName('function') : null
const argumentList = call.childForFieldName('arguments')
const generic = fn?.type === 'generic_function' ? fn : undefined
const target = generic ? generic.childForFieldName('function') : fn
const method = target?.type === 'field_expression' ? target.childForFieldName('field') : null
const receiver = target?.childForFieldName('value')
const dot = target && children(target).find(child => child.type === '.')
if (!argumentList || argumentList.type !== 'arguments' || method?.type !== 'field_identifier' || !receiver || !dot)
return undefined
const typeArguments = generic?.childForFieldName('type_arguments') ?? undefined
return {
receiver,
facts: {
dot,
method,
...(typeArguments && { typeArguments }),
call,
argumentList,
arguments: syntaxChildren(argumentList).filter(arg => arg.type !== 'attribute_item').map(chainArgument),
after,
comments: children(target).filter(child => isCommentNode(child) && child.endIndex <= dot.startIndex),
},
}
}
function chainArgument(node: Node): ChainArgument {
const deferred = deferredFacts(node)
return { node, closure: node.type === 'closure_expression' || node.type === 'async_block', ...(deferred && { deferred }) }
}
/** 收起来的闭包、async 块(见 [DeferredFacts]):主体是 `{}`、或闭包主体直接是 match;`{}` 里只有一个 match 也算主体只有一个 match。 */
function deferredFacts(node: Node): DeferredFacts | undefined {
const parameters = node.type === 'closure_expression' ? node.childForFieldName('parameters') ?? undefined : undefined
const kind = node.type === 'closure_expression' ? 'closure' : 'async'
const body = deferredBody(node) ?? (node.type === 'closure_expression' ? node.childForFieldName('body') : null)
if (!body || (body.type !== 'block' && body.type !== 'match_expression'))
return undefined
const items = body.type === 'block' ? statementItems(body) : [body]
const [only] = items
const match = items.length === 1 && only ? matchOf(only) : undefined
return {
kind,
...(parameters && { parameters }),
statements: items.length,
...(match && { match: { node: match, value: match.childForFieldName('value') ?? match, arms: named(match.childForFieldName('body')).filter(arm => arm.type === 'match_arm').length } }),
}
}
/** 一句就是一个 match(带不带分号)时是那个 match。 */
function matchOf(item: Node): Node | undefined {
const expression = item.type === 'expression_statement' ? syntaxChildren(item)[0] : item
return expression?.type === 'match_expression' ? expression : undefined
}
/** `{}` 里写的各句:注释、`#[…]`、多出来的空 `;` 不算。 */
function statementItems(block: Node): Node[] {
return blockItems(block).filter(item => !isCommentNode(item) && item.type !== 'attribute_item' && item.type !== 'empty_statement')
}
/** 写在 `.` 前面、和上一步的结尾同一行的注释是上一步的行尾注释,归上一步。 */
function withTrailingComments(steps: ChainStepFacts[], start: Node): ChainStepFacts[] {
return steps.map((step, i) => {
const next = steps[i + 1]
const trailing = next ? next.comments.filter(comment => comment.startPosition.row === stepEnd(step).endPosition.row) : []
const before = i > 0 ? stepEnd(steps[i - 1]).endPosition.row : start.endPosition.row
const own = step.comments.filter(comment => i === 0 || comment.startPosition.row !== before)
return { ...step, comments: [...own, ...trailing] }
})
}
/** 一步的结尾:调用,套着 `.await`、`?` 时是最外面那层。 */
function stepEnd(step: ChainStepFacts): Node {
return step.after[step.after.length - 1] ?? step.call
}
/**
* 链里收起来的闭包、async 块的主体,照源码顺序:各步的实参,只有一个实参、实参本身是链的那一步取那条小链各步的实参
* (翻译插件把它排成小链,见 [链的一步](vscode/src/codeReader/contract.ts#ChainStep) 的 lines)。
*/
function deferredBodies(chain: ChainFacts): Node[] {
return chain.steps.flatMap((step) => {
const [only] = step.arguments
const inner = step.arguments.length === 1 && only ? rustChain(only.node) : undefined
const args = inner ? inner.steps.flatMap(item => item.arguments) : step.arguments
return args.flatMap(arg => arg.deferred ? [arg.node] : [])
})
}
/** 收起来的闭包、async 块主体里的各句 → 块:主体是 `{}` 时同函数体,闭包主体直接是 match 时是那一块条件。 */
function deferredBlocks(node: Node): CodeBlock[] {
const body = deferredBody(node)
if (body)
return statements(blockItems(body), children(body).find(child => child.type === '{')?.startPosition.row ?? body.startPosition.row, true)
const match = node.childForFieldName('body')
return match?.type === 'match_expression' ? [matchBlock(match, match, true)] : []
}
/**
* 值是带收起来的闭包、async 块的链式调用时块的形状:头部是整句(链式调用卡片讲整条链),子块是那几个闭包、async 块主体里的各句,
* 宿主按位置挂到各自那一步下面;别的值没有。
*/
function chainShape(outer: Node, value: Node | null): BlockShape | undefined {
const chain = value ? rustChain(value) : undefined
const bodies = chain ? deferredBodies(chain) : []
return bodies.length > 0 ? { node: outer, header: nodeSpan(outer), children: bodies.flatMap(deferredBlocks) } : undefined
}
/**
* 调用 `f(a)`、`obj.method(a)`、`Type::new(a)`、`parse::<u8>(s)` 的结构,实参都是按位置传的;
* 元组结构体、变体(`Some(x)`)写法和调用一样,不给 construct,翻译插件问名字的定义是哪种块。
@@ -462,6 +599,7 @@ function statementBlocks(item: Node, tail: boolean): CodeBlock[] {
/**
* 当一句写的式子 → 块:outer 是整句(表达式语句,或一层最后那个不带分号的式子本身)。
* 条件、match、循环、`{}` 照控制流拆;是这一层的值(tail)时是 return;别的是 statement,带着闭包、async 块时子块来自它(见 [callbackBody])。
* 式子(return 的值)是带收起来的闭包的链式调用时,子块来自链里那几个闭包(见 [chainShape])。
*/
function expressionBlocks(outer: Node, expression: Node, tail: boolean): CodeBlock[] {
switch (expression.type) {
@@ -483,9 +621,11 @@ function expressionBlocks(outer: Node, expression: Node, tail: boolean): CodeBlo
case 'async_block':
return [bodyBlock('statement', outer, deferredBody(expression), true)]
case 'return_expression':
return [leafBlock('return', outer)]
return [{ kind: 'return', ...(chainShape(outer, syntaxChildren(expression)[0] ?? null) ?? leafShape(outer)) }]
default:
return [tail ? leafBlock('return', outer) : bodyBlock('statement', outer, callbackBody(expression), true)]
if (tail)
return [{ kind: 'return', ...(chainShape(outer, expression) ?? leafShape(outer)) }]
return [{ kind: 'statement', ...(chainShape(outer, expression) ?? bodyShape(outer, callbackBody(expression), true)) }]
}
}
@@ -518,7 +658,8 @@ function armBlock(arm: Node, tail: boolean): CodeBlock {
/**
* `let` → variable,模式是名字;值是主体写成 `{}` 的闭包时是 function,同 TypeScript 把箭头函数赋给变量。
* `let … else` 的 else 是它的子块;值带着闭包、async 块时子块来自它(见 [callbackBody])。
* `let … else` 的 else 是它的子块;值是带收起来的闭包的链式调用时子块来自那几个闭包(见 [chainShape]),
* 别的值带着闭包、async 块时子块来自它(见 [callbackBody])。
*/
function letBlock(node: Node): CodeBlock {
const pattern = node.childForFieldName('pattern')
@@ -530,7 +671,7 @@ function letBlock(node: Node): CodeBlock {
const alternative = node.childForFieldName('alternative')
return {
kind: 'variable',
...(alternative ? letElseShape(node, alternative) : bodyShape(node, value && callbackBody(value), true)),
...(alternative ? letElseShape(node, alternative) : chainShape(node, value) ?? bodyShape(node, value && callbackBody(value), true)),
facts: { declarators: [{ name: pattern, type: node.childForFieldName('type') ?? undefined, value: value ?? undefined }] },
}
}
@@ -3,11 +3,13 @@ import type { DefinitionStart, IndexedName, NameSources, ShownName, TextRange }
import { describe, expect, it } from 'vitest'
import { readBlockViews } from './blockView'
import { createPythonAdapter } from './languages/python'
import { createRustAdapter } from './languages/rust'
import { createTypeScriptAdapter } from './languages/typescript'
import { displayNameIndex, DocumentNames, indexedNames, renderedNames, resolveNames, shiftNames } from './nameResolution'
import { enumRule } from './rules/enum'
import { MemoryIndexHost, propertyLines, runtime, WASM_DIR } from './testSupport'
import { pythonZhTranslator } from './translators/pythonZh'
import { rustZhTranslator } from './translators/rustZh'
import { typescriptZhTranslator } from './translators/typescriptZh'
import { WorkspaceIndexes } from './workspaceIndex'
@@ -98,6 +100,15 @@ describe('中文名落到哪些名字上', () => {
expect(rendered.get(at(ROUTES, 'delete_item'))).toBe('delete_item')
})
it('renderedNames:链式调用卡片的起点、各步的方法名和实参、小链、合成一步的表里带下标的名字也算', async () => {
const source = 'fn f() {\n app\n .plugin(\n opener::new()\n .open(flag)\n .build(),\n )\n .setup(state)\n .manage(a)\n .manage(b);\n}\n'
const rendered = renderedNames((await readBlockViews(runtime, createRustAdapter(WASM_DIR), rustZhTranslator, 'rust', source)).blocks)
for (const name of ['app', '.plugin', 'opener', '.open', 'flag', '.build', '.setup', 'state', '.manage', '(a)', '(b)']) {
const word = name.replace(/[.()]/g, '')
expect(rendered.get(at(source, name) + name.indexOf(word))).toBe(word)
}
})
it('renderedNames:一行译文下面那张表(跨几行写的调用的实参)、值那一格里套着的表里带下标的名字也算', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n owner=Owner(\n name=user.name,\n ),\n )\n'
const rendered = renderedNames((await read(source)).blocks)
+17 -6
View File
@@ -1,5 +1,5 @@
import type { BlockView } from './blockView'
import type { DisplayName, IndexEntry, Inline, LanguageAdapter, NameBinding, PreviewText, PropertyItem, RuleLocation } from './contract'
import type { DisplayName, IndexEntry, Inline, LanguageAdapter, NameBinding, PreviewText, PropertyItem, PropertyRow, RuleLocation } from './contract'
import type { IndexedEntry, WorkspaceIndexOf } from './workspaceIndex'
/**
@@ -15,7 +15,7 @@ export interface ShownName {
definition?: true
}
/** 视图数据里照抄自源码、带下标的名字(一行译文和它下面拆开的各项、表,属性表的值、各层子行的格子、框里):下标 → 原文。 */
/** 视图数据里照抄自源码、带下标的名字(一行译文和它下面拆开的各项、表、几行属性,属性表的值、各层子行的格子、链式调用的各步、框里):下标 → 原文。 */
export function renderedNames(blocks: readonly BlockView[]): Map<number, string> {
const found = new Map<number, string>()
const inline = (pieces: readonly Inline[]): void => {
@@ -37,18 +37,29 @@ export function renderedNames(blocks: readonly BlockView[]): Map<number, string>
items(item.table?.items)
}
}
const rows = (list: readonly PropertyRow[] | undefined): void => {
for (const row of list ?? []) {
text(row.value)
items(row.items)
for (const step of row.chain ?? []) {
inline(step.name)
inline(step.value)
step.lines?.forEach(inline)
rows(step.rows)
items(step.table?.items)
}
}
}
const visit = (list: readonly BlockView[]): void => {
for (const block of list) {
if ('properties' in block) {
for (const row of block.properties) {
text(row.value)
items(row.items)
}
rows(block.properties)
}
else {
text(block.translation)
items(block.items)
items(block.table?.items)
rows(block.rows)
}
visit(block.children)
}
+1 -1
View File
@@ -537,7 +537,7 @@ describe('整份文件的扫描', () => {
await withBlocks(py, 'python', ROUTES, (blocks) => {
const imports = importsIn(blocks)
let reads = 0
const base = { document: { source: ROUTES, language: py, markers: [] }, translate: () => [], verbatim: () => [], doc: () => undefined, lookup: () => undefined }
const base = { document: { source: ROUTES, language: py, markers: [] }, translate: () => [], verbatim: () => [], doc: () => undefined, chain: () => undefined, lookup: () => undefined }
const shared: RuleContext = { ...base, get imports() {
reads++
return imports
+27 -6
View File
@@ -1,6 +1,6 @@
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { BlockView, SourceColors } from './blockView'
import type { BlockKind, CodeBlock, Inline, LanguageAdapter, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment, TokenRole } from './contract'
import type { BlockKind, ChainStep, CodeBlock, Inline, LanguageAdapter, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment, TokenRole } from './contract'
import type { FileEvent, IndexHost, WorkspaceFile } from './workspaceIndex'
import { readFileSync } from 'node:fs'
import * as path from 'node:path'
@@ -155,7 +155,7 @@ function itemLines(items: PropertyItem[] | undefined, indent: string): string[]
/** 属性表写成文字:每行 `名称: 值`(跟着说明的写成 `值 — 说明`),展开的子行比上一层多缩进两格(见 itemLines)。 */
export function propertyLines(rows: PropertyRow[]): string[] {
return rows.flatMap(row => [`${row.label}: ${previewText(row.value)}${row.note ? ` — ${row.note.markdown}` : ''}`, ...itemLines(row.items, ' ')])
return rows.flatMap(row => [`${row.label}: ${previewText(row.value)}${row.note ? ` — ${row.note.markdown}` : ''}`, ...itemLines(row.items, ' '), ...chainLines(row.chain, ' ')])
}
/** 一行译文下面那张表写成文字,同 propertyLines 的子行,第一层不缩进。 */
@@ -171,7 +171,7 @@ export function listLines(items: PropertyItem[] | undefined): string[] {
/**
* 视图数据写成文字(整份源码读出来的样子):一行译文的块是「种类 行号: 译文」(下面的「注释」一行写成「注释: 第一行」),属性表块是「种类 行号」下面每行「属性: 值」,
* 子块比上一层多缩进两格;Markdown 只取第一行。子行和表(属性行展开的、译文下面拆开的各项和表、格子里套的)每行比上一层多缩进,
* 格子用 | 隔开,格子里的换行写成 ⏎。
* 格子用 | 隔开,格子里的换行写成 ⏎。译文是空的块只写「种类 行号」;一行带着的链式调用见 [chainLines];挂在链第 N 步下面的子块在行号后面写 `@N`。
*/
export function viewLines(blocks: readonly BlockView[], indent = ''): string[] {
const value = (text: PreviewText): string => 'markdown' in text ? text.markdown.split('\n')[0] : segmentText(text)
@@ -180,19 +180,40 @@ export function viewLines(blocks: readonly BlockView[], indent = ''): string[] {
...items(item.items, `${pad} `),
...items(item.table?.items, `${pad} `),
])
const rows = (list: readonly PropertyRow[], pad: string): string[] => list.flatMap(row => [
`${pad}${row.label}: ${value(row.value)}`,
...items(row.items, `${pad} `),
...chainLines(row.chain, `${pad} `),
])
return blocks.flatMap((block) => {
const at = `${indent}${block.kind} ${block.startLine + 1}${block.step ? ` @${block.step.step}` : ''}`
const text = 'translation' in block ? value(block.translation) : ''
const head = 'properties' in block
? [`${indent}${block.kind} ${block.startLine + 1}`, ...block.properties.flatMap(row => [`${indent} ${row.label}: ${value(row.value)}`, ...items(row.items, `${indent} `)])]
? [at, ...rows(block.properties, `${indent} `)]
: [
`${indent}${block.kind} ${block.startLine + 1}: ${value(block.translation)}`,
text ? `${at}: ${text}` : at,
...items(block.items, `${indent} `),
...items(block.table?.items, `${indent} `),
...(block.rows ?? []).map(row => `${indent} ${row.label}: ${value(row.value)}`),
...rows(block.rows ?? [], `${indent} `),
]
return [...head, ...viewLines(block.children, `${indent} `)]
})
}
/**
* 链式调用卡片的各步写成文字:一步一行 `● 方法 | 值`(没有值只写方法),下面依次是小链 `○ 一节`、几行属性 `名称: 值`、
* 表(先一行列名,再每行一次),都比这一步多缩进两格。
*/
export function chainLines(steps: readonly ChainStep[] | undefined, pad: string): string[] {
const value = (text: PreviewText): string => 'markdown' in text ? text.markdown.split('\n')[0] : segmentText(text)
return (steps ?? []).flatMap(step => [
`${pad}● ${segmentText(step.name)}${step.value.length > 0 ? ` | ${segmentText(step.value)}` : ''}`,
...(step.lines ?? []).map(line => `${pad} ○ ${segmentText(line)}`),
...(step.rows ?? []).map(row => `${pad} ${row.label}: ${value(row.value)}`),
...(step.table ? [`${pad} ${step.table.columns.join(' | ')}`, ...itemLines(step.table.items, `${pad} `)] : []),
])
}
/** 卡片上「注释」一行(中文翻译插件给它起的名字就是「注释」)的 Markdown 原文;没有就是 undefined。 */
export function commentText(view: BlockView | undefined): string | undefined {
if (!view)
+46 -2
View File
@@ -52,6 +52,49 @@ const template = createZhTemplateTranslator({
operatorParents: [],
emptyLiterals: {},
collections: { array_expression: '数组', tuple_expression: '元组' },
// 迭代器、Option、Result 上常用的方法;unwrap、expect 这类照写方法名
methods: {
iter: '遍历',
iter_mut: '可改地遍历',
into_iter: '逐个取出',
filter: '筛选',
map: '映射',
filter_map: '筛选并映射',
flat_map: '映射并展开',
flatten: '展开',
for_each: '逐个执行',
collect: '收集',
enumerate: '编号',
zip: '配对',
chain: '接上',
rev: '倒序',
take: '取前几个',
skip: '跳过前几个',
take_while: '一直取到',
skip_while: '一直跳过',
fold: '累积',
count: '计数',
sum: '求和',
min: '最小',
max: '最大',
any: '有一个满足',
all: '全都满足',
find: '查找',
position: '找位置',
last: '最后一个',
nth: '取第几个',
peekable: '可预看',
cloned: '复制每项',
copied: '复制每项',
inspect: '顺便查看',
map_err: '转换错误',
and_then: '接着',
ok_or: '没有时报错',
ok_or_else: '没有时报错',
unwrap_or: '取值或用默认',
unwrap_or_else: '取值或另算',
unwrap_or_default: '取值或用缺省',
},
stringNodes: ['string_literal'],
commentText,
docNodes,
@@ -85,12 +128,13 @@ export const rustZhTranslator: Translator = {
/** 写了 `return` 的返回块:`return x;` 整句,或 match 一支的值 `=> return x`。 */
const RETURN_WRITTEN = new Set(['expression_statement', 'return_expression'])
/** 一层最后那个不带分号的式子是返回值,源码里没有 return 这个词,译文照写了的那样在前面补「返回」。 */
/** 一层最后那个不带分号的式子是返回值,源码里没有 return 这个词,译文照写了的那样在前面补「返回」(整句是链时译文就只有它)。 */
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
const translation = template.translate(block, document)
if (block.kind !== 'return' || RETURN_WRITTEN.has(block.node.type) || !Array.isArray(translation.text))
return translation
return { ...translation, text: [{ text: '返回', role: 'keyword' }, { text: ' ', role: 'plain' }, ...translation.text] }
const rest: Inline[] = translation.text.length > 0 ? [{ text: ' ', role: 'plain' }, ...translation.text] : []
return { ...translation, text: [{ text: '返回', role: 'keyword' }, ...rest] }
}
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
@@ -1442,7 +1442,7 @@ describe('跨几行写的调用', () => {
expect(propertyLines(rows.get(1) ?? []).slice(-4)).toEqual(['值: 新建 Server · 2 个参数', ' 第 1 个 | 新建 Options · 1 个参数', ' 第 1 个 | port', ' 第 2 个 | handler'])
})
it('链式调用跨了几行、实参列表各自写在一行里的不排成表;回调主体不在块头部里的调用也不排', async () => {
it('链式调用跨了几行、实参列表各自写在一行里的不排成表,挤成一行时另起一行的 . 贴着前面;回调主体不在块头部里的调用也不排', async () => {
const source = [
'const ids = items',
' .filter(a => a.ok)',
@@ -1452,7 +1452,7 @@ describe('跨几行写的调用', () => {
'})',
].join('\n')
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
expect(propertyLines(rows.get(1) ?? []).at(-1)).toBe('值: items .filter(a => a.ok) .map(a => a.id)')
expect(propertyLines(rows.get(1) ?? []).at(-1)).toBe('值: items.filter(a => a.ok).map(a => a.id)')
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
expect(texts.get(4)).toEqual(expect.not.objectContaining({ table: expect.anything() }))
})
+267 -24
View File
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CollectionFacts, 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 { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ChainArgument, ChainFacts, ChainGroup, ChainStep, ChainStepFacts, ChainView, ClassFacts, CollectionFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, DeferredFacts, 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'
@@ -45,6 +45,8 @@ export interface ZhTable {
emptyLiterals: Record<string, string>
/** 写出各项的字面量(语言插件给了各项,见 LanguageAdapter.collection)的叫法,按节点类型查:属性表里拆开的值写成「字典 3 项」「列表 2 项」。 */
collections: Record<string, string>
/** 链式调用卡片里常见方法的中文名,按方法名查(`iter` → 遍历):方法那一列写它,悬停看原名;没有的写 `.方法名`。 */
methods?: Record<string, string>
/** 整段原样照抄、按字符串着色的节点。 */
stringNodes: string[]
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
@@ -322,14 +324,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return parts.every((part): part is string | Node => part != null) ? parts : undefined
}
/**
* 源码里上一个词到 index 之间有空白,下一个词前面就空一格;跨行写的一对圆括号挤成一行时贴着里面的内容
* (`f(\n x\n)` 写成 `f(x)`),方括号、花括号照旧空一格。
*/
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格;跨行写的挤成一行时换行处该贴着写的贴着写(见 [hugs])。 */
function advance(out: Collector, index: number): void {
const between = out.document.source.slice(out.cursor, index)
const hugging = between.includes('\n') && (out.tokens.at(-1)?.raw === '(' || out.document.source[index] === ')')
out.gap ||= !hugging && /\s/.test(between)
out.gap ||= !hugs(between, out.tokens.at(-1)?.raw, out.document.source.slice(index, index + 1)) && /\s/.test(between)
out.cursor = index
}
@@ -719,28 +717,43 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
/**
* `node` 的源码原样照抄:叶子按源码着色(给了 `role` 就都按它),名字带源码下标,叶子之间的空白、标点照抄;
* `flat` 为真时压成一行,连着的空白(含换行)收成一个空格。
* `flat` 为真时压成一行:连着的空白(含换行)收成一个空格,换行处该贴着写的贴着写(见 [hugs]),注释不抄,右括号前的尾逗号去掉。
*/
function verbatim(node: Node, { source, language }: SourceDocument, role?: TokenRole, flat = false): Segment[] {
const copy = (text: string): string => flat ? text.replace(/\s+/g, ' ') : text
const segments: Segment[] = []
let cursor = node.startIndex
const visit = (current: Node): void => {
const leaves: Node[] = []
const gather = (current: Node): void => {
if (current.startIndex === current.endIndex)
return
if (current.childCount > 0) {
for (const child of current.children) {
if (child)
visit(child)
}
if (current.childCount === 0) {
leaves.push(current)
return
}
pushSegment(segments, copy(source.slice(cursor, current.startIndex)), 'plain')
pushSegment(segments, copy(current.text), role ?? language.tokenRole(current), offsetOf(current, language))
cursor = current.endIndex
for (const child of current.children) {
if (child)
gather(child)
}
visit(node)
pushSegment(segments, copy(source.slice(cursor, node.endIndex)), 'plain')
}
gather(node)
const segments: Segment[] = []
let cursor = node.startIndex
// 压成一行时不抄的注释、尾逗号连同它前面的空白,记到下一个词前面
let pending = ''
let previous: string | undefined
leaves.forEach((leaf, i) => {
const between = pending + source.slice(cursor, leaf.startIndex)
cursor = leaf.endIndex
const own = language.tokenRole(leaf)
if (flat && (own === 'comment' || (leaf.type === ',' && CLOSING.has(leaves[i + 1]?.type ?? '')))) {
pending = own === 'comment' ? `${between} ` : between
return
}
pending = ''
pushSegment(segments, !flat ? between : hugs(between, previous, leaf.text) ? '' : between.replace(/\s+/g, ' '), 'plain')
pushSegment(segments, flat ? leaf.text.replace(/\s+/g, ' ') : leaf.text, role ?? own, offsetOf(leaf, language))
previous = leaf.text
})
const tail = source.slice(cursor, node.endIndex)
pushSegment(segments, flat ? tail.replace(/\s+/g, ' ') : tail, 'plain')
return segments
}
@@ -757,12 +770,206 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return { markdown: docText(nodes.map(node => commentOf(node, document))) }
}
// ─── 链式调用 ─────────────────────────────────────────────────────────────
/** 一条链值不值得排成卡片:跨行写(某一步的 `.` 另起一行)或实参里有闭包时是它的结构;不值得、语言插件读不出链时没有。 */
function cardChain(node: Node, document: SourceDocument): ChainFacts | undefined {
const facts = document.language.chain?.(node)
if (!facts)
return undefined
const broken = facts.steps.some((step, i) => step.dot.startPosition.row > (i === 0 ? facts.start : stepEnd(facts.steps[i - 1])).endPosition.row)
return broken || facts.steps.some(step => step.arguments.some(arg => arg.closure)) ? facts : undefined
}
/**
* 链排成卡片的样子:起点照抄一行,各步照源码顺序。规则给的读法(groups)先合;别的连着调用同一个方法的合成一步(见 [repeatedStep]),
* 剩下的一步一项(见 [singleStep])。
*/
function chainView(facts: ChainFacts, document: SourceDocument, groups: readonly ChainGroup[] = []): ChainView {
const { steps } = facts
const out: ChainStep[] = []
for (let i = 0; i < steps.length;) {
const from = i
const group = groups.find(item => item.from === from && item.to >= from && item.to < steps.length)
if (group) {
out.push(groupStep(steps.slice(from, group.to + 1), group, document))
i = group.to + 1
continue
}
let end = from + 1
while (end < steps.length && steps[end].method.text === steps[from].method.text && !groups.some(item => item.from === end))
end++
out.push(end - from > 1 ? repeatedStep(steps.slice(from, end), document) : singleStep(steps[from], document))
i = end
}
return { start: verbatim(facts.start, document, undefined, true), steps: out }
}
/** 一步一项:实参本身是值得排成卡片的链时排成小链,写在这一步上面的注释是「注释」一行。 */
function singleStep(step: ChainStepFacts, document: SourceDocument): ChainStep {
const lines = miniLines(step, document)
const rows = noteRows([step], document)
return {
name: stepName([step], document),
value: stepValue(step, document, lines !== undefined),
...(lines && { lines }),
...(rows && { rows }),
range: stepRange([step]),
}
}
/** 连着几次调用同一个方法合成一步:值是「N 次」,下面一张表一行一次,列是各个实参(每次最多一个时只有一列「实参」)。 */
function repeatedStep(steps: ChainStepFacts[], document: SourceDocument): ChainStep {
const most = Math.max(...steps.map(step => step.arguments.length))
const columns = most <= 1 ? ['实参'] : Array.from({ length: most }, (_, i) => `第 ${i + 1} 个`)
const rows = noteRows(steps, document)
return {
name: stepName(steps, document),
value: [hint(`${steps.length} 次`)],
...(most > 0 && { table: { columns, items: steps.map(step => ({ cells: step.arguments.map(arg => argumentText(arg, document)) })) } }),
...(rows && { rows }),
range: stepRange(steps),
}
}
/** 规则合成的一步(见 [ChainGroup]):叫法是规则的;规则没换值、没给表时一步的照常,几步的每步一行排成小链。 */
function groupStep(steps: ChainStepFacts[], group: ChainGroup, document: SourceDocument): ChainStep {
const [first] = steps
const single = steps.length === 1 ? first : undefined
const replaced = group.value !== undefined || group.table !== undefined
const mini = single && !replaced ? miniLines(single, document) : undefined
const lines = replaced ? undefined : single ? mini : steps.map(step => callLine(step, document))
const rows = noteRows(steps, document)
return {
name: stepName(steps, document, group.label),
value: group.value ?? (single ? stepValue(single, document, mini !== undefined) : []),
...(lines && { lines }),
...(rows && { rows }),
...(group.table && { table: group.table }),
range: stepRange(steps),
}
}
/**
* 方法那一列:起了叫法(规则给的 label,或词表里这个方法的中文名,见 [ZhTable] 的 methods)写叫法,几步都是同一个方法时悬停看原名、
* ⌘ / Ctrl + 点击跳到第一步的方法名;几步不同的方法合成的没有原名可讲。没有叫法时写 `.名字`。
*/
function stepName(steps: ChainStepFacts[], document: SourceDocument, label?: string): Inline[] {
const [first] = steps
const same = steps.every(step => step.method.text === first.method.text)
const word = label ?? (same ? table.methods?.[first.method.text] : undefined)
if (word === undefined)
return methodText(first, document)
if (!same)
return [{ text: word, role: 'function' }]
return [{ text: word, role: 'function', refers: { startIndex: first.method.startIndex, endIndex: first.method.endIndex }, tooltip: `.${first.method.text}` }]
}
/** `.名字`,名字按函数着色、带源码下标;写了泛型实参的跟着 `::<…>`。 */
function methodText(step: ChainStepFacts, document: SourceDocument): Inline[] {
return [
{ text: '.', role: 'plain' },
segment(step.method.text, 'function', step.method.startIndex),
...(step.typeArguments ? [{ text: '::', role: 'plain' as const }, ...verbatim(step.typeArguments, document, undefined, true)] : []),
]
}
/** 值那一列:实参(见 [argumentText])用逗号隔开,`.await`、`?` 跟在后面(见 [afterText]);实参排成了小链时只剩后面这截。 */
function stepValue(step: ChainStepFacts, document: SourceDocument, mini: boolean): Inline[] {
const args = mini ? [] : joined(step.arguments.map(arg => argumentText(arg, document)), ', ')
const after = afterText(step, document)
return args.length > 0 && after.length > 0 ? [...args, { text: ' ', role: 'plain' }, ...after] : [...args, ...after]
}
/** 一个实参:收起来的闭包、async 块是一个 fold 框(见 [foldGroup]),别的照抄成一行。 */
function argumentText(arg: ChainArgument, document: SourceDocument): Inline[] {
return arg.deferred ? [foldGroup(arg.deferred, document)] : verbatim(arg.node, document, undefined, true)
}
/** 收起来的闭包、async 块:「闭包 |参数| · N 句」,主体只有一个 match 时是「闭包 |参数| · match 值 · N 支」;参数、match 照抄。 */
function foldGroup(deferred: DeferredFacts, document: SourceDocument): SegmentGroup {
const say = (node: Node): Inline[] => verbatim(node, document, undefined, true)
const parts: Inline[][] = [
...(deferred.parameters ? [say(deferred.parameters)] : []),
...(deferred.match
? [[...say(firstLeaf(deferred.match.node)), { text: ' ', role: 'plain' as const }, ...say(deferred.match.value)], [hint(`${deferred.match.arms} 支`)]]
: [[hint(`${deferred.statements} 句`)]]),
]
return { style: 'fold', label: deferred.kind === 'closure' ? '闭包' : '异步块', segments: parts.flatMap((part, i) => i > 0 ? [hint(' · '), ...part] : part) }
}
/** 跟在调用后面的 `.await`、`?` 照常翻(「等待」),连接的 `.` 不写。 */
function afterText(step: ChainStepFacts, document: SourceDocument): Inline[] {
const out: Inline[] = []
let from = step.call.endIndex
for (const layer of step.after) {
const mark = layer.children.find(child => child !== null && child.startIndex >= from && child.type !== '.')
if (mark)
appendInline(out, joinTokens(tokensIn(layer, { ...nodeSpan(layer), startIndex: mark.startIndex }, document)))
from = layer.endIndex
}
return out
}
/** 只有一个实参、它本身是值得排成卡片的链时排成小链:第一行是它的起点,后面一步一行(见 [callLine]);别的没有。 */
function miniLines(step: ChainStepFacts, document: SourceDocument): Inline[][] | undefined {
const [only] = step.arguments
const inner = step.arguments.length === 1 && only && !only.deferred ? cardChain(only.node, document) : undefined
return inner && [verbatim(inner.start, document, undefined, true), ...inner.steps.map(item => callLine(item, document))]
}
/** 小链的一节:`.名字(实参)`,`.await`、`?` 跟在后面。 */
function callLine(step: ChainStepFacts, document: SourceDocument): Inline[] {
const after = afterText(step, document)
return [
...methodText(step, document),
{ text: '(', role: 'plain' },
...joined(step.arguments.map(arg => argumentText(arg, document)), ', '),
{ text: ')', role: 'plain' },
...(after.length > 0 ? [{ text: ' ', role: 'plain' as const }, ...after] : []),
]
}
/** 写在这几步上面的注释读成「注释」一行(Markdown 原文),每一步的一段,空行隔开;都没有时没有。 */
function noteRows(steps: ChainStepFacts[], document: SourceDocument): PropertyRow[] | undefined {
const parts = steps.filter(step => step.comments.length > 0).map(step => docText(step.comments.map(comment => commentOf(comment, document))))
return parts.length > 0 ? [{ label: KIND_ZH.comment, value: { markdown: parts.join('\n\n') } }] : undefined
}
/** 块整句就是一条值得排成卡片的链(外面只套着 return 这类关键字和分号):从块的节点往下,沿着只有一个具名子节点的那几层找。 */
function wholeChain(root: Node, span: SourceSpan, document: SourceDocument): { node: Node, facts: ChainFacts } | undefined {
for (let node: Node | undefined = root; node && inside(node, span);) {
const facts = cardChain(node, document)
if (facts)
return { node, facts }
const kids: Node[] = node.namedChildren.filter((child): child is Node => child !== null && !child.isExtra)
node = kids.length === 1 ? kids[0] : undefined
}
return undefined
}
/** 整句是链的块:译文只剩链外面的词(`return` 的「返回」,分号不写),下面是「起点」「步骤」两行,链的各步在「步骤」那一行上。 */
function chainTranslation(block: TextBlock, chain: Node, facts: ChainFacts, document: SourceDocument): TextTranslation {
const { header } = block
const tokens = [
...tokensIn(block.node, { ...header, endIndex: chain.startIndex }, document),
...tokensIn(block.node, { ...header, startIndex: chain.endIndex }, document),
]
while (tokens.length > 0 && SEPARATORS.has(tokens[tokens.length - 1].raw))
tokens.pop()
const view = chainView(facts, document)
return { text: joinTokens(tokens), rows: [{ label: '起点', value: view.start }, { label: '步骤', value: count(view.steps.length), chain: view.steps }] }
}
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
if (block.kind === 'comment')
return { text: translateComment(block, document) }
const group = block.group && table.groups?.[block.node.type]
if (block.group && group)
return group(block.group, readerContext(collector(block.node, block.header, document), block.header))
const chained = wholeChain(block.node, block.header, document)
if (chained)
return chainTranslation(block, chained.node, chained.facts, document)
const slot: Slot = {}
const tokens = tokensIn(block.node, block.header, document, slot)
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
@@ -788,6 +995,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
const phrases: Phrases = {
translated: node => joinTokens(tokensIn(node, clipped(nodeSpan(node)), document)),
value: node => cellValue(node, clipped(nodeSpan(node)), document),
chain: (node) => {
const facts = inside(node, header) ? cardChain(node, document) : undefined
return facts && chainView(facts, document)
},
body: node => expression(node, document),
returned: (node) => {
const [value] = language.command?.(node)?.arguments ?? []
@@ -849,10 +1060,36 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
expression,
verbatim: (node, document, role) => verbatim(node, document, role, true),
doc,
chain: (node, document, groups) => {
const facts = document.language.chain?.(node)
return facts && chainView(facts, document, groups)
},
attributeNames: ZH_ATTRIBUTE_NAMES,
}
}
/**
* 跨行写的挤成一行时,两个词中间的换行处贴着写:圆括号贴着里面的内容(`f(\n x\n)` 写成 `f(x)`),另起一行的 `.` 贴着前面
* (链式调用 `a\n .b()` 写成 `a.b()`);方括号、花括号照旧空一格。
* @param between 两个词中间的源码
* @param previous 前一个词
* @param next 后一个词(或从它开头的源码)
*/
/** 链式调用一步的结尾:调用,套着 `.await`、`?` 时是最外面那层。 */
function stepEnd(step: ChainStepFacts): Node {
return step.after[step.after.length - 1] ?? step.call
}
/** 几步合起来在源码里的范围:第一步的 `.` 到最后一步的结尾(见 ChainStep.range)。 */
function stepRange(steps: readonly ChainStepFacts[]): SourceRange {
const last = steps[steps.length - 1]
return { startIndex: steps[0].dot.startIndex, endIndex: stepEnd(last).endIndex }
}
function hugs(between: string, previous: string | undefined, next: string): boolean {
return between.includes('\n') && (previous === '(' || next.startsWith(')') || next.startsWith('.'))
}
function nodeSpan(node: Node): SourceSpan {
return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }
}
@@ -928,6 +1165,8 @@ interface Phrases {
translated: (node: Node) => Inline[]
/** 一格里的值,只翻落在块头部里的那一段:字面量的各项拆成下一层子行,跨几行写的调用排成「新建 / 调用 …」带实参表(见 cellValue)。 */
value: (node: Node) => CellValue
/** 值是一条值得排成卡片的链式调用、整个落在块头部里时它排成的样子(见 ChainView);别的值没有。 */
chain: (node: Node) => ChainView | undefined
/** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */
body: (node: Node) => Inline[]
/** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */
@@ -1389,9 +1628,13 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
}
/**
* 声明的值(见 Phrases.value):拆开的字面量的各项、跨几行写的调用的实参表(参数 | 值,带列名)都展开在这一行下面。
* 声明的值(见 Phrases.value):拆开的字面量的各项、跨几行写的调用的实参表(参数 | 值,带列名)都展开在这一行下面;
* 值是一条值得排成卡片的链式调用时,这一行写起点,链的各步接在下面(见 Phrases.chain)。
*/
function declaredValue(value: Node, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
function declaredValue(value: Node, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns' | 'chain'> {
const chain = say.chain(value)
if (chain)
return { value: chain.start, chain: chain.steps }
const { text, items, table } = say.value(value)
if (table)
return { value: text, ...table }