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

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