@neo4j-cypher/react-codemirror 2.0.0-next.16 → 2.0.0-next.17
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/CHANGELOG.md +15 -0
- package/dist/CypherEditor.d.ts +5 -1
- package/dist/CypherEditor.js +20 -0
- package/dist/CypherEditor.js.map +1 -1
- package/dist/e2e_tests/autoCompletion.spec.js +80 -35
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/e2e_tests/e2eUtils.d.ts +1 -0
- package/dist/e2e_tests/e2eUtils.js +13 -2
- package/dist/e2e_tests/e2eUtils.js.map +1 -1
- package/dist/e2e_tests/performanceTest.spec.js +1 -1
- package/dist/e2e_tests/performanceTest.spec.js.map +1 -1
- package/dist/e2e_tests/signatureHelp.spec.js +59 -13
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
- package/dist/e2e_tests/snippets.spec.js +2 -2
- package/dist/e2e_tests/snippets.spec.js.map +1 -1
- package/dist/e2e_tests/syntaxValidation.spec.js +25 -10
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -1
- package/dist/lang-cypher/autocomplete.js +9 -4
- package/dist/lang-cypher/autocomplete.js.map +1 -1
- package/dist/lang-cypher/langCypher.d.ts +1 -0
- package/dist/lang-cypher/langCypher.js +1 -8
- package/dist/lang-cypher/langCypher.js.map +1 -1
- package/dist/lang-cypher/lintWorker.d.ts +8 -4
- package/dist/lang-cypher/lintWorker.js +12 -2
- package/dist/lang-cypher/lintWorker.js.map +1 -1
- package/dist/lang-cypher/syntaxValidation.d.ts +0 -1
- package/dist/lang-cypher/syntaxValidation.js +2 -25
- package/dist/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/CypherEditor.tsx +32 -4
- package/src/e2e_tests/autoCompletion.spec.tsx +119 -54
- package/src/e2e_tests/e2eUtils.ts +14 -2
- package/src/e2e_tests/performanceTest.spec.tsx +1 -1
- package/src/e2e_tests/signatureHelp.spec.tsx +74 -13
- package/src/e2e_tests/snippets.spec.tsx +2 -2
- package/src/e2e_tests/syntaxValidation.spec.tsx +58 -17
- package/src/lang-cypher/autocomplete.ts +13 -7
- package/src/lang-cypher/langCypher.ts +3 -12
- package/src/lang-cypher/lintWorker.ts +24 -7
- package/src/lang-cypher/syntaxValidation.ts +2 -39
|
@@ -194,14 +194,60 @@ test('can complete rel types', async ({ page, mount }) => {
|
|
|
194
194
|
await expect(component).toContainText('MATCH (n)-[:KNOWS');
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
test('can complete YIELD clauses without manual trigger', async ({
|
|
198
|
+
page,
|
|
199
|
+
mount,
|
|
200
|
+
}) => {
|
|
201
|
+
const component = await mount(
|
|
202
|
+
<CypherEditor
|
|
203
|
+
schema={{
|
|
204
|
+
procedures: testData.mockSchema.procedures,
|
|
205
|
+
}}
|
|
206
|
+
/>,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const textField = page.getByRole('textbox');
|
|
210
|
+
|
|
211
|
+
await textField.fill('CALL dbms.components() YIELD ');
|
|
212
|
+
|
|
213
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('edition').click();
|
|
214
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
215
|
+
|
|
216
|
+
await expect(component).toContainText('CALL dbms.components() YIELD edition');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('automatic yield trigger is not case sensitive', async ({
|
|
220
|
+
page,
|
|
221
|
+
mount,
|
|
222
|
+
}) => {
|
|
223
|
+
const component = await mount(
|
|
224
|
+
<CypherEditor
|
|
225
|
+
schema={{
|
|
226
|
+
procedures: testData.mockSchema.procedures,
|
|
227
|
+
}}
|
|
228
|
+
/>,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const textField = page.getByRole('textbox');
|
|
232
|
+
|
|
233
|
+
await textField.fill('CALL dbms.components() yIeLd ');
|
|
234
|
+
|
|
235
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('edition').click();
|
|
236
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
237
|
+
|
|
238
|
+
await expect(component).toContainText('CALL dbms.components() yIeLd edition');
|
|
239
|
+
});
|
|
240
|
+
|
|
197
241
|
test('can complete functions', async ({ page, mount }) => {
|
|
198
242
|
const component = await mount(
|
|
199
243
|
<CypherEditor
|
|
200
244
|
schema={{
|
|
201
245
|
functions: {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
'CYPHER 5': {
|
|
247
|
+
function123: {
|
|
248
|
+
...testData.emptyFunction,
|
|
249
|
+
name: 'function123',
|
|
250
|
+
},
|
|
205
251
|
},
|
|
206
252
|
},
|
|
207
253
|
}}
|
|
@@ -227,7 +273,9 @@ test('can complete procedures', async ({ page, mount }) => {
|
|
|
227
273
|
<CypherEditor
|
|
228
274
|
schema={{
|
|
229
275
|
procedures: {
|
|
230
|
-
'
|
|
276
|
+
'CYPHER 5': {
|
|
277
|
+
'db.ping': { ...testData.emptyProcedure, name: 'db.ping' },
|
|
278
|
+
},
|
|
231
279
|
},
|
|
232
280
|
}}
|
|
233
281
|
/>,
|
|
@@ -308,16 +356,9 @@ test('shows signature help information on auto-completion for procedures', async
|
|
|
308
356
|
page,
|
|
309
357
|
mount,
|
|
310
358
|
}) => {
|
|
311
|
-
await mount(
|
|
312
|
-
<CypherEditor
|
|
313
|
-
schema={testData.mockSchema}
|
|
314
|
-
featureFlags={{
|
|
315
|
-
signatureInfoOnAutoCompletions: true,
|
|
316
|
-
}}
|
|
317
|
-
/>,
|
|
318
|
-
);
|
|
359
|
+
await mount(<CypherEditor schema={testData.mockSchema} />);
|
|
319
360
|
const procName = 'apoc.periodic.iterate';
|
|
320
|
-
const procedure = testData.mockSchema.procedures[procName];
|
|
361
|
+
const procedure = testData.mockSchema.procedures['CYPHER 5'][procName];
|
|
321
362
|
|
|
322
363
|
const textField = page.getByRole('textbox');
|
|
323
364
|
await textField.fill('CALL apoc.periodic.');
|
|
@@ -333,16 +374,9 @@ test('shows signature help information on auto-completion for functions', async
|
|
|
333
374
|
page,
|
|
334
375
|
mount,
|
|
335
376
|
}) => {
|
|
336
|
-
await mount(
|
|
337
|
-
<CypherEditor
|
|
338
|
-
schema={testData.mockSchema}
|
|
339
|
-
featureFlags={{
|
|
340
|
-
signatureInfoOnAutoCompletions: true,
|
|
341
|
-
}}
|
|
342
|
-
/>,
|
|
343
|
-
);
|
|
377
|
+
await mount(<CypherEditor schema={testData.mockSchema} />);
|
|
344
378
|
const fnName = 'apoc.coll.combinations';
|
|
345
|
-
const fn = testData.mockSchema.functions[fnName];
|
|
379
|
+
const fn = testData.mockSchema.functions['CYPHER 5'][fnName];
|
|
346
380
|
|
|
347
381
|
const textField = page.getByRole('textbox');
|
|
348
382
|
await textField.fill('RETURN apoc.coll.');
|
|
@@ -363,10 +397,11 @@ test('shows deprecated procedures as strikethrough on auto-completion', async ({
|
|
|
363
397
|
await mount(
|
|
364
398
|
<CypherEditor
|
|
365
399
|
schema={{
|
|
366
|
-
procedures: {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
400
|
+
procedures: {
|
|
401
|
+
'CYPHER 5': {
|
|
402
|
+
[procName]: testData.mockSchema.procedures['CYPHER 5'][procName],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
370
405
|
}}
|
|
371
406
|
/>,
|
|
372
407
|
);
|
|
@@ -387,10 +422,11 @@ test('shows deprecated function as strikethrough on auto-completion', async ({
|
|
|
387
422
|
await mount(
|
|
388
423
|
<CypherEditor
|
|
389
424
|
schema={{
|
|
390
|
-
functions: {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
425
|
+
functions: {
|
|
426
|
+
'CYPHER 5': {
|
|
427
|
+
[fnName]: testData.mockSchema.functions['CYPHER 5'][fnName],
|
|
428
|
+
},
|
|
429
|
+
},
|
|
394
430
|
}}
|
|
395
431
|
/>,
|
|
396
432
|
);
|
|
@@ -406,14 +442,7 @@ test('does not signature help information on auto-completion if docs and signatu
|
|
|
406
442
|
page,
|
|
407
443
|
mount,
|
|
408
444
|
}) => {
|
|
409
|
-
await mount(
|
|
410
|
-
<CypherEditor
|
|
411
|
-
schema={testData.mockSchema}
|
|
412
|
-
featureFlags={{
|
|
413
|
-
signatureInfoOnAutoCompletions: true,
|
|
414
|
-
}}
|
|
415
|
-
/>,
|
|
416
|
-
);
|
|
445
|
+
await mount(<CypherEditor schema={testData.mockSchema} />);
|
|
417
446
|
|
|
418
447
|
const textField = page.getByRole('textbox');
|
|
419
448
|
await textField.fill('C');
|
|
@@ -430,17 +459,16 @@ test('shows signature help information on auto-completion if description is not
|
|
|
430
459
|
<CypherEditor
|
|
431
460
|
schema={{
|
|
432
461
|
procedures: {
|
|
433
|
-
'
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
462
|
+
'CYPHER 5': {
|
|
463
|
+
'db.ping': {
|
|
464
|
+
...testData.emptyProcedure,
|
|
465
|
+
description: 'foo',
|
|
466
|
+
signature: '',
|
|
467
|
+
name: 'db.ping',
|
|
468
|
+
},
|
|
438
469
|
},
|
|
439
470
|
},
|
|
440
471
|
}}
|
|
441
|
-
featureFlags={{
|
|
442
|
-
signatureInfoOnAutoCompletions: true,
|
|
443
|
-
}}
|
|
444
472
|
/>,
|
|
445
473
|
);
|
|
446
474
|
|
|
@@ -459,17 +487,16 @@ test('shows signature help information on auto-completion if signature is not em
|
|
|
459
487
|
<CypherEditor
|
|
460
488
|
schema={{
|
|
461
489
|
procedures: {
|
|
462
|
-
'
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
490
|
+
'CYPHER 5': {
|
|
491
|
+
'db.ping': {
|
|
492
|
+
...testData.emptyProcedure,
|
|
493
|
+
description: '',
|
|
494
|
+
signature: 'foo',
|
|
495
|
+
name: 'db.ping',
|
|
496
|
+
},
|
|
467
497
|
},
|
|
468
498
|
},
|
|
469
499
|
}}
|
|
470
|
-
featureFlags={{
|
|
471
|
-
signatureInfoOnAutoCompletions: true,
|
|
472
|
-
}}
|
|
473
500
|
/>,
|
|
474
501
|
);
|
|
475
502
|
|
|
@@ -479,3 +506,41 @@ test('shows signature help information on auto-completion if signature is not em
|
|
|
479
506
|
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
|
|
480
507
|
await expect(page.locator('.cm-completionInfo')).toBeVisible();
|
|
481
508
|
});
|
|
509
|
+
|
|
510
|
+
test('completions depend on the Cypher version', async ({ page, mount }) => {
|
|
511
|
+
await mount(
|
|
512
|
+
<CypherEditor
|
|
513
|
+
schema={{
|
|
514
|
+
functions: {
|
|
515
|
+
'CYPHER 5': {
|
|
516
|
+
cypher5Function: {
|
|
517
|
+
...testData.emptyFunction,
|
|
518
|
+
name: 'cypher5Function',
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
'CYPHER 25': {
|
|
522
|
+
cypher25Function: {
|
|
523
|
+
...testData.emptyFunction,
|
|
524
|
+
name: 'cypher25Function',
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
}}
|
|
529
|
+
featureFlags={{ cypher25: true }}
|
|
530
|
+
/>,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const textField = page.getByRole('textbox');
|
|
534
|
+
|
|
535
|
+
await textField.fill('CYPHER 5 RETURN cypher');
|
|
536
|
+
|
|
537
|
+
await expect(
|
|
538
|
+
page.locator('.cm-tooltip-autocomplete').getByText('cypher5Function'),
|
|
539
|
+
).toBeVisible();
|
|
540
|
+
|
|
541
|
+
await textField.fill('CYPHER 25 RETURN cypher');
|
|
542
|
+
|
|
543
|
+
await expect(
|
|
544
|
+
page.locator('.cm-tooltip-autocomplete').getByText('cypher25Function'),
|
|
545
|
+
).toBeVisible();
|
|
546
|
+
});
|
|
@@ -59,17 +59,29 @@ export class CypherEditorPage {
|
|
|
59
59
|
return this.checkNotificationMessage('warning', queryChunk, expectedMsg);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
async checkNoNotificationMessage(type: 'error' | 'warning') {
|
|
63
|
+
await this.page.waitForTimeout(10000);
|
|
64
|
+
await expect(this.page.locator('.cm-lintRange-' + type)).toHaveCount(0, {
|
|
65
|
+
timeout: 10000,
|
|
66
|
+
});
|
|
67
|
+
await expect(this.page.locator('.cm-lintPoint-' + type)).toHaveCount(0, {
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
private async checkNotificationMessage(
|
|
63
73
|
type: 'error' | 'warning',
|
|
64
74
|
queryChunk: string,
|
|
65
75
|
expectedMsg: string,
|
|
66
76
|
) {
|
|
67
77
|
await expect(this.page.locator('.cm-lintRange-' + type).last()).toBeVisible(
|
|
68
|
-
{ timeout:
|
|
78
|
+
{ timeout: 10000 },
|
|
69
79
|
);
|
|
70
80
|
|
|
71
81
|
await this.page.getByText(queryChunk, { exact: true }).hover();
|
|
72
|
-
await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible(
|
|
82
|
+
await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible({
|
|
83
|
+
timeout: 10000,
|
|
84
|
+
});
|
|
73
85
|
await expect(this.page.getByText(expectedMsg)).toBeVisible();
|
|
74
86
|
/* Return the mouse to the beginning of the query and
|
|
75
87
|
This is because if for example we have an overlay with a
|
|
@@ -100,7 +100,7 @@ test('benchmarking & performance test session', async ({
|
|
|
100
100
|
|
|
101
101
|
await editorPage.checkErrorMessage(
|
|
102
102
|
'RETRN',
|
|
103
|
-
'
|
|
103
|
+
`Invalid input 'RETRN': expected a graph pattern, 'FOREACH', ',', 'ORDER BY', 'CALL', 'CREATE', 'LOAD CSV', 'DELETE', 'DETACH', 'FINISH', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REMOVE', 'RETURN', 'SET', 'SKIP', 'UNION', 'UNWIND', 'USE', 'USING', 'WHERE', 'WITH' or <EOF>`,
|
|
104
104
|
);
|
|
105
105
|
|
|
106
106
|
await editorPage
|
|
@@ -11,7 +11,8 @@ type TooltipExpectations = {
|
|
|
11
11
|
excludes?: string[];
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const importCsvProc =
|
|
14
|
+
const importCsvProc =
|
|
15
|
+
testData.mockSchema.procedures['CYPHER 5']['apoc.import.csv'];
|
|
15
16
|
|
|
16
17
|
function testTooltip(tooltip: Locator, expectations: TooltipExpectations) {
|
|
17
18
|
const includes = expectations.includes ?? [];
|
|
@@ -20,7 +21,7 @@ function testTooltip(tooltip: Locator, expectations: TooltipExpectations) {
|
|
|
20
21
|
const included = Promise.all(
|
|
21
22
|
includes.map((text) => {
|
|
22
23
|
return expect(tooltip).toContainText(text, {
|
|
23
|
-
timeout:
|
|
24
|
+
timeout: 10000,
|
|
24
25
|
});
|
|
25
26
|
}),
|
|
26
27
|
);
|
|
@@ -28,7 +29,7 @@ function testTooltip(tooltip: Locator, expectations: TooltipExpectations) {
|
|
|
28
29
|
const excluded = Promise.all(
|
|
29
30
|
excludes.map((text) => {
|
|
30
31
|
return expect(tooltip).not.toContainText(text, {
|
|
31
|
-
timeout:
|
|
32
|
+
timeout: 10000,
|
|
32
33
|
});
|
|
33
34
|
}),
|
|
34
35
|
);
|
|
@@ -48,7 +49,7 @@ test('Signature help works for functions', async ({ page, mount }) => {
|
|
|
48
49
|
);
|
|
49
50
|
|
|
50
51
|
await expect(page.locator('.cm-signature-help-panel')).toBeVisible({
|
|
51
|
-
timeout:
|
|
52
|
+
timeout: 10000,
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
|
|
@@ -64,7 +65,7 @@ test('Signature help works for procedures', async ({ page, mount }) => {
|
|
|
64
65
|
);
|
|
65
66
|
|
|
66
67
|
await expect(page.locator('.cm-signature-help-panel')).toBeVisible({
|
|
67
|
-
timeout:
|
|
68
|
+
timeout: 10000,
|
|
68
69
|
});
|
|
69
70
|
});
|
|
70
71
|
|
|
@@ -86,9 +87,9 @@ test('Signature help shows the description for the first argument', async ({
|
|
|
86
87
|
|
|
87
88
|
await testTooltip(tooltip, {
|
|
88
89
|
includes: [
|
|
89
|
-
testData.mockSchema.procedures['apoc.import.csv']
|
|
90
|
-
.description,
|
|
91
|
-
testData.mockSchema.procedures['apoc.import.csv'].description,
|
|
90
|
+
testData.mockSchema.procedures['CYPHER 5']['apoc.import.csv']
|
|
91
|
+
.argumentDescription[0].description,
|
|
92
|
+
testData.mockSchema.procedures['CYPHER 5']['apoc.import.csv'].description,
|
|
92
93
|
],
|
|
93
94
|
});
|
|
94
95
|
});
|
|
@@ -289,7 +290,7 @@ test('Signature help does not show any help when method finished', async ({
|
|
|
289
290
|
);
|
|
290
291
|
|
|
291
292
|
await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
|
|
292
|
-
timeout:
|
|
293
|
+
timeout: 10000,
|
|
293
294
|
});
|
|
294
295
|
});
|
|
295
296
|
|
|
@@ -308,7 +309,7 @@ test('Signature help does not blow up on empty query', async ({
|
|
|
308
309
|
);
|
|
309
310
|
|
|
310
311
|
await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
|
|
311
|
-
timeout:
|
|
312
|
+
timeout: 10000,
|
|
312
313
|
});
|
|
313
314
|
});
|
|
314
315
|
|
|
@@ -331,7 +332,7 @@ test('Signature help is shown below the text by default', async ({
|
|
|
331
332
|
await expect(
|
|
332
333
|
page.locator('.cm-signature-help-panel.cm-tooltip-below'),
|
|
333
334
|
).toBeVisible({
|
|
334
|
-
timeout:
|
|
335
|
+
timeout: 10000,
|
|
335
336
|
});
|
|
336
337
|
});
|
|
337
338
|
|
|
@@ -355,7 +356,7 @@ test('Setting showSignatureTooltipBelow to true shows the signature help above t
|
|
|
355
356
|
await expect(
|
|
356
357
|
page.locator('.cm-signature-help-panel.cm-tooltip-below'),
|
|
357
358
|
).toBeVisible({
|
|
358
|
-
timeout:
|
|
359
|
+
timeout: 10000,
|
|
359
360
|
});
|
|
360
361
|
});
|
|
361
362
|
|
|
@@ -379,6 +380,66 @@ test('Setting showSignatureTooltipBelow to false shows the signature help above
|
|
|
379
380
|
await expect(
|
|
380
381
|
page.locator('.cm-signature-help-panel.cm-tooltip-above'),
|
|
381
382
|
).toBeVisible({
|
|
382
|
-
timeout:
|
|
383
|
+
timeout: 10000,
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('Signature help depends on the Cypher version', async ({
|
|
388
|
+
page,
|
|
389
|
+
mount,
|
|
390
|
+
}) => {
|
|
391
|
+
const cypher5ArgDescription = 'The Cypher 5 statement to run.';
|
|
392
|
+
const cypher25ArgDescription = 'The Cypher 25 statement to run.';
|
|
393
|
+
|
|
394
|
+
await mount(
|
|
395
|
+
<CypherEditor
|
|
396
|
+
schema={{
|
|
397
|
+
functions: {
|
|
398
|
+
'CYPHER 5': {
|
|
399
|
+
cypher5Function: {
|
|
400
|
+
...testData.emptyFunction,
|
|
401
|
+
argumentDescription: [
|
|
402
|
+
{
|
|
403
|
+
isDeprecated: false,
|
|
404
|
+
description: cypher5ArgDescription,
|
|
405
|
+
name: 'statement',
|
|
406
|
+
type: 'STRING',
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
name: 'cypher5Function',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
'CYPHER 25': {
|
|
413
|
+
cypher25Function: {
|
|
414
|
+
...testData.emptyFunction,
|
|
415
|
+
argumentDescription: [
|
|
416
|
+
{
|
|
417
|
+
isDeprecated: false,
|
|
418
|
+
description: cypher25ArgDescription,
|
|
419
|
+
name: 'statement',
|
|
420
|
+
type: 'STRING',
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
name: 'cypher25Function',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
}}
|
|
428
|
+
featureFlags={{ cypher25: true }}
|
|
429
|
+
/>,
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const textField = page.getByRole('textbox');
|
|
433
|
+
await textField.fill('CYPHER 5 RETURN cypher5Function(');
|
|
434
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
435
|
+
|
|
436
|
+
await testTooltip(tooltip, {
|
|
437
|
+
includes: [cypher5ArgDescription],
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await textField.fill('CYPHER 25 RETURN cypher25Function(');
|
|
441
|
+
|
|
442
|
+
await testTooltip(tooltip, {
|
|
443
|
+
includes: [cypher25ArgDescription],
|
|
383
444
|
});
|
|
384
445
|
});
|
|
@@ -65,11 +65,11 @@ test('can accept completion inside pattern snippet', async ({
|
|
|
65
65
|
page.locator('.cm-tooltip-autocomplete').getByText('City'),
|
|
66
66
|
).toBeVisible();
|
|
67
67
|
|
|
68
|
-
await textField.press('Tab');
|
|
68
|
+
await textField.press('Tab', { delay: 300 });
|
|
69
69
|
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
70
70
|
|
|
71
71
|
// tab out of the snippet
|
|
72
|
-
await textField.press('Tab');
|
|
72
|
+
await textField.press('Tab', { delay: 300 });
|
|
73
73
|
|
|
74
74
|
await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->(:City)');
|
|
75
75
|
});
|
|
@@ -13,7 +13,7 @@ test('Prop lint set to false disables syntax validation', async ({
|
|
|
13
13
|
await mount(<CypherEditor value={query} lint={false} />);
|
|
14
14
|
|
|
15
15
|
await expect(page.locator('.cm-lintRange-error').last()).not.toBeVisible({
|
|
16
|
-
timeout:
|
|
16
|
+
timeout: 10000,
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ test('Can turn linting back on', async ({ page, mount }) => {
|
|
|
24
24
|
const component = await mount(<CypherEditor value={query} lint={false} />);
|
|
25
25
|
|
|
26
26
|
await expect(page.locator('.cm-lintRange-error').last()).not.toBeVisible({
|
|
27
|
-
timeout:
|
|
27
|
+
timeout: 10000,
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
await component.update(<CypherEditor value={query} lint={true} />);
|
|
@@ -33,7 +33,7 @@ test('Can turn linting back on', async ({ page, mount }) => {
|
|
|
33
33
|
|
|
34
34
|
await editorPage.checkErrorMessage(
|
|
35
35
|
'METCH',
|
|
36
|
-
'
|
|
36
|
+
`Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'`,
|
|
37
37
|
);
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -45,10 +45,22 @@ test('Syntactic errors are surfaced', async ({ page, mount }) => {
|
|
|
45
45
|
|
|
46
46
|
await editorPage.checkErrorMessage(
|
|
47
47
|
'METCH',
|
|
48
|
-
'
|
|
48
|
+
`Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'`,
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
test('Does not trigger syntax errors for backticked parameters in parameter creation', async ({
|
|
53
|
+
page,
|
|
54
|
+
mount,
|
|
55
|
+
}) => {
|
|
56
|
+
const editorPage = new CypherEditorPage(page);
|
|
57
|
+
|
|
58
|
+
const query = ':param x => "abc"';
|
|
59
|
+
await mount(<CypherEditor value={query} />);
|
|
60
|
+
|
|
61
|
+
await editorPage.checkNoNotificationMessage('error');
|
|
62
|
+
});
|
|
63
|
+
|
|
52
64
|
test('Errors for undefined labels are surfaced', async ({ page, mount }) => {
|
|
53
65
|
const editorPage = new CypherEditorPage(page);
|
|
54
66
|
const query = 'MATCH (n: Person) RETURN n';
|
|
@@ -170,33 +182,62 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
|
|
|
170
182
|
);
|
|
171
183
|
});
|
|
172
184
|
|
|
173
|
-
test('Strikethroughs are shown for deprecated functions', async ({
|
|
185
|
+
test('Strikethroughs are shown for deprecated functions', async ({
|
|
186
|
+
page,
|
|
187
|
+
mount,
|
|
188
|
+
}) => {
|
|
174
189
|
const editorPage = new CypherEditorPage(page);
|
|
175
190
|
const query = `RETURN id()`;
|
|
176
191
|
|
|
177
192
|
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
178
|
-
await expect(
|
|
179
|
-
|
|
180
|
-
);
|
|
181
|
-
await editorPage.checkWarningMessage(
|
|
182
|
-
'id',
|
|
183
|
-
"Function id is deprecated.",
|
|
184
|
-
);
|
|
185
|
-
|
|
193
|
+
await expect(
|
|
194
|
+
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
195
|
+
).toBeVisible({ timeout: 10000 });
|
|
196
|
+
await editorPage.checkWarningMessage('id', 'Function id is deprecated.');
|
|
186
197
|
});
|
|
187
198
|
|
|
188
|
-
test('Strikethroughs are shown for deprecated procedures', async ({
|
|
199
|
+
test('Strikethroughs are shown for deprecated procedures', async ({
|
|
200
|
+
page,
|
|
201
|
+
mount,
|
|
202
|
+
}) => {
|
|
189
203
|
const editorPage = new CypherEditorPage(page);
|
|
190
204
|
const query = `CALL apoc.create.uuids()`;
|
|
191
205
|
|
|
192
206
|
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
193
|
-
await expect(
|
|
194
|
-
|
|
207
|
+
await expect(
|
|
208
|
+
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
209
|
+
).toBeVisible({ timeout: 10000 });
|
|
210
|
+
|
|
211
|
+
await editorPage.checkWarningMessage(
|
|
212
|
+
'apoc.create.uuids',
|
|
213
|
+
'Procedure apoc.create.uuids is deprecated.',
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('Syntax validation depends on the Cypher version', async ({
|
|
218
|
+
page,
|
|
219
|
+
mount,
|
|
220
|
+
}) => {
|
|
221
|
+
await mount(
|
|
222
|
+
<CypherEditor
|
|
223
|
+
schema={testData.mockSchema}
|
|
224
|
+
featureFlags={{ cypher25: true }}
|
|
225
|
+
/>,
|
|
195
226
|
);
|
|
196
227
|
|
|
228
|
+
const editorPage = new CypherEditorPage(page);
|
|
229
|
+
const textField = page.getByRole('textbox');
|
|
230
|
+
await textField.fill('CYPHER 5 CALL apoc.create.uuids(5)');
|
|
231
|
+
|
|
197
232
|
await editorPage.checkWarningMessage(
|
|
198
233
|
'apoc.create.uuids',
|
|
199
|
-
|
|
234
|
+
'Procedure apoc.create.uuids is deprecated.',
|
|
200
235
|
);
|
|
201
236
|
|
|
237
|
+
await textField.fill('CYPHER 25 CALL apoc.create.uuids(5)');
|
|
238
|
+
|
|
239
|
+
await editorPage.checkErrorMessage(
|
|
240
|
+
'apoc.create.uuids',
|
|
241
|
+
'Procedure apoc.create.uuids is not present in the database.',
|
|
242
|
+
);
|
|
202
243
|
});
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
CompletionSource,
|
|
4
4
|
snippet,
|
|
5
5
|
} from '@codemirror/autocomplete';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
autocomplete,
|
|
8
|
+
shouldAutoCompleteYield,
|
|
9
|
+
} from '@neo4j-cypher/language-support';
|
|
7
10
|
import {
|
|
8
11
|
CompletionItemKind,
|
|
9
12
|
CompletionItemTag,
|
|
@@ -58,15 +61,18 @@ export const completionStyles: (
|
|
|
58
61
|
export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
59
62
|
(config) => (context) => {
|
|
60
63
|
const documentText = context.state.doc.toString();
|
|
61
|
-
|
|
64
|
+
const offset = context.pos;
|
|
62
65
|
const triggerCharacters = ['.', ':', '{', '$', ')'];
|
|
63
|
-
const lastCharacter = documentText.at(
|
|
64
|
-
|
|
66
|
+
const lastCharacter = documentText.at(offset - 1);
|
|
67
|
+
const yieldTriggered = shouldAutoCompleteYield(documentText, offset);
|
|
65
68
|
const lastWord = context.matchBefore(/\w*/);
|
|
66
69
|
const inWord = lastWord.from !== lastWord.to;
|
|
67
70
|
|
|
68
71
|
const shouldTriggerCompletion =
|
|
69
|
-
inWord ||
|
|
72
|
+
inWord ||
|
|
73
|
+
context.explicit ||
|
|
74
|
+
triggerCharacters.includes(lastCharacter) ||
|
|
75
|
+
yieldTriggered;
|
|
70
76
|
|
|
71
77
|
if (config.useLightVersion && !context.explicit) {
|
|
72
78
|
return null;
|
|
@@ -78,9 +84,9 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
78
84
|
|
|
79
85
|
const options = autocomplete(
|
|
80
86
|
// TODO This is a temporary hack because completions are not working well
|
|
81
|
-
documentText.slice(0,
|
|
87
|
+
documentText.slice(0, offset),
|
|
82
88
|
config.schema ?? {},
|
|
83
|
-
|
|
89
|
+
offset,
|
|
84
90
|
context.explicit,
|
|
85
91
|
);
|
|
86
92
|
|
|
@@ -4,14 +4,11 @@ import {
|
|
|
4
4
|
Language,
|
|
5
5
|
LanguageSupport,
|
|
6
6
|
} from '@codemirror/language';
|
|
7
|
-
import {
|
|
8
|
-
_internalFeatureFlags,
|
|
9
|
-
type DbSchema,
|
|
10
|
-
} from '@neo4j-cypher/language-support';
|
|
7
|
+
import { type DbSchema } from '@neo4j-cypher/language-support';
|
|
11
8
|
import { completionStyles, cypherAutocomplete } from './autocomplete';
|
|
12
9
|
import { ParserAdapter } from './parser-adapter';
|
|
13
10
|
import { signatureHelpTooltip } from './signatureHelp';
|
|
14
|
-
import { cypherLinter
|
|
11
|
+
import { cypherLinter } from './syntaxValidation';
|
|
15
12
|
|
|
16
13
|
const facet = defineLanguageFacet({
|
|
17
14
|
commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
|
|
@@ -23,6 +20,7 @@ export type CypherConfig = {
|
|
|
23
20
|
showSignatureTooltipBelow?: boolean;
|
|
24
21
|
featureFlags?: {
|
|
25
22
|
consoleCommands?: boolean;
|
|
23
|
+
cypher25?: boolean;
|
|
26
24
|
};
|
|
27
25
|
schema?: DbSchema;
|
|
28
26
|
useLightVersion: boolean;
|
|
@@ -30,12 +28,6 @@ export type CypherConfig = {
|
|
|
30
28
|
};
|
|
31
29
|
|
|
32
30
|
export function cypher(config: CypherConfig) {
|
|
33
|
-
const featureFlags = config.featureFlags;
|
|
34
|
-
// We allow to override the consoleCommands feature flag
|
|
35
|
-
if (featureFlags.consoleCommands !== undefined) {
|
|
36
|
-
_internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
const parserAdapter = new ParserAdapter(facet, config);
|
|
40
32
|
|
|
41
33
|
const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
|
|
@@ -46,7 +38,6 @@ export function cypher(config: CypherConfig) {
|
|
|
46
38
|
optionClass: completionStyles,
|
|
47
39
|
}),
|
|
48
40
|
cypherLinter(config),
|
|
49
|
-
semanticAnalysisLinter(config),
|
|
50
41
|
signatureHelpTooltip(config),
|
|
51
42
|
]);
|
|
52
43
|
}
|