@neo4j-cypher/react-codemirror 2.0.0-next.7 → 2.0.0-next.8

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/CypherEditor.d.ts +32 -1
  3. package/dist/CypherEditor.js +45 -9
  4. package/dist/CypherEditor.js.map +1 -1
  5. package/dist/e2e_tests/autoCompletion.spec.js +0 -21
  6. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
  7. package/dist/e2e_tests/configuration.spec.d.ts +1 -0
  8. package/dist/e2e_tests/configuration.spec.js +73 -0
  9. package/dist/e2e_tests/configuration.spec.js.map +1 -0
  10. package/dist/e2e_tests/e2eUtils.js +9 -1
  11. package/dist/e2e_tests/e2eUtils.js.map +1 -1
  12. package/dist/e2e_tests/sanityChecks.spec.js +0 -9
  13. package/dist/e2e_tests/sanityChecks.spec.js.map +1 -1
  14. package/dist/e2e_tests/signatureHelp.spec.js +16 -15
  15. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
  16. package/dist/e2e_tests/snippets.spec.d.ts +1 -0
  17. package/dist/e2e_tests/snippets.spec.js +63 -0
  18. package/dist/e2e_tests/snippets.spec.js.map +1 -0
  19. package/dist/e2e_tests/syntaxValidation.spec.js +3 -3
  20. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -1
  21. package/dist/lang-cypher/createCypherTheme.js +26 -1
  22. package/dist/lang-cypher/createCypherTheme.js.map +1 -1
  23. package/dist/lang-cypher/signatureHelp.js +36 -20
  24. package/dist/lang-cypher/signatureHelp.js.map +1 -1
  25. package/dist/neo4jSetup.js +35 -1
  26. package/dist/neo4jSetup.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +2 -2
  29. package/src/CypherEditor.tsx +102 -10
  30. package/src/e2e_tests/autoCompletion.spec.tsx +0 -33
  31. package/src/e2e_tests/configuration.spec.tsx +97 -0
  32. package/src/e2e_tests/e2eUtils.ts +11 -1
  33. package/src/e2e_tests/sanityChecks.spec.tsx +0 -14
  34. package/src/e2e_tests/signatureHelp.spec.tsx +16 -19
  35. package/src/e2e_tests/snippets.spec.tsx +94 -0
  36. package/src/e2e_tests/syntaxValidation.spec.tsx +3 -3
  37. package/src/lang-cypher/createCypherTheme.ts +27 -1
  38. package/src/lang-cypher/signatureHelp.ts +57 -28
  39. package/src/neo4jSetup.tsx +51 -1
@@ -9,6 +9,7 @@ import {
9
9
  KeyBinding,
10
10
  keymap,
11
11
  lineNumbers,
12
+ placeholder,
12
13
  ViewUpdate,
13
14
  } from '@codemirror/view';
14
15
  import type { DbSchema } from '@neo4j-cypher/language-support';
@@ -23,6 +24,7 @@ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
23
24
  import { basicNeo4jSetup } from './neo4jSetup';
24
25
  import { getThemeExtension } from './themes';
25
26
 
27
+ type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
26
28
  export interface CypherEditorProps {
27
29
  /**
28
30
  * The prompt to show on single line editors
@@ -42,7 +44,7 @@ export interface CypherEditorProps {
42
44
  */
43
45
  onExecute?: (cmd: string) => void;
44
46
  /**
45
- * The editor history navigateable via up/down arrow keys. Order newest to oldest.
47
+ * The editor history navigable via up/down arrow keys. Order newest to oldest.
46
48
  * Add to this list with the `onExecute` callback for REPL style history.
47
49
  */
48
50
  history?: string[];
@@ -102,6 +104,37 @@ export interface CypherEditorProps {
102
104
  * @param {ViewUpdate} viewUpdate - the view update from codemirror
103
105
  */
104
106
  onChange?(value: string, viewUpdate: ViewUpdate): void;
107
+
108
+ /**
109
+ * Map of event handlers to add to the editor.
110
+ *
111
+ * Note that the props are compared by reference, meaning object defined inline
112
+ * will cause the editor to re-render (much like the style prop does in this example:
113
+ * <div style={{}} />
114
+ *
115
+ * Memoize the object if you want/need to avoid this.
116
+ *
117
+ * @example
118
+ * // listen to blur events
119
+ * <CypherEditor domEventHandlers={{blur: () => console.log("blur event fired")}} />
120
+ */
121
+ domEventHandlers?: DomEventHandlers;
122
+ /**
123
+ * Placeholder text to display when the editor is empty.
124
+ */
125
+ placeholder?: string;
126
+ /**
127
+ * Whether the editor should show line numbers.
128
+ *
129
+ * @default true
130
+ */
131
+ lineNumbers?: boolean;
132
+ /**
133
+ * Whether the editor is read-only.
134
+ *
135
+ * @default false
136
+ */
137
+ readonly?: boolean;
105
138
  }
106
139
 
107
140
  const executeKeybinding = (onExecute?: (cmd: string) => void) =>
@@ -125,6 +158,20 @@ const executeKeybinding = (onExecute?: (cmd: string) => void) =>
125
158
 
126
159
  const themeCompartment = new Compartment();
127
160
  const keyBindingCompartment = new Compartment();
161
+ const lineNumbersCompartment = new Compartment();
162
+ const readOnlyCompartment = new Compartment();
163
+ const placeholderCompartment = new Compartment();
164
+ const domEventHandlerCompartment = new Compartment();
165
+
166
+ const formatLineNumber =
167
+ (prompt?: string) => (a: number, state: EditorState) => {
168
+ if (state.doc.lines === 1 && prompt !== undefined) {
169
+ return prompt;
170
+ }
171
+
172
+ return a.toString();
173
+ };
174
+
128
175
  type CypherEditorState = { cypherSupportEnabled: boolean };
129
176
 
130
177
  const ExternalEdit = Annotation.define<boolean>();
@@ -188,6 +235,7 @@ export class CypherEditor extends Component<
188
235
  extraKeybindings: [],
189
236
  history: [],
190
237
  theme: 'light',
238
+ lineNumbers: true,
191
239
  };
192
240
 
193
241
  private debouncedOnChange = this.props.onChange
@@ -249,15 +297,20 @@ export class CypherEditor extends Component<
249
297
  cypher(this.schemaRef.current),
250
298
  lineWrap ? EditorView.lineWrapping : [],
251
299
 
252
- lineNumbers({
253
- formatNumber: (a, state) => {
254
- if (state.doc.lines === 1 && this.props.prompt !== undefined) {
255
- return this.props.prompt;
256
- }
257
-
258
- return a.toString();
259
- },
260
- }),
300
+ lineNumbersCompartment.of(
301
+ this.props.lineNumbers
302
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
303
+ : [],
304
+ ),
305
+ readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
306
+ placeholderCompartment.of(
307
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
308
+ ),
309
+ domEventHandlerCompartment.of(
310
+ this.props.domEventHandlers
311
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
312
+ : [],
313
+ ),
261
314
  ],
262
315
  doc: this.props.value,
263
316
  });
@@ -313,6 +366,35 @@ export class CypherEditor extends Component<
313
366
  });
314
367
  }
315
368
 
369
+ if (
370
+ prevProps.lineNumbers !== this.props.lineNumbers ||
371
+ prevProps.prompt !== this.props.prompt
372
+ ) {
373
+ this.editorView.current.dispatch({
374
+ effects: lineNumbersCompartment.reconfigure(
375
+ this.props.lineNumbers
376
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
377
+ : [],
378
+ ),
379
+ });
380
+ }
381
+
382
+ if (prevProps.readonly !== this.props.readonly) {
383
+ this.editorView.current.dispatch({
384
+ effects: readOnlyCompartment.reconfigure(
385
+ EditorState.readOnly.of(this.props.readonly),
386
+ ),
387
+ });
388
+ }
389
+
390
+ if (prevProps.placeholder !== this.props.placeholder) {
391
+ this.editorView.current.dispatch({
392
+ effects: placeholderCompartment.reconfigure(
393
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
394
+ ),
395
+ });
396
+ }
397
+
316
398
  if (
317
399
  prevProps.extraKeybindings !== this.props.extraKeybindings ||
318
400
  prevProps.onExecute !== this.props.onExecute
@@ -327,6 +409,16 @@ export class CypherEditor extends Component<
327
409
  });
328
410
  }
329
411
 
412
+ if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
413
+ this.editorView.current.dispatch({
414
+ effects: domEventHandlerCompartment.reconfigure(
415
+ this.props.domEventHandlers
416
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
417
+ : [],
418
+ ),
419
+ });
420
+ }
421
+
330
422
  // This component rerenders on every keystroke and comparing the
331
423
  // full lists of editor strings on every render could be expensive.
332
424
  const didChangeHistoryEstimate =
@@ -234,36 +234,3 @@ test('completes allShortestPaths correctly', async ({ page, mount }) => {
234
234
  'MATCH (n) REURN n; MATCH allShortestPaths',
235
235
  );
236
236
  });
237
-
238
- test('can complete pattern snippet', async ({ page, mount }) => {
239
- await mount(<CypherEditor />);
240
- const textField = page.getByRole('textbox');
241
-
242
- await textField.fill('MATCH ()-[]->()');
243
-
244
- await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
245
- await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
246
-
247
- await textField.press('Tab');
248
- await textField.press('Tab');
249
- await textField.press('Tab');
250
-
251
- await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->( )');
252
- });
253
-
254
- test('does not automatically open completion panel for expressions after snippet trigger char', async ({
255
- page,
256
- mount,
257
- }) => {
258
- await mount(<CypherEditor />);
259
- const textField = page.getByRole('textbox');
260
-
261
- await textField.fill('RETURN (1)');
262
-
263
- // expect the panel to not show up
264
- await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
265
-
266
- // unless manually triggered
267
- await textField.press('Control+ ');
268
- await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
269
- });
@@ -0,0 +1,97 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+
4
+ test.use({ viewport: { width: 500, height: 500 } });
5
+
6
+ test('prompt shows up', async ({ mount, page }) => {
7
+ const component = await mount(<CypherEditor prompt="neo4j>" />);
8
+
9
+ await expect(component).toContainText('neo4j>');
10
+
11
+ await component.update(<CypherEditor prompt="test>" />);
12
+ await expect(component).toContainText('test>');
13
+
14
+ const textField = page.getByRole('textbox');
15
+ await textField.press('a');
16
+
17
+ await expect(textField).toHaveText('a');
18
+ });
19
+
20
+ test('line numbers can be turned on/off', async ({ mount }) => {
21
+ const component = await mount(<CypherEditor lineNumbers />);
22
+
23
+ await expect(component).toContainText('1');
24
+
25
+ await component.update(<CypherEditor lineNumbers={false} />);
26
+ await expect(component).not.toContainText('1');
27
+ });
28
+
29
+ test('can configure readonly', async ({ mount, page }) => {
30
+ const component = await mount(<CypherEditor readonly />);
31
+
32
+ const textField = page.getByRole('textbox');
33
+ await textField.press('a');
34
+ await expect(textField).not.toHaveText('a');
35
+
36
+ await component.update(<CypherEditor readonly={false} />);
37
+ await textField.press('b');
38
+ await expect(textField).toHaveText('b');
39
+ });
40
+
41
+ test('can set placeholder ', async ({ mount, page }) => {
42
+ const component = await mount(<CypherEditor placeholder="bulbasaur" />);
43
+
44
+ const textField = page.getByRole('textbox');
45
+ await expect(textField).toHaveText('bulbasaur');
46
+
47
+ await component.update(<CypherEditor placeholder="venusaur" />);
48
+ await expect(textField).not.toHaveText('bulbasaur');
49
+ await expect(textField).toHaveText('venusaur');
50
+
51
+ await textField.fill('abc');
52
+ await expect(textField).not.toHaveText('venusaur');
53
+ await expect(textField).toHaveText('abc');
54
+ });
55
+
56
+ test('can set/unset onFocus/onBlur', async ({ mount, page }) => {
57
+ const component = await mount(<CypherEditor />);
58
+
59
+ let focusFireCount = 0;
60
+ let blurFireCount = 0;
61
+
62
+ const focus = () => {
63
+ focusFireCount += 1;
64
+ };
65
+ const blur = () => {
66
+ blurFireCount += 1;
67
+ };
68
+
69
+ await component.update(<CypherEditor domEventHandlers={{ blur, focus }} />);
70
+
71
+ const textField = page.getByRole('textbox');
72
+ await textField.click();
73
+ await expect(textField).toBeFocused();
74
+
75
+ // this is to give the events time to fire
76
+ await expect(() => {
77
+ expect(focusFireCount).toBe(1);
78
+ expect(blurFireCount).toBe(0);
79
+ }).toPass();
80
+
81
+ await textField.blur();
82
+
83
+ await expect(() => {
84
+ expect(focusFireCount).toBe(1);
85
+ expect(blurFireCount).toBe(1);
86
+ }).toPass();
87
+
88
+ await component.update(<CypherEditor />);
89
+ await textField.click();
90
+ await expect(textField).toBeFocused();
91
+ await textField.blur();
92
+
93
+ await expect(() => {
94
+ expect(focusFireCount).toBe(1);
95
+ expect(blurFireCount).toBe(1);
96
+ }).toPass();
97
+ });
@@ -65,11 +65,21 @@ export class CypherEditorPage {
65
65
  expectedMsg: string,
66
66
  ) {
67
67
  await expect(this.page.locator('.cm-lintRange-' + type).last()).toBeVisible(
68
- { timeout: 2000 },
68
+ { timeout: 3000 },
69
69
  );
70
70
 
71
71
  await this.page.getByText(queryChunk, { exact: true }).hover();
72
72
  await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible();
73
73
  await expect(this.page.getByText(expectedMsg)).toBeVisible();
74
+ /* Return the mouse to the beginning of the query and
75
+ This is because if for example we have an overlay with a
76
+ first interaction that covers the element we want to perform
77
+ the second interaction on, we won't be able to see that second element
78
+ */
79
+ await this.page.mouse.move(0, 0);
80
+ // Make the sure the tooltip closed
81
+ await expect(
82
+ this.page.locator('.cm-tooltip-hover').last(),
83
+ ).not.toBeVisible();
74
84
  }
75
85
  }
@@ -76,17 +76,3 @@ test('can complete CALL/CREATE', async ({ page, mount }) => {
76
76
 
77
77
  await expect(textField).toHaveText('CALL');
78
78
  });
79
-
80
- test('prompt shows up', async ({ mount, page }) => {
81
- const component = await mount(<CypherEditor prompt="neo4j>" />);
82
-
83
- await expect(component).toContainText('neo4j>');
84
-
85
- await component.update(<CypherEditor prompt="test>" />);
86
- await expect(component).toContainText('test>');
87
-
88
- const textField = page.getByRole('textbox');
89
- await textField.press('a');
90
-
91
- await expect(textField).toHaveText('a');
92
- });
@@ -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-tooltip-signature-help').last()).toBeVisible({
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-tooltip-signature-help').last()).toBeVisible({
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
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-tooltip-signature-help').last();
263
+ const tooltip = page.locator('.cm-signature-help-panel');
263
264
 
264
265
  await testTooltip(tooltip, {
265
266
  includes: [
266
- 'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file',
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,7 @@ test('Signature help does not blow up on empty query', async ({
304
303
  />,
305
304
  );
306
305
 
307
- await expect(
308
- page.locator('.cm-tooltip-signature-help').last(),
309
- ).not.toBeVisible({
306
+ await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
310
307
  timeout: 2000,
311
308
  });
312
309
  });
@@ -0,0 +1,94 @@
1
+ import { expect, test } from '@playwright/experimental-ct-react';
2
+ import { CypherEditor } from '../CypherEditor';
3
+
4
+ test.use({ viewport: { width: 500, height: 500 } });
5
+
6
+ test('can complete pattern snippet', async ({ page, mount }) => {
7
+ await mount(<CypherEditor />);
8
+ const textField = page.getByRole('textbox');
9
+
10
+ await textField.fill('MATCH ()-[]->()');
11
+
12
+ await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
13
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
14
+
15
+ await textField.press('Tab');
16
+ await textField.press('Tab');
17
+
18
+ await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->( )');
19
+ });
20
+
21
+ test('can navigate snippet', async ({ page, mount }) => {
22
+ await mount(<CypherEditor />);
23
+ const textField = page.getByRole('textbox');
24
+
25
+ await textField.fill('CREATE INDEX abc FOR ()');
26
+
27
+ await page
28
+ .locator('.cm-tooltip-autocomplete')
29
+ .getByText('-[]-()', { exact: true })
30
+ .click();
31
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
32
+ await expect(page.locator('.cm-snippetField')).toHaveCount(2);
33
+
34
+ await textField.press('Tab');
35
+ await textField.press('Shift+Tab');
36
+
37
+ await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[ ]-( )');
38
+
39
+ await textField.press('a');
40
+ await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[a]-( )');
41
+
42
+ await textField.press('Escape');
43
+ await textField.press('Escape');
44
+ await expect(page.locator('.cm-snippetField')).toHaveCount(0);
45
+ await textField.press('Tab');
46
+ await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[a ]-( )');
47
+ });
48
+
49
+ test('can accept completion inside pattern snippet', async ({
50
+ page,
51
+ mount,
52
+ }) => {
53
+ await mount(<CypherEditor schema={{ labels: ['City'] }} />);
54
+ const textField = page.getByRole('textbox');
55
+
56
+ await textField.fill('MATCH ()-[]->()');
57
+
58
+ await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
59
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
60
+
61
+ // move to node
62
+ await textField.press('Tab');
63
+
64
+ // get & accept completion
65
+ await textField.press(':');
66
+ await expect(
67
+ page.locator('.cm-tooltip-autocomplete').getByText('City'),
68
+ ).toBeVisible();
69
+
70
+ await textField.press('Tab');
71
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
72
+
73
+ // tab out of the snippet
74
+ await textField.press('Tab');
75
+
76
+ await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->(:City)');
77
+ });
78
+
79
+ test('does not automatically open completion panel for expressions after snippet trigger char', async ({
80
+ page,
81
+ mount,
82
+ }) => {
83
+ await mount(<CypherEditor />);
84
+ const textField = page.getByRole('textbox');
85
+
86
+ await textField.fill('RETURN (1)');
87
+
88
+ // expect the panel to not show up
89
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
90
+
91
+ // unless manually triggered
92
+ await textField.press('Control+ ');
93
+ await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
94
+ });
@@ -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(
@@ -64,8 +64,9 @@ export const createCypherTheme = ({
64
64
  color: settings.gutterForeground,
65
65
  border: 'none',
66
66
  },
67
- '&.cm-editor .cm-scroller': {
67
+ '&.cm-editor': {
68
68
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
69
+ height: '100%',
69
70
  },
70
71
  '.cm-content': {
71
72
  caretColor: settings.cursor,
@@ -99,7 +100,32 @@ export const createCypherTheme = ({
99
100
  color: settings.autoCompletionPanel.matchingTextColor,
100
101
  textDecoration: 'none',
101
102
  },
103
+ '& .cm-signature-help-panel': {
104
+ backgroundColor: settings.autoCompletionPanel.backgroundColor,
105
+ maxWidth: '700px',
106
+ maxHeight: '250px',
107
+ fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
108
+ },
109
+ '& .cm-signature-help-panel-contents': {
110
+ overflow: 'auto',
111
+ maxHeight: '100%',
112
+ },
113
+ '& .cm-signature-help-panel-current-argument': {
114
+ color: settings.autoCompletionPanel.matchingTextColor,
115
+ fontWeight: 'bold',
116
+ },
117
+ '& .cm-signature-help-panel-separator': {
118
+ borderBottom: '1px solid #ccc',
119
+ },
120
+ '& .cm-signature-help-panel-name': {
121
+ padding: '5px',
122
+ },
123
+ '& .cm-signature-help-panel-description': {
124
+ padding: '5px',
125
+ },
126
+
102
127
  '.cm-tooltip-autocomplete': {
128
+ maxWidth: '430px',
103
129
  '& > ul > li[aria-selected]': {
104
130
  backgroundColor: settings.autoCompletionPanel.selectedColor,
105
131
  color: settings.foreground,