@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.
- package/dist/expr/__tests__/__mocks__/monaco-editor.d.ts +28 -0
- package/dist/expr/completion/getCompletionProvider.d.ts +4 -0
- package/dist/expr/expr.d.ts +83 -0
- package/dist/expr/index.d.ts +3 -0
- package/dist/expr/parser/index.d.ts +3 -0
- package/dist/expr/parser/lexer.d.ts +27 -0
- package/dist/expr/parser/parser.d.ts +66 -0
- package/dist/expr/parser/types.d.ts +32 -0
- package/dist/expr/types.d.ts +17 -0
- package/dist/expr/validation.d.ts +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/monaco-editor.cjs.development.js +1986 -3
- package/dist/monaco-editor.cjs.development.js.map +1 -1
- package/dist/monaco-editor.cjs.production.min.js +1 -1
- package/dist/monaco-editor.cjs.production.min.js.map +1 -1
- package/dist/monaco-editor.esm.js +1989 -7
- package/dist/monaco-editor.esm.js.map +1 -1
- package/dist/promql/completion/situation.d.ts +2 -0
- package/package.json +6 -2
- package/src/expr/__tests__/__mocks__/monaco-editor.ts +34 -0
- package/src/expr/__tests__/expr.test.tsx +339 -0
- package/src/expr/completion/getCompletionProvider.ts +133 -0
- package/src/expr/expr.ts +229 -0
- package/src/expr/index.tsx +322 -0
- package/src/expr/parser/index.ts +3 -0
- package/src/expr/parser/lexer.ts +377 -0
- package/src/expr/parser/parser.ts +581 -0
- package/src/expr/parser/types.ts +77 -0
- package/src/expr/types.ts +17 -0
- package/src/expr/validation.ts +209 -0
- package/src/index.tsx +2 -0
- package/src/promql/__tests__/completions.test.ts +72 -0
- package/src/promql/__tests__/situation.test.ts +85 -0
- package/src/promql/completion/completions.ts +11 -2
- package/src/promql/completion/situation.ts +65 -1
- 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
|
package/src/promql/promql.ts
CHANGED
|
@@ -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,
|