@neo4j-cypher/react-codemirror 0.0.0-canary-20250423075344

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 (138) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +35 -0
  4. package/dist/CypherEditor.d.ts +192 -0
  5. package/dist/CypherEditor.js +326 -0
  6. package/dist/CypherEditor.js.map +1 -0
  7. package/dist/CypherEditor.test.d.ts +1 -0
  8. package/dist/CypherEditor.test.js +151 -0
  9. package/dist/CypherEditor.test.js.map +1 -0
  10. package/dist/constants.d.ts +1 -0
  11. package/dist/constants.js +2 -0
  12. package/dist/constants.js.map +1 -0
  13. package/dist/e2e_tests/autoCompletion.spec.d.ts +1 -0
  14. package/dist/e2e_tests/autoCompletion.spec.js +318 -0
  15. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
  16. package/dist/e2e_tests/configuration.spec.d.ts +1 -0
  17. package/dist/e2e_tests/configuration.spec.js +83 -0
  18. package/dist/e2e_tests/configuration.spec.js.map +1 -0
  19. package/dist/e2e_tests/debounce.spec.d.ts +1 -0
  20. package/dist/e2e_tests/debounce.spec.js +66 -0
  21. package/dist/e2e_tests/debounce.spec.js.map +1 -0
  22. package/dist/e2e_tests/e2eUtils.d.ts +13 -0
  23. package/dist/e2e_tests/e2eUtils.js +71 -0
  24. package/dist/e2e_tests/e2eUtils.js.map +1 -0
  25. package/dist/e2e_tests/extraKeybindings.spec.d.ts +1 -0
  26. package/dist/e2e_tests/extraKeybindings.spec.js +43 -0
  27. package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
  28. package/dist/e2e_tests/historyNavigation.spec.d.ts +1 -0
  29. package/dist/e2e_tests/historyNavigation.spec.js +227 -0
  30. package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
  31. package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
  32. package/dist/e2e_tests/performanceTest.spec.js +97 -0
  33. package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
  34. package/dist/e2e_tests/sanityChecks.spec.d.ts +1 -0
  35. package/dist/e2e_tests/sanityChecks.spec.js +53 -0
  36. package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
  37. package/dist/e2e_tests/signatureHelp.spec.d.ts +1 -0
  38. package/dist/e2e_tests/signatureHelp.spec.js +228 -0
  39. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
  40. package/dist/e2e_tests/snippets.spec.d.ts +1 -0
  41. package/dist/e2e_tests/snippets.spec.js +62 -0
  42. package/dist/e2e_tests/snippets.spec.js.map +1 -0
  43. package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  44. package/dist/e2e_tests/syntaxHighlighting.spec.js +90 -0
  45. package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  46. package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  47. package/dist/e2e_tests/syntaxValidation.spec.js +116 -0
  48. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
  49. package/dist/historyNavigation.d.ts +7 -0
  50. package/dist/historyNavigation.js +163 -0
  51. package/dist/historyNavigation.js.map +1 -0
  52. package/dist/icons.d.ts +2 -0
  53. package/dist/icons.js +62 -0
  54. package/dist/icons.js.map +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +5 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/lang-cypher/autocomplete.d.ts +6 -0
  59. package/dist/lang-cypher/autocomplete.js +115 -0
  60. package/dist/lang-cypher/autocomplete.js.map +1 -0
  61. package/dist/lang-cypher/constants.d.ts +42 -0
  62. package/dist/lang-cypher/constants.js +69 -0
  63. package/dist/lang-cypher/constants.js.map +1 -0
  64. package/dist/lang-cypher/contants.test.d.ts +1 -0
  65. package/dist/lang-cypher/contants.test.js +103 -0
  66. package/dist/lang-cypher/contants.test.js.map +1 -0
  67. package/dist/lang-cypher/createCypherTheme.d.ts +26 -0
  68. package/dist/lang-cypher/createCypherTheme.js +182 -0
  69. package/dist/lang-cypher/createCypherTheme.js.map +1 -0
  70. package/dist/lang-cypher/langCypher.d.ts +14 -0
  71. package/dist/lang-cypher/langCypher.js +23 -0
  72. package/dist/lang-cypher/langCypher.js.map +1 -0
  73. package/dist/lang-cypher/lintWorker.d.ts +12 -0
  74. package/dist/lang-cypher/lintWorker.js +14 -0
  75. package/dist/lang-cypher/lintWorker.js.map +1 -0
  76. package/dist/lang-cypher/parser-adapter.d.ts +19 -0
  77. package/dist/lang-cypher/parser-adapter.js +113 -0
  78. package/dist/lang-cypher/parser-adapter.js.map +1 -0
  79. package/dist/lang-cypher/signatureHelp.d.ts +4 -0
  80. package/dist/lang-cypher/signatureHelp.js +109 -0
  81. package/dist/lang-cypher/signatureHelp.js.map +1 -0
  82. package/dist/lang-cypher/syntaxValidation.d.ts +4 -0
  83. package/dist/lang-cypher/syntaxValidation.js +52 -0
  84. package/dist/lang-cypher/syntaxValidation.js.map +1 -0
  85. package/dist/lang-cypher/themeIcons.d.ts +7 -0
  86. package/dist/lang-cypher/themeIcons.js +22 -0
  87. package/dist/lang-cypher/themeIcons.js.map +1 -0
  88. package/dist/lang-cypher/utils.d.ts +2 -0
  89. package/dist/lang-cypher/utils.js +10 -0
  90. package/dist/lang-cypher/utils.js.map +1 -0
  91. package/dist/ndlTokensCopy.d.ts +570 -0
  92. package/dist/ndlTokensCopy.js +571 -0
  93. package/dist/ndlTokensCopy.js.map +1 -0
  94. package/dist/ndlTokensCopy.test.d.ts +1 -0
  95. package/dist/ndlTokensCopy.test.js +12 -0
  96. package/dist/ndlTokensCopy.test.js.map +1 -0
  97. package/dist/neo4jSetup.d.ts +6 -0
  98. package/dist/neo4jSetup.js +120 -0
  99. package/dist/neo4jSetup.js.map +1 -0
  100. package/dist/themes.d.ts +11 -0
  101. package/dist/themes.js +93 -0
  102. package/dist/themes.js.map +1 -0
  103. package/dist/tsconfig.tsbuildinfo +1 -0
  104. package/package.json +82 -0
  105. package/src/CypherEditor.test.tsx +200 -0
  106. package/src/CypherEditor.tsx +607 -0
  107. package/src/constants.ts +1 -0
  108. package/src/e2e_tests/autoCompletion.spec.tsx +546 -0
  109. package/src/e2e_tests/configuration.spec.tsx +111 -0
  110. package/src/e2e_tests/debounce.spec.tsx +106 -0
  111. package/src/e2e_tests/e2eUtils.ts +97 -0
  112. package/src/e2e_tests/extraKeybindings.spec.tsx +55 -0
  113. package/src/e2e_tests/historyNavigation.spec.tsx +315 -0
  114. package/src/e2e_tests/performanceTest.spec.tsx +163 -0
  115. package/src/e2e_tests/sanityChecks.spec.tsx +72 -0
  116. package/src/e2e_tests/signatureHelp.spec.tsx +445 -0
  117. package/src/e2e_tests/snippets.spec.tsx +92 -0
  118. package/src/e2e_tests/syntaxHighlighting.spec.tsx +200 -0
  119. package/src/e2e_tests/syntaxValidation.spec.tsx +243 -0
  120. package/src/historyNavigation.ts +191 -0
  121. package/src/icons.ts +90 -0
  122. package/src/index.ts +4 -0
  123. package/src/lang-cypher/autocomplete.ts +147 -0
  124. package/src/lang-cypher/constants.ts +88 -0
  125. package/src/lang-cypher/contants.test.ts +108 -0
  126. package/src/lang-cypher/createCypherTheme.ts +249 -0
  127. package/src/lang-cypher/langCypher.ts +43 -0
  128. package/src/lang-cypher/lintWorker.ts +31 -0
  129. package/src/lang-cypher/parser-adapter.ts +145 -0
  130. package/src/lang-cypher/signatureHelp.ts +151 -0
  131. package/src/lang-cypher/syntaxValidation.ts +66 -0
  132. package/src/lang-cypher/themeIcons.ts +27 -0
  133. package/src/lang-cypher/utils.ts +9 -0
  134. package/src/ndlTokensCopy.test.ts +12 -0
  135. package/src/ndlTokensCopy.ts +570 -0
  136. package/src/neo4jSetup.tsx +190 -0
  137. package/src/themes.ts +105 -0
  138. package/src/viteEnv.d.ts +1 -0
@@ -0,0 +1,607 @@
1
+ import { insertNewline } from '@codemirror/commands';
2
+ import {
3
+ Annotation,
4
+ Compartment,
5
+ EditorState,
6
+ Extension,
7
+ } from '@codemirror/state';
8
+ import {
9
+ EditorView,
10
+ KeyBinding,
11
+ keymap,
12
+ lineNumbers,
13
+ placeholder,
14
+ ViewUpdate,
15
+ } from '@codemirror/view';
16
+ import {
17
+ formatQuery,
18
+ _internalFeatureFlags,
19
+ type DbSchema,
20
+ } from '@neo4j-cypher/language-support';
21
+ import debounce from 'lodash.debounce';
22
+ import { Component, createRef } from 'react';
23
+ import { DEBOUNCE_TIME } from './constants';
24
+ import {
25
+ replaceHistory,
26
+ replMode as historyNavigation,
27
+ } from './historyNavigation';
28
+ import { cypher, CypherConfig } from './lang-cypher/langCypher';
29
+ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
30
+ import { basicNeo4jSetup } from './neo4jSetup';
31
+ import { getThemeExtension } from './themes';
32
+
33
+ type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
34
+ export interface CypherEditorProps {
35
+ /**
36
+ * The prompt to show on single line editors
37
+ */
38
+ prompt?: string;
39
+ /**
40
+ * Custom keybindings to add to the editor.
41
+ * See https://codemirror.net/6/docs/ref/#keymap.of
42
+ */
43
+ extraKeybindings?: KeyBinding[];
44
+ /**
45
+ * Callback on query execution, triggered via ctrl/cmd + Enter.
46
+ * If provided, will enable "repl-mode", which turns on navigating editor history
47
+ *
48
+ * @param cmd - the editor value when ctrl/cmd + enter was pressed
49
+ * @returns void
50
+ */
51
+ onExecute?: (cmd: string) => void;
52
+ /**
53
+ * If true, pressing enter will add a new line to the editor and cmd/ctrl + enter will execute the query.
54
+ * Otherwise pressing enter on a single line will execute the query.
55
+ *
56
+ * @default false
57
+ */
58
+ newLineOnEnter?: boolean;
59
+ /**
60
+ * The editor history navigable via up/down arrow keys. Order newest to oldest.
61
+ * Add to this list with the `onExecute` callback for REPL style history.
62
+ */
63
+ history?: string[];
64
+ /**
65
+ * When set to `true` the editor will use the background color of the parent element.
66
+ *
67
+ * @default false
68
+ */
69
+ overrideThemeBackgroundColor?: boolean;
70
+ /**
71
+ * Whether the editor should take focus on mount.
72
+ * Will move the cursor to the end of the query when provided with an initial value.
73
+ *
74
+ * @default false
75
+ */
76
+ autofocus?: boolean;
77
+ /**
78
+ * Where to place the cursor in the query. Cannot be enabled at the same time than autofocus
79
+ */
80
+ offset?: number;
81
+ /**
82
+ * Whether the editor should wrap lines.
83
+ *
84
+ * @default false
85
+ */
86
+ lineWrap?: boolean;
87
+ /**
88
+ * Whether the editor should perform syntax validation.
89
+ *
90
+ * @default true
91
+ */
92
+ lint?: boolean;
93
+ /**
94
+ * Whether the signature help tooltip should be shown below the text.
95
+ * If false, it will be shown above.
96
+ *
97
+ * @default true
98
+ */
99
+ showSignatureTooltipBelow?: boolean;
100
+ /**
101
+ * Internal feature flags for the editor. Don't use in production
102
+ *
103
+ */
104
+ featureFlags?: {
105
+ consoleCommands?: boolean;
106
+ cypher25?: boolean;
107
+ };
108
+ /**
109
+ * The schema to use for autocompletion and linting.
110
+ *
111
+ * @type {DbSchema}
112
+ */
113
+ schema?: DbSchema;
114
+ /**
115
+ * The current value of the editor.
116
+ */
117
+ value?: string;
118
+ /**
119
+ * Extra css classnames to add to the editor container.
120
+ */
121
+ className?: string;
122
+ /**
123
+ * Set the built in theme or provide a custom theme.
124
+ *
125
+ * `light` / `dark` / `Extension`
126
+ * @default light
127
+ */
128
+ theme?: 'light' | 'dark' | Extension;
129
+ /**
130
+ * Callback when the editor value changes.
131
+ * @param {string} value - the current editor value
132
+ * @param {ViewUpdate} viewUpdate - the view update from codemirror
133
+ */
134
+ onChange?(value: string, viewUpdate: ViewUpdate): void;
135
+
136
+ /**
137
+ * Map of event handlers to add to the editor.
138
+ *
139
+ * Note that the props are compared by reference, meaning object defined inline
140
+ * will cause the editor to re-render (much like the style prop does in this example:
141
+ * <div style={{}} />
142
+ *
143
+ * Memoize the object if you want/need to avoid this.
144
+ *
145
+ * @example
146
+ * // listen to blur events
147
+ * <CypherEditor domEventHandlers={{blur: () => console.log("blur event fired")}} />
148
+ */
149
+ domEventHandlers?: DomEventHandlers;
150
+ /**
151
+ * Placeholder text to display when the editor is empty.
152
+ */
153
+ placeholder?: string;
154
+ /**
155
+ * Whether the editor should show line numbers.
156
+ *
157
+ * @default true
158
+ */
159
+ lineNumbers?: boolean;
160
+ /**
161
+ * Whether the editor is read-only.
162
+ *
163
+ * @default false
164
+ */
165
+ readonly?: boolean;
166
+
167
+ /**
168
+ * String value to assign to the aria-label attribute of the editor.
169
+ */
170
+ ariaLabel?: string;
171
+
172
+ /**
173
+ * Whether keybindings for inserting indents with the Tab key should be disabled.
174
+ *
175
+ * true will not create keybindings for inserting indents.
176
+ * false will create keybindings for inserting indents.
177
+ *
178
+ * @default false
179
+ */
180
+ moveFocusOnTab?: boolean;
181
+ }
182
+
183
+ const format = (view: EditorView): void => {
184
+ try {
185
+ const doc = view.state.doc.toString();
186
+ const { formattedString, newCursorPos } = formatQuery(doc, view.state.selection.main.anchor);
187
+ view.dispatch({
188
+ changes: {
189
+ from: 0,
190
+ to: doc.length,
191
+ insert: formattedString,
192
+ },
193
+ selection: { anchor: newCursorPos },
194
+ });
195
+ } catch (error) {
196
+ // Formatting failed, likely because of a syntax error
197
+ }
198
+ };
199
+
200
+ const executeKeybinding = (
201
+ onExecute?: (cmd: string) => void,
202
+ newLineOnEnter?: boolean,
203
+ flush?: () => void,
204
+ ) => {
205
+ const keybindings: Record<string, KeyBinding> = {
206
+ 'Shift-Enter': {
207
+ key: 'Shift-Enter',
208
+ run: insertNewline,
209
+ },
210
+ 'Ctrl-Enter': {
211
+ key: 'Ctrl-Enter',
212
+ run: () => true,
213
+ },
214
+ Enter: {
215
+ key: 'Enter',
216
+ run: insertNewline,
217
+ },
218
+ };
219
+ if (onExecute) {
220
+ keybindings['Ctrl-Enter'] = {
221
+ key: 'Ctrl-Enter',
222
+ mac: 'Mod-Enter',
223
+ preventDefault: true,
224
+ run: (view: EditorView) => {
225
+ const doc = view.state.doc.toString();
226
+ if (doc.trim() !== '') {
227
+ flush?.();
228
+ onExecute(doc);
229
+ }
230
+
231
+ return true;
232
+ },
233
+ };
234
+
235
+ if (!newLineOnEnter) {
236
+ keybindings['Enter'] = {
237
+ key: 'Enter',
238
+ preventDefault: true,
239
+ run: (view: EditorView) => {
240
+ const doc = view.state.doc.toString();
241
+ if (doc.includes('\n')) {
242
+ // Returning false means the event will mark the event
243
+ // as not handled and the default behavior will be executed
244
+ return false;
245
+ }
246
+
247
+ if (doc.trim() !== '') {
248
+ flush?.();
249
+ onExecute(doc);
250
+ }
251
+
252
+ return true;
253
+ },
254
+ };
255
+ }
256
+ }
257
+
258
+ return Object.values(keybindings);
259
+ };
260
+
261
+ const themeCompartment = new Compartment();
262
+ const keyBindingCompartment = new Compartment();
263
+ const lineNumbersCompartment = new Compartment();
264
+ const readOnlyCompartment = new Compartment();
265
+ const placeholderCompartment = new Compartment();
266
+ const domEventHandlerCompartment = new Compartment();
267
+
268
+ const formatLineNumber =
269
+ (prompt?: string) => (a: number, state: EditorState) => {
270
+ if (state.doc.lines === 1 && prompt !== undefined) {
271
+ return prompt;
272
+ }
273
+
274
+ return a.toString();
275
+ };
276
+
277
+ type CypherEditorState = { cypherSupportEnabled: boolean };
278
+
279
+ const ExternalEdit = Annotation.define<boolean>();
280
+
281
+ export class CypherEditor extends Component<
282
+ CypherEditorProps,
283
+ CypherEditorState
284
+ > {
285
+ /**
286
+ * The codemirror editor container.
287
+ */
288
+ editorContainer: React.RefObject<HTMLDivElement> = createRef();
289
+ /**
290
+ * The codemirror editor state.
291
+ */
292
+ editorState: React.MutableRefObject<EditorState> = createRef();
293
+ /**
294
+ * The codemirror editor view.
295
+ */
296
+ editorView: React.MutableRefObject<EditorView> = createRef();
297
+ private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
298
+
299
+ /**
300
+ * Format Cypher query
301
+ */
302
+ format() {
303
+ format(this.editorView.current);
304
+ }
305
+
306
+ /**
307
+ * Focus the editor
308
+ */
309
+ focus() {
310
+ this.editorView.current?.focus();
311
+ }
312
+
313
+ /**
314
+ * Move the cursor to the supplied position.
315
+ * For example, to move the cursor to the end of the editor, use `value.length`
316
+ */
317
+ updateCursorPosition(position: number) {
318
+ this.editorView.current?.dispatch({
319
+ selection: { anchor: position, head: position },
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Externally set the editor value and focus the editor.
325
+ */
326
+ setValueAndFocus(value = '') {
327
+ const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
328
+ this.editorView.current.dispatch({
329
+ changes: {
330
+ from: 0,
331
+ to: currentCmValue.length,
332
+ insert: value,
333
+ },
334
+ selection: { anchor: value.length, head: value.length },
335
+ });
336
+ this.editorView.current?.focus();
337
+ }
338
+
339
+ static defaultProps: CypherEditorProps = {
340
+ lint: true,
341
+ schema: {},
342
+ overrideThemeBackgroundColor: false,
343
+ lineWrap: false,
344
+ showSignatureTooltipBelow: true,
345
+ extraKeybindings: [],
346
+ history: [],
347
+ theme: 'light',
348
+ lineNumbers: true,
349
+ newLineOnEnter: false,
350
+ moveFocusOnTab: false,
351
+ };
352
+
353
+ private debouncedOnChange = this.props.onChange
354
+ ? debounce(
355
+ ((value, viewUpdate) => {
356
+ this.props.onChange(value, viewUpdate);
357
+ }) satisfies CypherEditorProps['onChange'],
358
+ DEBOUNCE_TIME,
359
+ )
360
+ : undefined;
361
+
362
+ componentDidMount(): void {
363
+ const {
364
+ theme,
365
+ extraKeybindings,
366
+ lineWrap,
367
+ overrideThemeBackgroundColor,
368
+ schema,
369
+ lint,
370
+ showSignatureTooltipBelow,
371
+ featureFlags,
372
+ onExecute,
373
+ newLineOnEnter,
374
+ } = this.props;
375
+
376
+ _internalFeatureFlags.cypher25 = featureFlags?.cypher25 ?? false;
377
+
378
+ this.schemaRef.current = {
379
+ schema,
380
+ lint,
381
+ showSignatureTooltipBelow,
382
+ featureFlags: {
383
+ consoleCommands: true,
384
+ ...featureFlags,
385
+ },
386
+ useLightVersion: false,
387
+ setUseLightVersion: (newVal) => {
388
+ if (this.schemaRef.current !== undefined) {
389
+ this.schemaRef.current.useLightVersion = newVal;
390
+ }
391
+ },
392
+ };
393
+
394
+ const themeExtension = getThemeExtension(
395
+ theme,
396
+ overrideThemeBackgroundColor,
397
+ );
398
+
399
+ const changeListener = this.debouncedOnChange
400
+ ? [
401
+ EditorView.updateListener.of((upt: ViewUpdate) => {
402
+ const wasUserEdit = !upt.transactions.some((tr) =>
403
+ tr.annotation(ExternalEdit),
404
+ );
405
+
406
+ if (upt.docChanged && wasUserEdit) {
407
+ const doc = upt.state.doc;
408
+ const value = doc.toString();
409
+ this.debouncedOnChange(value, upt);
410
+ }
411
+ }),
412
+ ]
413
+ : [];
414
+
415
+ this.editorState.current = EditorState.create({
416
+ extensions: [
417
+ keyBindingCompartment.of(
418
+ keymap.of([
419
+ ...executeKeybinding(onExecute, newLineOnEnter, () =>
420
+ this.debouncedOnChange?.flush(),
421
+ ),
422
+ ...extraKeybindings,
423
+ ]),
424
+ ),
425
+ historyNavigation(this.props),
426
+ basicNeo4jSetup(this.props),
427
+ themeCompartment.of(themeExtension),
428
+ changeListener,
429
+ cypher(this.schemaRef.current),
430
+ lineWrap ? EditorView.lineWrapping : [],
431
+
432
+ lineNumbersCompartment.of(
433
+ this.props.lineNumbers
434
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
435
+ : [],
436
+ ),
437
+ readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
438
+ placeholderCompartment.of(
439
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
440
+ ),
441
+ domEventHandlerCompartment.of(
442
+ this.props.domEventHandlers
443
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
444
+ : [],
445
+ ),
446
+ this.props.ariaLabel
447
+ ? EditorView.contentAttributes.of({
448
+ 'aria-label': this.props.ariaLabel,
449
+ })
450
+ : [],
451
+ ],
452
+ doc: this.props.value,
453
+ });
454
+
455
+ this.editorView.current = new EditorView({
456
+ state: this.editorState.current,
457
+ parent: this.editorContainer.current,
458
+ });
459
+
460
+ if (this.props.autofocus) {
461
+ this.focus();
462
+ if (this.props.value) {
463
+ this.updateCursorPosition(this.props.value.length);
464
+ }
465
+ } else if (this.props.offset) {
466
+ this.updateCursorPosition(this.props.offset);
467
+ }
468
+ }
469
+
470
+ componentDidUpdate(prevProps: CypherEditorProps): void {
471
+ if (!this.editorView.current) {
472
+ return;
473
+ }
474
+
475
+ // Handle externally set value
476
+ const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
477
+
478
+ if (
479
+ this.props.value !== undefined && // If the component becomes uncontolled, we just leave the value as is
480
+ this.props.value !== prevProps.value && // The value prop has changed, we need to update the editor
481
+ this.props.value !== currentCmValue // No need to dispatch an update if the value is the same
482
+ ) {
483
+ this.editorView.current.dispatch({
484
+ changes: {
485
+ from: 0,
486
+ to: currentCmValue.length,
487
+ insert: this.props.value ?? '',
488
+ },
489
+ annotations: [ExternalEdit.of(true)],
490
+ });
491
+ }
492
+
493
+ // Handle theme change
494
+ const didChangeTheme =
495
+ prevProps.theme !== this.props.theme ||
496
+ prevProps.overrideThemeBackgroundColor !==
497
+ this.props.overrideThemeBackgroundColor;
498
+
499
+ if (didChangeTheme) {
500
+ this.editorView.current.dispatch({
501
+ effects: themeCompartment.reconfigure(
502
+ getThemeExtension(
503
+ this.props.theme,
504
+ this.props.overrideThemeBackgroundColor,
505
+ ),
506
+ ),
507
+ });
508
+ }
509
+
510
+ if (
511
+ prevProps.lineNumbers !== this.props.lineNumbers ||
512
+ prevProps.prompt !== this.props.prompt
513
+ ) {
514
+ this.editorView.current.dispatch({
515
+ effects: lineNumbersCompartment.reconfigure(
516
+ this.props.lineNumbers
517
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
518
+ : [],
519
+ ),
520
+ });
521
+ }
522
+
523
+ if (prevProps.readonly !== this.props.readonly) {
524
+ this.editorView.current.dispatch({
525
+ effects: readOnlyCompartment.reconfigure(
526
+ EditorState.readOnly.of(this.props.readonly),
527
+ ),
528
+ });
529
+ }
530
+
531
+ if (prevProps.placeholder !== this.props.placeholder) {
532
+ this.editorView.current.dispatch({
533
+ effects: placeholderCompartment.reconfigure(
534
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
535
+ ),
536
+ });
537
+ }
538
+
539
+ if (
540
+ prevProps.extraKeybindings !== this.props.extraKeybindings ||
541
+ prevProps.onExecute !== this.props.onExecute
542
+ ) {
543
+ this.editorView.current.dispatch({
544
+ effects: keyBindingCompartment.reconfigure(
545
+ keymap.of([
546
+ ...executeKeybinding(
547
+ this.props.onExecute,
548
+ this.props.newLineOnEnter,
549
+ () => this.debouncedOnChange?.flush(),
550
+ ),
551
+ ...this.props.extraKeybindings,
552
+ ]),
553
+ ),
554
+ });
555
+ }
556
+
557
+ if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
558
+ this.editorView.current.dispatch({
559
+ effects: domEventHandlerCompartment.reconfigure(
560
+ this.props.domEventHandlers
561
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
562
+ : [],
563
+ ),
564
+ });
565
+ }
566
+
567
+ // This component rerenders on every keystroke and comparing the
568
+ // full lists of editor strings on every render could be expensive.
569
+ const didChangeHistoryEstimate =
570
+ prevProps.history?.length !== this.props.history?.length ||
571
+ prevProps.history?.[0] !== this.props.history?.[0];
572
+
573
+ if (didChangeHistoryEstimate) {
574
+ this.editorView.current.dispatch({
575
+ effects: replaceHistory.of(this.props.history ?? []),
576
+ });
577
+ }
578
+
579
+ /*
580
+ The cypher configuration is a mutable object that is passed to the cypher language extension.
581
+ Much like how the schema based completions work in the official sql language extension.
582
+ https://github.com/codemirror/lang-sql/blob/4b7b2564dff7cdb1a15f8ccd08142f2cc8a0006f/src/sql.ts#L178C17-L178C18
583
+ */
584
+ this.schemaRef.current.schema = this.props.schema;
585
+ this.schemaRef.current.lint = this.props.lint;
586
+ this.schemaRef.current.featureFlags = this.props.featureFlags;
587
+ }
588
+
589
+ componentWillUnmount(): void {
590
+ this.editorView.current?.destroy();
591
+ cleanupWorkers();
592
+ }
593
+
594
+ render(): React.ReactNode {
595
+ const { className, theme } = this.props;
596
+
597
+ const themeClass =
598
+ typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme';
599
+
600
+ return (
601
+ <div
602
+ ref={this.editorContainer}
603
+ className={`${themeClass}${className ? ` ${className}` : ''}`}
604
+ />
605
+ );
606
+ }
607
+ }
@@ -0,0 +1 @@
1
+ export const DEBOUNCE_TIME = 200;