@fc-components/monaco-editor 0.1.26 → 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.
- package/dist/monaco-editor.cjs.development.js +87 -21
- 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 +88 -22
- package/dist/monaco-editor.esm.js.map +1 -1
- package/dist/promql/completion/situation.d.ts +3 -0
- package/dist/promql/index.d.ts +0 -1
- package/package.json +2 -2
- 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/getCompletionProvider.ts +15 -1
- package/src/promql/completion/situation.ts +73 -1
- package/src/promql/index.tsx +0 -18
- package/src/promql/promql.ts +3 -1
|
@@ -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;
|
|
@@ -22,5 +24,6 @@ export declare type Situation = {
|
|
|
22
24
|
labelName: string;
|
|
23
25
|
betweenQuotes: boolean;
|
|
24
26
|
otherLabels: Label[];
|
|
27
|
+
valueStartPos: number;
|
|
25
28
|
};
|
|
26
29
|
export declare function getSituation(text: string, pos: number): Situation | null;
|
package/dist/promql/index.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ interface PromQLEditorProps {
|
|
|
17
17
|
onEnter?: (value: string) => void;
|
|
18
18
|
onBlur?: (value: string) => void;
|
|
19
19
|
editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void;
|
|
20
|
-
debugEscKey?: boolean;
|
|
21
20
|
}
|
|
22
21
|
export default function PromQLEditor(props: PromQLEditorProps & DataProviderParams): React.JSX.Element;
|
|
23
22
|
export {};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1
|
|
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.
|
|
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);
|
|
@@ -70,6 +70,20 @@ export function getCompletionProvider(monaco: Monaco, dataProvider: DataProvider
|
|
|
70
70
|
// to stop it, we use a number-as-string sortkey,
|
|
71
71
|
// so that monaco keeps the order we use
|
|
72
72
|
const maxIndexDigits = items.length.toString().length;
|
|
73
|
+
|
|
74
|
+
// Determine the completion range based on situation type
|
|
75
|
+
let completionRange = range;
|
|
76
|
+
if (situation && situation.type === 'IN_LABEL_SELECTOR_WITH_LABEL_NAME' && situation.betweenQuotes) {
|
|
77
|
+
// For label values within quotes, replace from the start of the value to the current position
|
|
78
|
+
const valueStartPosition = model.getPositionAt(situation.valueStartPos);
|
|
79
|
+
completionRange = monaco.Range.lift({
|
|
80
|
+
startLineNumber: valueStartPosition.lineNumber,
|
|
81
|
+
endLineNumber: position.lineNumber,
|
|
82
|
+
startColumn: valueStartPosition.column,
|
|
83
|
+
endColumn: position.column,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({
|
|
74
88
|
kind: getMonacoCompletionItemKind(item.type, monaco),
|
|
75
89
|
label: item.label,
|
|
@@ -77,7 +91,7 @@ export function getCompletionProvider(monaco: Monaco, dataProvider: DataProvider
|
|
|
77
91
|
detail: item.detail,
|
|
78
92
|
documentation: item.documentation,
|
|
79
93
|
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
|
|
80
|
-
range,
|
|
94
|
+
range: completionRange,
|
|
81
95
|
command: item.triggerOnInsert
|
|
82
96
|
? {
|
|
83
97
|
id: 'editor.action.triggerSuggest',
|
|
@@ -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;
|
|
@@ -145,6 +152,7 @@ export type Situation =
|
|
|
145
152
|
labelName: string;
|
|
146
153
|
betweenQuotes: boolean;
|
|
147
154
|
otherLabels: Label[];
|
|
155
|
+
valueStartPos: number;
|
|
148
156
|
};
|
|
149
157
|
|
|
150
158
|
type Resolver = {
|
|
@@ -215,6 +223,18 @@ const RESOLVERS: Resolver[] = [
|
|
|
215
223
|
path: [GroupingLabels],
|
|
216
224
|
fun: resolveLabelsForGrouping,
|
|
217
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
|
+
},
|
|
218
238
|
];
|
|
219
239
|
|
|
220
240
|
const LABEL_OP_MAP = new Map<number, LabelOperator>([
|
|
@@ -335,6 +355,11 @@ function resolveLabelMatcher(node: SyntaxNode, text: string, _pos: number): Situ
|
|
|
335
355
|
// - or an error node (like in `{job=^}`)
|
|
336
356
|
const inStringNode = !node.type.isError;
|
|
337
357
|
|
|
358
|
+
// calculate where the value starts
|
|
359
|
+
// for string nodes, it's after the opening quote
|
|
360
|
+
// for error nodes, it's at the node start
|
|
361
|
+
const valueStartPos = inStringNode ? node.from + 1 : node.from;
|
|
362
|
+
|
|
338
363
|
const parent = walk(node, [['parent', UnquotedLabelMatcher]]);
|
|
339
364
|
if (parent === null) {
|
|
340
365
|
return null;
|
|
@@ -370,6 +395,7 @@ function resolveLabelMatcher(node: SyntaxNode, text: string, _pos: number): Situ
|
|
|
370
395
|
labelName,
|
|
371
396
|
betweenQuotes: inStringNode,
|
|
372
397
|
otherLabels,
|
|
398
|
+
valueStartPos,
|
|
373
399
|
};
|
|
374
400
|
}
|
|
375
401
|
|
|
@@ -381,6 +407,7 @@ function resolveLabelMatcher(node: SyntaxNode, text: string, _pos: number): Situ
|
|
|
381
407
|
labelName,
|
|
382
408
|
betweenQuotes: inStringNode,
|
|
383
409
|
otherLabels,
|
|
410
|
+
valueStartPos,
|
|
384
411
|
};
|
|
385
412
|
}
|
|
386
413
|
|
|
@@ -422,6 +449,23 @@ function resolveInFunction(): Situation {
|
|
|
422
449
|
};
|
|
423
450
|
}
|
|
424
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
|
+
|
|
425
469
|
function resolveDurations(): Situation {
|
|
426
470
|
return {
|
|
427
471
|
type: 'IN_DURATION',
|
|
@@ -555,6 +599,27 @@ function getErrorNode(tree: Tree, pos: number): SyntaxNode | null {
|
|
|
555
599
|
return null;
|
|
556
600
|
}
|
|
557
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
|
+
|
|
558
623
|
export function getSituation(text: string, pos: number): Situation | null {
|
|
559
624
|
// there is a special-case when we are at the start of writing text,
|
|
560
625
|
// so we handle that case first
|
|
@@ -565,6 +630,13 @@ export function getSituation(text: string, pos: number): Situation | null {
|
|
|
565
630
|
};
|
|
566
631
|
}
|
|
567
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
|
+
|
|
568
640
|
/**
|
|
569
641
|
PromQL
|
|
570
642
|
Expr
|
package/src/promql/index.tsx
CHANGED
|
@@ -30,7 +30,6 @@ interface PromQLEditorProps {
|
|
|
30
30
|
onEnter?: (value: string) => void;
|
|
31
31
|
onBlur?: (value: string) => void;
|
|
32
32
|
editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void;
|
|
33
|
-
debugEscKey?: boolean;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
const PROMQL_LANG_ID = promLanguageDefinition.id;
|
|
@@ -104,7 +103,6 @@ export default function PromQLEditor(props: PromQLEditorProps & DataProviderPara
|
|
|
104
103
|
onEnter,
|
|
105
104
|
onBlur,
|
|
106
105
|
editorDidMount,
|
|
107
|
-
debugEscKey = false,
|
|
108
106
|
} = props;
|
|
109
107
|
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
|
|
110
108
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -213,22 +211,6 @@ export default function PromQLEditor(props: PromQLEditorProps & DataProviderPara
|
|
|
213
211
|
'!suggestWidgetVisible && isEditorFocused' + id,
|
|
214
212
|
);
|
|
215
213
|
|
|
216
|
-
editor.addCommand(
|
|
217
|
-
monaco.KeyCode.Escape,
|
|
218
|
-
() => {
|
|
219
|
-
if (debugEscKey) {
|
|
220
|
-
const suggestWidgetVisible = (editor as any)?._contextKeyService?.getContextKeyValue?.('suggestWidgetVisible');
|
|
221
|
-
// eslint-disable-next-line no-console
|
|
222
|
-
console.log('[PromQLEditor][ESC]', {
|
|
223
|
-
suggestWidgetVisible,
|
|
224
|
-
valueLength: editor.getValue().length,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
editor.trigger('keyboard', 'hideSuggestWidget', {});
|
|
228
|
-
},
|
|
229
|
-
'suggestWidgetVisible && isEditorFocused' + id,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
214
|
// Initialize previous content tracking
|
|
233
215
|
previousContentRef.current = editor.getValue();
|
|
234
216
|
lastDeletionTriggerTimeRef.current = 0;
|
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,
|