mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tempo: Improve autocompletion and syntax highlighting for TraceQL tab (#73707)
This commit is contained in:
parent
94c9bee181
commit
96facbdfa2
@ -242,7 +242,7 @@
|
||||
"@grafana/faro-web-sdk": "1.1.2",
|
||||
"@grafana/google-sdk": "0.1.1",
|
||||
"@grafana/lezer-logql": "0.1.11",
|
||||
"@grafana/lezer-traceql": "0.0.4",
|
||||
"@grafana/lezer-traceql": "0.0.5",
|
||||
"@grafana/monaco-logql": "^0.0.7",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "^0.29.0",
|
||||
|
@ -9,6 +9,8 @@ import { Scope, TempoJsonData } from '../types';
|
||||
import { CompletionProvider } from './autocomplete';
|
||||
import { intrinsics, scopes } from './traceql';
|
||||
|
||||
const emptyPosition = {} as monacoTypes.Position;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
}));
|
||||
@ -16,10 +18,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
describe('CompletionProvider', () => {
|
||||
it('suggests tags, intrinsics and scopes (API v1)', async () => {
|
||||
const { provider, model } = setup('{}', 1, v1Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
@ -31,10 +30,7 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('suggests tags, intrinsics and scopes (API v2)', async () => {
|
||||
const { provider, model } = setup('{}', 1, undefined, v2Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
@ -60,10 +56,7 @@ describe('CompletionProvider', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
expect.objectContaining({ label: 'foobar', insertText: 'foobar' }),
|
||||
]);
|
||||
@ -85,10 +78,7 @@ describe('CompletionProvider', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
expect.objectContaining({ label: 'foobar', insertText: '"foobar"' }),
|
||||
]);
|
||||
@ -109,10 +99,7 @@ describe('CompletionProvider', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
expect.objectContaining({ label: 'foobar', insertText: 'foobar' }),
|
||||
]);
|
||||
@ -120,19 +107,13 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('suggests nothing without tags', async () => {
|
||||
const { provider, model } = setup('{.foo="}', 8, emptyTags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
it('suggests tags on empty input (API v1)', async () => {
|
||||
const { provider, model } = setup('', 0, v1Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}` })),
|
||||
...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}` })),
|
||||
@ -144,10 +125,7 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('suggests tags on empty input (API v2)', async () => {
|
||||
const { provider, model } = setup('', 0, undefined, v2Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}` })),
|
||||
...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}` })),
|
||||
@ -159,10 +137,7 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('only suggests tags after typing the global attribute scope (API v1)', async () => {
|
||||
const { provider, model } = setup('{.}', 2, v1Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
v1Tags.map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
@ -170,34 +145,15 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('only suggests tags after typing the global attribute scope (API v2)', async () => {
|
||||
const { provider, model } = setup('{.}', 2, undefined, v2Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
['cluster', 'container', 'db'].map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests operators after a space after the tag name', async () => {
|
||||
const { provider, model } = setup('{ .foo }', 7, v1Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
[...CompletionProvider.logicalOps, ...CompletionProvider.operators].map((s) =>
|
||||
expect.objectContaining({ label: s, insertText: s })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests tags after a scope (API v1)', async () => {
|
||||
const { provider, model } = setup('{ resource. }', 11, v1Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
v1Tags.map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
@ -205,10 +161,7 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('suggests correct tags after the resource scope (API v2)', async () => {
|
||||
const { provider, model } = setup('{ resource. }', 11, undefined, v2Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
['cluster', 'container'].map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
@ -216,10 +169,7 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('suggests correct tags after the span scope (API v2)', async () => {
|
||||
const { provider, model } = setup('{ span. }', 7, undefined, v2Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
['db'].map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
@ -227,16 +177,149 @@ describe('CompletionProvider', () => {
|
||||
|
||||
it('suggests logical operators and close bracket after the value', async () => {
|
||||
const { provider, model } = setup('{.foo=300 }', 10, v1Tags);
|
||||
const result = await provider.provideCompletionItems(
|
||||
model as unknown as monacoTypes.editor.ITextModel,
|
||||
{} as monacoTypes.Position
|
||||
);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
[...CompletionProvider.logicalOps, ...CompletionProvider.operators].map((s) =>
|
||||
expect.objectContaining({ label: s, insertText: s })
|
||||
[...CompletionProvider.logicalOps, ...CompletionProvider.arithmeticOps, ...CompletionProvider.comparisonOps].map(
|
||||
(s) => expect.objectContaining({ label: s.label, insertText: s.insertText })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests spanset combining operators after spanset selector', async () => {
|
||||
const { provider, model } = setup('{.foo=300} ', 11);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
CompletionProvider.spansetOps.map((s) => expect.objectContaining({ label: s.label, insertText: s.insertText }))
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['{.foo=300} | ', 13],
|
||||
['{.foo=300} && {.bar=200} | ', 27],
|
||||
['{.foo=300} && {.bar=300} && {.foo=300} | ', 41],
|
||||
])(
|
||||
'suggests operators that go after `|` (aggregators, selectorts, ...) - %s, %i',
|
||||
async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
CompletionProvider.functions.map((s) =>
|
||||
expect.objectContaining({ label: s.label, insertText: s.insertText, documentation: s.documentation })
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['{.foo=300} | avg(.value) ', 25],
|
||||
['{.foo=300} && {.foo=300} | avg(.value) ', 39],
|
||||
])(
|
||||
'suggests comparison operators after aggregator (avg, max, ...) - %s, %i',
|
||||
async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
CompletionProvider.comparisonOps.map((s) =>
|
||||
expect.objectContaining({ label: s.label, insertText: s.insertText })
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['{.foo=300} | avg(.value) = ', 27],
|
||||
['{.foo=300} && {.foo=300} | avg(.value) = ', 41],
|
||||
])('does not suggest after aggregator and comparison operator - %s, %i', async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
it('suggests when `}` missing', async () => {
|
||||
const { provider, model } = setup('{ span.http.status_code ', 24);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
[...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps].map((s) =>
|
||||
expect.objectContaining({ label: s.label, insertText: s.insertText })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['{ .foo }', 6],
|
||||
['{ .foo }', 7],
|
||||
['{.foo 300}', 5],
|
||||
['{.foo 300}', 6],
|
||||
['{.foo 300}', 7],
|
||||
['{.foo 300}', 8],
|
||||
['{.foo 300 && .bar = 200}', 5],
|
||||
['{.foo 300 && .bar = 200}', 6],
|
||||
['{.foo 300 && .bar = 200}', 7],
|
||||
['{.foo 300 && .bar 200}', 18],
|
||||
['{.foo 300 && .bar 200}', 19],
|
||||
['{.foo 300 && .bar 200}', 20],
|
||||
['{ .foo = 1 && .bar }', 18],
|
||||
['{ .foo = 1 && .bar }', 19],
|
||||
['{ .foo = 1 && .bar }', 19],
|
||||
])('suggests with incomplete spanset - %s, %i', async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
[...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps, ...CompletionProvider.arithmeticOps].map(
|
||||
(s) => expect.objectContaining({ label: s.label, insertText: s.insertText })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['{.foo=1} {.bar=2}', 8],
|
||||
['{.foo=1} {.bar=2}', 9],
|
||||
['{.foo=1} {.bar=2}', 10],
|
||||
])(
|
||||
'suggests spanset combining operators in an incomplete, multi-spanset query - %s, %i',
|
||||
async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
CompletionProvider.spansetOps.map((completionItem) =>
|
||||
expect.objectContaining({
|
||||
detail: completionItem.detail,
|
||||
documentation: completionItem.documentation,
|
||||
insertText: completionItem.insertText,
|
||||
label: completionItem.label,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
// After spanset
|
||||
['{ span.http.status_code = 200 && }', 33],
|
||||
['{ span.http.status_code = 200 || }', 33],
|
||||
['{ span.http.status_code = 200 && }', 34],
|
||||
['{ span.http.status_code = 200 || }', 34],
|
||||
['{ span.http.status_code = 200 && }', 35],
|
||||
['{ span.http.status_code = 200 || }', 35],
|
||||
['{ .foo = 200 } && ', 18],
|
||||
['{ .foo = 200 } && ', 19],
|
||||
['{ .foo = 200 } || ', 18],
|
||||
['{ .foo = 200 } >> ', 18],
|
||||
// Between spansets
|
||||
['{ .foo = 1 } && { .bar = 2 }', 16],
|
||||
// Inside `()`
|
||||
['{.foo=1} | avg()', 15],
|
||||
['{.foo=1} | avg() < 1s', 15],
|
||||
['{.foo=1} | max() = 3', 15],
|
||||
['{.foo=1} | by()', 14],
|
||||
['{.foo=1} | select()', 18],
|
||||
])('suggests attributes - %s, %i', async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
[...scopes, ...intrinsics].map((s) => expect.objectContaining({ label: s }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function setup(value: string, offset: number, tagsV1?: string[], tagsV2?: Scope[]) {
|
||||
@ -268,7 +351,7 @@ function setup(value: string, offset: number, tagsV1?: string[], tagsV2?: Scope[
|
||||
},
|
||||
} as any;
|
||||
|
||||
return { provider, model };
|
||||
return { provider, model } as unknown as { provider: CompletionProvider; model: monacoTypes.editor.ITextModel };
|
||||
}
|
||||
|
||||
function makeModel(value: string, offset: number) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { IMarkdownString } from 'monaco-editor';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||
@ -14,9 +16,19 @@ interface Props {
|
||||
languageProvider: TempoLanguageProvider;
|
||||
}
|
||||
|
||||
type MinimalCompletionItem = {
|
||||
label: string;
|
||||
insertText: string;
|
||||
detail?: string;
|
||||
documentation?: string | IMarkdownString;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class that implements CompletionItemProvider interface and allows us to provide suggestion for the Monaco
|
||||
* autocomplete system.
|
||||
*
|
||||
* Here we want to provide suggestions for TraceQL. Please refer to
|
||||
* https://grafana.com/docs/tempo/latest/traceql for the syntax of the language.
|
||||
*/
|
||||
export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider {
|
||||
languageProvider: TempoLanguageProvider;
|
||||
@ -28,8 +40,171 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
||||
}
|
||||
|
||||
triggerCharacters = ['{', '.', '[', '(', '=', '~', ' ', '"'];
|
||||
static readonly operators: string[] = ['=', '-', '+', '<', '>', '>=', '<=', '=~'];
|
||||
static readonly logicalOps: string[] = ['&&', '||'];
|
||||
|
||||
// Operators
|
||||
static readonly arithmeticOps: MinimalCompletionItem[] = [
|
||||
{
|
||||
label: '+',
|
||||
insertText: '+',
|
||||
detail: 'Plus',
|
||||
},
|
||||
{
|
||||
label: '-',
|
||||
insertText: '-',
|
||||
detail: 'Minus',
|
||||
},
|
||||
{
|
||||
label: '*',
|
||||
insertText: '*',
|
||||
detail: 'Times',
|
||||
},
|
||||
{
|
||||
label: '/',
|
||||
insertText: '/',
|
||||
detail: 'Over',
|
||||
},
|
||||
];
|
||||
|
||||
static readonly logicalOps: MinimalCompletionItem[] = [
|
||||
{
|
||||
label: '&&',
|
||||
insertText: '&&',
|
||||
detail: 'And',
|
||||
documentation: 'And (intersection) operator. Checks that both conditions found matches.',
|
||||
},
|
||||
{
|
||||
label: '||',
|
||||
insertText: '||',
|
||||
detail: 'Or',
|
||||
documentation: 'Or (union) operator. Checks that either condition found matches.',
|
||||
},
|
||||
];
|
||||
|
||||
static readonly comparisonOps: MinimalCompletionItem[] = [
|
||||
{
|
||||
label: '=',
|
||||
insertText: '=',
|
||||
detail: 'Equality',
|
||||
},
|
||||
{
|
||||
label: '!=',
|
||||
insertText: '!=',
|
||||
detail: 'Inequality',
|
||||
},
|
||||
{
|
||||
label: '>',
|
||||
insertText: '>',
|
||||
detail: 'Greater than',
|
||||
},
|
||||
{
|
||||
label: '>=',
|
||||
insertText: '>=',
|
||||
detail: 'Greater than or equal to',
|
||||
},
|
||||
{
|
||||
label: '<',
|
||||
insertText: '<',
|
||||
detail: 'Less than',
|
||||
},
|
||||
{
|
||||
label: '<=',
|
||||
insertText: '<=',
|
||||
detail: 'Less than or equal to',
|
||||
},
|
||||
{
|
||||
label: '=~',
|
||||
insertText: '=~',
|
||||
detail: 'Regular expression',
|
||||
},
|
||||
{
|
||||
label: '!~',
|
||||
insertText: '!~',
|
||||
detail: 'Negated regular expression',
|
||||
},
|
||||
];
|
||||
static readonly structuralOps: MinimalCompletionItem[] = [
|
||||
{
|
||||
label: '>>',
|
||||
insertText: '>>',
|
||||
detail: 'Descendant',
|
||||
documentation:
|
||||
'Descendant operator. Looks for spans matching {condB} that are descendants of a span matching {condA}',
|
||||
},
|
||||
{
|
||||
label: '>',
|
||||
insertText: '>',
|
||||
detail: 'Child',
|
||||
documentation:
|
||||
'Child operator. Looks for spans matching {condB} that are direct child spans of a parent matching {condA}',
|
||||
},
|
||||
{
|
||||
label: '~',
|
||||
insertText: '~',
|
||||
detail: 'Sibling',
|
||||
documentation:
|
||||
'Sibling operator. Checks that spans matching {condA} and {condB} are siblings of the same parent span.',
|
||||
},
|
||||
];
|
||||
|
||||
static readonly spansetOps: MinimalCompletionItem[] = [
|
||||
{
|
||||
label: '|',
|
||||
insertText: '|',
|
||||
detail: 'Pipe',
|
||||
},
|
||||
...CompletionProvider.logicalOps,
|
||||
...CompletionProvider.structuralOps,
|
||||
];
|
||||
|
||||
// Functions (aggregator, selector, and combining operators)
|
||||
static readonly spansetAggregatorOps: MinimalCompletionItem[] = [
|
||||
{
|
||||
label: 'count',
|
||||
insertText: 'count()$0',
|
||||
detail: 'Number of spans',
|
||||
documentation: 'Counts the number of spans in a spanset',
|
||||
},
|
||||
{
|
||||
label: 'avg',
|
||||
insertText: 'avg($0)',
|
||||
detail: 'Average of attribute',
|
||||
documentation: 'Computes the average of a given numeric attribute or intrinsic for a spanset.',
|
||||
},
|
||||
{
|
||||
label: 'max',
|
||||
insertText: 'max($0)',
|
||||
detail: 'Max value of attribute',
|
||||
documentation: 'Computes the maximum value of a given numeric attribute or intrinsic for a spanset.',
|
||||
},
|
||||
{
|
||||
label: 'min',
|
||||
insertText: 'min($0)',
|
||||
detail: 'Min value of attribute',
|
||||
documentation: 'Computes the minimum value of a given numeric attribute or intrinsic for a spanset.',
|
||||
},
|
||||
{
|
||||
label: 'sum',
|
||||
insertText: 'sum($0)',
|
||||
detail: 'Sum value of attribute',
|
||||
documentation: 'Computes the sum value of a given numeric attribute or intrinsic for a spanset.',
|
||||
},
|
||||
];
|
||||
|
||||
static readonly functions: MinimalCompletionItem[] = [
|
||||
...this.spansetAggregatorOps,
|
||||
{
|
||||
label: 'by',
|
||||
insertText: 'by($0)',
|
||||
detail: 'Grouping of attributes',
|
||||
documentation: 'Groups by arbitrary attributes.',
|
||||
},
|
||||
{
|
||||
label: 'select',
|
||||
insertText: 'select($0)',
|
||||
detail: 'Selection of fields',
|
||||
documentation: 'Selects arbitrary fields from spans.',
|
||||
},
|
||||
];
|
||||
|
||||
// We set these directly and ae required for the provider to function.
|
||||
monaco: Monaco | undefined;
|
||||
@ -66,6 +241,9 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
||||
kind: getMonacoCompletionItemKind(item.type, this.monaco!),
|
||||
label: item.label,
|
||||
insertText: item.insertText,
|
||||
insertTextRules: item.insertTextRules,
|
||||
detail: item.detail,
|
||||
documentation: item.documentation,
|
||||
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
|
||||
range,
|
||||
command: {
|
||||
@ -107,7 +285,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
||||
*/
|
||||
private async getCompletions(situation: Situation): Promise<Completion[]> {
|
||||
switch (situation.type) {
|
||||
// Not really sure what would make sense to suggest in this case so just leave it
|
||||
// This should only happen for cases that we do not support yet
|
||||
case 'UNKNOWN': {
|
||||
return [];
|
||||
}
|
||||
@ -121,14 +299,52 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
||||
case 'SPANSET_ONLY_DOT': {
|
||||
return this.getTagsCompletions();
|
||||
}
|
||||
case 'SPANSET_IN_THE_MIDDLE':
|
||||
return [...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps].map((key) => ({
|
||||
...key,
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE':
|
||||
return [...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps].map((key) => ({
|
||||
...key,
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'SPANSET_IN_NAME':
|
||||
return this.getScopesCompletions().concat(this.getIntrinsicsCompletions()).concat(this.getTagsCompletions());
|
||||
case 'SPANSET_IN_NAME_SCOPE':
|
||||
return this.getTagsCompletions(undefined, situation.scope);
|
||||
case 'SPANSET_EXPRESSION_OPERATORS':
|
||||
return [...CompletionProvider.logicalOps, ...CompletionProvider.operators].map((key) => ({
|
||||
label: key,
|
||||
insertText: key,
|
||||
return [
|
||||
...CompletionProvider.comparisonOps,
|
||||
...CompletionProvider.logicalOps,
|
||||
...CompletionProvider.arithmeticOps,
|
||||
].map((key) => ({
|
||||
...key,
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'SPANFIELD_COMBINING_OPERATORS':
|
||||
return [
|
||||
...CompletionProvider.logicalOps,
|
||||
...CompletionProvider.arithmeticOps,
|
||||
...CompletionProvider.comparisonOps,
|
||||
].map((key) => ({
|
||||
...key,
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'SPANSET_COMBINING_OPERATORS':
|
||||
return CompletionProvider.spansetOps.map((key) => ({
|
||||
...key,
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'SPANSET_PIPELINE_AFTER_OPERATOR':
|
||||
return CompletionProvider.functions.map((key) => ({
|
||||
...key,
|
||||
insertTextRules: this.monaco?.languages.CompletionItemInsertTextRule?.InsertAsSnippet,
|
||||
type: 'FUNCTION',
|
||||
}));
|
||||
case 'SPANSET_COMPARISON_OPERATORS':
|
||||
return CompletionProvider.comparisonOps.map((key) => ({
|
||||
...key,
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'SPANSET_IN_VALUE':
|
||||
@ -163,11 +379,17 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
||||
});
|
||||
return items;
|
||||
case 'SPANSET_AFTER_VALUE':
|
||||
return CompletionProvider.logicalOps.concat('}').map((key) => ({
|
||||
label: key,
|
||||
insertText: key,
|
||||
return CompletionProvider.logicalOps.map((key) => ({
|
||||
label: key.label,
|
||||
insertText: key.insertText + '}',
|
||||
type: 'OPERATOR',
|
||||
}));
|
||||
case 'NEW_SPANSET':
|
||||
return this.getScopesCompletions('{ ', '$0 }')
|
||||
.concat(this.getIntrinsicsCompletions('{ ', '$0 }'))
|
||||
.concat(this.getTagsCompletions('.'));
|
||||
case 'ATTRIBUTE_FOR_FUNCTION':
|
||||
return this.getScopesCompletions().concat(this.getIntrinsicsCompletions()).concat(this.getTagsCompletions('.'));
|
||||
default:
|
||||
throw new Error(`Unexpected situation ${situation}`);
|
||||
}
|
||||
@ -184,19 +406,21 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
|
||||
}));
|
||||
}
|
||||
|
||||
private getIntrinsicsCompletions(prepend?: string): Completion[] {
|
||||
private getIntrinsicsCompletions(prepend?: string, append?: string): Completion[] {
|
||||
return intrinsics.map((key) => ({
|
||||
label: key,
|
||||
insertText: (prepend || '') + key,
|
||||
insertText: (prepend || '') + key + (append || ''),
|
||||
type: 'KEYWORD',
|
||||
insertTextRules: this.monaco?.languages.CompletionItemInsertTextRule?.InsertAsSnippet,
|
||||
}));
|
||||
}
|
||||
|
||||
private getScopesCompletions(prepend?: string): Completion[] {
|
||||
private getScopesCompletions(prepend?: string, append?: string): Completion[] {
|
||||
return scopes.map((key) => ({
|
||||
label: key,
|
||||
insertText: (prepend || '') + key,
|
||||
insertText: (prepend || '') + key + (append || ''),
|
||||
type: 'SCOPE',
|
||||
insertTextRules: this.monaco?.languages.CompletionItemInsertTextRule?.InsertAsSnippet,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -218,16 +442,21 @@ function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): mona
|
||||
return monaco.languages.CompletionItemKind.EnumMember;
|
||||
case 'SCOPE':
|
||||
return monaco.languages.CompletionItemKind.Class;
|
||||
case 'FUNCTION':
|
||||
return monaco.languages.CompletionItemKind.Function;
|
||||
default:
|
||||
throw new Error(`Unexpected CompletionType: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export type CompletionType = 'TAG_NAME' | 'TAG_VALUE' | 'KEYWORD' | 'OPERATOR' | 'SCOPE';
|
||||
export type CompletionType = 'TAG_NAME' | 'TAG_VALUE' | 'KEYWORD' | 'OPERATOR' | 'SCOPE' | 'FUNCTION';
|
||||
type Completion = {
|
||||
type: CompletionType;
|
||||
label: string;
|
||||
insertText: string;
|
||||
insertTextRules?: monacoTypes.languages.CompletionItemInsertTextRule; // we used it to position the cursor
|
||||
documentation?: string | IMarkdownString;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
export type Tag = {
|
||||
|
@ -23,9 +23,9 @@ describe('situation', () => {
|
||||
expected: { type: 'SPANSET_ONLY_DOT' },
|
||||
},
|
||||
{
|
||||
query: '{foo}',
|
||||
cursorPos: 4,
|
||||
expected: { type: 'SPANSET_IN_NAME' },
|
||||
query: '{.foo}',
|
||||
cursorPos: 5,
|
||||
expected: { type: 'SPANSET_EXPRESSION_OPERATORS' },
|
||||
},
|
||||
{
|
||||
query: '{span.}',
|
||||
@ -45,7 +45,12 @@ describe('situation', () => {
|
||||
{
|
||||
query: '{span.foo = "val" }',
|
||||
cursorPos: 18,
|
||||
expected: { type: 'SPANSET_EXPRESSION_OPERATORS' },
|
||||
expected: { type: 'SPANFIELD_COMBINING_OPERATORS' },
|
||||
},
|
||||
{
|
||||
query: '{span.foo = 200 }',
|
||||
cursorPos: 15,
|
||||
expected: { type: 'SPANFIELD_COMBINING_OPERATORS' },
|
||||
},
|
||||
{
|
||||
query: '{span.foo = "val" && }',
|
||||
@ -57,6 +62,11 @@ describe('situation', () => {
|
||||
cursorPos: 30,
|
||||
expected: { type: 'SPANSET_IN_NAME_SCOPE', scope: 'resource' },
|
||||
},
|
||||
{
|
||||
query: '{ .sla && span.http.status_code && span.http.status_code = 200 }',
|
||||
cursorPos: 57,
|
||||
expected: { type: 'SPANSET_EXPRESSION_OPERATORS' },
|
||||
},
|
||||
];
|
||||
|
||||
tests.forEach((test) => {
|
||||
|
@ -1,6 +1,24 @@
|
||||
import { SyntaxNode, Tree } from '@lezer/common';
|
||||
|
||||
import { AttributeField, FieldExpression, FieldOp, parser, SpansetFilter } from '@grafana/lezer-traceql';
|
||||
import {
|
||||
Aggregate,
|
||||
And,
|
||||
AttributeField,
|
||||
ComparisonOp,
|
||||
FieldExpression,
|
||||
FieldOp,
|
||||
GroupOperation,
|
||||
IntrinsicField,
|
||||
Or,
|
||||
parser,
|
||||
ScalarFilter,
|
||||
SelectArgs,
|
||||
SpansetFilter,
|
||||
SpansetPipeline,
|
||||
SpansetPipelineExpression,
|
||||
Static,
|
||||
TraceQL,
|
||||
} from '@grafana/lezer-traceql';
|
||||
|
||||
type Direction = 'parent' | 'firstChild' | 'lastChild' | 'nextSibling' | 'prevSibling';
|
||||
type NodeType = number;
|
||||
@ -23,6 +41,9 @@ export type SituationType =
|
||||
| {
|
||||
type: 'SPANSET_EXPRESSION_OPERATORS';
|
||||
}
|
||||
| {
|
||||
type: 'SPANFIELD_COMBINING_OPERATORS';
|
||||
}
|
||||
| {
|
||||
type: 'SPANSET_IN_NAME';
|
||||
}
|
||||
@ -37,6 +58,27 @@ export type SituationType =
|
||||
}
|
||||
| {
|
||||
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[]]>;
|
||||
@ -65,13 +107,15 @@ function move(node: SyntaxNode, direction: Direction): SyntaxNode | null {
|
||||
|
||||
function walk(node: SyntaxNode, path: Path): SyntaxNode | null {
|
||||
let current: SyntaxNode | null = node;
|
||||
for (const [direction, expectedNodes] of path) {
|
||||
for (const [direction, expectedNodeIDs] of path) {
|
||||
current = move(current, direction);
|
||||
if (current === null) {
|
||||
// we could not move in the direction, we stop
|
||||
return null;
|
||||
}
|
||||
if (!expectedNodes.find((en) => en === current?.type.id)) {
|
||||
|
||||
// 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;
|
||||
}
|
||||
@ -90,8 +134,8 @@ function isPathMatch(resolverPath: NodeType[], cursorPath: number[]): boolean {
|
||||
|
||||
/**
|
||||
* Figure out where is the cursor and what kind of suggestions are appropriate.
|
||||
* @param text
|
||||
* @param offset
|
||||
* @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,
|
||||
@ -105,21 +149,29 @@ export function getSituation(text: string, offset: number): Situation | null {
|
||||
|
||||
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 is very probable that
|
||||
// our node is one of those error nodes.
|
||||
// also, 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 maybeErrorNode = getErrorNode(tree, offset);
|
||||
let maybeErrorNode = getErrorNode(tree, shiftedOffset);
|
||||
if (!maybeErrorNode) {
|
||||
// try again with the previous character
|
||||
maybeErrorNode = getErrorNode(tree, offset - 1);
|
||||
maybeErrorNode = getErrorNode(tree, shiftedOffset - 1);
|
||||
}
|
||||
|
||||
const cur = maybeErrorNode != null ? maybeErrorNode.cursor() : tree.cursorAt(offset);
|
||||
const cur = maybeErrorNode != null ? maybeErrorNode.cursor() : tree.cursorAt(shiftedOffset);
|
||||
|
||||
const currentNode = cur.node;
|
||||
|
||||
const ids = [cur.type.id];
|
||||
while (cur.parent()) {
|
||||
ids.push(cur.type.id);
|
||||
@ -128,7 +180,7 @@ export function getSituation(text: string, offset: number): Situation | null {
|
||||
let situationType: SituationType | null = null;
|
||||
for (let resolver of RESOLVERS) {
|
||||
if (isPathMatch(resolver.path, ids)) {
|
||||
situationType = resolver.fun(currentNode, text, offset);
|
||||
situationType = resolver.fun(currentNode, text, shiftedOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,6 +190,7 @@ export function getSituation(text: string, offset: number): Situation | null {
|
||||
const ERROR_NODE_ID = 0;
|
||||
|
||||
const RESOLVERS: Resolver[] = [
|
||||
// Incomplete query cases
|
||||
{
|
||||
path: [ERROR_NODE_ID, AttributeField],
|
||||
fun: resolveAttribute,
|
||||
@ -148,15 +201,85 @@ const RESOLVERS: Resolver[] = [
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, SpansetFilter],
|
||||
fun: resolveErrorInFilterRoot,
|
||||
fun: () => ({
|
||||
type: 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE',
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, SpansetPipeline],
|
||||
fun: resolveSpansetPipeline,
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, Aggregate],
|
||||
fun: resolveAttributeForFunction,
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, IntrinsicField],
|
||||
fun: resolveAttributeForFunction,
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, SpansetPipelineExpression],
|
||||
fun: () => {
|
||||
return {
|
||||
type: 'NEW_SPANSET',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, ScalarFilter, SpansetPipeline],
|
||||
fun: resolveArithmeticOperator,
|
||||
},
|
||||
{
|
||||
path: [ERROR_NODE_ID, TraceQL],
|
||||
fun: () => {
|
||||
return {
|
||||
type: 'UNKNOWN',
|
||||
};
|
||||
},
|
||||
},
|
||||
// Valid query cases
|
||||
{
|
||||
path: [FieldExpression],
|
||||
fun: () => ({
|
||||
type: 'SPANSET_EXPRESSION_OPERATORS',
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: [SpansetFilter],
|
||||
fun: resolveSpanset,
|
||||
},
|
||||
{
|
||||
path: [SpansetPipelineExpression],
|
||||
fun: resolveNewSpansetExpression,
|
||||
},
|
||||
{
|
||||
path: [TraceQL],
|
||||
fun: resolveNewSpansetExpression,
|
||||
},
|
||||
];
|
||||
|
||||
function resolveSpanset(node: SyntaxNode): SituationType {
|
||||
const firstChild = walk(node, [
|
||||
['firstChild', [FieldExpression]],
|
||||
['firstChild', [AttributeField]],
|
||||
]);
|
||||
if (firstChild) {
|
||||
return {
|
||||
type: 'SPANSET_EXPRESSION_OPERATORS',
|
||||
};
|
||||
}
|
||||
|
||||
const lastFieldExpression1 = walk(node, [
|
||||
['lastChild', [FieldExpression]],
|
||||
['lastChild', [FieldExpression]],
|
||||
['lastChild', [Static]],
|
||||
]);
|
||||
if (lastFieldExpression1) {
|
||||
return {
|
||||
type: 'SPANFIELD_COMBINING_OPERATORS',
|
||||
};
|
||||
}
|
||||
|
||||
const lastFieldExpression = walk(node, [['lastChild', [FieldExpression]]]);
|
||||
if (lastFieldExpression) {
|
||||
return {
|
||||
@ -204,13 +327,67 @@ function resolveExpression(node: SyntaxNode, text: string): SituationType {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (node.prevSibling?.type.name === 'And' || node.prevSibling?.type.name === 'Or') {
|
||||
return {
|
||||
type: 'SPANSET_EMPTY',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'SPANSET_EMPTY',
|
||||
type: 'SPANSET_IN_THE_MIDDLE',
|
||||
};
|
||||
}
|
||||
|
||||
function resolveErrorInFilterRoot(): SituationType {
|
||||
function resolveArithmeticOperator(node: SyntaxNode, _0: string, _1: number): SituationType {
|
||||
if (node.prevSibling?.type.id === ComparisonOp) {
|
||||
return {
|
||||
type: 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'SPANSET_IN_NAME',
|
||||
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 {
|
||||
const parent = node?.parent;
|
||||
if (!!parent && [IntrinsicField, Aggregate, GroupOperation, SelectArgs].includes(parent.type.id)) {
|
||||
return {
|
||||
type: 'ATTRIBUTE_FOR_FUNCTION',
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSpansetPipeline(_0: SyntaxNode, _1: string, _2: number): SituationType {
|
||||
return {
|
||||
type: 'SPANSET_PIPELINE_AFTER_OPERATOR',
|
||||
};
|
||||
}
|
||||
|
@ -27,9 +27,10 @@ export const stringOperators = ['=', '!=', '=~'];
|
||||
export const numberOperators = ['=', '!=', '>', '<', '>=', '<='];
|
||||
|
||||
export const intrinsics = ['duration', 'kind', 'name', 'status'];
|
||||
|
||||
export const scopes: string[] = ['resource', 'span'];
|
||||
|
||||
export const functions = ['avg', 'min', 'max', 'sum', 'count', 'by'];
|
||||
|
||||
const keywords = intrinsics.concat(scopes);
|
||||
|
||||
const statusValues = ['ok', 'unset', 'error', 'false', 'true'];
|
||||
@ -42,8 +43,8 @@ export const language = {
|
||||
keywords,
|
||||
operators,
|
||||
statusValues,
|
||||
functions,
|
||||
|
||||
// we include these common regular expressions
|
||||
symbols: /[=><!~?:&|+\-*\/^%]+/,
|
||||
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
||||
digits: /\d+(_+\d+)*/,
|
||||
@ -52,24 +53,21 @@ export const language = {
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
// labels
|
||||
[/[a-z_.][\w./_-]*(?=\s*(=|!=|>|<|>=|<=|=~|!~))/, 'tag'],
|
||||
[/[a-z_.][\w./_-]*/, 'tag'],
|
||||
|
||||
// durations
|
||||
[/[0-9.]+(s|ms|ns|m)/, 'number'],
|
||||
[/[0-9]+(.[0-9]+)?(us|µs|ns|ms|s|m|h)/, 'number'],
|
||||
|
||||
// trace ID
|
||||
[/^\s*[0-9A-Fa-f]+\s*$/, 'tag'],
|
||||
|
||||
// all keywords have the same color
|
||||
// functions, keywords, predefined values
|
||||
[
|
||||
/[a-zA-Z_.]\w*/,
|
||||
{
|
||||
cases: {
|
||||
'@keywords': 'type',
|
||||
'@statusValues': 'type.identifier',
|
||||
'@default': 'identifier',
|
||||
'@functions': 'predefined',
|
||||
'@keywords': 'keyword',
|
||||
'@statusValues': 'type',
|
||||
'@default': 'tag',
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -80,12 +78,8 @@ export const language = {
|
||||
[/"/, 'string', '@string_double'],
|
||||
[/'/, 'string', '@string_single'],
|
||||
|
||||
// whitespace
|
||||
{ include: '@whitespace' },
|
||||
|
||||
// delimiters and operators
|
||||
[/[{}()\[\]]/, '@brackets'],
|
||||
[/[<>](?!@symbols)/, '@brackets'],
|
||||
[/[{}()\[\]]/, 'delimiter.bracket'],
|
||||
[
|
||||
/@symbols/,
|
||||
{
|
||||
@ -118,13 +112,6 @@ export const language = {
|
||||
[/\\./, 'string.escape.invalid'],
|
||||
[/'/, 'string', '@pop'],
|
||||
],
|
||||
|
||||
clauses: [
|
||||
[/[^(,)]/, 'tag'],
|
||||
[/\)/, 'identifier', '@pop'],
|
||||
],
|
||||
|
||||
whitespace: [[/[ \t\r\n]+/, 'white']],
|
||||
},
|
||||
};
|
||||
|
||||
|
10
yarn.lock
10
yarn.lock
@ -3877,12 +3877,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/lezer-traceql@npm:0.0.4":
|
||||
version: 0.0.4
|
||||
resolution: "@grafana/lezer-traceql@npm:0.0.4"
|
||||
"@grafana/lezer-traceql@npm:0.0.5":
|
||||
version: 0.0.5
|
||||
resolution: "@grafana/lezer-traceql@npm:0.0.5"
|
||||
peerDependencies:
|
||||
"@lezer/lr": ^1.3.0
|
||||
checksum: 69acea33476d3cdabfb99f3eb62bb34289bc205da69920e0eccc82f40407f9d584fa03f9662706ab667ee97d3ea84b964b22a11925e6699deef5afe1ca9e1906
|
||||
checksum: 6fcf48acde1e444c155a4b4009f4c7211843b07960713821a2649b1db0e0ef819fd1062eec101173c2e6b8249b253faf0b6052e96f551663618fd2fe0d17e3c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -19226,7 +19226,7 @@ __metadata:
|
||||
"@grafana/faro-web-sdk": 1.1.2
|
||||
"@grafana/google-sdk": 0.1.1
|
||||
"@grafana/lezer-logql": 0.1.11
|
||||
"@grafana/lezer-traceql": 0.0.4
|
||||
"@grafana/lezer-traceql": 0.0.5
|
||||
"@grafana/monaco-logql": ^0.0.7
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": ^0.29.0
|
||||
|
Loading…
Reference in New Issue
Block a user