@neo4j-cypher/react-codemirror 2.0.0-next.2 → 2.0.0-next.20

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 (155) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/dist/{types/CypherEditor.d.ts → CypherEditor.d.ts} +82 -3
  3. package/dist/CypherEditor.js +326 -0
  4. package/dist/CypherEditor.js.map +1 -0
  5. package/dist/CypherEditor.test.js +151 -0
  6. package/dist/CypherEditor.test.js.map +1 -0
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/constants.js.map +1 -0
  10. package/dist/e2e_tests/autoCompletion.spec.js +318 -0
  11. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
  12. package/dist/e2e_tests/configuration.spec.js +83 -0
  13. package/dist/e2e_tests/configuration.spec.js.map +1 -0
  14. package/dist/e2e_tests/debounce.spec.js +66 -0
  15. package/dist/e2e_tests/debounce.spec.js.map +1 -0
  16. package/dist/{types/e2e_tests/e2e-utils.d.ts → e2e_tests/e2eUtils.d.ts} +1 -0
  17. package/dist/e2e_tests/e2eUtils.js +71 -0
  18. package/dist/e2e_tests/e2eUtils.js.map +1 -0
  19. package/dist/e2e_tests/extraKeybindings.spec.js +43 -0
  20. package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
  21. package/dist/e2e_tests/historyNavigation.spec.js +227 -0
  22. package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
  23. package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
  24. package/dist/e2e_tests/performanceTest.spec.js +97 -0
  25. package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
  26. package/dist/e2e_tests/sanityChecks.spec.js +53 -0
  27. package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
  28. package/dist/e2e_tests/signatureHelp.spec.js +228 -0
  29. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
  30. package/dist/e2e_tests/snippets.spec.js +62 -0
  31. package/dist/e2e_tests/snippets.spec.js.map +1 -0
  32. package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  33. package/dist/e2e_tests/syntaxHighlighting.spec.js +90 -0
  34. package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  35. package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  36. package/dist/e2e_tests/syntaxValidation.spec.js +116 -0
  37. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
  38. package/dist/historyNavigation.js +163 -0
  39. package/dist/historyNavigation.js.map +1 -0
  40. package/dist/{types/icons.d.ts → icons.d.ts} +1 -1
  41. package/dist/icons.js +62 -0
  42. package/dist/icons.js.map +1 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.js +5 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/lang-cypher/autocomplete.d.ts +6 -0
  47. package/dist/lang-cypher/autocomplete.js +115 -0
  48. package/dist/lang-cypher/autocomplete.js.map +1 -0
  49. package/dist/{types/lang-cypher → lang-cypher}/constants.d.ts +11 -0
  50. package/dist/lang-cypher/constants.js +69 -0
  51. package/dist/lang-cypher/constants.js.map +1 -0
  52. package/dist/lang-cypher/contants.test.d.ts +1 -0
  53. package/dist/lang-cypher/contants.test.js +103 -0
  54. package/dist/lang-cypher/contants.test.js.map +1 -0
  55. package/dist/lang-cypher/createCypherTheme.js +182 -0
  56. package/dist/lang-cypher/createCypherTheme.js.map +1 -0
  57. package/dist/lang-cypher/langCypher.d.ts +14 -0
  58. package/dist/lang-cypher/langCypher.js +23 -0
  59. package/dist/lang-cypher/langCypher.js.map +1 -0
  60. package/dist/lang-cypher/lintWorker.d.ts +12 -0
  61. package/dist/lang-cypher/lintWorker.js +14 -0
  62. package/dist/lang-cypher/lintWorker.js.map +1 -0
  63. package/dist/lang-cypher/parser-adapter.d.ts +19 -0
  64. package/dist/lang-cypher/parser-adapter.js +113 -0
  65. package/dist/lang-cypher/parser-adapter.js.map +1 -0
  66. package/dist/lang-cypher/signatureHelp.d.ts +4 -0
  67. package/dist/lang-cypher/signatureHelp.js +109 -0
  68. package/dist/lang-cypher/signatureHelp.js.map +1 -0
  69. package/dist/{types/lang-cypher/syntax-validation.d.ts → lang-cypher/syntaxValidation.d.ts} +2 -1
  70. package/dist/lang-cypher/syntaxValidation.js +52 -0
  71. package/dist/lang-cypher/syntaxValidation.js.map +1 -0
  72. package/dist/lang-cypher/themeIcons.js +22 -0
  73. package/dist/lang-cypher/themeIcons.js.map +1 -0
  74. package/dist/lang-cypher/utils.d.ts +2 -0
  75. package/dist/lang-cypher/utils.js +10 -0
  76. package/dist/lang-cypher/utils.js.map +1 -0
  77. package/dist/ndlTokensCopy.d.ts +570 -0
  78. package/dist/ndlTokensCopy.js +571 -0
  79. package/dist/ndlTokensCopy.js.map +1 -0
  80. package/dist/ndlTokensCopy.test.d.ts +1 -0
  81. package/dist/ndlTokensCopy.test.js +12 -0
  82. package/dist/ndlTokensCopy.test.js.map +1 -0
  83. package/dist/neo4jSetup.d.ts +6 -0
  84. package/dist/neo4jSetup.js +120 -0
  85. package/dist/neo4jSetup.js.map +1 -0
  86. package/dist/{types/themes.d.ts → themes.d.ts} +1 -1
  87. package/dist/themes.js +93 -0
  88. package/dist/themes.js.map +1 -0
  89. package/dist/tsconfig.tsbuildinfo +1 -0
  90. package/package.json +32 -34
  91. package/src/CypherEditor.test.tsx +200 -0
  92. package/src/CypherEditor.tsx +311 -41
  93. package/src/constants.ts +1 -0
  94. package/src/e2e_tests/autoCompletion.spec.tsx +546 -0
  95. package/src/e2e_tests/configuration.spec.tsx +111 -0
  96. package/src/e2e_tests/debounce.spec.tsx +106 -0
  97. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +24 -2
  98. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  99. package/src/e2e_tests/{history-navigation.spec.tsx → historyNavigation.spec.tsx} +137 -18
  100. package/src/e2e_tests/performanceTest.spec.tsx +163 -0
  101. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
  102. package/src/e2e_tests/signatureHelp.spec.tsx +445 -0
  103. package/src/e2e_tests/snippets.spec.tsx +92 -0
  104. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +26 -24
  105. package/src/e2e_tests/{syntax-validation.spec.tsx → syntaxValidation.spec.tsx} +97 -10
  106. package/src/{history-navigation.ts → historyNavigation.ts} +1 -1
  107. package/src/icons.ts +3 -0
  108. package/src/index.ts +2 -2
  109. package/src/lang-cypher/autocomplete.ts +100 -18
  110. package/src/lang-cypher/constants.ts +27 -0
  111. package/src/lang-cypher/contants.test.ts +6 -2
  112. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +44 -2
  113. package/src/lang-cypher/langCypher.ts +43 -0
  114. package/src/lang-cypher/lintWorker.ts +31 -0
  115. package/src/lang-cypher/parser-adapter.ts +145 -0
  116. package/src/lang-cypher/signatureHelp.ts +151 -0
  117. package/src/lang-cypher/syntaxValidation.ts +66 -0
  118. package/src/lang-cypher/utils.ts +9 -0
  119. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  120. package/src/ndlTokensCopy.ts +570 -0
  121. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +78 -17
  122. package/src/themes.ts +45 -70
  123. package/src/viteEnv.d.ts +1 -0
  124. package/dist/cjs/index.cjs +0 -1440
  125. package/dist/cjs/index.cjs.map +0 -7
  126. package/dist/esm/index.mjs +0 -1463
  127. package/dist/esm/index.mjs.map +0 -7
  128. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  129. package/dist/types/index.d.ts +0 -4
  130. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  131. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  132. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  133. package/dist/types/ndl-tokens-copy.d.ts +0 -379
  134. package/dist/types/neo4j-setup.d.ts +0 -2
  135. package/dist/types/tsconfig.tsbuildinfo +0 -1
  136. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  137. package/src/e2e_tests/mock-data.ts +0 -4310
  138. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  139. package/src/lang-cypher/ParserAdapter.ts +0 -92
  140. package/src/lang-cypher/lang-cypher.ts +0 -32
  141. package/src/lang-cypher/syntax-validation.ts +0 -24
  142. package/src/ndl-tokens-copy.ts +0 -379
  143. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → CypherEditor.test.d.ts} +0 -0
  144. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → e2e_tests/autoCompletion.spec.d.ts} +0 -0
  145. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → e2e_tests/configuration.spec.d.ts} +0 -0
  146. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → e2e_tests/debounce.spec.d.ts} +0 -0
  147. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  148. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → e2e_tests/historyNavigation.spec.d.ts} +0 -0
  149. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → e2e_tests/sanityChecks.spec.d.ts} +0 -0
  150. /package/dist/{types/lang-cypher/contants.test.d.ts → e2e_tests/signatureHelp.spec.d.ts} +0 -0
  151. /package/dist/{types/ndl-tokens-copy.test.d.ts → e2e_tests/snippets.spec.d.ts} +0 -0
  152. /package/dist/{types/history-navigation.d.ts → historyNavigation.d.ts} +0 -0
  153. /package/dist/{types/lang-cypher/create-cypher-theme.d.ts → lang-cypher/createCypherTheme.d.ts} +0 -0
  154. /package/dist/{types/lang-cypher/theme-icons.d.ts → lang-cypher/themeIcons.d.ts} +0 -0
  155. /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
@@ -0,0 +1,200 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { EditorView } from '@codemirror/view';
4
+ import { createRef } from 'react';
5
+ import { createRoot } from 'react-dom/client';
6
+ import { act } from 'react-dom/test-utils';
7
+ import { afterEach, beforeEach, expect, test, vi } from 'vitest';
8
+ import { DEBOUNCE_TIME } from './constants';
9
+ import { CypherEditor } from './CypherEditor';
10
+
11
+ const container = document.createElement('div');
12
+ let root: ReturnType<typeof createRoot>;
13
+
14
+ const ref = createRef<CypherEditor>();
15
+ let value = '';
16
+ const onChange = vi.fn((v: string) => {
17
+ value = v;
18
+ rerender();
19
+ });
20
+
21
+ global.IS_REACT_ACT_ENVIRONMENT = true;
22
+ const DEBOUNCE_TIME_WITH_MARGIN = DEBOUNCE_TIME + 100;
23
+
24
+ /** Avoids crash in test environment */
25
+ function mockEditorView(editorView: EditorView) {
26
+ editorView.coordsAtPos = vi.fn().mockReturnValue({
27
+ left: 0,
28
+ top: 0,
29
+ right: 0,
30
+ bottom: 0,
31
+ });
32
+ }
33
+
34
+ async function debounce() {
35
+ await new Promise((resolve) =>
36
+ setTimeout(resolve, DEBOUNCE_TIME_WITH_MARGIN),
37
+ );
38
+ }
39
+
40
+ function getEditorValue() {
41
+ return ref.current.editorView.current.state.doc.toString();
42
+ }
43
+
44
+ function rerender() {
45
+ act(() => {
46
+ root.render(<CypherEditor value={value} onChange={onChange} ref={ref} />);
47
+ });
48
+ }
49
+
50
+ beforeEach(() => {
51
+ root = createRoot(container);
52
+ act(() => {
53
+ root.render(<CypherEditor value={value} onChange={onChange} ref={ref} />);
54
+ });
55
+ mockEditorView(ref.current.editorView.current);
56
+ });
57
+
58
+ afterEach(() => {
59
+ act(() => {
60
+ root.unmount();
61
+ });
62
+ value = '';
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ test('editorValue updates props.value after debounce', async () => {
67
+ ref.current.setValueAndFocus('new value');
68
+ expect(onChange).not.toHaveBeenCalled();
69
+ expect(getEditorValue()).toBe('new value');
70
+ expect(value).toBe('');
71
+ await debounce();
72
+
73
+ expect(onChange).toHaveBeenCalledOnce();
74
+ expect(getEditorValue()).toBe('new value');
75
+ expect(value).toBe('new value');
76
+ });
77
+
78
+ test('editorValue updates should not be applied twice', async () => {
79
+ const dispatch = vi.spyOn(ref.current.editorView.current, 'dispatch');
80
+
81
+ ref.current.setValueAndFocus('new value');
82
+ await debounce();
83
+
84
+ expect(onChange).toHaveBeenCalledOnce();
85
+ expect(dispatch).toHaveBeenCalledOnce(); // it gets called once for the initial setValueAndFocus
86
+ });
87
+
88
+ test('props.value updates editorValue', () => {
89
+ value = 'external value';
90
+ rerender();
91
+
92
+ expect(getEditorValue()).toBe('external value');
93
+ expect(value).toBe('external value');
94
+ });
95
+
96
+ test('props.value set to undefined preserves editorValue', () => {
97
+ // 1. value is set initially
98
+ value = 'initial';
99
+ rerender();
100
+
101
+ // 2. value is set to undefined
102
+ value = undefined;
103
+ rerender();
104
+
105
+ expect(onChange).not.toHaveBeenCalled();
106
+ expect(value).toBeUndefined();
107
+ expect(getEditorValue()).toBe('initial');
108
+ });
109
+
110
+ // value updates from outside onExecute are overwritten by pending updates
111
+ test.fails('new props.value should cancel onChange', async () => {
112
+ // 1. value is updated internally
113
+ ref.current.setValueAndFocus('update');
114
+
115
+ // 2. editor is rerendered with a new value while a value update is still pending
116
+ value = 'new external value';
117
+ rerender();
118
+
119
+ await debounce();
120
+
121
+ // expect(onChange).not.toHaveBeenCalled();
122
+ expect(getEditorValue()).toBe('new external value');
123
+ expect(value).toBe('new external value');
124
+ });
125
+
126
+ // value updates from outside onExecute are overwritten by pending updates
127
+ test.fails(
128
+ 'new props.value set to same value should cancel onChange',
129
+ async () => {
130
+ // 1. value is set initially
131
+ value = 'same value';
132
+ rerender();
133
+
134
+ // 2. value is updated internally
135
+ ref.current.setValueAndFocus('update');
136
+
137
+ // 3. editor is rerendered with a new value while a value update is still pending
138
+ value = 'same value';
139
+ rerender();
140
+
141
+ await debounce();
142
+
143
+ // expect(onChange).not.toHaveBeenCalled();
144
+ expect(getEditorValue()).toBe('same value');
145
+ expect(value).toBe('same value');
146
+ },
147
+ );
148
+
149
+ test('rerender should not cancel onChange', async () => {
150
+ // 1. value is updated internally
151
+ ref.current.setValueAndFocus('changed');
152
+
153
+ // 2. editor is rerendered while a value update is still pending
154
+ rerender();
155
+
156
+ await debounce();
157
+
158
+ expect(onChange).toHaveBeenCalledOnce();
159
+ expect(getEditorValue()).toBe('changed');
160
+ expect(value).toBe('changed');
161
+ });
162
+
163
+ test('rerender with a previous update should not cancel onChange', async () => {
164
+ // 1. value is updated internally
165
+ ref.current.setValueAndFocus('changed');
166
+ await debounce();
167
+
168
+ // 2. value is updated internally again
169
+ ref.current.setValueAndFocus('new change');
170
+
171
+ // 3. editor is rerendered while a value update is still pending
172
+ rerender();
173
+
174
+ await debounce();
175
+
176
+ expect(onChange).toHaveBeenCalledTimes(2);
177
+ expect(getEditorValue()).toBe('new change');
178
+ expect(value).toBe('new change');
179
+ });
180
+
181
+ test('rerender with prior external update should not cancel onChange', async () => {
182
+ // 1. value is set initially
183
+ ref.current.setValueAndFocus('initial');
184
+ await debounce();
185
+
186
+ // 2. value is updated externally
187
+ value = 'external update';
188
+ rerender();
189
+
190
+ // 3. value is updated internally
191
+ ref.current.setValueAndFocus('internal update');
192
+
193
+ // 4. editor is rerendered while a value update is still pending
194
+ rerender();
195
+
196
+ await debounce();
197
+
198
+ expect(getEditorValue()).toBe('internal update');
199
+ expect(value).toBe('internal update');
200
+ });
@@ -1,3 +1,4 @@
1
+ import { insertNewline } from '@codemirror/commands';
1
2
  import {
2
3
  Annotation,
3
4
  Compartment,
@@ -9,18 +10,27 @@ 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';
16
+ import {
17
+ formatQuery,
18
+ _internalFeatureFlags,
19
+ type DbSchema,
20
+ } from '@neo4j-cypher/language-support';
21
+ import debounce from 'lodash.debounce';
15
22
  import { Component, createRef } from 'react';
23
+ import { DEBOUNCE_TIME } from './constants';
16
24
  import {
17
25
  replaceHistory,
18
26
  replMode as historyNavigation,
19
- } from './history-navigation';
20
- import { cypher, CypherConfig } from './lang-cypher/lang-cypher';
21
- import { basicNeo4jSetup } from './neo4j-setup';
27
+ } from './historyNavigation';
28
+ import { cypher, CypherConfig } from './lang-cypher/langCypher';
29
+ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
30
+ import { basicNeo4jSetup } from './neo4jSetup';
22
31
  import { getThemeExtension } from './themes';
23
32
 
33
+ type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
24
34
  export interface CypherEditorProps {
25
35
  /**
26
36
  * The prompt to show on single line editors
@@ -40,7 +50,14 @@ export interface CypherEditorProps {
40
50
  */
41
51
  onExecute?: (cmd: string) => void;
42
52
  /**
43
- * The editor history navigateable via up/down arrow keys. Order newest to oldest.
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.
44
61
  * Add to this list with the `onExecute` callback for REPL style history.
45
62
  */
46
63
  history?: string[];
@@ -57,6 +74,10 @@ export interface CypherEditorProps {
57
74
  * @default false
58
75
  */
59
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;
60
81
  /**
61
82
  * Whether the editor should wrap lines.
62
83
  *
@@ -69,6 +90,21 @@ export interface CypherEditorProps {
69
90
  * @default true
70
91
  */
71
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
+ };
72
108
  /**
73
109
  * The schema to use for autocompletion and linting.
74
110
  *
@@ -96,32 +132,156 @@ export interface CypherEditorProps {
96
132
  * @param {ViewUpdate} viewUpdate - the view update from codemirror
97
133
  */
98
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;
99
181
  }
100
182
 
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
- }
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
+ }
113
251
 
114
- return true;
115
- },
252
+ return true;
116
253
  },
117
- ]
118
- : [];
254
+ };
255
+ }
256
+ }
257
+
258
+ return Object.values(keybindings);
259
+ };
119
260
 
120
261
  const themeCompartment = new Compartment();
121
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 };
122
278
 
123
279
  const ExternalEdit = Annotation.define<boolean>();
124
- export class CypherEditor extends Component<CypherEditorProps> {
280
+
281
+ export class CypherEditor extends Component<
282
+ CypherEditorProps,
283
+ CypherEditorState
284
+ > {
125
285
  /**
126
286
  * The codemirror editor container.
127
287
  */
@@ -136,6 +296,13 @@ export class CypherEditor extends Component<CypherEditorProps> {
136
296
  editorView: React.MutableRefObject<EditorView> = createRef();
137
297
  private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
138
298
 
299
+ /**
300
+ * Format Cypher query
301
+ */
302
+ format() {
303
+ format(this.editorView.current);
304
+ }
305
+
139
306
  /**
140
307
  * Focus the editor
141
308
  */
@@ -174,11 +341,24 @@ export class CypherEditor extends Component<CypherEditorProps> {
174
341
  schema: {},
175
342
  overrideThemeBackgroundColor: false,
176
343
  lineWrap: false,
344
+ showSignatureTooltipBelow: true,
177
345
  extraKeybindings: [],
178
346
  history: [],
179
347
  theme: 'light',
348
+ lineNumbers: true,
349
+ newLineOnEnter: false,
350
+ moveFocusOnTab: false,
180
351
  };
181
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
+
182
362
  componentDidMount(): void {
183
363
  const {
184
364
  theme,
@@ -187,18 +367,36 @@ export class CypherEditor extends Component<CypherEditorProps> {
187
367
  overrideThemeBackgroundColor,
188
368
  schema,
189
369
  lint,
190
- onChange,
370
+ showSignatureTooltipBelow,
371
+ featureFlags,
191
372
  onExecute,
373
+ newLineOnEnter,
192
374
  } = this.props;
193
375
 
194
- this.schemaRef.current = { schema, lint };
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
+ };
195
393
 
196
394
  const themeExtension = getThemeExtension(
197
395
  theme,
198
396
  overrideThemeBackgroundColor,
199
397
  );
200
398
 
201
- const changeListener = onChange
399
+ const changeListener = this.debouncedOnChange
202
400
  ? [
203
401
  EditorView.updateListener.of((upt: ViewUpdate) => {
204
402
  const wasUserEdit = !upt.transactions.some((tr) =>
@@ -208,33 +406,48 @@ export class CypherEditor extends Component<CypherEditorProps> {
208
406
  if (upt.docChanged && wasUserEdit) {
209
407
  const doc = upt.state.doc;
210
408
  const value = doc.toString();
211
- onChange(value, upt);
409
+ this.debouncedOnChange(value, upt);
212
410
  }
213
411
  }),
214
412
  ]
215
413
  : [];
216
-
414
+
217
415
  this.editorState.current = EditorState.create({
218
416
  extensions: [
219
417
  keyBindingCompartment.of(
220
- keymap.of([...executeKeybinding(onExecute), ...extraKeybindings]),
418
+ keymap.of([
419
+ ...executeKeybinding(onExecute, newLineOnEnter, () =>
420
+ this.debouncedOnChange?.flush(),
421
+ ),
422
+ ...extraKeybindings,
423
+ ]),
221
424
  ),
222
425
  historyNavigation(this.props),
223
- basicNeo4jSetup(),
426
+ basicNeo4jSetup(this.props),
224
427
  themeCompartment.of(themeExtension),
225
428
  changeListener,
226
429
  cypher(this.schemaRef.current),
227
430
  lineWrap ? EditorView.lineWrapping : [],
228
431
 
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
- }),
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
+ : [],
238
451
  ],
239
452
  doc: this.props.value,
240
453
  });
@@ -249,6 +462,8 @@ export class CypherEditor extends Component<CypherEditorProps> {
249
462
  if (this.props.value) {
250
463
  this.updateCursorPosition(this.props.value.length);
251
464
  }
465
+ } else if (this.props.offset) {
466
+ this.updateCursorPosition(this.props.offset);
252
467
  }
253
468
  }
254
469
 
@@ -260,7 +475,11 @@ export class CypherEditor extends Component<CypherEditorProps> {
260
475
  // Handle externally set value
261
476
  const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
262
477
 
263
- if (this.props.value !== undefined && currentCmValue !== this.props.value) {
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
+ ) {
264
483
  this.editorView.current.dispatch({
265
484
  changes: {
266
485
  from: 0,
@@ -288,6 +507,35 @@ export class CypherEditor extends Component<CypherEditorProps> {
288
507
  });
289
508
  }
290
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
+
291
539
  if (
292
540
  prevProps.extraKeybindings !== this.props.extraKeybindings ||
293
541
  prevProps.onExecute !== this.props.onExecute
@@ -295,14 +543,34 @@ export class CypherEditor extends Component<CypherEditorProps> {
295
543
  this.editorView.current.dispatch({
296
544
  effects: keyBindingCompartment.reconfigure(
297
545
  keymap.of([
298
- ...executeKeybinding(this.props.onExecute),
546
+ ...executeKeybinding(
547
+ this.props.onExecute,
548
+ this.props.newLineOnEnter,
549
+ () => this.debouncedOnChange?.flush(),
550
+ ),
299
551
  ...this.props.extraKeybindings,
300
552
  ]),
301
553
  ),
302
554
  });
303
555
  }
304
556
 
305
- if (prevProps.history?.length !== this.props.history?.length) {
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) {
306
574
  this.editorView.current.dispatch({
307
575
  effects: replaceHistory.of(this.props.history ?? []),
308
576
  });
@@ -315,10 +583,12 @@ export class CypherEditor extends Component<CypherEditorProps> {
315
583
  */
316
584
  this.schemaRef.current.schema = this.props.schema;
317
585
  this.schemaRef.current.lint = this.props.lint;
586
+ this.schemaRef.current.featureFlags = this.props.featureFlags;
318
587
  }
319
588
 
320
589
  componentWillUnmount(): void {
321
590
  this.editorView.current?.destroy();
591
+ cleanupWorkers();
322
592
  }
323
593
 
324
594
  render(): React.ReactNode {
@@ -0,0 +1 @@
1
+ export const DEBOUNCE_TIME = 200;