@fc-components/monaco-editor 0.1.27 → 0.3.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.
Files changed (36) hide show
  1. package/dist/expr/__tests__/__mocks__/monaco-editor.d.ts +28 -0
  2. package/dist/expr/completion/getCompletionProvider.d.ts +4 -0
  3. package/dist/expr/expr.d.ts +83 -0
  4. package/dist/expr/index.d.ts +3 -0
  5. package/dist/expr/parser/index.d.ts +3 -0
  6. package/dist/expr/parser/lexer.d.ts +27 -0
  7. package/dist/expr/parser/parser.d.ts +66 -0
  8. package/dist/expr/parser/types.d.ts +32 -0
  9. package/dist/expr/types.d.ts +17 -0
  10. package/dist/expr/validation.d.ts +12 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/monaco-editor.cjs.development.js +1986 -3
  13. package/dist/monaco-editor.cjs.development.js.map +1 -1
  14. package/dist/monaco-editor.cjs.production.min.js +1 -1
  15. package/dist/monaco-editor.cjs.production.min.js.map +1 -1
  16. package/dist/monaco-editor.esm.js +1989 -7
  17. package/dist/monaco-editor.esm.js.map +1 -1
  18. package/dist/promql/completion/situation.d.ts +2 -0
  19. package/package.json +6 -2
  20. package/src/expr/__tests__/__mocks__/monaco-editor.ts +34 -0
  21. package/src/expr/__tests__/expr.test.tsx +339 -0
  22. package/src/expr/completion/getCompletionProvider.ts +133 -0
  23. package/src/expr/expr.ts +229 -0
  24. package/src/expr/index.tsx +322 -0
  25. package/src/expr/parser/index.ts +3 -0
  26. package/src/expr/parser/lexer.ts +377 -0
  27. package/src/expr/parser/parser.ts +581 -0
  28. package/src/expr/parser/types.ts +77 -0
  29. package/src/expr/types.ts +17 -0
  30. package/src/expr/validation.ts +209 -0
  31. package/src/index.tsx +2 -0
  32. package/src/promql/__tests__/completions.test.ts +72 -0
  33. package/src/promql/__tests__/situation.test.ts +85 -0
  34. package/src/promql/completion/completions.ts +11 -2
  35. package/src/promql/completion/situation.ts +65 -1
  36. package/src/promql/promql.ts +3 -1
@@ -0,0 +1,209 @@
1
+ import * as monaco from 'monaco-editor';
2
+ import { ExprParser } from './parser';
3
+
4
+ const EXPR_LANG_ID = 'expr';
5
+
6
+ const parser = new ExprParser();
7
+
8
+ /**
9
+ * Validate an expr-lang expression and return Monaco editor markers.
10
+ * Uses the recursive descent parser for syntax errors,
11
+ * plus regex-based checks for structural issues like unbalanced quotes/brackets.
12
+ */
13
+ export const validateExpr = (expr: string): Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] => {
14
+ const markers: Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] = [];
15
+
16
+ if (!expr || expr.trim().length === 0) {
17
+ return markers;
18
+ }
19
+
20
+ // 1. Parser-based syntax validation
21
+ const parseErrors = parser.parse(expr);
22
+ for (const err of parseErrors) {
23
+ markers.push({
24
+ severity: monaco.MarkerSeverity.Error,
25
+ startLineNumber: err.startLine,
26
+ startColumn: err.startColumn,
27
+ endLineNumber: err.endLine,
28
+ endColumn: err.endColumn,
29
+ message: err.message,
30
+ });
31
+ }
32
+
33
+ // 2. Line-level structural checks (fallback for issues the parser might miss)
34
+ const structuralMarkers = validateLineLevel(expr);
35
+ markers.push(...structuralMarkers);
36
+
37
+ return markers;
38
+ };
39
+
40
+ /**
41
+ * Basic line-level validation for quotes and bracket balancing.
42
+ */
43
+ function validateLineLevel(expr: string): Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] {
44
+ const markers: Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] = [];
45
+ const lines = expr.split('\n');
46
+ let inBlockComment = false;
47
+
48
+ lines.forEach((line, index) => {
49
+ const lineNumber = index + 1;
50
+
51
+ // Track block comment state
52
+ if (!inBlockComment) {
53
+ const blockCommentStart = line.indexOf('/*');
54
+ if (blockCommentStart !== -1) {
55
+ const blockCommentEnd = line.indexOf('*/', blockCommentStart + 2);
56
+ if (blockCommentEnd === -1) {
57
+ inBlockComment = true;
58
+ }
59
+ }
60
+ } else {
61
+ const blockCommentEnd = line.indexOf('*/');
62
+ if (blockCommentEnd !== -1) {
63
+ inBlockComment = false;
64
+ }
65
+ if (inBlockComment || line.trim().startsWith('/*')) {
66
+ return;
67
+ }
68
+ }
69
+
70
+ const trimmedLine = line.trim();
71
+ if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*')) {
72
+ return;
73
+ }
74
+
75
+ // Check for unclosed quotes
76
+ const singleQuotes = countQuotesOutsideBlockComments(line);
77
+ if (singleQuotes % 2 !== 0) {
78
+ markers.push({
79
+ severity: monaco.MarkerSeverity.Warning,
80
+ startLineNumber: lineNumber,
81
+ startColumn: 1,
82
+ endLineNumber: lineNumber,
83
+ endColumn: line.length + 1,
84
+ message: "Unclosed single quote '",
85
+ source: EXPR_LANG_ID,
86
+ });
87
+ }
88
+
89
+ const doubleQuotes = countDoubleQuotesOutsideBlockComments(line);
90
+ if (doubleQuotes % 2 !== 0) {
91
+ markers.push({
92
+ severity: monaco.MarkerSeverity.Warning,
93
+ startLineNumber: lineNumber,
94
+ startColumn: 1,
95
+ endLineNumber: lineNumber,
96
+ endColumn: line.length + 1,
97
+ message: 'Unclosed double quote "',
98
+ source: EXPR_LANG_ID,
99
+ });
100
+ }
101
+
102
+ const backticks = (line.match(/`/g) || []).length;
103
+ if (backticks % 2 !== 0) {
104
+ markers.push({
105
+ severity: monaco.MarkerSeverity.Warning,
106
+ startLineNumber: lineNumber,
107
+ startColumn: 1,
108
+ endLineNumber: lineNumber,
109
+ endColumn: line.length + 1,
110
+ message: 'Unclosed backtick `',
111
+ source: EXPR_LANG_ID,
112
+ });
113
+ }
114
+
115
+ // Check for unbalanced brackets
116
+ const openParens = (line.match(/\(/g) || []).length;
117
+ const closeParens = (line.match(/\)/g) || []).length;
118
+ if (openParens > closeParens) {
119
+ markers.push({
120
+ severity: monaco.MarkerSeverity.Warning,
121
+ startLineNumber: lineNumber,
122
+ startColumn: 1,
123
+ endLineNumber: lineNumber,
124
+ endColumn: line.length + 1,
125
+ message: 'Unmatched opening parenthesis',
126
+ source: EXPR_LANG_ID,
127
+ });
128
+ } else if (closeParens > openParens) {
129
+ markers.push({
130
+ severity: monaco.MarkerSeverity.Warning,
131
+ startLineNumber: lineNumber,
132
+ startColumn: 1,
133
+ endLineNumber: lineNumber,
134
+ endColumn: line.length + 1,
135
+ message: 'Unmatched closing parenthesis',
136
+ source: EXPR_LANG_ID,
137
+ });
138
+ }
139
+
140
+ const openBrackets = (line.match(/\[/g) || []).length;
141
+ const closeBrackets = (line.match(/\]/g) || []).length;
142
+ if (openBrackets > closeBrackets) {
143
+ markers.push({
144
+ severity: monaco.MarkerSeverity.Warning,
145
+ startLineNumber: lineNumber,
146
+ startColumn: 1,
147
+ endLineNumber: lineNumber,
148
+ endColumn: line.length + 1,
149
+ message: 'Unmatched opening bracket',
150
+ source: EXPR_LANG_ID,
151
+ });
152
+ } else if (closeBrackets > openBrackets) {
153
+ markers.push({
154
+ severity: monaco.MarkerSeverity.Warning,
155
+ startLineNumber: lineNumber,
156
+ startColumn: 1,
157
+ endLineNumber: lineNumber,
158
+ endColumn: line.length + 1,
159
+ message: 'Unmatched closing bracket',
160
+ source: EXPR_LANG_ID,
161
+ });
162
+ }
163
+
164
+ const openBraces = (line.match(/\{/g) || []).length;
165
+ const closeBraces = (line.match(/\}/g) || []).length;
166
+ if (openBraces > closeBraces) {
167
+ markers.push({
168
+ severity: monaco.MarkerSeverity.Warning,
169
+ startLineNumber: lineNumber,
170
+ startColumn: 1,
171
+ endLineNumber: lineNumber,
172
+ endColumn: line.length + 1,
173
+ message: 'Unmatched opening curly brace',
174
+ source: EXPR_LANG_ID,
175
+ });
176
+ } else if (closeBraces > openBraces) {
177
+ markers.push({
178
+ severity: monaco.MarkerSeverity.Warning,
179
+ startLineNumber: lineNumber,
180
+ startColumn: 1,
181
+ endLineNumber: lineNumber,
182
+ endColumn: line.length + 1,
183
+ message: 'Unmatched closing curly brace',
184
+ source: EXPR_LANG_ID,
185
+ });
186
+ }
187
+ });
188
+
189
+ return markers;
190
+ }
191
+
192
+ function countQuotesOutsideBlockComments(line: string): number {
193
+ const withoutBlockComments = line.replace(/\/\*[\s\S]*?\*\//g, '');
194
+ return (withoutBlockComments.match(/'/g) || []).length;
195
+ }
196
+
197
+ function countDoubleQuotesOutsideBlockComments(line: string): number {
198
+ const withoutBlockComments = line.replace(/\/\*[\s\S]*?\*\//g, '');
199
+ return (withoutBlockComments.match(/"/g) || []).length;
200
+ }
201
+
202
+ /**
203
+ * Validate an expr-lang expression and return a plain array of human-readable error messages.
204
+ * Returns an empty array if the expression is valid.
205
+ */
206
+ export function checkExpr(expr: string): string[] {
207
+ const errors = parser.parse(expr);
208
+ return errors.map((e) => `${e.startLine}:${e.startColumn}: ${e.message}`);
209
+ }
package/src/index.tsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import promql from './promql';
2
2
  import yaml from './yaml';
3
3
  import sql from './sql';
4
+ import expr from './expr';
4
5
 
5
6
  export { promql as PromQLMonacoEditor };
6
7
  export { yaml as YamlMonacoEditor };
7
8
  export { sql as SqlMonacoEditor };
9
+ export { expr as ExprMonacoEditor };
@@ -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,