169 lines
6.0 KiB
TypeScript
169 lines
6.0 KiB
TypeScript
import type { BlockShape, CodeBlock, DocumentedBlock, Node, 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 的 first 表示它是这串里第一条非注释语句(Python 认 docstring 用)。
|
|
*/
|
|
export function listBlocks(
|
|
items: Node[],
|
|
afterRow: number,
|
|
toBlocks: (item: Node, first: boolean) => CodeBlock[],
|
|
comments: CommentRules,
|
|
): CodeBlock[] {
|
|
const out: CodeBlock[] = []
|
|
let lastRow = afterRow
|
|
let lineRun: CodeBlock | undefined
|
|
let doc: Node | undefined
|
|
let first = true
|
|
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, first)
|
|
const [head] = blocks
|
|
if ((head?.kind === 'function' || head?.kind === 'class') && doc) {
|
|
blocks[0] = documented(head, doc)
|
|
doc = undefined
|
|
}
|
|
else if (head) {
|
|
flushDoc()
|
|
}
|
|
out.push(...blocks)
|
|
first = false
|
|
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
|
|
}
|