@bestcodetools/graphql-playground 0.0.0
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/.dockerignore +8 -0
- package/.github/workflows/docker-publish.yml +48 -0
- package/.github/workflows/npm-publish.yml +22 -0
- package/Dockerfile +28 -0
- package/README.md +110 -0
- package/jest.config.js +8 -0
- package/package.json +49 -0
- package/public/components/config-editor.html +38 -0
- package/public/components/field-args.html +16 -0
- package/public/components/field-type.html +27 -0
- package/public/components/headers-editor.html +3 -0
- package/public/components/query-editor.html +3 -0
- package/public/components/response-viewer.html +3 -0
- package/public/components/schema-viewer.html +74 -0
- package/public/components/variables-editor.html +3 -0
- package/public/index.html +114 -0
- package/public/js/components/config-editor.js +122 -0
- package/public/js/components/headers-editor.js +562 -0
- package/public/js/components/query-editor.js +1896 -0
- package/public/js/components/response-viewer.js +201 -0
- package/public/js/components/schema-viewer.js +644 -0
- package/public/js/components/variables-editor.js +1258 -0
- package/public/js/main.js +1016 -0
- package/public/styles/main.css +1313 -0
- package/public/styles/main.css.map +1 -0
- package/public/styles/main.scss +1319 -0
- package/src/middleware.ts +36 -0
- package/src/standalone.ts +14 -0
- package/tests/standalone.test.ts +74 -0
- package/tsconfig.json +71 -0
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
((app) => {
|
|
2
|
+
const TEMPLATE_VERSION = '20260409i';
|
|
3
|
+
|
|
4
|
+
app.component('variablesEditor', {
|
|
5
|
+
templateUrl: `components/variables-editor.html?v=${TEMPLATE_VERSION}`,
|
|
6
|
+
bindings: {
|
|
7
|
+
variables: '=',
|
|
8
|
+
query: '<',
|
|
9
|
+
schema: '<',
|
|
10
|
+
api: '=?',
|
|
11
|
+
onChange: '&?'
|
|
12
|
+
},
|
|
13
|
+
controller: ['$element', function ($element) {
|
|
14
|
+
const $ctrl = this;
|
|
15
|
+
let editor = null;
|
|
16
|
+
let tooltipElement = null;
|
|
17
|
+
let hideTooltipTimeout = null;
|
|
18
|
+
let resizeHandler = null;
|
|
19
|
+
|
|
20
|
+
function ensureVariablesMode() {
|
|
21
|
+
if (typeof window.CodeMirror === 'undefined') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (window.CodeMirror.modes && window.CodeMirror.modes['graphql-variables']) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
window.CodeMirror.defineMode('graphql-variables', function () {
|
|
30
|
+
return {
|
|
31
|
+
startState() {
|
|
32
|
+
return {
|
|
33
|
+
inString: false,
|
|
34
|
+
escaped: false
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
token(stream, state) {
|
|
38
|
+
if (state.inString) {
|
|
39
|
+
let next;
|
|
40
|
+
|
|
41
|
+
while ((next = stream.next()) != null) {
|
|
42
|
+
if (next === '"' && !state.escaped) {
|
|
43
|
+
state.inString = false;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
state.escaped = !state.escaped && next === '\\';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return 'string';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (stream.eatSpace()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (stream.peek() === '"') {
|
|
58
|
+
stream.next();
|
|
59
|
+
state.inString = true;
|
|
60
|
+
state.escaped = false;
|
|
61
|
+
return 'string';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stream.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)) {
|
|
65
|
+
return 'number';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (stream.match(/^(?:true|false|null)\b/)) {
|
|
69
|
+
return 'atom';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (stream.match(/^[{}\[\],:]/)) {
|
|
73
|
+
return 'bracket';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
stream.next();
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stripComments(query) {
|
|
84
|
+
return (query || '').replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractVariableDefinitions(query) {
|
|
88
|
+
const cleanQuery = stripComments(query);
|
|
89
|
+
const operationMatch = /(query|mutation|subscription)\b/.exec(cleanQuery);
|
|
90
|
+
|
|
91
|
+
if (!operationMatch) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const operationIndex = operationMatch.index;
|
|
96
|
+
const openParenIndex = cleanQuery.indexOf('(', operationIndex);
|
|
97
|
+
|
|
98
|
+
if (openParenIndex === -1) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let depth = 0;
|
|
103
|
+
let closeParenIndex = -1;
|
|
104
|
+
|
|
105
|
+
for (let index = openParenIndex; index < cleanQuery.length; index += 1) {
|
|
106
|
+
const char = cleanQuery[index];
|
|
107
|
+
|
|
108
|
+
if (char === '(') {
|
|
109
|
+
depth += 1;
|
|
110
|
+
} else if (char === ')') {
|
|
111
|
+
depth -= 1;
|
|
112
|
+
|
|
113
|
+
if (depth === 0) {
|
|
114
|
+
closeParenIndex = index;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (closeParenIndex === -1) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const definitionsText = cleanQuery.slice(openParenIndex + 1, closeParenIndex);
|
|
125
|
+
const parts = [];
|
|
126
|
+
let current = '';
|
|
127
|
+
let bracketDepth = 0;
|
|
128
|
+
|
|
129
|
+
for (let index = 0; index < definitionsText.length; index += 1) {
|
|
130
|
+
const char = definitionsText[index];
|
|
131
|
+
|
|
132
|
+
if (char === '[') {
|
|
133
|
+
bracketDepth += 1;
|
|
134
|
+
} else if (char === ']') {
|
|
135
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (char === ',' && bracketDepth === 0) {
|
|
139
|
+
if (current.trim()) {
|
|
140
|
+
parts.push(current.trim());
|
|
141
|
+
}
|
|
142
|
+
current = '';
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
current += char;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (current.trim()) {
|
|
150
|
+
parts.push(current.trim());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return parts.map((part) => {
|
|
154
|
+
const match = /^\s*\$([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=]+?)(?:\s*=\s*.+)?\s*$/.exec(part);
|
|
155
|
+
|
|
156
|
+
if (!match) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
name: match[1],
|
|
162
|
+
typeString: match[2].trim()
|
|
163
|
+
};
|
|
164
|
+
}).filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseTypeString(typeString) {
|
|
168
|
+
let index = 0;
|
|
169
|
+
|
|
170
|
+
function parseTypeRef() {
|
|
171
|
+
if (typeString[index] === '[') {
|
|
172
|
+
index += 1;
|
|
173
|
+
const innerType = parseTypeRef();
|
|
174
|
+
|
|
175
|
+
if (typeString[index] === ']') {
|
|
176
|
+
index += 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let typeRef = {
|
|
180
|
+
kind: 'LIST',
|
|
181
|
+
ofType: innerType
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (typeString[index] === '!') {
|
|
185
|
+
index += 1;
|
|
186
|
+
typeRef = {
|
|
187
|
+
kind: 'NON_NULL',
|
|
188
|
+
ofType: typeRef
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return typeRef;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nameMatch = /^[A-Za-z_][A-Za-z0-9_]*/.exec(typeString.slice(index));
|
|
196
|
+
|
|
197
|
+
if (!nameMatch) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
index += nameMatch[0].length;
|
|
202
|
+
|
|
203
|
+
let typeRef = {
|
|
204
|
+
kind: 'NAMED',
|
|
205
|
+
name: nameMatch[0]
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (typeString[index] === '!') {
|
|
209
|
+
index += 1;
|
|
210
|
+
typeRef = {
|
|
211
|
+
kind: 'NON_NULL',
|
|
212
|
+
ofType: typeRef
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return typeRef;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return parseTypeRef();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getNamedType(typeRef) {
|
|
223
|
+
if (!typeRef) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (typeRef.name) {
|
|
228
|
+
return typeRef;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return typeRef.ofType ? getNamedType(typeRef.ofType) : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getTypeMap(schema) {
|
|
235
|
+
if (!schema || !Array.isArray(schema.types)) {
|
|
236
|
+
return new Map();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return new Map(schema.types.filter((type) => type && type.name).map((type) => [type.name, type]));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildTypeTokens(typeRef) {
|
|
243
|
+
if (!typeRef) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (typeRef.kind === 'LIST') {
|
|
248
|
+
return [
|
|
249
|
+
{ text: '[', className: 'field-type-prefix' },
|
|
250
|
+
...buildTypeTokens(typeRef.ofType),
|
|
251
|
+
{ text: ']', className: 'field-type-suffix' }
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (typeRef.kind === 'NON_NULL') {
|
|
256
|
+
return [
|
|
257
|
+
...buildTypeTokens(typeRef.ofType),
|
|
258
|
+
{ text: '!', className: 'field-type-non-null' }
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [{
|
|
263
|
+
text: typeRef.name || '',
|
|
264
|
+
className: 'field-type'
|
|
265
|
+
}];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function ensureTooltipElement() {
|
|
269
|
+
if (tooltipElement || typeof document === 'undefined') {
|
|
270
|
+
return tooltipElement;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
tooltipElement = document.createElement('div');
|
|
274
|
+
tooltipElement.className = 'schema-tooltip';
|
|
275
|
+
tooltipElement.style.display = 'none';
|
|
276
|
+
document.body.appendChild(tooltipElement);
|
|
277
|
+
return tooltipElement;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function renderTooltip(payload, eventTarget, event) {
|
|
281
|
+
const tooltip = ensureTooltipElement();
|
|
282
|
+
|
|
283
|
+
if (!tooltip || !payload || !eventTarget || typeof eventTarget.getBoundingClientRect !== 'function') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const rect = eventTarget.getBoundingClientRect();
|
|
288
|
+
const tooltipWidth = 320;
|
|
289
|
+
const gap = 8;
|
|
290
|
+
const maxLeft = Math.max(gap, window.innerWidth - tooltipWidth - gap);
|
|
291
|
+
const anchorLeft = event && typeof event.clientX === 'number' ? event.clientX : rect.left;
|
|
292
|
+
const anchorTop = event && typeof event.clientY === 'number' ? event.clientY : rect.bottom;
|
|
293
|
+
const top = Math.min(window.innerHeight - 120, anchorTop + gap);
|
|
294
|
+
const left = Math.min(anchorLeft, maxLeft);
|
|
295
|
+
const typeTokens = buildTypeTokens(payload.typeRef);
|
|
296
|
+
const nameClass = payload.labelClass || 'argument';
|
|
297
|
+
|
|
298
|
+
tooltip.innerHTML = `
|
|
299
|
+
<div class="schema-tooltip-signature">
|
|
300
|
+
<span class="schema-tooltip-name schema-tooltip-name-${nameClass}"></span>
|
|
301
|
+
${typeTokens.length ? '<span>:</span><span class="schema-tooltip-type-tokens"></span>' : ''}
|
|
302
|
+
</div>
|
|
303
|
+
${payload.description ? '<div class="schema-tooltip-description"></div>' : ''}
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
const nameElement = tooltip.querySelector('.schema-tooltip-name');
|
|
307
|
+
if (nameElement) {
|
|
308
|
+
nameElement.textContent = payload.label || '';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const typeTokensContainer = tooltip.querySelector('.schema-tooltip-type-tokens');
|
|
312
|
+
if (typeTokensContainer) {
|
|
313
|
+
typeTokens.forEach((token) => {
|
|
314
|
+
const tokenElement = document.createElement('span');
|
|
315
|
+
tokenElement.className = token.className;
|
|
316
|
+
tokenElement.textContent = token.text;
|
|
317
|
+
typeTokensContainer.appendChild(tokenElement);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const descriptionElement = tooltip.querySelector('.schema-tooltip-description');
|
|
322
|
+
if (descriptionElement) {
|
|
323
|
+
descriptionElement.textContent = payload.description || '';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
tooltip.style.top = `${Math.max(gap, top)}px`;
|
|
327
|
+
tooltip.style.left = `${Math.max(gap, left)}px`;
|
|
328
|
+
tooltip.style.display = 'block';
|
|
329
|
+
tooltip.classList.remove('is-visible');
|
|
330
|
+
|
|
331
|
+
if (hideTooltipTimeout) {
|
|
332
|
+
window.clearTimeout(hideTooltipTimeout);
|
|
333
|
+
hideTooltipTimeout = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
window.requestAnimationFrame(() => {
|
|
337
|
+
if (tooltip) {
|
|
338
|
+
tooltip.classList.add('is-visible');
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function hideTooltip() {
|
|
344
|
+
if (tooltipElement) {
|
|
345
|
+
tooltipElement.classList.remove('is-visible');
|
|
346
|
+
|
|
347
|
+
if (hideTooltipTimeout) {
|
|
348
|
+
window.clearTimeout(hideTooltipTimeout);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
hideTooltipTimeout = window.setTimeout(() => {
|
|
352
|
+
if (tooltipElement) {
|
|
353
|
+
tooltipElement.style.display = 'none';
|
|
354
|
+
}
|
|
355
|
+
hideTooltipTimeout = null;
|
|
356
|
+
}, 300);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function typeToDisplayString(typeRef) {
|
|
361
|
+
if (!typeRef) {
|
|
362
|
+
return '';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (typeRef.kind === 'LIST') {
|
|
366
|
+
return `[${typeToDisplayString(typeRef.ofType)}]`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (typeRef.kind === 'NON_NULL') {
|
|
370
|
+
return `${typeToDisplayString(typeRef.ofType)}!`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return typeRef.name || '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildDefaultValue(typeRef, schema, includeOptionalFields) {
|
|
377
|
+
if (!typeRef) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (typeRef.kind === 'NON_NULL') {
|
|
382
|
+
return buildDefaultValue(typeRef.ofType, schema, includeOptionalFields);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (typeRef.kind === 'LIST') {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const namedType = getNamedType(typeRef);
|
|
390
|
+
const typeMap = getTypeMap(schema);
|
|
391
|
+
const schemaType = namedType ? typeMap.get(namedType.name) : null;
|
|
392
|
+
|
|
393
|
+
if (!schemaType) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (schemaType.kind === 'INPUT_OBJECT') {
|
|
398
|
+
const result = {};
|
|
399
|
+
|
|
400
|
+
(schemaType.inputFields || []).forEach((field) => {
|
|
401
|
+
if (field && field.type && (includeOptionalFields || field.type.kind === 'NON_NULL')) {
|
|
402
|
+
result[field.name] = buildDefaultValue(field.type, schema, includeOptionalFields);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (schemaType.kind === 'ENUM') {
|
|
410
|
+
return schemaType.enumValues && schemaType.enumValues.length ? schemaType.enumValues[0].name : null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
switch (namedType.name) {
|
|
414
|
+
case 'Int':
|
|
415
|
+
case 'Float':
|
|
416
|
+
return 0;
|
|
417
|
+
case 'Boolean':
|
|
418
|
+
return false;
|
|
419
|
+
case 'String':
|
|
420
|
+
case 'ID':
|
|
421
|
+
return '';
|
|
422
|
+
default:
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getVariableSuggestions(query, schema) {
|
|
428
|
+
const typeMap = getTypeMap(schema);
|
|
429
|
+
|
|
430
|
+
return extractVariableDefinitions(query).map((definition) => {
|
|
431
|
+
const typeRef = parseTypeString(definition.typeString);
|
|
432
|
+
const defaultValue = buildDefaultValue(typeRef, schema, false);
|
|
433
|
+
const defaultValueWithOptionalFields = buildDefaultValue(typeRef, schema, true);
|
|
434
|
+
const namedType = getNamedType(typeRef);
|
|
435
|
+
const schemaType = namedType ? typeMap.get(namedType.name) : null;
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
name: definition.name,
|
|
439
|
+
typeRef,
|
|
440
|
+
typeString: typeToDisplayString(typeRef),
|
|
441
|
+
defaultValue,
|
|
442
|
+
defaultValueWithOptionalFields,
|
|
443
|
+
supportsOptionalFieldExpansion: !!(schemaType && schemaType.kind === 'INPUT_OBJECT')
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getVariableDefinitionMap(query, schema) {
|
|
449
|
+
return new Map(getVariableSuggestions(query, schema).map((suggestion) => [suggestion.name, suggestion]));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function unwrapTypeRef(typeRef) {
|
|
453
|
+
if (!typeRef) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (typeRef.kind === 'NON_NULL') {
|
|
458
|
+
return unwrapTypeRef(typeRef.ofType);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return typeRef;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function resolveInputField(typeRef, key, schema) {
|
|
465
|
+
const namedType = getNamedType(typeRef);
|
|
466
|
+
const typeMap = getTypeMap(schema);
|
|
467
|
+
const schemaType = namedType ? typeMap.get(namedType.name) : null;
|
|
468
|
+
|
|
469
|
+
if (!schemaType || schemaType.kind !== 'INPUT_OBJECT' || !Array.isArray(schemaType.inputFields)) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return schemaType.inputFields.find((field) => field && field.name === key) || null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function collectVariableHoverTargets(text, query, schema) {
|
|
477
|
+
const targets = [];
|
|
478
|
+
const rootVariables = getVariableDefinitionMap(query, schema);
|
|
479
|
+
|
|
480
|
+
function skipWhitespace(index) {
|
|
481
|
+
while (index < text.length && /\s/.test(text[index])) {
|
|
482
|
+
index += 1;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return index;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function parseString(index) {
|
|
489
|
+
const start = index;
|
|
490
|
+
index += 1;
|
|
491
|
+
let escaped = false;
|
|
492
|
+
|
|
493
|
+
while (index < text.length) {
|
|
494
|
+
const char = text[index];
|
|
495
|
+
|
|
496
|
+
if (char === '"' && !escaped) {
|
|
497
|
+
return {
|
|
498
|
+
value: text.slice(start + 1, index),
|
|
499
|
+
start,
|
|
500
|
+
end: index + 1,
|
|
501
|
+
nextIndex: index + 1
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
escaped = !escaped && char === '\\';
|
|
506
|
+
index += 1;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function parsePrimitive(index, expectedTypeRef, label, description) {
|
|
513
|
+
const start = index;
|
|
514
|
+
|
|
515
|
+
while (index < text.length && !/[,\]\}\s]/.test(text[index])) {
|
|
516
|
+
index += 1;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
targets.push({
|
|
520
|
+
start,
|
|
521
|
+
end: index,
|
|
522
|
+
payload: {
|
|
523
|
+
label,
|
|
524
|
+
labelClass: 'argument',
|
|
525
|
+
typeRef: expectedTypeRef,
|
|
526
|
+
description: description || ''
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
return index;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function parseValue(index, expectedTypeRef, label, description) {
|
|
534
|
+
index = skipWhitespace(index);
|
|
535
|
+
|
|
536
|
+
if (index >= text.length) {
|
|
537
|
+
return index;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (text[index] === '{') {
|
|
541
|
+
const objectStart = index;
|
|
542
|
+
const nextIndex = parseObject(index, expectedTypeRef, label);
|
|
543
|
+
targets.push({
|
|
544
|
+
start: objectStart,
|
|
545
|
+
end: objectStart + 1,
|
|
546
|
+
payload: {
|
|
547
|
+
label,
|
|
548
|
+
labelClass: 'argument',
|
|
549
|
+
typeRef: expectedTypeRef,
|
|
550
|
+
description: description || ''
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
return nextIndex;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (text[index] === '[') {
|
|
557
|
+
const arrayStart = index;
|
|
558
|
+
index += 1;
|
|
559
|
+
const listType = unwrapTypeRef(expectedTypeRef);
|
|
560
|
+
const itemType = listType && listType.kind === 'LIST' ? listType.ofType : null;
|
|
561
|
+
|
|
562
|
+
targets.push({
|
|
563
|
+
start: arrayStart,
|
|
564
|
+
end: arrayStart + 1,
|
|
565
|
+
payload: {
|
|
566
|
+
label,
|
|
567
|
+
labelClass: 'argument',
|
|
568
|
+
typeRef: expectedTypeRef,
|
|
569
|
+
description: description || ''
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
while (index < text.length && text[index] !== ']') {
|
|
574
|
+
index = parseValue(index, itemType, label, description);
|
|
575
|
+
index = skipWhitespace(index);
|
|
576
|
+
|
|
577
|
+
if (text[index] === ',') {
|
|
578
|
+
index += 1;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return text[index] === ']' ? index + 1 : index;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (text[index] === '"') {
|
|
586
|
+
const stringToken = parseString(index);
|
|
587
|
+
|
|
588
|
+
if (stringToken) {
|
|
589
|
+
targets.push({
|
|
590
|
+
start: stringToken.start,
|
|
591
|
+
end: stringToken.end,
|
|
592
|
+
payload: {
|
|
593
|
+
label,
|
|
594
|
+
labelClass: 'argument',
|
|
595
|
+
typeRef: expectedTypeRef,
|
|
596
|
+
description: description || ''
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
return stringToken.nextIndex;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return parsePrimitive(index, expectedTypeRef, label, description);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function parseObject(index, expectedTypeRef, labelPrefix) {
|
|
608
|
+
if (text[index] !== '{') {
|
|
609
|
+
return index;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
index += 1;
|
|
613
|
+
|
|
614
|
+
while (index < text.length) {
|
|
615
|
+
index = skipWhitespace(index);
|
|
616
|
+
|
|
617
|
+
if (text[index] === '}') {
|
|
618
|
+
return index + 1;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (text[index] !== '"') {
|
|
622
|
+
index += 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const keyToken = parseString(index);
|
|
627
|
+
|
|
628
|
+
if (!keyToken) {
|
|
629
|
+
return index;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const fieldRef = labelPrefix
|
|
633
|
+
? resolveInputField(expectedTypeRef, keyToken.value, schema)
|
|
634
|
+
: (rootVariables.get(keyToken.value) || null);
|
|
635
|
+
const keyLabel = keyToken.value;
|
|
636
|
+
const keyTypeRef = fieldRef ? (fieldRef.type || fieldRef.typeRef) : null;
|
|
637
|
+
const keyDescription = fieldRef ? (fieldRef.description || '') : '';
|
|
638
|
+
|
|
639
|
+
targets.push({
|
|
640
|
+
start: keyToken.start,
|
|
641
|
+
end: keyToken.end,
|
|
642
|
+
payload: {
|
|
643
|
+
label: keyLabel,
|
|
644
|
+
labelClass: 'argument',
|
|
645
|
+
typeRef: keyTypeRef,
|
|
646
|
+
description: keyDescription
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
index = skipWhitespace(keyToken.nextIndex);
|
|
651
|
+
|
|
652
|
+
if (text[index] === ':') {
|
|
653
|
+
index += 1;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
index = parseValue(index, keyTypeRef, keyLabel, keyDescription);
|
|
657
|
+
index = skipWhitespace(index);
|
|
658
|
+
|
|
659
|
+
if (text[index] === ',') {
|
|
660
|
+
index += 1;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return index;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
parseObject(skipWhitespace(0), null, '');
|
|
668
|
+
return targets;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function resolveVariablesHoverPayload(cm, position) {
|
|
672
|
+
const text = cm.getValue();
|
|
673
|
+
const index = cm.indexFromPos(position);
|
|
674
|
+
const targets = collectVariableHoverTargets(text, $ctrl.query, $ctrl.schema);
|
|
675
|
+
|
|
676
|
+
for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
|
|
677
|
+
const target = targets[targetIndex];
|
|
678
|
+
|
|
679
|
+
if (index >= target.start && index <= target.end) {
|
|
680
|
+
return target.payload;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function bindHoverHandlers(cm) {
|
|
688
|
+
const wrapper = cm.getWrapperElement();
|
|
689
|
+
let lastTooltipKey = '';
|
|
690
|
+
let hoverTimer = null;
|
|
691
|
+
|
|
692
|
+
function clearHoverTimer() {
|
|
693
|
+
if (hoverTimer) {
|
|
694
|
+
window.clearTimeout(hoverTimer);
|
|
695
|
+
hoverTimer = null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
wrapper.addEventListener('mousemove', (event) => {
|
|
700
|
+
const target = event.target;
|
|
701
|
+
|
|
702
|
+
if (!target || !(target instanceof HTMLElement) || !target.closest('.CodeMirror-lines')) {
|
|
703
|
+
clearHoverTimer();
|
|
704
|
+
hideTooltip();
|
|
705
|
+
lastTooltipKey = '';
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const position = cm.coordsChar({ left: event.clientX, top: event.clientY }, 'window');
|
|
710
|
+
const payload = resolveVariablesHoverPayload(cm, position);
|
|
711
|
+
|
|
712
|
+
if (!payload || !payload.typeRef) {
|
|
713
|
+
clearHoverTimer();
|
|
714
|
+
hideTooltip();
|
|
715
|
+
lastTooltipKey = '';
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const nextKey = `${payload.label}|${payload.description}|${typeToDisplayString(payload.typeRef)}`;
|
|
720
|
+
|
|
721
|
+
if (lastTooltipKey !== nextKey) {
|
|
722
|
+
clearHoverTimer();
|
|
723
|
+
hideTooltip();
|
|
724
|
+
lastTooltipKey = nextKey;
|
|
725
|
+
hoverTimer = window.setTimeout(() => {
|
|
726
|
+
renderTooltip(payload, target, event);
|
|
727
|
+
hoverTimer = null;
|
|
728
|
+
}, 1000);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
wrapper.addEventListener('mouseleave', () => {
|
|
734
|
+
clearHoverTimer();
|
|
735
|
+
lastTooltipKey = '';
|
|
736
|
+
hideTooltip();
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function getExistingRootKeys(text) {
|
|
741
|
+
const keys = new Set();
|
|
742
|
+
let depth = 0;
|
|
743
|
+
let inString = false;
|
|
744
|
+
let escaped = false;
|
|
745
|
+
let pendingKey = null;
|
|
746
|
+
|
|
747
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
748
|
+
const char = text[index];
|
|
749
|
+
|
|
750
|
+
if (inString) {
|
|
751
|
+
if (char === '"' && !escaped) {
|
|
752
|
+
inString = false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
escaped = !escaped && char === '\\';
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (char === '"') {
|
|
760
|
+
inString = true;
|
|
761
|
+
escaped = false;
|
|
762
|
+
|
|
763
|
+
let end = index + 1;
|
|
764
|
+
let key = '';
|
|
765
|
+
let localEscaped = false;
|
|
766
|
+
|
|
767
|
+
while (end < text.length) {
|
|
768
|
+
const next = text[end];
|
|
769
|
+
|
|
770
|
+
if (next === '"' && !localEscaped) {
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
key += next;
|
|
775
|
+
localEscaped = !localEscaped && next === '\\';
|
|
776
|
+
end += 1;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
pendingKey = {
|
|
780
|
+
value: key,
|
|
781
|
+
end
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (char === '{') {
|
|
788
|
+
depth += 1;
|
|
789
|
+
} else if (char === '}') {
|
|
790
|
+
depth = Math.max(0, depth - 1);
|
|
791
|
+
} else if (char === ':' && depth === 1 && pendingKey) {
|
|
792
|
+
keys.add(pendingKey.value);
|
|
793
|
+
pendingKey = null;
|
|
794
|
+
} else if (!/\s/.test(char)) {
|
|
795
|
+
pendingKey = null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return keys;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function getRootInsertionContext(text, cursorIndex) {
|
|
803
|
+
const openBraceIndex = text.indexOf('{');
|
|
804
|
+
const closeBraceIndex = text.lastIndexOf('}');
|
|
805
|
+
|
|
806
|
+
if (openBraceIndex === -1 || closeBraceIndex === -1 || cursorIndex < openBraceIndex || cursorIndex > closeBraceIndex) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let depth = 0;
|
|
811
|
+
let inString = false;
|
|
812
|
+
let escaped = false;
|
|
813
|
+
|
|
814
|
+
for (let index = 0; index < cursorIndex; index += 1) {
|
|
815
|
+
const char = text[index];
|
|
816
|
+
|
|
817
|
+
if (inString) {
|
|
818
|
+
if (char === '"' && !escaped) {
|
|
819
|
+
inString = false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
escaped = !escaped && char === '\\';
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (char === '"') {
|
|
827
|
+
inString = true;
|
|
828
|
+
escaped = false;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (char === '{') {
|
|
833
|
+
depth += 1;
|
|
834
|
+
} else if (char === '}') {
|
|
835
|
+
depth = Math.max(0, depth - 1);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (depth !== 1) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let from = cursorIndex;
|
|
844
|
+
while (from > openBraceIndex && /[A-Za-z0-9_"]/.test(text[from - 1])) {
|
|
845
|
+
from -= 1;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
let to = cursorIndex;
|
|
849
|
+
while (to < closeBraceIndex && /[A-Za-z0-9_"]/.test(text[to])) {
|
|
850
|
+
to += 1;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
from,
|
|
855
|
+
to,
|
|
856
|
+
openBraceIndex,
|
|
857
|
+
closeBraceIndex
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildValueSnippet(value, baseIndent) {
|
|
862
|
+
if (typeof value === 'string') {
|
|
863
|
+
return {
|
|
864
|
+
text: '""',
|
|
865
|
+
cursorOffset: 1
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (typeof value === 'number') {
|
|
870
|
+
const text = String(value);
|
|
871
|
+
return {
|
|
872
|
+
text,
|
|
873
|
+
cursorOffset: 0,
|
|
874
|
+
selectionLength: text.length
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (typeof value === 'boolean') {
|
|
879
|
+
const text = value ? 'true' : 'false';
|
|
880
|
+
return {
|
|
881
|
+
text,
|
|
882
|
+
cursorOffset: 0
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (value === null) {
|
|
887
|
+
return {
|
|
888
|
+
text: 'null',
|
|
889
|
+
cursorOffset: 0
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (Array.isArray(value)) {
|
|
894
|
+
return {
|
|
895
|
+
text: '[]',
|
|
896
|
+
cursorOffset: 1
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (value && typeof value === 'object') {
|
|
901
|
+
const entries = Object.entries(value);
|
|
902
|
+
|
|
903
|
+
if (!entries.length) {
|
|
904
|
+
return {
|
|
905
|
+
text: '{}',
|
|
906
|
+
cursorOffset: 1
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const childIndent = `${baseIndent} `;
|
|
911
|
+
const childSnippets = entries.map(([key, childValue], index) => {
|
|
912
|
+
const childSnippet = buildPropertySnippet(key, childValue, childIndent);
|
|
913
|
+
return {
|
|
914
|
+
...childSnippet,
|
|
915
|
+
text: `${childIndent}${childSnippet.text}${index < entries.length - 1 ? ',' : ''}`
|
|
916
|
+
};
|
|
917
|
+
});
|
|
918
|
+
const firstChild = childSnippets[0];
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
text: `{\n${childSnippets.map((snippet) => snippet.text).join('\n')}\n${baseIndent}}`,
|
|
922
|
+
cursorOffset: `{\n`.length + firstChild.cursorOffset
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
text: 'null',
|
|
928
|
+
cursorOffset: 0
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function buildPropertySnippet(name, defaultValue, baseIndent) {
|
|
933
|
+
const keyText = `"${name}": `;
|
|
934
|
+
const valueSnippet = buildValueSnippet(defaultValue, baseIndent);
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
text: `${keyText}${valueSnippet.text}`,
|
|
938
|
+
cursorOffset: keyText.length + valueSnippet.cursorOffset,
|
|
939
|
+
selectionLength: valueSnippet.selectionLength || 0
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function applyVariableCompletion(cm, data, completion) {
|
|
944
|
+
const text = cm.getValue();
|
|
945
|
+
const cursor = cm.getCursor();
|
|
946
|
+
const cursorIndex = cm.indexFromPos(cursor);
|
|
947
|
+
const context = getRootInsertionContext(text, cursorIndex);
|
|
948
|
+
|
|
949
|
+
if (!context) {
|
|
950
|
+
cm.replaceRange(`"${completion.variableName}": ${JSON.stringify(completion.defaultValue)}`, data.from, data.to);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const baseIndent = ' ';
|
|
955
|
+
const snippet = buildPropertySnippet(completion.variableName, completion.defaultValue, baseIndent);
|
|
956
|
+
const before = text.slice(context.openBraceIndex + 1, context.from);
|
|
957
|
+
const after = text.slice(context.to, context.closeBraceIndex);
|
|
958
|
+
const needsCommaBefore = /[}\]"0-9a-zA-Z]/.test(before.trim().slice(-1)) && !before.trim().endsWith(',');
|
|
959
|
+
const needsCommaAfter = after.trim() && !after.trim().startsWith('}') && !after.trim().startsWith(',');
|
|
960
|
+
const lineStartIndex = text.lastIndexOf('\n', Math.max(0, context.from - 1)) + 1;
|
|
961
|
+
const currentLineText = text.slice(lineStartIndex, context.from);
|
|
962
|
+
const shouldReuseCurrentLine = currentLineText.trim() === '';
|
|
963
|
+
const prefix = before.trim() ? `\n${baseIndent}` : '\n ';
|
|
964
|
+
let commaAttachIndex = context.from;
|
|
965
|
+
|
|
966
|
+
if (needsCommaBefore) {
|
|
967
|
+
let previousValueIndex = context.from - 1;
|
|
968
|
+
|
|
969
|
+
while (previousValueIndex > context.openBraceIndex && /\s/.test(text[previousValueIndex])) {
|
|
970
|
+
previousValueIndex -= 1;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
commaAttachIndex = previousValueIndex + 1;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (shouldReuseCurrentLine) {
|
|
977
|
+
let insertionShift = 0;
|
|
978
|
+
|
|
979
|
+
if (needsCommaBefore) {
|
|
980
|
+
cm.replaceRange(',', cm.posFromIndex(commaAttachIndex), cm.posFromIndex(commaAttachIndex));
|
|
981
|
+
if (commaAttachIndex < lineStartIndex) {
|
|
982
|
+
insertionShift = 1;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const lineInsertion = `${baseIndent}${snippet.text}${needsCommaAfter ? ',' : ''}`;
|
|
987
|
+
const adjustedLineStartIndex = lineStartIndex + insertionShift;
|
|
988
|
+
const adjustedContextTo = context.to + insertionShift;
|
|
989
|
+
const lineEndIndex = text.indexOf('\n', adjustedLineStartIndex);
|
|
990
|
+
const replaceToIndex = lineEndIndex === -1 ? adjustedContextTo : Math.min(adjustedContextTo, lineEndIndex);
|
|
991
|
+
|
|
992
|
+
cm.replaceRange(lineInsertion, cm.posFromIndex(adjustedLineStartIndex), cm.posFromIndex(replaceToIndex));
|
|
993
|
+
|
|
994
|
+
const startIndex = adjustedLineStartIndex + lineInsertion.indexOf(snippet.text) + snippet.cursorOffset;
|
|
995
|
+
|
|
996
|
+
if (snippet.selectionLength) {
|
|
997
|
+
cm.setSelection(
|
|
998
|
+
cm.posFromIndex(startIndex),
|
|
999
|
+
cm.posFromIndex(startIndex + snippet.selectionLength)
|
|
1000
|
+
);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
cm.setCursor(cm.posFromIndex(startIndex));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const insertLeadingCommaInline = needsCommaBefore && !shouldReuseCurrentLine;
|
|
1009
|
+
const insertionPrefix = `${insertLeadingCommaInline ? ',' : ''}${prefix}`;
|
|
1010
|
+
const insertion = `${insertionPrefix}${snippet.text}${needsCommaAfter ? ',' : ''}`;
|
|
1011
|
+
const replaceFromIndex = commaAttachIndex;
|
|
1012
|
+
|
|
1013
|
+
cm.replaceRange(insertion, cm.posFromIndex(replaceFromIndex), cm.posFromIndex(context.to));
|
|
1014
|
+
|
|
1015
|
+
const startIndex = replaceFromIndex + insertion.indexOf(snippet.text) + snippet.cursorOffset;
|
|
1016
|
+
|
|
1017
|
+
if (snippet.selectionLength) {
|
|
1018
|
+
cm.setSelection(
|
|
1019
|
+
cm.posFromIndex(startIndex),
|
|
1020
|
+
cm.posFromIndex(startIndex + snippet.selectionLength)
|
|
1021
|
+
);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
cm.setCursor(cm.posFromIndex(startIndex));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function buildVariableCompletions(query, schema, currentText) {
|
|
1029
|
+
const existingKeys = getExistingRootKeys(currentText);
|
|
1030
|
+
|
|
1031
|
+
return getVariableSuggestions(query, schema)
|
|
1032
|
+
.filter((suggestion) => !existingKeys.has(suggestion.name))
|
|
1033
|
+
.flatMap((suggestion) => {
|
|
1034
|
+
const completions = [{
|
|
1035
|
+
text: suggestion.name,
|
|
1036
|
+
displayText: `${suggestion.name}: ${suggestion.typeString}`,
|
|
1037
|
+
className: 'cm-hint-argument',
|
|
1038
|
+
variableName: suggestion.name,
|
|
1039
|
+
defaultValue: suggestion.defaultValue,
|
|
1040
|
+
hint(cm, data, completion) {
|
|
1041
|
+
applyVariableCompletion(cm, data, completion);
|
|
1042
|
+
}
|
|
1043
|
+
}];
|
|
1044
|
+
|
|
1045
|
+
if (suggestion.supportsOptionalFieldExpansion) {
|
|
1046
|
+
completions.push({
|
|
1047
|
+
text: suggestion.name,
|
|
1048
|
+
displayText: `* ${suggestion.name}: ${suggestion.typeString} (add optional fields too)`,
|
|
1049
|
+
className: 'cm-hint-field',
|
|
1050
|
+
variableName: suggestion.name,
|
|
1051
|
+
defaultValue: suggestion.defaultValueWithOptionalFields,
|
|
1052
|
+
hint(cm, data, completion) {
|
|
1053
|
+
applyVariableCompletion(cm, data, completion);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return completions;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function getVariableHints(cm) {
|
|
1063
|
+
const cursor = cm.getCursor();
|
|
1064
|
+
const cursorIndex = cm.indexFromPos(cursor);
|
|
1065
|
+
const text = cm.getValue();
|
|
1066
|
+
const context = getRootInsertionContext(text, cursorIndex);
|
|
1067
|
+
const token = cm.getTokenAt(cursor);
|
|
1068
|
+
const rawToken = token && typeof token.string === 'string' ? token.string.replace(/"/g, '') : '';
|
|
1069
|
+
const prefix = /^[A-Za-z_][A-Za-z0-9_]*$/.test(rawToken) ? rawToken.slice(0, Math.max(0, cursor.ch - token.start)) : '';
|
|
1070
|
+
const suggestions = buildVariableCompletions($ctrl.query, $ctrl.schema, text).filter((completion) => {
|
|
1071
|
+
if (!prefix) {
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return completion.variableName.toLowerCase().startsWith(prefix.toLowerCase());
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
return {
|
|
1079
|
+
list: suggestions,
|
|
1080
|
+
from: context ? cm.posFromIndex(context.from) : cursor,
|
|
1081
|
+
to: context ? cm.posFromIndex(context.to) : cursor
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function triggerAutocomplete(cm) {
|
|
1086
|
+
if (typeof cm.showHint !== 'function') {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
cm.showHint({
|
|
1091
|
+
completeSingle: false,
|
|
1092
|
+
hint: getVariableHints
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function formatEditor() {
|
|
1097
|
+
if (!editor) {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
try {
|
|
1102
|
+
const parsed = JSON.parse(editor.getValue() || '{}');
|
|
1103
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
1104
|
+
|
|
1105
|
+
if (editor.getValue() !== formatted) {
|
|
1106
|
+
editor.setValue(formatted);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return true;
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function createEditor() {
|
|
1116
|
+
const textarea = $element[0].querySelector('textarea');
|
|
1117
|
+
|
|
1118
|
+
if (!textarea || typeof window.CodeMirror === 'undefined') {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
ensureVariablesMode();
|
|
1123
|
+
|
|
1124
|
+
editor = window.CodeMirror.fromTextArea(textarea, {
|
|
1125
|
+
mode: 'graphql-variables',
|
|
1126
|
+
lineNumbers: true,
|
|
1127
|
+
lineWrapping: true,
|
|
1128
|
+
matchBrackets: true,
|
|
1129
|
+
indentUnit: 2,
|
|
1130
|
+
tabSize: 2,
|
|
1131
|
+
extraKeys: {
|
|
1132
|
+
Tab(cm) {
|
|
1133
|
+
if (cm.state.completionActive) {
|
|
1134
|
+
cm.execCommand('pick');
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
cm.replaceSelection(' ', 'end');
|
|
1139
|
+
},
|
|
1140
|
+
'Ctrl-Space'(cm) {
|
|
1141
|
+
triggerAutocomplete(cm);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
editor.setValue($ctrl.variables || '{}');
|
|
1147
|
+
|
|
1148
|
+
editor.on('change', (instance) => {
|
|
1149
|
+
const nextValue = instance.getValue();
|
|
1150
|
+
|
|
1151
|
+
if ($ctrl.variables === nextValue) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
$ctrl.variables = nextValue;
|
|
1156
|
+
|
|
1157
|
+
if ($ctrl.onChange) {
|
|
1158
|
+
$ctrl.onChange();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const rootScope = $element.scope();
|
|
1162
|
+
if (rootScope && !rootScope.$$phase) {
|
|
1163
|
+
rootScope.$applyAsync();
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
editor.on('inputRead', (cm, change) => {
|
|
1168
|
+
if (!change || change.origin === 'setValue' || typeof cm.showHint !== 'function') {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const insertedText = Array.isArray(change.text) ? change.text.join('') : '';
|
|
1173
|
+
|
|
1174
|
+
if (/^[A-Za-z_"]$/.test(insertedText)) {
|
|
1175
|
+
triggerAutocomplete(cm);
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
bindHoverHandlers(editor);
|
|
1180
|
+
scheduleRefresh();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function scheduleRefresh() {
|
|
1184
|
+
if (!editor) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
window.requestAnimationFrame(() => {
|
|
1189
|
+
if (editor) {
|
|
1190
|
+
editor.refresh();
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
$ctrl.$postLink = function () {
|
|
1196
|
+
$ctrl.api = $ctrl.api || {};
|
|
1197
|
+
$ctrl.api.format = formatEditor;
|
|
1198
|
+
$ctrl.api.refresh = scheduleRefresh;
|
|
1199
|
+
createEditor();
|
|
1200
|
+
resizeHandler = () => scheduleRefresh();
|
|
1201
|
+
window.addEventListener('resize', resizeHandler);
|
|
1202
|
+
window.setTimeout(scheduleRefresh, 0);
|
|
1203
|
+
window.setTimeout(scheduleRefresh, 120);
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
$ctrl.$onChanges = function (changes) {
|
|
1207
|
+
if (!editor || !changes.variables) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const nextValue = changes.variables.currentValue || '{}';
|
|
1212
|
+
|
|
1213
|
+
if (editor.getValue() !== nextValue) {
|
|
1214
|
+
const cursor = editor.getCursor();
|
|
1215
|
+
editor.setValue(nextValue);
|
|
1216
|
+
editor.setCursor(cursor);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
$ctrl.$doCheck = function () {
|
|
1221
|
+
if (!editor) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const nextValue = typeof $ctrl.variables === 'string' ? $ctrl.variables : '{}';
|
|
1226
|
+
|
|
1227
|
+
if (editor.getValue() === nextValue) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const cursor = editor.getCursor();
|
|
1232
|
+
editor.setValue(nextValue);
|
|
1233
|
+
editor.setCursor(cursor);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
$ctrl.$onDestroy = function () {
|
|
1237
|
+
if (resizeHandler) {
|
|
1238
|
+
window.removeEventListener('resize', resizeHandler);
|
|
1239
|
+
resizeHandler = null;
|
|
1240
|
+
}
|
|
1241
|
+
if (editor) {
|
|
1242
|
+
hideTooltip();
|
|
1243
|
+
editor.toTextArea();
|
|
1244
|
+
editor = null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (tooltipElement && tooltipElement.parentNode) {
|
|
1248
|
+
if (hideTooltipTimeout) {
|
|
1249
|
+
window.clearTimeout(hideTooltipTimeout);
|
|
1250
|
+
hideTooltipTimeout = null;
|
|
1251
|
+
}
|
|
1252
|
+
tooltipElement.parentNode.removeChild(tooltipElement);
|
|
1253
|
+
tooltipElement = null;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}]
|
|
1257
|
+
});
|
|
1258
|
+
})(angular.module('app'));
|