✨ feat(vscode): 代码阅读认 axum、sqlx 和 tauri
This commit is contained in:
1 parent
44de0a1f70
commit
69608f9c70
9 files changed
+500
-3
No files matched your search
@@ -987,7 +987,7 @@ export interface LibraryRule {
|
||||
label: string // 'SQLAlchemy 数据表'
|
||||
languageIds: string[]
|
||||
/**
|
||||
* 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」开头(它的子模块)才启用。
|
||||
* 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」「它::」开头(它的子模块;Rust 的路径用 ::)才启用。
|
||||
* 宿主按语言插件给的导入判断,规则不用自己读导入语句。空数组是不看导入、languageIds 里这几种语言的源码都启用
|
||||
* (React 组件:新的 JSX 写法不用导入 react,TSX / JSX 文件里就是 React 组件)。
|
||||
*/
|
||||
|
||||
@@ -23,3 +23,26 @@ pub enum AgentError {
|
||||
#[error("上游错误")]
|
||||
Upstream(#[from] Other),
|
||||
}
|
||||
|
||||
use axum::routing::get;
|
||||
|
||||
/// 按 id 取一个代理。
|
||||
#[get("/agents/{id}")]
|
||||
async fn get_agent(Path(id): Path<Uuid>) -> Result<Json<AgentRow>, StatusCode> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
let app = Router::new().route("/health", get(health));
|
||||
|
||||
use sqlx::query_as;
|
||||
|
||||
// 按 id 取一行。
|
||||
let row = sqlx::query_as!(AgentRow, "SELECT id, name FROM agents WHERE id = $1", id);
|
||||
|
||||
use tauri::State;
|
||||
|
||||
/// 让窗口把这一行改名。
|
||||
#[tauri::command]
|
||||
async fn rename_agent(state: State<App>, name: String) -> Result<String, String> {
|
||||
todo!()
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { FileNotes } from './notes/fileNotes'
|
||||
import { CodeQa } from './qa/codeQa'
|
||||
import { CodeReaderRegistry } from './registry'
|
||||
import { ReviewMode } from './review/reviewMode'
|
||||
import { axumRule } from './rules/axum'
|
||||
import { celeryRule } from './rules/celery'
|
||||
import { dataclassRule } from './rules/dataclass'
|
||||
import { enumRule } from './rules/enum'
|
||||
@@ -37,6 +38,8 @@ import { reactRule } from './rules/react'
|
||||
import { rustImplRule } from './rules/rustImpl'
|
||||
import { serdeRule } from './rules/serde'
|
||||
import { sqlalchemyRule } from './rules/sqlalchemy'
|
||||
import { sqlxRule } from './rules/sqlx'
|
||||
import { tauriRule } from './rules/tauri'
|
||||
import { thiserrorRule } from './rules/thiserror'
|
||||
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
|
||||
import { bashZhTranslator } from './translators/bashZh'
|
||||
@@ -169,6 +172,9 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
api.registerLibraryRule(rustImplRule),
|
||||
api.registerLibraryRule(serdeRule),
|
||||
api.registerLibraryRule(thiserrorRule),
|
||||
api.registerLibraryRule(axumRule),
|
||||
api.registerLibraryRule(sqlxRule),
|
||||
api.registerLibraryRule(tauriRule),
|
||||
)
|
||||
return api
|
||||
}
|
||||
@@ -66,6 +66,7 @@ describe('库规则:启用', () => {
|
||||
expect(fromLibrary('sqlalchemy', 'sqlalchemy')).toBe(true)
|
||||
expect(fromLibrary('sqlalchemy.orm', 'sqlalchemy')).toBe(true)
|
||||
expect(fromLibrary('drizzle-orm/pg-core', 'drizzle-orm')).toBe(true)
|
||||
expect(fromLibrary('axum::routing', 'axum')).toBe(true)
|
||||
expect(fromLibrary('sqlalchemy_utils', 'sqlalchemy')).toBe(false)
|
||||
expect(fromLibrary('.sqlalchemy', 'sqlalchemy')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function moduleName(source: Node): string {
|
||||
|
||||
/** 模块是这个库或它的子模块:等于库名,或以「库名.」(Python 的点分模块名)「库名/」(JavaScript 的子路径)开头。 */
|
||||
export function fromLibrary(module: string, library: string): boolean {
|
||||
return module === library || module.startsWith(`${library}.`) || module.startsWith(`${library}/`)
|
||||
return module === library || module.startsWith(`${library}.`) || module.startsWith(`${library}/`) || module.startsWith(`${library}::`)
|
||||
}
|
||||
|
||||
/** 这份源码启用的规则,保持注册顺序:导入了规则关心的某个库,或规则不看导入(libraries 是空的)。 */
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
||||
import { attributeName, docSentence, oneLine, returnText, stringContent } from '../languages/rust'
|
||||
import { named } from '../languages/shared'
|
||||
|
||||
/** 和 FastAPI 接口同一个蓝。 */
|
||||
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
||||
const BADGE: KindBadge = { label: '接口', color: COLOR, icon: 'globe' }
|
||||
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'])
|
||||
|
||||
/**
|
||||
* 处理函数上的 get / post 等路由属性,或 Router 上能看出来的 route。
|
||||
* 请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。
|
||||
* 文件里导入了 axum 才启用。
|
||||
*/
|
||||
export const axumRule: LibraryRule = {
|
||||
id: 'axum',
|
||||
label: 'axum 接口',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['axum'],
|
||||
claim: (block) => {
|
||||
if (block.kind === 'function') {
|
||||
const route = attributeRoute(block)
|
||||
return route ? card(block, route) : undefined
|
||||
}
|
||||
if (block.kind === 'statement') {
|
||||
const [route] = routesOn(block.node).filter(item => !handled(block.node, item.handler))
|
||||
return route ? routerCard(block.node, route) : undefined
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'axum.route': '接口' } } }),
|
||||
}
|
||||
|
||||
interface Route {
|
||||
method: string
|
||||
path: string
|
||||
handler?: string
|
||||
}
|
||||
|
||||
function attributeRoute(block: FunctionBlock): Route | undefined {
|
||||
for (const attr of block.facts.decorators) {
|
||||
const method = attributeName(attr)?.split('::').pop()
|
||||
if (!method || !METHODS.has(method))
|
||||
continue
|
||||
const path = attributeString(attr)
|
||||
if (path)
|
||||
return { method: method.toUpperCase(), path }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function card(block: FunctionBlock, route: Route): RuleCard {
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const params = pathParams(route.path, block.node)
|
||||
const rows: PropertyRow[] = [
|
||||
nameRow(block.facts.name),
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||
requestRow(route, params),
|
||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
||||
]
|
||||
return {
|
||||
kind: 'axum.route',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一文件里的处理函数已经标了路由属性时,Router 上这句不再单独成卡。 */
|
||||
function handled(node: Node, handler: string | undefined): boolean {
|
||||
if (!handler)
|
||||
return false
|
||||
const root = node.tree.rootNode
|
||||
for (const item of named(root)) {
|
||||
if (item.type !== 'function_item' || item.childForFieldName('name')?.text !== handler)
|
||||
continue
|
||||
return itemAttributesHasRoute(item)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function routerCard(node: Node, route: Route & { handlerNode?: Node }): RuleCard {
|
||||
const fn = route.handler ? functionNamed(node, route.handler) : undefined
|
||||
const params = fn ? pathParams(route.path, fn) : []
|
||||
const doc = fn ? docSentence(leadingDoc(fn)) : undefined
|
||||
return {
|
||||
kind: 'axum.route',
|
||||
badge: BADGE,
|
||||
rows: [
|
||||
{ label: '名称', value: route.handler ? [plain(route.handler)] : [hint('路由')] },
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
requestRow(route, params),
|
||||
{ label: '返回', value: [plain(fn ? returnText(fn) : '—')] },
|
||||
],
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
function requestRow(route: Route, params: string[]): PropertyRow {
|
||||
const items: PropertyItem[] = [
|
||||
{ cells: [[plain('方法')], [keyword(route.method)]] },
|
||||
{ cells: [[plain('路径')], [{ text: route.path, role: 'string' }]] },
|
||||
...(params.length > 0 ? [{ cells: [[plain('路径参数')], params.map(text => plain(text)).flatMap((part, index) => index === 0 ? [part] : [plain(', '), part])] }] : []),
|
||||
]
|
||||
return {
|
||||
label: '请求',
|
||||
value: [keyword(route.method), plain(` ${route.path}`)],
|
||||
columns: ['项', '值'],
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
function routesOn(node: Node, found: (Route & { handlerNode?: Node })[] = []): (Route & { handlerNode?: Node })[] {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function')
|
||||
const field = callee?.type === 'field_expression' ? callee.childForFieldName('field')?.text : undefined
|
||||
if (field === 'route') {
|
||||
const args = named(node.childForFieldName('arguments'))
|
||||
const pathNode = args.find(arg => arg.type === 'string_literal')
|
||||
const call = args.find(arg => arg.type === 'call_expression')
|
||||
const method = call?.childForFieldName('function')?.text.split('::').pop()
|
||||
const handlerNode = named(call?.childForFieldName('arguments'))[0]
|
||||
const path = stringContent(pathNode)
|
||||
if (path && method && METHODS.has(method)) {
|
||||
found.push({
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
...(handlerNode?.type === 'identifier' && { handler: handlerNode.text, handlerNode }),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of named(node))
|
||||
routesOn(child, found)
|
||||
return found
|
||||
}
|
||||
|
||||
/** 路径里的 {名字} 对上参数。Path(id): Path<Uuid> 写成 id: Uuid。 */
|
||||
function pathParams(path: string, fn: Node): string[] {
|
||||
const names = [...path.matchAll(/\{(\w+)\}/g)].map(match => match[1] ?? '')
|
||||
return names.flatMap((name) => {
|
||||
const param = parameterNamed(fn, name)
|
||||
if (!param)
|
||||
return [`${name}`]
|
||||
const type = innerType(param.childForFieldName('type'))
|
||||
return [type ? `${name}: ${type}` : name]
|
||||
})
|
||||
}
|
||||
|
||||
function parameterNamed(fn: Node, name: string): Node | undefined {
|
||||
const list = fn.childForFieldName('parameters')
|
||||
return named(list).find((param) => {
|
||||
if (param.type !== 'parameter' && param.type !== 'self_parameter')
|
||||
return false
|
||||
const pattern = param.childForFieldName('pattern') ?? param
|
||||
return namedDeep(pattern).some(node => (node.type === 'identifier' || node.type === 'self') && node.text === name)
|
||||
})
|
||||
}
|
||||
|
||||
function innerType(type: Node | null): string | undefined {
|
||||
if (!type)
|
||||
return undefined
|
||||
if (type.type === 'generic_type' && type.childForFieldName('type')?.text === 'Path') {
|
||||
const arg = named(type.childForFieldName('type_arguments'))[0]
|
||||
if (arg)
|
||||
return oneLine(arg.text)
|
||||
}
|
||||
return oneLine(type.text)
|
||||
}
|
||||
|
||||
function itemAttributesHasRoute(fn: Node): boolean {
|
||||
let prev = fn.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item') {
|
||||
const method = attributeName(prev)?.split('::').pop()
|
||||
if (method && METHODS.has(method))
|
||||
return true
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function functionNamed(node: Node, name: string): Node | undefined {
|
||||
return named(node.tree.rootNode).find(item => item.type === 'function_item' && item.childForFieldName('name')?.text === name)
|
||||
}
|
||||
|
||||
function leadingDoc(fn: Node): Node | undefined {
|
||||
let prev = fn.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item')
|
||||
prev = prev.previousNamedSibling
|
||||
return prev && (prev.type === 'line_comment' || prev.type === 'block_comment') ? prev : undefined
|
||||
}
|
||||
|
||||
function attributeString(attributeItem: Node): string | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
const tree = attribute?.children.find(child => child?.type === 'token_tree')
|
||||
return stringContent(tree?.children.find(child => child?.type === 'string_literal') ?? undefined)
|
||||
}
|
||||
|
||||
function namedDeep(node: Node): Node[] {
|
||||
return [node, ...named(node).flatMap(namedDeep)]
|
||||
}
|
||||
|
||||
function nameRow(name: Node | undefined): PropertyRow {
|
||||
return {
|
||||
label: '名称',
|
||||
value: name ? [{ text: name.text, role: 'function', offset: name.startIndex }] : [hint('匿名')],
|
||||
...(name && { defines: range(name) }),
|
||||
}
|
||||
}
|
||||
|
||||
function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function plain(text: string): Inline {
|
||||
return { text, role: 'plain' }
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function range(node: Node): { startIndex: number, endIndex: number } {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { blockMarks } from '../diff/viewDiff'
|
||||
import { createRustAdapter } from '../languages/rust'
|
||||
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { rustZhTranslator } from '../translators/rustZh'
|
||||
import { axumRule } from './axum'
|
||||
import { rustImplRule } from './rustImpl'
|
||||
import { serdeRule } from './serde'
|
||||
import { sqlxRule } from './sqlx'
|
||||
import { tauriRule } from './tauri'
|
||||
import { thiserrorRule } from './thiserror'
|
||||
|
||||
const adapter = createRustAdapter(WASM_DIR)
|
||||
const rules = [rustImplRule, serdeRule, thiserrorRule]
|
||||
const rules = [rustImplRule, serdeRule, thiserrorRule, axumRule, sqlxRule, tauriRule]
|
||||
|
||||
function views(source: string): Promise<BlockView[]> {
|
||||
return withBlocks(adapter, 'rust', source, blocks =>
|
||||
@@ -48,6 +52,46 @@ describe('serde 与 thiserror', () => {
|
||||
' 变体: 2 个',
|
||||
' NotFound | 这个 id 没有代理 | agent {id} 不存在',
|
||||
' Upstream | 调用上游失败 | 上游错误 ⟦透传来源|⟧',
|
||||
'import 27',
|
||||
' 来源: axum::routing',
|
||||
' 导入项: 1 个',
|
||||
' get | —',
|
||||
'axum.route 29',
|
||||
' 名称: get_agent',
|
||||
' 说明: 按 id 取一个代理。',
|
||||
' 异步: 是',
|
||||
' 请求: GET /agents/{id}',
|
||||
' 方法 | GET',
|
||||
' 路径 | /agents/{id}',
|
||||
' 路径参数 | id: Uuid',
|
||||
' 返回: Result<Json<AgentRow>, StatusCode>',
|
||||
'axum.route 35',
|
||||
' 名称: health',
|
||||
' 请求: GET /health',
|
||||
' 方法 | GET',
|
||||
' 路径 | /health',
|
||||
' 返回: —',
|
||||
'import 37',
|
||||
' 来源: sqlx',
|
||||
' 导入项: 1 个',
|
||||
' query_as | —',
|
||||
'sqlx.query 40',
|
||||
' 说明: 按 id 取一行。',
|
||||
' SQL: SELECT id, name FROM agents WHERE id = $1',
|
||||
' 参数: $1 = id',
|
||||
' 返回类型: AgentRow',
|
||||
'import 42',
|
||||
' 来源: tauri',
|
||||
' 导入项: 1 个',
|
||||
' State | —',
|
||||
'tauri.command 44',
|
||||
' 名称: rename_agent ⟦桌面命令|⟧',
|
||||
' 说明: 让窗口把这一行改名。',
|
||||
' 异步: 是',
|
||||
' 参数: 2 个',
|
||||
' state | State<App> | 窗口状态',
|
||||
' name | String | 调用方传入',
|
||||
' 返回: Result<String, String>',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -57,6 +101,23 @@ describe('serde 与 thiserror', () => {
|
||||
expect(card?.kind).toBe('class')
|
||||
})
|
||||
|
||||
it('改 axum 的路径,路径那一格标成改过', async () => {
|
||||
const before = 'use axum::routing::get;\n#[get("/agents/{id}")]\nasync fn get_agent(Path(id): Path<Uuid>) -> Result<Json<AgentRow>, StatusCode> {}\n'
|
||||
const after = before.replace('/agents/{id}', '/people/{id}')
|
||||
const oldCard = (await views(before)).find(block => block.kind === 'axum.route')
|
||||
const newCard = (await views(after)).find(block => block.kind === 'axum.route')
|
||||
const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined
|
||||
const row = newCard && 'properties' in newCard ? newCard.properties.findIndex(item => item.label === '请求') : -1
|
||||
const parts = marks?.rows?.changed[row]?.items?.changed[1]?.cells?.[1] ?? []
|
||||
const text = (change: 'add' | 'del') => parts
|
||||
.filter(part => part.change === change)
|
||||
.flatMap(part => part.segments)
|
||||
.map(segment => 'text' in segment ? segment.text : '')
|
||||
.join('')
|
||||
expect(text('del')).toContain('/agents/{id}')
|
||||
expect(text('add')).toContain('/people/{id}')
|
||||
})
|
||||
|
||||
it('skip_serializing 也是不输出', async () => {
|
||||
const source = 'use serde::Serialize;\n#[derive(Serialize)]\nstruct Row {\n #[serde(skip_serializing)]\n token: String,\n}\n'
|
||||
const lines = viewLines(await views(source))
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
||||
import { commentPlain, firstSentence, macroInvocation, oneLine, stringContent } from '../languages/rust'
|
||||
import { named } from '../languages/shared'
|
||||
|
||||
/** 查询卡用字符串那种橙。 */
|
||||
const COLOR: ThemeColor = { dark: '#ce9178', light: '#a31515' }
|
||||
const BADGE: KindBadge = { label: '查询', color: COLOR, icon: 'database' }
|
||||
const MACROS = new Set(['query', 'query_as', 'query_scalar'])
|
||||
|
||||
/**
|
||||
* sqlx::query! / query_as! 这类宏。一句说明、等宽 SQL、参数、返回类型。
|
||||
* 文件里导入了 sqlx 才启用。
|
||||
*/
|
||||
export const sqlxRule: LibraryRule = {
|
||||
id: 'sqlx',
|
||||
label: 'sqlx 查询',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['sqlx'],
|
||||
claim: (block) => {
|
||||
if (block.kind === 'comment')
|
||||
return nextQuery(block.node) ? { omit: true } : undefined
|
||||
const macro = block.kind === 'statement' ? findQuery(block.node) : undefined
|
||||
return macro ? card(block, macro) : undefined
|
||||
},
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'sqlx.query': '查询' } } }),
|
||||
}
|
||||
|
||||
function card(block: CodeBlock, macro: Node): RuleCard {
|
||||
const call = macroInvocation(macro)
|
||||
const args = call?.args ?? []
|
||||
const typed = call?.name.split('::').pop() !== 'query'
|
||||
const sqlNode = args[typed ? 1 : 0]
|
||||
const sql = stringContent(sqlNode) ?? ''
|
||||
const params = args.slice(typed ? 2 : 1)
|
||||
const returned = typed ? args[0] : undefined
|
||||
const note = precedingNote(block.node)
|
||||
const rows: PropertyRow[] = [
|
||||
...(note ? [{ label: '说明', value: { markdown: note } }] : []),
|
||||
{ label: 'SQL', value: [{ text: sql, role: 'string', ...(sqlNode && { offset: sqlNode.startIndex }) }] },
|
||||
...(params.length > 0
|
||||
? [{ label: '参数', value: [{ text: params.map((param, index) => `$${index + 1} = ${oneLine(param.text)}`).join('、'), role: 'plain' as const }] }]
|
||||
: []),
|
||||
{ label: '返回类型', value: returned ? [{ text: oneLine(returned.text), role: 'type', offset: returned.startIndex }] : [hint('—')] },
|
||||
]
|
||||
return { kind: 'sqlx.query', badge: BADGE, rows, absorbs: [] }
|
||||
}
|
||||
|
||||
function findQuery(node: Node): Node | undefined {
|
||||
if (node.type === 'macro_invocation') {
|
||||
const last = node.childForFieldName('macro')?.text.split('::').pop()
|
||||
if (last && MACROS.has(last))
|
||||
return node
|
||||
}
|
||||
for (const child of named(node)) {
|
||||
const found = findQuery(child)
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function nextQuery(comment: Node): Node | undefined {
|
||||
let next = comment.nextNamedSibling
|
||||
while (next?.type === 'attribute_item')
|
||||
next = next.nextNamedSibling
|
||||
return next ? findQuery(next) : undefined
|
||||
}
|
||||
|
||||
function precedingNote(node: Node): string | undefined {
|
||||
let prev = node.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item')
|
||||
prev = prev.previousNamedSibling
|
||||
if (!prev || (prev.type !== 'line_comment' && prev.type !== 'block_comment'))
|
||||
return undefined
|
||||
const sentence = firstSentence(commentPlain(prev))
|
||||
return sentence || undefined
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor } from '../contract'
|
||||
import { attributeName, docSentence, oneLine, returnText } from '../languages/rust'
|
||||
|
||||
/** 桌面命令的边框用这个橙。 */
|
||||
const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' }
|
||||
const BADGE: KindBadge = { label: '桌面命令', color: COLOR, icon: 'square-function' }
|
||||
|
||||
/**
|
||||
* #[tauri::command] 的函数。名称后标「桌面命令」。
|
||||
* 参数多一列来源:State<…> 是窗口状态,其余是调用方传入。
|
||||
* 文件里导入了 tauri 才启用。
|
||||
*/
|
||||
export const tauriRule: LibraryRule = {
|
||||
id: 'tauri',
|
||||
label: 'tauri 桌面命令',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['tauri'],
|
||||
claim: block => block.kind === 'function' && commanded(block) ? card(block) : undefined,
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令' } } }),
|
||||
}
|
||||
|
||||
function commanded(block: FunctionBlock): boolean {
|
||||
return block.facts.decorators.some(attr => attributeName(attr) === 'tauri::command')
|
||||
}
|
||||
|
||||
function card(block: FunctionBlock): RuleCard {
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const rows: PropertyRow[] = [
|
||||
{
|
||||
label: '名称',
|
||||
value: block.facts.name
|
||||
? [{ text: block.facts.name.text, role: 'function', offset: block.facts.name.startIndex }, plain(' '), tag('桌面命令')]
|
||||
: [tag('桌面命令')],
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
},
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||
...(block.facts.parameters.length > 0
|
||||
? [{
|
||||
label: '参数',
|
||||
value: [hint(`${block.facts.parameters.length} 个`)],
|
||||
columns: ['名称', '类型', '来源'],
|
||||
items: block.facts.parameters.map(param => ({
|
||||
cells: [
|
||||
[variable(param.name)],
|
||||
[param.type ? typed(param.type) : hint('—')],
|
||||
[hint(isWindowState(param.type) ? '窗口状态' : '调用方传入')],
|
||||
],
|
||||
defines: range(param.name),
|
||||
})),
|
||||
}]
|
||||
: []),
|
||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
||||
]
|
||||
return {
|
||||
kind: 'tauri.command',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowState(type: Node | undefined): boolean {
|
||||
if (!type)
|
||||
return false
|
||||
if (type.type === 'generic_type')
|
||||
return type.childForFieldName('type')?.text === 'State'
|
||||
return type.text.startsWith('State<')
|
||||
}
|
||||
|
||||
function variable(node: Node): Inline {
|
||||
return { text: node.text, role: 'variable', offset: node.startIndex }
|
||||
}
|
||||
|
||||
function typed(node: Node): Inline {
|
||||
return { text: oneLine(node.text), role: 'type', offset: node.startIndex }
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
Reference in new issue
Block a user