75 lines
3.7 KiB
TypeScript
75 lines
3.7 KiB
TypeScript
import type { DisplayName, DisplayNameFacts, Node, SourceRange } from '../contract'
|
||
|
||
/** 给定义起中文名的那个调用 / 装饰器 / 注释记号的名字:`Comment("当前用户")`、`@Comment("…")`、`# @Comment 当前用户`。 */
|
||
export const COMMENT_MARK = 'Comment'
|
||
|
||
/** 说明文字里的一句话最多几个字才当中文名 */
|
||
export const CLAUSE_LIMIT = 12
|
||
|
||
/** 说明文字的第一小句(到第一个 ,。,.;;:: 为止)去掉首尾空白;空的、超过 CLAUSE_LIMIT 个字的不当中文名。 */
|
||
export function firstClause(text: string): string | undefined {
|
||
const clause = text.split(/[,。,.;;::]/, 1)[0].trim()
|
||
return clause && [...clause].length <= CLAUSE_LIMIT ? clause : undefined
|
||
}
|
||
|
||
/** 行注释里的中文名记号 `@Comment 名字`(名字一直到注释结尾):名字,和记号从注释文字的第几个字开始。 */
|
||
export function commentTag(text: string): { text: string, start: number } | undefined {
|
||
const match = new RegExp(`@${COMMENT_MARK}[ \\t]+(\\S.*)$`).exec(text)
|
||
return match ? { text: match[1].trimEnd(), start: match.index } : undefined
|
||
}
|
||
|
||
/** 一个定义和语言插件从代码里读到的中文名。 */
|
||
export interface NameCandidate {
|
||
/** 定义处那个名字 */
|
||
name: Node
|
||
/** 写在代码里的记号给的:`Annotated[T, Comment("…")]`、`@Comment("…")` */
|
||
marked?: string
|
||
/** 说明文字里取的:docstring 的参数说明、JSDoc 的 @param */
|
||
described?: string
|
||
/** 行尾的 `@Comment` 记号能给它:参数、变量、字段、枚举成员、属性(函数、类这些用装饰器) */
|
||
trailing?: boolean
|
||
}
|
||
|
||
function rangeOf(node: Node): SourceRange {
|
||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||
}
|
||
|
||
/** 注释前面同一行有代码:是写在行尾的注释。 */
|
||
function isTrailing(comment: Node, source: string): boolean {
|
||
const lineStart = source.lastIndexOf('\n', comment.startIndex - 1) + 1
|
||
return source.slice(lineStart, comment.startIndex).trim() !== ''
|
||
}
|
||
|
||
/**
|
||
* 汇总一份源码里的中文名。行尾注释里的记号(`x = 1 # @Comment 数量`)给那一行第一个能带行尾记号的定义(多行的参数表里就是那一行的参数),
|
||
* 记号都算 markers(翻注释、说明时去掉),不管这个定义最后取没取它。
|
||
* 每个定义取哪个:写在代码里的记号,其次行尾记号,最后才是说明文字。
|
||
*/
|
||
export function displayNameFacts(candidates: readonly NameCandidate[], lineComments: readonly Node[], source: string): DisplayNameFacts {
|
||
const byStart = new Map<number, NameCandidate & { tagged?: string }>()
|
||
for (const candidate of candidates) {
|
||
const known = byStart.get(candidate.name.startIndex)
|
||
byStart.set(candidate.name.startIndex, {
|
||
name: candidate.name,
|
||
marked: known?.marked ?? candidate.marked,
|
||
described: known?.described ?? candidate.described,
|
||
trailing: known?.trailing || candidate.trailing,
|
||
})
|
||
}
|
||
const ordered = [...byStart.values()].sort((a, b) => a.name.startIndex - b.name.startIndex)
|
||
const markers: SourceRange[] = []
|
||
for (const comment of lineComments) {
|
||
const tag = isTrailing(comment, source) ? commentTag(comment.text) : undefined
|
||
const owner = tag && ordered.find(candidate => candidate.trailing && candidate.name.startPosition.row === comment.startPosition.row)
|
||
if (!tag || !owner)
|
||
continue
|
||
owner.tagged ??= tag.text
|
||
markers.push({ startIndex: comment.startIndex + tag.start, endIndex: comment.endIndex })
|
||
}
|
||
const names = ordered.flatMap((candidate): DisplayName[] => {
|
||
const text = candidate.marked ?? candidate.tagged ?? candidate.described
|
||
return text ? [{ name: rangeOf(candidate.name), text }] : []
|
||
})
|
||
return { names, markers }
|
||
}
|