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(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(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 | 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() 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) }