Files
superwork/vscode/src/codeReader/languages/shared.ts
T

348 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}
const COMMENT_TYPES = new Set(['comment', 'line_comment', 'block_comment'])
/** 注释节点:多数语法包叫 comment,Rust 分行注释 line_comment 和块注释 block_comment。 */
export function isCommentNode(node: Node): boolean {
return COMMENT_TYPES.has(node.type)
}
/** 注释节点的范围,不算末尾的换行:Rust 的 `///`、`//!` 把行尾换行也算进节点,不去掉就多占下一行。 */
function commentNodeSpan(comment: Node): SourceSpan {
const span = nodeSpan(comment)
const newline = /\r?\n$/.exec(comment.text)?.[0].length ?? 0
return newline ? { ...span, endIndex: span.endIndex - newline, endRow: span.endRow - 1 } : span
}
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 || isCommentNode(child) || 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 (isCommentNode(node))
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 {
/**
* 行注释的写法(`//`、`#`),块注释没有:相邻行同一写法的行注释合成一块。
* 写法不同的不合,例如 Rust 的 `//`、讲下一项的 `///`、讲所在模块的 `//!` 各是一种。
*/
lineStyle: (comment: Node) => string | undefined
/**
* 文档注释(JSDoc、Rust 的 `///`):紧贴在 documents 这几种块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc;
* 连着几行的行文档注释合成一段,第一条注释节点是说明。
*/
isDoc: (comment: Node) => boolean
/** 文档注释能当哪几种块的说明。 */
documents: ReadonlySet<DocumentedBlock['kind']>
}
/** 块带上说明:说明进 facts.doc,头部往上延到它开头。 */
function documented<B extends DocumentedBlock>(block: B, doc: Node): B {
return { ...extendHeader(block, doc), facts: { ...block.facts, doc } }
}
function documentable(block: CodeBlock, kinds: ReadonlySet<string>): block is DocumentedBlock {
return kinds.has(block.kind)
}
/**
* 一串语句 → 块。注释单独成块,相邻行同一写法的行注释合成一块(见 CommentRules.lineStyle);
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
* 文档注释(连着几行的算一段)和下一条语句之间只有空白、下一条语句是 CommentRules.documents 里的块时,它是这一块的说明,不单独成块;
* 中间隔着的装饰器(不出块的语句)不算隔开。
* 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 run: { block: CodeBlock, style: string } | undefined
/** 还没定归属的文档注释:下一条语句能带说明就是它的说明,不然单独成块。 */
let doc: CodeBlock | undefined
const flushDoc = (): void => {
if (doc)
out.push(doc)
doc = undefined
}
/** 接在 run 下一行、写法相同的行注释。 */
const continues = (comment: Node): boolean =>
!!run && comments.lineStyle(comment) === run.style && comment.startPosition.row === run.block.header.endRow + 1
items.forEach((item, i) => {
if (isCommentNode(item)) {
const span = commentNodeSpan(item)
if (span.startRow === lastRow)
return
if (run && continues(item)) {
run.block.header = { ...run.block.header, endIndex: span.endIndex, endRow: span.endRow }
}
else {
flushDoc()
const block: CodeBlock = { kind: 'comment', node: item, header: span, children: [] }
const style = comments.lineStyle(item)
run = style === undefined ? undefined : { block, style }
if (comments.isDoc(item))
doc = block
else
out.push(block)
}
// 文档注释后面紧跟的得是下一条语句或它自己的下一行,隔着别的注释就只是一块注释
const next = items[i + 1]
if (!next || !sameNode(item.nextSibling, next) || (isCommentNode(next) && !continues(next)))
flushDoc()
lastRow = span.endRow
return
}
const blocks = toBlocks(item)
const [head] = blocks
if (head && doc && documentable(head, comments.documents)) {
blocks[0] = documented(head, doc.node)
doc = undefined
}
else if (head) {
flushDoc()
}
out.push(...blocks)
run = 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 (!isCommentNode(block.node))
return syntaxChildren(block.node).slice(0, 1)
const nodes: Node[] = []
for (let node: Node | null = block.node; node && isCommentNode(node) && node.startIndex < 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 && isCommentNode(trailing) && 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)
}