@neo4j-cypher/react-codemirror 2.0.0-next.6 → 2.0.0-next.8
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 +15 -0
- package/dist/CypherEditor.d.ts +32 -1
- package/dist/CypherEditor.js +45 -9
- package/dist/CypherEditor.js.map +1 -1
- 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.js +9 -1
- package/dist/e2e_tests/e2eUtils.js.map +1 -1
- package/dist/e2e_tests/sanityChecks.spec.js +0 -9
- package/dist/e2e_tests/sanityChecks.spec.js.map +1 -1
- package/dist/e2e_tests/signatureHelp.spec.js +16 -15
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
- 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/syntaxValidation.spec.js +3 -3
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -1
- package/dist/lang-cypher/autocomplete.js +9 -3
- package/dist/lang-cypher/autocomplete.js.map +1 -1
- package/dist/lang-cypher/createCypherTheme.js +29 -1
- package/dist/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/lang-cypher/signatureHelp.js +36 -20
- package/dist/lang-cypher/signatureHelp.js.map +1 -1
- package/dist/neo4jSetup.js +35 -1
- package/dist/neo4jSetup.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/CypherEditor.tsx +102 -10
- package/src/e2e_tests/configuration.spec.tsx +97 -0
- package/src/e2e_tests/e2eUtils.ts +11 -1
- package/src/e2e_tests/sanityChecks.spec.tsx +0 -14
- package/src/e2e_tests/signatureHelp.spec.tsx +16 -19
- package/src/e2e_tests/snippets.spec.tsx +94 -0
- package/src/e2e_tests/syntaxValidation.spec.tsx +3 -3
- package/src/lang-cypher/autocomplete.ts +15 -4
- package/src/lang-cypher/createCypherTheme.ts +30 -1
- package/src/lang-cypher/signatureHelp.ts +57 -28
- package/src/neo4jSetup.tsx +51 -1
package/src/CypherEditor.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
KeyBinding,
|
|
10
10
|
keymap,
|
|
11
11
|
lineNumbers,
|
|
12
|
+
placeholder,
|
|
12
13
|
ViewUpdate,
|
|
13
14
|
} from '@codemirror/view';
|
|
14
15
|
import type { DbSchema } from '@neo4j-cypher/language-support';
|
|
@@ -23,6 +24,7 @@ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
|
|
|
23
24
|
import { basicNeo4jSetup } from './neo4jSetup';
|
|
24
25
|
import { getThemeExtension } from './themes';
|
|
25
26
|
|
|
27
|
+
type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
|
|
26
28
|
export interface CypherEditorProps {
|
|
27
29
|
/**
|
|
28
30
|
* The prompt to show on single line editors
|
|
@@ -42,7 +44,7 @@ export interface CypherEditorProps {
|
|
|
42
44
|
*/
|
|
43
45
|
onExecute?: (cmd: string) => void;
|
|
44
46
|
/**
|
|
45
|
-
* The editor history
|
|
47
|
+
* The editor history navigable via up/down arrow keys. Order newest to oldest.
|
|
46
48
|
* Add to this list with the `onExecute` callback for REPL style history.
|
|
47
49
|
*/
|
|
48
50
|
history?: string[];
|
|
@@ -102,6 +104,37 @@ export interface CypherEditorProps {
|
|
|
102
104
|
* @param {ViewUpdate} viewUpdate - the view update from codemirror
|
|
103
105
|
*/
|
|
104
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;
|
|
105
138
|
}
|
|
106
139
|
|
|
107
140
|
const executeKeybinding = (onExecute?: (cmd: string) => void) =>
|
|
@@ -125,6 +158,20 @@ const executeKeybinding = (onExecute?: (cmd: string) => void) =>
|
|
|
125
158
|
|
|
126
159
|
const themeCompartment = new Compartment();
|
|
127
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
|
+
|
|
128
175
|
type CypherEditorState = { cypherSupportEnabled: boolean };
|
|
129
176
|
|
|
130
177
|
const ExternalEdit = Annotation.define<boolean>();
|
|
@@ -188,6 +235,7 @@ export class CypherEditor extends Component<
|
|
|
188
235
|
extraKeybindings: [],
|
|
189
236
|
history: [],
|
|
190
237
|
theme: 'light',
|
|
238
|
+
lineNumbers: true,
|
|
191
239
|
};
|
|
192
240
|
|
|
193
241
|
private debouncedOnChange = this.props.onChange
|
|
@@ -249,15 +297,20 @@ export class CypherEditor extends Component<
|
|
|
249
297
|
cypher(this.schemaRef.current),
|
|
250
298
|
lineWrap ? EditorView.lineWrapping : [],
|
|
251
299
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
),
|
|
261
314
|
],
|
|
262
315
|
doc: this.props.value,
|
|
263
316
|
});
|
|
@@ -313,6 +366,35 @@ export class CypherEditor extends Component<
|
|
|
313
366
|
});
|
|
314
367
|
}
|
|
315
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
|
+
|
|
316
398
|
if (
|
|
317
399
|
prevProps.extraKeybindings !== this.props.extraKeybindings ||
|
|
318
400
|
prevProps.onExecute !== this.props.onExecute
|
|
@@ -327,6 +409,16 @@ export class CypherEditor extends Component<
|
|
|
327
409
|
});
|
|
328
410
|
}
|
|
329
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
|
+
|
|
330
422
|
// This component rerenders on every keystroke and comparing the
|
|
331
423
|
// full lists of editor strings on every render could be expensive.
|
|
332
424
|
const didChangeHistoryEstimate =
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/experimental-ct-react';
|
|
2
|
+
import { CypherEditor } from '../CypherEditor';
|
|
3
|
+
|
|
4
|
+
test.use({ viewport: { width: 500, height: 500 } });
|
|
5
|
+
|
|
6
|
+
test('prompt shows up', async ({ mount, page }) => {
|
|
7
|
+
const component = await mount(<CypherEditor prompt="neo4j>" />);
|
|
8
|
+
|
|
9
|
+
await expect(component).toContainText('neo4j>');
|
|
10
|
+
|
|
11
|
+
await component.update(<CypherEditor prompt="test>" />);
|
|
12
|
+
await expect(component).toContainText('test>');
|
|
13
|
+
|
|
14
|
+
const textField = page.getByRole('textbox');
|
|
15
|
+
await textField.press('a');
|
|
16
|
+
|
|
17
|
+
await expect(textField).toHaveText('a');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('line numbers can be turned on/off', async ({ mount }) => {
|
|
21
|
+
const component = await mount(<CypherEditor lineNumbers />);
|
|
22
|
+
|
|
23
|
+
await expect(component).toContainText('1');
|
|
24
|
+
|
|
25
|
+
await component.update(<CypherEditor lineNumbers={false} />);
|
|
26
|
+
await expect(component).not.toContainText('1');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('can configure readonly', async ({ mount, page }) => {
|
|
30
|
+
const component = await mount(<CypherEditor readonly />);
|
|
31
|
+
|
|
32
|
+
const textField = page.getByRole('textbox');
|
|
33
|
+
await textField.press('a');
|
|
34
|
+
await expect(textField).not.toHaveText('a');
|
|
35
|
+
|
|
36
|
+
await component.update(<CypherEditor readonly={false} />);
|
|
37
|
+
await textField.press('b');
|
|
38
|
+
await expect(textField).toHaveText('b');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('can set placeholder ', async ({ mount, page }) => {
|
|
42
|
+
const component = await mount(<CypherEditor placeholder="bulbasaur" />);
|
|
43
|
+
|
|
44
|
+
const textField = page.getByRole('textbox');
|
|
45
|
+
await expect(textField).toHaveText('bulbasaur');
|
|
46
|
+
|
|
47
|
+
await component.update(<CypherEditor placeholder="venusaur" />);
|
|
48
|
+
await expect(textField).not.toHaveText('bulbasaur');
|
|
49
|
+
await expect(textField).toHaveText('venusaur');
|
|
50
|
+
|
|
51
|
+
await textField.fill('abc');
|
|
52
|
+
await expect(textField).not.toHaveText('venusaur');
|
|
53
|
+
await expect(textField).toHaveText('abc');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('can set/unset onFocus/onBlur', async ({ mount, page }) => {
|
|
57
|
+
const component = await mount(<CypherEditor />);
|
|
58
|
+
|
|
59
|
+
let focusFireCount = 0;
|
|
60
|
+
let blurFireCount = 0;
|
|
61
|
+
|
|
62
|
+
const focus = () => {
|
|
63
|
+
focusFireCount += 1;
|
|
64
|
+
};
|
|
65
|
+
const blur = () => {
|
|
66
|
+
blurFireCount += 1;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await component.update(<CypherEditor domEventHandlers={{ blur, focus }} />);
|
|
70
|
+
|
|
71
|
+
const textField = page.getByRole('textbox');
|
|
72
|
+
await textField.click();
|
|
73
|
+
await expect(textField).toBeFocused();
|
|
74
|
+
|
|
75
|
+
// this is to give the events time to fire
|
|
76
|
+
await expect(() => {
|
|
77
|
+
expect(focusFireCount).toBe(1);
|
|
78
|
+
expect(blurFireCount).toBe(0);
|
|
79
|
+
}).toPass();
|
|
80
|
+
|
|
81
|
+
await textField.blur();
|
|
82
|
+
|
|
83
|
+
await expect(() => {
|
|
84
|
+
expect(focusFireCount).toBe(1);
|
|
85
|
+
expect(blurFireCount).toBe(1);
|
|
86
|
+
}).toPass();
|
|
87
|
+
|
|
88
|
+
await component.update(<CypherEditor />);
|
|
89
|
+
await textField.click();
|
|
90
|
+
await expect(textField).toBeFocused();
|
|
91
|
+
await textField.blur();
|
|
92
|
+
|
|
93
|
+
await expect(() => {
|
|
94
|
+
expect(focusFireCount).toBe(1);
|
|
95
|
+
expect(blurFireCount).toBe(1);
|
|
96
|
+
}).toPass();
|
|
97
|
+
});
|
|
@@ -65,11 +65,21 @@ export class CypherEditorPage {
|
|
|
65
65
|
expectedMsg: string,
|
|
66
66
|
) {
|
|
67
67
|
await expect(this.page.locator('.cm-lintRange-' + type).last()).toBeVisible(
|
|
68
|
-
{ timeout:
|
|
68
|
+
{ timeout: 3000 },
|
|
69
69
|
);
|
|
70
70
|
|
|
71
71
|
await this.page.getByText(queryChunk, { exact: true }).hover();
|
|
72
72
|
await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible();
|
|
73
73
|
await expect(this.page.getByText(expectedMsg)).toBeVisible();
|
|
74
|
+
/* Return the mouse to the beginning of the query and
|
|
75
|
+
This is because if for example we have an overlay with a
|
|
76
|
+
first interaction that covers the element we want to perform
|
|
77
|
+
the second interaction on, we won't be able to see that second element
|
|
78
|
+
*/
|
|
79
|
+
await this.page.mouse.move(0, 0);
|
|
80
|
+
// Make the sure the tooltip closed
|
|
81
|
+
await expect(
|
|
82
|
+
this.page.locator('.cm-tooltip-hover').last(),
|
|
83
|
+
).not.toBeVisible();
|
|
74
84
|
}
|
|
75
85
|
}
|
|
@@ -76,17 +76,3 @@ test('can complete CALL/CREATE', async ({ page, mount }) => {
|
|
|
76
76
|
|
|
77
77
|
await expect(textField).toHaveText('CALL');
|
|
78
78
|
});
|
|
79
|
-
|
|
80
|
-
test('prompt shows up', async ({ mount, page }) => {
|
|
81
|
-
const component = await mount(<CypherEditor prompt="neo4j>" />);
|
|
82
|
-
|
|
83
|
-
await expect(component).toContainText('neo4j>');
|
|
84
|
-
|
|
85
|
-
await component.update(<CypherEditor prompt="test>" />);
|
|
86
|
-
await expect(component).toContainText('test>');
|
|
87
|
-
|
|
88
|
-
const textField = page.getByRole('textbox');
|
|
89
|
-
await textField.press('a');
|
|
90
|
-
|
|
91
|
-
await expect(textField).toHaveText('a');
|
|
92
|
-
});
|
|
@@ -44,7 +44,7 @@ test('Signature help works for functions', async ({ page, mount }) => {
|
|
|
44
44
|
/>,
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
-
await expect(page.locator('.cm-
|
|
47
|
+
await expect(page.locator('.cm-signature-help-panel')).toBeVisible({
|
|
48
48
|
timeout: 2000,
|
|
49
49
|
});
|
|
50
50
|
});
|
|
@@ -60,7 +60,7 @@ test('Signature help works for procedures', async ({ page, mount }) => {
|
|
|
60
60
|
/>,
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
await expect(page.locator('.cm-
|
|
63
|
+
await expect(page.locator('.cm-signature-help-panel')).toBeVisible({
|
|
64
64
|
timeout: 2000,
|
|
65
65
|
});
|
|
66
66
|
});
|
|
@@ -79,7 +79,7 @@ test('Signature help shows the description for the first argument', async ({
|
|
|
79
79
|
/>,
|
|
80
80
|
);
|
|
81
81
|
|
|
82
|
-
const tooltip = page.locator('.cm-
|
|
82
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
83
83
|
|
|
84
84
|
await testTooltip(tooltip, {
|
|
85
85
|
includes: [
|
|
@@ -99,7 +99,7 @@ test('Signature help shows the description for the first argument when the curso
|
|
|
99
99
|
<CypherEditor value={query} schema={testData.mockSchema} offset={21} />,
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
-
const tooltip = page.locator('.cm-
|
|
102
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
103
103
|
|
|
104
104
|
await testTooltip(tooltip, {
|
|
105
105
|
includes: [
|
|
@@ -123,7 +123,7 @@ test('Signature help shows the description for the second argument', async ({
|
|
|
123
123
|
/>,
|
|
124
124
|
);
|
|
125
125
|
|
|
126
|
-
const tooltip = page.locator('.cm-
|
|
126
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
127
127
|
|
|
128
128
|
await testTooltip(tooltip, {
|
|
129
129
|
includes: [
|
|
@@ -143,7 +143,7 @@ test('Signature help shows the description for the second argument when the curs
|
|
|
143
143
|
<CypherEditor value={query} schema={testData.mockSchema} offset={27} />,
|
|
144
144
|
);
|
|
145
145
|
|
|
146
|
-
const tooltip = page.locator('.cm-
|
|
146
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
147
147
|
|
|
148
148
|
await testTooltip(tooltip, {
|
|
149
149
|
includes: [
|
|
@@ -163,7 +163,7 @@ test('Signature help shows the description for the second argument when the curs
|
|
|
163
163
|
<CypherEditor value={query} schema={testData.mockSchema} offset={28} />,
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
const tooltip = page.locator('.cm-
|
|
166
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
167
167
|
|
|
168
168
|
await testTooltip(tooltip, {
|
|
169
169
|
includes: [
|
|
@@ -187,7 +187,7 @@ test('Signature help shows description for arguments with a space following a se
|
|
|
187
187
|
/>,
|
|
188
188
|
);
|
|
189
189
|
|
|
190
|
-
const tooltip = page.locator('.cm-
|
|
190
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
191
191
|
|
|
192
192
|
await testTooltip(tooltip, {
|
|
193
193
|
includes: [
|
|
@@ -211,7 +211,7 @@ test('Signature help shows the description for the third argument', async ({
|
|
|
211
211
|
/>,
|
|
212
212
|
);
|
|
213
213
|
|
|
214
|
-
const tooltip = page.locator('.cm-
|
|
214
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
215
215
|
|
|
216
216
|
await testTooltip(tooltip, {
|
|
217
217
|
includes: [
|
|
@@ -235,7 +235,7 @@ test('Signature help works on multiline queries', async ({ page, mount }) => {
|
|
|
235
235
|
/>,
|
|
236
236
|
);
|
|
237
237
|
|
|
238
|
-
const tooltip = page.locator('.cm-
|
|
238
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
239
239
|
|
|
240
240
|
await testTooltip(tooltip, {
|
|
241
241
|
includes: [
|
|
@@ -258,14 +258,15 @@ test('Signature help only shows the description past the last argument', async (
|
|
|
258
258
|
autofocus={true}
|
|
259
259
|
/>,
|
|
260
260
|
);
|
|
261
|
+
1;
|
|
261
262
|
|
|
262
|
-
const tooltip = page.locator('.cm-
|
|
263
|
+
const tooltip = page.locator('.cm-signature-help-panel');
|
|
263
264
|
|
|
264
265
|
await testTooltip(tooltip, {
|
|
265
266
|
includes: [
|
|
266
|
-
'
|
|
267
|
+
'apoc.import.csv(nodes :: LIST<MAP>, rels :: LIST<MAP>, config :: MAP)',
|
|
268
|
+
'Imports `NODE` and `RELATIONSHIP` values with the given labels and types from the provided CSV file.',
|
|
267
269
|
],
|
|
268
|
-
excludes: ['config :: MAP'],
|
|
269
270
|
});
|
|
270
271
|
});
|
|
271
272
|
|
|
@@ -283,9 +284,7 @@ test('Signature help does not show any help when method finished', async ({
|
|
|
283
284
|
/>,
|
|
284
285
|
);
|
|
285
286
|
|
|
286
|
-
await expect(
|
|
287
|
-
page.locator('.cm-tooltip-signature-help').last(),
|
|
288
|
-
).not.toBeVisible({
|
|
287
|
+
await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
|
|
289
288
|
timeout: 2000,
|
|
290
289
|
});
|
|
291
290
|
});
|
|
@@ -304,9 +303,7 @@ test('Signature help does not blow up on empty query', async ({
|
|
|
304
303
|
/>,
|
|
305
304
|
);
|
|
306
305
|
|
|
307
|
-
await expect(
|
|
308
|
-
page.locator('.cm-tooltip-signature-help').last(),
|
|
309
|
-
).not.toBeVisible({
|
|
306
|
+
await expect(page.locator('.cm-signature-help-panel')).not.toBeVisible({
|
|
310
307
|
timeout: 2000,
|
|
311
308
|
});
|
|
312
309
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/experimental-ct-react';
|
|
2
|
+
import { CypherEditor } from '../CypherEditor';
|
|
3
|
+
|
|
4
|
+
test.use({ viewport: { width: 500, height: 500 } });
|
|
5
|
+
|
|
6
|
+
test('can complete pattern snippet', async ({ page, mount }) => {
|
|
7
|
+
await mount(<CypherEditor />);
|
|
8
|
+
const textField = page.getByRole('textbox');
|
|
9
|
+
|
|
10
|
+
await textField.fill('MATCH ()-[]->()');
|
|
11
|
+
|
|
12
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
|
|
13
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
14
|
+
|
|
15
|
+
await textField.press('Tab');
|
|
16
|
+
await textField.press('Tab');
|
|
17
|
+
|
|
18
|
+
await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->( )');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('can navigate snippet', async ({ page, mount }) => {
|
|
22
|
+
await mount(<CypherEditor />);
|
|
23
|
+
const textField = page.getByRole('textbox');
|
|
24
|
+
|
|
25
|
+
await textField.fill('CREATE INDEX abc FOR ()');
|
|
26
|
+
|
|
27
|
+
await page
|
|
28
|
+
.locator('.cm-tooltip-autocomplete')
|
|
29
|
+
.getByText('-[]-()', { exact: true })
|
|
30
|
+
.click();
|
|
31
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
32
|
+
await expect(page.locator('.cm-snippetField')).toHaveCount(2);
|
|
33
|
+
|
|
34
|
+
await textField.press('Tab');
|
|
35
|
+
await textField.press('Shift+Tab');
|
|
36
|
+
|
|
37
|
+
await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[ ]-( )');
|
|
38
|
+
|
|
39
|
+
await textField.press('a');
|
|
40
|
+
await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[a]-( )');
|
|
41
|
+
|
|
42
|
+
await textField.press('Escape');
|
|
43
|
+
await textField.press('Escape');
|
|
44
|
+
await expect(page.locator('.cm-snippetField')).toHaveCount(0);
|
|
45
|
+
await textField.press('Tab');
|
|
46
|
+
await expect(textField).toHaveText('CREATE INDEX abc FOR ()-[a ]-( )');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('can accept completion inside pattern snippet', async ({
|
|
50
|
+
page,
|
|
51
|
+
mount,
|
|
52
|
+
}) => {
|
|
53
|
+
await mount(<CypherEditor schema={{ labels: ['City'] }} />);
|
|
54
|
+
const textField = page.getByRole('textbox');
|
|
55
|
+
|
|
56
|
+
await textField.fill('MATCH ()-[]->()');
|
|
57
|
+
|
|
58
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('-[]->()').click();
|
|
59
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
60
|
+
|
|
61
|
+
// move to node
|
|
62
|
+
await textField.press('Tab');
|
|
63
|
+
|
|
64
|
+
// get & accept completion
|
|
65
|
+
await textField.press(':');
|
|
66
|
+
await expect(
|
|
67
|
+
page.locator('.cm-tooltip-autocomplete').getByText('City'),
|
|
68
|
+
).toBeVisible();
|
|
69
|
+
|
|
70
|
+
await textField.press('Tab');
|
|
71
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
72
|
+
|
|
73
|
+
// tab out of the snippet
|
|
74
|
+
await textField.press('Tab');
|
|
75
|
+
|
|
76
|
+
await expect(textField).toHaveText('MATCH ()-[]->()-[ ]->(:City)');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('does not automatically open completion panel for expressions after snippet trigger char', async ({
|
|
80
|
+
page,
|
|
81
|
+
mount,
|
|
82
|
+
}) => {
|
|
83
|
+
await mount(<CypherEditor />);
|
|
84
|
+
const textField = page.getByRole('textbox');
|
|
85
|
+
|
|
86
|
+
await textField.fill('RETURN (1)');
|
|
87
|
+
|
|
88
|
+
// expect the panel to not show up
|
|
89
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
90
|
+
|
|
91
|
+
// unless manually triggered
|
|
92
|
+
await textField.press('Control+ ');
|
|
93
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
|
|
94
|
+
});
|
|
@@ -106,7 +106,7 @@ test('Semantic errors are correctly accumulated', async ({ page, mount }) => {
|
|
|
106
106
|
|
|
107
107
|
await editorPage.checkErrorMessage(
|
|
108
108
|
'MATCH (n)',
|
|
109
|
-
'Query cannot conclude with MATCH (must be a RETURN clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
109
|
+
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
|
|
110
110
|
);
|
|
111
111
|
|
|
112
112
|
await editorPage.checkErrorMessage(
|
|
@@ -126,7 +126,7 @@ test('Multiline errors are correctly placed', async ({ page, mount }) => {
|
|
|
126
126
|
|
|
127
127
|
await editorPage.checkErrorMessage(
|
|
128
128
|
'MATCH (n)',
|
|
129
|
-
'Query cannot conclude with MATCH (must be a RETURN clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
129
|
+
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
130
130
|
);
|
|
131
131
|
|
|
132
132
|
await editorPage.checkErrorMessage(
|
|
@@ -146,7 +146,7 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
|
|
|
146
146
|
|
|
147
147
|
await editorPage.checkErrorMessage(
|
|
148
148
|
'-1',
|
|
149
|
-
'Query cannot conclude with CALL (must be a RETURN clause, an update clause, a unit subquery call, or a procedure call with no YIELD)',
|
|
149
|
+
'Query cannot conclude with CALL (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
|
|
150
150
|
);
|
|
151
151
|
|
|
152
152
|
await editorPage.checkErrorMessage(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompletionSource } from '@codemirror/autocomplete';
|
|
1
|
+
import { CompletionSource, snippet } from '@codemirror/autocomplete';
|
|
2
2
|
import { autocomplete } from '@neo4j-cypher/language-support';
|
|
3
3
|
import { CompletionItemKind } from 'vscode-languageserver-types';
|
|
4
4
|
import { CompletionItemIcons } from '../icons';
|
|
@@ -28,7 +28,7 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
|
|
|
28
28
|
[CompletionItemKind.EnumMember]: 'EnumMember',
|
|
29
29
|
[CompletionItemKind.Constant]: 'Constant',
|
|
30
30
|
[CompletionItemKind.Struct]: 'Struct',
|
|
31
|
-
// we're
|
|
31
|
+
// we're miss-using the enum here as there is no `Console` kind in the predefined list
|
|
32
32
|
[CompletionItemKind.Event]: 'Console',
|
|
33
33
|
[CompletionItemKind.Operator]: 'Operator',
|
|
34
34
|
[CompletionItemKind.TypeParameter]: 'TypeParameter',
|
|
@@ -41,7 +41,7 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
41
41
|
(config) => (context) => {
|
|
42
42
|
const textUntilCursor = context.state.doc.toString().slice(0, context.pos);
|
|
43
43
|
|
|
44
|
-
const triggerCharacters = ['.', ':', '{', '$'];
|
|
44
|
+
const triggerCharacters = ['.', ':', '{', '$', ')'];
|
|
45
45
|
const lastCharacter = textUntilCursor.slice(-1);
|
|
46
46
|
|
|
47
47
|
const lastWord = context.matchBefore(/\w*/);
|
|
@@ -58,13 +58,24 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const options = autocomplete(
|
|
61
|
+
const options = autocomplete(
|
|
62
|
+
textUntilCursor,
|
|
63
|
+
config.schema ?? {},
|
|
64
|
+
undefined,
|
|
65
|
+
context.explicit,
|
|
66
|
+
);
|
|
62
67
|
|
|
63
68
|
return {
|
|
64
69
|
from: context.matchBefore(/(\w|\$)*$/).from,
|
|
65
70
|
options: options.map((o) => ({
|
|
66
71
|
label: o.label,
|
|
67
72
|
type: completionKindToCodemirrorIcon(o.kind),
|
|
73
|
+
apply:
|
|
74
|
+
o.kind === CompletionItemKind.Snippet
|
|
75
|
+
? // codemirror requires an empty snippet space to be able to tab out of the completion
|
|
76
|
+
snippet((o.insertText ?? o.label) + '${}')
|
|
77
|
+
: undefined,
|
|
78
|
+
detail: o.detail,
|
|
68
79
|
})),
|
|
69
80
|
};
|
|
70
81
|
};
|
|
@@ -53,6 +53,9 @@ export const createCypherTheme = ({
|
|
|
53
53
|
color: settings.foreground,
|
|
54
54
|
fontVariantLigatures: 'none',
|
|
55
55
|
},
|
|
56
|
+
'& .cm-snippetField': {
|
|
57
|
+
backgroundColor: settings.autoCompletionPanel.selectedColor,
|
|
58
|
+
},
|
|
56
59
|
'&.cm-focused': {
|
|
57
60
|
outline: 'none',
|
|
58
61
|
},
|
|
@@ -61,8 +64,9 @@ export const createCypherTheme = ({
|
|
|
61
64
|
color: settings.gutterForeground,
|
|
62
65
|
border: 'none',
|
|
63
66
|
},
|
|
64
|
-
'&.cm-editor
|
|
67
|
+
'&.cm-editor': {
|
|
65
68
|
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
69
|
+
height: '100%',
|
|
66
70
|
},
|
|
67
71
|
'.cm-content': {
|
|
68
72
|
caretColor: settings.cursor,
|
|
@@ -96,7 +100,32 @@ export const createCypherTheme = ({
|
|
|
96
100
|
color: settings.autoCompletionPanel.matchingTextColor,
|
|
97
101
|
textDecoration: 'none',
|
|
98
102
|
},
|
|
103
|
+
'& .cm-signature-help-panel': {
|
|
104
|
+
backgroundColor: settings.autoCompletionPanel.backgroundColor,
|
|
105
|
+
maxWidth: '700px',
|
|
106
|
+
maxHeight: '250px',
|
|
107
|
+
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
108
|
+
},
|
|
109
|
+
'& .cm-signature-help-panel-contents': {
|
|
110
|
+
overflow: 'auto',
|
|
111
|
+
maxHeight: '100%',
|
|
112
|
+
},
|
|
113
|
+
'& .cm-signature-help-panel-current-argument': {
|
|
114
|
+
color: settings.autoCompletionPanel.matchingTextColor,
|
|
115
|
+
fontWeight: 'bold',
|
|
116
|
+
},
|
|
117
|
+
'& .cm-signature-help-panel-separator': {
|
|
118
|
+
borderBottom: '1px solid #ccc',
|
|
119
|
+
},
|
|
120
|
+
'& .cm-signature-help-panel-name': {
|
|
121
|
+
padding: '5px',
|
|
122
|
+
},
|
|
123
|
+
'& .cm-signature-help-panel-description': {
|
|
124
|
+
padding: '5px',
|
|
125
|
+
},
|
|
126
|
+
|
|
99
127
|
'.cm-tooltip-autocomplete': {
|
|
128
|
+
maxWidth: '430px',
|
|
100
129
|
'& > ul > li[aria-selected]': {
|
|
101
130
|
backgroundColor: settings.autoCompletionPanel.selectedColor,
|
|
102
131
|
color: settings.foreground,
|