@neo4j-cypher/react-codemirror 2.0.0-next.8 → 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 +18 -0
- package/dist/CypherEditor.d.ts +28 -1
- package/dist/CypherEditor.js +70 -11
- package/dist/CypherEditor.js.map +1 -1
- package/dist/e2e_tests/autoCompletion.spec.js +117 -1
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/e2e_tests/configuration.spec.js +11 -1
- package/dist/e2e_tests/configuration.spec.js.map +1 -1
- 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/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 -1
- package/dist/e2e_tests/sanityChecks.spec.js.map +1 -1
- package/dist/e2e_tests/signatureHelp.spec.js +27 -0
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
- package/dist/e2e_tests/snippets.spec.js +0 -1
- package/dist/e2e_tests/snippets.spec.js.map +1 -1
- package/dist/e2e_tests/syntaxHighlighting.spec.js +0 -1
- package/dist/e2e_tests/syntaxHighlighting.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 +9 -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 +4 -3
- 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/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/CypherEditor.tsx +131 -21
- package/src/e2e_tests/autoCompletion.spec.tsx +206 -2
- package/src/e2e_tests/configuration.spec.tsx +16 -2
- package/src/e2e_tests/debounce.spec.tsx +100 -0
- 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 -2
- package/src/e2e_tests/signatureHelp.spec.tsx +71 -0
- package/src/e2e_tests/snippets.spec.tsx +0 -2
- package/src/e2e_tests/syntaxHighlighting.spec.tsx +0 -2
- 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 +9 -3
- package/src/lang-cypher/langCypher.ts +17 -5
- package/src/lang-cypher/signatureHelp.ts +5 -3
- package/src/lang-cypher/utils.ts +9 -0
|
@@ -2,8 +2,6 @@ import { expect, test } from '@playwright/experimental-ct-react';
|
|
|
2
2
|
import { CypherEditor } from '../CypherEditor';
|
|
3
3
|
import { CypherEditorPage } from './e2eUtils';
|
|
4
4
|
|
|
5
|
-
test.use({ viewport: { width: 500, height: 500 } });
|
|
6
|
-
|
|
7
5
|
test('respects preloaded history', async ({ page, mount }) => {
|
|
8
6
|
const editorPage = new CypherEditorPage(page);
|
|
9
7
|
|
|
@@ -27,13 +25,75 @@ test('respects preloaded history', async ({ page, mount }) => {
|
|
|
27
25
|
await editorPage.getEditor().press('ArrowUp');
|
|
28
26
|
await expect(page.getByText('second')).toBeVisible();
|
|
29
27
|
|
|
28
|
+
// First arrow down is to get to end of line
|
|
29
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
30
30
|
await editorPage.getEditor().press('ArrowDown');
|
|
31
31
|
await expect(page.getByText('first')).toBeVisible();
|
|
32
32
|
await editorPage.getEditor().press('ArrowDown');
|
|
33
33
|
await expect(page.getByText(initialValue)).toBeVisible();
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
test('can
|
|
36
|
+
test('can add new lines without onExecute', async ({ page, mount }) => {
|
|
37
|
+
const editorPage = new CypherEditorPage(page);
|
|
38
|
+
|
|
39
|
+
const editorComponent = await mount(<CypherEditor />);
|
|
40
|
+
|
|
41
|
+
// Ctrl-Enter does nothing when onExecute is false
|
|
42
|
+
await editorPage.getEditor().press('Control+Enter');
|
|
43
|
+
await expect(editorComponent).toHaveText('1\n', {
|
|
44
|
+
useInnerText: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Enter adds new lines
|
|
48
|
+
await editorPage.getEditor().fill('Brock');
|
|
49
|
+
await editorPage.getEditor().press('Enter');
|
|
50
|
+
await editorPage.getEditor().press('Enter');
|
|
51
|
+
await expect(editorComponent).toHaveText('1\n2\n3\nBrock', {
|
|
52
|
+
useInnerText: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Shift-Enter adds new lines
|
|
56
|
+
await editorPage.getEditor().press('Shift+Enter');
|
|
57
|
+
await editorPage.getEditor().press('Shift+Enter');
|
|
58
|
+
await expect(editorComponent).toHaveText('1\n2\n3\n4\n5\nBrock', {
|
|
59
|
+
useInnerText: true,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('can add new lines with newLineOnEnter and without onExecute', async ({
|
|
64
|
+
page,
|
|
65
|
+
mount,
|
|
66
|
+
}) => {
|
|
67
|
+
const editorPage = new CypherEditorPage(page);
|
|
68
|
+
|
|
69
|
+
const editorComponent = await mount(<CypherEditor newLineOnEnter />);
|
|
70
|
+
|
|
71
|
+
// Ctrl-Enter does nothing when onExecute is false
|
|
72
|
+
await editorPage.getEditor().press('Control+Enter');
|
|
73
|
+
await expect(editorComponent).toHaveText('1\n', {
|
|
74
|
+
useInnerText: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Enter adds new lines
|
|
78
|
+
await editorPage.getEditor().fill('Brock');
|
|
79
|
+
await editorPage.getEditor().press('Enter');
|
|
80
|
+
await editorPage.getEditor().press('Enter');
|
|
81
|
+
await expect(editorComponent).toHaveText('1\n2\n3\nBrock', {
|
|
82
|
+
useInnerText: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Shift-Enter adds new lines
|
|
86
|
+
await editorPage.getEditor().press('Shift+Enter');
|
|
87
|
+
await editorPage.getEditor().press('Shift+Enter');
|
|
88
|
+
await expect(editorComponent).toHaveText('1\n2\n3\n4\n5\nBrock', {
|
|
89
|
+
useInnerText: true,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('can execute queries and see them in history with newLineOnEnter', async ({
|
|
94
|
+
page,
|
|
95
|
+
mount,
|
|
96
|
+
}) => {
|
|
37
97
|
const editorPage = new CypherEditorPage(page);
|
|
38
98
|
|
|
39
99
|
const initialValue = `MATCH (n)
|
|
@@ -49,6 +109,7 @@ RETURN n;`;
|
|
|
49
109
|
value={initialValue}
|
|
50
110
|
history={history}
|
|
51
111
|
onExecute={onExecute}
|
|
112
|
+
newLineOnEnter
|
|
52
113
|
/>,
|
|
53
114
|
);
|
|
54
115
|
|
|
@@ -63,12 +124,12 @@ RETURN n;`;
|
|
|
63
124
|
await editorPage.getEditor().press('Meta+Enter');
|
|
64
125
|
expect(history.length).toBe(1);
|
|
65
126
|
|
|
66
|
-
// Ensure
|
|
127
|
+
// Ensure cmd+enter is required in multiline
|
|
67
128
|
await editorPage.getEditor().fill('multiline');
|
|
68
129
|
await editorPage.getEditor().press('Enter');
|
|
69
130
|
await editorPage.getEditor().press('Enter');
|
|
70
131
|
await editorPage.getEditor().press('Enter');
|
|
71
|
-
await editorPage.getEditor().press('Enter');
|
|
132
|
+
await editorPage.getEditor().press('Shift+Enter');
|
|
72
133
|
await page.keyboard.type('entry');
|
|
73
134
|
expect(history.length).toBe(1);
|
|
74
135
|
|
|
@@ -102,10 +163,15 @@ RETURN n;`;
|
|
|
102
163
|
await expect(page.getByText('multiline')).toBeVisible();
|
|
103
164
|
|
|
104
165
|
// arrow movements don't matter until bottom is hit
|
|
166
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
167
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
105
168
|
await editorPage.getEditor().press('ArrowUp');
|
|
106
169
|
await editorPage.getEditor().press('ArrowUp');
|
|
107
170
|
await editorPage.getEditor().press('ArrowDown');
|
|
108
171
|
await editorPage.getEditor().press('ArrowDown');
|
|
172
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
173
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
174
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
109
175
|
|
|
110
176
|
// editor still multiline
|
|
111
177
|
await expect(page.getByText('multiline')).toBeVisible();
|
|
@@ -115,6 +181,59 @@ RETURN n;`;
|
|
|
115
181
|
await expect(page.getByText('draft')).toBeVisible();
|
|
116
182
|
});
|
|
117
183
|
|
|
184
|
+
test('can execute queries without newLineOnEnter', async ({ page, mount }) => {
|
|
185
|
+
const editorPage = new CypherEditorPage(page);
|
|
186
|
+
|
|
187
|
+
const initialValue = 'Brock';
|
|
188
|
+
|
|
189
|
+
const history: string[] = [];
|
|
190
|
+
const onExecute = (cmd: string) => {
|
|
191
|
+
history.unshift(cmd);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const editorComponent = await mount(
|
|
195
|
+
<CypherEditor value={initialValue} onExecute={onExecute} />,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Cmd/Control still executes initial query
|
|
199
|
+
await editorPage.getEditor().press('Control+Enter');
|
|
200
|
+
await editorPage.getEditor().press('Meta+Enter');
|
|
201
|
+
expect(history.length).toBe(1);
|
|
202
|
+
|
|
203
|
+
// Ensure query execution doesn't fire if the query is only whitespace
|
|
204
|
+
await editorPage.getEditor().fill(' ');
|
|
205
|
+
await editorPage.getEditor().press('Control+Enter');
|
|
206
|
+
await editorPage.getEditor().press('Meta+Enter');
|
|
207
|
+
await editorPage.getEditor().press('Enter');
|
|
208
|
+
expect(history.length).toBe(1);
|
|
209
|
+
|
|
210
|
+
// Ensure enter executes query when in single line
|
|
211
|
+
await editorPage.getEditor().fill('Misty');
|
|
212
|
+
await editorPage.getEditor().press('Enter');
|
|
213
|
+
expect(history.length).toBe(2);
|
|
214
|
+
expect(history).toEqual(['Misty', 'Brock']);
|
|
215
|
+
|
|
216
|
+
// Ensure cmd+enter is required in multiline
|
|
217
|
+
await editorPage.getEditor().fill('multiline');
|
|
218
|
+
await editorPage.getEditor().press('Shift+Enter');
|
|
219
|
+
await editorPage.getEditor().press('Enter');
|
|
220
|
+
await editorPage.getEditor().press('A');
|
|
221
|
+
|
|
222
|
+
// line numbers and the text
|
|
223
|
+
await expect(editorComponent).toHaveText('1\n2\n3\nmultiline\nA', {
|
|
224
|
+
useInnerText: true,
|
|
225
|
+
});
|
|
226
|
+
await editorPage.getEditor().press('Enter');
|
|
227
|
+
await editorPage.getEditor().press('Enter');
|
|
228
|
+
await editorPage.getEditor().press('Enter');
|
|
229
|
+
await editorPage.getEditor().press('Enter');
|
|
230
|
+
expect(history.length).toBe(2);
|
|
231
|
+
|
|
232
|
+
await editorPage.getEditor().press('Control+Enter');
|
|
233
|
+
await editorPage.getEditor().press('Meta+Enter');
|
|
234
|
+
expect(history.length).toBe(3);
|
|
235
|
+
});
|
|
236
|
+
|
|
118
237
|
test('can navigate with cmd+up as well', async ({ page, mount }) => {
|
|
119
238
|
const editorPage = new CypherEditorPage(page);
|
|
120
239
|
const isMac = process.platform === 'darwin';
|
|
@@ -127,11 +246,11 @@ test('can navigate with cmd+up as well', async ({ page, mount }) => {
|
|
|
127
246
|
<CypherEditor
|
|
128
247
|
value={initialValue}
|
|
129
248
|
history={[
|
|
249
|
+
'first',
|
|
130
250
|
`one
|
|
131
251
|
multiline
|
|
132
252
|
entry
|
|
133
253
|
.`,
|
|
134
|
-
'second',
|
|
135
254
|
]}
|
|
136
255
|
onExecute={() => {
|
|
137
256
|
/* needed to turn on history movements */
|
|
@@ -139,24 +258,24 @@ entry
|
|
|
139
258
|
/>,
|
|
140
259
|
);
|
|
141
260
|
|
|
261
|
+
await editorPage.getEditor().press(metaUp);
|
|
262
|
+
await expect(page.getByText('first')).toBeVisible();
|
|
263
|
+
|
|
264
|
+
// move in history
|
|
142
265
|
await editorPage.getEditor().press(metaUp);
|
|
143
266
|
await expect(page.getByText('multiline')).toBeVisible();
|
|
144
267
|
|
|
145
|
-
// Single meta
|
|
268
|
+
// Single meta down moves all the way to top of editor on mac
|
|
146
269
|
if (isMac) {
|
|
147
|
-
await editorPage.getEditor().press(
|
|
270
|
+
await editorPage.getEditor().press(metaDown);
|
|
148
271
|
} else {
|
|
149
|
-
await editorPage.getEditor().press('
|
|
150
|
-
await editorPage.getEditor().press('
|
|
151
|
-
await editorPage.getEditor().press('
|
|
152
|
-
await editorPage.getEditor().press('
|
|
272
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
273
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
274
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
275
|
+
await editorPage.getEditor().press('ArrowDown');
|
|
153
276
|
}
|
|
154
|
-
// move in history
|
|
155
|
-
await editorPage.getEditor().press(metaUp);
|
|
156
|
-
await expect(page.getByText('second')).toBeVisible();
|
|
157
|
-
|
|
158
277
|
await editorPage.getEditor().press(metaDown);
|
|
159
|
-
await expect(page.getByText('
|
|
278
|
+
await expect(page.getByText('first')).toBeVisible();
|
|
160
279
|
});
|
|
161
280
|
|
|
162
281
|
test('test onExecute', async ({ page, mount }) => {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { expect, test } from '@playwright/experimental-ct-react';
|
|
2
2
|
import { CypherEditor } from '../CypherEditor';
|
|
3
3
|
|
|
4
|
-
test.use({ viewport: { width: 500, height: 500 } });
|
|
5
|
-
|
|
6
4
|
test('can mount the editor with text', async ({ mount }) => {
|
|
7
5
|
const component = await mount(<CypherEditor value="MATCH (n) RETURN n;" />);
|
|
8
6
|
|
|
@@ -307,3 +307,74 @@ test('Signature help does not blow up on empty query', async ({
|
|
|
307
307
|
timeout: 2000,
|
|
308
308
|
});
|
|
309
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
|
+
|
|
375
|
+
await expect(
|
|
376
|
+
page.locator('.cm-signature-help-panel.cm-tooltip-above'),
|
|
377
|
+
).toBeVisible({
|
|
378
|
+
timeout: 2000,
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { expect, test } from '@playwright/experimental-ct-react';
|
|
2
2
|
import { CypherEditor } from '../CypherEditor';
|
|
3
3
|
|
|
4
|
-
test.use({ viewport: { width: 500, height: 500 } });
|
|
5
|
-
|
|
6
4
|
test('can complete pattern snippet', async ({ page, mount }) => {
|
|
7
5
|
await mount(<CypherEditor />);
|
|
8
6
|
const textField = page.getByRole('textbox');
|
|
@@ -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 = `
|
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 {
|
|
@@ -108,7 +109,7 @@ export const createCypherTheme = ({
|
|
|
108
109
|
},
|
|
109
110
|
'& .cm-signature-help-panel-contents': {
|
|
110
111
|
overflow: 'auto',
|
|
111
|
-
maxHeight: '
|
|
112
|
+
maxHeight: '250px',
|
|
112
113
|
},
|
|
113
114
|
'& .cm-signature-help-panel-current-argument': {
|
|
114
115
|
color: settings.autoCompletionPanel.matchingTextColor,
|
|
@@ -123,7 +124,12 @@ export const createCypherTheme = ({
|
|
|
123
124
|
'& .cm-signature-help-panel-description': {
|
|
124
125
|
padding: '5px',
|
|
125
126
|
},
|
|
126
|
-
|
|
127
|
+
'.cm-completionInfo-signature': {
|
|
128
|
+
color: 'darkgrey',
|
|
129
|
+
},
|
|
130
|
+
'.cm-deprecated-completion': {
|
|
131
|
+
'text-decoration': 'line-through',
|
|
132
|
+
},
|
|
127
133
|
'.cm-tooltip-autocomplete': {
|
|
128
134
|
maxWidth: '430px',
|
|
129
135
|
'& > ul > li[aria-selected]': {
|
|
@@ -230,7 +236,7 @@ export const createCypherTheme = ({
|
|
|
230
236
|
([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
|
|
231
237
|
tag: tokenTypeToStyleTag[token],
|
|
232
238
|
color,
|
|
233
|
-
class: token ===
|
|
239
|
+
class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,
|
|
234
240
|
}),
|
|
235
241
|
);
|
|
236
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),
|
|
@@ -3,6 +3,7 @@ import { showTooltip, Tooltip } from '@codemirror/view';
|
|
|
3
3
|
import { signatureHelp } from '@neo4j-cypher/language-support';
|
|
4
4
|
import { SignatureInformation } from 'vscode-languageserver-types';
|
|
5
5
|
import { CypherConfig } from './langCypher';
|
|
6
|
+
import { getDocString } from './utils';
|
|
6
7
|
|
|
7
8
|
function getTriggerCharacter(query: string, caretPosition: number) {
|
|
8
9
|
let i = caretPosition - 1;
|
|
@@ -27,7 +28,7 @@ const createSignatureHelpElement =
|
|
|
27
28
|
}) =>
|
|
28
29
|
() => {
|
|
29
30
|
const parameters = signature.parameters;
|
|
30
|
-
const doc = signature.documentation
|
|
31
|
+
const doc = getDocString(signature.documentation);
|
|
31
32
|
const dom = document.createElement('div');
|
|
32
33
|
dom.className = 'cm-signature-help-panel';
|
|
33
34
|
|
|
@@ -100,12 +101,13 @@ function getSignatureHelpTooltip(
|
|
|
100
101
|
signatures[activeSignature].documentation !== undefined
|
|
101
102
|
) {
|
|
102
103
|
const signature = signatures[activeSignature];
|
|
104
|
+
const showSignatureTooltipBelow =
|
|
105
|
+
config.showSignatureTooltipBelow ?? true;
|
|
103
106
|
|
|
104
107
|
result = [
|
|
105
108
|
{
|
|
106
109
|
pos: caretPosition,
|
|
107
|
-
above:
|
|
108
|
-
strictSide: true,
|
|
110
|
+
above: !showSignatureTooltipBelow,
|
|
109
111
|
arrow: true,
|
|
110
112
|
create: createSignatureHelpElement({ signature, activeParameter }),
|
|
111
113
|
},
|