@neo4j-cypher/react-codemirror 2.0.0-next.34 → 2.0.0-next.36
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 +21 -0
- package/dist/src/CypherEditor.d.ts +22 -0
- package/dist/src/CypherEditor.js +58 -1
- package/dist/src/CypherEditor.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inlinePanel.d.ts +33 -0
- package/dist/src/inlinePanel.js +73 -0
- package/dist/src/inlinePanel.js.map +1 -0
- package/dist/src/lang-cypher/lintWorker.mjs +253 -245
- package/dist/src/lang-cypher/syntaxValidation.js +6 -2
- package/dist/src/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/CypherEditor.tsx +98 -6
- package/src/index.ts +1 -0
- package/src/inlinePanel.ts +120 -0
- package/src/lang-cypher/lintWorker.mjs +253 -245
- package/src/lang-cypher/syntaxValidation.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-cypher/react-codemirror",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.36",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"codemirror",
|
|
6
6
|
"codemirror 6",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"style-mod": "^4.1.2",
|
|
48
48
|
"vscode-languageserver-types": "^3.17.3",
|
|
49
49
|
"workerpool": "^9.3.3",
|
|
50
|
-
"@neo4j-cypher/language-support": "2.0.0-next.
|
|
51
|
-
"@neo4j-cypher/lint-worker": "1.10.1-next.
|
|
50
|
+
"@neo4j-cypher/language-support": "2.0.0-next.33",
|
|
51
|
+
"@neo4j-cypher/lint-worker": "1.10.1-next.10"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@neo4j-ndl/base": "^3.2.10",
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
placeholder,
|
|
14
14
|
ViewUpdate,
|
|
15
15
|
} from '@codemirror/view';
|
|
16
|
+
import {
|
|
17
|
+
createInlinePanelController,
|
|
18
|
+
type InlinePanelCallbacks,
|
|
19
|
+
type InlinePanelController,
|
|
20
|
+
} from './inlinePanel';
|
|
16
21
|
import {
|
|
17
22
|
formatQuery,
|
|
18
23
|
CypherLanguageService,
|
|
@@ -180,8 +185,26 @@ export interface CypherEditorProps {
|
|
|
180
185
|
* @default false
|
|
181
186
|
*/
|
|
182
187
|
moveFocusOnTab?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Render a panel as a block widget inside the editor.
|
|
190
|
+
* The widget DOM is only rebuilt when `pos` or `placement` change
|
|
191
|
+
*/
|
|
192
|
+
inlinePanel?: InlinePanelProps | null;
|
|
183
193
|
}
|
|
184
194
|
|
|
195
|
+
export type InlinePanelProps = {
|
|
196
|
+
/**
|
|
197
|
+
* Position the panel anchors to.
|
|
198
|
+
*/
|
|
199
|
+
pos: number;
|
|
200
|
+
/**
|
|
201
|
+
* Whether to render above or below the line
|
|
202
|
+
*
|
|
203
|
+
* @default 'above'
|
|
204
|
+
*/
|
|
205
|
+
placement?: 'above' | 'below';
|
|
206
|
+
} & InlinePanelCallbacks;
|
|
207
|
+
|
|
185
208
|
const format = (view: EditorView): void => {
|
|
186
209
|
try {
|
|
187
210
|
const doc = view.state.doc.toString();
|
|
@@ -290,10 +313,12 @@ class CodemirrorSymbolFetcher {
|
|
|
290
313
|
}
|
|
291
314
|
private languageService: CypherLanguageService;
|
|
292
315
|
private processing = false;
|
|
293
|
-
private nextJob:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
private nextJob:
|
|
317
|
+
| {
|
|
318
|
+
query: string;
|
|
319
|
+
schema: DbSchema;
|
|
320
|
+
}
|
|
321
|
+
| undefined;
|
|
297
322
|
private symbolTablePool = workerpool.pool(WorkerURL, {
|
|
298
323
|
minWorkers: 1,
|
|
299
324
|
workerOpts: { type: 'module' },
|
|
@@ -316,11 +341,11 @@ class CodemirrorSymbolFetcher {
|
|
|
316
341
|
this.processing = true;
|
|
317
342
|
while (this.nextJob) {
|
|
318
343
|
try {
|
|
319
|
-
const proxyWorker =
|
|
320
|
-
(await this.symbolTablePool.proxy()) as unknown as LintWorker;
|
|
321
344
|
const query = this.nextJob.query;
|
|
322
345
|
const dbSchema = this.nextJob.schema;
|
|
323
346
|
this.nextJob = undefined;
|
|
347
|
+
const proxyWorker =
|
|
348
|
+
(await this.symbolTablePool.proxy()) as unknown as LintWorker;
|
|
324
349
|
|
|
325
350
|
const result = await proxyWorker.lintCypherQuery(query, dbSchema);
|
|
326
351
|
|
|
@@ -360,6 +385,7 @@ export class CypherEditor extends Component<
|
|
|
360
385
|
*/
|
|
361
386
|
editorView: React.MutableRefObject<EditorView> = createRef();
|
|
362
387
|
private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
|
|
388
|
+
private inlinePanelController: InlinePanelController | null = null;
|
|
363
389
|
|
|
364
390
|
/**
|
|
365
391
|
* Format Cypher query
|
|
@@ -471,6 +497,8 @@ export class CypherEditor extends Component<
|
|
|
471
497
|
overrideThemeBackgroundColor,
|
|
472
498
|
);
|
|
473
499
|
|
|
500
|
+
this.inlinePanelController = createInlinePanelController();
|
|
501
|
+
|
|
474
502
|
const changeListener = this.debouncedOnChange
|
|
475
503
|
? [
|
|
476
504
|
EditorView.updateListener.of((upt: ViewUpdate) => {
|
|
@@ -536,6 +564,7 @@ export class CypherEditor extends Component<
|
|
|
536
564
|
'Press Escape to leave the editor and continue tabbing through the page',
|
|
537
565
|
})
|
|
538
566
|
: [],
|
|
567
|
+
this.inlinePanelController.extension,
|
|
539
568
|
],
|
|
540
569
|
doc: this.props.value,
|
|
541
570
|
});
|
|
@@ -553,6 +582,53 @@ export class CypherEditor extends Component<
|
|
|
553
582
|
} else if (this.props.offset) {
|
|
554
583
|
this.updateCursorPosition(this.props.offset);
|
|
555
584
|
}
|
|
585
|
+
|
|
586
|
+
if (this.props.inlinePanel) {
|
|
587
|
+
this.openInlinePanel(this.props.inlinePanel);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private openInlinePanel(
|
|
592
|
+
props: NonNullable<CypherEditorProps['inlinePanel']>,
|
|
593
|
+
): void {
|
|
594
|
+
const view = this.editorView.current;
|
|
595
|
+
const controller = this.inlinePanelController;
|
|
596
|
+
if (!view || !controller) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const pos = Math.max(0, Math.min(props.pos, view.state.doc.length));
|
|
601
|
+
const line = view.state.doc.lineAt(pos);
|
|
602
|
+
controller.updateCallbacks({
|
|
603
|
+
onMount: props.onMount,
|
|
604
|
+
onUnmount: props.onUnmount,
|
|
605
|
+
});
|
|
606
|
+
view.dispatch({
|
|
607
|
+
effects: controller.show({
|
|
608
|
+
pos: props.placement === 'below' ? line.to : line.from,
|
|
609
|
+
placement: props.placement,
|
|
610
|
+
onMount: props.onMount,
|
|
611
|
+
onUnmount: props.onUnmount,
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private updateInlinePanel(
|
|
617
|
+
props: NonNullable<CypherEditorProps['inlinePanel']>,
|
|
618
|
+
): void {
|
|
619
|
+
this.inlinePanelController?.updateCallbacks({
|
|
620
|
+
onMount: props.onMount,
|
|
621
|
+
onUnmount: props.onUnmount,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private closeInlinePanel(): void {
|
|
626
|
+
const view = this.editorView.current;
|
|
627
|
+
const controller = this.inlinePanelController;
|
|
628
|
+
if (!view || !controller) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
view.dispatch({ effects: controller.hide() });
|
|
556
632
|
}
|
|
557
633
|
|
|
558
634
|
componentDidUpdate(prevProps: CypherEditorProps): void {
|
|
@@ -642,6 +718,22 @@ export class CypherEditor extends Component<
|
|
|
642
718
|
});
|
|
643
719
|
}
|
|
644
720
|
|
|
721
|
+
const prevPanel = prevProps.inlinePanel;
|
|
722
|
+
const nextPanel = this.props.inlinePanel;
|
|
723
|
+
if (prevPanel !== nextPanel) {
|
|
724
|
+
if (!nextPanel) {
|
|
725
|
+
this.closeInlinePanel();
|
|
726
|
+
} else if (
|
|
727
|
+
!prevPanel ||
|
|
728
|
+
prevPanel.pos !== nextPanel.pos ||
|
|
729
|
+
prevPanel.placement !== nextPanel.placement
|
|
730
|
+
) {
|
|
731
|
+
this.openInlinePanel(nextPanel);
|
|
732
|
+
} else {
|
|
733
|
+
this.updateInlinePanel(nextPanel);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
645
737
|
if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
|
|
646
738
|
this.editorView.current.dispatch({
|
|
647
739
|
effects: domEventHandlerCompartment.reconfigure(
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * as LanguageSupport from '@neo4j-cypher/language-support';
|
|
2
2
|
export { CypherEditor } from './CypherEditor';
|
|
3
|
+
export type { InlinePanelProps } from './CypherEditor';
|
|
3
4
|
export { cypher } from './lang-cypher/langCypher';
|
|
4
5
|
export { darkThemeConstants, lightThemeConstants } from './themes';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { StateEffect, StateField, type Extension } from '@codemirror/state';
|
|
2
|
+
import {
|
|
3
|
+
Decoration,
|
|
4
|
+
type DecorationSet,
|
|
5
|
+
EditorView,
|
|
6
|
+
WidgetType,
|
|
7
|
+
} from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lifecycle callbacks for an inline panel. The host renders into the
|
|
11
|
+
* provided DOM container (e.g. via React `createPortal`) and is expected
|
|
12
|
+
* to clean up on unmount.
|
|
13
|
+
*/
|
|
14
|
+
export type InlinePanelCallbacks = {
|
|
15
|
+
onMount: (container: HTMLElement) => void;
|
|
16
|
+
onUnmount: () => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type InlinePanelShowOptions = {
|
|
20
|
+
/** Document position the panel anchors to. */
|
|
21
|
+
pos: number;
|
|
22
|
+
/**
|
|
23
|
+
* Where to render the panel relative to the line at `pos`. `'above'`
|
|
24
|
+
* places it before the line, `'below'` after it.
|
|
25
|
+
*
|
|
26
|
+
* @default 'above'
|
|
27
|
+
*/
|
|
28
|
+
placement?: 'above' | 'below';
|
|
29
|
+
} & InlinePanelCallbacks;
|
|
30
|
+
|
|
31
|
+
type ShowPayload = InlinePanelShowOptions | null;
|
|
32
|
+
|
|
33
|
+
export type InlinePanelController = {
|
|
34
|
+
/** CodeMirror extension to register on the editor. */
|
|
35
|
+
extension: Extension;
|
|
36
|
+
/** Build an effect that mounts the panel with the given options. */
|
|
37
|
+
show: (options: InlinePanelShowOptions) => StateEffect<ShowPayload>;
|
|
38
|
+
/** Build an effect that unmounts the panel. */
|
|
39
|
+
hide: () => StateEffect<ShowPayload>;
|
|
40
|
+
updateCallbacks: (callbacks: InlinePanelCallbacks) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function createInlinePanelController(): InlinePanelController {
|
|
44
|
+
const showEffect = StateEffect.define<ShowPayload>();
|
|
45
|
+
|
|
46
|
+
let callbacksRef: InlinePanelCallbacks | null = null;
|
|
47
|
+
|
|
48
|
+
class InlinePanelWidget extends WidgetType {
|
|
49
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
50
|
+
|
|
51
|
+
constructor(readonly options: InlinePanelShowOptions) {
|
|
52
|
+
super();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toDOM(view: EditorView): HTMLElement {
|
|
56
|
+
const container = document.createElement('div');
|
|
57
|
+
container.className = 'cm-inline-panel';
|
|
58
|
+
this.resizeObserver = new ResizeObserver(() => view.requestMeasure());
|
|
59
|
+
this.resizeObserver.observe(container);
|
|
60
|
+
callbacksRef?.onMount(container);
|
|
61
|
+
return container;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
destroy(): void {
|
|
65
|
+
this.resizeObserver?.disconnect();
|
|
66
|
+
this.resizeObserver = null;
|
|
67
|
+
callbacksRef?.onUnmount();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
eq(other: InlinePanelWidget): boolean {
|
|
71
|
+
return (
|
|
72
|
+
other.options.pos === this.options.pos &&
|
|
73
|
+
other.options.placement === this.options.placement
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ignoreEvent(): boolean {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const field = StateField.define<DecorationSet>({
|
|
83
|
+
create() {
|
|
84
|
+
return Decoration.none;
|
|
85
|
+
},
|
|
86
|
+
update(decorations, transaction) {
|
|
87
|
+
let next: DecorationSet | null = null;
|
|
88
|
+
for (const effect of transaction.effects) {
|
|
89
|
+
if (effect.is(showEffect)) {
|
|
90
|
+
if (effect.value === null) {
|
|
91
|
+
next = Decoration.none;
|
|
92
|
+
} else {
|
|
93
|
+
const side = effect.value.placement === 'below' ? 1 : -1;
|
|
94
|
+
next = Decoration.set([
|
|
95
|
+
Decoration.widget({
|
|
96
|
+
widget: new InlinePanelWidget(effect.value),
|
|
97
|
+
block: true,
|
|
98
|
+
side,
|
|
99
|
+
}).range(effect.value.pos),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (next !== null) {
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
return decorations.map(transaction.changes);
|
|
108
|
+
},
|
|
109
|
+
provide: (f) => EditorView.decorations.from(f),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
extension: field,
|
|
114
|
+
show: (options) => showEffect.of(options),
|
|
115
|
+
hide: () => showEffect.of(null),
|
|
116
|
+
updateCallbacks: (callbacks) => {
|
|
117
|
+
callbacksRef = callbacks;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|