@neo4j-cypher/react-codemirror 2.0.0-next.4 → 2.0.0-next.41

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 (186) hide show
  1. package/CHANGELOG.md +370 -0
  2. package/README.md +3 -2
  3. package/dist/src/CypherEditor.d.ts +245 -0
  4. package/dist/src/CypherEditor.js +513 -0
  5. package/dist/src/CypherEditor.js.map +1 -0
  6. package/dist/src/CypherEditor.test.js +162 -0
  7. package/dist/src/CypherEditor.test.js.map +1 -0
  8. package/dist/src/constants.d.ts +1 -0
  9. package/dist/src/constants.js +2 -0
  10. package/dist/src/constants.js.map +1 -0
  11. package/dist/src/diffView.d.ts +13 -0
  12. package/dist/src/diffView.js +11 -0
  13. package/dist/src/diffView.js.map +1 -0
  14. package/dist/src/e2e_tests/autoCompletion.spec.js +334 -0
  15. package/dist/src/e2e_tests/autoCompletion.spec.js.map +1 -0
  16. package/dist/src/e2e_tests/configuration.spec.js +83 -0
  17. package/dist/src/e2e_tests/configuration.spec.js.map +1 -0
  18. package/dist/src/e2e_tests/debounce.spec.js +66 -0
  19. package/dist/src/e2e_tests/debounce.spec.js.map +1 -0
  20. package/dist/{types/e2e_tests/e2e-utils.d.ts → src/e2e_tests/e2eUtils.d.ts} +2 -0
  21. package/dist/src/e2e_tests/e2eUtils.js +79 -0
  22. package/dist/src/e2e_tests/e2eUtils.js.map +1 -0
  23. package/dist/src/e2e_tests/extraKeybindings.spec.js +43 -0
  24. package/dist/src/e2e_tests/extraKeybindings.spec.js.map +1 -0
  25. package/dist/src/e2e_tests/historyNavigation.spec.js +227 -0
  26. package/dist/src/e2e_tests/historyNavigation.spec.js.map +1 -0
  27. package/dist/src/e2e_tests/performanceTest.spec.d.ts +6 -0
  28. package/dist/src/e2e_tests/performanceTest.spec.js +97 -0
  29. package/dist/src/e2e_tests/performanceTest.spec.js.map +1 -0
  30. package/dist/src/e2e_tests/sanityChecks.spec.js +64 -0
  31. package/dist/src/e2e_tests/sanityChecks.spec.js.map +1 -0
  32. package/dist/src/e2e_tests/signatureHelp.spec.js +226 -0
  33. package/dist/src/e2e_tests/signatureHelp.spec.js.map +1 -0
  34. package/dist/src/e2e_tests/snippets.spec.js +14 -0
  35. package/dist/src/e2e_tests/snippets.spec.js.map +1 -0
  36. package/dist/src/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  37. package/dist/src/e2e_tests/syntaxHighlighting.spec.js +90 -0
  38. package/dist/src/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  39. package/dist/src/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  40. package/dist/src/e2e_tests/syntaxValidation.spec.js +129 -0
  41. package/dist/src/e2e_tests/syntaxValidation.spec.js.map +1 -0
  42. package/dist/src/editorActions.d.ts +17 -0
  43. package/dist/src/editorActions.js +259 -0
  44. package/dist/src/editorActions.js.map +1 -0
  45. package/dist/{types/history-navigation.d.ts → src/historyNavigation.d.ts} +1 -1
  46. package/dist/src/historyNavigation.js +163 -0
  47. package/dist/src/historyNavigation.js.map +1 -0
  48. package/dist/src/hostCallbacks.d.ts +9 -0
  49. package/dist/src/hostCallbacks.js +2 -0
  50. package/dist/src/hostCallbacks.js.map +1 -0
  51. package/dist/{types → src}/icons.d.ts +1 -1
  52. package/dist/src/icons.js +62 -0
  53. package/dist/src/icons.js.map +1 -0
  54. package/dist/src/index.d.ts +6 -0
  55. package/dist/src/index.js +5 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/inlinePanel.d.ts +27 -0
  58. package/dist/src/inlinePanel.js +86 -0
  59. package/dist/src/inlinePanel.js.map +1 -0
  60. package/dist/src/lang-cypher/autocomplete.d.ts +6 -0
  61. package/dist/src/lang-cypher/autocomplete.js +116 -0
  62. package/dist/src/lang-cypher/autocomplete.js.map +1 -0
  63. package/dist/{types → src}/lang-cypher/constants.d.ts +15 -2
  64. package/dist/src/lang-cypher/constants.js +74 -0
  65. package/dist/src/lang-cypher/constants.js.map +1 -0
  66. package/dist/src/lang-cypher/contants.test.d.ts +1 -0
  67. package/dist/src/lang-cypher/contants.test.js +103 -0
  68. package/dist/src/lang-cypher/contants.test.js.map +1 -0
  69. package/dist/{types/lang-cypher/create-cypher-theme.d.ts → src/lang-cypher/createCypherTheme.d.ts} +14 -3
  70. package/dist/src/lang-cypher/createCypherTheme.js +202 -0
  71. package/dist/src/lang-cypher/createCypherTheme.js.map +1 -0
  72. package/dist/src/lang-cypher/langCypher.d.ts +15 -0
  73. package/dist/src/lang-cypher/langCypher.js +23 -0
  74. package/dist/src/lang-cypher/langCypher.js.map +1 -0
  75. package/dist/src/lang-cypher/lintWorker.mjs +2039 -0
  76. package/dist/src/lang-cypher/parser-adapter.d.ts +21 -0
  77. package/dist/src/lang-cypher/parser-adapter.js +115 -0
  78. package/dist/src/lang-cypher/parser-adapter.js.map +1 -0
  79. package/dist/src/lang-cypher/prism-global.d.ts +1 -0
  80. package/dist/src/lang-cypher/prism-global.js +7 -0
  81. package/dist/src/lang-cypher/prism-global.js.map +1 -0
  82. package/dist/src/lang-cypher/signatureHelp.d.ts +4 -0
  83. package/dist/src/lang-cypher/signatureHelp.js +108 -0
  84. package/dist/src/lang-cypher/signatureHelp.js.map +1 -0
  85. package/dist/src/lang-cypher/syntaxValidation.d.ts +4 -0
  86. package/dist/src/lang-cypher/syntaxValidation.js +65 -0
  87. package/dist/src/lang-cypher/syntaxValidation.js.map +1 -0
  88. package/dist/src/lang-cypher/themeIcons.js +22 -0
  89. package/dist/src/lang-cypher/themeIcons.js.map +1 -0
  90. package/dist/src/lang-cypher/utils.d.ts +2 -0
  91. package/dist/src/lang-cypher/utils.js +10 -0
  92. package/dist/src/lang-cypher/utils.js.map +1 -0
  93. package/dist/src/ndlTokensCopy.d.ts +570 -0
  94. package/dist/src/ndlTokensCopy.js +571 -0
  95. package/dist/src/ndlTokensCopy.js.map +1 -0
  96. package/dist/src/ndlTokensCopy.test.d.ts +1 -0
  97. package/dist/src/ndlTokensCopy.test.js +12 -0
  98. package/dist/src/ndlTokensCopy.test.js.map +1 -0
  99. package/dist/src/neo4jSetup.d.ts +6 -0
  100. package/dist/src/neo4jSetup.js +120 -0
  101. package/dist/src/neo4jSetup.js.map +1 -0
  102. package/dist/src/richClipboardCopier.d.ts +4 -0
  103. package/dist/src/richClipboardCopier.js +78 -0
  104. package/dist/src/richClipboardCopier.js.map +1 -0
  105. package/dist/src/richClipboardCopier.test.d.ts +1 -0
  106. package/dist/src/richClipboardCopier.test.js +53 -0
  107. package/dist/src/richClipboardCopier.test.js.map +1 -0
  108. package/dist/src/themes.d.ts +5 -0
  109. package/dist/src/themes.js +107 -0
  110. package/dist/src/themes.js.map +1 -0
  111. package/dist/tsconfig.tsbuildinfo +1 -0
  112. package/package.json +64 -62
  113. package/src/CypherEditor.test.tsx +211 -0
  114. package/src/CypherEditor.tsx +586 -59
  115. package/src/constants.ts +1 -0
  116. package/src/diffView.ts +24 -0
  117. package/src/e2e_tests/autoCompletion.spec.tsx +576 -0
  118. package/src/e2e_tests/configuration.spec.tsx +111 -0
  119. package/src/e2e_tests/debounce.spec.tsx +102 -0
  120. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +41 -3
  121. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  122. package/src/e2e_tests/{history-navigation.spec.tsx → historyNavigation.spec.tsx} +138 -19
  123. package/src/e2e_tests/performanceTest.spec.tsx +163 -0
  124. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +28 -20
  125. package/src/e2e_tests/signatureHelp.spec.tsx +442 -0
  126. package/src/e2e_tests/snippets.spec.tsx +19 -0
  127. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +26 -24
  128. package/src/e2e_tests/syntaxValidation.spec.tsx +252 -0
  129. package/src/editorActions.ts +334 -0
  130. package/src/{history-navigation.ts → historyNavigation.ts} +6 -4
  131. package/src/hostCallbacks.ts +9 -0
  132. package/src/icons.ts +3 -0
  133. package/src/index.ts +4 -2
  134. package/src/inlinePanel.ts +140 -0
  135. package/src/lang-cypher/autocomplete.ts +96 -19
  136. package/src/lang-cypher/constants.ts +35 -2
  137. package/src/lang-cypher/contants.test.ts +7 -3
  138. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +97 -33
  139. package/src/lang-cypher/langCypher.ts +44 -0
  140. package/src/lang-cypher/lintWorker.mjs +2039 -0
  141. package/src/lang-cypher/parser-adapter.ts +146 -0
  142. package/src/lang-cypher/prism-global.ts +7 -0
  143. package/src/lang-cypher/signatureHelp.ts +154 -0
  144. package/src/lang-cypher/syntaxValidation.ts +81 -0
  145. package/src/lang-cypher/utils.ts +9 -0
  146. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  147. package/src/ndlTokensCopy.ts +570 -0
  148. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +79 -18
  149. package/src/richClipboardCopier.test.ts +67 -0
  150. package/src/richClipboardCopier.ts +99 -0
  151. package/src/themes.ts +63 -71
  152. package/src/viteEnv.d.ts +1 -0
  153. package/dist/cjs/index.cjs +0 -1441
  154. package/dist/cjs/index.cjs.map +0 -7
  155. package/dist/esm/index.mjs +0 -1464
  156. package/dist/esm/index.mjs.map +0 -7
  157. package/dist/types/CypherEditor.d.ts +0 -113
  158. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  159. package/dist/types/index.d.ts +0 -4
  160. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  161. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  162. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  163. package/dist/types/lang-cypher/syntax-validation.d.ts +0 -3
  164. package/dist/types/ndl-tokens-copy.d.ts +0 -379
  165. package/dist/types/neo4j-setup.d.ts +0 -2
  166. package/dist/types/themes.d.ts +0 -11
  167. package/dist/types/tsconfig.tsbuildinfo +0 -1
  168. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  169. package/src/e2e_tests/mock-data.ts +0 -4310
  170. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  171. package/src/e2e_tests/syntax-validation.spec.tsx +0 -156
  172. package/src/lang-cypher/ParserAdapter.ts +0 -92
  173. package/src/lang-cypher/lang-cypher.ts +0 -32
  174. package/src/lang-cypher/syntax-validation.ts +0 -24
  175. package/src/ndl-tokens-copy.ts +0 -379
  176. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → src/CypherEditor.test.d.ts} +0 -0
  177. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → src/e2e_tests/autoCompletion.spec.d.ts} +0 -0
  178. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → src/e2e_tests/configuration.spec.d.ts} +0 -0
  179. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → src/e2e_tests/debounce.spec.d.ts} +0 -0
  180. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → src/e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  181. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → src/e2e_tests/historyNavigation.spec.d.ts} +0 -0
  182. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → src/e2e_tests/sanityChecks.spec.d.ts} +0 -0
  183. /package/dist/{types/lang-cypher/contants.test.d.ts → src/e2e_tests/signatureHelp.spec.d.ts} +0 -0
  184. /package/dist/{types/ndl-tokens-copy.test.d.ts → src/e2e_tests/snippets.spec.d.ts} +0 -0
  185. /package/dist/{types/lang-cypher/theme-icons.d.ts → src/lang-cypher/themeIcons.d.ts} +0 -0
  186. /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
@@ -1,25 +1,54 @@
1
+ import { insertNewline } from '@codemirror/commands';
2
+ import type { Extension } from '@codemirror/state';
3
+ import { Annotation, Compartment, EditorState } from '@codemirror/state';
4
+ import type { KeyBinding, ViewUpdate } from '@codemirror/view';
5
+ import { EditorView, keymap, lineNumbers, placeholder } from '@codemirror/view';
1
6
  import {
2
- Annotation,
3
- Compartment,
4
- EditorState,
5
- Extension,
6
- } from '@codemirror/state';
7
+ createInlinePanelController,
8
+ type InlinePanelCallbacks,
9
+ type InlinePanelController,
10
+ } from './inlinePanel';
11
+ import { createDiffExtension, type DiffProps } from './diffView';
7
12
  import {
8
- EditorView,
9
- KeyBinding,
10
- keymap,
11
- lineNumbers,
12
- ViewUpdate,
13
- } from '@codemirror/view';
14
- import type { DbSchema } from '@neo4j-cypher/language-support';
15
- import { Component, createRef } from 'react';
13
+ createEditorActionsController,
14
+ type EditorActionsCallbacks,
15
+ type EditorActionsController,
16
+ } from './editorActions';
17
+ import {
18
+ formatQuery,
19
+ CypherLanguageService,
20
+ type DbSchema,
21
+ } from '@neo4j-cypher/language-support';
22
+ import debounce from 'lodash.debounce';
23
+ import { Component, createRef, type ReactNode } from 'react';
24
+ import { createPortal } from 'react-dom';
25
+ import { DEBOUNCE_TIME } from './constants';
16
26
  import {
17
27
  replaceHistory,
18
28
  replMode as historyNavigation,
19
- } from './history-navigation';
20
- import { cypher, CypherConfig } from './lang-cypher/lang-cypher';
21
- import { basicNeo4jSetup } from './neo4j-setup';
29
+ } from './historyNavigation';
30
+ import type { CypherConfig } from './lang-cypher/langCypher';
31
+ import { cypher } from './lang-cypher/langCypher';
32
+ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
33
+ import { basicNeo4jSetup } from './neo4jSetup';
22
34
  import { getThemeExtension } from './themes';
35
+ import { richClipboardCopier } from './richClipboardCopier';
36
+ import type { LintWorker } from '@neo4j-cypher/lint-worker';
37
+ import workerpool from 'workerpool';
38
+
39
+ type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
40
+
41
+ /**
42
+ * Normalize CRLF line endings to LF, which is what CodeMirror expects.
43
+ * CodeMirror collapses `\r\n` into a single line break, so a raw `value`
44
+ * string with CRLF endings is longer than the resulting document. Normalizing
45
+ * up front keeps the document length in sync with the value length used for
46
+ * cursor placement, and avoids "Selection points outside of document" errors.
47
+ * https://codemirror.net/docs/ref/#state.EditorState^lineSeparator
48
+ */
49
+ function normalizeLineEndings(value: string): string {
50
+ return value.replace(/\r\n/g, '\n');
51
+ }
23
52
 
24
53
  export interface CypherEditorProps {
25
54
  /**
@@ -40,7 +69,14 @@ export interface CypherEditorProps {
40
69
  */
41
70
  onExecute?: (cmd: string) => void;
42
71
  /**
43
- * The editor history navigateable via up/down arrow keys. Order newest to oldest.
72
+ * If true, pressing enter will add a new line to the editor and cmd/ctrl + enter will execute the query.
73
+ * Otherwise pressing enter on a single line will execute the query.
74
+ *
75
+ * @default false
76
+ */
77
+ newLineOnEnter?: boolean;
78
+ /**
79
+ * The editor history navigable via up/down arrow keys. Order newest to oldest.
44
80
  * Add to this list with the `onExecute` callback for REPL style history.
45
81
  */
46
82
  history?: string[];
@@ -57,6 +93,10 @@ export interface CypherEditorProps {
57
93
  * @default false
58
94
  */
59
95
  autofocus?: boolean;
96
+ /**
97
+ * Where to place the cursor in the query. Cannot be enabled at the same time than autofocus
98
+ */
99
+ offset?: number;
60
100
  /**
61
101
  * Whether the editor should wrap lines.
62
102
  *
@@ -69,6 +109,20 @@ export interface CypherEditorProps {
69
109
  * @default true
70
110
  */
71
111
  lint?: boolean;
112
+ /**
113
+ * Whether the signature help tooltip should be shown below the text.
114
+ * If false, it will be shown above.
115
+ *
116
+ * @default true
117
+ */
118
+ showSignatureTooltipBelow?: boolean;
119
+ /**
120
+ * Internal feature flags for the editor. Don't use in production
121
+ *
122
+ */
123
+ featureFlags?: {
124
+ consoleCommands?: boolean;
125
+ };
72
126
  /**
73
127
  * The schema to use for autocompletion and linting.
74
128
  *
@@ -96,32 +150,253 @@ export interface CypherEditorProps {
96
150
  * @param {ViewUpdate} viewUpdate - the view update from codemirror
97
151
  */
98
152
  onChange?(value: string, viewUpdate: ViewUpdate): void;
99
- }
100
153
 
101
- const executeKeybinding = (onExecute?: (cmd: string) => void) =>
102
- onExecute
103
- ? [
104
- {
105
- key: 'Ctrl-Enter',
106
- mac: 'Mod-Enter',
107
- preventDefault: true,
108
- run: (view: EditorView) => {
109
- const doc = view.state.doc.toString();
110
- if (doc.trim() !== '') {
111
- onExecute(doc);
112
- }
154
+ /**
155
+ * Map of event handlers to add to the editor.
156
+ *
157
+ * Note that the props are compared by reference, meaning object defined inline
158
+ * will cause the editor to re-render (much like the style prop does in this example:
159
+ * <div style={{}} />
160
+ *
161
+ * Memoize the object if you want/need to avoid this.
162
+ *
163
+ * @example
164
+ * // listen to blur events
165
+ * <CypherEditor domEventHandlers={{blur: () => console.log("blur event fired")}} />
166
+ */
167
+ domEventHandlers?: DomEventHandlers;
168
+ /**
169
+ * Placeholder text to display when the editor is empty.
170
+ */
171
+ placeholder?: string;
172
+ /**
173
+ * Whether the editor should show line numbers.
174
+ *
175
+ * @default true
176
+ */
177
+ lineNumbers?: boolean;
178
+ /**
179
+ * Whether the editor is read-only.
180
+ *
181
+ * @default false
182
+ */
183
+ readonly?: boolean;
184
+
185
+ /**
186
+ * String value to assign to the aria-label attribute of the editor.
187
+ */
188
+ ariaLabel?: string;
189
+
190
+ /**
191
+ * Whether keybindings for inserting indents with the Tab key should be disabled.
192
+ *
193
+ * true will not create keybindings for inserting indents.
194
+ * false will create keybindings for inserting indents.
195
+ *
196
+ * @default false
197
+ */
198
+ moveFocusOnTab?: boolean;
199
+ /**
200
+ * Render a panel as a block widget inside the editor.
201
+ * The widget DOM is only rebuilt when `pos` or `placement` change
202
+ */
203
+ inlinePanel?: InlinePanelProps | null;
204
+ /**
205
+ * Render a unified diff of the current document against `diff.original`.
206
+ * Deleted lines are shown as uneditable widgets.
207
+ */
208
+ diff?: DiffProps | null;
209
+ /**
210
+ * React content rendered as a cluster of action buttons (e.g. a context menu
211
+ * + Run) pinned to the top-right corner of the editor. Omit (or `null`) to
212
+ * hide it.
213
+ */
214
+ editorActions?: ReactNode;
215
+ }
113
216
 
114
- return true;
115
- },
217
+ export type InlinePanelProps = {
218
+ /**
219
+ * Position the panel anchors to.
220
+ */
221
+ pos: number;
222
+ /**
223
+ * Whether to render above or below the line
224
+ *
225
+ * @default 'above'
226
+ */
227
+ placement?: 'above' | 'below';
228
+ } & InlinePanelCallbacks;
229
+
230
+ const format = (view: EditorView): void => {
231
+ try {
232
+ const doc = view.state.doc.toString();
233
+ const { formattedQuery, newCursorPos } = formatQuery(doc, {
234
+ cursorPosition: view.state.selection.main.anchor,
235
+ });
236
+ view.dispatch({
237
+ changes: {
238
+ from: 0,
239
+ to: doc.length,
240
+ insert: formattedQuery,
241
+ },
242
+ selection: { anchor: newCursorPos },
243
+ });
244
+ } catch {
245
+ // Formatting failed, likely because of a syntax error
246
+ }
247
+ };
248
+
249
+ const executeKeybinding = (
250
+ onExecute?: (cmd: string) => void,
251
+ newLineOnEnter?: boolean,
252
+ flush?: () => void,
253
+ ) => {
254
+ const keybindings: Record<string, KeyBinding> = {
255
+ 'Shift-Enter': {
256
+ key: 'Shift-Enter',
257
+ run: insertNewline,
258
+ },
259
+ 'Ctrl-Enter': {
260
+ key: 'Ctrl-Enter',
261
+ run: () => true,
262
+ },
263
+ Enter: {
264
+ key: 'Enter',
265
+ run: insertNewline,
266
+ },
267
+ };
268
+ if (onExecute) {
269
+ keybindings['Ctrl-Enter'] = {
270
+ key: 'Ctrl-Enter',
271
+ mac: 'Mod-Enter',
272
+ preventDefault: true,
273
+ run: (view: EditorView) => {
274
+ const doc = view.state.doc.toString();
275
+ if (doc.trim() !== '') {
276
+ flush?.();
277
+ onExecute(doc);
278
+ }
279
+
280
+ return true;
281
+ },
282
+ };
283
+
284
+ if (!newLineOnEnter) {
285
+ keybindings['Enter'] = {
286
+ key: 'Enter',
287
+ preventDefault: true,
288
+ run: (view: EditorView) => {
289
+ const doc = view.state.doc.toString();
290
+ if (doc.includes('\n')) {
291
+ // Returning false means the event will mark the event
292
+ // as not handled and the default behavior will be executed
293
+ return false;
294
+ }
295
+
296
+ if (doc.trim() !== '') {
297
+ flush?.();
298
+ onExecute(doc);
299
+ }
300
+
301
+ return true;
116
302
  },
117
- ]
118
- : [];
303
+ };
304
+ }
305
+ }
306
+
307
+ return Object.values(keybindings);
308
+ };
119
309
 
120
310
  const themeCompartment = new Compartment();
121
311
  const keyBindingCompartment = new Compartment();
312
+ const lineNumbersCompartment = new Compartment();
313
+ const readOnlyCompartment = new Compartment();
314
+ const placeholderCompartment = new Compartment();
315
+ const domEventHandlerCompartment = new Compartment();
316
+ const diffCompartment = new Compartment();
317
+
318
+ const formatLineNumber =
319
+ (prompt?: string) => (a: number, state: EditorState) => {
320
+ if (state.doc.lines === 1 && prompt !== undefined) {
321
+ return prompt;
322
+ }
323
+
324
+ return a.toString();
325
+ };
326
+
327
+ type CypherEditorState = {
328
+ editorActionsContainer: HTMLElement | null;
329
+ };
122
330
 
123
331
  const ExternalEdit = Annotation.define<boolean>();
124
- export class CypherEditor extends Component<CypherEditorProps> {
332
+ const WorkerURL = new URL('./lang-cypher/lintWorker.mjs', import.meta.url)
333
+ .pathname;
334
+
335
+ class CodemirrorSymbolFetcher {
336
+ constructor(languageService: CypherLanguageService) {
337
+ this.languageService = languageService;
338
+ }
339
+ private languageService: CypherLanguageService;
340
+ private processing = false;
341
+ private nextJob:
342
+ | {
343
+ query: string;
344
+ schema: DbSchema;
345
+ }
346
+ | undefined;
347
+ private symbolTablePool = workerpool.pool(WorkerURL, {
348
+ minWorkers: 1,
349
+ workerOpts: { type: 'module' },
350
+ workerTerminateTimeout: 2000,
351
+ });
352
+
353
+ public queueSymbolJob(query: string, schema: DbSchema) {
354
+ this.nextJob = { query, schema };
355
+ if (!this.processing) {
356
+ void this.processJobQueue();
357
+ }
358
+ }
359
+
360
+ public terminate() {
361
+ this.nextJob = undefined;
362
+ void this.symbolTablePool.terminate();
363
+ }
364
+
365
+ private async processJobQueue() {
366
+ this.processing = true;
367
+ while (this.nextJob) {
368
+ try {
369
+ const query = this.nextJob.query;
370
+ const dbSchema = this.nextJob.schema;
371
+ this.nextJob = undefined;
372
+ const proxyWorker =
373
+ (await this.symbolTablePool.proxy()) as unknown as LintWorker;
374
+
375
+ const result = await proxyWorker.lintCypherQuery(query, dbSchema);
376
+
377
+ if (result.symbolTables) {
378
+ this.languageService.setSymbolsInfo({
379
+ query,
380
+ symbolTables: result.symbolTables,
381
+ });
382
+ }
383
+ } catch (err) {
384
+ //eslint-disable-next-line
385
+ console.log('Symbol table calculation failed ' + String(err));
386
+ }
387
+ }
388
+ this.processing = false;
389
+ }
390
+ }
391
+
392
+ export class CypherEditor extends Component<
393
+ CypherEditorProps,
394
+ CypherEditorState
395
+ > {
396
+ /**
397
+ * The symbol fetcher object used to fetch the current symbol table on document changes
398
+ */
399
+ symbolFetcher: CodemirrorSymbolFetcher;
125
400
  /**
126
401
  * The codemirror editor container.
127
402
  */
@@ -135,6 +410,25 @@ export class CypherEditor extends Component<CypherEditorProps> {
135
410
  */
136
411
  editorView: React.MutableRefObject<EditorView> = createRef();
137
412
  private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
413
+ private inlinePanelController: InlinePanelController | null = null;
414
+ private editorActionsController: EditorActionsController | null = null;
415
+
416
+ state: CypherEditorState = {
417
+ editorActionsContainer: null,
418
+ };
419
+
420
+ private editorActionsCallbacks: EditorActionsCallbacks = {
421
+ onMount: (container) =>
422
+ this.setState({ editorActionsContainer: container }),
423
+ onUnmount: () => this.setState({ editorActionsContainer: null }),
424
+ };
425
+
426
+ /**
427
+ * Format Cypher query
428
+ */
429
+ format() {
430
+ format(this.editorView.current);
431
+ }
138
432
 
139
433
  /**
140
434
  * Focus the editor
@@ -148,8 +442,17 @@ export class CypherEditor extends Component<CypherEditorProps> {
148
442
  * For example, to move the cursor to the end of the editor, use `value.length`
149
443
  */
150
444
  updateCursorPosition(position: number) {
151
- this.editorView.current?.dispatch({
152
- selection: { anchor: position, head: position },
445
+ const view = this.editorView.current;
446
+ if (!view) {
447
+ return;
448
+ }
449
+
450
+ // Clamp to the doc length; `position` may come from a raw value longer than
451
+ // the doc (e.g. CRLF endings), which would throw "Selection points outside
452
+ // of document".
453
+ const clamped = Math.max(0, Math.min(position, view.state.doc.length));
454
+ view.dispatch({
455
+ selection: { anchor: clamped, head: clamped },
153
456
  });
154
457
  }
155
458
 
@@ -158,13 +461,17 @@ export class CypherEditor extends Component<CypherEditorProps> {
158
461
  */
159
462
  setValueAndFocus(value = '') {
160
463
  const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
464
+ const normalizedValue = normalizeLineEndings(value);
161
465
  this.editorView.current.dispatch({
162
466
  changes: {
163
467
  from: 0,
164
468
  to: currentCmValue.length,
165
- insert: value,
469
+ insert: normalizedValue,
470
+ },
471
+ selection: {
472
+ anchor: normalizedValue.length,
473
+ head: normalizedValue.length,
166
474
  },
167
- selection: { anchor: value.length, head: value.length },
168
475
  });
169
476
  this.editorView.current?.focus();
170
477
  }
@@ -174,11 +481,24 @@ export class CypherEditor extends Component<CypherEditorProps> {
174
481
  schema: {},
175
482
  overrideThemeBackgroundColor: false,
176
483
  lineWrap: false,
484
+ showSignatureTooltipBelow: true,
177
485
  extraKeybindings: [],
178
486
  history: [],
179
487
  theme: 'light',
488
+ lineNumbers: true,
489
+ newLineOnEnter: false,
490
+ moveFocusOnTab: false,
180
491
  };
181
492
 
493
+ private debouncedOnChange = this.props.onChange
494
+ ? debounce(
495
+ ((value, viewUpdate) => {
496
+ this.props.onChange(value, viewUpdate);
497
+ }) satisfies CypherEditorProps['onChange'],
498
+ DEBOUNCE_TIME,
499
+ )
500
+ : undefined;
501
+
182
502
  componentDidMount(): void {
183
503
  const {
184
504
  theme,
@@ -187,20 +507,50 @@ export class CypherEditor extends Component<CypherEditorProps> {
187
507
  overrideThemeBackgroundColor,
188
508
  schema,
189
509
  lint,
190
- onChange,
510
+ showSignatureTooltipBelow,
511
+ featureFlags,
191
512
  onExecute,
513
+ newLineOnEnter,
192
514
  } = this.props;
193
515
 
194
- this.schemaRef.current = { schema, lint };
516
+ this.schemaRef.current = {
517
+ languageService: new CypherLanguageService(),
518
+ schema,
519
+ lint,
520
+ showSignatureTooltipBelow,
521
+ featureFlags: {
522
+ consoleCommands: true,
523
+ ...featureFlags,
524
+ },
525
+ useLightVersion: false,
526
+ setUseLightVersion: (newVal) => {
527
+ if (this.schemaRef.current !== undefined) {
528
+ this.schemaRef.current.useLightVersion = newVal;
529
+ }
530
+ },
531
+ };
532
+
533
+ this.symbolFetcher = new CodemirrorSymbolFetcher(
534
+ this.schemaRef.current.languageService,
535
+ );
195
536
 
196
537
  const themeExtension = getThemeExtension(
197
538
  theme,
198
539
  overrideThemeBackgroundColor,
199
540
  );
200
541
 
201
- const changeListener = onChange
542
+ this.inlinePanelController = createInlinePanelController();
543
+ this.editorActionsController = createEditorActionsController();
544
+
545
+ const changeListener = this.debouncedOnChange
202
546
  ? [
203
547
  EditorView.updateListener.of((upt: ViewUpdate) => {
548
+ if (upt.docChanged) {
549
+ this.symbolFetcher.queueSymbolJob(
550
+ upt.state.doc.toString(),
551
+ this.schemaRef.current.schema,
552
+ );
553
+ }
204
554
  const wasUserEdit = !upt.transactions.some((tr) =>
205
555
  tr.annotation(ExternalEdit),
206
556
  );
@@ -208,7 +558,7 @@ export class CypherEditor extends Component<CypherEditorProps> {
208
558
  if (upt.docChanged && wasUserEdit) {
209
559
  const doc = upt.state.doc;
210
560
  const value = doc.toString();
211
- onChange(value, upt);
561
+ this.debouncedOnChange(value, upt);
212
562
  }
213
563
  }),
214
564
  ]
@@ -217,26 +567,53 @@ export class CypherEditor extends Component<CypherEditorProps> {
217
567
  this.editorState.current = EditorState.create({
218
568
  extensions: [
219
569
  keyBindingCompartment.of(
220
- keymap.of([...executeKeybinding(onExecute), ...extraKeybindings]),
570
+ keymap.of([
571
+ ...executeKeybinding(onExecute, newLineOnEnter, () =>
572
+ this.debouncedOnChange?.flush(),
573
+ ),
574
+ ...extraKeybindings,
575
+ ]),
221
576
  ),
577
+ richClipboardCopier,
222
578
  historyNavigation(this.props),
223
- basicNeo4jSetup(),
579
+ basicNeo4jSetup(this.props),
224
580
  themeCompartment.of(themeExtension),
225
581
  changeListener,
226
582
  cypher(this.schemaRef.current),
227
583
  lineWrap ? EditorView.lineWrapping : [],
228
584
 
229
- lineNumbers({
230
- formatNumber: (a, state) => {
231
- if (state.doc.lines === 1 && this.props.prompt !== undefined) {
232
- return this.props.prompt;
233
- }
234
-
235
- return a.toString();
236
- },
237
- }),
585
+ lineNumbersCompartment.of(
586
+ this.props.lineNumbers
587
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
588
+ : [],
589
+ ),
590
+ readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
591
+ placeholderCompartment.of(
592
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
593
+ ),
594
+ domEventHandlerCompartment.of(
595
+ this.props.domEventHandlers
596
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
597
+ : [],
598
+ ),
599
+ this.props.ariaLabel
600
+ ? EditorView.contentAttributes.of({
601
+ 'aria-label': this.props.ariaLabel,
602
+ })
603
+ : [],
604
+ !this.props.moveFocusOnTab
605
+ ? EditorView.contentAttributes.of({
606
+ 'aria-description':
607
+ 'Press Escape to leave the editor and continue tabbing through the page',
608
+ })
609
+ : [],
610
+ this.inlinePanelController.extension,
611
+ diffCompartment.of(
612
+ this.props.diff ? createDiffExtension(this.props.diff) : [],
613
+ ),
614
+ this.editorActionsController.extension,
238
615
  ],
239
- doc: this.props.value,
616
+ doc: normalizeLineEndings(this.props.value ?? ''),
240
617
  });
241
618
 
242
619
  this.editorView.current = new EditorView({
@@ -249,7 +626,72 @@ export class CypherEditor extends Component<CypherEditorProps> {
249
626
  if (this.props.value) {
250
627
  this.updateCursorPosition(this.props.value.length);
251
628
  }
629
+ } else if (this.props.offset) {
630
+ this.updateCursorPosition(this.props.offset);
631
+ }
632
+
633
+ if (this.props.inlinePanel) {
634
+ this.openInlinePanel(this.props.inlinePanel);
635
+ }
636
+
637
+ if (this.props.editorActions != null) {
638
+ this.setEditorActions(true);
639
+ }
640
+ }
641
+
642
+ private setEditorActions(active: boolean): void {
643
+ const view = this.editorView.current;
644
+ const controller = this.editorActionsController;
645
+ if (!view || !controller) {
646
+ return;
647
+ }
648
+
649
+ view.dispatch({
650
+ effects: controller.set(active ? this.editorActionsCallbacks : null),
651
+ });
652
+ }
653
+
654
+ private openInlinePanel(
655
+ props: NonNullable<CypherEditorProps['inlinePanel']>,
656
+ ): void {
657
+ const view = this.editorView.current;
658
+ const controller = this.inlinePanelController;
659
+ if (!view || !controller) {
660
+ return;
661
+ }
662
+
663
+ const pos = Math.max(0, Math.min(props.pos, view.state.doc.length));
664
+ const line = view.state.doc.lineAt(pos);
665
+ controller.updateCallbacks({
666
+ onMount: props.onMount,
667
+ onUnmount: props.onUnmount,
668
+ });
669
+ view.dispatch({
670
+ effects: controller.show({
671
+ pos: props.placement === 'below' ? line.to : line.from,
672
+ placement: props.placement,
673
+ onMount: props.onMount,
674
+ onUnmount: props.onUnmount,
675
+ }),
676
+ });
677
+ }
678
+
679
+ private updateInlinePanel(
680
+ props: NonNullable<CypherEditorProps['inlinePanel']>,
681
+ ): void {
682
+ this.inlinePanelController?.updateCallbacks({
683
+ onMount: props.onMount,
684
+ onUnmount: props.onUnmount,
685
+ });
686
+ }
687
+
688
+ private closeInlinePanel(): void {
689
+ const view = this.editorView.current;
690
+ const controller = this.inlinePanelController;
691
+ if (!view || !controller) {
692
+ return;
252
693
  }
694
+ view.dispatch({ effects: controller.hide() });
253
695
  }
254
696
 
255
697
  componentDidUpdate(prevProps: CypherEditorProps): void {
@@ -260,12 +702,16 @@ export class CypherEditor extends Component<CypherEditorProps> {
260
702
  // Handle externally set value
261
703
  const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
262
704
 
263
- if (this.props.value !== undefined && currentCmValue !== this.props.value) {
705
+ if (
706
+ this.props.value !== undefined && // If the component becomes uncontolled, we just leave the value as is
707
+ this.props.value !== prevProps.value && // The value prop has changed, we need to update the editor
708
+ this.props.value !== currentCmValue // No need to dispatch an update if the value is the same
709
+ ) {
264
710
  this.editorView.current.dispatch({
265
711
  changes: {
266
712
  from: 0,
267
713
  to: currentCmValue.length,
268
- insert: this.props.value ?? '',
714
+ insert: normalizeLineEndings(this.props.value ?? ''),
269
715
  },
270
716
  annotations: [ExternalEdit.of(true)],
271
717
  });
@@ -288,6 +734,35 @@ export class CypherEditor extends Component<CypherEditorProps> {
288
734
  });
289
735
  }
290
736
 
737
+ if (
738
+ prevProps.lineNumbers !== this.props.lineNumbers ||
739
+ prevProps.prompt !== this.props.prompt
740
+ ) {
741
+ this.editorView.current.dispatch({
742
+ effects: lineNumbersCompartment.reconfigure(
743
+ this.props.lineNumbers
744
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
745
+ : [],
746
+ ),
747
+ });
748
+ }
749
+
750
+ if (prevProps.readonly !== this.props.readonly) {
751
+ this.editorView.current.dispatch({
752
+ effects: readOnlyCompartment.reconfigure(
753
+ EditorState.readOnly.of(this.props.readonly),
754
+ ),
755
+ });
756
+ }
757
+
758
+ if (prevProps.placeholder !== this.props.placeholder) {
759
+ this.editorView.current.dispatch({
760
+ effects: placeholderCompartment.reconfigure(
761
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
762
+ ),
763
+ });
764
+ }
765
+
291
766
  if (
292
767
  prevProps.extraKeybindings !== this.props.extraKeybindings ||
293
768
  prevProps.onExecute !== this.props.onExecute
@@ -295,13 +770,57 @@ export class CypherEditor extends Component<CypherEditorProps> {
295
770
  this.editorView.current.dispatch({
296
771
  effects: keyBindingCompartment.reconfigure(
297
772
  keymap.of([
298
- ...executeKeybinding(this.props.onExecute),
773
+ ...executeKeybinding(
774
+ this.props.onExecute,
775
+ this.props.newLineOnEnter,
776
+ () => this.debouncedOnChange?.flush(),
777
+ ),
299
778
  ...this.props.extraKeybindings,
300
779
  ]),
301
780
  ),
302
781
  });
303
782
  }
304
783
 
784
+ const prevPanel = prevProps.inlinePanel;
785
+ const nextPanel = this.props.inlinePanel;
786
+ if (prevPanel !== nextPanel) {
787
+ if (!nextPanel) {
788
+ this.closeInlinePanel();
789
+ } else if (
790
+ !prevPanel ||
791
+ prevPanel.pos !== nextPanel.pos ||
792
+ prevPanel.placement !== nextPanel.placement
793
+ ) {
794
+ this.openInlinePanel(nextPanel);
795
+ } else {
796
+ this.updateInlinePanel(nextPanel);
797
+ }
798
+ }
799
+
800
+ if (prevProps.diff?.original !== this.props.diff?.original) {
801
+ this.editorView.current.dispatch({
802
+ effects: diffCompartment.reconfigure(
803
+ this.props.diff ? createDiffExtension(this.props.diff) : [],
804
+ ),
805
+ });
806
+ }
807
+
808
+ const hadActions = prevProps.editorActions != null;
809
+ const hasActions = this.props.editorActions != null;
810
+ if (hadActions !== hasActions) {
811
+ this.setEditorActions(hasActions);
812
+ }
813
+
814
+ if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
815
+ this.editorView.current.dispatch({
816
+ effects: domEventHandlerCompartment.reconfigure(
817
+ this.props.domEventHandlers
818
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
819
+ : [],
820
+ ),
821
+ });
822
+ }
823
+
305
824
  // This component rerenders on every keystroke and comparing the
306
825
  // full lists of editor strings on every render could be expensive.
307
826
  const didChangeHistoryEstimate =
@@ -321,10 +840,13 @@ export class CypherEditor extends Component<CypherEditorProps> {
321
840
  */
322
841
  this.schemaRef.current.schema = this.props.schema;
323
842
  this.schemaRef.current.lint = this.props.lint;
843
+ this.schemaRef.current.featureFlags = this.props.featureFlags;
324
844
  }
325
845
 
326
846
  componentWillUnmount(): void {
327
847
  this.editorView.current?.destroy();
848
+ this.symbolFetcher?.terminate();
849
+ cleanupWorkers();
328
850
  }
329
851
 
330
852
  render(): React.ReactNode {
@@ -333,11 +855,16 @@ export class CypherEditor extends Component<CypherEditorProps> {
333
855
  const themeClass =
334
856
  typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme';
335
857
 
858
+ const { editorActionsContainer } = this.state;
859
+
336
860
  return (
337
861
  <div
338
862
  ref={this.editorContainer}
339
863
  className={`${themeClass}${className ? ` ${className}` : ''}`}
340
- />
864
+ >
865
+ {editorActionsContainer &&
866
+ createPortal(this.props.editorActions, editorActionsContainer)}
867
+ </div>
341
868
  );
342
869
  }
343
870
  }