@neo4j-cypher/react-codemirror 2.0.0-next.32 → 2.0.0-next.34
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 +22 -0
- package/dist/src/CypherEditor.d.ts +15 -1
- package/dist/src/CypherEditor.js +66 -2
- package/dist/src/CypherEditor.js.map +1 -1
- package/dist/src/CypherEditor.test.js.map +1 -1
- package/dist/src/e2e_tests/autoCompletion.spec.js +6 -3
- package/dist/src/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/src/e2e_tests/debounce.spec.js +1 -1
- package/dist/src/e2e_tests/debounce.spec.js.map +1 -1
- package/dist/src/e2e_tests/signatureHelp.spec.js +0 -1
- package/dist/src/e2e_tests/signatureHelp.spec.js.map +1 -1
- package/dist/src/e2e_tests/snippets.spec.js.map +1 -1
- package/dist/src/e2e_tests/syntaxValidation.spec.js +10 -4
- package/dist/src/e2e_tests/syntaxValidation.spec.js.map +1 -1
- package/dist/src/lang-cypher/autocomplete.js +6 -3
- package/dist/src/lang-cypher/autocomplete.js.map +1 -1
- package/dist/src/lang-cypher/contants.test.js +2 -2
- package/dist/src/lang-cypher/contants.test.js.map +1 -1
- package/dist/src/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/src/lang-cypher/langCypher.d.ts +2 -1
- package/dist/src/lang-cypher/langCypher.js.map +1 -1
- package/dist/src/lang-cypher/lintWorker.mjs +176 -175
- package/dist/src/lang-cypher/parser-adapter.js +1 -2
- package/dist/src/lang-cypher/parser-adapter.js.map +1 -1
- package/dist/src/lang-cypher/signatureHelp.js +1 -2
- package/dist/src/lang-cypher/signatureHelp.js.map +1 -1
- package/dist/src/lang-cypher/syntaxValidation.js +1 -2
- package/dist/src/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/src/lang-cypher/utils.js +1 -1
- package/dist/src/lang-cypher/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -27
- package/src/CypherEditor.test.tsx +18 -19
- package/src/CypherEditor.tsx +87 -2
- package/src/e2e_tests/autoCompletion.spec.tsx +13 -7
- package/src/e2e_tests/debounce.spec.tsx +32 -36
- package/src/e2e_tests/signatureHelp.spec.tsx +0 -1
- package/src/e2e_tests/snippets.spec.tsx +0 -1
- package/src/e2e_tests/syntaxValidation.spec.tsx +21 -13
- package/src/lang-cypher/autocomplete.ts +7 -8
- package/src/lang-cypher/contants.test.ts +2 -2
- package/src/lang-cypher/createCypherTheme.ts +7 -21
- package/src/lang-cypher/langCypher.ts +5 -1
- package/src/lang-cypher/lintWorker.mjs +176 -175
- package/src/lang-cypher/parser-adapter.ts +6 -6
- package/src/lang-cypher/signatureHelp.ts +5 -2
- package/src/lang-cypher/syntaxValidation.ts +1 -2
- package/src/lang-cypher/utils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-cypher/react-codemirror",
|
|
3
|
+
"version": "2.0.0-next.34",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"codemirror",
|
|
6
|
+
"codemirror 6",
|
|
7
|
+
"cypher",
|
|
8
|
+
"editor",
|
|
9
|
+
"neo4j",
|
|
10
|
+
"react"
|
|
11
|
+
],
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/neo4j/cypher-language-support/issues"
|
|
14
|
+
},
|
|
3
15
|
"license": "Apache-2.0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git://github.com/neo4j/cypher-language-support.git"
|
|
19
|
+
},
|
|
4
20
|
"files": [
|
|
5
21
|
"dist",
|
|
6
22
|
"src",
|
|
@@ -9,30 +25,10 @@
|
|
|
9
25
|
"LICENSE.md",
|
|
10
26
|
"CHANGELOG.md"
|
|
11
27
|
],
|
|
12
|
-
"keywords": [
|
|
13
|
-
"neo4j",
|
|
14
|
-
"cypher",
|
|
15
|
-
"react",
|
|
16
|
-
"editor",
|
|
17
|
-
"codemirror",
|
|
18
|
-
"codemirror 6"
|
|
19
|
-
],
|
|
20
|
-
"version": "2.0.0-next.32",
|
|
21
|
-
"main": "./dist/src/index.js",
|
|
22
|
-
"types": "./dist/src/index.d.ts",
|
|
23
28
|
"type": "module",
|
|
24
29
|
"sideEffects": false,
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
"url": "git://github.com/neo4j/cypher-language-support.git"
|
|
28
|
-
},
|
|
29
|
-
"bugs": {
|
|
30
|
-
"url": "https://github.com/neo4j/cypher-language-support/issues"
|
|
31
|
-
},
|
|
32
|
-
"engineStrict": true,
|
|
33
|
-
"engines": {
|
|
34
|
-
"node": ">=24.11.1"
|
|
35
|
-
},
|
|
30
|
+
"main": "./dist/src/index.js",
|
|
31
|
+
"types": "./dist/src/index.d.ts",
|
|
36
32
|
"dependencies": {
|
|
37
33
|
"@codemirror/autocomplete": "^6.18.6",
|
|
38
34
|
"@codemirror/commands": "^6.8.1",
|
|
@@ -51,13 +47,13 @@
|
|
|
51
47
|
"style-mod": "^4.1.2",
|
|
52
48
|
"vscode-languageserver-types": "^3.17.3",
|
|
53
49
|
"workerpool": "^9.3.3",
|
|
54
|
-
"@neo4j-cypher/language-support": "2.0.0-next.
|
|
55
|
-
"@neo4j-cypher/lint-worker": "1.10.1-next.
|
|
50
|
+
"@neo4j-cypher/language-support": "2.0.0-next.31",
|
|
51
|
+
"@neo4j-cypher/lint-worker": "1.10.1-next.8"
|
|
56
52
|
},
|
|
57
53
|
"devDependencies": {
|
|
58
54
|
"@neo4j-ndl/base": "^3.2.10",
|
|
59
|
-
"@playwright/experimental-ct-react": "^1.
|
|
60
|
-
"@playwright/test": "^1.
|
|
55
|
+
"@playwright/experimental-ct-react": "^1.55.1",
|
|
56
|
+
"@playwright/test": "^1.55.1",
|
|
61
57
|
"@types/lodash.debounce": "^4.0.9",
|
|
62
58
|
"@types/react": "^18.0.28",
|
|
63
59
|
"@types/react-dom": "^18.0.11",
|
|
@@ -65,7 +61,7 @@
|
|
|
65
61
|
"copyfiles": "^2.4.1",
|
|
66
62
|
"jsdom": "^24.1.1",
|
|
67
63
|
"lodash": "^4.17.21",
|
|
68
|
-
"playwright": "^1.
|
|
64
|
+
"playwright": "^1.55.1",
|
|
69
65
|
"react": "^18.2.0",
|
|
70
66
|
"react-dom": "^18.2.0",
|
|
71
67
|
"vite": "^4.5.10"
|
|
@@ -73,6 +69,10 @@
|
|
|
73
69
|
"peerDependencies": {
|
|
74
70
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
|
75
71
|
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=24.11.1"
|
|
74
|
+
},
|
|
75
|
+
"engineStrict": true,
|
|
76
76
|
"scripts": {
|
|
77
77
|
"dev": "tsc --watch",
|
|
78
78
|
"build": "pnpm copy-lint-worker && tsc --declaration --outDir dist/",
|
|
@@ -124,27 +124,24 @@ test.fails('new props.value should cancel onChange', async () => {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
// value updates from outside onExecute are overwritten by pending updates
|
|
127
|
-
test.fails(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
value = 'same value';
|
|
132
|
-
rerender();
|
|
127
|
+
test.fails('new props.value set to same value should cancel onChange', async () => {
|
|
128
|
+
// 1. value is set initially
|
|
129
|
+
value = 'same value';
|
|
130
|
+
rerender();
|
|
133
131
|
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
// 2. value is updated internally
|
|
133
|
+
ref.current.setValueAndFocus('update');
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
// 3. editor is rerendered with a new value while a value update is still pending
|
|
136
|
+
value = 'same value';
|
|
137
|
+
rerender();
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
await debounce();
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
141
|
+
// expect(onChange).not.toHaveBeenCalled();
|
|
142
|
+
expect(getEditorValue()).toBe('same value');
|
|
143
|
+
expect(value).toBe('same value');
|
|
144
|
+
});
|
|
148
145
|
|
|
149
146
|
test('rerender should not cancel onChange', async () => {
|
|
150
147
|
// 1. value is updated internally
|
|
@@ -200,5 +197,7 @@ test('rerender with prior external update should not cancel onChange', async ()
|
|
|
200
197
|
});
|
|
201
198
|
|
|
202
199
|
test('setValueAndFocus should handle CRLF newline characters', () => {
|
|
203
|
-
expect(() =>
|
|
204
|
-
|
|
200
|
+
expect(() =>
|
|
201
|
+
ref.current.setValueAndFocus('new value\r\nnew line'),
|
|
202
|
+
).not.toThrow();
|
|
203
|
+
});
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
placeholder,
|
|
14
14
|
ViewUpdate,
|
|
15
15
|
} from '@codemirror/view';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
formatQuery,
|
|
18
|
+
CypherLanguageService,
|
|
19
|
+
type DbSchema,
|
|
20
|
+
} from '@neo4j-cypher/language-support';
|
|
17
21
|
import debounce from 'lodash.debounce';
|
|
18
22
|
import { Component, createRef } from 'react';
|
|
19
23
|
import { DEBOUNCE_TIME } from './constants';
|
|
@@ -26,6 +30,8 @@ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
|
|
|
26
30
|
import { basicNeo4jSetup } from './neo4jSetup';
|
|
27
31
|
import { getThemeExtension } from './themes';
|
|
28
32
|
import { richClipboardCopier } from './richClipboardCopier';
|
|
33
|
+
import { LintWorker } from '@neo4j-cypher/lint-worker';
|
|
34
|
+
import workerpool from 'workerpool';
|
|
29
35
|
|
|
30
36
|
type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
|
|
31
37
|
export interface CypherEditorProps {
|
|
@@ -190,7 +196,7 @@ const format = (view: EditorView): void => {
|
|
|
190
196
|
},
|
|
191
197
|
selection: { anchor: newCursorPos },
|
|
192
198
|
});
|
|
193
|
-
} catch
|
|
199
|
+
} catch {
|
|
194
200
|
// Formatting failed, likely because of a syntax error
|
|
195
201
|
}
|
|
196
202
|
};
|
|
@@ -275,11 +281,72 @@ const formatLineNumber =
|
|
|
275
281
|
type CypherEditorState = { cypherSupportEnabled: boolean };
|
|
276
282
|
|
|
277
283
|
const ExternalEdit = Annotation.define<boolean>();
|
|
284
|
+
const WorkerURL = new URL('./lang-cypher/lintWorker.mjs', import.meta.url)
|
|
285
|
+
.pathname;
|
|
286
|
+
|
|
287
|
+
class CodemirrorSymbolFetcher {
|
|
288
|
+
constructor(languageService: CypherLanguageService) {
|
|
289
|
+
this.languageService = languageService;
|
|
290
|
+
}
|
|
291
|
+
private languageService: CypherLanguageService;
|
|
292
|
+
private processing = false;
|
|
293
|
+
private nextJob: {
|
|
294
|
+
query: string;
|
|
295
|
+
schema: DbSchema;
|
|
296
|
+
};
|
|
297
|
+
private symbolTablePool = workerpool.pool(WorkerURL, {
|
|
298
|
+
minWorkers: 1,
|
|
299
|
+
workerOpts: { type: 'module' },
|
|
300
|
+
workerTerminateTimeout: 2000,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
public queueSymbolJob(query: string, schema: DbSchema) {
|
|
304
|
+
this.nextJob = { query, schema };
|
|
305
|
+
if (!this.processing) {
|
|
306
|
+
void this.processJobQueue();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public terminate() {
|
|
311
|
+
this.nextJob = undefined;
|
|
312
|
+
void this.symbolTablePool.terminate();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async processJobQueue() {
|
|
316
|
+
this.processing = true;
|
|
317
|
+
while (this.nextJob) {
|
|
318
|
+
try {
|
|
319
|
+
const proxyWorker =
|
|
320
|
+
(await this.symbolTablePool.proxy()) as unknown as LintWorker;
|
|
321
|
+
const query = this.nextJob.query;
|
|
322
|
+
const dbSchema = this.nextJob.schema;
|
|
323
|
+
this.nextJob = undefined;
|
|
324
|
+
|
|
325
|
+
const result = await proxyWorker.lintCypherQuery(query, dbSchema);
|
|
326
|
+
|
|
327
|
+
if (result.symbolTables) {
|
|
328
|
+
this.languageService.setSymbolsInfo({
|
|
329
|
+
query,
|
|
330
|
+
symbolTables: result.symbolTables,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
//eslint-disable-next-line
|
|
335
|
+
console.log('Symbol table calculation failed ' + String(err));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.processing = false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
278
341
|
|
|
279
342
|
export class CypherEditor extends Component<
|
|
280
343
|
CypherEditorProps,
|
|
281
344
|
CypherEditorState
|
|
282
345
|
> {
|
|
346
|
+
/**
|
|
347
|
+
* The symbol fetcher object used to fetch the current symbol table on document changes
|
|
348
|
+
*/
|
|
349
|
+
symbolFetcher: CodemirrorSymbolFetcher;
|
|
283
350
|
/**
|
|
284
351
|
* The codemirror editor container.
|
|
285
352
|
*/
|
|
@@ -379,6 +446,7 @@ export class CypherEditor extends Component<
|
|
|
379
446
|
} = this.props;
|
|
380
447
|
|
|
381
448
|
this.schemaRef.current = {
|
|
449
|
+
languageService: new CypherLanguageService(),
|
|
382
450
|
schema,
|
|
383
451
|
lint,
|
|
384
452
|
showSignatureTooltipBelow,
|
|
@@ -394,6 +462,10 @@ export class CypherEditor extends Component<
|
|
|
394
462
|
},
|
|
395
463
|
};
|
|
396
464
|
|
|
465
|
+
this.symbolFetcher = new CodemirrorSymbolFetcher(
|
|
466
|
+
this.schemaRef.current.languageService,
|
|
467
|
+
);
|
|
468
|
+
|
|
397
469
|
const themeExtension = getThemeExtension(
|
|
398
470
|
theme,
|
|
399
471
|
overrideThemeBackgroundColor,
|
|
@@ -402,6 +474,12 @@ export class CypherEditor extends Component<
|
|
|
402
474
|
const changeListener = this.debouncedOnChange
|
|
403
475
|
? [
|
|
404
476
|
EditorView.updateListener.of((upt: ViewUpdate) => {
|
|
477
|
+
if (upt.docChanged) {
|
|
478
|
+
this.symbolFetcher.queueSymbolJob(
|
|
479
|
+
upt.state.doc.toString(),
|
|
480
|
+
this.schemaRef.current.schema,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
405
483
|
const wasUserEdit = !upt.transactions.some((tr) =>
|
|
406
484
|
tr.annotation(ExternalEdit),
|
|
407
485
|
);
|
|
@@ -452,6 +530,12 @@ export class CypherEditor extends Component<
|
|
|
452
530
|
'aria-label': this.props.ariaLabel,
|
|
453
531
|
})
|
|
454
532
|
: [],
|
|
533
|
+
!this.props.moveFocusOnTab
|
|
534
|
+
? EditorView.contentAttributes.of({
|
|
535
|
+
'aria-description':
|
|
536
|
+
'Press Escape to leave the editor and continue tabbing through the page',
|
|
537
|
+
})
|
|
538
|
+
: [],
|
|
455
539
|
],
|
|
456
540
|
doc: this.props.value,
|
|
457
541
|
});
|
|
@@ -592,6 +676,7 @@ export class CypherEditor extends Component<
|
|
|
592
676
|
|
|
593
677
|
componentWillUnmount(): void {
|
|
594
678
|
this.editorView.current?.destroy();
|
|
679
|
+
this.symbolFetcher?.terminate();
|
|
595
680
|
cleanupWorkers();
|
|
596
681
|
}
|
|
597
682
|
|
|
@@ -33,7 +33,10 @@ RETURN n;`}
|
|
|
33
33
|
await textField.press('Control+ ');
|
|
34
34
|
|
|
35
35
|
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
|
|
36
|
-
await page
|
|
36
|
+
await page
|
|
37
|
+
.locator('.cm-tooltip-autocomplete')
|
|
38
|
+
.getByText('WHERE', { exact: true })
|
|
39
|
+
.click();
|
|
37
40
|
|
|
38
41
|
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
39
42
|
|
|
@@ -544,13 +547,16 @@ test('completions depend on the Cypher version', async ({ page, mount }) => {
|
|
|
544
547
|
).toBeVisible();
|
|
545
548
|
});
|
|
546
549
|
|
|
547
|
-
test('does not complete properties for non node / relationship variables', async ({
|
|
550
|
+
test('does not complete properties for non node / relationship variables', async ({
|
|
551
|
+
page,
|
|
552
|
+
mount,
|
|
553
|
+
}) => {
|
|
548
554
|
await mount(
|
|
549
555
|
<CypherEditor
|
|
550
|
-
|
|
551
|
-
propertyKeys: [
|
|
556
|
+
schema={{
|
|
557
|
+
propertyKeys: ['nodeProperty'],
|
|
552
558
|
}}
|
|
553
|
-
|
|
559
|
+
/>,
|
|
554
560
|
);
|
|
555
561
|
|
|
556
562
|
const textField = page.getByRole('textbox');
|
|
@@ -562,10 +568,10 @@ test('does not complete properties for non node / relationship variables', async
|
|
|
562
568
|
|
|
563
569
|
await textField.fill('WITH 1 AS x RETURN x.');
|
|
564
570
|
// This could be flaky if the semantic analysis takes too long
|
|
565
|
-
await page.waitForTimeout(500)
|
|
571
|
+
await page.waitForTimeout(500);
|
|
566
572
|
await textField.press('Escape');
|
|
567
573
|
await textField.press('Control+ ');
|
|
568
574
|
await expect(
|
|
569
|
-
page.locator('.cm-tooltip-autocomplete').getByText('nodeProperty')
|
|
575
|
+
page.locator('.cm-tooltip-autocomplete').getByText('nodeProperty'),
|
|
570
576
|
).not.toBeVisible();
|
|
571
577
|
});
|
|
@@ -28,47 +28,43 @@ test.fail(
|
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
// TODO Fix this test
|
|
31
|
-
test.fixme(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const onExecute = () => {
|
|
38
|
-
value = '';
|
|
39
|
-
void component.update(
|
|
40
|
-
<CypherEditor
|
|
41
|
-
value={value}
|
|
42
|
-
onChange={onChange}
|
|
43
|
-
onExecute={onExecute}
|
|
44
|
-
/>,
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const onChange = (val: string) => {
|
|
49
|
-
value = val;
|
|
50
|
-
void component.update(
|
|
51
|
-
<CypherEditor value={val} onChange={onChange} onExecute={onExecute} />,
|
|
52
|
-
);
|
|
53
|
-
};
|
|
31
|
+
test.fixme('onExecute updates should override debounce updates', async ({
|
|
32
|
+
mount,
|
|
33
|
+
page,
|
|
34
|
+
}) => {
|
|
35
|
+
const editorPage = new CypherEditorPage(page);
|
|
36
|
+
let value = '';
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
const onExecute = () => {
|
|
39
|
+
value = '';
|
|
40
|
+
void component.update(
|
|
56
41
|
<CypherEditor value={value} onChange={onChange} onExecute={onExecute} />,
|
|
57
42
|
);
|
|
43
|
+
};
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const onChange = (val: string) => {
|
|
46
|
+
value = val;
|
|
47
|
+
void component.update(
|
|
48
|
+
<CypherEditor value={val} onChange={onChange} onExecute={onExecute} />,
|
|
49
|
+
);
|
|
50
|
+
};
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
52
|
+
const component = await mount(
|
|
53
|
+
<CypherEditor value={value} onChange={onChange} onExecute={onExecute} />,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
57
|
+
await editorPage.getEditor().press('Enter');
|
|
58
|
+
await page.waitForTimeout(DEBOUNCE_TIME_WITH_MARGIN);
|
|
59
|
+
await expect(component).not.toContainText('RETURN 1');
|
|
60
|
+
|
|
61
|
+
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
62
|
+
await editorPage.getEditor().pressSequentially('');
|
|
63
|
+
await editorPage.getEditor().pressSequentially('RETURN 1');
|
|
64
|
+
await editorPage.getEditor().press('Enter');
|
|
65
|
+
await page.waitForTimeout(DEBOUNCE_TIME_WITH_MARGIN);
|
|
66
|
+
await expect(component).not.toContainText('RETURN 1');
|
|
67
|
+
});
|
|
72
68
|
|
|
73
69
|
test('onExecute should fire after debounced updates', async ({
|
|
74
70
|
mount,
|
|
@@ -182,9 +182,9 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
|
|
|
182
182
|
);
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
test('Syntax highlighting works as expected with multiple separate linting messages', async (
|
|
185
|
+
test('Syntax highlighting works as expected with multiple separate linting messages', async ({
|
|
186
186
|
page,
|
|
187
|
-
mount
|
|
187
|
+
mount,
|
|
188
188
|
}) => {
|
|
189
189
|
const editorPage = new CypherEditorPage(page);
|
|
190
190
|
const query = `MATCH (n)--(m) CALL (n) {RETURN id(n) AS b} RETURN apoc.create.uuid(), a`;
|
|
@@ -192,12 +192,20 @@ test('Syntax highlighting works as expected with multiple separate linting messa
|
|
|
192
192
|
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
193
193
|
await expect(
|
|
194
194
|
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
195
|
-
).toBeVisible({
|
|
195
|
+
).toBeVisible({
|
|
196
|
+
timeout: 10000,
|
|
197
|
+
});
|
|
196
198
|
await editorPage.checkWarningMessage('id', 'Function id is deprecated.');
|
|
197
|
-
await editorPage.checkWarningMessage(
|
|
198
|
-
|
|
199
|
+
await editorPage.checkWarningMessage(
|
|
200
|
+
'id',
|
|
201
|
+
`The query used a deprecated function. ('id' has been replaced by 'elementId or consider using an application-generated id')`,
|
|
202
|
+
);
|
|
203
|
+
await editorPage.checkWarningMessage(
|
|
204
|
+
'apoc.create.uuid',
|
|
205
|
+
'Function apoc.create.uuid is deprecated. Alternative: Neo4j randomUUID() function',
|
|
206
|
+
);
|
|
199
207
|
await editorPage.checkErrorMessage('a', 'Variable `a` not defined');
|
|
200
|
-
})
|
|
208
|
+
});
|
|
201
209
|
|
|
202
210
|
test('Strikethroughs are shown for deprecated functions', async ({
|
|
203
211
|
page,
|
|
@@ -209,7 +217,9 @@ test('Strikethroughs are shown for deprecated functions', async ({
|
|
|
209
217
|
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
210
218
|
await expect(
|
|
211
219
|
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
212
|
-
).toBeVisible({
|
|
220
|
+
).toBeVisible({
|
|
221
|
+
timeout: 10000,
|
|
222
|
+
});
|
|
213
223
|
await editorPage.checkWarningMessage('id', 'Function id is deprecated.');
|
|
214
224
|
});
|
|
215
225
|
|
|
@@ -223,7 +233,9 @@ test('Strikethroughs are shown for deprecated procedures', async ({
|
|
|
223
233
|
await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
|
|
224
234
|
await expect(
|
|
225
235
|
editorPage.page.locator('.cm-deprecated-element').last(),
|
|
226
|
-
).toBeVisible({
|
|
236
|
+
).toBeVisible({
|
|
237
|
+
timeout: 10000,
|
|
238
|
+
});
|
|
227
239
|
|
|
228
240
|
await editorPage.checkWarningMessage(
|
|
229
241
|
'apoc.create.uuids',
|
|
@@ -235,11 +247,7 @@ test('Syntax validation depends on the Cypher version', async ({
|
|
|
235
247
|
page,
|
|
236
248
|
mount,
|
|
237
249
|
}) => {
|
|
238
|
-
await mount(
|
|
239
|
-
<CypherEditor
|
|
240
|
-
schema={testData.mockSchema}
|
|
241
|
-
/>,
|
|
242
|
-
);
|
|
250
|
+
await mount(<CypherEditor schema={testData.mockSchema} />);
|
|
243
251
|
|
|
244
252
|
const editorPage = new CypherEditorPage(page);
|
|
245
253
|
const textField = page.getByRole('textbox');
|
|
@@ -3,10 +3,7 @@ import {
|
|
|
3
3
|
CompletionSource,
|
|
4
4
|
snippet,
|
|
5
5
|
} from '@codemirror/autocomplete';
|
|
6
|
-
import {
|
|
7
|
-
autocomplete,
|
|
8
|
-
shouldAutoCompleteYield,
|
|
9
|
-
} from '@neo4j-cypher/language-support';
|
|
6
|
+
import { shouldAutoCompleteYield } from '@neo4j-cypher/language-support';
|
|
10
7
|
import {
|
|
11
8
|
CompletionItemKind,
|
|
12
9
|
CompletionItemTag,
|
|
@@ -62,7 +59,7 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
62
59
|
(config) => (context) => {
|
|
63
60
|
const documentText = context.state.doc.toString();
|
|
64
61
|
const offset = context.pos;
|
|
65
|
-
const triggerCharacters = ['.', ':', '{', '$', ')', ']'];
|
|
62
|
+
const triggerCharacters = ['.', ':', '{', '$', ')', ']', '-', '<'];
|
|
66
63
|
const lastCharacter = documentText.at(offset - 1);
|
|
67
64
|
const yieldTriggered = shouldAutoCompleteYield(documentText, offset);
|
|
68
65
|
const lastWord = context.matchBefore(/\w*/);
|
|
@@ -81,11 +78,13 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
81
78
|
return null;
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
const options = autocomplete(
|
|
81
|
+
const options = config.languageService.autocomplete(
|
|
85
82
|
documentText,
|
|
86
83
|
config.schema ?? {},
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
{
|
|
85
|
+
caretPosition: offset,
|
|
86
|
+
manual: context.explicit,
|
|
87
|
+
},
|
|
89
88
|
);
|
|
90
89
|
|
|
91
90
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tags } from '@lezer/highlight';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
highlightSyntax,
|
|
4
4
|
CypherTokenType,
|
|
5
5
|
} from '@neo4j-cypher/language-support';
|
|
6
6
|
import { expect, test } from 'vitest';
|
|
@@ -14,7 +14,7 @@ WHERE variable.property = "String"
|
|
|
14
14
|
RETURN variable;`;
|
|
15
15
|
|
|
16
16
|
test('correctly parses all cypher token types to style tags', () => {
|
|
17
|
-
const tokens =
|
|
17
|
+
const tokens = highlightSyntax(cypherQueryWithAllTokenTypes);
|
|
18
18
|
const tokenTypes = tokens.map((token) => token.tokenType);
|
|
19
19
|
expect(tokenTypes).toEqual([
|
|
20
20
|
'keyword',
|
|
@@ -160,24 +160,16 @@ export const createCypherTheme = ({
|
|
|
160
160
|
border: 'none',
|
|
161
161
|
verticalAlign: 'middle',
|
|
162
162
|
'&[name=next]::before': {
|
|
163
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
164
|
-
downArrowSvg,
|
|
165
|
-
)}")`,
|
|
163
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(downArrowSvg)}")`,
|
|
166
164
|
},
|
|
167
165
|
'&[name=prev]::before': {
|
|
168
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
169
|
-
upArrowSvg,
|
|
170
|
-
)}")`,
|
|
166
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(upArrowSvg)}")`,
|
|
171
167
|
},
|
|
172
168
|
'&[name=replace]::before': {
|
|
173
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
174
|
-
replaceSvg,
|
|
175
|
-
)}")`,
|
|
169
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(replaceSvg)}")`,
|
|
176
170
|
},
|
|
177
171
|
'&[name=replaceAll]::before': {
|
|
178
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
179
|
-
replaceAllSvg,
|
|
180
|
-
)}")`,
|
|
172
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(replaceAllSvg)}")`,
|
|
181
173
|
},
|
|
182
174
|
width: '20px',
|
|
183
175
|
height: '20px',
|
|
@@ -209,19 +201,13 @@ export const createCypherTheme = ({
|
|
|
209
201
|
borderRadius: '4px',
|
|
210
202
|
|
|
211
203
|
'&[name=case]::before': {
|
|
212
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
213
|
-
caseSensitiveSvg,
|
|
214
|
-
)}")`,
|
|
204
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(caseSensitiveSvg)}")`,
|
|
215
205
|
},
|
|
216
206
|
'&[name=re]::before': {
|
|
217
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
218
|
-
regexSvg,
|
|
219
|
-
)}")`,
|
|
207
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(regexSvg)}")`,
|
|
220
208
|
},
|
|
221
209
|
'&[name=word]::before': {
|
|
222
|
-
content: `url("data:image/svg+xml;base64,${window.btoa(
|
|
223
|
-
byWordSvg,
|
|
224
|
-
)}")`,
|
|
210
|
+
content: `url("data:image/svg+xml;base64,${window.btoa(byWordSvg)}")`,
|
|
225
211
|
},
|
|
226
212
|
'&:hover': {
|
|
227
213
|
backgroundColor: settings.searchPanel.buttonHoverBackground,
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
Language,
|
|
5
5
|
LanguageSupport,
|
|
6
6
|
} from '@codemirror/language';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CypherLanguageService,
|
|
9
|
+
type DbSchema,
|
|
10
|
+
} from '@neo4j-cypher/language-support';
|
|
8
11
|
import { completionStyles, cypherAutocomplete } from './autocomplete';
|
|
9
12
|
import { ParserAdapter } from './parser-adapter';
|
|
10
13
|
import { signatureHelpTooltip } from './signatureHelp';
|
|
@@ -16,6 +19,7 @@ const facet = defineLanguageFacet({
|
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
export type CypherConfig = {
|
|
22
|
+
languageService: CypherLanguageService;
|
|
19
23
|
lint?: boolean;
|
|
20
24
|
showSignatureTooltipBelow?: boolean;
|
|
21
25
|
featureFlags?: {
|