@neo4j-cypher/react-codemirror 2.0.0-next.2 → 2.0.0-next.21
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 +174 -0
- package/dist/{types/CypherEditor.d.ts → CypherEditor.d.ts} +82 -3
- package/dist/CypherEditor.js +326 -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 +318 -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 +66 -0
- package/dist/e2e_tests/debounce.spec.js.map +1 -0
- package/dist/{types/e2e_tests/e2e-utils.d.ts → e2e_tests/e2eUtils.d.ts} +1 -0
- package/dist/e2e_tests/e2eUtils.js +71 -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 +97 -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 +228 -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 +116 -0
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -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 +115 -0
- package/dist/lang-cypher/autocomplete.js.map +1 -0
- package/dist/{types/lang-cypher → lang-cypher}/constants.d.ts +11 -0
- package/dist/lang-cypher/constants.js +69 -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 +182 -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 +23 -0
- package/dist/lang-cypher/langCypher.js.map +1 -0
- package/dist/lang-cypher/lintWorker.d.ts +12 -0
- package/dist/lang-cypher/lintWorker.js +14 -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 +109 -0
- package/dist/lang-cypher/signatureHelp.js.map +1 -0
- package/dist/{types/lang-cypher/syntax-validation.d.ts → lang-cypher/syntaxValidation.d.ts} +2 -1
- package/dist/lang-cypher/syntaxValidation.js +52 -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.d.ts +570 -0
- package/dist/ndlTokensCopy.js +571 -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.d.ts +6 -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 +93 -0
- package/dist/themes.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +32 -34
- package/src/CypherEditor.test.tsx +200 -0
- package/src/CypherEditor.tsx +311 -41
- package/src/constants.ts +1 -0
- package/src/e2e_tests/autoCompletion.spec.tsx +546 -0
- package/src/e2e_tests/configuration.spec.tsx +111 -0
- package/src/e2e_tests/debounce.spec.tsx +106 -0
- package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +24 -2
- package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
- package/src/e2e_tests/{history-navigation.spec.tsx → historyNavigation.spec.tsx} +137 -18
- package/src/e2e_tests/performanceTest.spec.tsx +163 -0
- package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
- package/src/e2e_tests/signatureHelp.spec.tsx +445 -0
- package/src/e2e_tests/snippets.spec.tsx +92 -0
- package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +26 -24
- package/src/e2e_tests/{syntax-validation.spec.tsx → syntaxValidation.spec.tsx} +97 -10
- package/src/{history-navigation.ts → historyNavigation.ts} +1 -1
- package/src/icons.ts +3 -0
- package/src/index.ts +2 -2
- package/src/lang-cypher/autocomplete.ts +100 -18
- package/src/lang-cypher/constants.ts +27 -0
- package/src/lang-cypher/contants.test.ts +6 -2
- package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +44 -2
- package/src/lang-cypher/langCypher.ts +43 -0
- package/src/lang-cypher/lintWorker.ts +31 -0
- package/src/lang-cypher/parser-adapter.ts +145 -0
- package/src/lang-cypher/signatureHelp.ts +151 -0
- package/src/lang-cypher/syntaxValidation.ts +66 -0
- package/src/lang-cypher/utils.ts +9 -0
- package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
- package/src/ndlTokensCopy.ts +570 -0
- package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +78 -17
- package/src/themes.ts +45 -70
- package/src/viteEnv.d.ts +1 -0
- package/dist/cjs/index.cjs +0 -1440
- package/dist/cjs/index.cjs.map +0 -7
- package/dist/esm/index.mjs +0 -1463
- 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 -4
- 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/ndl-tokens-copy.d.ts +0 -379
- package/dist/types/neo4j-setup.d.ts +0 -2
- package/dist/types/tsconfig.tsbuildinfo +0 -1
- package/src/e2e_tests/auto-completion.spec.tsx +0 -232
- 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/src/ndl-tokens-copy.ts +0 -379
- /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/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/history-navigation.d.ts → historyNavigation.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/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
|
+
});
|
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,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
|
|
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 './
|
|
20
|
-
import { cypher, CypherConfig } from './lang-cypher/
|
|
21
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
+
showSignatureTooltipBelow,
|
|
371
|
+
featureFlags,
|
|
191
372
|
onExecute,
|
|
373
|
+
newLineOnEnter,
|
|
192
374
|
} = this.props;
|
|
193
375
|
|
|
194
|
-
|
|
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 =
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 (
|
|
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(
|
|
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.
|
|
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 {
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEBOUNCE_TIME = 200;
|