@neo4j-cypher/react-codemirror 2.0.0-next.7 → 2.0.0-next.9
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 +26 -0
- package/dist/CypherEditor.d.ts +60 -2
- package/dist/CypherEditor.js +115 -20
- package/dist/CypherEditor.js.map +1 -1
- package/dist/e2e_tests/autoCompletion.spec.js +112 -17
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/e2e_tests/configuration.spec.d.ts +1 -0
- package/dist/e2e_tests/configuration.spec.js +83 -0
- package/dist/e2e_tests/configuration.spec.js.map +1 -0
- package/dist/e2e_tests/debounce.spec.d.ts +1 -0
- package/dist/e2e_tests/debounce.spec.js +63 -0
- package/dist/e2e_tests/debounce.spec.js.map +1 -0
- package/dist/e2e_tests/e2eUtils.js +9 -1
- package/dist/e2e_tests/e2eUtils.js.map +1 -1
- package/dist/e2e_tests/extraKeybindings.spec.js +0 -1
- package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -1
- package/dist/e2e_tests/historyNavigation.spec.js +107 -16
- package/dist/e2e_tests/historyNavigation.spec.js.map +1 -1
- package/dist/e2e_tests/sanityChecks.spec.js +0 -10
- package/dist/e2e_tests/sanityChecks.spec.js.map +1 -1
- package/dist/e2e_tests/signatureHelp.spec.js +43 -15
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
- package/dist/e2e_tests/snippets.spec.d.ts +1 -0
- package/dist/e2e_tests/snippets.spec.js +62 -0
- package/dist/e2e_tests/snippets.spec.js.map +1 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.js +0 -1
- package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -1
- package/dist/e2e_tests/syntaxValidation.spec.js +3 -3
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -1
- package/dist/historyNavigation.js +1 -1
- package/dist/historyNavigation.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lang-cypher/autocomplete.d.ts +4 -1
- package/dist/lang-cypher/autocomplete.js +79 -17
- package/dist/lang-cypher/autocomplete.js.map +1 -1
- package/dist/lang-cypher/contants.test.js +2 -2
- package/dist/lang-cypher/contants.test.js.map +1 -1
- package/dist/lang-cypher/createCypherTheme.js +34 -2
- package/dist/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/lang-cypher/langCypher.d.ts +5 -0
- package/dist/lang-cypher/langCypher.js +11 -5
- package/dist/lang-cypher/langCypher.js.map +1 -1
- package/dist/lang-cypher/signatureHelp.js +39 -22
- package/dist/lang-cypher/signatureHelp.js.map +1 -1
- package/dist/lang-cypher/utils.d.ts +2 -0
- package/dist/lang-cypher/utils.js +10 -0
- package/dist/lang-cypher/utils.js.map +1 -0
- package/dist/neo4jSetup.js +35 -1
- package/dist/neo4jSetup.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/CypherEditor.tsx +233 -31
- package/src/e2e_tests/autoCompletion.spec.tsx +189 -18
- package/src/e2e_tests/configuration.spec.tsx +111 -0
- package/src/e2e_tests/debounce.spec.tsx +100 -0
- package/src/e2e_tests/e2eUtils.ts +11 -1
- package/src/e2e_tests/extraKeybindings.spec.tsx +0 -2
- package/src/e2e_tests/historyNavigation.spec.tsx +136 -17
- package/src/e2e_tests/sanityChecks.spec.tsx +0 -16
- package/src/e2e_tests/signatureHelp.spec.tsx +86 -18
- package/src/e2e_tests/snippets.spec.tsx +92 -0
- package/src/e2e_tests/syntaxHighlighting.spec.tsx +0 -2
- package/src/e2e_tests/syntaxValidation.spec.tsx +3 -3
- package/src/historyNavigation.ts +1 -1
- package/src/index.ts +4 -1
- package/src/lang-cypher/autocomplete.ts +95 -19
- package/src/lang-cypher/contants.test.ts +5 -2
- package/src/lang-cypher/createCypherTheme.ts +34 -2
- package/src/lang-cypher/langCypher.ts +17 -5
- package/src/lang-cypher/signatureHelp.ts +61 -30
- package/src/lang-cypher/utils.ts +9 -0
- package/src/neo4jSetup.tsx +51 -1
|
@@ -44,7 +44,7 @@ test('Signature help works for functions', async ({ page, mount }) => {
|
|
|
44
44
|
/>,
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
-
await expect(page.locator('.cm-
|
|
47
|
+
await expect(page.locator('.cm-signature-help-panel')).toBeVisible({
|
|
48
48
|
timeout: 2000,
|
|
49
49
|
});
|
|
50
50
|
});
|
|
@@ -60,7 +60,7 @@ test('Signature help works for procedures', async ({ page, mount }) => {
|
|
|
60
60
|
/>,
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
await expect(page.locator('.cm-
|
|
63
|
+
await expect(page.locator('.cm-signature-help-panel')).toBeVisible({
|
|
64
64
|
timeout: 2000,
|
|
65
65
|
});
|
|
66
66
|
});
|
|
@@ -79,7 +79,7 @@ test('Signature help shows the description for the first argument', async ({
|
|
|
79
79
|
/>,
|
|
80
80
|
);
|
|
81
81
|
|
|
82
|
-
const tooltip = page.locator('.cm-
|
|
82
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
83
83
|
|
|
84
84
|
await testTooltip(tooltip, {
|
|
85
85
|
includes: [
|
|
@@ -99,7 +99,7 @@ test('Signature help shows the description for the first argument when the curso
|
|
|
99
99
|
<CypherEditor value={query} schema={testData.mockSchema} offset={21} />,
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
-
const tooltip = page.locator('.cm-
|
|
102
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
103
103
|
|
|
104
104
|
await testTooltip(tooltip, {
|
|
105
105
|
includes: [
|
|
@@ -123,7 +123,7 @@ test('Signature help shows the description for the second argument', async ({
|
|
|
123
123
|
/>,
|
|
124
124
|
);
|
|
125
125
|
|
|
126
|
-
const tooltip = page.locator('.cm-
|
|
126
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
127
127
|
|
|
128
128
|
await testTooltip(tooltip, {
|
|
129
129
|
includes: [
|
|
@@ -143,7 +143,7 @@ test('Signature help shows the description for the second argument when the curs
|
|
|
143
143
|
<CypherEditor value={query} schema={testData.mockSchema} offset={27} />,
|
|
144
144
|
);
|
|
145
145
|
|
|
146
|
-
const tooltip = page.locator('.cm-
|
|
146
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
147
147
|
|
|
148
148
|
await testTooltip(tooltip, {
|
|
149
149
|
includes: [
|
|
@@ -163,7 +163,7 @@ test('Signature help shows the description for the second argument when the curs
|
|
|
163
163
|
<CypherEditor value={query} schema={testData.mockSchema} offset={28} />,
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
const tooltip = page.locator('.cm-
|
|
166
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
167
167
|
|
|
168
168
|
await testTooltip(tooltip, {
|
|
169
169
|
includes: [
|
|
@@ -187,7 +187,7 @@ test('Signature help shows description for arguments with a space following a se
|
|
|
187
187
|
/>,
|
|
188
188
|
);
|
|
189
189
|
|
|
190
|
-
const tooltip = page.locator('.cm-
|
|
190
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
191
191
|
|
|
192
192
|
await testTooltip(tooltip, {
|
|
193
193
|
includes: [
|
|
@@ -211,7 +211,7 @@ test('Signature help shows the description for the third argument', async ({
|
|
|
211
211
|
/>,
|
|
212
212
|
);
|
|
213
213
|
|
|
214
|
-
const tooltip = page.locator('.cm-
|
|
214
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
215
215
|
|
|
216
216
|
await testTooltip(tooltip, {
|
|
217
217
|
includes: [
|
|
@@ -235,7 +235,7 @@ test('Signature help works on multiline queries', async ({ page, mount }) => {
|
|
|
235
235
|
/>,
|
|
236
236
|
);
|
|
237
237
|
|
|
238
|
-
const tooltip = page.locator('.cm-
|
|
238
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
239
239
|
|
|
240
240
|
await testTooltip(tooltip, {
|
|
241
241
|
includes: [
|
|
@@ -258,14 +258,15 @@ test('Signature help only shows the description past the last argument', async (
|
|
|
258
258
|
autofocus={true}
|
|
259
259
|
/>,
|
|
260
260
|
);
|
|
261
|
+
1;
|
|
261
262
|
|
|
262
|
-
const tooltip = page.locator('.cm-
|
|
263
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
263
264
|
|
|
264
265
|
await testTooltip(tooltip, {
|
|
265
266
|
includes: [
|
|
266
|
-
'
|
|
267
|
+
'apoc.import.csv(nodes :: LIST<MAP>, rels :: LIST<MAP>, config :: MAP)',
|
|
268
|
+
'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file.',
|
|
267
269
|
],
|
|
268
|
-
excludes: ['config :: MAP'],
|
|
269
270
|
});
|
|
270
271
|
});
|
|
271
272
|
|
|
@@ -283,9 +284,7 @@ test('Signature help does not show any help when method finished', async ({
|
|
|
283
284
|
/>,
|
|
284
285
|
);
|
|
285
286
|
|
|
286
|
-
await expect(
|
|
287
|
-
page.locator('.cm-tooltip-signature-help').last(),
|
|
288
|
-
).not.toBeVisible({
|
|
287
|
+
await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
|
|
289
288
|
timeout: 2000,
|
|
290
289
|
});
|
|
291
290
|
});
|
|
@@ -304,9 +303,78 @@ test('Signature help does not blow up on empty query', async ({
|
|
|
304
303
|
/>,
|
|
305
304
|
);
|
|
306
305
|
|
|
306
|
+
await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
|
|
307
|
+
timeout: 2000,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('Signature help is shown below the text by default', async ({
|
|
312
|
+
page,
|
|
313
|
+
mount,
|
|
314
|
+
}) => {
|
|
315
|
+
// We need to introduce new lines to make sure there's
|
|
316
|
+
// enough space to show the tooltip above
|
|
317
|
+
const query = '\n\n\n\n\n\n\nRETURN abs(';
|
|
318
|
+
|
|
319
|
+
await mount(
|
|
320
|
+
<CypherEditor
|
|
321
|
+
value={query}
|
|
322
|
+
schema={testData.mockSchema}
|
|
323
|
+
autofocus={true}
|
|
324
|
+
/>,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
await expect(
|
|
328
|
+
page.locator('.cm-signature-help-panel.cm-tooltip-below'),
|
|
329
|
+
).toBeVisible({
|
|
330
|
+
timeout: 2000,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('Setting showSignatureTooltipBelow to true shows the signature help above the text', async ({
|
|
335
|
+
page,
|
|
336
|
+
mount,
|
|
337
|
+
}) => {
|
|
338
|
+
// We need to introduce new lines to make sure there's
|
|
339
|
+
// enough space to show the tooltip above
|
|
340
|
+
const query = '\n\n\n\n\n\n\nRETURN abs(';
|
|
341
|
+
|
|
342
|
+
await mount(
|
|
343
|
+
<CypherEditor
|
|
344
|
+
value={query}
|
|
345
|
+
schema={testData.mockSchema}
|
|
346
|
+
showSignatureTooltipBelow={true}
|
|
347
|
+
autofocus={true}
|
|
348
|
+
/>,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
await expect(
|
|
352
|
+
page.locator('.cm-signature-help-panel.cm-tooltip-below'),
|
|
353
|
+
).toBeVisible({
|
|
354
|
+
timeout: 2000,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('Setting showSignatureTooltipBelow to false shows the signature help above the text', async ({
|
|
359
|
+
page,
|
|
360
|
+
mount,
|
|
361
|
+
}) => {
|
|
362
|
+
// We need to introduce new lines to make sure there's
|
|
363
|
+
// enough space to show the tooltip above
|
|
364
|
+
const query = '\n\n\n\n\n\n\nRETURN abs(';
|
|
365
|
+
|
|
366
|
+
await mount(
|
|
367
|
+
<CypherEditor
|
|
368
|
+
value={query}
|
|
369
|
+
schema={testData.mockSchema}
|
|
370
|
+
showSignatureTooltipBelow={false}
|
|
371
|
+
autofocus={true}
|
|
372
|
+
/>,
|
|
373
|
+
);
|
|
374
|
+
|
|
307
375
|
await expect(
|
|
308
|
-
page.locator('.cm-
|
|
309
|
-
).
|
|
376
|
+
page.locator('.cm-signature-help-panel.cm-tooltip-above'),
|
|
377
|
+
).toBeVisible({
|
|
310
378
|
timeout: 2000,
|
|
311
379
|
});
|
|
312
380
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/experimental-ct-react';
|
|
2
|
+
import { CypherEditor } from '../CypherEditor';
|
|
3
|
+
|
|
4
|
+
test('can complete pattern snippet', async ({ page, mount }) => {
|
|
5
|
+
await mount(<CypherEditor />);
|
|
6
|
+
const textField = page.getByRole('textbox');
|
|
7
|
+
|
|
8
|
+
await textField.fill('MATCH ()-[]->()');
|
|
9
|
+
|
|
10
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
|
|
11
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
12
|
+
|
|
13
|
+
await textField.press('Tab');
|
|
14
|
+
await textField.press('Tab');
|
|
15
|
+
|
|
16
|
+
await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->( )');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('can navigate snippet', async ({ page, mount }) => {
|
|
20
|
+
await mount(<CypherEditor />);
|
|
21
|
+
const textField = page.getByRole('textbox');
|
|
22
|
+
|
|
23
|
+
await textField.fill('CREATE INDEX abc FOR ()');
|
|
24
|
+
|
|
25
|
+
await page
|
|
26
|
+
.locator('.cm-tooltip-autocomplete')
|
|
27
|
+
.getByText('-[]-()', { exact: true })
|
|
28
|
+
.click();
|
|
29
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
30
|
+
await expect(page.locator('.cm-snippetField')).toHaveCount(2);
|
|
31
|
+
|
|
32
|
+
await textField.press('Tab');
|
|
33
|
+
await textField.press('Shift+Tab');
|
|
34
|
+
|
|
35
|
+
await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[ ]-( )');
|
|
36
|
+
|
|
37
|
+
await textField.press('a');
|
|
38
|
+
await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[a]-( )');
|
|
39
|
+
|
|
40
|
+
await textField.press('Escape');
|
|
41
|
+
await textField.press('Escape');
|
|
42
|
+
await expect(page.locator('.cm-snippetField')).toHaveCount(0);
|
|
43
|
+
await textField.press('Tab');
|
|
44
|
+
await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[a ]-( )');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('can accept completion inside pattern snippet', async ({
|
|
48
|
+
page,
|
|
49
|
+
mount,
|
|
50
|
+
}) => {
|
|
51
|
+
await mount(<CypherEditor schema={{ labels: ['City'] }} />);
|
|
52
|
+
const textField = page.getByRole('textbox');
|
|
53
|
+
|
|
54
|
+
await textField.fill('MATCH ()-[]->()');
|
|
55
|
+
|
|
56
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
|
|
57
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
58
|
+
|
|
59
|
+
// move to node
|
|
60
|
+
await textField.press('Tab');
|
|
61
|
+
|
|
62
|
+
// get & accept completion
|
|
63
|
+
await textField.press(':');
|
|
64
|
+
await expect(
|
|
65
|
+
page.locator('.cm-tooltip-autocomplete').getByText('City'),
|
|
66
|
+
).toBeVisible();
|
|
67
|
+
|
|
68
|
+
await textField.press('Tab');
|
|
69
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
70
|
+
|
|
71
|
+
// tab out of the snippet
|
|
72
|
+
await textField.press('Tab');
|
|
73
|
+
|
|
74
|
+
await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->(:City)');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('does not automatically open completion panel for expressions after snippet trigger char', async ({
|
|
78
|
+
page,
|
|
79
|
+
mount,
|
|
80
|
+
}) => {
|
|
81
|
+
await mount(<CypherEditor />);
|
|
82
|
+
const textField = page.getByRole('textbox');
|
|
83
|
+
|
|
84
|
+
await textField.fill('RETURN (1)');
|
|
85
|
+
|
|
86
|
+
// expect the panel to not show up
|
|
87
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
88
|
+
|
|
89
|
+
// unless manually triggered
|
|
90
|
+
await textField.press('Control+ ');
|
|
91
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
|
|
92
|
+
});
|
|
@@ -3,8 +3,6 @@ import { CypherEditor } from '../CypherEditor';
|
|
|
3
3
|
import { darkThemeConstants, lightThemeConstants } from '../themes';
|
|
4
4
|
import { CypherEditorPage } from './e2eUtils';
|
|
5
5
|
|
|
6
|
-
test.use({ viewport: { width: 500, height: 500 } });
|
|
7
|
-
|
|
8
6
|
test('light theme highlighting', async ({ page, mount }) => {
|
|
9
7
|
const editorPage = new CypherEditorPage(page);
|
|
10
8
|
const query = `
|
|
@@ -106,7 +106,7 @@ test('Semantic errors are correctly accumulated', async ({ page, mount }) => {
|
|
|
106
106
|
|
|
107
107
|
await editorPage.checkErrorMessage(
|
|
108
108
|
'MATCH (n)',
|
|
109
|
-
'Query cannot conclude with MATCH (must be a RETURN clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
109
|
+
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
|
|
110
110
|
);
|
|
111
111
|
|
|
112
112
|
await editorPage.checkErrorMessage(
|
|
@@ -126,7 +126,7 @@ test('Multiline errors are correctly placed', async ({ page, mount }) => {
|
|
|
126
126
|
|
|
127
127
|
await editorPage.checkErrorMessage(
|
|
128
128
|
'MATCH (n)',
|
|
129
|
-
'Query cannot conclude with MATCH (must be a RETURN clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
129
|
+
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
130
130
|
);
|
|
131
131
|
|
|
132
132
|
await editorPage.checkErrorMessage(
|
|
@@ -146,7 +146,7 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
|
|
|
146
146
|
|
|
147
147
|
await editorPage.checkErrorMessage(
|
|
148
148
|
'-1',
|
|
149
|
-
'Query cannot conclude with CALL (must be a RETURN clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
149
|
+
'Query cannot conclude with CALL (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
|
|
150
150
|
);
|
|
151
151
|
|
|
152
152
|
await editorPage.checkErrorMessage(
|
package/src/historyNavigation.ts
CHANGED
|
@@ -97,7 +97,7 @@ function navigateHistory(view: EditorView, direction: HistoryNavigation) {
|
|
|
97
97
|
view.dispatch(
|
|
98
98
|
view.state.update({
|
|
99
99
|
effects: moveInHistory.of(direction),
|
|
100
|
-
selection: { anchor: view.state.doc.length },
|
|
100
|
+
selection: { anchor: direction === 'BACK' ? 0 : view.state.doc.length },
|
|
101
101
|
}),
|
|
102
102
|
);
|
|
103
103
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
CypherParser,
|
|
3
|
+
_internalFeatureFlags,
|
|
4
|
+
} from '@neo4j-cypher/language-support';
|
|
2
5
|
export { CypherEditor } from './CypherEditor';
|
|
3
6
|
export { cypher } from './lang-cypher/langCypher';
|
|
4
7
|
export { darkThemeConstants, lightThemeConstants } from './themes';
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Completion,
|
|
3
|
+
CompletionSource,
|
|
4
|
+
snippet,
|
|
5
|
+
} from '@codemirror/autocomplete';
|
|
2
6
|
import { autocomplete } from '@neo4j-cypher/language-support';
|
|
3
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CompletionItemKind,
|
|
9
|
+
CompletionItemTag,
|
|
10
|
+
} from 'vscode-languageserver-types';
|
|
4
11
|
import { CompletionItemIcons } from '../icons';
|
|
5
12
|
import type { CypherConfig } from './langCypher';
|
|
13
|
+
import { getDocString } from './utils';
|
|
6
14
|
|
|
7
15
|
const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
|
|
8
16
|
const map: Record<CompletionItemKind, CompletionItemIcons> = {
|
|
@@ -37,12 +45,22 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
|
|
|
37
45
|
return map[c];
|
|
38
46
|
};
|
|
39
47
|
|
|
48
|
+
export const completionStyles: (
|
|
49
|
+
completion: Completion & { deprecated?: boolean },
|
|
50
|
+
) => string = (completion) => {
|
|
51
|
+
if (completion.deprecated) {
|
|
52
|
+
return 'cm-deprecated-completion';
|
|
53
|
+
} else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
40
58
|
export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
41
59
|
(config) => (context) => {
|
|
42
|
-
const
|
|
60
|
+
const documentText = context.state.doc.toString();
|
|
43
61
|
|
|
44
62
|
const triggerCharacters = ['.', ':', '{', '$', ')'];
|
|
45
|
-
const lastCharacter =
|
|
63
|
+
const lastCharacter = documentText.at(context.pos - 1);
|
|
46
64
|
|
|
47
65
|
const lastWord = context.matchBefore(/\w*/);
|
|
48
66
|
const inWord = lastWord.from !== lastWord.to;
|
|
@@ -59,23 +77,81 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
const options = autocomplete(
|
|
62
|
-
|
|
80
|
+
documentText,
|
|
63
81
|
config.schema ?? {},
|
|
64
|
-
|
|
82
|
+
context.pos,
|
|
65
83
|
context.explicit,
|
|
66
84
|
);
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
if (config.featureFlags?.signatureInfoOnAutoCompletions) {
|
|
87
|
+
return {
|
|
88
|
+
from: context.matchBefore(/(\w|\$)*$/).from,
|
|
89
|
+
options: options.map((o) => {
|
|
90
|
+
let maybeInfo = {};
|
|
91
|
+
let emptyInfo = true;
|
|
92
|
+
const newDiv = document.createElement('div');
|
|
93
|
+
|
|
94
|
+
if (o.signature) {
|
|
95
|
+
const header = document.createElement('p');
|
|
96
|
+
header.setAttribute('class', 'cm-completionInfo-signature');
|
|
97
|
+
header.textContent = o.signature;
|
|
98
|
+
if (header.textContent.length > 0) {
|
|
99
|
+
emptyInfo = false;
|
|
100
|
+
newDiv.appendChild(header);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (o.documentation) {
|
|
105
|
+
const paragraph = document.createElement('p');
|
|
106
|
+
paragraph.textContent = getDocString(o.documentation);
|
|
107
|
+
if (paragraph.textContent.length > 0) {
|
|
108
|
+
emptyInfo = false;
|
|
109
|
+
newDiv.appendChild(paragraph);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!emptyInfo) {
|
|
114
|
+
maybeInfo = {
|
|
115
|
+
info: () => Promise.resolve(newDiv),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const deprecated =
|
|
119
|
+
o.tags?.find((tag) => tag === CompletionItemTag.Deprecated) ??
|
|
120
|
+
false;
|
|
121
|
+
// The negative boost moves the deprecation down the list
|
|
122
|
+
// so we offer the user the completions that are
|
|
123
|
+
// deprecated the last
|
|
124
|
+
const maybeDeprecated = deprecated
|
|
125
|
+
? { boost: -99, deprecated: true }
|
|
126
|
+
: {};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
label: o.label,
|
|
130
|
+
type: completionKindToCodemirrorIcon(o.kind),
|
|
131
|
+
apply:
|
|
132
|
+
o.kind === CompletionItemKind.Snippet
|
|
133
|
+
? // codemirror requires an empty snippet space to be able to tab out of the completion
|
|
134
|
+
snippet((o.insertText ?? o.label) + '${}')
|
|
135
|
+
: undefined,
|
|
136
|
+
detail: o.detail,
|
|
137
|
+
...maybeDeprecated,
|
|
138
|
+
...maybeInfo,
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
return {
|
|
144
|
+
from: context.matchBefore(/(\w|\$)*$/).from,
|
|
145
|
+
options: options.map((o) => ({
|
|
146
|
+
label: o.label,
|
|
147
|
+
type: completionKindToCodemirrorIcon(o.kind),
|
|
148
|
+
apply:
|
|
149
|
+
o.kind === CompletionItemKind.Snippet
|
|
150
|
+
? // codemirror requires an empty snippet space to be able to tab out of the completion
|
|
151
|
+
snippet((o.insertText ?? o.label) + '${}')
|
|
152
|
+
: undefined,
|
|
153
|
+
detail: o.detail,
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
81
157
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { tags } from '@lezer/highlight';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applySyntaxColouring,
|
|
4
|
+
CypherTokenType,
|
|
5
|
+
} from '@neo4j-cypher/language-support';
|
|
3
6
|
import { tokenTypeToStyleTag } from './constants';
|
|
4
7
|
|
|
5
8
|
const cypherQueryWithAllTokenTypes = `MATCH (variable :Label)-[:REL_TYPE]->()
|
|
@@ -54,7 +57,7 @@ test('correctly parses all cypher token types to style tags', () => {
|
|
|
54
57
|
]);
|
|
55
58
|
|
|
56
59
|
const styleTags = tokenTypes.map((tokenType) => {
|
|
57
|
-
if (tokenType ===
|
|
60
|
+
if (tokenType === CypherTokenType.none) return undefined;
|
|
58
61
|
return tokenTypeToStyleTag[tokenType];
|
|
59
62
|
});
|
|
60
63
|
const correctTags = [
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@codemirror/language';
|
|
6
6
|
import { Extension } from '@codemirror/state';
|
|
7
7
|
import { EditorView } from '@codemirror/view';
|
|
8
|
+
import { CypherTokenType } from '@neo4j-cypher/language-support';
|
|
8
9
|
import { StyleSpec } from 'style-mod';
|
|
9
10
|
import { HighlightedCypherTokenTypes, tokenTypeToStyleTag } from './constants';
|
|
10
11
|
import {
|
|
@@ -64,8 +65,9 @@ export const createCypherTheme = ({
|
|
|
64
65
|
color: settings.gutterForeground,
|
|
65
66
|
border: 'none',
|
|
66
67
|
},
|
|
67
|
-
'&.cm-editor
|
|
68
|
+
'&.cm-editor': {
|
|
68
69
|
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
70
|
+
height: '100%',
|
|
69
71
|
},
|
|
70
72
|
'.cm-content': {
|
|
71
73
|
caretColor: settings.cursor,
|
|
@@ -99,7 +101,37 @@ export const createCypherTheme = ({
|
|
|
99
101
|
color: settings.autoCompletionPanel.matchingTextColor,
|
|
100
102
|
textDecoration: 'none',
|
|
101
103
|
},
|
|
104
|
+
'& .cm-signature-help-panel': {
|
|
105
|
+
backgroundColor: settings.autoCompletionPanel.backgroundColor,
|
|
106
|
+
maxWidth: '700px',
|
|
107
|
+
maxHeight: '250px',
|
|
108
|
+
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
109
|
+
},
|
|
110
|
+
'& .cm-signature-help-panel-contents': {
|
|
111
|
+
overflow: 'auto',
|
|
112
|
+
maxHeight: '250px',
|
|
113
|
+
},
|
|
114
|
+
'& .cm-signature-help-panel-current-argument': {
|
|
115
|
+
color: settings.autoCompletionPanel.matchingTextColor,
|
|
116
|
+
fontWeight: 'bold',
|
|
117
|
+
},
|
|
118
|
+
'& .cm-signature-help-panel-separator': {
|
|
119
|
+
borderBottom: '1px solid #ccc',
|
|
120
|
+
},
|
|
121
|
+
'& .cm-signature-help-panel-name': {
|
|
122
|
+
padding: '5px',
|
|
123
|
+
},
|
|
124
|
+
'& .cm-signature-help-panel-description': {
|
|
125
|
+
padding: '5px',
|
|
126
|
+
},
|
|
127
|
+
'.cm-completionInfo-signature': {
|
|
128
|
+
color: 'darkgrey',
|
|
129
|
+
},
|
|
130
|
+
'.cm-deprecated-completion': {
|
|
131
|
+
'text-decoration': 'line-through',
|
|
132
|
+
},
|
|
102
133
|
'.cm-tooltip-autocomplete': {
|
|
134
|
+
maxWidth: '430px',
|
|
103
135
|
'& > ul > li[aria-selected]': {
|
|
104
136
|
backgroundColor: settings.autoCompletionPanel.selectedColor,
|
|
105
137
|
color: settings.foreground,
|
|
@@ -204,7 +236,7 @@ export const createCypherTheme = ({
|
|
|
204
236
|
([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
|
|
205
237
|
tag: tokenTypeToStyleTag[token],
|
|
206
238
|
color,
|
|
207
|
-
class: token ===
|
|
239
|
+
class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,
|
|
208
240
|
}),
|
|
209
241
|
);
|
|
210
242
|
const highlightStyle = HighlightStyle.define(styles);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { autocompletion } from '@codemirror/autocomplete';
|
|
1
2
|
import {
|
|
2
3
|
defineLanguageFacet,
|
|
3
4
|
Language,
|
|
4
5
|
LanguageSupport,
|
|
5
6
|
} from '@codemirror/language';
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
+
_internalFeatureFlags,
|
|
8
9
|
type DbSchema,
|
|
9
10
|
} from '@neo4j-cypher/language-support';
|
|
10
|
-
import { cypherAutocomplete } from './autocomplete';
|
|
11
|
+
import { completionStyles, cypherAutocomplete } from './autocomplete';
|
|
11
12
|
import { ParserAdapter } from './parser-adapter';
|
|
12
13
|
import { signatureHelpTooltip } from './signatureHelp';
|
|
13
14
|
import { cypherLinter, semanticAnalysisLinter } from './syntaxValidation';
|
|
@@ -19,20 +20,31 @@ const facet = defineLanguageFacet({
|
|
|
19
20
|
|
|
20
21
|
export type CypherConfig = {
|
|
21
22
|
lint?: boolean;
|
|
23
|
+
showSignatureTooltipBelow?: boolean;
|
|
24
|
+
featureFlags?: {
|
|
25
|
+
signatureInfoOnAutoCompletions?: boolean;
|
|
26
|
+
consoleCommands?: boolean;
|
|
27
|
+
};
|
|
22
28
|
schema?: DbSchema;
|
|
23
29
|
useLightVersion: boolean;
|
|
24
30
|
setUseLightVersion?: (useLightVersion: boolean) => void;
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
export function cypher(config: CypherConfig) {
|
|
28
|
-
|
|
34
|
+
const featureFlags = config.featureFlags;
|
|
35
|
+
// We allow to override the consoleCommands feature flag
|
|
36
|
+
if (featureFlags.consoleCommands !== undefined) {
|
|
37
|
+
_internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
const parserAdapter = new ParserAdapter(facet, config);
|
|
30
41
|
|
|
31
42
|
const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
|
|
32
43
|
|
|
33
44
|
return new LanguageSupport(cypherLanguage, [
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
autocompletion({
|
|
46
|
+
override: [cypherAutocomplete(config)],
|
|
47
|
+
optionClass: completionStyles,
|
|
36
48
|
}),
|
|
37
49
|
cypherLinter(config),
|
|
38
50
|
semanticAnalysisLinter(config),
|