@neo4j-cypher/react-codemirror 2.0.0-alpha.0 → 2.0.0-canary-e8a1279
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 +68 -0
- package/LICENSE.md +201 -0
- package/README.md +27 -4
- package/dist/CypherEditor.d.ts +153 -0
- package/dist/CypherEditor.js +242 -0
- package/dist/CypherEditor.js.map +1 -0
- package/dist/e2e_tests/autoCompletion.spec.d.ts +1 -0
- package/dist/e2e_tests/autoCompletion.spec.js +133 -0
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
- package/dist/e2e_tests/configuration.spec.d.ts +1 -0
- package/dist/e2e_tests/configuration.spec.js +73 -0
- package/dist/e2e_tests/configuration.spec.js.map +1 -0
- package/dist/e2e_tests/e2eUtils.d.ts +12 -0
- package/dist/e2e_tests/e2eUtils.js +60 -0
- package/dist/e2e_tests/e2eUtils.js.map +1 -0
- package/dist/e2e_tests/extraKeybindings.spec.d.ts +1 -0
- package/dist/e2e_tests/extraKeybindings.spec.js +44 -0
- package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
- package/dist/e2e_tests/historyNavigation.spec.d.ts +1 -0
- package/dist/e2e_tests/historyNavigation.spec.js +136 -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.d.ts +1 -0
- package/dist/e2e_tests/sanityChecks.spec.js +56 -0
- package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
- package/dist/e2e_tests/signatureHelp.spec.d.ts +1 -0
- package/dist/e2e_tests/signatureHelp.spec.js +152 -0
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
- package/dist/e2e_tests/snippets.spec.d.ts +1 -0
- package/dist/e2e_tests/snippets.spec.js +63 -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 +91 -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/icons.d.ts +2 -0
- 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 +3 -0
- package/dist/lang-cypher/autocomplete.js +62 -0
- package/dist/lang-cypher/autocomplete.js.map +1 -0
- package/dist/lang-cypher/constants.d.ts +40 -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 +102 -0
- package/dist/lang-cypher/contants.test.js.map +1 -0
- package/dist/lang-cypher/createCypherTheme.d.ts +26 -0
- package/dist/lang-cypher/createCypherTheme.js +172 -0
- package/dist/lang-cypher/createCypherTheme.js.map +1 -0
- package/dist/lang-cypher/langCypher.d.ts +9 -0
- package/dist/lang-cypher/langCypher.js +24 -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 +93 -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 +71 -0
- package/dist/lang-cypher/syntaxValidation.js.map +1 -0
- package/dist/lang-cypher/themeIcons.d.ts +7 -0
- package/dist/lang-cypher/themeIcons.js +22 -0
- package/dist/lang-cypher/themeIcons.js.map +1 -0
- package/dist/ndlTokensCopy.d.ts +379 -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 +11 -0
- package/dist/ndlTokensCopy.test.js.map +1 -0
- package/dist/neo4jSetup.d.ts +2 -0
- package/dist/neo4jSetup.js +120 -0
- package/dist/neo4jSetup.js.map +1 -0
- package/dist/themes.d.ts +11 -0
- package/dist/themes.js +114 -0
- package/dist/themes.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +46 -16
- package/src/CypherEditor.tsx +461 -0
- package/src/e2e_tests/autoCompletion.spec.tsx +236 -0
- package/src/e2e_tests/configuration.spec.tsx +97 -0
- package/src/e2e_tests/e2eUtils.ts +85 -0
- package/src/e2e_tests/extraKeybindings.spec.tsx +57 -0
- package/src/e2e_tests/historyNavigation.spec.tsx +196 -0
- package/src/e2e_tests/performanceTest.spec.tsx +158 -0
- package/src/e2e_tests/sanityChecks.spec.tsx +78 -0
- package/src/e2e_tests/signatureHelp.spec.tsx +309 -0
- package/src/e2e_tests/snippets.spec.tsx +94 -0
- package/src/e2e_tests/syntaxHighlighting.spec.tsx +198 -0
- package/src/e2e_tests/syntaxValidation.spec.tsx +156 -0
- package/src/historyNavigation.ts +191 -0
- package/{esm/index.mjs → src/icons.ts} +37 -1283
- package/src/index.ts +4 -0
- package/src/lang-cypher/autocomplete.ts +81 -0
- package/src/lang-cypher/constants.ts +84 -0
- package/src/lang-cypher/contants.test.ts +104 -0
- package/src/lang-cypher/createCypherTheme.ts +240 -0
- package/src/lang-cypher/langCypher.ts +41 -0
- package/src/lang-cypher/lintWorker.ts +14 -0
- package/src/lang-cypher/parser-adapter.ts +145 -0
- package/src/lang-cypher/signatureHelp.ts +131 -0
- package/src/lang-cypher/syntaxValidation.ts +99 -0
- package/src/lang-cypher/themeIcons.ts +27 -0
- package/src/ndlTokensCopy.test.ts +11 -0
- package/src/ndlTokensCopy.ts +379 -0
- package/src/neo4jSetup.tsx +179 -0
- package/src/themes.ts +132 -0
- package/dist/index.cjs +0 -1330
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
Compartment,
|
|
4
|
+
EditorState,
|
|
5
|
+
Extension,
|
|
6
|
+
} from '@codemirror/state';
|
|
7
|
+
import {
|
|
8
|
+
EditorView,
|
|
9
|
+
KeyBinding,
|
|
10
|
+
keymap,
|
|
11
|
+
lineNumbers,
|
|
12
|
+
placeholder,
|
|
13
|
+
ViewUpdate,
|
|
14
|
+
} from '@codemirror/view';
|
|
15
|
+
import type { DbSchema } from '@neo4j-cypher/language-support';
|
|
16
|
+
import debounce from 'lodash.debounce';
|
|
17
|
+
import { Component, createRef } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
replaceHistory,
|
|
20
|
+
replMode as historyNavigation,
|
|
21
|
+
} from './historyNavigation';
|
|
22
|
+
import { cypher, CypherConfig } from './lang-cypher/langCypher';
|
|
23
|
+
import { cleanupWorkers } from './lang-cypher/syntaxValidation';
|
|
24
|
+
import { basicNeo4jSetup } from './neo4jSetup';
|
|
25
|
+
import { getThemeExtension } from './themes';
|
|
26
|
+
|
|
27
|
+
type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
|
|
28
|
+
export interface CypherEditorProps {
|
|
29
|
+
/**
|
|
30
|
+
* The prompt to show on single line editors
|
|
31
|
+
*/
|
|
32
|
+
prompt?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Custom keybindings to add to the editor.
|
|
35
|
+
* See https://codemirror.net/6/docs/ref/#keymap.of
|
|
36
|
+
*/
|
|
37
|
+
extraKeybindings?: KeyBinding[];
|
|
38
|
+
/**
|
|
39
|
+
* Callback on query execution, triggered via ctrl/cmd + Enter.
|
|
40
|
+
* If provided, will enable "repl-mode", which turns on navigating editor history
|
|
41
|
+
*
|
|
42
|
+
* @param cmd - the editor value when ctrl/cmd + enter was pressed
|
|
43
|
+
* @returns void
|
|
44
|
+
*/
|
|
45
|
+
onExecute?: (cmd: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* The editor history navigable via up/down arrow keys. Order newest to oldest.
|
|
48
|
+
* Add to this list with the `onExecute` callback for REPL style history.
|
|
49
|
+
*/
|
|
50
|
+
history?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* When set to `true` the editor will use the background color of the parent element.
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
overrideThemeBackgroundColor?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the editor should take focus on mount.
|
|
59
|
+
* Will move the cursor to the end of the query when provided with an initial value.
|
|
60
|
+
*
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
autofocus?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Where to place the cursor in the query. Cannot be enabled at the same time than autofocus
|
|
66
|
+
*/
|
|
67
|
+
offset?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Whether the editor should wrap lines.
|
|
70
|
+
*
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
lineWrap?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Whether the editor should perform syntax validation.
|
|
76
|
+
*
|
|
77
|
+
* @default true
|
|
78
|
+
*/
|
|
79
|
+
lint?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The schema to use for autocompletion and linting.
|
|
82
|
+
*
|
|
83
|
+
* @type {DbSchema}
|
|
84
|
+
*/
|
|
85
|
+
schema?: DbSchema;
|
|
86
|
+
/**
|
|
87
|
+
* The current value of the editor.
|
|
88
|
+
*/
|
|
89
|
+
value?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Extra css classnames to add to the editor container.
|
|
92
|
+
*/
|
|
93
|
+
className?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Set the built in theme or provide a custom theme.
|
|
96
|
+
*
|
|
97
|
+
* `light` / `dark` / `Extension`
|
|
98
|
+
* @default light
|
|
99
|
+
*/
|
|
100
|
+
theme?: 'light' | 'dark' | Extension;
|
|
101
|
+
/**
|
|
102
|
+
* Callback when the editor value changes.
|
|
103
|
+
* @param {string} value - the current editor value
|
|
104
|
+
* @param {ViewUpdate} viewUpdate - the view update from codemirror
|
|
105
|
+
*/
|
|
106
|
+
onChange?(value: string, viewUpdate: ViewUpdate): void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Map of event handlers to add to the editor.
|
|
110
|
+
*
|
|
111
|
+
* Note that the props are compared by reference, meaning object defined inline
|
|
112
|
+
* will cause the editor to re-render (much like the style prop does in this example:
|
|
113
|
+
* <div style={{}} />
|
|
114
|
+
*
|
|
115
|
+
* Memoize the object if you want/need to avoid this.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // listen to blur events
|
|
119
|
+
* <CypherEditor domEventHandlers={{blur: () => console.log("blur event fired")}} />
|
|
120
|
+
*/
|
|
121
|
+
domEventHandlers?: DomEventHandlers;
|
|
122
|
+
/**
|
|
123
|
+
* Placeholder text to display when the editor is empty.
|
|
124
|
+
*/
|
|
125
|
+
placeholder?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Whether the editor should show line numbers.
|
|
128
|
+
*
|
|
129
|
+
* @default true
|
|
130
|
+
*/
|
|
131
|
+
lineNumbers?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the editor is read-only.
|
|
134
|
+
*
|
|
135
|
+
* @default false
|
|
136
|
+
*/
|
|
137
|
+
readonly?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const executeKeybinding = (onExecute?: (cmd: string) => void) =>
|
|
141
|
+
onExecute
|
|
142
|
+
? [
|
|
143
|
+
{
|
|
144
|
+
key: 'Ctrl-Enter',
|
|
145
|
+
mac: 'Mod-Enter',
|
|
146
|
+
preventDefault: true,
|
|
147
|
+
run: (view: EditorView) => {
|
|
148
|
+
const doc = view.state.doc.toString();
|
|
149
|
+
if (doc.trim() !== '') {
|
|
150
|
+
onExecute(doc);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
]
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
const themeCompartment = new Compartment();
|
|
160
|
+
const keyBindingCompartment = new Compartment();
|
|
161
|
+
const lineNumbersCompartment = new Compartment();
|
|
162
|
+
const readOnlyCompartment = new Compartment();
|
|
163
|
+
const placeholderCompartment = new Compartment();
|
|
164
|
+
const domEventHandlerCompartment = new Compartment();
|
|
165
|
+
|
|
166
|
+
const formatLineNumber =
|
|
167
|
+
(prompt?: string) => (a: number, state: EditorState) => {
|
|
168
|
+
if (state.doc.lines === 1 && prompt !== undefined) {
|
|
169
|
+
return prompt;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return a.toString();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
type CypherEditorState = { cypherSupportEnabled: boolean };
|
|
176
|
+
|
|
177
|
+
const ExternalEdit = Annotation.define<boolean>();
|
|
178
|
+
|
|
179
|
+
export class CypherEditor extends Component<
|
|
180
|
+
CypherEditorProps,
|
|
181
|
+
CypherEditorState
|
|
182
|
+
> {
|
|
183
|
+
/**
|
|
184
|
+
* The codemirror editor container.
|
|
185
|
+
*/
|
|
186
|
+
editorContainer: React.RefObject<HTMLDivElement> = createRef();
|
|
187
|
+
/**
|
|
188
|
+
* The codemirror editor state.
|
|
189
|
+
*/
|
|
190
|
+
editorState: React.MutableRefObject<EditorState> = createRef();
|
|
191
|
+
/**
|
|
192
|
+
* The codemirror editor view.
|
|
193
|
+
*/
|
|
194
|
+
editorView: React.MutableRefObject<EditorView> = createRef();
|
|
195
|
+
private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Focus the editor
|
|
199
|
+
*/
|
|
200
|
+
focus() {
|
|
201
|
+
this.editorView.current?.focus();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Move the cursor to the supplied position.
|
|
206
|
+
* For example, to move the cursor to the end of the editor, use `value.length`
|
|
207
|
+
*/
|
|
208
|
+
updateCursorPosition(position: number) {
|
|
209
|
+
this.editorView.current?.dispatch({
|
|
210
|
+
selection: { anchor: position, head: position },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Externally set the editor value and focus the editor.
|
|
216
|
+
*/
|
|
217
|
+
setValueAndFocus(value = '') {
|
|
218
|
+
const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
|
|
219
|
+
this.editorView.current.dispatch({
|
|
220
|
+
changes: {
|
|
221
|
+
from: 0,
|
|
222
|
+
to: currentCmValue.length,
|
|
223
|
+
insert: value,
|
|
224
|
+
},
|
|
225
|
+
selection: { anchor: value.length, head: value.length },
|
|
226
|
+
});
|
|
227
|
+
this.editorView.current?.focus();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static defaultProps: CypherEditorProps = {
|
|
231
|
+
lint: true,
|
|
232
|
+
schema: {},
|
|
233
|
+
overrideThemeBackgroundColor: false,
|
|
234
|
+
lineWrap: false,
|
|
235
|
+
extraKeybindings: [],
|
|
236
|
+
history: [],
|
|
237
|
+
theme: 'light',
|
|
238
|
+
lineNumbers: true,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
private debouncedOnChange = this.props.onChange
|
|
242
|
+
? debounce(this.props.onChange, 200)
|
|
243
|
+
: undefined;
|
|
244
|
+
|
|
245
|
+
componentDidMount(): void {
|
|
246
|
+
const {
|
|
247
|
+
theme,
|
|
248
|
+
extraKeybindings,
|
|
249
|
+
lineWrap,
|
|
250
|
+
overrideThemeBackgroundColor,
|
|
251
|
+
schema,
|
|
252
|
+
lint,
|
|
253
|
+
onExecute,
|
|
254
|
+
} = this.props;
|
|
255
|
+
|
|
256
|
+
this.schemaRef.current = {
|
|
257
|
+
schema,
|
|
258
|
+
lint,
|
|
259
|
+
useLightVersion: false,
|
|
260
|
+
setUseLightVersion: (newVal) => {
|
|
261
|
+
if (this.schemaRef.current !== undefined) {
|
|
262
|
+
this.schemaRef.current.useLightVersion = newVal;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const themeExtension = getThemeExtension(
|
|
268
|
+
theme,
|
|
269
|
+
overrideThemeBackgroundColor,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const changeListener = this.debouncedOnChange
|
|
273
|
+
? [
|
|
274
|
+
EditorView.updateListener.of((upt: ViewUpdate) => {
|
|
275
|
+
const wasUserEdit = !upt.transactions.some((tr) =>
|
|
276
|
+
tr.annotation(ExternalEdit),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (upt.docChanged && wasUserEdit) {
|
|
280
|
+
const doc = upt.state.doc;
|
|
281
|
+
const value = doc.toString();
|
|
282
|
+
this.debouncedOnChange(value, upt);
|
|
283
|
+
}
|
|
284
|
+
}),
|
|
285
|
+
]
|
|
286
|
+
: [];
|
|
287
|
+
|
|
288
|
+
this.editorState.current = EditorState.create({
|
|
289
|
+
extensions: [
|
|
290
|
+
keyBindingCompartment.of(
|
|
291
|
+
keymap.of([...executeKeybinding(onExecute), ...extraKeybindings]),
|
|
292
|
+
),
|
|
293
|
+
historyNavigation(this.props),
|
|
294
|
+
basicNeo4jSetup(),
|
|
295
|
+
themeCompartment.of(themeExtension),
|
|
296
|
+
changeListener,
|
|
297
|
+
cypher(this.schemaRef.current),
|
|
298
|
+
lineWrap ? EditorView.lineWrapping : [],
|
|
299
|
+
|
|
300
|
+
lineNumbersCompartment.of(
|
|
301
|
+
this.props.lineNumbers
|
|
302
|
+
? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
|
|
303
|
+
: [],
|
|
304
|
+
),
|
|
305
|
+
readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
|
|
306
|
+
placeholderCompartment.of(
|
|
307
|
+
this.props.placeholder ? placeholder(this.props.placeholder) : [],
|
|
308
|
+
),
|
|
309
|
+
domEventHandlerCompartment.of(
|
|
310
|
+
this.props.domEventHandlers
|
|
311
|
+
? EditorView.domEventHandlers(this.props.domEventHandlers)
|
|
312
|
+
: [],
|
|
313
|
+
),
|
|
314
|
+
],
|
|
315
|
+
doc: this.props.value,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
this.editorView.current = new EditorView({
|
|
319
|
+
state: this.editorState.current,
|
|
320
|
+
parent: this.editorContainer.current,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (this.props.autofocus) {
|
|
324
|
+
this.focus();
|
|
325
|
+
if (this.props.value) {
|
|
326
|
+
this.updateCursorPosition(this.props.value.length);
|
|
327
|
+
}
|
|
328
|
+
} else if (this.props.offset) {
|
|
329
|
+
this.updateCursorPosition(this.props.offset);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
componentDidUpdate(prevProps: CypherEditorProps): void {
|
|
334
|
+
if (!this.editorView.current) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Handle externally set value
|
|
339
|
+
const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
|
|
340
|
+
|
|
341
|
+
if (this.props.value !== undefined && currentCmValue !== this.props.value) {
|
|
342
|
+
this.editorView.current.dispatch({
|
|
343
|
+
changes: {
|
|
344
|
+
from: 0,
|
|
345
|
+
to: currentCmValue.length,
|
|
346
|
+
insert: this.props.value ?? '',
|
|
347
|
+
},
|
|
348
|
+
annotations: [ExternalEdit.of(true)],
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Handle theme change
|
|
353
|
+
const didChangeTheme =
|
|
354
|
+
prevProps.theme !== this.props.theme ||
|
|
355
|
+
prevProps.overrideThemeBackgroundColor !==
|
|
356
|
+
this.props.overrideThemeBackgroundColor;
|
|
357
|
+
|
|
358
|
+
if (didChangeTheme) {
|
|
359
|
+
this.editorView.current.dispatch({
|
|
360
|
+
effects: themeCompartment.reconfigure(
|
|
361
|
+
getThemeExtension(
|
|
362
|
+
this.props.theme,
|
|
363
|
+
this.props.overrideThemeBackgroundColor,
|
|
364
|
+
),
|
|
365
|
+
),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
prevProps.lineNumbers !== this.props.lineNumbers ||
|
|
371
|
+
prevProps.prompt !== this.props.prompt
|
|
372
|
+
) {
|
|
373
|
+
this.editorView.current.dispatch({
|
|
374
|
+
effects: lineNumbersCompartment.reconfigure(
|
|
375
|
+
this.props.lineNumbers
|
|
376
|
+
? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
|
|
377
|
+
: [],
|
|
378
|
+
),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (prevProps.readonly !== this.props.readonly) {
|
|
383
|
+
this.editorView.current.dispatch({
|
|
384
|
+
effects: readOnlyCompartment.reconfigure(
|
|
385
|
+
EditorState.readOnly.of(this.props.readonly),
|
|
386
|
+
),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (prevProps.placeholder !== this.props.placeholder) {
|
|
391
|
+
this.editorView.current.dispatch({
|
|
392
|
+
effects: placeholderCompartment.reconfigure(
|
|
393
|
+
this.props.placeholder ? placeholder(this.props.placeholder) : [],
|
|
394
|
+
),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (
|
|
399
|
+
prevProps.extraKeybindings !== this.props.extraKeybindings ||
|
|
400
|
+
prevProps.onExecute !== this.props.onExecute
|
|
401
|
+
) {
|
|
402
|
+
this.editorView.current.dispatch({
|
|
403
|
+
effects: keyBindingCompartment.reconfigure(
|
|
404
|
+
keymap.of([
|
|
405
|
+
...executeKeybinding(this.props.onExecute),
|
|
406
|
+
...this.props.extraKeybindings,
|
|
407
|
+
]),
|
|
408
|
+
),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
|
|
413
|
+
this.editorView.current.dispatch({
|
|
414
|
+
effects: domEventHandlerCompartment.reconfigure(
|
|
415
|
+
this.props.domEventHandlers
|
|
416
|
+
? EditorView.domEventHandlers(this.props.domEventHandlers)
|
|
417
|
+
: [],
|
|
418
|
+
),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// This component rerenders on every keystroke and comparing the
|
|
423
|
+
// full lists of editor strings on every render could be expensive.
|
|
424
|
+
const didChangeHistoryEstimate =
|
|
425
|
+
prevProps.history?.length !== this.props.history?.length ||
|
|
426
|
+
prevProps.history?.[0] !== this.props.history?.[0];
|
|
427
|
+
|
|
428
|
+
if (didChangeHistoryEstimate) {
|
|
429
|
+
this.editorView.current.dispatch({
|
|
430
|
+
effects: replaceHistory.of(this.props.history ?? []),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/*
|
|
435
|
+
The cypher configuration is a mutable object that is passed to the cypher language extension.
|
|
436
|
+
Much like how the schema based completions work in the official sql language extension.
|
|
437
|
+
https://github.com/codemirror/lang-sql/blob/4b7b2564dff7cdb1a15f8ccd08142f2cc8a0006f/src/sql.ts#L178C17-L178C18
|
|
438
|
+
*/
|
|
439
|
+
this.schemaRef.current.schema = this.props.schema;
|
|
440
|
+
this.schemaRef.current.lint = this.props.lint;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
componentWillUnmount(): void {
|
|
444
|
+
this.editorView.current?.destroy();
|
|
445
|
+
cleanupWorkers();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
render(): React.ReactNode {
|
|
449
|
+
const { className, theme } = this.props;
|
|
450
|
+
|
|
451
|
+
const themeClass =
|
|
452
|
+
typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme';
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div
|
|
456
|
+
ref={this.editorContainer}
|
|
457
|
+
className={`${themeClass}${className ? ` ${className}` : ''}`}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { testData } from '@neo4j-cypher/language-support';
|
|
2
|
+
import { expect, test } from '@playwright/experimental-ct-react';
|
|
3
|
+
import { CypherEditor } from '../CypherEditor';
|
|
4
|
+
|
|
5
|
+
test.use({ viewport: { width: 500, height: 500 } });
|
|
6
|
+
|
|
7
|
+
test('hello world end 2 end test', async ({ mount }) => {
|
|
8
|
+
const component = await mount(<CypherEditor value="hello world" />);
|
|
9
|
+
await expect(component).toContainText('hello world');
|
|
10
|
+
await component.update(<CypherEditor value="RETURN 123" />);
|
|
11
|
+
await expect(component).toContainText('RETURN 123');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('can complete in the middle of statement', async ({ mount, page }) => {
|
|
15
|
+
const component = await mount(
|
|
16
|
+
<CypherEditor
|
|
17
|
+
value={`MATCH ()
|
|
18
|
+
WHER true
|
|
19
|
+
RETURN n;`}
|
|
20
|
+
/>,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Move into the statement and trigger autocompletion
|
|
24
|
+
const textField = page.getByRole('textbox');
|
|
25
|
+
|
|
26
|
+
await textField.focus();
|
|
27
|
+
await textField.press('ArrowDown');
|
|
28
|
+
await textField.press('ArrowRight');
|
|
29
|
+
await textField.press('ArrowRight');
|
|
30
|
+
await textField.press('ArrowRight');
|
|
31
|
+
await textField.press('ArrowRight');
|
|
32
|
+
|
|
33
|
+
await textField.press('Control+ ');
|
|
34
|
+
|
|
35
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
|
|
36
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('WHERE').click();
|
|
37
|
+
|
|
38
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
39
|
+
|
|
40
|
+
await expect(component).toContainText('WHERE true');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('get completions when typing and can accept completions with tab', async ({
|
|
44
|
+
mount,
|
|
45
|
+
page,
|
|
46
|
+
}) => {
|
|
47
|
+
const component = await mount(<CypherEditor />);
|
|
48
|
+
const textField = page.getByRole('textbox');
|
|
49
|
+
|
|
50
|
+
await textField.fill('RETU');
|
|
51
|
+
|
|
52
|
+
await expect(
|
|
53
|
+
page.locator('.cm-tooltip-autocomplete').getByText('RETURN'),
|
|
54
|
+
).toBeVisible();
|
|
55
|
+
|
|
56
|
+
// We need to wait for the editor to realise there is a completion open
|
|
57
|
+
// so that it does not just indent with tab key
|
|
58
|
+
await page.waitForTimeout(500);
|
|
59
|
+
await textField.press('Tab');
|
|
60
|
+
|
|
61
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
62
|
+
|
|
63
|
+
await expect(component).toContainText('RETURN');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('can complete labels', async ({ mount, page }) => {
|
|
67
|
+
const component = await mount(
|
|
68
|
+
<CypherEditor
|
|
69
|
+
schema={{
|
|
70
|
+
labels: ['Pokemon'],
|
|
71
|
+
}}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const textField = page.getByRole('textbox');
|
|
76
|
+
|
|
77
|
+
await textField.fill('MATCH (n :P');
|
|
78
|
+
|
|
79
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('Pokemon').click();
|
|
80
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
81
|
+
|
|
82
|
+
await expect(component).toContainText('MATCH (n :Pokemon');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('can update dbschema', async ({ mount, page }) => {
|
|
86
|
+
const component = await mount(
|
|
87
|
+
<CypherEditor
|
|
88
|
+
schema={{
|
|
89
|
+
labels: ['Pokemon'],
|
|
90
|
+
}}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const textField = page.getByRole('textbox');
|
|
95
|
+
|
|
96
|
+
await textField.fill('MATCH (n :');
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
page.locator('.cm-tooltip-autocomplete').getByText('Pokemon'),
|
|
100
|
+
).toBeVisible();
|
|
101
|
+
|
|
102
|
+
await textField.press('Escape');
|
|
103
|
+
|
|
104
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
105
|
+
|
|
106
|
+
await component.update(
|
|
107
|
+
<CypherEditor
|
|
108
|
+
schema={{
|
|
109
|
+
labels: ['Pokemon', 'Digimon'],
|
|
110
|
+
}}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await textField.press('Control+ ');
|
|
115
|
+
|
|
116
|
+
await expect(
|
|
117
|
+
page.locator('.cm-tooltip-autocomplete').getByText('Pokemon'),
|
|
118
|
+
).toBeVisible();
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
page.locator('.cm-tooltip-autocomplete').getByText('Digimon'),
|
|
122
|
+
).toBeVisible();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('can complete rel types', async ({ page, mount }) => {
|
|
126
|
+
const component = await mount(
|
|
127
|
+
<CypherEditor
|
|
128
|
+
schema={{
|
|
129
|
+
relationshipTypes: ['KNOWS'],
|
|
130
|
+
}}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const textField = page.getByRole('textbox');
|
|
135
|
+
|
|
136
|
+
await textField.fill('MATCH (n)-[:');
|
|
137
|
+
|
|
138
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('KNOWS').click();
|
|
139
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
140
|
+
|
|
141
|
+
await expect(component).toContainText('MATCH (n)-[:KNOWS');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('can complete functions', async ({ page, mount }) => {
|
|
145
|
+
const component = await mount(
|
|
146
|
+
<CypherEditor
|
|
147
|
+
schema={{
|
|
148
|
+
functions: {
|
|
149
|
+
function123: {
|
|
150
|
+
...testData.emptyFunction,
|
|
151
|
+
name: 'function123',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}}
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const textField = page.getByRole('textbox');
|
|
159
|
+
|
|
160
|
+
await textField.fill('RETURN func');
|
|
161
|
+
|
|
162
|
+
await page
|
|
163
|
+
.locator('.cm-tooltip-autocomplete')
|
|
164
|
+
.getByText('function123')
|
|
165
|
+
.click();
|
|
166
|
+
|
|
167
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
168
|
+
|
|
169
|
+
await expect(component).toContainText('RETURN function123');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('can complete procedures', async ({ page, mount }) => {
|
|
173
|
+
const component = await mount(
|
|
174
|
+
<CypherEditor
|
|
175
|
+
schema={{
|
|
176
|
+
procedures: {
|
|
177
|
+
'db.ping': { ...testData.emptyProcedure, name: 'db.ping' },
|
|
178
|
+
},
|
|
179
|
+
}}
|
|
180
|
+
/>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const textField = page.getByRole('textbox');
|
|
184
|
+
|
|
185
|
+
await textField.fill('CALL d');
|
|
186
|
+
|
|
187
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('db.ping').click();
|
|
188
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
189
|
+
|
|
190
|
+
await expect(component).toContainText('CALL db.ping');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('can complete parameters', async ({ page, mount }) => {
|
|
194
|
+
const component = await mount(
|
|
195
|
+
<CypherEditor
|
|
196
|
+
schema={{
|
|
197
|
+
parameters: { parameter: { type: 'string' } },
|
|
198
|
+
}}
|
|
199
|
+
/>,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const textField = page.getByRole('textbox');
|
|
203
|
+
|
|
204
|
+
await textField.fill('RETURN $p');
|
|
205
|
+
|
|
206
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('parameter').click();
|
|
207
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
208
|
+
|
|
209
|
+
await expect(component).toContainText('RETURN $parameter');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('completes allShortestPaths correctly', async ({ page, mount }) => {
|
|
213
|
+
await mount(
|
|
214
|
+
<CypherEditor
|
|
215
|
+
schema={{
|
|
216
|
+
parameters: { parameter: { type: 'string' } },
|
|
217
|
+
}}
|
|
218
|
+
/>,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const textField = page.getByRole('textbox');
|
|
222
|
+
|
|
223
|
+
// The first query contains errors on purpose so the
|
|
224
|
+
// syntax errors get triggered before the auto-completion
|
|
225
|
+
await textField.fill('MATCH (n) REURN n; MATCH a');
|
|
226
|
+
|
|
227
|
+
await page
|
|
228
|
+
.locator('.cm-tooltip-autocomplete')
|
|
229
|
+
.getByText('allShortestPaths')
|
|
230
|
+
.click();
|
|
231
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
232
|
+
|
|
233
|
+
expect(await textField.textContent()).toEqual(
|
|
234
|
+
'MATCH (n) REURN n; MATCH allShortestPaths',
|
|
235
|
+
);
|
|
236
|
+
});
|