langium
Version:
A language engineering tool for the Language Server Protocol
342 lines (313 loc) • 11.8 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 } from '@chevrotain/types';
import type { Range } from 'vscode-languageserver-types';
import type { CstNode, CompositeCstNode, LeafCstNode } from '../syntax-tree.js';
import type { DocumentSegment } from '../workspace/documents.js';
import type { Stream, TreeStream } from './stream.js';
import { isCompositeCstNode, isLeafCstNode, isRootCstNode } from '../syntax-tree.js';
import { TreeStreamImpl } from './stream.js';
/**
* Create a stream of all CST nodes that are directly and indirectly contained in the given root node,
* including the root node itself.
*/
export function streamCst(node: CstNode): TreeStream<CstNode> {
return new TreeStreamImpl(node, element => {
if (isCompositeCstNode(element)) {
return element.content;
} else {
return [];
}
}, { includeRoot: true });
}
/**
* Create a stream of all leaf nodes that are directly and indirectly contained in the given root node.
*/
export function flattenCst(node: CstNode): Stream<LeafCstNode> {
return streamCst(node).filter(isLeafCstNode);
}
/**
* Determines whether the specified cst node is a child of the specified parent node.
*/
export function isChildNode(child: CstNode, parent: CstNode): boolean {
while (child.container) {
child = child.container;
if (child === parent) {
return true;
}
}
return false;
}
export function tokenToRange(token: IToken): Range {
// Chevrotain uses 1-based indices everywhere
// So we subtract 1 from every value to align with the LSP
return {
start: {
character: token.startColumn! - 1,
line: token.startLine! - 1
},
end: {
character: token.endColumn!, // endColumn uses the correct index
line: token.endLine! - 1
}
};
}
export function toDocumentSegment(node: CstNode): DocumentSegment;
export function toDocumentSegment(node?: CstNode): DocumentSegment | undefined;
export function toDocumentSegment(node?: CstNode): DocumentSegment | undefined {
if (!node) {
return undefined;
}
const { offset, end, range } = node;
return {
range,
offset,
end,
length: end - offset
};
}
export enum RangeComparison {
Before = 0,
After = 1,
OverlapFront = 2,
OverlapBack = 3,
Inside = 4,
Outside = 5,
}
export function compareRange(range: Range, to: Range): RangeComparison {
if (range.end.line < to.start.line || (range.end.line === to.start.line && range.end.character <= to.start.character)) {
return RangeComparison.Before;
} else if (range.start.line > to.end.line || (range.start.line === to.end.line && range.start.character >= to.end.character)) {
return RangeComparison.After;
}
const startInside = range.start.line > to.start.line || (range.start.line === to.start.line && range.start.character >= to.start.character);
const endInside = range.end.line < to.end.line || (range.end.line === to.end.line && range.end.character <= to.end.character);
if (startInside && endInside) {
return RangeComparison.Inside;
} else if (startInside) {
return RangeComparison.OverlapBack;
} else if (endInside) {
return RangeComparison.OverlapFront;
} else {
return RangeComparison.Outside;
}
}
export function inRange(range: Range, to: Range): boolean {
const comparison = compareRange(range, to);
return comparison > RangeComparison.After;
}
// The \p{L} regex matches any unicode letter character, i.e. characters from non-english alphabets
// Together with \w it matches any kind of character which can commonly appear in IDs
export const DefaultNameRegexp = /^[\w\p{L}]$/u;
/**
* Performs `findLeafNodeAtOffset` with a minor difference: When encountering a character that matches the `nameRegexp` argument,
* it will instead return the leaf node at the `offset - 1` position.
*
* For LSP services, users expect that the declaration of an element is available if the cursor is directly after the element.
*/
export function findDeclarationNodeAtOffset(cstNode: CstNode | undefined, offset: number, nameRegexp = DefaultNameRegexp): LeafCstNode | undefined {
if (cstNode) {
if (offset > 0) {
const localOffset = offset - cstNode.offset;
const textAtOffset = cstNode.text.charAt(localOffset);
if (!nameRegexp.test(textAtOffset)) {
offset--;
}
}
return findLeafNodeAtOffset(cstNode, offset);
}
return undefined;
}
export function findCommentNode(cstNode: CstNode | undefined, commentNames: string[]): CstNode | undefined {
if (cstNode) {
const previous = getPreviousNode(cstNode, true);
if (previous && isCommentNode(previous, commentNames)) {
return previous;
}
if (isRootCstNode(cstNode)) {
// Go from the first non-hidden node through all nodes in reverse order
// We do this to find the comment node which directly precedes the root node
const endIndex = cstNode.content.findIndex(e => !e.hidden);
for (let i = endIndex - 1; i >= 0; i--) {
const child = cstNode.content[i];
if (isCommentNode(child, commentNames)) {
return child;
}
}
}
}
return undefined;
}
export function isCommentNode(cstNode: CstNode, commentNames: string[]): boolean {
return isLeafCstNode(cstNode) && commentNames.includes(cstNode.tokenType.name);
}
/**
* Finds the leaf CST node at the specified 0-based string offset.
* Note that the given offset will be within the range of the returned leaf node.
*
* If the offset does not point to a CST node (but just white space), this method will return `undefined`.
*
* @param node The CST node to search through.
* @param offset The specified offset.
* @returns The CST node at the specified offset.
*/
export function findLeafNodeAtOffset(node: CstNode, offset: number): LeafCstNode | undefined {
if (isLeafCstNode(node)) {
return node;
} else if (isCompositeCstNode(node)) {
const searchResult = binarySearch(node, offset, false);
if (searchResult) {
return findLeafNodeAtOffset(searchResult, offset);
}
}
return undefined;
}
/**
* Finds the leaf CST node at the specified 0-based string offset.
* If no CST node exists at the specified position, it will return the leaf node before it.
*
* If there is no leaf node before the specified offset, this method will return `undefined`.
*
* @param node The CST node to search through.
* @param offset The specified offset.
* @returns The CST node closest to the specified offset.
*/
export function findLeafNodeBeforeOffset(node: CstNode, offset: number): LeafCstNode | undefined {
if (isLeafCstNode(node)) {
return node;
} else if (isCompositeCstNode(node)) {
const searchResult = binarySearch(node, offset, true);
if (searchResult) {
return findLeafNodeBeforeOffset(searchResult, offset);
}
}
return undefined;
}
function binarySearch(node: CompositeCstNode, offset: number, closest: boolean): CstNode | undefined {
let left = 0;
let right = node.content.length - 1;
let closestNode: CstNode | undefined = undefined;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
const middleNode = node.content[middle];
if (middleNode.offset <= offset && middleNode.end > offset) {
// Found an exact match
return middleNode;
}
if (middleNode.end <= offset) {
// Update the closest node (less than offset) and move to the right half
closestNode = closest ? middleNode : undefined;
left = middle + 1;
} else {
// Move to the left half
right = middle - 1;
}
}
return closestNode;
}
export function getPreviousNode(node: CstNode, hidden = true): CstNode | undefined {
while (node.container) {
const parent = node.container;
let index = parent.content.indexOf(node);
while (index > 0) {
index--;
const previous = parent.content[index];
if (hidden || !previous.hidden) {
return previous;
}
}
node = parent;
}
return undefined;
}
export function getNextNode(node: CstNode, hidden = true): CstNode | undefined {
while (node.container) {
const parent = node.container;
let index = parent.content.indexOf(node);
const last = parent.content.length - 1;
while (index < last) {
index++;
const next = parent.content[index];
if (hidden || !next.hidden) {
return next;
}
}
node = parent;
}
return undefined;
}
export function getStartlineNode(node: CstNode): CstNode {
if (node.range.start.character === 0) {
return node;
}
const line = node.range.start.line;
let last = node;
let index: number | undefined;
while (node.container) {
const parent = node.container;
const selfIndex = index ?? parent.content.indexOf(node);
if (selfIndex === 0) {
node = parent;
index = undefined;
} else {
index = selfIndex - 1;
node = parent.content[index];
}
if (node.range.start.line !== line) {
break;
}
last = node;
}
return last;
}
export function getInteriorNodes(start: CstNode, end: CstNode): CstNode[] {
const commonParent = getCommonParent(start, end);
if (!commonParent) {
return [];
}
return commonParent.parent.content.slice(commonParent.a + 1, commonParent.b);
}
function getCommonParent(a: CstNode, b: CstNode): CommonParent | undefined {
const aParents = getParentChain(a);
const bParents = getParentChain(b);
let current: CommonParent | undefined;
for (let i = 0; i < aParents.length && i < bParents.length; i++) {
const aParent = aParents[i];
const bParent = bParents[i];
if (aParent.parent === bParent.parent) {
current = {
parent: aParent.parent,
a: aParent.index,
b: bParent.index
};
} else {
break;
}
}
return current;
}
interface CommonParent {
parent: CompositeCstNode
a: number
b: number
}
function getParentChain(node: CstNode): ParentLink[] {
const chain: ParentLink[] = [];
while (node.container) {
const parent = node.container;
const index = parent.content.indexOf(node);
chain.push({
parent,
index
});
node = parent;
}
return chain.reverse();
}
interface ParentLink {
parent: CompositeCstNode
index: number
}