@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.
- package/CHANGELOG.md +85 -0
- package/README.md +4 -1
- package/dist/{types/CypherEditor.d.ts → CypherEditor.d.ts} +69 -8
- package/dist/CypherEditor.js +300 -0
- package/dist/CypherEditor.js.map +1 -0
- package/dist/CypherEditor.test.js +151 -0
- package/dist/CypherEditor.test.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/e2e_tests/autoCompletion.spec.js +267 -0
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
- package/dist/e2e_tests/configuration.spec.js +83 -0
- package/dist/e2e_tests/configuration.spec.js.map +1 -0
- package/dist/e2e_tests/debounce.spec.js +65 -0
- package/dist/e2e_tests/debounce.spec.js.map +1 -0
- package/dist/e2e_tests/e2eUtils.js +60 -0
- package/dist/e2e_tests/e2eUtils.js.map +1 -0
- package/dist/e2e_tests/extraKeybindings.spec.js +43 -0
- package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
- package/dist/e2e_tests/historyNavigation.spec.js +227 -0
- package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
- package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
- package/dist/e2e_tests/performanceTest.spec.js +96 -0
- package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
- package/dist/e2e_tests/sanityChecks.spec.js +53 -0
- package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
- package/dist/e2e_tests/signatureHelp.spec.js +179 -0
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
- package/dist/e2e_tests/snippets.spec.js +62 -0
- package/dist/e2e_tests/snippets.spec.js.map +1 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.js +90 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
- package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
- package/dist/e2e_tests/syntaxValidation.spec.js +79 -0
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
- package/dist/historyNavigation.d.ts +7 -0
- package/dist/historyNavigation.js +163 -0
- package/dist/historyNavigation.js.map +1 -0
- package/dist/{types/icons.d.ts → icons.d.ts} +1 -1
- package/dist/icons.js +62 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lang-cypher/autocomplete.d.ts +6 -0
- package/dist/lang-cypher/autocomplete.js +124 -0
- package/dist/lang-cypher/autocomplete.js.map +1 -0
- package/dist/{types/lang-cypher → lang-cypher}/constants.d.ts +9 -0
- package/dist/lang-cypher/constants.js +65 -0
- package/dist/lang-cypher/constants.js.map +1 -0
- package/dist/lang-cypher/contants.test.d.ts +1 -0
- package/dist/lang-cypher/contants.test.js +103 -0
- package/dist/lang-cypher/contants.test.js.map +1 -0
- package/dist/lang-cypher/createCypherTheme.js +179 -0
- package/dist/lang-cypher/createCypherTheme.js.map +1 -0
- package/dist/lang-cypher/langCypher.d.ts +14 -0
- package/dist/lang-cypher/langCypher.js +30 -0
- package/dist/lang-cypher/langCypher.js.map +1 -0
- package/dist/lang-cypher/lintWorker.d.ts +8 -0
- package/dist/lang-cypher/lintWorker.js +4 -0
- package/dist/lang-cypher/lintWorker.js.map +1 -0
- package/dist/lang-cypher/parser-adapter.d.ts +19 -0
- package/dist/lang-cypher/parser-adapter.js +113 -0
- package/dist/lang-cypher/parser-adapter.js.map +1 -0
- package/dist/lang-cypher/signatureHelp.d.ts +4 -0
- package/dist/lang-cypher/signatureHelp.js +94 -0
- package/dist/lang-cypher/signatureHelp.js.map +1 -0
- package/dist/lang-cypher/syntaxValidation.d.ts +5 -0
- package/dist/lang-cypher/syntaxValidation.js +69 -0
- package/dist/lang-cypher/syntaxValidation.js.map +1 -0
- package/dist/lang-cypher/themeIcons.js +22 -0
- package/dist/lang-cypher/themeIcons.js.map +1 -0
- package/dist/lang-cypher/utils.d.ts +2 -0
- package/dist/lang-cypher/utils.js +10 -0
- package/dist/lang-cypher/utils.js.map +1 -0
- package/dist/ndlTokensCopy.js +380 -0
- package/dist/ndlTokensCopy.js.map +1 -0
- package/dist/ndlTokensCopy.test.d.ts +1 -0
- package/dist/ndlTokensCopy.test.js +12 -0
- package/dist/ndlTokensCopy.test.js.map +1 -0
- package/dist/neo4jSetup.js +120 -0
- package/dist/neo4jSetup.js.map +1 -0
- package/dist/{types/themes.d.ts → themes.d.ts} +1 -1
- package/dist/themes.js +114 -0
- package/dist/themes.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +30 -31
- package/src/CypherEditor.test.tsx +200 -0
- package/src/CypherEditor.tsx +292 -41
- package/src/constants.ts +1 -0
- package/src/e2e_tests/autoCompletion.spec.tsx +472 -0
- package/src/e2e_tests/configuration.spec.tsx +111 -0
- package/src/e2e_tests/debounce.spec.tsx +101 -0
- package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +11 -1
- package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
- package/src/e2e_tests/historyNavigation.spec.tsx +315 -0
- package/src/e2e_tests/performanceTest.spec.tsx +158 -0
- package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
- package/src/e2e_tests/signatureHelp.spec.tsx +380 -0
- package/src/e2e_tests/snippets.spec.tsx +92 -0
- package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +1 -3
- package/src/e2e_tests/{syntax-validation.spec.tsx → syntaxValidation.spec.tsx} +6 -7
- package/src/{repl-mode.ts → historyNavigation.ts} +7 -30
- package/src/icons.ts +3 -0
- package/src/index.ts +5 -3
- package/src/lang-cypher/autocomplete.ts +107 -15
- package/src/lang-cypher/constants.ts +23 -0
- package/src/lang-cypher/contants.test.ts +6 -2
- package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +41 -2
- package/src/lang-cypher/langCypher.ts +53 -0
- package/src/lang-cypher/lintWorker.ts +14 -0
- package/src/lang-cypher/parser-adapter.ts +145 -0
- package/src/lang-cypher/signatureHelp.ts +133 -0
- package/src/lang-cypher/syntaxValidation.ts +96 -0
- package/src/lang-cypher/utils.ts +9 -0
- package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
- package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +51 -1
- package/src/themes.ts +4 -2
- package/src/viteEnv.d.ts +1 -0
- package/dist/cjs/index.cjs +0 -1455
- package/dist/cjs/index.cjs.map +0 -7
- package/dist/esm/index.mjs +0 -1468
- package/dist/esm/index.mjs.map +0 -7
- package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
- package/dist/types/index.d.ts +0 -5
- package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
- package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
- package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
- package/dist/types/lang-cypher/syntax-validation.d.ts +0 -3
- package/dist/types/repl-mode.d.ts +0 -8
- package/dist/types/tsconfig.tsbuildinfo +0 -1
- package/src/e2e_tests/auto-completion.spec.tsx +0 -232
- package/src/e2e_tests/history-navigation.spec.tsx +0 -144
- package/src/e2e_tests/mock-data.ts +0 -4310
- package/src/e2e_tests/performance-test.spec.tsx +0 -71
- package/src/lang-cypher/ParserAdapter.ts +0 -92
- package/src/lang-cypher/lang-cypher.ts +0 -32
- package/src/lang-cypher/syntax-validation.ts +0 -24
- /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → CypherEditor.test.d.ts} +0 -0
- /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → e2e_tests/autoCompletion.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → e2e_tests/configuration.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/performance-test.spec.d.ts → e2e_tests/debounce.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/e2e-utils.d.ts → e2e_tests/e2eUtils.d.ts} +0 -0
- /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → e2e_tests/extraKeybindings.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → e2e_tests/historyNavigation.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → e2e_tests/sanityChecks.spec.d.ts} +0 -0
- /package/dist/{types/lang-cypher/contants.test.d.ts → e2e_tests/signatureHelp.spec.d.ts} +0 -0
- /package/dist/{types/ndl-tokens-copy.test.d.ts → e2e_tests/snippets.spec.d.ts} +0 -0
- /package/dist/{types/lang-cypher/create-cypher-theme.d.ts → lang-cypher/createCypherTheme.d.ts} +0 -0
- /package/dist/{types/lang-cypher/theme-icons.d.ts → lang-cypher/themeIcons.d.ts} +0 -0
- /package/dist/{types/ndl-tokens-copy.d.ts → ndlTokensCopy.d.ts} +0 -0
- /package/dist/{types/neo4j-setup.d.ts → neo4jSetup.d.ts} +0 -0
- /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
- /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
|
+
});
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -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
|
|
16
|
+
import { type DbSchema } from '@neo4j-cypher/language-support';
|
|
17
|
+
import debounce from 'lodash.debounce';
|
|
15
18
|
import { Component, createRef } from 'react';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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
|
-
|
|
54
|
+
newLineOnEnter?: boolean;
|
|
44
55
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
+
showSignatureTooltipBelow,
|
|
333
|
+
featureFlags,
|
|
334
|
+
onExecute,
|
|
335
|
+
newLineOnEnter,
|
|
177
336
|
} = this.props;
|
|
178
337
|
|
|
179
|
-
this.schemaRef.current = {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 {
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEBOUNCE_TIME = 200;
|