✨ feat(vscode): 代码阅读预览 Rust 的语言卡片
This commit is contained in:
1 parent
4480f936f3
commit
6d58b681cf
15 files changed
+1016
-6
No files matched your search
@@ -0,0 +1,503 @@
|
||||
import type { Access, BlockAttributes, ClassBlock, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, TokenRole, TraitBlock } from '../contract'
|
||||
import * as path from 'node:path'
|
||||
import { compoundShape, extendHeader, headerSpan, leafBlock, leafShape, named } from './shared'
|
||||
|
||||
/** 可见性:pub 是公开,没写是本模块,pub(super) 是父模块,pub(crate) 是本 crate。 */
|
||||
export type RustVisibility = 'public' | 'module' | 'super' | 'crate'
|
||||
|
||||
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
|
||||
|
||||
/** 一条说明的第一句,句号留着。 */
|
||||
export function firstSentence(text: string): string {
|
||||
const trimmed = text.trim()
|
||||
const sentence = trimmed.match(/^[\s\S]*?[。!?.!?]/)
|
||||
return (sentence ? sentence[0] : trimmed.split('\n')[0] ?? '').trim()
|
||||
}
|
||||
|
||||
/** 文档注释去掉记号后的第一句;普通注释是整段去掉记号。 */
|
||||
export function commentPlain(node: Node): string {
|
||||
const doc = node.childForFieldName('doc')
|
||||
const raw = doc ? doc.text : node.text.replace(/^\/\/[/!]?\s?/, '').replace(/^\/\*[*!]?\s?/, '').replace(/\s*\*\/$/, '')
|
||||
return raw.replace(/\n/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** 说明行用的第一句。没有文字时没有。 */
|
||||
export function docSentence(node: Node | undefined): string | undefined {
|
||||
if (!node)
|
||||
return undefined
|
||||
const sentence = firstSentence(commentPlain(node))
|
||||
return sentence || undefined
|
||||
}
|
||||
|
||||
/** 空白收成一个空格,方便属性表里的类型、签名。 */
|
||||
export function oneLine(text: string): string {
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** 节点上的可见性。pub(in …) 不在这四档里,按本 crate 算(只本包能用)。 */
|
||||
export function rustVisibility(node: Node): RustVisibility {
|
||||
const vis = named(node).find(child => child.type === 'visibility_modifier')
|
||||
if (!vis)
|
||||
return 'module'
|
||||
const words = new Set(vis.children.map(child => child?.type))
|
||||
if (words.has('super'))
|
||||
return 'super'
|
||||
if (words.has('crate') || words.has('in'))
|
||||
return 'crate'
|
||||
return 'public'
|
||||
}
|
||||
|
||||
/** 公开写在 facts.access 上;其余写在块的 access 属性上,盖过 facts。 */
|
||||
export function visibilityFacts(node: Node): { access?: Access, attributes?: BlockAttributes } {
|
||||
const visibility = rustVisibility(node)
|
||||
return visibility === 'public' ? { access: 'public' } : { attributes: { access: visibility } }
|
||||
}
|
||||
|
||||
/** 参数表括号里的原文,空白收成一行。 */
|
||||
export function signatureParams(fn: Node): string {
|
||||
const params = fn.childForFieldName('parameters')
|
||||
if (!params)
|
||||
return ''
|
||||
return oneLine(params.text.replace(/^\(/, '').replace(/\)$/, ''))
|
||||
}
|
||||
|
||||
/** 返回类型原文;没写是 ()。 */
|
||||
export function returnText(fn: Node): string {
|
||||
const returned = fn.childForFieldName('return_type')
|
||||
return returned ? oneLine(returned.text) : '()'
|
||||
}
|
||||
|
||||
/** 函数体里直接写的 match,每一支的模式和「做什么」。 */
|
||||
export function matchArms(fn: Node): { pattern: string, does: string }[] {
|
||||
const body = fn.childForFieldName('body')
|
||||
if (!body)
|
||||
return []
|
||||
const arms: { pattern: string, does: string }[] = []
|
||||
for (const statement of named(body)) {
|
||||
const expr = statement.type === 'expression_statement' ? named(statement)[0] : statement
|
||||
if (expr?.type !== 'match_expression')
|
||||
continue
|
||||
const block = expr.childForFieldName('body')
|
||||
for (const arm of named(block).filter(child => child.type === 'match_arm')) {
|
||||
const pattern = arm.childForFieldName('pattern')
|
||||
if (pattern)
|
||||
arms.push({ pattern: oneLine(pattern.text), does: armDoes(arm) })
|
||||
}
|
||||
}
|
||||
return arms
|
||||
}
|
||||
|
||||
/** 变体携带的数据:没带是空的,元组变体是类型,结构变体是字段原文。body 是变体的 body。 */
|
||||
export function variantPayload(body: Node | undefined): string | undefined {
|
||||
if (!body)
|
||||
return undefined
|
||||
if (body.type === 'ordered_field_declaration_list') {
|
||||
const types = named(body).filter(child => child.type !== 'attribute_item').map(child => oneLine(child.text))
|
||||
return types.join(', ') || undefined
|
||||
}
|
||||
return oneLine(body.text)
|
||||
}
|
||||
|
||||
/** 写在这条前面、紧挨着的属性(#[…]),从外往里。 */
|
||||
export function leadingAttributes(node: Node): Node[] {
|
||||
const attrs: Node[] = []
|
||||
let prev = node.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item') {
|
||||
attrs.unshift(prev)
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
/** 属性的名字:get、serde、axum::post、tauri::command。 */
|
||||
export function attributeName(attributeItem: Node): string | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
const head = attribute ? named(attribute)[0] : undefined
|
||||
return head?.text
|
||||
}
|
||||
|
||||
/** 这条自己的属性,含写在它前面的。 */
|
||||
export function itemAttributes(node: Node): Node[] {
|
||||
return [...leadingAttributes(node), ...named(node).filter(child => child.type === 'attribute_item')]
|
||||
}
|
||||
|
||||
/** derive(...) 里列出的名字。 */
|
||||
export function deriveNames(node: Node): string[] {
|
||||
const names: string[] = []
|
||||
for (const item of itemAttributes(node)) {
|
||||
if (attributeName(item) !== 'derive')
|
||||
continue
|
||||
const attribute = named(item).find(child => child.type === 'attribute')
|
||||
const tree = attribute?.children.find(child => child?.type === 'token_tree')
|
||||
if (!tree)
|
||||
continue
|
||||
for (const name of named(tree)) {
|
||||
if (name.type === 'identifier')
|
||||
names.push(name.text)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/** 字符串字面量里的原文,不含引号。 */
|
||||
export function stringContent(node: Node | undefined): string | undefined {
|
||||
if (!node)
|
||||
return undefined
|
||||
if (node.type === 'string_content')
|
||||
return node.text
|
||||
const content = node.children.find(child => child?.type === 'string_content')
|
||||
return content?.text
|
||||
}
|
||||
|
||||
/** 宏调用的名字和括号里的具名实参。 */
|
||||
export function macroInvocation(node: Node): { name: string, args: Node[] } | undefined {
|
||||
if (node.type !== 'macro_invocation')
|
||||
return undefined
|
||||
const macro = node.childForFieldName('macro')
|
||||
const args = node.children.find(child => child?.type === 'token_tree')
|
||||
if (!macro || !args)
|
||||
return undefined
|
||||
return { name: macro.text, args: named(args) }
|
||||
}
|
||||
|
||||
/** 函数写在 impl 或 trait 里面,参数表叫「参数」而不是「输入参数」。 */
|
||||
export function isMethod(node: Node): boolean {
|
||||
const parent = node.parent
|
||||
return parent?.type === 'declaration_list' && (parent.parent?.type === 'impl_item' || parent.parent?.type === 'trait_item')
|
||||
}
|
||||
|
||||
/** 结构体的字段:名字、类型、可见性。元组结构体没有名字,不进这张表。 */
|
||||
export function structFields(node: Node): { name: Node, type: Node, visibility: RustVisibility }[] {
|
||||
const body = node.childForFieldName('body')
|
||||
if (body?.type !== 'field_declaration_list')
|
||||
return []
|
||||
return named(body).flatMap((field) => {
|
||||
if (field.type !== 'field_declaration')
|
||||
return []
|
||||
const name = field.childForFieldName('name')
|
||||
const type = field.childForFieldName('type')
|
||||
return name && type ? [{ name, type, visibility: rustVisibility(field) }] : []
|
||||
})
|
||||
}
|
||||
|
||||
/** trait 或 impl 主体里的函数(含只写签名的)。 */
|
||||
export function bodyFunctions(node: Node): Node[] {
|
||||
const body = node.childForFieldName('body')
|
||||
return named(body).filter(child => child.type === 'function_item' || child.type === 'function_signature_item')
|
||||
}
|
||||
|
||||
export function createRustAdapter(wasmDir: string): LanguageAdapter {
|
||||
return {
|
||||
id: 'rust',
|
||||
languageIds: ['rust'],
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-rust.wasm'),
|
||||
toBlocks: root => topBlocks(named(root)),
|
||||
tokenRole: rustTokenRole,
|
||||
}
|
||||
}
|
||||
|
||||
function topBlocks(items: Node[]): CodeBlock[] {
|
||||
const blocks: CodeBlock[] = []
|
||||
let index = 0
|
||||
while (index < items.length) {
|
||||
const item = items[index]
|
||||
if (isDoc(item)) {
|
||||
const docs = [item]
|
||||
let next = index + 1
|
||||
while (next < items.length && isDoc(items[next]) && items[next].startPosition.row <= docs[docs.length - 1].endPosition.row + 1)
|
||||
docs.push(items[next++])
|
||||
while (next < items.length && items[next].type === 'attribute_item')
|
||||
next++
|
||||
const target = items[next]
|
||||
if (target && target.type !== 'line_comment' && target.type !== 'block_comment') {
|
||||
blocks.push(withPreamble(itemBlock(target), docs))
|
||||
index = next + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (item.type === 'attribute_item') {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if (item.type === 'line_comment' || item.type === 'block_comment')
|
||||
blocks.push(leafBlock('comment', item))
|
||||
else
|
||||
blocks.push(withPreamble(itemBlock(item), []))
|
||||
index++
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function itemBlock(item: Node): CodeBlock {
|
||||
switch (item.type) {
|
||||
case 'use_declaration':
|
||||
return useBlock(item)
|
||||
case 'struct_item':
|
||||
return structBlock(item)
|
||||
case 'enum_item':
|
||||
return enumBlock(item)
|
||||
case 'impl_item':
|
||||
return implBlock(item)
|
||||
case 'trait_item':
|
||||
return traitBlock(item)
|
||||
case 'function_item':
|
||||
return functionBlock(item)
|
||||
default:
|
||||
return leafBlock('statement', item)
|
||||
}
|
||||
}
|
||||
|
||||
/** 文档注释和写在前面的属性算进头部。文档注释的第一句是说明。 */
|
||||
function withPreamble(block: CodeBlock, docs: Node[]): CodeBlock {
|
||||
const [doc] = docs
|
||||
const attribute = leadingAttributes(block.node)[0]
|
||||
let next = block
|
||||
if (doc && isDocumentedKind(next))
|
||||
next = { ...extendHeader(next, doc), facts: { ...next.facts, doc } } as typeof next
|
||||
const from = doc ?? attribute
|
||||
if (from && next.header.startIndex > from.startIndex)
|
||||
next = extendHeader(next, from)
|
||||
return next
|
||||
}
|
||||
|
||||
function isDocumentedKind(block: CodeBlock): block is FunctionBlock | ClassBlock | TraitBlock | ImplBlock | EnumBlock {
|
||||
return block.kind === 'function' || block.kind === 'class' || block.kind === 'trait' || block.kind === 'impl' || block.kind === 'enum'
|
||||
}
|
||||
|
||||
function isDoc(node: Node): boolean {
|
||||
return (node.type === 'line_comment' || node.type === 'block_comment') && named(node).some(child => child.type === 'outer_doc_comment_marker')
|
||||
}
|
||||
|
||||
function useBlock(node: Node): ImportBlock {
|
||||
const argument = node.childForFieldName('argument')
|
||||
const { source, items } = argument ? useParts(argument) : { items: [] }
|
||||
return { kind: 'import', ...leafShape(node), facts: { sources: source ? [source] : [], items } }
|
||||
}
|
||||
|
||||
function useParts(node: Node): { source?: Node, items: ImportItem[] } {
|
||||
switch (node.type) {
|
||||
case 'scoped_identifier': {
|
||||
const source = node.childForFieldName('path') ?? undefined
|
||||
const name = node.childForFieldName('name')
|
||||
return { source, items: name ? [{ kind: 'named', name }] : [] }
|
||||
}
|
||||
case 'scoped_use_list': {
|
||||
const source = node.childForFieldName('path') ?? undefined
|
||||
const list = node.childForFieldName('list')
|
||||
return { source, items: list ? useList(list) : [] }
|
||||
}
|
||||
case 'use_list':
|
||||
return { items: useList(node) }
|
||||
case 'use_as_clause':
|
||||
return { items: asItem(node) }
|
||||
case 'use_wildcard':
|
||||
return { items: [{ kind: 'namespace' }] }
|
||||
case 'identifier':
|
||||
return { items: [{ kind: 'named', name: node }] }
|
||||
default:
|
||||
return { items: [] }
|
||||
}
|
||||
}
|
||||
|
||||
function useList(list: Node): ImportItem[] {
|
||||
return named(list).flatMap((child) => {
|
||||
if (child.type === 'identifier')
|
||||
return [{ kind: 'named' as const, name: child }]
|
||||
if (child.type === 'use_as_clause')
|
||||
return asItem(child)
|
||||
if (child.type === 'use_wildcard')
|
||||
return [{ kind: 'namespace' as const }]
|
||||
if (child.type === 'scoped_identifier') {
|
||||
const name = child.childForFieldName('name')
|
||||
return name ? [{ kind: 'named' as const, name }] : []
|
||||
}
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
function asItem(node: Node): ImportItem[] {
|
||||
const name = node.childForFieldName('path')
|
||||
const alias = node.childForFieldName('alias') ?? undefined
|
||||
return name ? [{ kind: 'named', name, alias }] : []
|
||||
}
|
||||
|
||||
function structBlock(node: Node): CodeBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
return {
|
||||
kind: 'class',
|
||||
...shape(node),
|
||||
...(attributes && { attributes }),
|
||||
facts: classFacts(node, access),
|
||||
}
|
||||
}
|
||||
|
||||
function enumBlock(node: Node): EnumBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
const body = node.childForFieldName('body')
|
||||
const members = named(body).flatMap((variant) => {
|
||||
if (variant.type !== 'enum_variant')
|
||||
return []
|
||||
const name = variant.childForFieldName('name')
|
||||
if (!name)
|
||||
return []
|
||||
const payload = variant.childForFieldName('body') ?? undefined
|
||||
return [{ name, ...(payload && { value: payload }), docs: [] }]
|
||||
})
|
||||
return {
|
||||
kind: 'enum',
|
||||
...shape(node),
|
||||
...(attributes && { attributes }),
|
||||
facts: { name: node.childForFieldName('name') ?? node, doc: undefined, access, decorators: [], bases: [], members },
|
||||
}
|
||||
}
|
||||
|
||||
function traitBlock(node: Node): TraitBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
return { kind: 'trait', ...shape(node), ...(attributes && { attributes }), facts: classFacts(node, access) }
|
||||
}
|
||||
|
||||
function implBlock(node: Node): ImplBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
const body = node.childForFieldName('body')
|
||||
const children: CodeBlock[] = []
|
||||
let docs: Node[] = []
|
||||
for (const child of named(body)) {
|
||||
if (isDoc(child)) {
|
||||
docs.push(child)
|
||||
continue
|
||||
}
|
||||
if (child.type === 'attribute_item')
|
||||
continue
|
||||
if (child.type === 'function_item')
|
||||
children.push(withPreamble(functionBlock(child), docs))
|
||||
else if (child.type === 'const_item' || child.type === 'type_item')
|
||||
children.push(leafBlock('statement', child))
|
||||
docs = []
|
||||
}
|
||||
return {
|
||||
kind: 'impl',
|
||||
...compoundShape(node, body?.startIndex ?? node.endIndex, [], () => children),
|
||||
...(attributes && { attributes }),
|
||||
facts: classFacts(node, access, implType(node)),
|
||||
}
|
||||
}
|
||||
|
||||
function functionBlock(node: Node): FunctionBlock {
|
||||
const { access, attributes } = visibilityFacts(node)
|
||||
const body = node.childForFieldName('body')
|
||||
const headerEnd = body?.startIndex ?? node.endIndex
|
||||
return {
|
||||
kind: 'function',
|
||||
node,
|
||||
header: headerSpan(node, headerEnd),
|
||||
children: [],
|
||||
...(attributes && { attributes }),
|
||||
facts: {
|
||||
name: node.childForFieldName('name') ?? undefined,
|
||||
access,
|
||||
...(isAsync(node) && { async: true }),
|
||||
decorators: leadingAttributes(node),
|
||||
parameters: parametersOf(node.childForFieldName('parameters')),
|
||||
returnType: node.childForFieldName('return_type') ?? undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function classFacts(node: Node, access: Access | undefined, name = node.childForFieldName('name') ?? undefined): ClassFacts {
|
||||
return { name, access, decorators: leadingAttributes(node), typeParameters: [], bases: [], implements: [] }
|
||||
}
|
||||
|
||||
/** impl 的自己的类型:impl Trait for Type 取 Type,impl Type 取 Type。 */
|
||||
function implType(node: Node): Node | undefined {
|
||||
return node.childForFieldName('type') ?? node.childForFieldName('trait') ?? undefined
|
||||
}
|
||||
|
||||
function shape(node: Node): { node: Node, header: { startIndex: number, endIndex: number, startRow: number, endRow: number }, children: CodeBlock[] } {
|
||||
const body = node.childForFieldName('body')
|
||||
return body ? { node, header: headerSpan(node, body.startIndex), children: [] } : { ...leafShape(node) }
|
||||
}
|
||||
|
||||
function parametersOf(list: Node | null): ParameterFacts[] {
|
||||
if (!list)
|
||||
return []
|
||||
return named(list).flatMap((child) => {
|
||||
if (child.type === 'self_parameter') {
|
||||
const name = [...child.children].reverse().find(token => token?.type === 'self')
|
||||
return name ? [{ name, optional: false, type: child }] : []
|
||||
}
|
||||
if (child.type !== 'parameter')
|
||||
return []
|
||||
const name = child.childForFieldName('pattern')
|
||||
const type = child.childForFieldName('type') ?? undefined
|
||||
return name ? [{ name, optional: false, type }] : []
|
||||
})
|
||||
}
|
||||
|
||||
function isAsync(node: Node): boolean {
|
||||
const modifiers = named(node).find(child => child.type === 'function_modifiers')
|
||||
return modifiers?.children.some(child => child?.type === 'async') ?? false
|
||||
}
|
||||
|
||||
function armDoes(arm: Node): string {
|
||||
const comment = firstComment(arm)
|
||||
if (comment)
|
||||
return firstSentence(commentPlain(comment)) || '—'
|
||||
const value = arm.childForFieldName('value')
|
||||
if (!value)
|
||||
return '—'
|
||||
if (value.type === 'return_expression') {
|
||||
const returned = named(value)[0]
|
||||
return returned ? `返回 ${oneLine(returned.text)}` : '返回'
|
||||
}
|
||||
if (value.type === 'block') {
|
||||
const inner = firstComment(value)
|
||||
if (inner)
|
||||
return firstSentence(commentPlain(inner)) || '—'
|
||||
const statement = named(value)[0]
|
||||
return statement ? oneLine(statement.text) : '—'
|
||||
}
|
||||
return oneLine(value.text)
|
||||
}
|
||||
|
||||
function firstComment(node: Node): Node | undefined {
|
||||
if (node.type === 'line_comment' || node.type === 'block_comment')
|
||||
return node
|
||||
for (const child of node.children) {
|
||||
if (!child)
|
||||
continue
|
||||
const found = firstComment(child)
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function rustTokenRole(leaf: Node): TokenRole {
|
||||
if (leaf.type === 'line_comment' || leaf.type === 'block_comment' || inside(leaf, 'line_comment') || inside(leaf, 'block_comment'))
|
||||
return 'comment'
|
||||
if (inside(leaf, 'string_literal') || leaf.type === 'string_content')
|
||||
return 'string'
|
||||
if (leaf.type === 'integer_literal' || leaf.type === 'float_literal' || leaf.type === 'char_literal')
|
||||
return 'number'
|
||||
if (leaf.type === 'primitive_type' || leaf.type === 'type_identifier')
|
||||
return 'type'
|
||||
if (leaf.type === 'mutable_specifier')
|
||||
return 'keyword'
|
||||
if (!leaf.isNamed)
|
||||
return STORAGE.has(leaf.type) ? 'storage' : /^[a-z]+$/.test(leaf.type) ? 'keyword' : 'plain'
|
||||
if (leaf.type === 'identifier' || leaf.type === 'field_identifier' || leaf.type === 'self') {
|
||||
const parent = leaf.parent?.type
|
||||
return parent === 'function_item' || parent === 'function_signature_item' ? 'function' : 'variable'
|
||||
}
|
||||
return 'plain'
|
||||
}
|
||||
|
||||
function inside(node: Node, type: string): boolean {
|
||||
let current = node.parent
|
||||
while (current) {
|
||||
if (current.type === type)
|
||||
return true
|
||||
current = current.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user