From ee595f3da9f3cb27eb637621287bd46664341262 Mon Sep 17 00:00:00 2001 From: cruldra Date: Thu, 15 Oct 2026 20:54:59 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E7=9A=84=E9=93=BE=E5=BC=8F=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=8D=A1=E7=89=87=EF=BC=9A=E8=AF=AD=E8=A8=80=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E8=AF=BB=E9=93=BE=E3=80=81=E7=BF=BB=E8=AF=91=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=8E=92=E6=88=90=E8=B5=B7=E7=82=B9=E5=92=8C=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 契约加 LanguageAdapter.chain、Translator.chain、RuleContext.chain、PropertyRow.chain、TextTranslation.rows;跨行写或实参里有闭包的链排成卡片,连着同名调用合成一步列出每次实参,注释挂在步骤上,闭包收起、子块挂到那一步(BlockView.step);Rust 插件读链、标准库常见方法写中文名;挤成一行时另起一行的 . 贴着前面 --- vscode/src/codeReader/blockView.ts | 71 ++++- vscode/src/codeReader/contract.ts | 128 +++++++- vscode/src/codeReader/diff/viewDiff.ts | 14 +- vscode/src/codeReader/editor/messages.ts | 3 +- .../src/codeReader/editor/pageBlocks.test.ts | 21 ++ vscode/src/codeReader/editor/pageBlocks.ts | 16 +- vscode/src/codeReader/fixtures/rustChain.rs | 46 +++ vscode/src/codeReader/languages/rust.test.ts | 249 ++++++++++++++- vscode/src/codeReader/languages/rust.ts | 153 ++++++++- vscode/src/codeReader/nameResolution.test.ts | 11 + vscode/src/codeReader/nameResolution.ts | 23 +- vscode/src/codeReader/rules/fastapi.test.ts | 2 +- vscode/src/codeReader/testSupport.ts | 33 +- vscode/src/codeReader/translators/rustZh.ts | 48 ++- .../codeReader/translators/zhTemplate.test.ts | 4 +- .../src/codeReader/translators/zhTemplate.ts | 291 ++++++++++++++++-- 16 files changed, 1041 insertions(+), 72 deletions(-) create mode 100644 vscode/src/codeReader/fixtures/rustChain.rs diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index 613b28d..b3fe6c7 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -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 + +/** + * 子块挂在卡片链式调用的哪一步(见 ChainSlot):rows 是卡片排好的属性表(连「注释」一行),某一行带着链时, + * 落在那条链某一步源码范围里的子块(实参里闭包、async 块主体的各句)挂在那一步下面。 + */ +function chainPlaces(children: readonly CodeBlock[], rows: readonly PropertyRow[] | undefined): Map { + const places = new Map() + 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 & { rows?: PropertyRow[] } { + /** 一行译文的卡片:译文、实参表、说明、翻译插件排在下面的几行,有注释时「注释」一行插进这几行(见 withCommentRow),没有这几行时自成一行。 */ + function textCard(block: TextBlock): ReturnType { 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): BlockView[] { + /** + * 一串兄弟块转成视图数据;略去的块不占位置,id 按出了块的往下数。places 是挂在父卡片里的那几块挂在哪: + * 属性表的子行上(见 BlockSlot),或链式调用的某一步下面(见 ChainSlot)。 + */ + function list(siblings: readonly CodeBlock[], prefix: string, scope: string, from = 0, places?: ReadonlyMap): 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 } diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index 76ab6c7..c1b9db8 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -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 + /** 方法名后面写的泛型实参(`::>`);没写没有 */ + 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 认出的某一项的名字上才算;同一份源码里同名的名字按一处查。 diff --git a/vscode/src/codeReader/diff/viewDiff.ts b/vscode/src/codeReader/diff/viewDiff.ts index 3653bae..a17fb52 100644 --- a/vscode/src/codeReader/diff/viewDiff.ts +++ b/vscode/src/codeReader/diff/viewDiff.ts @@ -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, } } diff --git a/vscode/src/codeReader/editor/messages.ts b/vscode/src/codeReader/editor/messages.ts index bf4f5d0..7680e1b 100644 --- a/vscode/src/codeReader/editor/messages.ts +++ b/vscode/src/codeReader/editor/messages.ts @@ -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 } diff --git a/vscode/src/codeReader/editor/pageBlocks.test.ts b/vscode/src/codeReader/editor/pageBlocks.test.ts index 38d7da9..e2c5447 100644 --- a/vscode/src/codeReader/editor/pageBlocks.test.ts +++ b/vscode/src/codeReader/editor/pageBlocks.test.ts @@ -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 }) + }) +}) diff --git a/vscode/src/codeReader/editor/pageBlocks.ts b/vscode/src/codeReader/editor/pageBlocks.ts index 8653503..e25f132 100644 --- a/vscode/src/codeReader/editor/pageBlocks.ts +++ b/vscode/src/codeReader/editor/pageBlocks.ts @@ -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) diff --git a/vscode/src/codeReader/fixtures/rustChain.rs b/vscode/src/codeReader/fixtures/rustChain.rs new file mode 100644 index 0000000..6178a43 --- /dev/null +++ b/vscode/src/codeReader/fixtures/rustChain.rs @@ -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::>().shutdown(), + _ => {} + }); +} + +#[tauri::command] +fn device_resume() {} + +#[tauri::command] +fn settings_get() {} diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index 187e982..3cbe2b1 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -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 => 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::(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', '', ['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::>().shutdown()', + ' if 38: _', + ]) + }) + + it('写在一行里、实参里有闭包的链也排;变量的初始值是起点,链接在下面;标准库常见方法写中文名;.await、? 照常翻、跟在那一步后面;返回值同样排;单行又没闭包的照旧一行', async () => { + const source = [ + 'async fn load(client: Client, rows: Vec) -> Result, Error> {', + ' let names: Vec = 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', + ' 初始值: 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') }) + }) +}) diff --git a/vscode/src/codeReader/languages/rust.ts b/vscode/src/codeReader/languages/rust.ts index 730b4c3..33a80d0 100644 --- a/vscode/src/codeReader/languages/rust.ts +++ b/vscode/src/codeReader/languages/rust.ts @@ -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::(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 }] }, } } diff --git a/vscode/src/codeReader/nameResolution.test.ts b/vscode/src/codeReader/nameResolution.test.ts index 71b40ff..5c7bc61 100644 --- a/vscode/src/codeReader/nameResolution.test.ts +++ b/vscode/src/codeReader/nameResolution.test.ts @@ -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) diff --git a/vscode/src/codeReader/nameResolution.ts b/vscode/src/codeReader/nameResolution.ts index e832566..ca75b9a 100644 --- a/vscode/src/codeReader/nameResolution.ts +++ b/vscode/src/codeReader/nameResolution.ts @@ -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 { const found = new Map() const inline = (pieces: readonly Inline[]): void => { @@ -37,18 +37,29 @@ export function renderedNames(blocks: readonly BlockView[]): Map 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) } diff --git a/vscode/src/codeReader/rules/fastapi.test.ts b/vscode/src/codeReader/rules/fastapi.test.ts index 7ac1a6f..555110e 100644 --- a/vscode/src/codeReader/rules/fastapi.test.ts +++ b/vscode/src/codeReader/rules/fastapi.test.ts @@ -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 diff --git a/vscode/src/codeReader/testSupport.ts b/vscode/src/codeReader/testSupport.ts index ec5e359..cb16bd3 100644 --- a/vscode/src/codeReader/testSupport.ts +++ b/vscode/src/codeReader/testSupport.ts @@ -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) diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts index 2f76f23..70e03e1 100644 --- a/vscode/src/codeReader/translators/rustZh.ts +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -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[] { diff --git a/vscode/src/codeReader/translators/zhTemplate.test.ts b/vscode/src/codeReader/translators/zhTemplate.test.ts index b614c7b..0b9c368 100644 --- a/vscode/src/codeReader/translators/zhTemplate.test.ts +++ b/vscode/src/codeReader/translators/zhTemplate.test.ts @@ -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() })) }) diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index e4dc7f9..6c70231 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -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 /** 写出各项的字面量(语言插件给了各项,见 LanguageAdapter.collection)的叫法,按节点类型查:属性表里拆开的值写成「字典 3 项」「列表 2 项」。 */ collections: Record + /** 链式调用卡片里常见方法的中文名,按方法名查(`iter` → 遍历):方法那一列写它,悬停看原名;没有的写 `.方法名`。 */ + methods?: Record /** 整段原样照抄、按字符串着色的节点。 */ 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 { +function declaredValue(value: Node, say: Phrases): Pick { + 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 }