From aaa695eae4168160715ee91a90596ec80c02e42b Mon Sep 17 00:00:00 2001 From: cruldra Date: Thu, 15 Oct 2026 21:12:04 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20tauri=E3=80=81axum?= =?UTF-8?q?=20=E7=9A=84=E9=93=BE=E5=BC=8F=E8=B0=83=E7=94=A8=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E6=8D=A2=E6=88=90=E5=BA=93=E8=87=AA=E5=B7=B1=E7=9A=84?= =?UTF-8?q?=E5=8F=AB=E6=B3=95=EF=BC=8CRouter=20=E5=88=97=E5=87=BA=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 规则按起点认链、用 RuleContext.chain 带上读法:tauri::Builder 连着的 .plugin 合成「插件 N 个」表(插件 | 配置),setup / on_window_event / manage 叫启动时 / 窗口事件 / 托管状态,invoke_handler 列出命令名,build → expect → run 合成「构建并运行」;axum Router 连着的 .route 合成路由表(路径 | 方法 | 处理函数,每条都列),with_state、layer、nest 等起叫法;valueCard 交出语句的式子 --- vscode/src/codeReader/rules/axum.ts | 79 +++++++++- vscode/src/codeReader/rules/rustLibs.test.ts | 109 ++++++++++++- vscode/src/codeReader/rules/rustRows.ts | 33 +++- vscode/src/codeReader/rules/tauri.ts | 151 ++++++++++++++++--- 4 files changed, 335 insertions(+), 37 deletions(-) diff --git a/vscode/src/codeReader/rules/axum.ts b/vscode/src/codeReader/rules/axum.ts index a79d708..bb8c70b 100644 --- a/vscode/src/codeReader/rules/axum.ts +++ b/vscode/src/codeReader/rules/axum.ts @@ -1,16 +1,29 @@ -import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract' +import type { ChainGroup, ChainStepFacts, FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract' import { returnNote, throwsRows, writtenParamRows } from './docRows' -import { returnValue, valueCard } from './rustRows' +import { chainRows, methodRuns, returnValue, valueCard } from './rustRows' import { attributeName, docSummary, named, stringContent } from './rustTree' /** 和 FastAPI 接口同一个蓝。 */ const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' } const BADGE: KindBadge = { label: '接口', color: COLOR, icon: 'globe' } +/** 和 FastAPI 路由器同一个蓝、同一个图标。 */ +const ROUTER_BADGE: KindBadge = { label: '路由器', color: { dark: '#7aa2f7', light: '#34548a' }, icon: 'network' } const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']) +/** Router 链里这几个方法的叫法;连着调用几次的合成一步。连着的 `.route` 另合成路由表(见 [routerGroups])。 */ +const STEP_NAMES: Record = { + with_state: '共享状态', + layer: '中间件', + route_layer: '路由中间件', + nest: '嵌套路由', + nest_service: '嵌套服务', + merge: '合并路由', + fallback: '兜底处理', +} + /** - * 处理函数上的 get / post 等路由属性,或语句、变量的初始值、返回值里 Router 上能看出来的 route。 - * 请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。 + * 处理函数上的 get / post 等路由属性;语句、变量的初始值、返回值是 Router 的链时排成「路由器」的链式调用卡片,每条路由都列出来 + * (见 [routerChainCard]);只写了一步 `.route` 的照旧是那一个接口的卡片。请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。 * 文件里导入了 axum 才启用。 */ export const axumRule: LibraryRule = { @@ -24,11 +37,67 @@ export const axumRule: LibraryRule = { return route ? card(block, route, context) : undefined } return valueCard(block, context, (value, own) => { + const chained = routerChainCard(value, context) + if (chained) + return chained const [route] = routesOn(value, own).filter(item => !handled(value, item.handler)) return route ? routerCard(value, route, context) : undefined }) }, - attributeNames: () => ({ attributes: {}, values: { kind: { 'axum.route': '接口' } } }), + attributeNames: () => ({ attributes: {}, values: { kind: { 'axum.route': '接口', 'axum.router': '路由器' } } }), +} + +/** + * Router 的链(起点是 `Router::new()` / `Router::default()`,或链里有 `.route`)排成链式调用卡片:连着的 `.route` 合成路由表, + * 别的几步按 [STEP_NAMES] 起叫法;不是这种链时没有。 + */ +function routerChainCard(value: Node, context: RuleContext): RuleCard | undefined { + const facts = context.document.language.chain?.(value) + const start = facts?.start.text.replace(/\s+/g, '') + if (!facts || !(/^(?:axum::)?Router::(?:new|default)\(\)$/.test(start ?? '') || facts.steps.some(step => step.method.text === 'route'))) + return undefined + const view = context.chain(value, routerGroups(facts.steps, context)) + return view && { kind: 'axum.router', badge: ROUTER_BADGE, rows: chainRows(view), absorbs: [] } +} + +/** 连着的 `.route` 合成「路由 N 个」表(路径 | 方法 | 处理函数,一个路径挂几个方法的各占一行),别的按 [STEP_NAMES] 起叫法。 */ +function routerGroups(steps: readonly ChainStepFacts[], context: RuleContext): ChainGroup[] { + return methodRuns(steps).flatMap(({ from, to, method }): ChainGroup[] => { + if (method === 'route') { + const items = steps.slice(from, to + 1).flatMap(step => routeItems(step, context)) + return [{ from, to, label: `路由 ${items.length} 个`, table: { columns: ['路径', '方法', '处理函数'], items } }] + } + const label = STEP_NAMES[method] + return label ? [{ from, to, label }] : [] + }) +} + +/** 一步 `.route(路径, get(a).post(b))` 的各行:路径去掉引号、带源码下标,方法大写,处理函数是名字时按函数着色、带源码下标。 */ +function routeItems(step: ChainStepFacts, context: RuleContext): PropertyItem[] { + const [path, router] = step.arguments + const content = path?.node.children.find(child => child?.type === 'string_content') + const pathCell: Inline[] = content ? [{ text: content.text, role: 'string', offset: content.startIndex }] : path ? context.verbatim(path.node) : [hint('—')] + return methodRoutes(router?.node).map(({ method, handler }) => ({ + cells: [ + pathCell, + [keyword(method.toUpperCase())], + handler?.type === 'identifier' ? [{ text: handler.text, role: 'function', offset: handler.startIndex }] : handler ? context.verbatim(handler) : [hint('—')], + ], + })) +} + +/** 方法路由 `get(a)`、`get(a).post(b)` 里的各个方法和它的处理函数,照源码顺序;认不出时是空的。 */ +function methodRoutes(node: Node | undefined): { method: string, handler?: Node }[] { + if (node?.type !== 'call_expression') + return [] + const fn = node.childForFieldName('function') + const handler = named(node.childForFieldName('arguments')).find(arg => !arg.isExtra) + if (fn?.type === 'field_expression') { + const method = fn.childForFieldName('field')?.text + return method && METHODS.has(method) ? [...methodRoutes(fn.childForFieldName('value') ?? undefined), { method, handler }] : [] + } + const method = fn?.text.split('::').pop() + return method && METHODS.has(method) ? [{ method, handler }] : [] } interface Route { diff --git a/vscode/src/codeReader/rules/rustLibs.test.ts b/vscode/src/codeReader/rules/rustLibs.test.ts index 7cc190d..532be6c 100644 --- a/vscode/src/codeReader/rules/rustLibs.test.ts +++ b/vscode/src/codeReader/rules/rustLibs.test.ts @@ -189,7 +189,7 @@ describe('函数体里的 sqlx 查询、axum 路由', () => { '', ].join('\n') - it('let 的初始值、末尾不带分号的返回值是查询或路由时出库卡片,变量名是第一行「变量」,let … else 的 else 照常是子块', async () => { + it('let 的初始值、末尾不带分号的返回值是查询或路由时出库卡片,变量名是第一行「变量」,let … else 的 else 照常是子块;Router 的链列出全部路由', async () => { const list = (await views(source)).filter(block => block.kind !== 'import') expect(viewLines(list)).toEqual([ 'function 5', @@ -242,12 +242,14 @@ describe('函数体里的 sqlx 查询、axum 路由', () => { ' 方法 | GET', ' 路径 | /health', ' 返回: —', - ' axum.route 21', - ' 名称: show', - ' 请求: GET /agents/{id}', - ' 方法 | GET', - ' 路径 | /agents/{id}', - ' 返回: —', + ' axum.router 21', + ' 起点: Router::new()', + ' 步骤: 2 个', + ' ● 路由 2 个', + ' 路径 | 方法 | 处理函数', + ' /agents | GET | list', + ' /agents/{id} | GET | show', + ' ● 共享状态 | state', ]) }) @@ -260,7 +262,7 @@ describe('函数体里的 sqlx 查询、axum 路由', () => { expect(row?.defines).toEqual({ startIndex: at, endIndex: at + 'row'.length }) expect(propertyRow(row, '变量').defines).toEqual({ startIndex: at, endIndex: at + 'row'.length }) expect(names?.children[0]?.concepts.map(concept => concept.id)).toEqual(['return', 'sqlx.query']) - expect(app?.children[1]?.concepts.map(concept => concept.id)).toEqual(['return', 'axum.route']) + expect(app?.children[1]?.concepts.map(concept => concept.id)).toEqual(['return', 'axum.router']) }) it('函数写法的 SQL 是拼出来的变量时照翻成一行,每个 .bind 依次是 $1、$2', async () => { @@ -304,3 +306,94 @@ describe('实参里的闭包、async 块拆成子块时', () => { ]) }) }) + +describe('库规则给链式调用起叫法', () => { + it('tauri::Builder 的链:连着的 .plugin 合成「插件 N 个」表,setup / on_window_event / manage 各有叫法,命令列出名字,build → expect → run 合成「构建并运行」;闭包的子块照常挂在各自那一步下面', async () => { + const list = await views(readFixture('rustChain.rs')) + const [run] = list.filter(block => block.kind === 'function') + const app = run?.children.find(block => block.kind === 'tauri.app') + expect(viewLines(app ? [app] : [])).toEqual([ + 'tauri.app 7', + ' 起点: tauri::Builder::default()', + ' 步骤: 6 个', + ' ● 插件 4 个', + ' 注释: 新窗口链接只走主窗口 on_new_window 一条路', + ' 插件 | 配置', + ' tauri_plugin_opener | open_js_links_on_click(false)', + ' tauri_plugin_dialog | —', + ' tauri_plugin_autostart | tauri_plugin_autostart::MacosLauncher::LaunchAgent, None', + ' tauri_plugin_updater | —', + ' ● 启动时 | ⟦闭包||app| · 3 句⟧', + ' ● 窗口事件 | ⟦闭包||window, event| · 1 句⟧', + ' ● 托管状态 | app_state', + ' ● 命令 3 个 | 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 @5: 匹配 event', + ' if 37: RunEvent::Exit', + ' return 37: 返回 app.state::>().shutdown()', + ' if 38: _', + ]) + expect(app?.concepts.map(concept => concept.id)).toEqual(['statement', 'tauri.app']) + }) + + it('tauri 链里的插件名、命令名带自己的下标:⌘ 悬停命令名跳到命令函数', async () => { + const source = readFixture('rustChain.rs') + const [run] = (await views(source)).filter(block => block.kind === 'function') + const steps = propertyRow(run?.children.find(block => block.kind === 'tauri.app'), '步骤').chain ?? [] + const commands = steps.find(step => previewText(step.name).startsWith('命令')) + expect(wordsOf(commands?.value, 'function')).toEqual([ + ['device_resume', source.indexOf('device_resume,')], + ['settings_get', source.indexOf('settings_get,')], + ['preview_open', source.indexOf('preview_open')], + ]) + expect(typeNames(steps[0]?.table?.items[2]?.cells[0], source)).toEqual(['tauri_plugin_autostart']) + }) + + it('axum 的 Router 链:每条路由都列出来,一个路径挂几个方法的各占一行;nest、layer、with_state 各有叫法', async () => { + const source = [ + 'use axum::Router;', + 'use axum::routing::{get, post};', + '', + 'fn app(state: AppState) -> Router {', + ' let api = Router::new()', + ' .route("/agents", get(list).post(create))', + ' .route("/agents/{id}", get(show));', + ' Router::new()', + ' .nest("/api", api)', + ' .layer(TraceLayer::new_for_http())', + ' .with_state(state)', + '}', + '', + ].join('\n') + const [app] = (await views(source)).filter(block => block.kind === 'function') + expect(viewLines(app?.children ?? [])).toEqual([ + 'axum.router 5', + ' 变量: api', + ' 起点: Router::new()', + ' 步骤: 1 个', + ' ● 路由 3 个', + ' 路径 | 方法 | 处理函数', + ' /agents | GET | list', + ' /agents | POST | create', + ' /agents/{id} | GET | show', + 'axum.router 8', + ' 起点: Router::new()', + ' 步骤: 3 个', + ' ● 嵌套路由 | "/api", api', + ' ● 中间件 | TraceLayer::new_for_http()', + ' ● 共享状态 | state', + ]) + const routes = propertyRow(app?.children[0], '步骤').chain?.[0]?.table?.items ?? [] + expect(wordsOf(routes[1]?.cells[2], 'function')).toEqual([['create', source.indexOf('create')]]) + expect(wordsOf(routes[2]?.cells[0], 'string')).toEqual([['/agents/{id}', source.indexOf('/agents/{id}')]]) + }) +}) diff --git a/vscode/src/codeReader/rules/rustRows.ts b/vscode/src/codeReader/rules/rustRows.ts index f318e57..516b88d 100644 --- a/vscode/src/codeReader/rules/rustRows.ts +++ b/vscode/src/codeReader/rules/rustRows.ts @@ -1,7 +1,7 @@ -import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, SourceSpan, TokenRole, TraitBlock } from '../contract' +import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ChainStepFacts, ChainView, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, SourceSpan, TokenRole, TraitBlock } from '../contract' import type { RustVisibility } from './rustTree' import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows' -import { docSummary, isFnItem, isMethod, signatureParams, structFields, variantPayload } from './rustTree' +import { docSummary, isFnItem, isMethod, named, signatureParams, structFields, variantPayload } from './rustTree' import { hint, joined, plain, range, name as sourceName } from './segments' /** @@ -292,8 +292,15 @@ export function payloadValue(body: Node | undefined, say: RustSay): Inline[] { return parts.length > 0 ? joined(parts.map(part => say.verbatim(part)), plain(', ')) : [hint('—')] } +/** 一句的式子:表达式语句去掉分号,`return x` 取 x;别的(末尾不带分号的式子)就是它自己。 */ +function statementValue(node: Node): Node { + const first = (parent: Node): Node | undefined => named(parent).find(child => !child.isExtra) + const value = node.type === 'expression_statement' ? first(node) ?? node : node + return value.type === 'return_expression' ? first(value) ?? value : value +} + /** - * Rust 库规则认函数体里的一块时看它算出的值:语句看整句,变量看初始值,返回块看返回的值,交给 `card` 认成卡片。 + * Rust 库规则认函数体里的一块时看它算出的值:语句看它的式子,变量看初始值,返回块看返回的值,交给 `card` 认成卡片。 * * 认领变量时变量名排在卡片第一行「变量」,卡片和这一行的定义范围都是它,同没认领时的变量块; * `let … else` 的 else、实参里的闭包这些子块不并进卡片,照常是它的子块。拆成子块的那几段讲在子块自己的卡片上, @@ -308,7 +315,7 @@ export function valueCard(block: CodeBlock, context: RuleContext, card: (value: // 子块从头部开头(else 子块从 else 这个词)到它的节点结尾;else 的节点是整条 let,不能拿节点的开头算 const own = (node: Node): boolean => !block.children.some(child => node.startIndex >= child.header.startIndex && node.endIndex <= child.node.endIndex) if (block.kind === 'statement' || block.kind === 'return') - return card(block.node, own) + return card(statementValue(block.node), own) if (block.kind !== 'variable') return undefined const [declarator, ...rest] = block.facts.declarators @@ -320,3 +327,21 @@ export function valueCard(block: CodeBlock, context: RuleContext, card: (value: const defines = range(declarator.name) return { ...found, rows: [{ label: '变量', value: context.translate(declarator.name), defines }, ...found.rows], defines } } + +/** 链式调用卡片的「起点」「步骤」两行,同翻译插件给整句是链的块排的:链的各步在「步骤」那一行上(见 PropertyRow.chain)。 */ +export function chainRows(view: ChainView): PropertyRow[] { + return [{ label: '起点', value: view.start }, { label: '步骤', value: [hint(`${view.steps.length} 个`)], chain: view.steps }] +} + +/** 链里连着调用同一个方法的几步各成一段(第 from 到第 to 步,含两头),照源码顺序:规则按方法名给这几段起叫法、合成一步。 */ +export function methodRuns(steps: readonly ChainStepFacts[]): { from: number, to: number, method: string }[] { + const runs: { from: number, to: number, method: string }[] = [] + steps.forEach((step, i) => { + const last = runs[runs.length - 1] + if (last && last.method === step.method.text) + last.to = i + else + runs.push({ from: i, to: i, method: step.method.text }) + }) + return runs +} diff --git a/vscode/src/codeReader/rules/tauri.ts b/vscode/src/codeReader/rules/tauri.ts index 47349e2..1885c17 100644 --- a/vscode/src/codeReader/rules/tauri.ts +++ b/vscode/src/codeReader/rules/tauri.ts @@ -1,15 +1,20 @@ -import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract' +import type { ChainGroup, ChainStepFacts, FunctionBlock, ImportFacts, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract' import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows' -import { returnValue } from './rustRows' -import { attributeName, docSummary } from './rustTree' +import { chainRows, methodRuns, returnValue, valueCard } from './rustRows' +import { attributeName, docSummary, named } from './rustTree' +import { hint, joined, plain, range } from './segments' -/** 桌面命令的边框用这个橙。 */ +/** 桌面命令、桌面应用的边框用这个橙。 */ const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' } const BADGE: KindBadge = { label: '桌面命令', color: COLOR, icon: 'square-function' } +const APP_BADGE: KindBadge = { label: '桌面应用', color: COLOR, icon: 'package' } + +/** tauri::Builder 链里这几个方法的叫法;连着调用几次的合成一步。插件、命令、构建并运行另有读法(见 [builderGroups])。 */ +const STEP_NAMES: Record = { setup: '启动时', on_window_event: '窗口事件', manage: '托管状态' } /** - * #[tauri::command] 的函数。名称后标「桌面命令」。 - * 参数多一列来源:State<…> 是窗口状态,其余是调用方传入。 + * #[tauri::command] 的函数:名称后标「桌面命令」,参数多一列来源(State<…> 是窗口状态,其余是调用方传入)。 + * 语句、变量的初始值、返回值是 `tauri::Builder` 起头的链时排成「桌面应用」的链式调用卡片(见 [builderCard])。 * 文件里导入了 tauri 才启用。 */ export const tauriRule: LibraryRule = { @@ -17,8 +22,126 @@ export const tauriRule: LibraryRule = { label: 'tauri 桌面命令', languageIds: ['rust'], libraries: ['tauri'], - claim: (block, context) => block.kind === 'function' && commanded(block) ? card(block, context) : undefined, - attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令' } } }), + claim: (block, context) => { + if (block.kind === 'function') + return commanded(block) ? card(block, context) : undefined + return valueCard(block, context, value => builderCard(value, context)) + }, + attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令', 'tauri.app': '桌面应用' } } }), +} + +/** + * `tauri::Builder::default()` 起头的链(导入了 tauri 的 Builder 时写 `Builder::default()`、`Builder::new()` 也算)排成链式调用卡片, + * 几步换成桌面应用的叫法(见 [builderGroups]),不是这种链时没有。 + */ +function builderCard(value: Node, context: RuleContext): RuleCard | undefined { + const facts = context.document.language.chain?.(value) + if (!facts || !isBuilder(facts.start, context.imports)) + return undefined + const view = context.chain(value, builderGroups(facts.steps, context)) + return view && { kind: 'tauri.app', badge: APP_BADGE, rows: chainRows(view), absorbs: [] } +} + +/** 起点是 `tauri::Builder::default()` / `new()`,或导入了 tauri 的 Builder(没起别名)时的 `Builder::default()` / `new()`。 */ +function isBuilder(start: Node, imports: readonly ImportFacts[]): boolean { + const callee = start.type === 'call_expression' ? start.childForFieldName('function')?.text.replace(/\s+/g, '') : undefined + const imported = imports.some(item => item.sources.some(source => source.text === 'tauri') && item.items.some(entry => entry.kind === 'named' && entry.name.text === 'Builder' && !entry.alias)) + return callee === 'tauri::Builder::default' || callee === 'tauri::Builder::new' || (imported && (callee === 'Builder::default' || callee === 'Builder::new')) +} + +/** + * 桌面应用的读法:连着的 `.plugin` 合成「插件 N 个」表(插件 | 配置),`.invoke_handler(tauri::generate_handler![…])` 叫「命令 N 个」、 + * 值是各个命令名,`build`(中间可以隔着 `expect` / `unwrap`)到 `run` 合成「构建并运行」,setup 这几个按 [STEP_NAMES] 起叫法。 + */ +function builderGroups(steps: readonly ChainStepFacts[], context: RuleContext): ChainGroup[] { + const groups: ChainGroup[] = [] + for (const { from, to, method } of methodRuns(steps)) { + if (groups.some(group => from <= group.to)) + continue + const run = steps.slice(from, to + 1) + const ending = runEnd(steps, from) + if (method === 'plugin') + groups.push({ from, to, label: `插件 ${run.length} 个`, table: { columns: ['插件', '配置'], items: run.map(step => pluginItem(step, context)) } }) + else if (method === 'invoke_handler' && from === to) + groups.push(commandsGroup(steps[from], from, context)) + else if (ending !== undefined) + groups.push({ from, to: ending, label: '构建并运行' }) + else if (STEP_NAMES[method]) + groups.push({ from, to, label: STEP_NAMES[method] }) + } + return groups +} + +/** 从第 from 步起的构建并运行到第几步:`run` 本身,或 `build` 后面(隔着 `expect` / `unwrap`)紧跟的 `run`;不是时没有。 */ +function runEnd(steps: readonly ChainStepFacts[], from: number): number | undefined { + if (steps[from]?.method.text === 'run') + return from + if (steps[from]?.method.text !== 'build') + return undefined + let at = from + 1 + while (steps[at]?.method.text === 'expect' || steps[at]?.method.text === 'unwrap') + at++ + return steps[at]?.method.text === 'run' ? at : undefined +} + +/** + * 插件表的一行:插件是 crate 名(`tauri_plugin_opener`),配置是 `X::Builder::new().a(…).build()` 中间那几步、`X::init(…)` 的实参, + * 没有写「—」;认不出写法的整个实参照抄在插件那一格。 + */ +function pluginItem(step: ChainStepFacts, context: RuleContext): PropertyItem { + const [arg] = step.arguments + if (!arg) + return { cells: [[hint('—')], [hint('—')]] } + const inner = context.document.language.chain?.(arg.node) + if (inner && inner.steps[inner.steps.length - 1]?.method.text === 'build') { + const configured = inner.steps.slice(0, -1).map(item => [...context.verbatim(item.method, 'function'), ...context.verbatim(item.argumentList)]) + return { cells: [crateCell(inner.start, context), configured.length > 0 ? joined(configured, plain(', ')) : [hint('—')]] } + } + const built = builtFrom(arg.node) + if (built) + return { cells: [crateCell(built, context), [hint('—')]] } + const callee = arg.node.type === 'call_expression' ? arg.node.childForFieldName('function') : null + if (callee?.type === 'scoped_identifier' && callee.childForFieldName('name')?.text === 'init') { + const args = named(arg.node.childForFieldName('arguments')).filter(item => !item.isExtra) + return { cells: [crateCell(arg.node, context), args.length > 0 ? joined(args.map(item => context.verbatim(item)), plain(', ')) : [hint('—')]] } + } + return { cells: [context.verbatim(arg.node), [hint('—')]] } +} + +/** `X::Builder::new().build()` 的 `X::Builder::new()`;别的写法没有。 */ +function builtFrom(node: Node): Node | undefined { + const fn = node.type === 'call_expression' ? node.childForFieldName('function') : null + return fn?.type === 'field_expression' && fn.childForFieldName('field')?.text === 'build' ? fn.childForFieldName('value') ?? undefined : undefined +} + +/** 一次调用的被调用路径最前面那一节(crate 名),按类型着色、带源码下标;被调用的不是路径时照抄整个调用。 */ +function crateCell(call: Node, context: RuleContext): Inline[] { + let path = call.type === 'call_expression' ? call.childForFieldName('function') : null + while (path?.type === 'scoped_identifier') + path = path.childForFieldName('path') + return path?.type === 'identifier' ? context.verbatim(path, 'type') : context.verbatim(call) +} + +/** + * `.invoke_handler(tauri::generate_handler![…])`:叫「命令 N 个」,值是各个命令名(路径照写,最后那个名字按函数着色、带源码下标, + * ⌘ 悬停跳到命令函数)。实参不是 generate_handler! 时只起叫法「命令」。 + */ +function commandsGroup(step: ChainStepFacts, at: number, context: RuleContext): ChainGroup { + const [arg] = step.arguments + const macro = arg?.node.type === 'macro_invocation' && arg.node.childForFieldName('macro')?.text.endsWith('generate_handler') ? arg.node : undefined + const tree = macro?.children.find(child => child?.type === 'token_tree') + if (!tree) + return { from: at, to: at, label: '命令' } + const commands: Node[][] = [[]] + for (const token of tree.children.slice(1, -1)) { + if (token?.type === ',') + commands.push([]) + else if (token && !token.isExtra) + commands[commands.length - 1].push(token) + } + const names = commands.filter(tokens => tokens.length > 0).map(tokens => tokens.flatMap((token, i) => + token.type === 'identifier' ? context.verbatim(token, i === tokens.length - 1 ? 'function' : 'variable') : [plain(token.text)])) + return { from: at, to: at, label: `命令 ${names.length} 个`, value: joined(names, plain(', ')) } } function commanded(block: FunctionBlock): boolean { @@ -86,18 +209,6 @@ function tag(label: string): SegmentGroup { return { style: 'tag', label, segments: [], color: COLOR } } -function plain(text: string): Inline { - return { text, role: 'plain' } -} - function keyword(text: string): Inline { return { text, role: 'keyword' } } - -function hint(text: string): Inline { - return { text, role: 'hint' } -} - -function range(node: Node): { startIndex: number, endIndex: number } { - return { startIndex: node.startIndex, endIndex: node.endIndex } -}