✨ feat(vscode): 代码阅读认出代码里起的中文名(Annotated[…, Comment("…")]、@Comment 装饰器、行尾 # @Comment、docstring / JSDoc 的参数说明),预览里定义和用到它的各处都写中文名,悬停看原名,定义处后面淡淡写原名
This commit is contained in:
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),
|
||||
}
|
||||
Reference in new issue
Block a user