2025-04-01 10:38:02 +09:00

466 lines
12 KiB
TypeScript

import { SyntaxNode, Tree } from '@lezer/common';
import {
Aggregate,
And,
AttributeField,
ComparisonOp,
FieldExpression,
FieldOp,
GroupOperation,
IntrinsicField,
Or,
parser,
Pipe,
ScalarFilter,
SelectArgs,
SelectOperation,
SpansetFilter,
SpansetPipeline,
SpansetPipelineExpression,
Static,
String as StringNode,
TraceQL,
} from '@grafana/lezer-traceql';
type Direction = 'parent' | 'firstChild' | 'lastChild' | 'nextSibling' | 'prevSibling';
type NodeType = number;
export type Situation = { query: string } & SituationType;
export type SituationType =
| {
type: 'UNKNOWN';
}
| {
type: 'EMPTY';
}
| {
type: 'SPANSET_EMPTY';
}
| {
type: 'SPANSET_ONLY_DOT';
}
| {
type: 'SPANSET_EXPRESSION_OPERATORS';
}
| {
type: 'SPANFIELD_COMBINING_OPERATORS';
}
| {
type: 'SPANSET_IN_NAME';
}
| {
type: 'SPANSET_IN_NAME_SCOPE';
scope: string;
}
| {
type: 'SPANSET_IN_VALUE';
tagName: string;
betweenQuotes: boolean;
}
| {
type: 'SPANSET_AFTER_VALUE';
}
| {
type: 'SPANSET_COMBINING_OPERATORS';
}
| {
type: 'SPANSET_PIPELINE_AFTER_OPERATOR';
}
| {
type: 'SPANSET_IN_THE_MIDDLE';
}
| {
type: 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE';
}
| {
type: 'NEW_SPANSET';
}
| {
type: 'ATTRIBUTE_FOR_FUNCTION';
}
| {
type: 'SPANSET_COMPARISON_OPERATORS';
};
type Path = Array<[Direction, NodeType[]]>;
type Resolver = {
path: NodeType[];
fun: (node: SyntaxNode, text: string, pos: number, originalPos: number) => SituationType | void;
};
function getErrorNode(tree: Tree, cursorPos: number): SyntaxNode | null {
const cur = tree.cursorAt(cursorPos);
do {
if (cur.from === cursorPos || cur.to === cursorPos) {
const { node } = cur;
if (node.type.isError) {
return node;
}
}
} while (cur.next());
return null;
}
function move(node: SyntaxNode, direction: Direction): SyntaxNode | null {
return node[direction];
}
function walk(node: SyntaxNode, path: Path): SyntaxNode | null {
let current: SyntaxNode | null = node;
for (const [direction, expectedNodeIDs] of path) {
current = move(current, direction);
if (current === null) {
// we could not move in the direction, we stop
return null;
}
// note that the found value can be 0, which is acceptable
if (expectedNodeIDs.find((id) => id === current?.type.id) === undefined) {
// the reached node has wrong type, we stop
return null;
}
}
return current;
}
function getNodeText(node: SyntaxNode, text: string): string {
// if the from and to are them same (e.g. for an error node) we can subtract 1 from the start/from index
return text.slice(node.from === node.to ? node.from - 1 : node.from, node.to);
}
function isPathMatch(resolverPath: NodeType[], cursorPath: number[]): boolean {
return resolverPath.every((item, index) => item === cursorPath[index]);
}
/**
* Figure out where is the cursor and what kind of suggestions are appropriate.
* @param text the user input
* @param offset the position of the cursor (starting from 0) in the user input
*/
export function getSituation(text: string, offset: number): Situation | null {
// there is a special case when we are at the start of writing text,
// so we handle that case first
if (text === '') {
return {
query: text,
type: 'EMPTY',
};
}
const tree = parser.parse(text);
// Whitespaces (especially when multiple) on the left of the text cursor can trick the Lezer parser,
// causing a wrong tree cursor to be picked.
// Example: `{ span.foo = ↓ }`, with `↓` being the cursor, tricks the parser.
// Quick and dirty hack: Shift the cursor to the left until we find a non-whitespace character on its left.
let shiftedOffset = offset;
while (shiftedOffset - 1 >= 0 && text[shiftedOffset - 1] === ' ') {
shiftedOffset -= 1;
}
// If the tree contains error, it's probable that our node is one of those error nodes.
// If there are errors, the node lezer finds us might not be the best node.
// So, first we check if there is an error node at the cursor position.
let errorNode = getErrorNode(tree, shiftedOffset);
if (!errorNode) {
// Try again with the previous character.
errorNode = getErrorNode(tree, shiftedOffset - 1);
}
if (!errorNode) {
// Try again with the next character
errorNode = getErrorNode(tree, shiftedOffset + 1);
}
const cur = errorNode != null ? errorNode.cursor() : tree.cursorAt(shiftedOffset);
const currentNode = cur.node;
const ids = [cur.type.id];
while (cur.parent()) {
ids.push(cur.type.id);
}
let situationType: SituationType | void = undefined;
for (let resolver of RESOLVERS) {
if (isPathMatch(resolver.path, ids)) {
situationType = resolver.fun(currentNode, text, shiftedOffset, offset);
}
}
return { query: text, ...(situationType ?? { type: 'UNKNOWN' }) };
}
const ERROR_NODE_ID = 0;
const RESOLVERS: Resolver[] = [
// Curson on error node cases
{
path: [ERROR_NODE_ID, AttributeField],
fun: resolveAttribute,
},
{
path: [ERROR_NODE_ID, FieldExpression],
fun: resolveExpression,
},
{
path: [ERROR_NODE_ID, SpansetFilter],
fun: resolveSpansetWithNoClosedBrace,
},
{
path: [ERROR_NODE_ID, Aggregate],
fun: resolveAttributeForFunction,
},
{
path: [ERROR_NODE_ID, IntrinsicField],
fun: resolveAttributeForFunction,
},
{
path: [ERROR_NODE_ID, GroupOperation],
fun: resolveAttributeForFunction,
},
{
path: [ERROR_NODE_ID, SelectOperation],
fun: resolveAttributeForFunction,
},
{
path: [ERROR_NODE_ID, SpansetPipelineExpression],
fun: resolveSpansetPipeline,
},
{
path: [ERROR_NODE_ID, ScalarFilter, SpansetPipeline],
fun: resolveArithmeticOperator,
},
// Curson on valid node cases (the whole query could contain errors nevertheless)
{
path: [FieldExpression],
fun: resolveSpanset,
},
{
path: [SpansetFilter],
fun: resolveSpanset,
},
{
path: [SpansetPipelineExpression],
fun: resolveNewSpansetExpression,
},
{
path: [TraceQL],
fun: resolveNewSpansetExpression,
},
{
path: [StringNode, Static],
fun: resolveExpression,
},
];
const resolveAttributeCompletion = (node: SyntaxNode, text: string, pos: number): SituationType | void => {
// The user is completing an expression. We can take advantage of the fact that the Monaco editor is smart
// enough to automatically detect that there are some characters before the cursor and to take them into
// account when providing suggestions.
const getAttributeFieldUpToDot = (node: SyntaxNode) => {
const attributeFieldParent = walk(node, [['firstChild', [AttributeField]]]);
const attributeFieldParentText = attributeFieldParent ? getNodeText(attributeFieldParent, text) : '';
const indexOfDot = attributeFieldParentText.indexOf('.');
return attributeFieldParentText.slice(0, indexOfDot);
};
// If there is a space, for sure the attribute is completed and no suggestions to complete it should be provided
if (text[pos - 1] === ' ') {
return;
}
const endOfPathNode = walk(node, [['firstChild', [FieldExpression]]]);
if (endOfPathNode) {
return {
type: 'SPANSET_IN_NAME_SCOPE',
scope: getAttributeFieldUpToDot(endOfPathNode),
};
}
const endOfPathNode2 = walk(node, [
['parent', [SpansetFilter]],
['firstChild', [FieldExpression]],
]);
// In this case, we also need to check the character at `pos`
if (endOfPathNode2 && text[pos] !== ' ') {
return {
type: 'SPANSET_IN_NAME_SCOPE',
scope: getAttributeFieldUpToDot(endOfPathNode2),
};
}
};
function resolveSpanset(node: SyntaxNode, text: string, _: number, originalPos: number): SituationType {
const situation = resolveAttributeCompletion(node, text, originalPos);
if (situation) {
return situation;
}
let endOfPathNode = walk(node, [
['firstChild', [FieldExpression]],
['firstChild', [AttributeField]],
]);
if (endOfPathNode) {
return {
type: 'SPANSET_EXPRESSION_OPERATORS',
};
}
endOfPathNode = walk(node, [
['lastChild', [FieldExpression]],
['lastChild', [FieldExpression]],
['lastChild', [Static]],
]);
if (endOfPathNode) {
return {
type: 'SPANFIELD_COMBINING_OPERATORS',
};
}
endOfPathNode = walk(node, [['lastChild', [FieldExpression]]]);
if (endOfPathNode) {
return {
type: 'SPANSET_EXPRESSION_OPERATORS',
};
}
return {
type: 'SPANSET_EMPTY',
};
}
function resolveAttribute(node: SyntaxNode, text: string): SituationType {
const attributeFieldParent = walk(node, [['parent', [AttributeField]]]);
const attributeFieldParentText = attributeFieldParent ? getNodeText(attributeFieldParent, text) : '';
if (attributeFieldParentText === '.') {
return {
type: 'SPANSET_ONLY_DOT',
};
}
const indexOfDot = attributeFieldParentText.indexOf('.');
const attributeFieldUpToDot = attributeFieldParentText.slice(0, indexOfDot);
if (
['event', 'instrumentation', 'link', 'resource', 'span', 'parent'].find((item) => item === attributeFieldUpToDot)
) {
return {
type: 'SPANSET_IN_NAME_SCOPE',
scope: attributeFieldUpToDot,
};
}
return {
type: 'SPANSET_IN_NAME',
};
}
function resolveExpression(node: SyntaxNode, text: string, _: number, originalPos: number): SituationType {
const situation = resolveAttributeCompletion(node, text, originalPos);
if (situation) {
return situation;
}
if (
walk(node, [
['parent', [Static]],
['parent', [FieldExpression]],
['prevSibling', [FieldOp]],
])
) {
let attributeField = node.parent?.parent?.prevSibling?.prevSibling;
if (attributeField) {
return {
type: 'SPANSET_IN_VALUE',
tagName: getNodeText(attributeField, text),
betweenQuotes: true,
};
}
}
if (node.prevSibling?.type.id === FieldOp) {
let attributeField = node.prevSibling?.prevSibling;
if (attributeField) {
return {
type: 'SPANSET_IN_VALUE',
tagName: getNodeText(attributeField, text),
betweenQuotes: false,
};
}
}
if (node.prevSibling?.type.name === 'And' || node.prevSibling?.type.name === 'Or') {
return {
type: 'SPANSET_EMPTY',
};
}
return {
type: 'SPANSET_IN_THE_MIDDLE',
};
}
function resolveArithmeticOperator(node: SyntaxNode, _0: string, _1: number): SituationType | void {
if (node.prevSibling?.type.id !== ComparisonOp) {
return {
type: 'SPANSET_COMPARISON_OPERATORS',
};
}
}
function resolveNewSpansetExpression(node: SyntaxNode, text: string, offset: number): SituationType {
// Select the node immediately before the one pointed by the cursor
let previousNode = node.firstChild;
try {
previousNode = node.firstChild;
while (previousNode!.to < offset) {
previousNode = previousNode!.nextSibling;
}
} catch (error) {
console.error('Unexpected error while searching for previous node', error);
}
if (previousNode?.type.id === And || previousNode?.type.id === Or) {
return {
type: 'NEW_SPANSET',
};
}
return {
type: 'SPANSET_COMBINING_OPERATORS',
};
}
function resolveAttributeForFunction(node: SyntaxNode, _0: string, _1: number): SituationType | void {
const parent = node?.parent;
if (!!parent && [IntrinsicField, Aggregate, GroupOperation, SelectOperation, SelectArgs].includes(parent.type.id)) {
return {
type: 'ATTRIBUTE_FOR_FUNCTION',
};
}
}
function resolveSpansetPipeline(node: SyntaxNode, _1: string, _2: number): SituationType {
if (node.prevSibling?.type.id === Pipe) {
return {
type: 'SPANSET_PIPELINE_AFTER_OPERATOR',
};
}
return {
type: 'NEW_SPANSET',
};
}
function resolveSpansetWithNoClosedBrace(node: SyntaxNode, text: string, originalPos: number): SituationType {
const situation = resolveAttributeCompletion(node, text, originalPos);
if (situation) {
return situation;
}
return {
type: 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE',
};
}