✨ feat(vscode): tauri、axum 的链式调用卡片换成库自己的叫法,Router 列出全部路由

规则按起点认链、用 RuleContext.chain 带上读法:tauri::Builder 连着的 .plugin 合成「插件 N 个」表(插件 | 配置),setup / on_window_event / manage 叫启动时 / 窗口事件 / 托管状态,invoke_handler 列出命令名,build → expect → run 合成「构建并运行」;axum Router 连着的 .route 合成路由表(路径 | 方法 | 处理函数,每条都列),with_state、layer、nest 等起叫法;valueCard 交出语句的式子
This commit is contained in:
cruldra committed 2026-10-15 21:12:04 +11:00
1 parent 6ad2a5b27a
commit aaa695eae4
4 files changed
+335 -37

No files matched your search

+74 -5
View File
@@ -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 { returnNote, throwsRows, writtenParamRows } from './docRows'
import { returnValue, valueCard } from './rustRows' import { chainRows, methodRuns, returnValue, valueCard } from './rustRows'
import { attributeName, docSummary, named, stringContent } from './rustTree' import { attributeName, docSummary, named, stringContent } from './rustTree'
/** 和 FastAPI 接口同一个蓝。 */ /** 和 FastAPI 接口同一个蓝。 */
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' } const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
const BADGE: KindBadge = { label: '接口', color: COLOR, icon: 'globe' } 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']) const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'])
/** Router 链里这几个方法的叫法;连着调用几次的合成一步。连着的 `.route` 另合成路由表(见 [routerGroups])。 */
const STEP_NAMES: Record<string, string> = {
with_state: '共享状态',
layer: '中间件',
route_layer: '路由中间件',
nest: '嵌套路由',
nest_service: '嵌套服务',
merge: '合并路由',
fallback: '兜底处理',
}
/** /**
* 处理函数上的 get / post 等路由属性,或语句、变量的初始值、返回值里 Router 上能看出来的 route。 * 处理函数上的 get / post 等路由属性;语句、变量的初始值、返回值是 Router 的链时排成「路由器」的链式调用卡片,每条路由都列出来
* 请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。 * (见 [routerChainCard]);只写了一步 `.route` 的照旧是那一个接口的卡片。请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。
* 文件里导入了 axum 才启用。 * 文件里导入了 axum 才启用。
*/ */
export const axumRule: LibraryRule = { export const axumRule: LibraryRule = {
@@ -24,11 +37,67 @@ export const axumRule: LibraryRule = {
return route ? card(block, route, context) : undefined return route ? card(block, route, context) : undefined
} }
return valueCard(block, context, (value, own) => { 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)) const [route] = routesOn(value, own).filter(item => !handled(value, item.handler))
return route ? routerCard(value, route, context) : undefined 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 { interface Route {
+101 -8
View File
@@ -189,7 +189,7 @@ describe('函数体里的 sqlx 查询、axum 路由', () => {
'', '',
].join('\n') ].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') const list = (await views(source)).filter(block => block.kind !== 'import')
expect(viewLines(list)).toEqual([ expect(viewLines(list)).toEqual([
'function 5', 'function 5',
@@ -242,12 +242,14 @@ describe('函数体里的 sqlx 查询、axum 路由', () => {
' 方法 | GET', ' 方法 | GET',
' 路径 | /health', ' 路径 | /health',
' 返回: —', ' 返回: —',
' axum.route 21', ' axum.router 21',
' 名称: show', ' 起点: Router::new()',
' 请求: GET /agents/{id}', ' 步骤: 2 个',
' 方法 | GET', ' ● 路由 2 个',
' 路径 | /agents/{id}', ' 路径 | 方法 | 处理函数',
' 返回: —', ' /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(row?.defines).toEqual({ startIndex: at, endIndex: at + 'row'.length })
expect(propertyRow(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(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 () => { 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::<Arc<Host>>().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}')]])
})
})
+29 -4
View File
@@ -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 type { RustVisibility } from './rustTree'
import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows' 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' 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 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、实参里的闭包这些子块不并进卡片,照常是它的子块。拆成子块的那几段讲在子块自己的卡片上, * `let … else` 的 else、实参里的闭包这些子块不并进卡片,照常是它的子块。拆成子块的那几段讲在子块自己的卡片上,
@@ -308,7 +315,7 @@ export function valueCard(block: CodeBlock, context: RuleContext, card: (value:
// 子块从头部开头(else 子块从 else 这个词)到它的节点结尾;else 的节点是整条 let,不能拿节点的开头算 // 子块从头部开头(else 子块从 else 这个词)到它的节点结尾;else 的节点是整条 let,不能拿节点的开头算
const own = (node: Node): boolean => !block.children.some(child => node.startIndex >= child.header.startIndex && node.endIndex <= child.node.endIndex) 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') if (block.kind === 'statement' || block.kind === 'return')
return card(block.node, own) return card(statementValue(block.node), own)
if (block.kind !== 'variable') if (block.kind !== 'variable')
return undefined return undefined
const [declarator, ...rest] = block.facts.declarators const [declarator, ...rest] = block.facts.declarators
@@ -320,3 +327,21 @@ export function valueCard(block: CodeBlock, context: RuleContext, card: (value:
const defines = range(declarator.name) const defines = range(declarator.name)
return { ...found, rows: [{ label: '变量', value: context.translate(declarator.name), defines }, ...found.rows], defines } 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
}
+131 -20
View File
@@ -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 { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
import { returnValue } from './rustRows' import { chainRows, methodRuns, returnValue, valueCard } from './rustRows'
import { attributeName, docSummary } from './rustTree' import { attributeName, docSummary, named } from './rustTree'
import { hint, joined, plain, range } from './segments'
/** 桌面命令的边框用这个橙。 */ /** 桌面命令、桌面应用的边框用这个橙。 */
const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' } const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' }
const BADGE: KindBadge = { label: '桌面命令', color: COLOR, icon: 'square-function' } 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<string, string> = { setup: '启动时', on_window_event: '窗口事件', manage: '托管状态' }
/** /**
* #[tauri::command] 的函数。名称后标「桌面命令」。 * #[tauri::command] 的函数:名称后标「桌面命令」,参数多一列来源(State<…> 是窗口状态,其余是调用方传入)。
* 参数多一列来源:State<…> 是窗口状态,其余是调用方传入。 * 语句、变量的初始值、返回值是 `tauri::Builder` 起头的链时排成「桌面应用」的链式调用卡片(见 [builderCard])。
* 文件里导入了 tauri 才启用。 * 文件里导入了 tauri 才启用。
*/ */
export const tauriRule: LibraryRule = { export const tauriRule: LibraryRule = {
@@ -17,8 +22,126 @@ export const tauriRule: LibraryRule = {
label: 'tauri 桌面命令', label: 'tauri 桌面命令',
languageIds: ['rust'], languageIds: ['rust'],
libraries: ['tauri'], libraries: ['tauri'],
claim: (block, context) => block.kind === 'function' && commanded(block) ? card(block, context) : undefined, claim: (block, context) => {
attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令' } } }), 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 { function commanded(block: FunctionBlock): boolean {
@@ -86,18 +209,6 @@ function tag(label: string): SegmentGroup {
return { style: 'tag', label, segments: [], color: COLOR } return { style: 'tag', label, segments: [], color: COLOR }
} }
function plain(text: string): Inline {
return { text, role: 'plain' }
}
function keyword(text: string): Inline { function keyword(text: string): Inline {
return { text, role: 'keyword' } 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 }
}