315 lines
13 KiB
TypeScript
315 lines
13 KiB
TypeScript
import type { BaseFacts, BlockShape, ClassBlock, ClassFacts, CodeBlock, DocumentedBlock, ExceptionBlock, FieldFacts, Node, ParameterFacts, SourceSpan, TextKind } from '../contract'
|
||
|
||
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
|
||
|
||
export function named(node: Node | null | undefined): Node[] {
|
||
return node ? node.namedChildren.filter((child): child is Node => child !== null) : []
|
||
}
|
||
|
||
export function children(node: Node): Node[] {
|
||
return node.children.filter((child): child is Node => child !== null)
|
||
}
|
||
|
||
/** 具名子节点,去掉注释这类语法之外的节点。 */
|
||
export function syntaxChildren(node: Node | null | undefined): Node[] {
|
||
return named(node).filter(child => !child.isExtra)
|
||
}
|
||
|
||
export function nodeSpan(node: Node): SourceSpan {
|
||
return {
|
||
startIndex: node.startIndex,
|
||
endIndex: node.endIndex,
|
||
startRow: node.startPosition.row,
|
||
endRow: node.endPosition.row,
|
||
}
|
||
}
|
||
|
||
export function leafShape(node: Node): BlockShape {
|
||
return { node, header: nodeSpan(node), children: [] }
|
||
}
|
||
|
||
export function leafBlock(kind: TextKind, node: Node): CodeBlock {
|
||
return { kind, ...leafShape(node) }
|
||
}
|
||
|
||
/** 头部往前扩到 from(装饰器)。 */
|
||
export function extendHeader<B extends CodeBlock>(block: B, from: Node): B {
|
||
return { ...block, header: { ...block.header, startIndex: from.startIndex, startRow: from.startPosition.row } }
|
||
}
|
||
|
||
function lastLeafBefore(node: Node, limit: number): Node | undefined {
|
||
for (let i = node.childCount - 1; i >= 0; i--) {
|
||
const child = node.child(i)
|
||
if (!child || child.startIndex >= limit || child.type === 'comment' || child.startIndex === child.endIndex)
|
||
continue
|
||
if (child.childCount === 0)
|
||
return child
|
||
const found = lastLeafBefore(child, limit)
|
||
if (found)
|
||
return found
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/** 复合语句的头部:从 outer 开头到主体开始之前最后一个非注释的词为止。 */
|
||
export function headerSpan(outer: Node, bodyStart: number): SourceSpan {
|
||
const last = lastLeafBefore(outer, bodyStart)
|
||
if (!last)
|
||
return nodeSpan(outer)
|
||
return {
|
||
startIndex: outer.startIndex,
|
||
endIndex: last.endIndex,
|
||
startRow: outer.startPosition.row,
|
||
endRow: last.endPosition.row,
|
||
}
|
||
}
|
||
|
||
/** node 里完全落在 [from, to) 之间的注释。 */
|
||
function commentsIn(node: Node, from: number, to: number): Node[] {
|
||
if (node.endIndex <= from || node.startIndex >= to)
|
||
return []
|
||
if (node.type === 'comment')
|
||
return node.startIndex >= from && node.endIndex <= to ? [node] : []
|
||
return children(node).flatMap(child => commentsIn(child, from, to))
|
||
}
|
||
|
||
export type ListBlocks = (items: Node[], afterRow: number) => CodeBlock[]
|
||
|
||
/**
|
||
* 复合语句块(不含种类):头部到 bodyStart 之前,子块来自 items。
|
||
* 头部和主体之间的注释(例如 Python 冒号后、主体前的注释)也归子块。
|
||
*/
|
||
export function compoundShape(outer: Node, bodyStart: number, items: Node[], list: ListBlocks): BlockShape {
|
||
const header = headerSpan(outer, bodyStart)
|
||
const between = commentsIn(outer, header.endIndex, bodyStart)
|
||
return { node: outer, header, children: list([...between, ...items], header.endRow) }
|
||
}
|
||
|
||
/** 语言怎么认注释。 */
|
||
export interface CommentRules {
|
||
/** 行注释:相邻几行合成一块。 */
|
||
isLine: (comment: Node) => boolean
|
||
/** 文档注释(JSDoc):紧贴在函数、类块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc。 */
|
||
isDoc: (comment: Node) => boolean
|
||
}
|
||
|
||
/** 函数、类块带上说明:说明进 facts.doc,头部往上延到它开头。 */
|
||
function documented<B extends DocumentedBlock>(block: B, doc: Node): B {
|
||
return { ...extendHeader(block, doc), facts: { ...block.facts, doc } }
|
||
}
|
||
|
||
/**
|
||
* 一串语句 → 块。注释单独成块,相邻行的行注释合成一块;
|
||
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
|
||
* 文档注释和下一条语句之间只有空白、下一条语句是函数或类块时,它是这个函数、类的说明,不单独成块;
|
||
* 中间隔着的装饰器(不出块的语句)不算隔开。
|
||
* afterRow 是这串语句之前的最后一行(头部结尾行),模块顶层传 -1;
|
||
* toBlocks 可以返回空数组:这条语句不出块(装饰器、并进上一块的属性 docstring),和它同一行的注释照样算行尾注释。
|
||
*/
|
||
export function listBlocks(
|
||
items: Node[],
|
||
afterRow: number,
|
||
toBlocks: (item: Node) => CodeBlock[],
|
||
comments: CommentRules,
|
||
): CodeBlock[] {
|
||
const out: CodeBlock[] = []
|
||
let lastRow = afterRow
|
||
let lineRun: CodeBlock | undefined
|
||
let doc: Node | undefined
|
||
const flushDoc = (): void => {
|
||
if (doc)
|
||
out.push(leafBlock('comment', doc))
|
||
doc = undefined
|
||
}
|
||
items.forEach((item, i) => {
|
||
if (item.type === 'comment') {
|
||
if (item.startPosition.row === lastRow)
|
||
return
|
||
flushDoc()
|
||
const line = comments.isLine(item)
|
||
const next = items[i + 1]
|
||
if (line && lineRun && item.startPosition.row === lineRun.header.endRow + 1) {
|
||
lineRun.header = { ...lineRun.header, endIndex: item.endIndex, endRow: item.endPosition.row }
|
||
}
|
||
else if (comments.isDoc(item) && next && next.type !== 'comment' && sameNode(item.nextSibling, next)) {
|
||
doc = item
|
||
lineRun = undefined
|
||
}
|
||
else {
|
||
const block = leafBlock('comment', item)
|
||
out.push(block)
|
||
lineRun = line ? block : undefined
|
||
}
|
||
}
|
||
else {
|
||
const blocks = toBlocks(item)
|
||
const [head] = blocks
|
||
if ((head?.kind === 'function' || head?.kind === 'class' || head?.kind === 'enum') && doc) {
|
||
blocks[0] = documented(head, doc)
|
||
doc = undefined
|
||
}
|
||
else if (head) {
|
||
flushDoc()
|
||
}
|
||
out.push(...blocks)
|
||
lineRun = undefined
|
||
}
|
||
lastRow = item.endPosition.row
|
||
})
|
||
flushDoc()
|
||
return out
|
||
}
|
||
|
||
/** 同一个节点:web-tree-sitter 每次取子节点都会新建对象,只能按位置和类型比。 */
|
||
export function sameNode(a: Node | null | undefined, b: Node): boolean {
|
||
return !!a && a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type
|
||
}
|
||
|
||
// ─── 异常类 ───────────────────────────────────────────────────────────────────
|
||
|
||
/** 语言怎么认异常类、类主体里哪些子块是类属性、哪个是构造函数。 */
|
||
export interface ExceptionRules {
|
||
/** 父类一项写的类名(点分写法取最后一节);关键字参数、展开、调用这类不是类名的项没有。 */
|
||
baseName: (base: BaseFacts) => string | undefined
|
||
/** 光看名字就认得是异常类:语言自带的异常类,或按命名习惯以 Error 这类后缀结尾。 */
|
||
isExceptionName: (name: string) => boolean
|
||
/** 类主体里的子块是类属性时它的名字、类型、值;不是时没有。 */
|
||
field: (block: CodeBlock) => Omit<FieldFacts, 'docs'> | undefined
|
||
/** 子块是构造函数时它的参数(Python 去掉 self);不是时没有。 */
|
||
constructorParameters: (block: CodeBlock) => ParameterFacts[] | undefined
|
||
}
|
||
|
||
function classBlocks(blocks: readonly CodeBlock[]): ClassBlock[] {
|
||
return blocks.flatMap(block => [...(block.kind === 'class' ? [block] : []), ...classBlocks(block.children)])
|
||
}
|
||
|
||
/** 紧挨在 child 前面的注释块(连着几块都算,中间没隔别的子块),照源码顺序:讲的是它下面的这个 child。 */
|
||
function leadingComments(children: readonly CodeBlock[], child: CodeBlock): CodeBlock[] {
|
||
const at = children.indexOf(child)
|
||
let from = at
|
||
while (from > 0 && children[from - 1].kind === 'comment')
|
||
from--
|
||
return children.slice(from, at)
|
||
}
|
||
|
||
/** 注释块里的各条注释(几条行注释连成的一块是好几个节点);Python 当注释用的字符串是那个字符串。 */
|
||
function commentNodes(block: CodeBlock): Node[] {
|
||
if (block.node.type !== 'comment')
|
||
return syntaxChildren(block.node).slice(0, 1)
|
||
const nodes: Node[] = []
|
||
for (let node: Node | null = block.node; node?.type === 'comment' && node.endIndex <= block.header.endIndex; node = node.nextSibling)
|
||
nodes.push(node)
|
||
return nodes
|
||
}
|
||
|
||
/** 块自己的说明节点:属性表块的 facts.doc,一行译文块的 doc(Python 字段的属性 docstring)。 */
|
||
function ownDoc(block: CodeBlock): Node | undefined {
|
||
if (!('facts' in block))
|
||
return block.doc
|
||
switch (block.kind) {
|
||
case 'function':
|
||
case 'class':
|
||
case 'trait':
|
||
case 'impl':
|
||
case 'module':
|
||
case 'enum':
|
||
case 'exception':
|
||
case 'const':
|
||
case 'variable':
|
||
return block.facts.doc
|
||
default:
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
/** 能重写的那段注释:注释块是整块;函数、类、枚举、异常、常量、变量是它的说明。没有说明就没有。 */
|
||
export function commentSpan(block: CodeBlock): { startLine: number, endLine: number } | undefined {
|
||
if (block.kind === 'comment')
|
||
return { startLine: block.header.startRow, endLine: block.header.endRow }
|
||
const doc = ownDoc(block)
|
||
if (!doc)
|
||
return undefined
|
||
return { startLine: doc.startPosition.row, endLine: doc.endPosition.row }
|
||
}
|
||
|
||
/** 类属性的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
|
||
function fieldDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] {
|
||
const trailing = block.node.nextNamedSibling
|
||
const doc = ownDoc(block)
|
||
return [
|
||
...leadingComments(children, block).flatMap(commentNodes),
|
||
...(trailing?.type === 'comment' && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []),
|
||
...(doc ? [doc] : []),
|
||
]
|
||
}
|
||
|
||
/**
|
||
* 类块换成异常块:类属性连同紧挨在它前面的注释块并进属性表,构造函数(写了几个取最后一个,重载的实现写在最后)的参数进属性表,
|
||
* 留下的子块(方法、构造函数本身、别的语句)不管写在哪都照源码顺序留作子块;头部是整个类(到它连同各子块的最后一行),
|
||
* 「编辑」时整段一起改,webview 在它编辑时不画留下的子块。
|
||
*/
|
||
function exceptionBlock(block: ClassBlock, rules: ExceptionRules): ExceptionBlock {
|
||
const { children, facts } = block
|
||
const fields: FieldFacts[] = []
|
||
const members: CodeBlock[] = []
|
||
let parameters: ParameterFacts[] | undefined
|
||
for (const child of children) {
|
||
const field = rules.field(child)
|
||
if (field) {
|
||
fields.push({ ...field, docs: fieldDocs(children, child) })
|
||
members.push(child)
|
||
}
|
||
parameters = rules.constructorParameters(child) ?? parameters
|
||
}
|
||
const absorbed = new Set(members.flatMap(member => [...leadingComments(children, member), member]))
|
||
const ends = [block.header, nodeSpan(block.node), ...children.flatMap(child => [child.header, nodeSpan(child.node)])]
|
||
const last = ends.reduce((end, span) => span.endIndex > end.endIndex ? span : end)
|
||
return {
|
||
kind: 'exception',
|
||
node: block.node,
|
||
header: { ...block.header, endIndex: last.endIndex, endRow: last.endRow },
|
||
children: children.filter(child => !absorbed.has(child)),
|
||
...(block.attributes && { attributes: block.attributes }),
|
||
facts: {
|
||
name: facts.name,
|
||
doc: facts.doc,
|
||
access: facts.access,
|
||
decorators: facts.decorators,
|
||
bases: facts.bases,
|
||
fields,
|
||
...(parameters && { parameters }),
|
||
},
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 认出异常类,把它们的类块换成异常块(见 exceptionBlock)。父类里有一项是异常类就是异常类:光看名字认得出(isExceptionName),
|
||
* 或是同一文件里(不论写在前面还是后面、嵌套在哪)认出来的异常类,一层层往下传。父类从别的文件导入、名字又不合命名习惯的认不出来。
|
||
*/
|
||
export function withExceptions(blocks: CodeBlock[], rules: ExceptionRules): CodeBlock[] {
|
||
const classes = classBlocks(blocks)
|
||
const local = new Set<string>()
|
||
const isException = (facts: ClassFacts): boolean => facts.bases.some((base) => {
|
||
const name = rules.baseName(base)
|
||
return name !== undefined && (rules.isExceptionName(name) || local.has(name))
|
||
})
|
||
let grew = true
|
||
while (grew) {
|
||
grew = false
|
||
for (const { facts } of classes) {
|
||
const name = facts.name?.text
|
||
if (name !== undefined && !local.has(name) && isException(facts)) {
|
||
local.add(name)
|
||
grew = true
|
||
}
|
||
}
|
||
}
|
||
const convert = (list: CodeBlock[]): CodeBlock[] => list.map((block) => {
|
||
const children = convert(block.children)
|
||
const same = children.every((child, i) => child === block.children[i])
|
||
const next: CodeBlock = same ? block : { ...block, children }
|
||
return next.kind === 'class' && isException(next.facts) ? exceptionBlock(next, rules) : next
|
||
})
|
||
return convert(blocks)
|
||
}
|