@fc-components/monaco-editor 0.1.27 → 0.2.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.
@@ -7,6 +7,8 @@ export declare type Situation = {
7
7
  type: 'EMPTY';
8
8
  } | {
9
9
  type: 'IN_DURATION';
10
+ } | {
11
+ type: 'IN_WITH_BODY';
10
12
  } | {
11
13
  type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME';
12
14
  metricName?: string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.27",
6
+ "version": "0.2.1",
7
7
  "license": "MIT",
8
8
  "main": "dist/index.js",
9
9
  "module": "dist/monaco-editor.esm.js",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@emotion/css": "11.13.4",
48
- "@fc-components/lezer-metricsql": "0.0.1",
48
+ "@fc-components/lezer-metricsql": "^0.0.2",
49
49
  "@leeoniya/ufuzzy": "^1.0.18",
50
50
  "@lezer/common": "1.2.3",
51
51
  "@lezer/highlight": "1.2.1",
@@ -0,0 +1,72 @@
1
+ import { getCompletions } from '../completion/completions';
2
+ import type { Situation } from '../completion/situation';
3
+ import type { DataProvider } from '../completion/DataProvider';
4
+
5
+ // Minimal mock DataProvider that returns empty data
6
+ function createMockDataProvider(overrides: Partial<DataProvider> = {}): DataProvider {
7
+ return {
8
+ getAllMetricNames: () => [],
9
+ metricNamesToMetrics: (names: string[]) => names.map((name) => ({ name, help: '', type: '' })),
10
+ fetchLabels: async () => [],
11
+ fetchSeries: async () => [],
12
+ fetchLabelValues: async () => [],
13
+ getVariablesNames: () => [],
14
+ durationVariablesCompletion: false,
15
+ ...overrides,
16
+ } as unknown as DataProvider;
17
+ }
18
+
19
+ describe('getCompletions', () => {
20
+ const mockDataProvider = createMockDataProvider();
21
+
22
+ describe('with keyword completion', () => {
23
+ it('includes "with" in EMPTY completions', async () => {
24
+ const situation: Situation = { type: 'EMPTY' };
25
+ const completions = await getCompletions(situation, mockDataProvider);
26
+ const withCompletion = completions.find((c) => c.label === 'with');
27
+ expect(withCompletion).toBeDefined();
28
+ expect(withCompletion!.insertText).toBe('with (');
29
+ expect(withCompletion!.detail).toContain('cte_name');
30
+ });
31
+
32
+ it('includes "with" in AT_ROOT completions', async () => {
33
+ const situation: Situation = { type: 'AT_ROOT' };
34
+ const completions = await getCompletions(situation, mockDataProvider);
35
+ const withCompletion = completions.find((c) => c.label === 'with');
36
+ expect(withCompletion).toBeDefined();
37
+ });
38
+
39
+ it('includes "with" in IN_WITH_BODY completions', async () => {
40
+ const situation: Situation = { type: 'IN_WITH_BODY' };
41
+ const completions = await getCompletions(situation, mockDataProvider);
42
+ const withCompletion = completions.find((c) => c.label === 'with');
43
+ expect(withCompletion).toBeDefined();
44
+ });
45
+
46
+ it('includes "with" in IN_FUNCTION completions', async () => {
47
+ const situation: Situation = { type: 'IN_FUNCTION' };
48
+ const completions = await getCompletions(situation, mockDataProvider);
49
+ const withCompletion = completions.find((c) => c.label === 'with');
50
+ expect(withCompletion).toBeDefined();
51
+ });
52
+ });
53
+
54
+ describe('Function completions', () => {
55
+ it('includes standard PromQL functions', async () => {
56
+ const situation: Situation = { type: 'AT_ROOT' };
57
+ const completions = await getCompletions(situation, mockDataProvider);
58
+ const sumCompletion = completions.find((c) => c.label === 'sum');
59
+ expect(sumCompletion).toBeDefined();
60
+ expect(sumCompletion!.type).toBe('FUNCTION');
61
+ });
62
+ });
63
+
64
+ describe('Duration completions', () => {
65
+ it('returns duration values for IN_DURATION', async () => {
66
+ const situation: Situation = { type: 'IN_DURATION' };
67
+ const completions = await getCompletions(situation, mockDataProvider);
68
+ expect(completions.length).toBeGreaterThan(0);
69
+ expect(completions.every((c) => c.type === 'DURATION')).toBe(true);
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,85 @@
1
+ import { getSituation } from '../completion/situation';
2
+
3
+ describe('getSituation', () => {
4
+ describe('EMPTY', () => {
5
+ it('returns EMPTY for empty text', () => {
6
+ expect(getSituation('', 0)).toEqual({ type: 'EMPTY' });
7
+ });
8
+ });
9
+
10
+ describe('IN_WITH_BODY', () => {
11
+ it('returns IN_WITH_BODY when cursor is after with block', () => {
12
+ const text = 'with (cpu = sum(rate(foo[5m])))\ncpu';
13
+ // find the second occurrence of 'cpu' (after the with block)
14
+ const firstCpu = text.indexOf('cpu');
15
+ const pos = text.indexOf('cpu', firstCpu + 1);
16
+ const result = getSituation(text, pos);
17
+ expect(result).toEqual({ type: 'IN_WITH_BODY' });
18
+ });
19
+
20
+ it('returns IN_WITH_BODY for simple with expression', () => {
21
+ const text = 'with (a = foo) a';
22
+ const pos = text.length; // cursor at end
23
+ const result = getSituation(text, pos);
24
+ expect(result).toEqual({ type: 'IN_WITH_BODY' });
25
+ });
26
+
27
+ it('returns IN_WITH_BODY for multi-CTE with expressions', () => {
28
+ const text = 'with (x = rate(foo[5m]), y = sum(bar))\nx + y';
29
+ const pos = text.length;
30
+ const result = getSituation(text, pos);
31
+ expect(result).toEqual({ type: 'IN_WITH_BODY' });
32
+ });
33
+
34
+ it('does NOT return IN_WITH_BODY when cursor is before closing paren', () => {
35
+ const text = 'with (a = foo|) a';
36
+ const pipePos = text.indexOf('|');
37
+ const cleanText = text.slice(0, pipePos) + text.slice(pipePos + 1);
38
+ const result = getSituation(cleanText, pipePos);
39
+ expect(result?.type).not.toBe('IN_WITH_BODY');
40
+ });
41
+
42
+ it('does NOT return IN_WITH_BODY for regular query starting with identifier containing "with"', () => {
43
+ const text = 'without_cpu';
44
+ const pos = text.length;
45
+ const result = getSituation(text, pos);
46
+ expect(result?.type).not.toBe('IN_WITH_BODY');
47
+ });
48
+ });
49
+
50
+ describe('AT_ROOT', () => {
51
+ it('returns AT_ROOT for simple metric name', () => {
52
+ const text = 'node_cpu_seconds_total';
53
+ const pos = text.length;
54
+ const result = getSituation(text, pos);
55
+ expect(result).toEqual({ type: 'AT_ROOT' });
56
+ });
57
+
58
+ it('returns AT_ROOT for function call', () => {
59
+ const text = 'rate(node_cpu_seconds_total[5m])';
60
+ const pos = text.length;
61
+ const result = getSituation(text, pos);
62
+ expect(result).toEqual({ type: 'AT_ROOT' });
63
+ });
64
+ });
65
+
66
+ describe('IN_FUNCTION', () => {
67
+ it('returns IN_FUNCTION inside function body', () => {
68
+ const text = 'sum()';
69
+ const pos = text.indexOf(')');
70
+ const result = getSituation(text, pos);
71
+ expect(result?.type).toBe('IN_FUNCTION');
72
+ });
73
+ });
74
+
75
+ describe('IN_DURATION', () => {
76
+ it('returns a situation for incomplete duration', () => {
77
+ // Cursor just after `[` but before `]` should trigger some situation
78
+ const text = 'foo[5';
79
+ const pos = text.length;
80
+ const result = getSituation(text, pos);
81
+ // Result depends on exact parse tree; not null for valid/incomplete input
82
+ expect(result).toBeDefined();
83
+ });
84
+ });
85
+ });
@@ -58,9 +58,17 @@ const FUNCTION_COMPLETIONS: Completion[] = FUNCTIONS.map((f) => ({
58
58
  async function getAllFunctionsAndMetricNamesCompletions(dataProvider: DataProvider): Promise<Completion[]> {
59
59
  const metricNames = getAllMetricNamesCompletions(dataProvider);
60
60
 
61
- return [...FUNCTION_COMPLETIONS, ...metricNames];
61
+ return [CTE_KEYWORD_COMPLETION, ...FUNCTION_COMPLETIONS, ...metricNames];
62
62
  }
63
63
 
64
+ const CTE_KEYWORD_COMPLETION: Completion = {
65
+ type: 'FUNCTION',
66
+ label: 'with',
67
+ insertText: 'with (',
68
+ detail: 'with (cte_name = expr, ...) expr',
69
+ documentation: 'Define Common Table Expressions (CTEs) for use in the query. MetricsQL extension.',
70
+ };
71
+
64
72
  const DURATION_COMPLETIONS: Completion[] = ['1m', '5m', '10m', '30m', '1h', '1d'].map((text) => ({
65
73
  type: 'DURATION',
66
74
  label: text,
@@ -177,13 +185,14 @@ export function getCompletions(situation: Situation, dataProvider: DataProvider)
177
185
  return Promise.resolve(DURATION_COMPLETIONS);
178
186
  case 'IN_FUNCTION':
179
187
  return getAllFunctionsAndMetricNamesCompletions(dataProvider);
188
+ case 'IN_WITH_BODY':
180
189
  case 'AT_ROOT': {
181
190
  return getAllFunctionsAndMetricNamesCompletions(dataProvider);
182
191
  }
183
192
  case 'EMPTY': {
184
193
  const metricNames = getAllMetricNamesCompletions(dataProvider);
185
194
  const historyCompletions = getAllHistoryCompletions(dataProvider);
186
- return Promise.resolve([...historyCompletions, ...FUNCTION_COMPLETIONS, ...metricNames]);
195
+ return Promise.resolve([...historyCompletions, CTE_KEYWORD_COMPLETION, ...FUNCTION_COMPLETIONS, ...metricNames]);
187
196
  }
188
197
  case 'IN_LABEL_SELECTOR_NO_LABEL_NAME':
189
198
  return getLabelNamesForSelectorCompletions(situation.metricName, situation.hasOperator, situation.otherLabels, dataProvider);
@@ -20,6 +20,8 @@ import {
20
20
  StringLiteral,
21
21
  UnquotedLabelMatcher,
22
22
  VectorSelector,
23
+ WithExpr,
24
+ WithAssignment,
23
25
  } from '@fc-components/lezer-metricsql';
24
26
 
25
27
  import { NeverCaseError } from '../util';
@@ -45,7 +47,9 @@ type NodeTypeId =
45
47
  | typeof EqlSingle
46
48
  | typeof Neq
47
49
  | typeof EqlRegex
48
- | typeof NeqRegex;
50
+ | typeof NeqRegex
51
+ | typeof WithExpr
52
+ | typeof WithAssignment;
49
53
 
50
54
  type Path = Array<[Direction, NodeTypeId]>;
51
55
 
@@ -128,6 +132,9 @@ export type Situation =
128
132
  | {
129
133
  type: 'IN_DURATION';
130
134
  }
135
+ | {
136
+ type: 'IN_WITH_BODY';
137
+ }
131
138
  | {
132
139
  type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME';
133
140
  metricName?: string;
@@ -216,6 +223,18 @@ const RESOLVERS: Resolver[] = [
216
223
  path: [GroupingLabels],
217
224
  fun: resolveLabelsForGrouping,
218
225
  },
226
+ {
227
+ path: [WithExpr],
228
+ fun: resolveWithExpr,
229
+ },
230
+ {
231
+ path: [WithExpr, PromQL],
232
+ fun: resolveWithExpr,
233
+ },
234
+ {
235
+ path: [WithAssignment, WithExpr],
236
+ fun: resolveWithExpr,
237
+ },
219
238
  ];
220
239
 
221
240
  const LABEL_OP_MAP = new Map<number, LabelOperator>([
@@ -430,6 +449,23 @@ function resolveInFunction(): Situation {
430
449
  };
431
450
  }
432
451
 
452
+ function resolveWithExpr(node: SyntaxNode, text: string, pos: number): Situation | null {
453
+ // Find the containing WithExpr node (node may be a WithAssignment child)
454
+ let withExprNode: SyntaxNode | null = node.type.id === WithExpr ? node : node.parent;
455
+ while (withExprNode && withExprNode.type.id !== WithExpr) {
456
+ withExprNode = withExprNode.parent;
457
+ }
458
+ if (!withExprNode) return null;
459
+
460
+ // Only return IN_WITH_BODY when cursor is in the body expression (after the closing ')')
461
+ const children = getNodeChildren(withExprNode);
462
+ const closeParen = children.find((c) => getNodeText(c, text) === ')');
463
+ if (closeParen && pos > closeParen.to) {
464
+ return { type: 'IN_WITH_BODY' };
465
+ }
466
+ return null;
467
+ }
468
+
433
469
  function resolveDurations(): Situation {
434
470
  return {
435
471
  type: 'IN_DURATION',
@@ -563,6 +599,27 @@ function getErrorNode(tree: Tree, pos: number): SyntaxNode | null {
563
599
  return null;
564
600
  }
565
601
 
602
+ function findMatchingParen(text: string, openPos: number): number {
603
+ let depth = 0;
604
+ for (let i = openPos; i < text.length; i++) {
605
+ if (text[i] === '(') depth++;
606
+ if (text[i] === ')') {
607
+ depth--;
608
+ if (depth === 0) return i;
609
+ }
610
+ }
611
+ return -1;
612
+ }
613
+
614
+ function isInWithBody(text: string, pos: number): boolean {
615
+ const withMatch = text.match(/^with\s*\(/i);
616
+ if (!withMatch) return false;
617
+ const openParenPos = withMatch[0].length - 1;
618
+ const closeParenPos = findMatchingParen(text, openParenPos);
619
+ if (closeParenPos === -1) return false;
620
+ return pos > closeParenPos;
621
+ }
622
+
566
623
  export function getSituation(text: string, pos: number): Situation | null {
567
624
  // there is a special-case when we are at the start of writing text,
568
625
  // so we handle that case first
@@ -573,6 +630,13 @@ export function getSituation(text: string, pos: number): Situation | null {
573
630
  };
574
631
  }
575
632
 
633
+ // text-based check for with body (fallback until grammar supports WithExpr)
634
+ if (isInWithBody(text, pos)) {
635
+ return {
636
+ type: 'IN_WITH_BODY',
637
+ };
638
+ }
639
+
576
640
  /**
577
641
  PromQL
578
642
  Expr
@@ -243,6 +243,8 @@ for (let _i = 0, aggregations_1 = aggregations; _i < aggregations_1.length; _i++
243
243
  // PromQL vector matching + the by and without clauses
244
244
  // (https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching)
245
245
  const vectorMatching = ['on', 'ignoring', 'group_right', 'group_left', 'by', 'without'];
246
+
247
+ const cteKeywords = ['with'];
246
248
  // Produce a regex matching elements : (elt1|elt2|...)
247
249
  const vectorMatchingRegex =
248
250
  '(' +
@@ -257,7 +259,7 @@ const operators = ['+', '-', '*', '/', '%', '^', '==', '!=', '>', '<', '>=', '<=
257
259
  // (https://prometheus.io/docs/prometheus/latest/querying/basics/#offset-modifier)
258
260
  const offsetModifier = ['offset'];
259
261
  // Merging all the keywords in one list
260
- const keywords = aggregations.concat(functions).concat(aggregationsOverTime).concat(vectorMatching).concat(offsetModifier);
262
+ const keywords = aggregations.concat(functions).concat(aggregationsOverTime).concat(vectorMatching).concat(offsetModifier).concat(cteKeywords);
261
263
  // noinspection JSUnusedGlobalSymbols
262
264
  export const language = {
263
265
  ignoreCase: false,