@fc-components/monaco-editor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,491 @@
1
+ import type { SyntaxNode, Tree } from '@lezer/common';
2
+ import {
3
+ AggregateExpr,
4
+ AggregateModifier,
5
+ BinaryExpr,
6
+ EqlRegex,
7
+ EqlSingle,
8
+ FunctionCallBody,
9
+ GroupingLabels,
10
+ Identifier,
11
+ LabelMatchers,
12
+ LabelName,
13
+ MatchOp,
14
+ MatrixSelector,
15
+ Neq,
16
+ NeqRegex,
17
+ NumberDurationLiteralInDurationContext,
18
+ parser,
19
+ PromQL,
20
+ StringLiteral,
21
+ UnquotedLabelMatcher,
22
+ VectorSelector,
23
+ } from '@fc-components/lezer-metricsql';
24
+
25
+ import { NeverCaseError } from '../util';
26
+ import { LabelOperator, Label } from '../types';
27
+
28
+ type Direction = 'parent' | 'firstChild' | 'lastChild' | 'nextSibling';
29
+
30
+ type NodeTypeId =
31
+ | 0 // this is used as error-id
32
+ | typeof AggregateExpr
33
+ | typeof AggregateModifier
34
+ | typeof FunctionCallBody
35
+ | typeof GroupingLabels
36
+ | typeof Identifier
37
+ | typeof UnquotedLabelMatcher
38
+ | typeof LabelMatchers
39
+ | typeof LabelName
40
+ | typeof PromQL
41
+ | typeof StringLiteral
42
+ | typeof VectorSelector
43
+ | typeof MatrixSelector
44
+ | typeof MatchOp
45
+ | typeof EqlSingle
46
+ | typeof Neq
47
+ | typeof EqlRegex
48
+ | typeof NeqRegex;
49
+
50
+ type Path = Array<[Direction, NodeTypeId]>;
51
+
52
+ function move(node: SyntaxNode, direction: Direction): SyntaxNode | null {
53
+ switch (direction) {
54
+ case 'parent':
55
+ return node.parent;
56
+ case 'firstChild':
57
+ return node.firstChild;
58
+ case 'lastChild':
59
+ return node.lastChild;
60
+ case 'nextSibling':
61
+ return node.nextSibling;
62
+ default:
63
+ throw new NeverCaseError(direction);
64
+ }
65
+ }
66
+
67
+ function walk(node: SyntaxNode, path: Path): SyntaxNode | null {
68
+ let current: SyntaxNode | null = node;
69
+ for (const [direction, expectedType] of path) {
70
+ current = move(current, direction);
71
+ if (current === null) {
72
+ // we could not move in the direction, we stop
73
+ return null;
74
+ }
75
+ if (current.type.id !== expectedType) {
76
+ // the reached node has wrong type, we stop
77
+ return null;
78
+ }
79
+ }
80
+ return current;
81
+ }
82
+
83
+ function getNodeText(node: SyntaxNode, text: string): string {
84
+ return text.slice(node.from, node.to);
85
+ }
86
+
87
+ function parsePromQLStringLiteral(text: string): string {
88
+ // if it is a string-literal, it is inside quotes of some kind
89
+ const inside = text.slice(1, text.length - 1);
90
+
91
+ // FIXME: support https://prometheus.io/docs/prometheus/latest/querying/basics/#string-literals
92
+ // FIXME: maybe check other promql code, if all is supported or not
93
+
94
+ // for now we do only some very simple un-escaping
95
+
96
+ // we start with double-quotes
97
+ if (text.startsWith('"') && text.endsWith('"')) {
98
+ // NOTE: this is not 100% perfect, we only unescape the double-quote,
99
+ // there might be other characters too
100
+ return inside.replace(/\\"/, '"');
101
+ }
102
+
103
+ // then single-quote
104
+ if (text.startsWith("'") && text.endsWith("'")) {
105
+ // NOTE: this is not 100% perfect, we only unescape the single-quote,
106
+ // there might be other characters too
107
+ return inside.replace(/\\'/, "'");
108
+ }
109
+
110
+ // then backticks
111
+ if (text.startsWith('`') && text.endsWith('`')) {
112
+ return inside;
113
+ }
114
+
115
+ throw new Error('FIXME: invalid string literal');
116
+ }
117
+
118
+ export type Situation =
119
+ | {
120
+ type: 'IN_FUNCTION';
121
+ }
122
+ | {
123
+ type: 'AT_ROOT';
124
+ }
125
+ | {
126
+ type: 'EMPTY';
127
+ }
128
+ | {
129
+ type: 'IN_DURATION';
130
+ }
131
+ | {
132
+ type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME';
133
+ metricName?: string;
134
+ otherLabels: Label[];
135
+ }
136
+ | {
137
+ type: 'IN_GROUPING';
138
+ metricName: string;
139
+ otherLabels: Label[];
140
+ }
141
+ | {
142
+ type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME';
143
+ metricName?: string;
144
+ labelName: string;
145
+ betweenQuotes: boolean;
146
+ otherLabels: Label[];
147
+ };
148
+
149
+ type Resolver = {
150
+ path: NodeTypeId[];
151
+ fun: (node: SyntaxNode, text: string, pos: number) => Situation | null;
152
+ };
153
+
154
+ function isPathMatch(resolverPath: NodeTypeId[], cursorPath: number[]): boolean {
155
+ return resolverPath.every((item, index) => item === cursorPath[index]);
156
+ }
157
+
158
+ const ERROR_NODE_NAME: NodeTypeId = 0; // this is used as error-id
159
+
160
+ const RESOLVERS: Resolver[] = [
161
+ {
162
+ path: [LabelMatchers, VectorSelector],
163
+ fun: resolveLabelKeysWithEquals,
164
+ },
165
+ {
166
+ path: [PromQL],
167
+ fun: resolveTopLevel,
168
+ },
169
+ {
170
+ path: [FunctionCallBody],
171
+ fun: resolveInFunction,
172
+ },
173
+ {
174
+ path: [StringLiteral, UnquotedLabelMatcher],
175
+ fun: resolveLabelMatcher,
176
+ },
177
+ {
178
+ path: [ERROR_NODE_NAME, BinaryExpr, PromQL],
179
+ fun: resolveTopLevel,
180
+ },
181
+ {
182
+ path: [ERROR_NODE_NAME, UnquotedLabelMatcher],
183
+ fun: resolveLabelMatcher,
184
+ },
185
+ {
186
+ path: [ERROR_NODE_NAME, NumberDurationLiteralInDurationContext, MatrixSelector],
187
+ fun: resolveDurations,
188
+ },
189
+ {
190
+ path: [GroupingLabels],
191
+ fun: resolveLabelsForGrouping,
192
+ },
193
+ ];
194
+
195
+ const LABEL_OP_MAP = new Map<number, LabelOperator>([
196
+ [EqlSingle, '='],
197
+ [EqlRegex, '=~'],
198
+ [Neq, '!='],
199
+ [NeqRegex, '!~'],
200
+ ]);
201
+
202
+ function getLabelOp(opNode: SyntaxNode): LabelOperator | null {
203
+ const opChild = opNode.firstChild;
204
+ if (opChild === null) {
205
+ return null;
206
+ }
207
+
208
+ return LABEL_OP_MAP.get(opChild.type.id) ?? null;
209
+ }
210
+
211
+ function getLabel(labelMatcherNode: SyntaxNode, text: string): Label | null {
212
+ if (labelMatcherNode.type.id !== UnquotedLabelMatcher) {
213
+ return null;
214
+ }
215
+
216
+ const nameNode = walk(labelMatcherNode, [['firstChild', LabelName]]);
217
+
218
+ if (nameNode === null) {
219
+ return null;
220
+ }
221
+
222
+ const opNode = walk(nameNode, [['nextSibling', MatchOp]]);
223
+ if (opNode === null) {
224
+ return null;
225
+ }
226
+
227
+ const op = getLabelOp(opNode);
228
+ if (op === null) {
229
+ return null;
230
+ }
231
+
232
+ const valueNode = walk(labelMatcherNode, [['lastChild', StringLiteral]]);
233
+
234
+ if (valueNode === null) {
235
+ return null;
236
+ }
237
+
238
+ const name = getNodeText(nameNode, text);
239
+ const value = parsePromQLStringLiteral(getNodeText(valueNode, text));
240
+
241
+ return { name, value, op };
242
+ }
243
+
244
+ function getLabels(labelMatchersNode: SyntaxNode, text: string): Label[] {
245
+ if (labelMatchersNode.type.id !== LabelMatchers) {
246
+ return [];
247
+ }
248
+
249
+ const labelNodes = labelMatchersNode.getChildren(UnquotedLabelMatcher);
250
+ return labelNodes.map((ln) => getLabel(ln, text)).filter(notEmpty);
251
+ }
252
+
253
+ function getNodeChildren(node: SyntaxNode): SyntaxNode[] {
254
+ let child: SyntaxNode | null = node.firstChild;
255
+ const children: SyntaxNode[] = [];
256
+ while (child !== null) {
257
+ children.push(child);
258
+ child = child.nextSibling;
259
+ }
260
+ return children;
261
+ }
262
+
263
+ function getNodeInSubtree(node: SyntaxNode, typeId: NodeTypeId): SyntaxNode | null {
264
+ // first we try the current node
265
+ if (node.type.id === typeId) {
266
+ return node;
267
+ }
268
+
269
+ // then we try the children
270
+ const children = getNodeChildren(node);
271
+ for (const child of children) {
272
+ const n = getNodeInSubtree(child, typeId);
273
+ if (n !== null) {
274
+ return n;
275
+ }
276
+ }
277
+
278
+ return null;
279
+ }
280
+
281
+ function resolveLabelsForGrouping(node: SyntaxNode, text: string, _pos: number): Situation | null {
282
+ const aggrExpNode = walk(node, [
283
+ ['parent', AggregateModifier],
284
+ ['parent', AggregateExpr],
285
+ ]);
286
+ if (aggrExpNode === null) {
287
+ return null;
288
+ }
289
+ const bodyNode = aggrExpNode.getChild(FunctionCallBody);
290
+ if (bodyNode === null) {
291
+ return null;
292
+ }
293
+
294
+ const metricIdNode = getNodeInSubtree(bodyNode, Identifier);
295
+ if (metricIdNode === null) {
296
+ return null;
297
+ }
298
+
299
+ const metricName = getNodeText(metricIdNode, text);
300
+ return {
301
+ type: 'IN_GROUPING',
302
+ metricName,
303
+ otherLabels: [],
304
+ };
305
+ }
306
+
307
+ function resolveLabelMatcher(node: SyntaxNode, text: string, _pos: number): Situation | null {
308
+ // we can arrive here in two situation. `node` is either:
309
+ // - a StringNode (like in `{job="^"}`)
310
+ // - or an error node (like in `{job=^}`)
311
+ const inStringNode = !node.type.isError;
312
+
313
+ const parent = walk(node, [['parent', UnquotedLabelMatcher]]);
314
+ if (parent === null) {
315
+ return null;
316
+ }
317
+
318
+ const labelNameNode = walk(parent, [['firstChild', LabelName]]);
319
+ if (labelNameNode === null) {
320
+ return null;
321
+ }
322
+
323
+ const labelName = getNodeText(labelNameNode, text);
324
+
325
+ const labelMatchersNode = walk(parent, [['parent', LabelMatchers]]);
326
+ if (labelMatchersNode === null) {
327
+ return null;
328
+ }
329
+
330
+ // now we need to find the other names
331
+ const allLabels = getLabels(labelMatchersNode, text);
332
+
333
+ // we need to remove "our" label from all-labels, if it is in there
334
+ const otherLabels = allLabels.filter((label) => label.name !== labelName);
335
+
336
+ const metricNameNode = walk(labelMatchersNode, [
337
+ ['parent', VectorSelector],
338
+ ['firstChild', Identifier],
339
+ ]);
340
+
341
+ if (metricNameNode === null) {
342
+ // we are probably in a situation without a metric name
343
+ return {
344
+ type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
345
+ labelName,
346
+ betweenQuotes: inStringNode,
347
+ otherLabels,
348
+ };
349
+ }
350
+
351
+ const metricName = getNodeText(metricNameNode, text);
352
+
353
+ return {
354
+ type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME',
355
+ metricName,
356
+ labelName,
357
+ betweenQuotes: inStringNode,
358
+ otherLabels,
359
+ };
360
+ }
361
+
362
+ function resolveTopLevel(): Situation {
363
+ return {
364
+ type: 'AT_ROOT',
365
+ };
366
+ }
367
+
368
+ function resolveInFunction(): Situation {
369
+ return {
370
+ type: 'IN_FUNCTION',
371
+ };
372
+ }
373
+
374
+ function resolveDurations(): Situation {
375
+ return {
376
+ type: 'IN_DURATION',
377
+ };
378
+ }
379
+
380
+ function resolveLabelKeysWithEquals(node: SyntaxNode, text: string, pos: number): Situation | null {
381
+ // next false positive:
382
+ // `something{a="1"^}`
383
+ const child = walk(node, [['firstChild', UnquotedLabelMatcher]]);
384
+ if (child !== null) {
385
+ // means the label-matching part contains at least one label already.
386
+ //
387
+ // in this case, we will need to have a `,` character at the end,
388
+ // to be able to suggest adding the next label.
389
+ // the area between the end-of-the-child-node and the cursor-pos
390
+ // must contain a `,` in this case.
391
+ const textToCheck = text.slice(child.to, pos);
392
+
393
+ if (!textToCheck.includes(',')) {
394
+ return null;
395
+ }
396
+ }
397
+
398
+ const metricNameNode = walk(node, [
399
+ ['parent', VectorSelector],
400
+ ['firstChild', Identifier],
401
+ ]);
402
+
403
+ const otherLabels = getLabels(node, text);
404
+
405
+ if (metricNameNode === null) {
406
+ // we are probably in a situation without a metric name.
407
+ return {
408
+ type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
409
+ otherLabels,
410
+ };
411
+ }
412
+
413
+ const metricName = getNodeText(metricNameNode, text);
414
+
415
+ return {
416
+ type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME',
417
+ metricName,
418
+ otherLabels,
419
+ };
420
+ }
421
+
422
+ // we find the first error-node in the tree that is at the cursor-position.
423
+ // NOTE: this might be too slow, might need to optimize it
424
+ // (ideas: we do not need to go into every subtree, based on from/to)
425
+ // also, only go to places that are in the sub-tree of the node found
426
+ // by default by lezer. problem is, `next()` will go upward too,
427
+ // and we do not want to go higher than our node
428
+ function getErrorNode(tree: Tree, pos: number): SyntaxNode | null {
429
+ const cur = tree.cursorAt(pos);
430
+ while (true) {
431
+ if (cur.from === pos && cur.to === pos) {
432
+ const { node } = cur;
433
+ if (node.type.isError) {
434
+ return node;
435
+ }
436
+ }
437
+
438
+ if (!cur.next()) {
439
+ break;
440
+ }
441
+ }
442
+ return null;
443
+ }
444
+
445
+ export function getSituation(text: string, pos: number): Situation | null {
446
+ // there is a special-case when we are at the start of writing text,
447
+ // so we handle that case first
448
+
449
+ if (text === '') {
450
+ return {
451
+ type: 'EMPTY',
452
+ };
453
+ }
454
+
455
+ /**
456
+ PromQL
457
+ Expr
458
+ VectorSelector
459
+ LabelMatchers
460
+ */
461
+ const tree = parser.parse(text);
462
+
463
+ // if the tree contains error, it is very probable that
464
+ // our node is one of those error-nodes.
465
+ // also, if there are errors, the node lezer finds us,
466
+ // might not be the best node.
467
+ // so first we check if there is an error-node at the cursor-position
468
+ const maybeErrorNode = getErrorNode(tree, pos);
469
+
470
+ const cur = maybeErrorNode != null ? maybeErrorNode.cursor() : tree.cursorAt(pos);
471
+ const currentNode = cur.node;
472
+
473
+ const ids = [cur.type.id];
474
+ while (cur.parent()) {
475
+ ids.push(cur.type.id);
476
+ }
477
+
478
+ for (let resolver of RESOLVERS) {
479
+ // i do not use a foreach because i want to stop as soon
480
+ // as i find something
481
+ if (isPathMatch(resolver.path, ids)) {
482
+ return resolver.fun(currentNode, text, pos);
483
+ }
484
+ }
485
+
486
+ return null;
487
+ }
488
+
489
+ function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
490
+ return value !== null && value !== undefined;
491
+ }