@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.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/monaco-editor.cjs.development.js +2170 -0
- package/dist/monaco-editor.cjs.development.js.map +1 -0
- package/dist/monaco-editor.cjs.production.min.js +2 -0
- package/dist/monaco-editor.cjs.production.min.js.map +1 -0
- package/dist/monaco-editor.esm.js +2164 -0
- package/dist/monaco-editor.esm.js.map +1 -0
- package/dist/promql/completion/DataProvider.d.ts +44 -0
- package/dist/promql/completion/completions.d.ts +13 -0
- package/dist/promql/completion/getCompletionProvider.d.ts +6 -0
- package/dist/promql/completion/situation.d.ts +25 -0
- package/dist/promql/index.d.ts +14 -0
- package/dist/promql/promql.d.ts +60 -0
- package/dist/promql/types.d.ts +30 -0
- package/dist/promql/util.d.ts +6 -0
- package/dist/promql/validation.d.ts +11 -0
- package/package.json +56 -0
- package/src/index.tsx +3 -0
- package/src/promql/NOTICE.md +3 -0
- package/src/promql/completion/DataProvider.ts +252 -0
- package/src/promql/completion/completions.ts +188 -0
- package/src/promql/completion/getCompletionProvider.ts +96 -0
- package/src/promql/completion/situation.ts +491 -0
- package/src/promql/index.tsx +263 -0
- package/src/promql/promql.ts +912 -0
- package/src/promql/types.ts +35 -0
- package/src/promql/util.ts +29 -0
- package/src/promql/validation.ts +93 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { makeSelector } from '../util';
|
|
2
|
+
import type { Metric, PromMetricsMetadata, DataProviderParams } from '../types';
|
|
3
|
+
|
|
4
|
+
type CustomRequest = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
interface APIResponse<T> {
|
|
7
|
+
status: 'success' | 'error';
|
|
8
|
+
data?: T;
|
|
9
|
+
error?: string;
|
|
10
|
+
warnings?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SERIES_LIMIT = '40000';
|
|
14
|
+
const badRequest = 400;
|
|
15
|
+
const unprocessableEntity = 422;
|
|
16
|
+
const serviceUnavailable = 503;
|
|
17
|
+
const CODE_MODE_SUGGESTIONS_INCOMPLETE_EVENT = 'codeModeSuggestionsIncomplete';
|
|
18
|
+
|
|
19
|
+
export class DataProvider {
|
|
20
|
+
readonly metricNamesSuggestionLimit: number;
|
|
21
|
+
private inputInRange: string;
|
|
22
|
+
private suggestionsIncomplete: boolean;
|
|
23
|
+
private readonly lookbackInterval = 60 * 60 * 1000 * 12; // 12 hours
|
|
24
|
+
private variablesNames: string[] = [];
|
|
25
|
+
private readonly url: string;
|
|
26
|
+
private readonly errorHandler?: (error: any) => void;
|
|
27
|
+
private readonly httpMethod: 'POST' | 'GET' = 'GET';
|
|
28
|
+
private readonly apiPrefix: string = '/api/v1';
|
|
29
|
+
private readonly customRequest: CustomRequest = (input: RequestInfo, init?: RequestInit): Promise<Response> => fetch(input, init);
|
|
30
|
+
metrics: string[];
|
|
31
|
+
labelKeys: string[];
|
|
32
|
+
metricsMetadata?: PromMetricsMetadata;
|
|
33
|
+
|
|
34
|
+
constructor(params: DataProviderParams) {
|
|
35
|
+
this.inputInRange = '';
|
|
36
|
+
this.metricNamesSuggestionLimit = 1000;
|
|
37
|
+
this.suggestionsIncomplete = false;
|
|
38
|
+
|
|
39
|
+
this.url = params.url ? params.url : '';
|
|
40
|
+
this.errorHandler = params.httpErrorHandler;
|
|
41
|
+
if (params.lookbackInterval) {
|
|
42
|
+
this.lookbackInterval = params.lookbackInterval;
|
|
43
|
+
}
|
|
44
|
+
if (params.variablesNames) {
|
|
45
|
+
this.variablesNames = [...params.variablesNames];
|
|
46
|
+
}
|
|
47
|
+
if (params.request) {
|
|
48
|
+
this.customRequest = params.request;
|
|
49
|
+
}
|
|
50
|
+
if (params.httpMethod) {
|
|
51
|
+
this.httpMethod = params.httpMethod;
|
|
52
|
+
}
|
|
53
|
+
if (params.apiPrefix) {
|
|
54
|
+
this.apiPrefix = params.apiPrefix;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.metrics = [];
|
|
58
|
+
this.labelKeys = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getVariablesNames(): string[] {
|
|
62
|
+
return this.variablesNames;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setVariablesNames(variablesNames: string[]) {
|
|
66
|
+
this.variablesNames = [...variablesNames];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private buildRequest(endpoint: string, params: URLSearchParams) {
|
|
70
|
+
let uri = endpoint;
|
|
71
|
+
let body: URLSearchParams | null = params;
|
|
72
|
+
if (this.httpMethod === 'GET') {
|
|
73
|
+
uri = `${uri}?${params}`;
|
|
74
|
+
body = null;
|
|
75
|
+
}
|
|
76
|
+
return { uri, body };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private request<T>(resource: string, init?: RequestInit): Promise<T> {
|
|
80
|
+
return this.customRequest(this.url + resource, init)
|
|
81
|
+
.then((res) => {
|
|
82
|
+
if (!res.ok && ![badRequest, unprocessableEntity, serviceUnavailable].includes(res.status)) {
|
|
83
|
+
throw new Error(res.statusText);
|
|
84
|
+
}
|
|
85
|
+
return res;
|
|
86
|
+
})
|
|
87
|
+
.then((res) => res.json())
|
|
88
|
+
.then((apiRes: APIResponse<T>) => {
|
|
89
|
+
if (apiRes.status === 'error') {
|
|
90
|
+
const error = new Error(apiRes.error !== undefined ? apiRes.error : 'missing "error" field in response JSON');
|
|
91
|
+
if (this.errorHandler) {
|
|
92
|
+
this.errorHandler(error);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (apiRes.data === undefined) {
|
|
97
|
+
const error = new Error(apiRes.error !== undefined ? apiRes.error : 'missing "data" field in response JSON');
|
|
98
|
+
if (this.errorHandler) {
|
|
99
|
+
this.errorHandler(error);
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
return apiRes.data;
|
|
104
|
+
})
|
|
105
|
+
.catch((error) => {
|
|
106
|
+
if (this.errorHandler) {
|
|
107
|
+
this.errorHandler(error);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fetchSeries = async (selector: string, withLimit?: string): Promise<Record<string, string>[]> => {
|
|
114
|
+
const end = new Date();
|
|
115
|
+
const start = new Date(end.getTime() - this.lookbackInterval);
|
|
116
|
+
const url = `${this.apiPrefix}/series`;
|
|
117
|
+
let urlParams: any = {
|
|
118
|
+
start: start.toISOString() as string,
|
|
119
|
+
end: end.toISOString() as string,
|
|
120
|
+
};
|
|
121
|
+
if (selector) {
|
|
122
|
+
urlParams['match[]'] = selector;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (withLimit !== 'none') {
|
|
126
|
+
urlParams = { ...urlParams, limit: withLimit ?? DEFAULT_SERIES_LIMIT };
|
|
127
|
+
}
|
|
128
|
+
const request = this.buildRequest(url, new URLSearchParams(urlParams));
|
|
129
|
+
|
|
130
|
+
return await this.request<Record<string, string>[]>(request.uri, {
|
|
131
|
+
method: this.httpMethod,
|
|
132
|
+
body: request.body,
|
|
133
|
+
}).catch(() => {
|
|
134
|
+
return [] as Record<string, string>[];
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
fetchLabels = async (selector: string): Promise<string[]> => {
|
|
139
|
+
const end = new Date();
|
|
140
|
+
const start = new Date(end.getTime() - this.lookbackInterval);
|
|
141
|
+
const url = `${this.apiPrefix}/labels`;
|
|
142
|
+
const urlParams: any = {
|
|
143
|
+
start: start.toISOString(),
|
|
144
|
+
end: end.toISOString(),
|
|
145
|
+
};
|
|
146
|
+
if (selector) {
|
|
147
|
+
urlParams['match[]'] = selector;
|
|
148
|
+
}
|
|
149
|
+
const request = this.buildRequest(url, new URLSearchParams(urlParams));
|
|
150
|
+
|
|
151
|
+
return await this.request<string[]>(request.uri, {
|
|
152
|
+
method: this.httpMethod,
|
|
153
|
+
body: request.body,
|
|
154
|
+
})
|
|
155
|
+
.then((res) => {
|
|
156
|
+
this.labelKeys = res;
|
|
157
|
+
return res;
|
|
158
|
+
})
|
|
159
|
+
.catch(() => {
|
|
160
|
+
return [] as string[];
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
fetchLabelValues = async (labelName: string, selector: string): Promise<string[]> => {
|
|
165
|
+
const end = new Date();
|
|
166
|
+
const start = new Date(end.getTime() - this.lookbackInterval);
|
|
167
|
+
const url = `${this.apiPrefix}/label/${labelName}/values`;
|
|
168
|
+
const urlParams: any = {
|
|
169
|
+
start: start.toISOString(),
|
|
170
|
+
end: end.toISOString(),
|
|
171
|
+
};
|
|
172
|
+
if (selector) {
|
|
173
|
+
urlParams['match[]'] = selector;
|
|
174
|
+
}
|
|
175
|
+
const request = this.buildRequest(url, new URLSearchParams(urlParams));
|
|
176
|
+
|
|
177
|
+
return await this.request<string[]>(request.uri, {
|
|
178
|
+
method: this.httpMethod,
|
|
179
|
+
body: request.body,
|
|
180
|
+
}).catch(() => {
|
|
181
|
+
return [] as string[];
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
getAllMetricNames(): string[] {
|
|
186
|
+
return this.metrics;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
start = async () => {
|
|
190
|
+
this.metrics = (await this.fetchLabelValues('__name__', makeSelector('', [], '__name__'))) || [];
|
|
191
|
+
|
|
192
|
+
return Promise.all([
|
|
193
|
+
this.loadMetricsMetadata(),
|
|
194
|
+
// this.fetchLabels()
|
|
195
|
+
]);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
async loadMetricsMetadata() {
|
|
199
|
+
const request = this.buildRequest(`${this.apiPrefix}/metadata`, new URLSearchParams({}));
|
|
200
|
+
this.metricsMetadata = await this.request<PromMetricsMetadata>(request.uri, {
|
|
201
|
+
method: this.httpMethod,
|
|
202
|
+
body: request.body,
|
|
203
|
+
}).catch(() => {
|
|
204
|
+
return {} as PromMetricsMetadata;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
metricNamesToMetrics(metricNames: string[]): Metric[] {
|
|
209
|
+
const result: Metric[] = metricNames.map((m) => {
|
|
210
|
+
const metaItem = this.metricsMetadata?.[m];
|
|
211
|
+
return {
|
|
212
|
+
name: m,
|
|
213
|
+
help: metaItem?.help ?? '',
|
|
214
|
+
type: metaItem?.type ?? '',
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private setInputInRange(textInput: string): void {
|
|
222
|
+
this.inputInRange = textInput;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private enableAutocompleteSuggestionsUpdate(): void {
|
|
226
|
+
this.suggestionsIncomplete = true;
|
|
227
|
+
dispatchEvent(
|
|
228
|
+
new CustomEvent(CODE_MODE_SUGGESTIONS_INCOMPLETE_EVENT, {
|
|
229
|
+
detail: {
|
|
230
|
+
limit: this.metricNamesSuggestionLimit,
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get monacoSettings() {
|
|
237
|
+
return {
|
|
238
|
+
/**
|
|
239
|
+
* Enable autocomplete suggestions update on every input change.
|
|
240
|
+
*
|
|
241
|
+
* @remarks
|
|
242
|
+
* If fuzzy search is used in `getCompletions` to trim down results to improve performance,
|
|
243
|
+
* we need to instruct Monaco to update the completions on every input change, so that the
|
|
244
|
+
* completions reflect the current input.
|
|
245
|
+
*/
|
|
246
|
+
enableAutocompleteSuggestionsUpdate: this.enableAutocompleteSuggestionsUpdate.bind(this),
|
|
247
|
+
inputInRange: this.inputInRange,
|
|
248
|
+
setInputInRange: this.setInputInRange.bind(this),
|
|
249
|
+
suggestionsIncomplete: this.suggestionsIncomplete,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import UFuzzy from '@leeoniya/ufuzzy';
|
|
2
|
+
|
|
3
|
+
import { FUNCTIONS } from '../promql';
|
|
4
|
+
import { makeSelector, NeverCaseError } from '../util';
|
|
5
|
+
import type { DataProvider } from './DataProvider';
|
|
6
|
+
import type { Situation } from './situation';
|
|
7
|
+
import type { Label } from '../types';
|
|
8
|
+
|
|
9
|
+
export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE';
|
|
10
|
+
|
|
11
|
+
type Completion = {
|
|
12
|
+
type: CompletionType;
|
|
13
|
+
label: string;
|
|
14
|
+
insertText: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
documentation?: string;
|
|
17
|
+
triggerOnInsert?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const metricNamesSearchClient = new UFuzzy({ intraMode: 1 });
|
|
21
|
+
|
|
22
|
+
// we order items like: history, functions, metrics
|
|
23
|
+
function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[] {
|
|
24
|
+
let metricNames = dataProvider.getAllMetricNames();
|
|
25
|
+
|
|
26
|
+
if (metricNames.length > dataProvider.metricNamesSuggestionLimit) {
|
|
27
|
+
const { monacoSettings } = dataProvider;
|
|
28
|
+
monacoSettings.enableAutocompleteSuggestionsUpdate();
|
|
29
|
+
|
|
30
|
+
if (monacoSettings.inputInRange) {
|
|
31
|
+
metricNames =
|
|
32
|
+
metricNamesSearchClient
|
|
33
|
+
.filter(metricNames, monacoSettings.inputInRange)
|
|
34
|
+
?.slice(0, dataProvider.metricNamesSuggestionLimit)
|
|
35
|
+
.map((idx) => metricNames[idx]) ?? [];
|
|
36
|
+
} else {
|
|
37
|
+
metricNames = metricNames.slice(0, dataProvider.metricNamesSuggestionLimit);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return dataProvider.metricNamesToMetrics(metricNames).map((metric) => ({
|
|
42
|
+
type: 'METRIC_NAME',
|
|
43
|
+
label: metric.name,
|
|
44
|
+
insertText: metric.name,
|
|
45
|
+
detail: `${metric.name} : ${metric.type}`,
|
|
46
|
+
documentation: metric.help,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const FUNCTION_COMPLETIONS: Completion[] = FUNCTIONS.map((f) => ({
|
|
51
|
+
type: 'FUNCTION',
|
|
52
|
+
label: f.label,
|
|
53
|
+
insertText: f.insertText ?? '', // i don't know what to do when this is nullish. it should not be.
|
|
54
|
+
detail: f.detail,
|
|
55
|
+
documentation: f.documentation,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
async function getAllFunctionsAndMetricNamesCompletions(dataProvider: DataProvider): Promise<Completion[]> {
|
|
59
|
+
const metricNames = getAllMetricNamesCompletions(dataProvider);
|
|
60
|
+
|
|
61
|
+
return [...FUNCTION_COMPLETIONS, ...metricNames];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DURATION_COMPLETIONS: Completion[] = ['$__interval', '$__range', '$__rate_interval', '1m', '5m', '10m', '30m', '1h', '1d'].map((text) => ({
|
|
65
|
+
type: 'DURATION',
|
|
66
|
+
label: text,
|
|
67
|
+
insertText: text,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
function getAllHistoryCompletions(_dataProvider: DataProvider): Completion[] {
|
|
71
|
+
return [];
|
|
72
|
+
// function getAllHistoryCompletions(queryHistory: PromHistoryItem[]): Completion[] {
|
|
73
|
+
// NOTE: the typescript types are wrong. historyItem.query.expr can be undefined
|
|
74
|
+
// const allHistory = dataProvider.getHistory();
|
|
75
|
+
// FIXME: find a better history-limit
|
|
76
|
+
// return allHistory.slice(0, 10).map((expr) => ({
|
|
77
|
+
// type: 'HISTORY',
|
|
78
|
+
// label: expr,
|
|
79
|
+
// insertText: expr,
|
|
80
|
+
// }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function getLabelNames(metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider): Promise<string[]> {
|
|
84
|
+
if (metric === undefined || metric === '') {
|
|
85
|
+
const selector = makeSelector('', otherLabels);
|
|
86
|
+
return await dataProvider.fetchLabels(selector);
|
|
87
|
+
} else {
|
|
88
|
+
const selector = makeSelector(metric, otherLabels);
|
|
89
|
+
const series = await dataProvider.fetchSeries(selector);
|
|
90
|
+
const labelNames = new Set<string>();
|
|
91
|
+
for (const labelSet of series) {
|
|
92
|
+
for (const [key] of Object.entries(labelSet)) {
|
|
93
|
+
if (key === '__name__') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
labelNames.add(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Array.from(labelNames);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function getLabelNamesForCompletions(
|
|
104
|
+
metric: string | undefined,
|
|
105
|
+
suffix: string,
|
|
106
|
+
triggerOnInsert: boolean,
|
|
107
|
+
otherLabels: Label[],
|
|
108
|
+
dataProvider: DataProvider,
|
|
109
|
+
): Promise<Completion[]> {
|
|
110
|
+
const labelNames = await getLabelNames(metric, otherLabels, dataProvider);
|
|
111
|
+
return labelNames.map((text) => ({
|
|
112
|
+
type: 'LABEL_NAME',
|
|
113
|
+
label: text,
|
|
114
|
+
insertText: `${text}${suffix}`,
|
|
115
|
+
triggerOnInsert,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function getLabelNamesForSelectorCompletions(metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider): Promise<Completion[]> {
|
|
120
|
+
return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getLabelNamesForByCompletions(metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider): Promise<Completion[]> {
|
|
124
|
+
return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function getLabelValues(metric: string | undefined, labelName: string, otherLabels: Label[], dataProvider: DataProvider): Promise<string[]> {
|
|
128
|
+
if (metric === undefined || metric === '') {
|
|
129
|
+
const selector = makeSelector('', otherLabels);
|
|
130
|
+
return await dataProvider.fetchLabelValues(labelName, selector);
|
|
131
|
+
} else {
|
|
132
|
+
const selector = makeSelector(metric, otherLabels, labelName);
|
|
133
|
+
const series = await dataProvider.fetchSeries(selector);
|
|
134
|
+
const labelValues = new Set<string>();
|
|
135
|
+
for (const labelSet of series) {
|
|
136
|
+
for (const [key, value] of Object.entries(labelSet)) {
|
|
137
|
+
if (key === '__name__') {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (key === labelName) {
|
|
141
|
+
labelValues.add(value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const variablesNames = dataProvider.getVariablesNames();
|
|
146
|
+
return variablesNames.concat(Array.from(labelValues));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function getLabelValuesForMetricCompletions(
|
|
151
|
+
metric: string | undefined,
|
|
152
|
+
labelName: string,
|
|
153
|
+
betweenQuotes: boolean,
|
|
154
|
+
otherLabels: Label[],
|
|
155
|
+
dataProvider: DataProvider,
|
|
156
|
+
): Promise<Completion[]> {
|
|
157
|
+
const values = await getLabelValues(metric, labelName, otherLabels, dataProvider);
|
|
158
|
+
return values.map((text) => ({
|
|
159
|
+
type: 'LABEL_VALUE',
|
|
160
|
+
label: text,
|
|
161
|
+
insertText: betweenQuotes ? text : `"${text}"`, // FIXME: escaping strange characters?
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getCompletions(situation: Situation, dataProvider: DataProvider): Promise<Completion[]> {
|
|
166
|
+
switch (situation.type) {
|
|
167
|
+
case 'IN_DURATION':
|
|
168
|
+
return Promise.resolve(DURATION_COMPLETIONS);
|
|
169
|
+
case 'IN_FUNCTION':
|
|
170
|
+
return getAllFunctionsAndMetricNamesCompletions(dataProvider);
|
|
171
|
+
case 'AT_ROOT': {
|
|
172
|
+
return getAllFunctionsAndMetricNamesCompletions(dataProvider);
|
|
173
|
+
}
|
|
174
|
+
case 'EMPTY': {
|
|
175
|
+
const metricNames = getAllMetricNamesCompletions(dataProvider);
|
|
176
|
+
const historyCompletions = getAllHistoryCompletions(dataProvider);
|
|
177
|
+
return Promise.resolve([...historyCompletions, ...FUNCTION_COMPLETIONS, ...metricNames]);
|
|
178
|
+
}
|
|
179
|
+
case 'IN_LABEL_SELECTOR_NO_LABEL_NAME':
|
|
180
|
+
return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider);
|
|
181
|
+
case 'IN_GROUPING':
|
|
182
|
+
return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider);
|
|
183
|
+
case 'IN_LABEL_SELECTOR_WITH_LABEL_NAME':
|
|
184
|
+
return getLabelValuesForMetricCompletions(situation.metricName, situation.labelName, situation.betweenQuotes, situation.otherLabels, dataProvider);
|
|
185
|
+
default:
|
|
186
|
+
throw new NeverCaseError(situation);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api';
|
|
2
|
+
|
|
3
|
+
import { NeverCaseError } from '../util';
|
|
4
|
+
import { DataProvider } from './DataProvider';
|
|
5
|
+
import { getSituation } from './situation';
|
|
6
|
+
import { getCompletions } from './completions';
|
|
7
|
+
|
|
8
|
+
export type { monacoTypes };
|
|
9
|
+
export type Monaco = typeof monacoTypes;
|
|
10
|
+
export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE';
|
|
11
|
+
|
|
12
|
+
function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind {
|
|
13
|
+
switch (type) {
|
|
14
|
+
case 'DURATION':
|
|
15
|
+
return monaco.languages.CompletionItemKind.Unit;
|
|
16
|
+
case 'FUNCTION':
|
|
17
|
+
return monaco.languages.CompletionItemKind.Variable;
|
|
18
|
+
case 'HISTORY':
|
|
19
|
+
return monaco.languages.CompletionItemKind.Snippet;
|
|
20
|
+
case 'LABEL_NAME':
|
|
21
|
+
return monaco.languages.CompletionItemKind.Enum;
|
|
22
|
+
case 'LABEL_VALUE':
|
|
23
|
+
return monaco.languages.CompletionItemKind.EnumMember;
|
|
24
|
+
case 'METRIC_NAME':
|
|
25
|
+
return monaco.languages.CompletionItemKind.Constructor;
|
|
26
|
+
default:
|
|
27
|
+
throw new NeverCaseError(type);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getCompletionProvider(monaco: Monaco, dataProvider: DataProvider): monacoTypes.languages.CompletionItemProvider {
|
|
32
|
+
const provideCompletionItems = (
|
|
33
|
+
model: monacoTypes.editor.ITextModel,
|
|
34
|
+
position: monacoTypes.Position,
|
|
35
|
+
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CompletionList> => {
|
|
36
|
+
const word = model.getWordAtPosition(position);
|
|
37
|
+
const range =
|
|
38
|
+
word != null
|
|
39
|
+
? monaco.Range.lift({
|
|
40
|
+
startLineNumber: position.lineNumber,
|
|
41
|
+
endLineNumber: position.lineNumber,
|
|
42
|
+
startColumn: word.startColumn,
|
|
43
|
+
endColumn: word.endColumn,
|
|
44
|
+
})
|
|
45
|
+
: monaco.Range.fromPositions(position);
|
|
46
|
+
// documentation says `position` will be "adjusted" in `getOffsetAt`
|
|
47
|
+
// i don't know what that means, to be sure i clone it
|
|
48
|
+
|
|
49
|
+
const positionClone = {
|
|
50
|
+
column: position.column,
|
|
51
|
+
lineNumber: position.lineNumber,
|
|
52
|
+
};
|
|
53
|
+
dataProvider.monacoSettings.setInputInRange(model.getValueInRange(range));
|
|
54
|
+
|
|
55
|
+
// Check to see if the browser supports window.getSelection()
|
|
56
|
+
if (window.getSelection) {
|
|
57
|
+
const selectedText = window.getSelection()?.toString();
|
|
58
|
+
// If the user has selected text, adjust the cursor position to be at the start of the selection, instead of the end
|
|
59
|
+
if (selectedText && selectedText.length > 0) {
|
|
60
|
+
positionClone.column = positionClone.column - selectedText.length;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const offset = model.getOffsetAt(positionClone);
|
|
65
|
+
const situation = getSituation(model.getValue(), offset);
|
|
66
|
+
const completionsPromise = situation != null ? getCompletions(situation, dataProvider) : Promise.resolve([]);
|
|
67
|
+
|
|
68
|
+
return completionsPromise.then((items) => {
|
|
69
|
+
// monaco by-default alphabetically orders the items.
|
|
70
|
+
// to stop it, we use a number-as-string sortkey,
|
|
71
|
+
// so that monaco keeps the order we use
|
|
72
|
+
const maxIndexDigits = items.length.toString().length;
|
|
73
|
+
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({
|
|
74
|
+
kind: getMonacoCompletionItemKind(item.type, monaco),
|
|
75
|
+
label: item.label,
|
|
76
|
+
insertText: item.insertText,
|
|
77
|
+
detail: item.detail,
|
|
78
|
+
documentation: item.documentation,
|
|
79
|
+
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
|
|
80
|
+
range,
|
|
81
|
+
command: item.triggerOnInsert
|
|
82
|
+
? {
|
|
83
|
+
id: 'editor.action.triggerSuggest',
|
|
84
|
+
title: '',
|
|
85
|
+
}
|
|
86
|
+
: undefined,
|
|
87
|
+
}));
|
|
88
|
+
return { suggestions, incomplete: dataProvider.monacoSettings.suggestionsIncomplete };
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
triggerCharacters: ['{', ',', '[', '(', '=', '~', ' ', '"'],
|
|
94
|
+
provideCompletionItems,
|
|
95
|
+
};
|
|
96
|
+
}
|