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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +4 -1
  3. package/dist/{types/CypherEditor.d.ts → CypherEditor.d.ts} +69 -8
  4. package/dist/CypherEditor.js +300 -0
  5. package/dist/CypherEditor.js.map +1 -0
  6. package/dist/CypherEditor.test.js +151 -0
  7. package/dist/CypherEditor.test.js.map +1 -0
  8. package/dist/constants.d.ts +1 -0
  9. package/dist/constants.js +2 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/e2e_tests/autoCompletion.spec.js +267 -0
  12. package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
  13. package/dist/e2e_tests/configuration.spec.js +83 -0
  14. package/dist/e2e_tests/configuration.spec.js.map +1 -0
  15. package/dist/e2e_tests/debounce.spec.js +65 -0
  16. package/dist/e2e_tests/debounce.spec.js.map +1 -0
  17. package/dist/e2e_tests/e2eUtils.js +60 -0
  18. package/dist/e2e_tests/e2eUtils.js.map +1 -0
  19. package/dist/e2e_tests/extraKeybindings.spec.js +43 -0
  20. package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
  21. package/dist/e2e_tests/historyNavigation.spec.js +227 -0
  22. package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
  23. package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
  24. package/dist/e2e_tests/performanceTest.spec.js +96 -0
  25. package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
  26. package/dist/e2e_tests/sanityChecks.spec.js +53 -0
  27. package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
  28. package/dist/e2e_tests/signatureHelp.spec.js +179 -0
  29. package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
  30. package/dist/e2e_tests/snippets.spec.js +62 -0
  31. package/dist/e2e_tests/snippets.spec.js.map +1 -0
  32. package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  33. package/dist/e2e_tests/syntaxHighlighting.spec.js +90 -0
  34. package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  35. package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  36. package/dist/e2e_tests/syntaxValidation.spec.js +79 -0
  37. package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
  38. package/dist/historyNavigation.d.ts +7 -0
  39. package/dist/historyNavigation.js +163 -0
  40. package/dist/historyNavigation.js.map +1 -0
  41. package/dist/{types/icons.d.ts → icons.d.ts} +1 -1
  42. package/dist/icons.js +62 -0
  43. package/dist/icons.js.map +1 -0
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lang-cypher/autocomplete.d.ts +6 -0
  48. package/dist/lang-cypher/autocomplete.js +124 -0
  49. package/dist/lang-cypher/autocomplete.js.map +1 -0
  50. package/dist/{types/lang-cypher → lang-cypher}/constants.d.ts +9 -0
  51. package/dist/lang-cypher/constants.js +65 -0
  52. package/dist/lang-cypher/constants.js.map +1 -0
  53. package/dist/lang-cypher/contants.test.d.ts +1 -0
  54. package/dist/lang-cypher/contants.test.js +103 -0
  55. package/dist/lang-cypher/contants.test.js.map +1 -0
  56. package/dist/lang-cypher/createCypherTheme.js +179 -0
  57. package/dist/lang-cypher/createCypherTheme.js.map +1 -0
  58. package/dist/lang-cypher/langCypher.d.ts +14 -0
  59. package/dist/lang-cypher/langCypher.js +30 -0
  60. package/dist/lang-cypher/langCypher.js.map +1 -0
  61. package/dist/lang-cypher/lintWorker.d.ts +8 -0
  62. package/dist/lang-cypher/lintWorker.js +4 -0
  63. package/dist/lang-cypher/lintWorker.js.map +1 -0
  64. package/dist/lang-cypher/parser-adapter.d.ts +19 -0
  65. package/dist/lang-cypher/parser-adapter.js +113 -0
  66. package/dist/lang-cypher/parser-adapter.js.map +1 -0
  67. package/dist/lang-cypher/signatureHelp.d.ts +4 -0
  68. package/dist/lang-cypher/signatureHelp.js +94 -0
  69. package/dist/lang-cypher/signatureHelp.js.map +1 -0
  70. package/dist/lang-cypher/syntaxValidation.d.ts +5 -0
  71. package/dist/lang-cypher/syntaxValidation.js +69 -0
  72. package/dist/lang-cypher/syntaxValidation.js.map +1 -0
  73. package/dist/lang-cypher/themeIcons.js +22 -0
  74. package/dist/lang-cypher/themeIcons.js.map +1 -0
  75. package/dist/lang-cypher/utils.d.ts +2 -0
  76. package/dist/lang-cypher/utils.js +10 -0
  77. package/dist/lang-cypher/utils.js.map +1 -0
  78. package/dist/ndlTokensCopy.js +380 -0
  79. package/dist/ndlTokensCopy.js.map +1 -0
  80. package/dist/ndlTokensCopy.test.d.ts +1 -0
  81. package/dist/ndlTokensCopy.test.js +12 -0
  82. package/dist/ndlTokensCopy.test.js.map +1 -0
  83. package/dist/neo4jSetup.js +120 -0
  84. package/dist/neo4jSetup.js.map +1 -0
  85. package/dist/{types/themes.d.ts → themes.d.ts} +1 -1
  86. package/dist/themes.js +114 -0
  87. package/dist/themes.js.map +1 -0
  88. package/dist/tsconfig.tsbuildinfo +1 -0
  89. package/package.json +30 -31
  90. package/src/CypherEditor.test.tsx +200 -0
  91. package/src/CypherEditor.tsx +292 -41
  92. package/src/constants.ts +1 -0
  93. package/src/e2e_tests/autoCompletion.spec.tsx +472 -0
  94. package/src/e2e_tests/configuration.spec.tsx +111 -0
  95. package/src/e2e_tests/debounce.spec.tsx +101 -0
  96. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +11 -1
  97. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  98. package/src/e2e_tests/historyNavigation.spec.tsx +315 -0
  99. package/src/e2e_tests/performanceTest.spec.tsx +158 -0
  100. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
  101. package/src/e2e_tests/signatureHelp.spec.tsx +380 -0
  102. package/src/e2e_tests/snippets.spec.tsx +92 -0
  103. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +1 -3
  104. package/src/e2e_tests/{syntax-validation.spec.tsx → syntaxValidation.spec.tsx} +6 -7
  105. package/src/{repl-mode.ts → historyNavigation.ts} +7 -30
  106. package/src/icons.ts +3 -0
  107. package/src/index.ts +5 -3
  108. package/src/lang-cypher/autocomplete.ts +107 -15
  109. package/src/lang-cypher/constants.ts +23 -0
  110. package/src/lang-cypher/contants.test.ts +6 -2
  111. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +41 -2
  112. package/src/lang-cypher/langCypher.ts +53 -0
  113. package/src/lang-cypher/lintWorker.ts +14 -0
  114. package/src/lang-cypher/parser-adapter.ts +145 -0
  115. package/src/lang-cypher/signatureHelp.ts +133 -0
  116. package/src/lang-cypher/syntaxValidation.ts +96 -0
  117. package/src/lang-cypher/utils.ts +9 -0
  118. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  119. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +51 -1
  120. package/src/themes.ts +4 -2
  121. package/src/viteEnv.d.ts +1 -0
  122. package/dist/cjs/index.cjs +0 -1455
  123. package/dist/cjs/index.cjs.map +0 -7
  124. package/dist/esm/index.mjs +0 -1468
  125. package/dist/esm/index.mjs.map +0 -7
  126. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  127. package/dist/types/index.d.ts +0 -5
  128. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  129. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  130. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  131. package/dist/types/lang-cypher/syntax-validation.d.ts +0 -3
  132. package/dist/types/repl-mode.d.ts +0 -8
  133. package/dist/types/tsconfig.tsbuildinfo +0 -1
  134. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  135. package/src/e2e_tests/history-navigation.spec.tsx +0 -144
  136. package/src/e2e_tests/mock-data.ts +0 -4310
  137. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  138. package/src/lang-cypher/ParserAdapter.ts +0 -92
  139. package/src/lang-cypher/lang-cypher.ts +0 -32
  140. package/src/lang-cypher/syntax-validation.ts +0 -24
  141. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → CypherEditor.test.d.ts} +0 -0
  142. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → e2e_tests/autoCompletion.spec.d.ts} +0 -0
  143. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → e2e_tests/configuration.spec.d.ts} +0 -0
  144. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → e2e_tests/debounce.spec.d.ts} +0 -0
  145. /package/dist/{types/e2e_tests/e2e-utils.d.ts → e2e_tests/e2eUtils.d.ts} +0 -0
  146. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  147. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → e2e_tests/historyNavigation.spec.d.ts} +0 -0
  148. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → e2e_tests/sanityChecks.spec.d.ts} +0 -0
  149. /package/dist/{types/lang-cypher/contants.test.d.ts → e2e_tests/signatureHelp.spec.d.ts} +0 -0
  150. /package/dist/{types/ndl-tokens-copy.test.d.ts → e2e_tests/snippets.spec.d.ts} +0 -0
  151. /package/dist/{types/lang-cypher/create-cypher-theme.d.ts → lang-cypher/createCypherTheme.d.ts} +0 -0
  152. /package/dist/{types/lang-cypher/theme-icons.d.ts → lang-cypher/themeIcons.d.ts} +0 -0
  153. /package/dist/{types/ndl-tokens-copy.d.ts → ndlTokensCopy.d.ts} +0 -0
  154. /package/dist/{types/neo4j-setup.d.ts → neo4jSetup.d.ts} +0 -0
  155. /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
  156. /package/src/{ndl-tokens-copy.ts → ndlTokensCopy.ts} +0 -0
@@ -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,15 +10,23 @@ 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 { type DbSchema } from '@neo4j-cypher/language-support';
17
+ import debounce from 'lodash.debounce';
15
18
  import { Component, createRef } from 'react';
16
- import { cypher, CypherConfig } from './lang-cypher/lang-cypher';
17
- import { basicNeo4jSetup } from './neo4j-setup';
18
- import { replMode } from './repl-mode';
19
+ import { DEBOUNCE_TIME } from './constants';
20
+ import {
21
+ replaceHistory,
22
+ replMode as historyNavigation,
23
+ } from './historyNavigation';
24
+ import { cypher, CypherConfig } from './lang-cypher/langCypher';
25
+ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
26
+ import { basicNeo4jSetup } from './neo4jSetup';
19
27
  import { getThemeExtension } from './themes';
20
28
 
29
+ type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
21
30
  export interface CypherEditorProps {
22
31
  /**
23
32
  * The prompt to show on single line editors
@@ -37,15 +46,17 @@ export interface CypherEditorProps {
37
46
  */
38
47
  onExecute?: (cmd: string) => void;
39
48
  /**
40
- * Seed the editor history with some initial history entries
41
- * Navigateable via up/down arrow keys
49
+ * If true, pressing enter will add a new line to the editor and cmd/ctrl + enter will execute the query.
50
+ * Otherwise pressing enter on a single line will execute the query.
51
+ *
52
+ * @default false
42
53
  */
43
- initialHistory?: string[];
54
+ newLineOnEnter?: boolean;
44
55
  /**
45
- * Callback when a new history entry is added, useful for keeping track of history
46
- * outside of the editor.
56
+ * The editor history navigable via up/down arrow keys. Order newest to oldest.
57
+ * Add to this list with the `onExecute` callback for REPL style history.
47
58
  */
48
- onNewHistoryEntry?: (historyEntry: string) => void;
59
+ history?: string[];
49
60
  /**
50
61
  * When set to `true` the editor will use the background color of the parent element.
51
62
  *
@@ -59,6 +70,10 @@ export interface CypherEditorProps {
59
70
  * @default false
60
71
  */
61
72
  autofocus?: boolean;
73
+ /**
74
+ * Where to place the cursor in the query. Cannot be enabled at the same time than autofocus
75
+ */
76
+ offset?: number;
62
77
  /**
63
78
  * Whether the editor should wrap lines.
64
79
  *
@@ -71,6 +86,21 @@ export interface CypherEditorProps {
71
86
  * @default true
72
87
  */
73
88
  lint?: boolean;
89
+ /**
90
+ * Whether the signature help tooltip should be shown below the text.
91
+ * If false, it will be shown above.
92
+ *
93
+ * @default true
94
+ */
95
+ showSignatureTooltipBelow?: boolean;
96
+ /**
97
+ * Internal feature flags for the editor. Don't use in production
98
+ *
99
+ */
100
+ featureFlags?: {
101
+ consoleCommands?: boolean;
102
+ signatureInfoOnAutoCompletions?: boolean;
103
+ };
74
104
  /**
75
105
  * The schema to use for autocompletion and linting.
76
106
  *
@@ -98,13 +128,130 @@ export interface CypherEditorProps {
98
128
  * @param {ViewUpdate} viewUpdate - the view update from codemirror
99
129
  */
100
130
  onChange?(value: string, viewUpdate: ViewUpdate): void;
131
+
132
+ /**
133
+ * Map of event handlers to add to the editor.
134
+ *
135
+ * Note that the props are compared by reference, meaning object defined inline
136
+ * will cause the editor to re-render (much like the style prop does in this example:
137
+ * <div style={{}} />
138
+ *
139
+ * Memoize the object if you want/need to avoid this.
140
+ *
141
+ * @example
142
+ * // listen to blur events
143
+ * <CypherEditor domEventHandlers={{blur: () => console.log("blur event fired")}} />
144
+ */
145
+ domEventHandlers?: DomEventHandlers;
146
+ /**
147
+ * Placeholder text to display when the editor is empty.
148
+ */
149
+ placeholder?: string;
150
+ /**
151
+ * Whether the editor should show line numbers.
152
+ *
153
+ * @default true
154
+ */
155
+ lineNumbers?: boolean;
156
+ /**
157
+ * Whether the editor is read-only.
158
+ *
159
+ * @default false
160
+ */
161
+ readonly?: boolean;
162
+
163
+ /**
164
+ * String value to assign to the aria-label attribute of the editor
165
+ */
166
+ ariaLabel?: string;
101
167
  }
102
168
 
169
+ const executeKeybinding = (
170
+ onExecute?: (cmd: string) => void,
171
+ newLineOnEnter?: boolean,
172
+ flush?: () => void,
173
+ ) => {
174
+ const keybindings: Record<string, KeyBinding> = {
175
+ 'Shift-Enter': {
176
+ key: 'Shift-Enter',
177
+ run: insertNewline,
178
+ },
179
+ 'Ctrl-Enter': {
180
+ key: 'Ctrl-Enter',
181
+ run: () => true,
182
+ },
183
+ Enter: {
184
+ key: 'Enter',
185
+ run: insertNewline,
186
+ },
187
+ };
188
+
189
+ if (onExecute) {
190
+ keybindings['Ctrl-Enter'] = {
191
+ key: 'Ctrl-Enter',
192
+ mac: 'Mod-Enter',
193
+ preventDefault: true,
194
+ run: (view: EditorView) => {
195
+ const doc = view.state.doc.toString();
196
+ if (doc.trim() !== '') {
197
+ flush?.();
198
+ onExecute(doc);
199
+ }
200
+
201
+ return true;
202
+ },
203
+ };
204
+
205
+ if (!newLineOnEnter) {
206
+ keybindings['Enter'] = {
207
+ key: 'Enter',
208
+ preventDefault: true,
209
+ run: (view: EditorView) => {
210
+ const doc = view.state.doc.toString();
211
+ if (doc.includes('\n')) {
212
+ // Returning false means the event will mark the event
213
+ // as not handled and the default behavior will be executed
214
+ return false;
215
+ }
216
+
217
+ if (doc.trim() !== '') {
218
+ flush?.();
219
+ onExecute(doc);
220
+ }
221
+
222
+ return true;
223
+ },
224
+ };
225
+ }
226
+ }
227
+
228
+ return Object.values(keybindings);
229
+ };
230
+
103
231
  const themeCompartment = new Compartment();
104
232
  const keyBindingCompartment = new Compartment();
233
+ const lineNumbersCompartment = new Compartment();
234
+ const readOnlyCompartment = new Compartment();
235
+ const placeholderCompartment = new Compartment();
236
+ const domEventHandlerCompartment = new Compartment();
237
+
238
+ const formatLineNumber =
239
+ (prompt?: string) => (a: number, state: EditorState) => {
240
+ if (state.doc.lines === 1 && prompt !== undefined) {
241
+ return prompt;
242
+ }
243
+
244
+ return a.toString();
245
+ };
246
+
247
+ type CypherEditorState = { cypherSupportEnabled: boolean };
105
248
 
106
249
  const ExternalEdit = Annotation.define<boolean>();
107
- export class CypherEditor extends Component<CypherEditorProps> {
250
+
251
+ export class CypherEditor extends Component<
252
+ CypherEditorProps,
253
+ CypherEditorState
254
+ > {
108
255
  /**
109
256
  * The codemirror editor container.
110
257
  */
@@ -157,41 +304,59 @@ export class CypherEditor extends Component<CypherEditorProps> {
157
304
  schema: {},
158
305
  overrideThemeBackgroundColor: false,
159
306
  lineWrap: false,
307
+ showSignatureTooltipBelow: true,
160
308
  extraKeybindings: [],
161
- initialHistory: [],
309
+ history: [],
162
310
  theme: 'light',
311
+ lineNumbers: true,
312
+ newLineOnEnter: false,
163
313
  };
164
314
 
315
+ private debouncedOnChange = this.props.onChange
316
+ ? debounce(
317
+ ((value, viewUpdate) => {
318
+ this.props.onChange(value, viewUpdate);
319
+ }) satisfies CypherEditorProps['onChange'],
320
+ DEBOUNCE_TIME,
321
+ )
322
+ : undefined;
323
+
165
324
  componentDidMount(): void {
166
325
  const {
167
326
  theme,
168
- onExecute,
169
- initialHistory,
170
- onNewHistoryEntry,
171
327
  extraKeybindings,
172
328
  lineWrap,
173
329
  overrideThemeBackgroundColor,
174
330
  schema,
175
331
  lint,
176
- onChange,
332
+ showSignatureTooltipBelow,
333
+ featureFlags,
334
+ onExecute,
335
+ newLineOnEnter,
177
336
  } = this.props;
178
337
 
179
- this.schemaRef.current = { schema, lint };
180
-
181
- const maybeReplMode = onExecute
182
- ? replMode({
183
- onExecute,
184
- initialHistory,
185
- onNewHistoryEntry,
186
- })
187
- : [];
338
+ this.schemaRef.current = {
339
+ schema,
340
+ lint,
341
+ showSignatureTooltipBelow,
342
+ featureFlags: {
343
+ consoleCommands: true,
344
+ ...featureFlags,
345
+ },
346
+ useLightVersion: false,
347
+ setUseLightVersion: (newVal) => {
348
+ if (this.schemaRef.current !== undefined) {
349
+ this.schemaRef.current.useLightVersion = newVal;
350
+ }
351
+ },
352
+ };
188
353
 
189
354
  const themeExtension = getThemeExtension(
190
355
  theme,
191
356
  overrideThemeBackgroundColor,
192
357
  );
193
358
 
194
- const changeListener = onChange
359
+ const changeListener = this.debouncedOnChange
195
360
  ? [
196
361
  EditorView.updateListener.of((upt: ViewUpdate) => {
197
362
  const wasUserEdit = !upt.transactions.some((tr) =>
@@ -201,7 +366,7 @@ export class CypherEditor extends Component<CypherEditorProps> {
201
366
  if (upt.docChanged && wasUserEdit) {
202
367
  const doc = upt.state.doc;
203
368
  const value = doc.toString();
204
- onChange(value, upt);
369
+ this.debouncedOnChange(value, upt);
205
370
  }
206
371
  }),
207
372
  ]
@@ -209,23 +374,40 @@ export class CypherEditor extends Component<CypherEditorProps> {
209
374
 
210
375
  this.editorState.current = EditorState.create({
211
376
  extensions: [
212
- maybeReplMode,
377
+ keyBindingCompartment.of(
378
+ keymap.of([
379
+ ...executeKeybinding(onExecute, newLineOnEnter, () =>
380
+ this.debouncedOnChange?.flush(),
381
+ ),
382
+ ...extraKeybindings,
383
+ ]),
384
+ ),
385
+ historyNavigation(this.props),
213
386
  basicNeo4jSetup(),
214
387
  themeCompartment.of(themeExtension),
215
388
  changeListener,
216
389
  cypher(this.schemaRef.current),
217
- keyBindingCompartment.of(keymap.of(extraKeybindings)),
218
390
  lineWrap ? EditorView.lineWrapping : [],
219
391
 
220
- lineNumbers({
221
- formatNumber: (a, state) => {
222
- if (state.doc.lines === 1 && this.props.prompt !== undefined) {
223
- return this.props.prompt;
224
- }
225
-
226
- return a.toString();
227
- },
228
- }),
392
+ lineNumbersCompartment.of(
393
+ this.props.lineNumbers
394
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
395
+ : [],
396
+ ),
397
+ readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
398
+ placeholderCompartment.of(
399
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
400
+ ),
401
+ domEventHandlerCompartment.of(
402
+ this.props.domEventHandlers
403
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
404
+ : [],
405
+ ),
406
+ this.props.ariaLabel
407
+ ? EditorView.contentAttributes.of({
408
+ 'aria-label': this.props.ariaLabel,
409
+ })
410
+ : [],
229
411
  ],
230
412
  doc: this.props.value,
231
413
  });
@@ -240,6 +422,8 @@ export class CypherEditor extends Component<CypherEditorProps> {
240
422
  if (this.props.value) {
241
423
  this.updateCursorPosition(this.props.value.length);
242
424
  }
425
+ } else if (this.props.offset) {
426
+ this.updateCursorPosition(this.props.offset);
243
427
  }
244
428
  }
245
429
 
@@ -251,7 +435,11 @@ export class CypherEditor extends Component<CypherEditorProps> {
251
435
  // Handle externally set value
252
436
  const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
253
437
 
254
- if (this.props.value !== undefined && currentCmValue !== this.props.value) {
438
+ if (
439
+ this.props.value !== undefined && // If the component becomes uncontolled, we just leave the value as is
440
+ this.props.value !== prevProps.value && // The value prop has changed, we need to update the editor
441
+ this.props.value !== currentCmValue // No need to dispatch an update if the value is the same
442
+ ) {
255
443
  this.editorView.current.dispatch({
256
444
  changes: {
257
445
  from: 0,
@@ -279,14 +467,75 @@ export class CypherEditor extends Component<CypherEditorProps> {
279
467
  });
280
468
  }
281
469
 
282
- if (prevProps.extraKeybindings !== this.props.extraKeybindings) {
470
+ if (
471
+ prevProps.lineNumbers !== this.props.lineNumbers ||
472
+ prevProps.prompt !== this.props.prompt
473
+ ) {
474
+ this.editorView.current.dispatch({
475
+ effects: lineNumbersCompartment.reconfigure(
476
+ this.props.lineNumbers
477
+ ? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
478
+ : [],
479
+ ),
480
+ });
481
+ }
482
+
483
+ if (prevProps.readonly !== this.props.readonly) {
484
+ this.editorView.current.dispatch({
485
+ effects: readOnlyCompartment.reconfigure(
486
+ EditorState.readOnly.of(this.props.readonly),
487
+ ),
488
+ });
489
+ }
490
+
491
+ if (prevProps.placeholder !== this.props.placeholder) {
492
+ this.editorView.current.dispatch({
493
+ effects: placeholderCompartment.reconfigure(
494
+ this.props.placeholder ? placeholder(this.props.placeholder) : [],
495
+ ),
496
+ });
497
+ }
498
+
499
+ if (
500
+ prevProps.extraKeybindings !== this.props.extraKeybindings ||
501
+ prevProps.onExecute !== this.props.onExecute
502
+ ) {
283
503
  this.editorView.current.dispatch({
284
504
  effects: keyBindingCompartment.reconfigure(
285
- keymap.of(this.props.extraKeybindings),
505
+ keymap.of([
506
+ ...executeKeybinding(
507
+ this.props.onExecute,
508
+ this.props.newLineOnEnter,
509
+ () => this.debouncedOnChange?.flush(),
510
+ ),
511
+ ...this.props.extraKeybindings,
512
+ ]),
513
+ ),
514
+ });
515
+ }
516
+
517
+ if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
518
+ this.editorView.current.dispatch({
519
+ effects: domEventHandlerCompartment.reconfigure(
520
+ this.props.domEventHandlers
521
+ ? EditorView.domEventHandlers(this.props.domEventHandlers)
522
+ : [],
286
523
  ),
287
524
  });
288
525
  }
289
526
 
527
+ // This component rerenders on every keystroke and comparing the
528
+ // full lists of editor strings on every render could be expensive.
529
+ const didChangeHistoryEstimate =
530
+ prevProps.history?.length !== this.props.history?.length ||
531
+ prevProps.history?.[0] !== this.props.history?.[0];
532
+
533
+ if (didChangeHistoryEstimate) {
534
+ this.editorView.current.dispatch({
535
+ effects: replaceHistory.of(this.props.history ?? []),
536
+ });
537
+ }
538
+
290
539
  /*
291
540
  The cypher configuration is a mutable object that is passed to the cypher language extension.
292
541
  Much like how the schema based completions work in the official sql language extension.
@@ -294,10 +543,12 @@ export class CypherEditor extends Component<CypherEditorProps> {
294
543
  */
295
544
  this.schemaRef.current.schema = this.props.schema;
296
545
  this.schemaRef.current.lint = this.props.lint;
546
+ this.schemaRef.current.featureFlags = this.props.featureFlags;
297
547
  }
298
548
 
299
549
  componentWillUnmount(): void {
300
550
  this.editorView.current?.destroy();
551
+ cleanupWorkers();
301
552
  }
302
553
 
303
554
  render(): React.ReactNode {
@@ -0,0 +1 @@
1
+ export const DEBOUNCE_TIME = 200;