@neo4j-cypher/react-codemirror 2.0.0-next.1 → 2.0.0-next.10

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 (156) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +4 -1
  3. package/dist/{types/CypherEditor.d.ts → CypherEditor.d.ts} +69 -8
  4. package/dist/CypherEditor.js +300 -0
  5. package/dist/CypherEditor.js.map +1 -0
  6. package/dist/CypherEditor.test.js +151 -0
  7. package/dist/CypherEditor.test.js.map +1 -0
  8. package/dist/constants.d.ts +1 -0
  9. package/dist/constants.js +2 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/e2e_tests/autoCompletion.spec.js +267 -0
  12. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
  13. package/dist/e2e_tests/configuration.spec.js +83 -0
  14. package/dist/e2e_tests/configuration.spec.js.map +1 -0
  15. package/dist/e2e_tests/debounce.spec.js +65 -0
  16. package/dist/e2e_tests/debounce.spec.js.map +1 -0
  17. package/dist/e2e_tests/e2eUtils.js +60 -0
  18. package/dist/e2e_tests/e2eUtils.js.map +1 -0
  19. package/dist/e2e_tests/extraKeybindings.spec.js +43 -0
  20. package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
  21. package/dist/e2e_tests/historyNavigation.spec.js +227 -0
  22. package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
  23. package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
  24. package/dist/e2e_tests/performanceTest.spec.js +96 -0
  25. package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
  26. package/dist/e2e_tests/sanityChecks.spec.js +53 -0
  27. package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
  28. package/dist/e2e_tests/signatureHelp.spec.js +179 -0
  29. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
  30. package/dist/e2e_tests/snippets.spec.js +62 -0
  31. package/dist/e2e_tests/snippets.spec.js.map +1 -0
  32. package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  33. package/dist/e2e_tests/syntaxHighlighting.spec.js +90 -0
  34. package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  35. package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  36. package/dist/e2e_tests/syntaxValidation.spec.js +79 -0
  37. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
  38. package/dist/historyNavigation.d.ts +7 -0
  39. package/dist/historyNavigation.js +163 -0
  40. package/dist/historyNavigation.js.map +1 -0
  41. package/dist/{types/icons.d.ts → icons.d.ts} +1 -1
  42. package/dist/icons.js +62 -0
  43. package/dist/icons.js.map +1 -0
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lang-cypher/autocomplete.d.ts +6 -0
  48. package/dist/lang-cypher/autocomplete.js +124 -0
  49. package/dist/lang-cypher/autocomplete.js.map +1 -0
  50. package/dist/{types/lang-cypher → lang-cypher}/constants.d.ts +9 -0
  51. package/dist/lang-cypher/constants.js +65 -0
  52. package/dist/lang-cypher/constants.js.map +1 -0
  53. package/dist/lang-cypher/contants.test.d.ts +1 -0
  54. package/dist/lang-cypher/contants.test.js +103 -0
  55. package/dist/lang-cypher/contants.test.js.map +1 -0
  56. package/dist/lang-cypher/createCypherTheme.js +179 -0
  57. package/dist/lang-cypher/createCypherTheme.js.map +1 -0
  58. package/dist/lang-cypher/langCypher.d.ts +14 -0
  59. package/dist/lang-cypher/langCypher.js +30 -0
  60. package/dist/lang-cypher/langCypher.js.map +1 -0
  61. package/dist/lang-cypher/lintWorker.d.ts +8 -0
  62. package/dist/lang-cypher/lintWorker.js +4 -0
  63. package/dist/lang-cypher/lintWorker.js.map +1 -0
  64. package/dist/lang-cypher/parser-adapter.d.ts +19 -0
  65. package/dist/lang-cypher/parser-adapter.js +113 -0
  66. package/dist/lang-cypher/parser-adapter.js.map +1 -0
  67. package/dist/lang-cypher/signatureHelp.d.ts +4 -0
  68. package/dist/lang-cypher/signatureHelp.js +94 -0
  69. package/dist/lang-cypher/signatureHelp.js.map +1 -0
  70. package/dist/lang-cypher/syntaxValidation.d.ts +5 -0
  71. package/dist/lang-cypher/syntaxValidation.js +69 -0
  72. package/dist/lang-cypher/syntaxValidation.js.map +1 -0
  73. package/dist/lang-cypher/themeIcons.js +22 -0
  74. package/dist/lang-cypher/themeIcons.js.map +1 -0
  75. package/dist/lang-cypher/utils.d.ts +2 -0
  76. package/dist/lang-cypher/utils.js +10 -0
  77. package/dist/lang-cypher/utils.js.map +1 -0
  78. package/dist/ndlTokensCopy.js +380 -0
  79. package/dist/ndlTokensCopy.js.map +1 -0
  80. package/dist/ndlTokensCopy.test.d.ts +1 -0
  81. package/dist/ndlTokensCopy.test.js +12 -0
  82. package/dist/ndlTokensCopy.test.js.map +1 -0
  83. package/dist/neo4jSetup.js +120 -0
  84. package/dist/neo4jSetup.js.map +1 -0
  85. package/dist/{types/themes.d.ts → themes.d.ts} +1 -1
  86. package/dist/themes.js +114 -0
  87. package/dist/themes.js.map +1 -0
  88. package/dist/tsconfig.tsbuildinfo +1 -0
  89. package/package.json +30 -31
  90. package/src/CypherEditor.test.tsx +200 -0
  91. package/src/CypherEditor.tsx +292 -41
  92. package/src/constants.ts +1 -0
  93. package/src/e2e_tests/autoCompletion.spec.tsx +472 -0
  94. package/src/e2e_tests/configuration.spec.tsx +111 -0
  95. package/src/e2e_tests/debounce.spec.tsx +101 -0
  96. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +11 -1
  97. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  98. package/src/e2e_tests/historyNavigation.spec.tsx +315 -0
  99. package/src/e2e_tests/performanceTest.spec.tsx +158 -0
  100. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
  101. package/src/e2e_tests/signatureHelp.spec.tsx +380 -0
  102. package/src/e2e_tests/snippets.spec.tsx +92 -0
  103. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +1 -3
  104. package/src/e2e_tests/{syntax-validation.spec.tsx → syntaxValidation.spec.tsx} +6 -7
  105. package/src/{repl-mode.ts → historyNavigation.ts} +7 -30
  106. package/src/icons.ts +3 -0
  107. package/src/index.ts +5 -2
  108. package/src/lang-cypher/autocomplete.ts +107 -15
  109. package/src/lang-cypher/constants.ts +23 -0
  110. package/src/lang-cypher/contants.test.ts +6 -2
  111. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +41 -2
  112. package/src/lang-cypher/langCypher.ts +53 -0
  113. package/src/lang-cypher/lintWorker.ts +14 -0
  114. package/src/lang-cypher/parser-adapter.ts +145 -0
  115. package/src/lang-cypher/signatureHelp.ts +133 -0
  116. package/src/lang-cypher/syntaxValidation.ts +96 -0
  117. package/src/lang-cypher/utils.ts +9 -0
  118. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  119. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +51 -1
  120. package/src/themes.ts +4 -2
  121. package/src/viteEnv.d.ts +1 -0
  122. package/dist/cjs/index.cjs +0 -1443
  123. package/dist/cjs/index.cjs.map +0 -7
  124. package/dist/esm/index.mjs +0 -1466
  125. package/dist/esm/index.mjs.map +0 -7
  126. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  127. package/dist/types/index.d.ts +0 -4
  128. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  129. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  130. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  131. package/dist/types/lang-cypher/syntax-validation.d.ts +0 -3
  132. package/dist/types/repl-mode.d.ts +0 -8
  133. package/dist/types/tsconfig.tsbuildinfo +0 -1
  134. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  135. package/src/e2e_tests/history-navigation.spec.tsx +0 -144
  136. package/src/e2e_tests/mock-data.ts +0 -4310
  137. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  138. package/src/lang-cypher/ParserAdapter.ts +0 -92
  139. package/src/lang-cypher/lang-cypher.ts +0 -32
  140. package/src/lang-cypher/syntax-validation.ts +0 -24
  141. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → CypherEditor.test.d.ts} +0 -0
  142. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → e2e_tests/autoCompletion.spec.d.ts} +0 -0
  143. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → e2e_tests/configuration.spec.d.ts} +0 -0
  144. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → e2e_tests/debounce.spec.d.ts} +0 -0
  145. /package/dist/{types/e2e_tests/e2e-utils.d.ts → e2e_tests/e2eUtils.d.ts} +0 -0
  146. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  147. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → e2e_tests/historyNavigation.spec.d.ts} +0 -0
  148. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → e2e_tests/sanityChecks.spec.d.ts} +0 -0
  149. /package/dist/{types/lang-cypher/contants.test.d.ts → e2e_tests/signatureHelp.spec.d.ts} +0 -0
  150. /package/dist/{types/ndl-tokens-copy.test.d.ts → e2e_tests/snippets.spec.d.ts} +0 -0
  151. /package/dist/{types/lang-cypher/create-cypher-theme.d.ts → lang-cypher/createCypherTheme.d.ts} +0 -0
  152. /package/dist/{types/lang-cypher/theme-icons.d.ts → lang-cypher/themeIcons.d.ts} +0 -0
  153. /package/dist/{types/ndl-tokens-copy.d.ts → ndlTokensCopy.d.ts} +0 -0
  154. /package/dist/{types/neo4j-setup.d.ts → neo4jSetup.d.ts} +0 -0
  155. /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
  156. /package/src/{ndl-tokens-copy.ts → ndlTokensCopy.ts} +0 -0
@@ -1,8 +1,16 @@
1
- import { CompletionSource } from '@codemirror/autocomplete';
1
+ import {
2
+ Completion,
3
+ CompletionSource,
4
+ snippet,
5
+ } from '@codemirror/autocomplete';
2
6
  import { autocomplete } from '@neo4j-cypher/language-support';
3
- import { CompletionItemKind } from 'vscode-languageserver-types';
7
+ import {
8
+ CompletionItemKind,
9
+ CompletionItemTag,
10
+ } from 'vscode-languageserver-types';
4
11
  import { CompletionItemIcons } from '../icons';
5
- import type { CypherConfig } from './lang-cypher';
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> = {
@@ -28,7 +36,8 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
28
36
  [CompletionItemKind.EnumMember]: 'EnumMember',
29
37
  [CompletionItemKind.Constant]: 'Constant',
30
38
  [CompletionItemKind.Struct]: 'Struct',
31
- [CompletionItemKind.Event]: 'Event',
39
+ // we're miss-using the enum here as there is no `Console` kind in the predefined list
40
+ [CompletionItemKind.Event]: 'Console',
32
41
  [CompletionItemKind.Operator]: 'Operator',
33
42
  [CompletionItemKind.TypeParameter]: 'TypeParameter',
34
43
  };
@@ -36,12 +45,22 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
36
45
  return map[c];
37
46
  };
38
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
+
39
58
  export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
40
59
  (config) => (context) => {
41
- const textUntilCursor = context.state.doc.toString().slice(0, context.pos);
60
+ const documentText = context.state.doc.toString();
42
61
 
43
- const triggerCharacters = ['.', ':', '{', '$'];
44
- const lastCharacter = textUntilCursor.slice(-1);
62
+ const triggerCharacters = ['.', ':', '{', '$', ')'];
63
+ const lastCharacter = documentText.at(context.pos - 1);
45
64
 
46
65
  const lastWord = context.matchBefore(/\w*/);
47
66
  const inWord = lastWord.from !== lastWord.to;
@@ -49,17 +68,90 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
49
68
  const shouldTriggerCompletion =
50
69
  inWord || context.explicit || triggerCharacters.includes(lastCharacter);
51
70
 
71
+ if (config.useLightVersion && !context.explicit) {
72
+ return null;
73
+ }
74
+
52
75
  if (!shouldTriggerCompletion) {
53
76
  return null;
54
77
  }
55
78
 
56
- const options = autocomplete(textUntilCursor, config.schema ?? {});
79
+ const options = autocomplete(
80
+ documentText,
81
+ config.schema ?? {},
82
+ context.pos,
83
+ context.explicit,
84
+ );
57
85
 
58
- return {
59
- from: context.matchBefore(/(\w|\$)*$/).from,
60
- options: options.map((o) => ({
61
- label: o.label,
62
- type: completionKindToCodemirrorIcon(o.kind),
63
- })),
64
- };
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
+ }
65
157
  };
@@ -31,8 +31,30 @@ export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
31
31
  none: NodeType.define({ id: 19, name: 'none' }),
32
32
  separator: NodeType.define({ id: 20, name: 'separator' }),
33
33
  punctuation: NodeType.define({ id: 21, name: 'punctuation' }),
34
+ consoleCommand: NodeType.define({ id: 22, name: 'consoleCommand' }),
35
+ // also include prism token types
36
+ 'class-name': NodeType.define({ id: 23, name: 'label' }),
37
+ // this is escaped variables
38
+ identifier: NodeType.define({ id: 24, name: 'variable' }),
39
+ string: NodeType.define({ id: 25, name: 'stringLiteral' }),
40
+ relationship: NodeType.define({ id: 26, name: 'label' }),
41
+ boolean: NodeType.define({ id: 27, name: 'booleanLiteral' }),
42
+ number: NodeType.define({ id: 28, name: 'numberLiteral' }),
34
43
  });
35
44
 
45
+ export type PrismSpecificTokenType =
46
+ | 'class-name'
47
+ | 'identifier'
48
+ | 'string'
49
+ | 'relationship'
50
+ | 'boolean'
51
+ | 'number';
52
+
53
+ export type CodemirrorParseTokenType =
54
+ | CypherTokenType
55
+ | PrismSpecificTokenType
56
+ | 'topNode';
57
+
36
58
  export type HighlightedCypherTokenTypes = Exclude<CypherTokenType, 'none'>;
37
59
  export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
38
60
  comment: tags.comment,
@@ -55,6 +77,7 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
55
77
  bracket: tags.bracket,
56
78
  punctuation: tags.punctuation,
57
79
  separator: tags.separator,
80
+ consoleCommand: tags.macroName,
58
81
  };
59
82
 
60
83
  export const parserAdapterNodeSet = (nodes: Record<string, NodeType>) =>
@@ -1,5 +1,9 @@
1
1
  import { tags } from '@lezer/highlight';
2
- import { applySyntaxColouring } from '@neo4j-cypher/language-support';
2
+ import {
3
+ applySyntaxColouring,
4
+ CypherTokenType,
5
+ } from '@neo4j-cypher/language-support';
6
+ import { expect, test } from 'vitest';
3
7
  import { tokenTypeToStyleTag } from './constants';
4
8
 
5
9
  const cypherQueryWithAllTokenTypes = `MATCH (variable :Label)-[:REL_TYPE]->()
@@ -54,7 +58,7 @@ test('correctly parses all cypher token types to style tags', () => {
54
58
  ]);
55
59
 
56
60
  const styleTags = tokenTypes.map((tokenType) => {
57
- if (tokenType === 'none') return undefined;
61
+ if (tokenType === CypherTokenType.none) return undefined;
58
62
  return tokenTypeToStyleTag[tokenType];
59
63
  });
60
64
  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 {
@@ -15,7 +16,7 @@ import {
15
16
  replaceAllSvg,
16
17
  replaceSvg,
17
18
  upArrowSvg,
18
- } from './theme-icons';
19
+ } from './themeIcons';
19
20
 
20
21
  export interface ThemeOptions {
21
22
  dark: boolean;
@@ -53,6 +54,9 @@ export const createCypherTheme = ({
53
54
  color: settings.foreground,
54
55
  fontVariantLigatures: 'none',
55
56
  },
57
+ '& .cm-snippetField': {
58
+ backgroundColor: settings.autoCompletionPanel.selectedColor,
59
+ },
56
60
  '&.cm-focused': {
57
61
  outline: 'none',
58
62
  },
@@ -61,8 +65,9 @@ export const createCypherTheme = ({
61
65
  color: settings.gutterForeground,
62
66
  border: 'none',
63
67
  },
64
- '&.cm-editor .cm-scroller': {
68
+ '&.cm-editor': {
65
69
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
70
+ height: '100%',
66
71
  },
67
72
  '.cm-content': {
68
73
  caretColor: settings.cursor,
@@ -80,6 +85,9 @@ export const createCypherTheme = ({
80
85
  '& .cm-selectionMatch': {
81
86
  backgroundColor: settings.textMatchingSelection,
82
87
  },
88
+ '& .cm-bold': {
89
+ fontWeight: 'bold',
90
+ },
83
91
  '& .cm-panels': {
84
92
  backgroundColor: settings.searchPanel.background,
85
93
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
@@ -93,7 +101,37 @@ export const createCypherTheme = ({
93
101
  color: settings.autoCompletionPanel.matchingTextColor,
94
102
  textDecoration: 'none',
95
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
+ },
96
133
  '.cm-tooltip-autocomplete': {
134
+ maxWidth: '430px',
97
135
  '& > ul > li[aria-selected]': {
98
136
  backgroundColor: settings.autoCompletionPanel.selectedColor,
99
137
  color: settings.foreground,
@@ -198,6 +236,7 @@ export const createCypherTheme = ({
198
236
  ([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
199
237
  tag: tokenTypeToStyleTag[token],
200
238
  color,
239
+ class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,
201
240
  }),
202
241
  );
203
242
  const highlightStyle = HighlightStyle.define(styles);
@@ -0,0 +1,53 @@
1
+ import { autocompletion } from '@codemirror/autocomplete';
2
+ import {
3
+ defineLanguageFacet,
4
+ Language,
5
+ LanguageSupport,
6
+ } from '@codemirror/language';
7
+ import {
8
+ _internalFeatureFlags,
9
+ type DbSchema,
10
+ } from '@neo4j-cypher/language-support';
11
+ import { completionStyles, cypherAutocomplete } from './autocomplete';
12
+ import { ParserAdapter } from './parser-adapter';
13
+ import { signatureHelpTooltip } from './signatureHelp';
14
+ import { cypherLinter, semanticAnalysisLinter } from './syntaxValidation';
15
+
16
+ const facet = defineLanguageFacet({
17
+ commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
18
+ closeBrackets: { brackets: ['(', '[', '{', "'", '"', '`'] },
19
+ });
20
+
21
+ export type CypherConfig = {
22
+ lint?: boolean;
23
+ showSignatureTooltipBelow?: boolean;
24
+ featureFlags?: {
25
+ signatureInfoOnAutoCompletions?: boolean;
26
+ consoleCommands?: boolean;
27
+ };
28
+ schema?: DbSchema;
29
+ useLightVersion: boolean;
30
+ setUseLightVersion?: (useLightVersion: boolean) => void;
31
+ };
32
+
33
+ export function cypher(config: CypherConfig) {
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
+
40
+ const parserAdapter = new ParserAdapter(facet, config);
41
+
42
+ const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
43
+
44
+ return new LanguageSupport(cypherLanguage, [
45
+ autocompletion({
46
+ override: [cypherAutocomplete(config)],
47
+ optionClass: completionStyles,
48
+ }),
49
+ cypherLinter(config),
50
+ semanticAnalysisLinter(config),
51
+ signatureHelpTooltip(config),
52
+ ]);
53
+ }
@@ -0,0 +1,14 @@
1
+ import { validateSemantics } from '@neo4j-cypher/language-support';
2
+ import workerpool from 'workerpool';
3
+
4
+ workerpool.worker({ validateSemantics });
5
+
6
+ type LinterArgs = Parameters<typeof validateSemantics>;
7
+
8
+ export type LinterTask = workerpool.Promise<
9
+ ReturnType<typeof validateSemantics>
10
+ >;
11
+
12
+ export type LintWorker = {
13
+ validateSemantics: (...args: LinterArgs) => LinterTask;
14
+ };
@@ -0,0 +1,145 @@
1
+ import { Facet } from '@codemirror/state';
2
+ import { Input, NodeType, Parser, PartialParse, Tree } from '@lezer/common';
3
+ import {
4
+ applySyntaxColouring,
5
+ ParsedCypherToken,
6
+ } from '@neo4j-cypher/language-support';
7
+
8
+ import Prism from 'prismjs';
9
+ import {
10
+ CodemirrorParseTokenType,
11
+ cypherTokenTypeToNode,
12
+ parserAdapterNodeSet,
13
+ } from './constants';
14
+ // This import will load the cypher support in prisma
15
+ import 'prismjs/components/prism-cypher';
16
+ import { CypherConfig } from './langCypher';
17
+
18
+ const DEFAULT_NODE_GROUP_SIZE = 4;
19
+ Prism.manual = true;
20
+
21
+ export class ParserAdapter extends Parser {
22
+ cypherTokenTypeToNode: Record<CodemirrorParseTokenType, NodeType>;
23
+
24
+ constructor(facet: Facet<unknown>, private config: CypherConfig) {
25
+ super();
26
+ this.cypherTokenTypeToNode = cypherTokenTypeToNode(facet);
27
+ }
28
+
29
+ createParse(input: Input): PartialParse {
30
+ return this.startParse(input);
31
+ }
32
+
33
+ /* There are more arguments, but since we don't do any incremental parsing, they are not useful */
34
+ startParse(input: string | Input): PartialParse {
35
+ const document =
36
+ typeof input === 'string' ? input : input.read(0, input.length);
37
+
38
+ const tree = this.buildTree(document);
39
+
40
+ return {
41
+ stoppedAt: input.length,
42
+ parsedPos: input.length,
43
+ stopAt: () => {
44
+ return undefined;
45
+ },
46
+ advance: () => tree,
47
+ };
48
+ }
49
+
50
+ private buildTree(document: string) {
51
+ const parse = this.config.useLightVersion
52
+ ? this.prismParse(document)
53
+ : this.antlrParse(document);
54
+
55
+ if (parse.tokens.length === 0) {
56
+ return this.createEmptyTree(document);
57
+ }
58
+
59
+ const buffer =
60
+ parse.type === 'prism'
61
+ ? this.createBufferForPrismTokens(parse.tokens)
62
+ : this.createBufferForAntlrTokens(parse.tokens);
63
+
64
+ this.addTopNodeToBuffer(buffer, document);
65
+
66
+ return Tree.build({
67
+ buffer: buffer.flat(),
68
+ nodeSet: parserAdapterNodeSet(this.cypherTokenTypeToNode),
69
+ topID: this.cypherTokenTypeToNode.topNode.id,
70
+ });
71
+ }
72
+
73
+ private antlrParse(document: string) {
74
+ const startTime = performance.now();
75
+ const tokens = applySyntaxColouring(document);
76
+ const timeTaken = performance.now() - startTime;
77
+ if (timeTaken > 300) {
78
+ this.config.setUseLightVersion?.(true);
79
+ }
80
+ return { type: 'antlr' as const, tokens };
81
+ }
82
+
83
+ private prismParse(document: string) {
84
+ if (document.length === 0) {
85
+ this.config.setUseLightVersion?.(false);
86
+ }
87
+ const tokens = Prism.tokenize(document, Prism.languages.cypher);
88
+ return {
89
+ type: 'prism' as const,
90
+ tokens,
91
+ };
92
+ }
93
+ private createBufferForAntlrTokens(tokens: ParsedCypherToken[]) {
94
+ return tokens.map((token) => {
95
+ const nodeTypeId = this.cypherTokenTypeToNode[token.tokenType].id;
96
+ const startOffset = token.position.startOffset;
97
+ const endOffset = token.position.startOffset + token.length;
98
+
99
+ return [nodeTypeId, startOffset, endOffset, DEFAULT_NODE_GROUP_SIZE];
100
+ });
101
+ }
102
+
103
+ private createBufferForPrismTokens(tokens: (string | Prism.Token)[]) {
104
+ let totalOffset = 0;
105
+ return tokens.map((token) => {
106
+ const tokenType = (
107
+ typeof token === 'string' ? 'variable' : token.type
108
+ ) as CodemirrorParseTokenType;
109
+
110
+ const nodeTypeId = this.cypherTokenTypeToNode[tokenType].id;
111
+ const startOffset = totalOffset;
112
+ const endOffset = startOffset + token.length;
113
+ totalOffset = endOffset;
114
+
115
+ return [nodeTypeId, startOffset, endOffset, DEFAULT_NODE_GROUP_SIZE];
116
+ });
117
+ }
118
+
119
+ private createEmptyTree(document: string) {
120
+ return Tree.build({
121
+ buffer: [
122
+ this.cypherTokenTypeToNode.topNode.id,
123
+ 0,
124
+ document.length,
125
+ DEFAULT_NODE_GROUP_SIZE,
126
+ ],
127
+ nodeSet: parserAdapterNodeSet(this.cypherTokenTypeToNode),
128
+ topID: this.cypherTokenTypeToNode.topNode.id,
129
+ });
130
+ }
131
+
132
+ private addTopNodeToBuffer(buffer: number[][], document: string) {
133
+ const id = this.cypherTokenTypeToNode.topNode.id;
134
+ const startOffset = 0;
135
+ const endOffset = document.length;
136
+ const totalBufferLength = buffer.length * DEFAULT_NODE_GROUP_SIZE;
137
+
138
+ buffer.push([
139
+ id,
140
+ startOffset,
141
+ endOffset,
142
+ totalBufferLength + DEFAULT_NODE_GROUP_SIZE,
143
+ ]);
144
+ }
145
+ }
@@ -0,0 +1,133 @@
1
+ import { EditorState, StateField } from '@codemirror/state';
2
+ import { showTooltip, Tooltip } from '@codemirror/view';
3
+ import { signatureHelp } from '@neo4j-cypher/language-support';
4
+ import { SignatureInformation } from 'vscode-languageserver-types';
5
+ import { CypherConfig } from './langCypher';
6
+ import { getDocString } from './utils';
7
+
8
+ function getTriggerCharacter(query: string, caretPosition: number) {
9
+ let i = caretPosition - 1;
10
+ let triggerCharacter = query.at(i);
11
+
12
+ // Discard all space characters. Note that a space can be more than just a ' '
13
+ while (/\s/.test(triggerCharacter) && i > 0) {
14
+ i -= 1;
15
+ triggerCharacter = query.at(i);
16
+ }
17
+
18
+ return triggerCharacter;
19
+ }
20
+
21
+ const createSignatureHelpElement =
22
+ ({
23
+ signature,
24
+ activeParameter,
25
+ }: {
26
+ signature: SignatureInformation;
27
+ activeParameter: number;
28
+ }) =>
29
+ () => {
30
+ const parameters = signature.parameters;
31
+ const doc = getDocString(signature.documentation);
32
+ const dom = document.createElement('div');
33
+ dom.className = 'cm-signature-help-panel';
34
+
35
+ const contents = document.createElement('div');
36
+ contents.className = 'cm-signature-help-panel-contents';
37
+ dom.appendChild(contents);
38
+
39
+ const signatureLabel = document.createElement('div');
40
+ signatureLabel.className = 'cm-signature-help-panel-name';
41
+ signatureLabel.appendChild(document.createTextNode(`${signature.label}(`));
42
+
43
+ parameters.forEach((param, index) => {
44
+ if (typeof param.documentation === 'string') {
45
+ const span = document.createElement('span');
46
+ span.appendChild(document.createTextNode(param.documentation));
47
+ if (index !== parameters.length - 1) {
48
+ span.appendChild(document.createTextNode(', '));
49
+ }
50
+
51
+ if (index === activeParameter) {
52
+ span.className = 'cm-signature-help-panel-current-argument';
53
+ }
54
+ signatureLabel.appendChild(span);
55
+ }
56
+ });
57
+
58
+ signatureLabel.appendChild(document.createTextNode(')'));
59
+
60
+ contents.appendChild(signatureLabel);
61
+
62
+ const separator = document.createElement('div');
63
+ separator.className = 'cm-signature-help-panel-separator';
64
+
65
+ contents.appendChild(separator);
66
+
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);
72
+
73
+ return { dom };
74
+ };
75
+
76
+ function getSignatureHelpTooltip(
77
+ state: EditorState,
78
+ config: CypherConfig,
79
+ ): Tooltip[] {
80
+ let result: Tooltip[] = [];
81
+ const schema = config.schema;
82
+ const ranges = state.selection.ranges;
83
+ const range = ranges.at(0);
84
+
85
+ if (schema && ranges.length === 1 && range.from === range.to) {
86
+ const caretPosition = range.from;
87
+ const query = state.doc.toString();
88
+
89
+ const triggerCharacter = getTriggerCharacter(query, caretPosition);
90
+
91
+ if (triggerCharacter === '(' || triggerCharacter === ',') {
92
+ const signatureHelpInfo = signatureHelp(query, schema, caretPosition);
93
+ const activeSignature = signatureHelpInfo.activeSignature;
94
+ const signatures = signatureHelpInfo.signatures;
95
+ const activeParameter = signatureHelpInfo.activeParameter;
96
+
97
+ if (
98
+ activeSignature !== undefined &&
99
+ activeSignature >= 0 &&
100
+ activeSignature < signatures.length &&
101
+ signatures[activeSignature].documentation !== undefined
102
+ ) {
103
+ const signature = signatures[activeSignature];
104
+ const showSignatureTooltipBelow =
105
+ config.showSignatureTooltipBelow ?? true;
106
+
107
+ result = [
108
+ {
109
+ pos: caretPosition,
110
+ above: !showSignatureTooltipBelow,
111
+ arrow: true,
112
+ create: createSignatureHelpElement({ signature, activeParameter }),
113
+ },
114
+ ];
115
+ }
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ export function signatureHelpTooltip(config: CypherConfig) {
123
+ return StateField.define<readonly Tooltip[]>({
124
+ create: (state) => getSignatureHelpTooltip(state, config),
125
+
126
+ update(tooltips, tr) {
127
+ if (!tr.docChanged && !tr.selection) return tooltips;
128
+ return getSignatureHelpTooltip(tr.state, config);
129
+ },
130
+
131
+ provide: (f) => showTooltip.computeN([f], (state) => state.field(f)),
132
+ });
133
+ }