langium
Version:
A language engineering tool for the Language Server Protocol
298 lines (252 loc) • 9.66 kB
text/typescript
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import type { IToken, TokenType } from 'chevrotain';
import type { Range } from 'vscode-languageserver-types';
import type { AbstractElement } from '../languages/generated/ast.js';
import type { AstNode, CompositeCstNode, CstNode, LeafCstNode, RootCstNode } from '../syntax-tree.js';
import { Position } from 'vscode-languageserver-types';
import { tokenToRange } from '../utils/cst-utils.js';
export class CstNodeBuilder {
private rootNode!: RootCstNodeImpl;
private nodeStack: CompositeCstNodeImpl[] = [];
get current(): CompositeCstNodeImpl {
return this.nodeStack[this.nodeStack.length - 1] ?? this.rootNode;
}
buildRootNode(input: string): RootCstNode {
this.rootNode = new RootCstNodeImpl(input);
this.rootNode.root = this.rootNode;
this.nodeStack = [this.rootNode];
return this.rootNode;
}
buildCompositeNode(feature: AbstractElement): CompositeCstNode {
const compositeNode = new CompositeCstNodeImpl();
compositeNode.grammarSource = feature;
compositeNode.root = this.rootNode;
this.current.content.push(compositeNode);
this.nodeStack.push(compositeNode);
return compositeNode;
}
buildLeafNode(token: IToken, feature?: AbstractElement): LeafCstNode {
const leafNode = new LeafCstNodeImpl(token.startOffset, token.image.length, tokenToRange(token), token.tokenType, !feature);
leafNode.grammarSource = feature;
leafNode.root = this.rootNode;
this.current.content.push(leafNode);
return leafNode;
}
removeNode(node: CstNode): void {
const parent = node.container;
if (parent) {
const index = parent.content.indexOf(node);
if (index >= 0) {
parent.content.splice(index, 1);
}
}
}
addHiddenNodes(tokens: IToken[]): void {
const nodes: LeafCstNode[] = [];
for (const token of tokens) {
const leafNode = new LeafCstNodeImpl(token.startOffset, token.image.length, tokenToRange(token), token.tokenType, true);
leafNode.root = this.rootNode;
nodes.push(leafNode);
}
let current: CompositeCstNode = this.current;
let added = false;
// If we are within a composite node, we add the hidden nodes to the content
if (current.content.length > 0) {
current.content.push(...nodes);
return;
}
// Otherwise we are at a newly created node
// Instead of adding the hidden nodes here, we search for the first parent node with content
while (current.container) {
const index = current.container.content.indexOf(current);
if (index > 0) {
// Add the hidden nodes before the current node
current.container.content.splice(index, 0, ...nodes);
added = true;
break;
}
current = current.container;
}
// If we arrive at the root node, we add the hidden nodes at the beginning
// This is the case if the hidden nodes are the first nodes in the tree
if (!added) {
this.rootNode.content.unshift(...nodes);
}
}
construct(item: { $type: string | symbol | undefined, $cstNode: CstNode }): void {
const current: CstNode = this.current;
// The specified item could be a datatype ($type is symbol) or a fragment ($type is undefined)
// Only if the $type is a string, we actually assign the element
if (typeof item.$type === 'string') {
this.current.astNode = <AstNode>item;
}
item.$cstNode = current;
const node = this.nodeStack.pop();
// Empty composite nodes are not valid
// Simply remove the node from the tree
if (node?.content.length === 0) {
this.removeNode(node);
}
}
}
export abstract class AbstractCstNode implements CstNode {
abstract get offset(): number;
abstract get length(): number;
abstract get end(): number;
abstract get range(): Range;
container?: CompositeCstNode;
grammarSource?: AbstractElement;
root: RootCstNode;
private _astNode?: AstNode;
/** @deprecated use `container` instead. */
get parent(): CompositeCstNode | undefined {
return this.container;
}
/** @deprecated use `grammarSource` instead. */
get feature(): AbstractElement | undefined {
return this.grammarSource;
}
get hidden(): boolean {
return false;
}
get astNode(): AstNode {
const node = typeof this._astNode?.$type === 'string' ? this._astNode : this.container?.astNode;
if (!node) {
throw new Error('This node has no associated AST element');
}
return node;
}
set astNode(value: AstNode | undefined) {
this._astNode = value;
}
/** @deprecated use `astNode` instead. */
get element(): AstNode {
return this.astNode;
}
get text(): string {
return this.root.fullText.substring(this.offset, this.end);
}
}
export class LeafCstNodeImpl extends AbstractCstNode implements LeafCstNode {
get offset(): number {
return this._offset;
}
get length(): number {
return this._length;
}
get end(): number {
return this._offset + this._length;
}
override get hidden(): boolean {
return this._hidden;
}
get tokenType(): TokenType {
return this._tokenType;
}
get range(): Range {
return this._range;
}
private _hidden: boolean;
private _offset: number;
private _length: number;
private _range: Range;
private _tokenType: TokenType;
constructor(offset: number, length: number, range: Range, tokenType: TokenType, hidden = false) {
super();
this._hidden = hidden;
this._offset = offset;
this._tokenType = tokenType;
this._length = length;
this._range = range;
}
}
export class CompositeCstNodeImpl extends AbstractCstNode implements CompositeCstNode {
readonly content: CstNode[] = new CstNodeContainer(this);
private _rangeCache?: Range;
/** @deprecated use `content` instead. */
get children(): CstNode[] {
return this.content;
}
get offset(): number {
return this.firstNonHiddenNode?.offset ?? 0;
}
get length(): number {
return this.end - this.offset;
}
get end(): number {
return this.lastNonHiddenNode?.end ?? 0;
}
get range(): Range {
const firstNode = this.firstNonHiddenNode;
const lastNode = this.lastNonHiddenNode;
if (firstNode && lastNode) {
if (this._rangeCache === undefined) {
const { range: firstRange } = firstNode;
const { range: lastRange } = lastNode;
this._rangeCache = { start: firstRange.start, end: lastRange.end.line < firstRange.start.line ? firstRange.start : lastRange.end };
}
return this._rangeCache;
} else {
return { start: Position.create(0, 0), end: Position.create(0, 0) };
}
}
private get firstNonHiddenNode(): CstNode | undefined {
for (const child of this.content) {
if (!child.hidden) {
return child;
}
}
return this.content[0];
}
private get lastNonHiddenNode(): CstNode | undefined {
for (let i = this.content.length - 1; i >= 0; i--) {
const child = this.content[i];
if (!child.hidden) {
return child;
}
}
return this.content[this.content.length - 1];
}
}
class CstNodeContainer extends Array<CstNode> {
readonly parent: CompositeCstNode;
constructor(parent: CompositeCstNode) {
super();
this.parent = parent;
Object.setPrototypeOf(this, CstNodeContainer.prototype);
}
override push(...items: CstNode[]): number {
this.addParents(items);
return super.push(...items);
}
override unshift(...items: CstNode[]): number {
this.addParents(items);
return super.unshift(...items);
}
override splice(start: number, count: number, ...items: CstNode[]): CstNode[] {
this.addParents(items);
return super.splice(start, count, ...items);
}
private addParents(items: CstNode[]): void {
for (const item of items) {
(<AbstractCstNode>item).container = this.parent;
}
}
}
export class RootCstNodeImpl extends CompositeCstNodeImpl implements RootCstNode {
private _text = '';
override get text(): string {
return this._text.substring(this.offset, this.end);
}
get fullText(): string {
return this._text;
}
constructor(input?: string) {
super();
this._text = input ?? '';
}
}