@neo4j-cypher/react-codemirror 2.0.0-next.11 → 2.0.0-next.13

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 (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/CypherEditor.d.ts +10 -1
  3. package/dist/CypherEditor.js +2 -1
  4. package/dist/CypherEditor.js.map +1 -1
  5. package/dist/e2e_tests/autoCompletion.spec.js +13 -7
  6. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
  7. package/dist/e2e_tests/signatureHelp.spec.js +11 -8
  8. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/lang-cypher/autocomplete.js +43 -57
  13. package/dist/lang-cypher/autocomplete.js.map +1 -1
  14. package/dist/lang-cypher/createCypherTheme.js +3 -0
  15. package/dist/lang-cypher/createCypherTheme.js.map +1 -1
  16. package/dist/lang-cypher/langCypher.d.ts +0 -1
  17. package/dist/lang-cypher/langCypher.js.map +1 -1
  18. package/dist/lang-cypher/signatureHelp.js +22 -7
  19. package/dist/lang-cypher/signatureHelp.js.map +1 -1
  20. package/dist/lang-cypher/syntaxValidation.js +1 -2
  21. package/dist/lang-cypher/syntaxValidation.js.map +1 -1
  22. package/dist/neo4jSetup.d.ts +5 -1
  23. package/dist/neo4jSetup.js +8 -8
  24. package/dist/neo4jSetup.js.map +1 -1
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +2 -2
  27. package/src/CypherEditor.tsx +13 -2
  28. package/src/e2e_tests/autoCompletion.spec.tsx +22 -13
  29. package/src/e2e_tests/signatureHelp.spec.tsx +12 -8
  30. package/src/index.ts +1 -4
  31. package/src/lang-cypher/autocomplete.ts +44 -59
  32. package/src/lang-cypher/createCypherTheme.ts +3 -0
  33. package/src/lang-cypher/langCypher.ts +0 -1
  34. package/src/lang-cypher/signatureHelp.ts +27 -9
  35. package/src/lang-cypher/syntaxValidation.ts +3 -3
  36. package/src/neo4jSetup.tsx +27 -16
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "codemirror",
18
18
  "codemirror 6"
19
19
  ],
20
- "version": "2.0.0-next.11",
20
+ "version": "2.0.0-next.13",
21
21
  "main": "./dist/index.js",
22
22
  "types": "./dist/index.d.ts",
23
23
  "type": "module",
@@ -51,7 +51,7 @@
51
51
  "@codemirror/view": "^6.29.1",
52
52
  "@lezer/common": "^1.0.2",
53
53
  "@lezer/highlight": "^1.1.3",
54
- "@neo4j-cypher/language-support": "2.0.0-next.8",
54
+ "@neo4j-cypher/language-support": "2.0.0-next.10",
55
55
  "@types/prismjs": "^1.26.3",
56
56
  "@types/workerpool": "^6.4.7",
57
57
  "ayu": "^8.0.1",
@@ -161,9 +161,19 @@ export interface CypherEditorProps {
161
161
  readonly?: boolean;
162
162
 
163
163
  /**
164
- * String value to assign to the aria-label attribute of the editor
164
+ * String value to assign to the aria-label attribute of the editor.
165
165
  */
166
166
  ariaLabel?: string;
167
+
168
+ /**
169
+ * Whether keybindings for inserting indents with the Tab key should be disabled.
170
+ *
171
+ * true will not create keybindings for inserting indents.
172
+ * false will create keybindings for inserting indents.
173
+ *
174
+ * @default false
175
+ */
176
+ moveFocusOnTab?: boolean;
167
177
  }
168
178
 
169
179
  const executeKeybinding = (
@@ -310,6 +320,7 @@ export class CypherEditor extends Component<
310
320
  theme: 'light',
311
321
  lineNumbers: true,
312
322
  newLineOnEnter: false,
323
+ moveFocusOnTab: false,
313
324
  };
314
325
 
315
326
  private debouncedOnChange = this.props.onChange
@@ -383,7 +394,7 @@ export class CypherEditor extends Component<
383
394
  ]),
384
395
  ),
385
396
  historyNavigation(this.props),
386
- basicNeo4jSetup(),
397
+ basicNeo4jSetup(this.props),
387
398
  themeCompartment.of(themeExtension),
388
399
  changeListener,
389
400
  cypher(this.schemaRef.current),
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
1
2
  import { testData } from '@neo4j-cypher/language-support';
2
3
  import { expect, test } from '@playwright/experimental-ct-react';
3
4
  import type { Page } from '@playwright/test';
@@ -113,6 +114,27 @@ test('can complete labels', async ({ mount, page }) => {
113
114
  await expect(component).toContainText('MATCH (n :Pokemon');
114
115
  });
115
116
 
117
+ test('can complete properties with backticks', async ({ mount, page }) => {
118
+ const component = await mount(
119
+ <CypherEditor
120
+ schema={{
121
+ propertyKeys: ['foo bar'],
122
+ }}
123
+ />,
124
+ );
125
+
126
+ const textField = page.getByRole('textbox');
127
+
128
+ await textField.fill('MATCH (n) RETURN n.foo');
129
+ await textField.press('Escape');
130
+ await textField.press('Control+ ');
131
+
132
+ await page.locator('.cm-tooltip-autocomplete').getByText('foo bar').click();
133
+ await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
134
+
135
+ await expect(component).toContainText('MATCH (n) RETURN n.`foo bar`');
136
+ });
137
+
116
138
  test('can update dbschema', async ({ mount, page }) => {
117
139
  const component = await mount(
118
140
  <CypherEditor
@@ -380,19 +402,6 @@ test('shows deprecated function as strikethrough on auto-completion', async ({
380
402
  await expect(page.locator('.cm-deprecated-completion')).toBeVisible();
381
403
  });
382
404
 
383
- test('does not signature help information on auto-completion if flag not enabled explicitly', async ({
384
- page,
385
- mount,
386
- }) => {
387
- await mount(<CypherEditor schema={testData.mockSchema} />);
388
-
389
- const textField = page.getByRole('textbox');
390
- await textField.fill('CALL apoc.periodic.');
391
-
392
- await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
393
- await expect(page.locator('.cm-completionInfo')).not.toBeVisible();
394
- });
395
-
396
405
  test('does not signature help information on auto-completion if docs and signature are empty', async ({
397
406
  page,
398
407
  mount,
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method */
1
2
  import { testData } from '@neo4j-cypher/language-support';
2
3
  import { expect, test } from '@playwright/experimental-ct-react';
3
4
  import { Locator } from 'playwright/test';
@@ -10,6 +11,8 @@ type TooltipExpectations = {
10
11
  excludes?: string[];
11
12
  };
12
13
 
14
+ const importCsvProc = testData.mockSchema.procedures['apoc.import.csv'];
15
+
13
16
  function testTooltip(tooltip: Locator, expectations: TooltipExpectations) {
14
17
  const includes = expectations.includes ?? [];
15
18
  const excludes = expectations.excludes ?? [];
@@ -83,8 +86,9 @@ test('Signature help shows the description for the first argument', async ({
83
86
 
84
87
  await testTooltip(tooltip, {
85
88
  includes: [
86
- 'nodes :: LIST<MAP>',
87
- 'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file',
89
+ testData.mockSchema.procedures['apoc.import.csv'].argumentDescription[0]
90
+ .description,
91
+ testData.mockSchema.procedures['apoc.import.csv'].description,
88
92
  ],
89
93
  });
90
94
  });
@@ -103,8 +107,8 @@ test('Signature help shows the description for the first argument when the curso
103
107
 
104
108
  await testTooltip(tooltip, {
105
109
  includes: [
106
- 'nodes :: LIST<MAP>',
107
- 'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file',
110
+ importCsvProc.argumentDescription[0].description,
111
+ importCsvProc.description,
108
112
  ],
109
113
  });
110
114
  });
@@ -127,8 +131,8 @@ test('Signature help shows the description for the second argument', async ({
127
131
 
128
132
  await testTooltip(tooltip, {
129
133
  includes: [
130
- 'rels :: LIST<MAP>',
131
- 'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file',
134
+ importCsvProc.argumentDescription[1].description,
135
+ importCsvProc.description,
132
136
  ],
133
137
  });
134
138
  });
@@ -147,8 +151,8 @@ test('Signature help shows the description for the second argument when the curs
147
151
 
148
152
  await testTooltip(tooltip, {
149
153
  includes: [
150
- 'rels :: LIST<MAP>',
151
- 'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file',
154
+ importCsvProc.argumentDescription[1].description,
155
+ importCsvProc.description,
152
156
  ],
153
157
  });
154
158
  });
package/src/index.ts CHANGED
@@ -1,7 +1,4 @@
1
- export {
2
- CypherParser,
3
- _internalFeatureFlags,
4
- } from '@neo4j-cypher/language-support';
1
+ export * as LanguageSupport from '@neo4j-cypher/language-support';
5
2
  export { CypherEditor } from './CypherEditor';
6
3
  export { cypher } from './lang-cypher/langCypher';
7
4
  export { darkThemeConstants, lightThemeConstants } from './themes';
@@ -77,73 +77,56 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
77
77
  }
78
78
 
79
79
  const options = autocomplete(
80
- documentText,
80
+ // TODO This is a temporary hack because completions are not working well
81
+ documentText.slice(0, context.pos),
81
82
  config.schema ?? {},
82
83
  context.pos,
83
84
  context.explicit,
84
85
  );
85
86
 
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');
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
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
- }
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);
102
101
  }
102
+ }
103
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
- }
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);
111
110
  }
111
+ }
112
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,
113
+ if (!emptyInfo) {
114
+ maybeInfo = {
115
+ info: () => Promise.resolve(newDiv),
139
116
  };
140
- }),
141
- };
142
- } else {
143
- return {
144
- from: context.matchBefore(/(\w|\$)*$/).from,
145
- options: options.map((o) => ({
146
- label: o.label,
117
+ }
118
+ const deprecated =
119
+ o.tags?.find((tag) => tag === CompletionItemTag.Deprecated) ?? false;
120
+ // The negative boost moves the deprecation down the list
121
+ // so we offer the user the completions that are
122
+ // deprecated the last
123
+ const maybeDeprecated = deprecated
124
+ ? { boost: -99, deprecated: true }
125
+ : {};
126
+
127
+ return {
128
+ label: o.insertText ? o.insertText : o.label,
129
+ displayLabel: o.label,
147
130
  type: completionKindToCodemirrorIcon(o.kind),
148
131
  apply:
149
132
  o.kind === CompletionItemKind.Snippet
@@ -151,7 +134,9 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
151
134
  snippet((o.insertText ?? o.label) + '${}')
152
135
  : undefined,
153
136
  detail: o.detail,
154
- })),
155
- };
156
- }
137
+ ...maybeDeprecated,
138
+ ...maybeInfo,
139
+ };
140
+ }),
141
+ };
157
142
  };
@@ -121,6 +121,9 @@ export const createCypherTheme = ({
121
121
  '& .cm-signature-help-panel-name': {
122
122
  padding: '5px',
123
123
  },
124
+ '& .cm-signature-help-panel-arg-description': {
125
+ padding: '5px',
126
+ },
124
127
  '& .cm-signature-help-panel-description': {
125
128
  padding: '5px',
126
129
  },
@@ -22,7 +22,6 @@ export type CypherConfig = {
22
22
  lint?: boolean;
23
23
  showSignatureTooltipBelow?: boolean;
24
24
  featureFlags?: {
25
- signatureInfoOnAutoCompletions?: boolean;
26
25
  consoleCommands?: boolean;
27
26
  };
28
27
  schema?: DbSchema;
@@ -1,7 +1,10 @@
1
1
  import { EditorState, StateField } from '@codemirror/state';
2
2
  import { showTooltip, Tooltip } from '@codemirror/view';
3
3
  import { signatureHelp } from '@neo4j-cypher/language-support';
4
- import { SignatureInformation } from 'vscode-languageserver-types';
4
+ import {
5
+ MarkupContent,
6
+ SignatureInformation,
7
+ } from 'vscode-languageserver-types';
5
8
  import { CypherConfig } from './langCypher';
6
9
  import { getDocString } from './utils';
7
10
 
@@ -38,24 +41,32 @@ const createSignatureHelpElement =
38
41
 
39
42
  const signatureLabel = document.createElement('div');
40
43
  signatureLabel.className = 'cm-signature-help-panel-name';
41
- signatureLabel.appendChild(document.createTextNode(`${signature.label}(`));
44
+ const methodName = signature.label.slice(0, signature.label.indexOf('('));
45
+ const returnType = signature.label.slice(signature.label.indexOf(')') + 1);
46
+ signatureLabel.appendChild(document.createTextNode(`${methodName}(`));
47
+ let currentParamDescription: string | undefined = undefined;
42
48
 
43
49
  parameters.forEach((param, index) => {
44
- if (typeof param.documentation === 'string') {
50
+ if (typeof param.label === 'string') {
45
51
  const span = document.createElement('span');
46
- span.appendChild(document.createTextNode(param.documentation));
52
+ span.appendChild(document.createTextNode(param.label));
47
53
  if (index !== parameters.length - 1) {
48
54
  span.appendChild(document.createTextNode(', '));
49
55
  }
50
56
 
51
57
  if (index === activeParameter) {
52
58
  span.className = 'cm-signature-help-panel-current-argument';
59
+ const paramDoc = param.documentation;
60
+ currentParamDescription = MarkupContent.is(paramDoc)
61
+ ? paramDoc.value
62
+ : paramDoc;
53
63
  }
54
64
  signatureLabel.appendChild(span);
55
65
  }
56
66
  });
57
67
 
58
68
  signatureLabel.appendChild(document.createTextNode(')'));
69
+ signatureLabel.appendChild(document.createTextNode(returnType));
59
70
 
60
71
  contents.appendChild(signatureLabel);
61
72
 
@@ -64,11 +75,18 @@ const createSignatureHelpElement =
64
75
 
65
76
  contents.appendChild(separator);
66
77
 
67
- const description = document.createElement('div');
68
- description.className = 'cm-signature-help-panel-description';
69
- description.appendChild(document.createTextNode(doc));
70
-
71
- contents.appendChild(description);
78
+ if (currentParamDescription !== undefined) {
79
+ const argDescription = document.createElement('div');
80
+ argDescription.className = 'cm-signature-help-panel-arg-description';
81
+ argDescription.appendChild(
82
+ document.createTextNode(currentParamDescription),
83
+ );
84
+ contents.appendChild(argDescription);
85
+ }
86
+ const methodDescription = document.createElement('div');
87
+ methodDescription.className = 'cm-signature-help-panel-description';
88
+ methodDescription.appendChild(document.createTextNode(doc));
89
+ contents.appendChild(methodDescription);
72
90
 
73
91
  return { dom };
74
92
  };
@@ -54,9 +54,9 @@ export const semanticAnalysisLinter: (config: CypherConfig) => Extension = (
54
54
  const parse = parserWrapper.parse(query);
55
55
  const statements = parse.statementsParsing;
56
56
 
57
- const anySyntacticError =
58
- statements.filter((statement) => statement.diagnostics.length !== 0)
59
- .length > 0;
57
+ const anySyntacticError = statements.some(
58
+ (statement) => statement.syntaxErrors.length !== 0,
59
+ );
60
60
 
61
61
  if (anySyntacticError) {
62
62
  return [];
@@ -61,7 +61,13 @@ const insertTab: StateCommand = (cmd) => {
61
61
  return true;
62
62
  };
63
63
 
64
- export const basicNeo4jSetup = (): Extension[] => {
64
+ type SetupProps = {
65
+ moveFocusOnTab?: boolean;
66
+ };
67
+
68
+ export const basicNeo4jSetup = ({
69
+ moveFocusOnTab = false,
70
+ }: SetupProps): Extension[] => {
65
71
  const keymaps: KeyBinding[] = [
66
72
  closeBracketsKeymap,
67
73
  defaultKeymap,
@@ -70,23 +76,28 @@ export const basicNeo4jSetup = (): Extension[] => {
70
76
  foldKeymap,
71
77
  completionKeymap,
72
78
  lintKeymap,
73
- {
74
- key: 'Tab',
75
- preventDefault: true,
76
- run: acceptCompletion,
77
- },
78
- {
79
- key: 'Tab',
80
- preventDefault: true,
81
- run: insertTab,
82
- },
83
- {
84
- key: 'Shift-Tab',
85
- preventDefault: true,
86
- run: indentLess,
87
- },
88
79
  ].flat();
89
80
 
81
+ if (!moveFocusOnTab) {
82
+ keymaps.push(
83
+ {
84
+ key: 'Tab',
85
+ preventDefault: true,
86
+ run: acceptCompletion,
87
+ },
88
+ {
89
+ key: 'Tab',
90
+ preventDefault: true,
91
+ run: insertTab,
92
+ },
93
+ {
94
+ key: 'Shift-Tab',
95
+ preventDefault: true,
96
+ run: indentLess,
97
+ },
98
+ );
99
+ }
100
+
90
101
  const extensions: Extension[] = [];
91
102
 
92
103
  extensions.push(highlightSpecialChars());