@neo4j-cypher/react-codemirror 2.0.0-next.15 → 2.0.0-next.17

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 (51) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/CypherEditor.d.ts +5 -1
  3. package/dist/CypherEditor.js +20 -0
  4. package/dist/CypherEditor.js.map +1 -1
  5. package/dist/e2e_tests/autoCompletion.spec.js +82 -37
  6. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
  7. package/dist/e2e_tests/debounce.spec.js +2 -1
  8. package/dist/e2e_tests/debounce.spec.js.map +1 -1
  9. package/dist/e2e_tests/e2eUtils.d.ts +1 -0
  10. package/dist/e2e_tests/e2eUtils.js +13 -2
  11. package/dist/e2e_tests/e2eUtils.js.map +1 -1
  12. package/dist/e2e_tests/performanceTest.spec.js +1 -1
  13. package/dist/e2e_tests/performanceTest.spec.js.map +1 -1
  14. package/dist/e2e_tests/signatureHelp.spec.js +59 -13
  15. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
  16. package/dist/e2e_tests/snippets.spec.js +2 -2
  17. package/dist/e2e_tests/snippets.spec.js.map +1 -1
  18. package/dist/e2e_tests/syntaxValidation.spec.js +33 -4
  19. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -1
  20. package/dist/lang-cypher/autocomplete.js +10 -5
  21. package/dist/lang-cypher/autocomplete.js.map +1 -1
  22. package/dist/lang-cypher/constants.d.ts +2 -0
  23. package/dist/lang-cypher/constants.js +4 -0
  24. package/dist/lang-cypher/constants.js.map +1 -1
  25. package/dist/lang-cypher/createCypherTheme.js +1 -1
  26. package/dist/lang-cypher/createCypherTheme.js.map +1 -1
  27. package/dist/lang-cypher/langCypher.d.ts +1 -0
  28. package/dist/lang-cypher/langCypher.js +1 -8
  29. package/dist/lang-cypher/langCypher.js.map +1 -1
  30. package/dist/lang-cypher/lintWorker.d.ts +8 -4
  31. package/dist/lang-cypher/lintWorker.js +12 -2
  32. package/dist/lang-cypher/lintWorker.js.map +1 -1
  33. package/dist/lang-cypher/syntaxValidation.d.ts +0 -1
  34. package/dist/lang-cypher/syntaxValidation.js +15 -31
  35. package/dist/lang-cypher/syntaxValidation.js.map +1 -1
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +2 -2
  38. package/src/CypherEditor.tsx +32 -4
  39. package/src/e2e_tests/autoCompletion.spec.tsx +121 -56
  40. package/src/e2e_tests/debounce.spec.tsx +37 -32
  41. package/src/e2e_tests/e2eUtils.ts +14 -2
  42. package/src/e2e_tests/performanceTest.spec.tsx +1 -1
  43. package/src/e2e_tests/signatureHelp.spec.tsx +74 -13
  44. package/src/e2e_tests/snippets.spec.tsx +2 -2
  45. package/src/e2e_tests/syntaxValidation.spec.tsx +76 -4
  46. package/src/lang-cypher/autocomplete.ts +14 -8
  47. package/src/lang-cypher/constants.ts +4 -0
  48. package/src/lang-cypher/createCypherTheme.ts +1 -1
  49. package/src/lang-cypher/langCypher.ts +3 -12
  50. package/src/lang-cypher/lintWorker.ts +24 -7
  51. package/src/lang-cypher/syntaxValidation.ts +15 -45
@@ -13,7 +13,7 @@ test('Prop lint set to false disables syntax validation', async ({
13
13
  await mount(<CypherEditor value={query} lint={false} />);
14
14
 
15
15
  await expect(page.locator('.cm-lintRange-error').last()).not.toBeVisible({
16
- timeout: 2000,
16
+ timeout: 10000,
17
17
  });
18
18
  });
19
19
 
@@ -24,7 +24,7 @@ test('Can turn linting back on', async ({ page, mount }) => {
24
24
  const component = await mount(<CypherEditor value={query} lint={false} />);
25
25
 
26
26
  await expect(page.locator('.cm-lintRange-error').last()).not.toBeVisible({
27
- timeout: 2000,
27
+ timeout: 10000,
28
28
  });
29
29
 
30
30
  await component.update(<CypherEditor value={query} lint={true} />);
@@ -33,7 +33,7 @@ test('Can turn linting back on', async ({ page, mount }) => {
33
33
 
34
34
  await editorPage.checkErrorMessage(
35
35
  'METCH',
36
- 'Unrecognized keyword. Did you mean MATCH?',
36
+ `Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'`,
37
37
  );
38
38
  });
39
39
 
@@ -45,10 +45,22 @@ test('Syntactic errors are surfaced', async ({ page, mount }) => {
45
45
 
46
46
  await editorPage.checkErrorMessage(
47
47
  'METCH',
48
- 'Unrecognized keyword. Did you mean MATCH?',
48
+ `Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'`,
49
49
  );
50
50
  });
51
51
 
52
+ test('Does not trigger syntax errors for backticked parameters in parameter creation', async ({
53
+ page,
54
+ mount,
55
+ }) => {
56
+ const editorPage = new CypherEditorPage(page);
57
+
58
+ const query = ':param x => "abc"';
59
+ await mount(<CypherEditor value={query} />);
60
+
61
+ await editorPage.checkNoNotificationMessage('error');
62
+ });
63
+
52
64
  test('Errors for undefined labels are surfaced', async ({ page, mount }) => {
53
65
  const editorPage = new CypherEditorPage(page);
54
66
  const query = 'MATCH (n: Person) RETURN n';
@@ -169,3 +181,63 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
169
181
  "Invalid input. '-1' is not a valid value. Must be a positive integer",
170
182
  );
171
183
  });
184
+
185
+ test('Strikethroughs are shown for deprecated functions', async ({
186
+ page,
187
+ mount,
188
+ }) => {
189
+ const editorPage = new CypherEditorPage(page);
190
+ const query = `RETURN id()`;
191
+
192
+ await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
193
+ await expect(
194
+ editorPage.page.locator('.cm-deprecated-element').last(),
195
+ ).toBeVisible({ timeout: 10000 });
196
+ await editorPage.checkWarningMessage('id', 'Function id is deprecated.');
197
+ });
198
+
199
+ test('Strikethroughs are shown for deprecated procedures', async ({
200
+ page,
201
+ mount,
202
+ }) => {
203
+ const editorPage = new CypherEditorPage(page);
204
+ const query = `CALL apoc.create.uuids()`;
205
+
206
+ await mount(<CypherEditor value={query} schema={testData.mockSchema} />);
207
+ await expect(
208
+ editorPage.page.locator('.cm-deprecated-element').last(),
209
+ ).toBeVisible({ timeout: 10000 });
210
+
211
+ await editorPage.checkWarningMessage(
212
+ 'apoc.create.uuids',
213
+ 'Procedure apoc.create.uuids is deprecated.',
214
+ );
215
+ });
216
+
217
+ test('Syntax validation depends on the Cypher version', async ({
218
+ page,
219
+ mount,
220
+ }) => {
221
+ await mount(
222
+ <CypherEditor
223
+ schema={testData.mockSchema}
224
+ featureFlags={{ cypher25: true }}
225
+ />,
226
+ );
227
+
228
+ const editorPage = new CypherEditorPage(page);
229
+ const textField = page.getByRole('textbox');
230
+ await textField.fill('CYPHER 5 CALL apoc.create.uuids(5)');
231
+
232
+ await editorPage.checkWarningMessage(
233
+ 'apoc.create.uuids',
234
+ 'Procedure apoc.create.uuids is deprecated.',
235
+ );
236
+
237
+ await textField.fill('CYPHER 25 CALL apoc.create.uuids(5)');
238
+
239
+ await editorPage.checkErrorMessage(
240
+ 'apoc.create.uuids',
241
+ 'Procedure apoc.create.uuids is not present in the database.',
242
+ );
243
+ });
@@ -3,7 +3,10 @@ import {
3
3
  CompletionSource,
4
4
  snippet,
5
5
  } from '@codemirror/autocomplete';
6
- import { autocomplete } from '@neo4j-cypher/language-support';
6
+ import {
7
+ autocomplete,
8
+ shouldAutoCompleteYield,
9
+ } from '@neo4j-cypher/language-support';
7
10
  import {
8
11
  CompletionItemKind,
9
12
  CompletionItemTag,
@@ -49,7 +52,7 @@ export const completionStyles: (
49
52
  completion: Completion & { deprecated?: boolean },
50
53
  ) => string = (completion) => {
51
54
  if (completion.deprecated) {
52
- return 'cm-deprecated-completion';
55
+ return 'cm-deprecated-element';
53
56
  } else {
54
57
  return null;
55
58
  }
@@ -58,15 +61,18 @@ export const completionStyles: (
58
61
  export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
59
62
  (config) => (context) => {
60
63
  const documentText = context.state.doc.toString();
61
-
64
+ const offset = context.pos;
62
65
  const triggerCharacters = ['.', ':', '{', '$', ')'];
63
- const lastCharacter = documentText.at(context.pos - 1);
64
-
66
+ const lastCharacter = documentText.at(offset - 1);
67
+ const yieldTriggered = shouldAutoCompleteYield(documentText, offset);
65
68
  const lastWord = context.matchBefore(/\w*/);
66
69
  const inWord = lastWord.from !== lastWord.to;
67
70
 
68
71
  const shouldTriggerCompletion =
69
- inWord || context.explicit || triggerCharacters.includes(lastCharacter);
72
+ inWord ||
73
+ context.explicit ||
74
+ triggerCharacters.includes(lastCharacter) ||
75
+ yieldTriggered;
70
76
 
71
77
  if (config.useLightVersion && !context.explicit) {
72
78
  return null;
@@ -78,9 +84,9 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
78
84
 
79
85
  const options = autocomplete(
80
86
  // TODO This is a temporary hack because completions are not working well
81
- documentText.slice(0, context.pos),
87
+ documentText.slice(0, offset),
82
88
  config.schema ?? {},
83
- context.pos,
89
+ offset,
84
90
  context.explicit,
85
91
  );
86
92
 
@@ -40,6 +40,8 @@ export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
40
40
  relationship: NodeType.define({ id: 26, name: 'label' }),
41
41
  boolean: NodeType.define({ id: 27, name: 'booleanLiteral' }),
42
42
  number: NodeType.define({ id: 28, name: 'numberLiteral' }),
43
+ setting: NodeType.define({ id: 29, name: 'setting' }),
44
+ settingValue: NodeType.define({ id: 30, name: 'settingValue' }),
43
45
  });
44
46
 
45
47
  export type PrismSpecificTokenType =
@@ -78,6 +80,8 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
78
80
  punctuation: tags.punctuation,
79
81
  separator: tags.separator,
80
82
  consoleCommand: tags.macroName,
83
+ setting: tags.attributeName,
84
+ settingValue: tags.attributeValue,
81
85
  };
82
86
 
83
87
  export const parserAdapterNodeSet = (nodes: Record<string, NodeType>) =>
@@ -130,7 +130,7 @@ export const createCypherTheme = ({
130
130
  '.cm-completionInfo-signature': {
131
131
  color: 'darkgrey',
132
132
  },
133
- '.cm-deprecated-completion': {
133
+ '.cm-deprecated-element': {
134
134
  'text-decoration': 'line-through',
135
135
  },
136
136
  '.cm-tooltip-autocomplete': {
@@ -4,14 +4,11 @@ import {
4
4
  Language,
5
5
  LanguageSupport,
6
6
  } from '@codemirror/language';
7
- import {
8
- _internalFeatureFlags,
9
- type DbSchema,
10
- } from '@neo4j-cypher/language-support';
7
+ import { type DbSchema } from '@neo4j-cypher/language-support';
11
8
  import { completionStyles, cypherAutocomplete } from './autocomplete';
12
9
  import { ParserAdapter } from './parser-adapter';
13
10
  import { signatureHelpTooltip } from './signatureHelp';
14
- import { cypherLinter, semanticAnalysisLinter } from './syntaxValidation';
11
+ import { cypherLinter } from './syntaxValidation';
15
12
 
16
13
  const facet = defineLanguageFacet({
17
14
  commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
@@ -23,6 +20,7 @@ export type CypherConfig = {
23
20
  showSignatureTooltipBelow?: boolean;
24
21
  featureFlags?: {
25
22
  consoleCommands?: boolean;
23
+ cypher25?: boolean;
26
24
  };
27
25
  schema?: DbSchema;
28
26
  useLightVersion: boolean;
@@ -30,12 +28,6 @@ export type CypherConfig = {
30
28
  };
31
29
 
32
30
  export function cypher(config: CypherConfig) {
33
- const featureFlags = config.featureFlags;
34
- // We allow to override the consoleCommands feature flag
35
- if (featureFlags.consoleCommands !== undefined) {
36
- _internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
37
- }
38
-
39
31
  const parserAdapter = new ParserAdapter(facet, config);
40
32
 
41
33
  const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
@@ -46,7 +38,6 @@ export function cypher(config: CypherConfig) {
46
38
  optionClass: completionStyles,
47
39
  }),
48
40
  cypherLinter(config),
49
- semanticAnalysisLinter(config),
50
41
  signatureHelpTooltip(config),
51
42
  ]);
52
43
  }
@@ -1,14 +1,31 @@
1
- import { validateSemantics } from '@neo4j-cypher/language-support';
1
+ import {
2
+ DbSchema,
3
+ lintCypherQuery as _lintCypherQuery,
4
+ _internalFeatureFlags,
5
+ } from '@neo4j-cypher/language-support';
2
6
  import workerpool from 'workerpool';
3
7
 
4
- workerpool.worker({ validateSemantics });
8
+ function lintCypherQuery(
9
+ query: string,
10
+ dbSchema: DbSchema,
11
+ featureFlags: { consoleCommands?: boolean; cypher25?: boolean } = {},
12
+ ) {
13
+ // We allow to override the consoleCommands feature flag
14
+ if (featureFlags.consoleCommands !== undefined) {
15
+ _internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
16
+ }
17
+ if (featureFlags.cypher25 !== undefined) {
18
+ _internalFeatureFlags.cypher25 = featureFlags.cypher25;
19
+ }
20
+ return _lintCypherQuery(query, dbSchema);
21
+ }
5
22
 
6
- type LinterArgs = Parameters<typeof validateSemantics>;
23
+ workerpool.worker({ lintCypherQuery });
7
24
 
8
- export type LinterTask = workerpool.Promise<
9
- ReturnType<typeof validateSemantics>
10
- >;
25
+ type LinterArgs = Parameters<typeof lintCypherQuery>;
26
+
27
+ export type LinterTask = workerpool.Promise<ReturnType<typeof lintCypherQuery>>;
11
28
 
12
29
  export type LintWorker = {
13
- validateSemantics: (...args: LinterArgs) => LinterTask;
30
+ lintCypherQuery: (...args: LinterArgs) => LinterTask;
14
31
  };
@@ -1,7 +1,6 @@
1
1
  import { Diagnostic, linter } from '@codemirror/lint';
2
2
  import { Extension } from '@codemirror/state';
3
- import { parserWrapper, validateSyntax } from '@neo4j-cypher/language-support';
4
- import { DiagnosticSeverity } from 'vscode-languageserver-types';
3
+ import { DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver-types';
5
4
  import workerpool from 'workerpool';
6
5
  import type { CypherConfig } from './langCypher';
7
6
  import type { LinterTask, LintWorker } from './lintWorker';
@@ -16,73 +15,44 @@ const pool = workerpool.pool(WorkerURL, {
16
15
  let lastSemanticJob: LinterTask | undefined;
17
16
 
18
17
  export const cypherLinter: (config: CypherConfig) => Extension = (config) =>
19
- linter((view) => {
20
- if (!config.lint) {
21
- return [];
22
- }
23
-
24
- const query = view.state.doc.toString();
25
- const syntaxErrors = validateSyntax(query, config.schema ?? {});
26
-
27
- return syntaxErrors.map(
28
- (diagnostic): Diagnostic => ({
29
- from: diagnostic.offsets.start,
30
- to: diagnostic.offsets.end,
31
- severity:
32
- diagnostic.severity === DiagnosticSeverity.Error
33
- ? 'error'
34
- : 'warning',
35
- message: diagnostic.message,
36
- }),
37
- );
38
- });
39
-
40
- export const semanticAnalysisLinter: (config: CypherConfig) => Extension = (
41
- config,
42
- ) =>
43
18
  linter(async (view) => {
44
19
  if (!config.lint) {
45
20
  return [];
46
21
  }
47
-
48
22
  const query = view.state.doc.toString();
49
23
  if (query.length === 0) {
50
24
  return [];
51
25
  }
52
26
 
53
- // we want to avoid the ANTLR4 reparse in the worker thread, this should hit our main thread cache
54
- const parse = parserWrapper.parse(query);
55
- const statements = parse.statementsParsing;
56
-
57
- const anySyntacticError = statements.some(
58
- (statement) => statement.syntaxErrors.length !== 0,
59
- );
60
-
61
- if (anySyntacticError) {
62
- return [];
63
- }
64
-
65
27
  try {
66
28
  if (lastSemanticJob !== undefined && !lastSemanticJob.resolved) {
67
29
  void lastSemanticJob.cancel();
68
30
  }
69
31
 
70
32
  const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
71
- lastSemanticJob = proxyWorker.validateSemantics(
33
+ lastSemanticJob = proxyWorker.lintCypherQuery(
72
34
  query,
73
35
  config.schema ?? {},
36
+ config.featureFlags ?? {},
74
37
  );
75
38
  const result = await lastSemanticJob;
76
39
 
77
- return result.map((diag) => {
40
+ const a: Diagnostic[] = result.map((diagnostic) => {
78
41
  return {
79
- from: diag.offsets.start,
80
- to: diag.offsets.end,
42
+ from: diagnostic.offsets.start,
43
+ to: diagnostic.offsets.end,
81
44
  severity:
82
- diag.severity === DiagnosticSeverity.Error ? 'error' : 'warning',
83
- message: diag.message,
45
+ diagnostic.severity === DiagnosticSeverity.Error
46
+ ? 'error'
47
+ : 'warning',
48
+ message: diagnostic.message,
49
+ ...(diagnostic.tags !== undefined &&
50
+ diagnostic.tags.includes(DiagnosticTag.Deprecated)
51
+ ? { markClass: 'cm-deprecated-element' }
52
+ : {}),
84
53
  };
85
54
  });
55
+ return a;
86
56
  } catch (err) {
87
57
  if (!(err instanceof workerpool.Promise.CancellationError)) {
88
58
  console.error(String(err) + ' ' + query);