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

315 lines
13 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,
}
}
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)
}