✨ feat(vscode): 代码阅读函数块默认折叠,函数说明进属性表「说明」行

This commit is contained in:
cruldra committed 2026-09-25 13:09:53 +08:00
1 parent 62f48fe67c
commit 9b32d4b5a8
17 files changed
+489 -100

No files matched your search

+33 -7
View File
@@ -1,4 +1,5 @@
import type { Access, BlockShape, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
@@ -8,9 +9,11 @@ type Scope = 'module' | 'class' | 'other'
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
const PARAMETER_SEPARATORS = new Set(['keyword_separator', 'positional_separator'])
const PATTERNS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern'])
// 只有 # 行注释;函数的说明是主体开头的 docstring,不是写在前面的注释
const COMMENTS: CommentRules = { isLine: () => true, isDoc: () => false }
function statements(items: Node[], scope: Scope, afterRow: number, docstring: boolean): CodeBlock[] {
return listBlocks(items, afterRow, (item, first) => statementBlocks(item, scope, docstring && first), () => true)
return listBlocks(items, afterRow, (item, first) => statementBlocks(item, scope, docstring && first), COMMENTS)
}
/** 冒号后面的主体:body / consequence 字段,except / finally 子句里是不带字段名的 block。 */
@@ -69,15 +72,37 @@ function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBloc
/** function_definition / class_definition;outer 是带装饰器的整体时头部从装饰器开始。 */
function definitionBlock(definition: Node, outer: Node, scope: Scope): CodeBlock {
const body = definition.childForFieldName('body')
if (definition.type === 'class_definition')
return { kind: 'class', ...bodyShape(outer, definition.childForFieldName('body'), 'class', true) }
return { kind: 'class', ...bodyShape(outer, body, 'class', true) }
const doc = docstringOf(body)
return {
kind: 'function',
...bodyShape(outer, definition.childForFieldName('body'), 'other', true),
facts: functionFacts(definition, outer, scope),
...(body && doc ? documentedShape(outer, body, doc) : bodyShape(outer, body, 'other')),
facts: functionFacts(definition, outer, scope, doc),
}
}
/** 主体开头的 docstring:第一条语句(注释不算)是单独一个字符串表达式时,返回那个字符串节点。 */
function docstringOf(body: Node | null): Node | undefined {
const first = named(body).find(child => child.type !== 'comment')
return first && docstringIn(first)
}
/** 语句是单独一个字符串表达式(docstring 的写法)时返回那个字符串节点。 */
function docstringIn(statement: Node): Node | undefined {
if (statement.type !== 'expression_statement')
return undefined
const [expression, ...rest] = named(statement)
return rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string') ? expression : undefined
}
/** 函数的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
function documentedShape(outer: Node, body: Node, doc: Node): BlockShape {
const rest = named(body).filter(item => item.startIndex >= doc.endIndex)
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, 'other', afterRow, false))
}
/** 单下划线开头的名字是私有(`_x`、`__x`),前后双下划线的特殊方法(`__init__`)是公共。 */
function nameAccess(name: string): Access {
const dunder = name.length > 4 && name.startsWith('__') && name.endsWith('__')
@@ -108,10 +133,11 @@ function scopeAccess(target: Node, scope: Scope): Access | undefined {
return names.every(name => nameAccess(name.text) === 'private') ? 'private' : 'public'
}
function functionFacts(definition: Node, outer: Node, scope: Scope): FunctionFacts {
function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node | undefined): FunctionFacts {
const name = definition.childForFieldName('name')
return {
name: name ?? undefined,
doc,
access: name ? scopeAccess(name, scope) : undefined,
async: children(definition).some(child => child.type === 'async'),
decorators: named(outer).filter(child => child.type === 'decorator'),
@@ -177,9 +203,9 @@ function isConstantTarget(target: Node | null): boolean {
/** 赋值 → 字段 / 常量 / 变量;主体开头的字符串是 docstring;其余表达式是语句。 */
function expressionBlock(node: Node, scope: Scope, docstring: boolean): CodeBlock {
const [expression, ...rest] = named(node)
if (docstring && rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string'))
if (docstring && docstringIn(node))
return leafBlock('comment', node)
const expression = named(node)[0]
const left = expression?.type === 'assignment' ? expression.childForFieldName('left') : null
if (!expression || !left)
return leafBlock('statement', node)