✨ feat(vscode): 代码阅读认出代码里起的中文名(Annotated[…, Comment("…")]、@Comment 装饰器、行尾 # @Comment、docstring / JSDoc 的参数说明),预览里定义和用到它的各处都写中文名,悬停看原名,定义处后面淡淡写原名

This commit is contained in:
cruldra committed 2026-09-27 00:56:39 +08:00
1 parent 1b1299dc1f
commit 44c221ea2b
45 files changed
+1685 -128

No files matched your search

@@ -0,0 +1,189 @@
import type { DisplayNameFacts, DisplayNameReader, Node } from '../contract'
import type { NameCandidate } from './displayNames'
import { COMMENT_MARK, displayNameFacts, firstClause } from './displayNames'
import { children, named } from './shared'
/** 名字的最后一节:`Comment`、`typing.Annotated` 的 `Annotated`。 */
function lastName(node: Node | null | undefined): string | undefined {
if (node?.type === 'identifier')
return node.text
return node?.type === 'attribute' ? node.childForFieldName('attribute')?.text : undefined
}
/** 普通字符串字面量的内容(不带引号、前缀);f-string 这类带插值的没有。 */
function plainString(node: Node | undefined): string | undefined {
if (node?.type !== 'string' || named(node).some(part => part.type === 'interpolation'))
return undefined
const start = named(node).find(part => part.type === 'string_start')
const end = named(node).find(part => part.type === 'string_end')
const text = start && end ? node.text.slice(start.endIndex - node.startIndex, end.startIndex - node.startIndex).trim() : ''
return text || undefined
}
/** `Comment("当前用户")`(调用的名字最后一节是 Comment,不管从哪导入、是不是本地定义的)的第一个参数。 */
function commentCall(node: Node | undefined): string | undefined {
if (node?.type !== 'call' || lastName(node.childForFieldName('function')) !== COMMENT_MARK)
return undefined
return plainString(named(node.childForFieldName('arguments'))[0])
}
/** 注解 `Annotated[T, …, Comment("…")]`(`typing.Annotated` 也算)里的中文名。 */
function annotatedName(annotation: Node | null): string | undefined {
const inner = named(annotation)[0]
let head: Node | undefined
let items: Node[] = []
if (inner?.type === 'generic_type') {
head = named(inner)[0]
items = named(named(inner).find(part => part.type === 'type_parameter')).map(item => item.type === 'type' ? named(item)[0] : item)
}
else if (inner?.type === 'subscript') {
head = inner.childForFieldName('value') ?? undefined
items = inner.childrenForFieldName('subscript').filter((item): item is Node => item !== null)
}
if (lastName(head) !== 'Annotated')
return undefined
return items.slice(1).map(commentCall).find(text => text !== undefined)
}
/** 装饰器 `@Comment("…")` 给的中文名。 */
function decoratorName(definition: Node): string | undefined {
const outer = definition.parent?.type === 'decorated_definition' ? definition.parent : undefined
return named(outer).filter(child => child.type === 'decorator').map(decorator => commentCall(named(decorator)[0])).find(text => text !== undefined)
}
/** 参数节点里定义的那个名字:`x`、`x: int`、`x=1`、`*args`、`**kw`;`*`、`/` 分隔符没有。 */
function parameterName(param: Node): Node | undefined {
switch (param.type) {
case 'identifier':
return param
case 'typed_parameter':
return parameterName(named(param)[0])
case 'default_parameter':
case 'typed_default_parameter':
return param.childForFieldName('name') ?? undefined
case 'list_splat_pattern':
case 'dictionary_splat_pattern':
return named(param).find(child => child.type === 'identifier')
default:
return undefined
}
}
/**
* docstring 里各参数的说明取第一小句:Google 写法 `Args:` 一节里的 `name (type): 说明`,Sphinx 写法 `:param name: 说明`
* (`:param type name:` 也算)。名字去掉 `*` / `**`。
*/
export function docstringParameters(doc: string): Map<string, string> {
const found = new Map<string, string>()
const put = (name: string, text: string): void => {
const clause = firstClause(text)
if (clause && !found.has(name))
found.set(name, clause)
}
const lines = doc.split('\n')
const indentOf = (line: string): number => line.length - line.trimStart().length
for (let i = 0; i < lines.length; i++) {
const sphinx = /^\s*:param\s+(?:[^\s:]+\s+)?\*{0,2}(\w+)\s*:(.*)$/.exec(lines[i])
if (sphinx) {
put(sphinx[1], sphinx[2])
continue
}
if (!/^\s*(?:Args|Arguments):\s*$/.test(lines[i]))
continue
const sectionIndent = indentOf(lines[i])
let entryIndent: number | undefined
for (i++; i < lines.length; i++) {
const line = lines[i]
if (line.trim() === '')
continue
const indent = indentOf(line)
if (indent <= sectionIndent) {
i--
break
}
entryIndent ??= indent
const entry = indent === entryIndent ? /^\s*\*{0,2}(\w+)(?:\s*\([^)]*\))?\s*:(.*)$/.exec(line) : null
if (entry)
put(entry[1], entry[2])
}
}
return found
}
/** 函数主体开头的 docstring 的原文。 */
function docstringOf(fn: Node): string | undefined {
const first = named(fn.childForFieldName('body')).find(child => child.type !== 'comment')
const [expression, ...rest] = first?.type === 'expression_statement' ? named(first) : []
return rest.length === 0 ? plainString(expression) : undefined
}
function functionCandidates(fn: Node): NameCandidate[] {
const name = fn.childForFieldName('name')
const doc = docstringOf(fn)
const described = doc === undefined ? new Map<string, string>() : docstringParameters(doc)
const params = named(fn.childForFieldName('parameters')).flatMap((param): NameCandidate[] => {
const paramName = parameterName(param)
if (!paramName)
return []
return [{ name: paramName, marked: annotatedName(param.childForFieldName('type')), described: described.get(paramName.text), trailing: true }]
})
return [...(name ? [{ name, marked: decoratorName(fn) }] : []), ...params]
}
/** 赋值里定义的名字:`x = …`、`x: T = …`、只写注解的 `x: T`、`self.x = …` 的 x、解构的各个名字。 */
function targetNames(target: Node | null): Node[] {
if (!target)
return []
if (target.type === 'identifier')
return [target]
if (target.type === 'attribute') {
const attribute = target.childForFieldName('attribute')
return attribute ? [attribute] : []
}
if (['pattern_list', 'tuple_pattern', 'list_pattern', 'list_splat_pattern'].includes(target.type))
return named(target).flatMap(targetNames)
return []
}
function assignmentCandidates(assignment: Node): NameCandidate[] {
const marked = annotatedName(assignment.childForFieldName('type'))
return targetNames(assignment.childForFieldName('left')).map(name => ({ name, marked, trailing: true }))
}
/**
* Python 的中文名:`Annotated[T, Comment("…")]`(参数、变量、类字段)、`@Comment("…")`(函数、类)、
* 定义那一行行尾的 `# @Comment 名字`(多行参数表里的参数、变量、字段、枚举成员),都没有时参数取函数 docstring 里它的说明。
*/
export function pythonDisplayNames(root: Node, source: string): DisplayNameFacts {
const candidates: NameCandidate[] = []
const comments: Node[] = []
const visit = (node: Node): void => {
switch (node.type) {
case 'comment':
comments.push(node)
return
case 'function_definition':
candidates.push(...functionCandidates(node))
break
case 'class_definition': {
const name = node.childForFieldName('name')
if (name)
candidates.push({ name, marked: decoratorName(node) })
break
}
case 'assignment':
candidates.push(...assignmentCandidates(node))
break
}
for (const child of children(node))
visit(child)
}
visit(root)
return displayNameFacts(candidates, comments, source)
}
export const pythonNameReader: DisplayNameReader = {
files: { python: '**/*.py' },
mentions: source => source.includes(COMMENT_MARK) || /\b(?:Args|Arguments):|:param\s/.test(source),
find: (root, source) => pythonDisplayNames(root, source),
}