@neo4j-cypher/react-codemirror 2.0.0-next.35 → 2.0.0-next.37
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 +20 -0
- package/dist/src/CypherEditor.d.ts +28 -0
- package/dist/src/CypherEditor.js +66 -1
- package/dist/src/CypherEditor.js.map +1 -1
- package/dist/src/diffView.d.ts +13 -0
- package/dist/src/diffView.js +11 -0
- package/dist/src/diffView.js.map +1 -0
- package/dist/src/e2e_tests/syntaxValidation.spec.js +0 -3
- package/dist/src/e2e_tests/syntaxValidation.spec.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inlinePanel.d.ts +33 -0
- package/dist/src/inlinePanel.js +86 -0
- package/dist/src/inlinePanel.js.map +1 -0
- package/dist/src/lang-cypher/createCypherTheme.d.ts +12 -1
- package/dist/src/lang-cypher/createCypherTheme.js +20 -1
- package/dist/src/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/src/lang-cypher/lintWorker.mjs +247 -239
- package/dist/src/lang-cypher/syntaxValidation.js +6 -2
- package/dist/src/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/src/themes.js +14 -0
- package/dist/src/themes.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/CypherEditor.tsx +116 -6
- package/src/diffView.ts +24 -0
- package/src/e2e_tests/syntaxValidation.spec.tsx +0 -15
- package/src/index.ts +2 -0
- package/src/inlinePanel.ts +145 -0
- package/src/lang-cypher/createCypherTheme.ts +33 -0
- package/src/lang-cypher/lintWorker.mjs +247 -239
- package/src/lang-cypher/syntaxValidation.ts +7 -2
- package/src/themes.ts +17 -0
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.37",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"codemirror",
|
|
6
6
|
"codemirror 6",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@codemirror/commands": "^6.8.1",
|
|
35
35
|
"@codemirror/language": "^6.11.2",
|
|
36
36
|
"@codemirror/lint": "^6.8.5",
|
|
37
|
+
"@codemirror/merge": "^6.12.1",
|
|
37
38
|
"@codemirror/search": "^6.5.11",
|
|
38
39
|
"@codemirror/state": "^6.5.2",
|
|
39
40
|
"@codemirror/view": "^6.38.1",
|
|
@@ -47,8 +48,8 @@
|
|
|
47
48
|
"style-mod": "^4.1.2",
|
|
48
49
|
"vscode-languageserver-types": "^3.17.3",
|
|
49
50
|
"workerpool": "^9.3.3",
|
|
50
|
-
"@neo4j-cypher/
|
|
51
|
-
"@neo4j-cypher/
|
|
51
|
+
"@neo4j-cypher/language-support": "2.0.0-next.34",
|
|
52
|
+
"@neo4j-cypher/lint-worker": "1.10.1-next.11"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@neo4j-ndl/base": "^3.2.10",
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -13,6 +13,12 @@ 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';
|
|
21
|
+
import { createDiffExtension, type DiffProps } from './diffView';
|
|
16
22
|
import {
|
|
17
23
|
formatQuery,
|
|
18
24
|
CypherLanguageService,
|
|
@@ -180,8 +186,31 @@ export interface CypherEditorProps {
|
|
|
180
186
|
* @default false
|
|
181
187
|
*/
|
|
182
188
|
moveFocusOnTab?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Render a panel as a block widget inside the editor.
|
|
191
|
+
* The widget DOM is only rebuilt when `pos` or `placement` change
|
|
192
|
+
*/
|
|
193
|
+
inlinePanel?: InlinePanelProps | null;
|
|
194
|
+
/**
|
|
195
|
+
* Render a unified diff of the current document against `diff.original`.
|
|
196
|
+
* Deleted lines are shown as uneditable widgets.
|
|
197
|
+
*/
|
|
198
|
+
diff?: DiffProps | null;
|
|
183
199
|
}
|
|
184
200
|
|
|
201
|
+
export type InlinePanelProps = {
|
|
202
|
+
/**
|
|
203
|
+
* Position the panel anchors to.
|
|
204
|
+
*/
|
|
205
|
+
pos: number;
|
|
206
|
+
/**
|
|
207
|
+
* Whether to render above or below the line
|
|
208
|
+
*
|
|
209
|
+
* @default 'above'
|
|
210
|
+
*/
|
|
211
|
+
placement?: 'above' | 'below';
|
|
212
|
+
} & InlinePanelCallbacks;
|
|
213
|
+
|
|
185
214
|
const format = (view: EditorView): void => {
|
|
186
215
|
try {
|
|
187
216
|
const doc = view.state.doc.toString();
|
|
@@ -268,6 +297,7 @@ const lineNumbersCompartment = new Compartment();
|
|
|
268
297
|
const readOnlyCompartment = new Compartment();
|
|
269
298
|
const placeholderCompartment = new Compartment();
|
|
270
299
|
const domEventHandlerCompartment = new Compartment();
|
|
300
|
+
const diffCompartment = new Compartment();
|
|
271
301
|
|
|
272
302
|
const formatLineNumber =
|
|
273
303
|
(prompt?: string) => (a: number, state: EditorState) => {
|
|
@@ -290,10 +320,12 @@ class CodemirrorSymbolFetcher {
|
|
|
290
320
|
}
|
|
291
321
|
private languageService: CypherLanguageService;
|
|
292
322
|
private processing = false;
|
|
293
|
-
private nextJob:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
323
|
+
private nextJob:
|
|
324
|
+
| {
|
|
325
|
+
query: string;
|
|
326
|
+
schema: DbSchema;
|
|
327
|
+
}
|
|
328
|
+
| undefined;
|
|
297
329
|
private symbolTablePool = workerpool.pool(WorkerURL, {
|
|
298
330
|
minWorkers: 1,
|
|
299
331
|
workerOpts: { type: 'module' },
|
|
@@ -316,11 +348,11 @@ class CodemirrorSymbolFetcher {
|
|
|
316
348
|
this.processing = true;
|
|
317
349
|
while (this.nextJob) {
|
|
318
350
|
try {
|
|
319
|
-
const proxyWorker =
|
|
320
|
-
(await this.symbolTablePool.proxy()) as unknown as LintWorker;
|
|
321
351
|
const query = this.nextJob.query;
|
|
322
352
|
const dbSchema = this.nextJob.schema;
|
|
323
353
|
this.nextJob = undefined;
|
|
354
|
+
const proxyWorker =
|
|
355
|
+
(await this.symbolTablePool.proxy()) as unknown as LintWorker;
|
|
324
356
|
|
|
325
357
|
const result = await proxyWorker.lintCypherQuery(query, dbSchema);
|
|
326
358
|
|
|
@@ -360,6 +392,7 @@ export class CypherEditor extends Component<
|
|
|
360
392
|
*/
|
|
361
393
|
editorView: React.MutableRefObject<EditorView> = createRef();
|
|
362
394
|
private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
|
|
395
|
+
private inlinePanelController: InlinePanelController | null = null;
|
|
363
396
|
|
|
364
397
|
/**
|
|
365
398
|
* Format Cypher query
|
|
@@ -471,6 +504,8 @@ export class CypherEditor extends Component<
|
|
|
471
504
|
overrideThemeBackgroundColor,
|
|
472
505
|
);
|
|
473
506
|
|
|
507
|
+
this.inlinePanelController = createInlinePanelController();
|
|
508
|
+
|
|
474
509
|
const changeListener = this.debouncedOnChange
|
|
475
510
|
? [
|
|
476
511
|
EditorView.updateListener.of((upt: ViewUpdate) => {
|
|
@@ -536,6 +571,10 @@ export class CypherEditor extends Component<
|
|
|
536
571
|
'Press Escape to leave the editor and continue tabbing through the page',
|
|
537
572
|
})
|
|
538
573
|
: [],
|
|
574
|
+
this.inlinePanelController.extension,
|
|
575
|
+
diffCompartment.of(
|
|
576
|
+
this.props.diff ? createDiffExtension(this.props.diff) : [],
|
|
577
|
+
),
|
|
539
578
|
],
|
|
540
579
|
doc: this.props.value,
|
|
541
580
|
});
|
|
@@ -553,6 +592,53 @@ export class CypherEditor extends Component<
|
|
|
553
592
|
} else if (this.props.offset) {
|
|
554
593
|
this.updateCursorPosition(this.props.offset);
|
|
555
594
|
}
|
|
595
|
+
|
|
596
|
+
if (this.props.inlinePanel) {
|
|
597
|
+
this.openInlinePanel(this.props.inlinePanel);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private openInlinePanel(
|
|
602
|
+
props: NonNullable<CypherEditorProps['inlinePanel']>,
|
|
603
|
+
): void {
|
|
604
|
+
const view = this.editorView.current;
|
|
605
|
+
const controller = this.inlinePanelController;
|
|
606
|
+
if (!view || !controller) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const pos = Math.max(0, Math.min(props.pos, view.state.doc.length));
|
|
611
|
+
const line = view.state.doc.lineAt(pos);
|
|
612
|
+
controller.updateCallbacks({
|
|
613
|
+
onMount: props.onMount,
|
|
614
|
+
onUnmount: props.onUnmount,
|
|
615
|
+
});
|
|
616
|
+
view.dispatch({
|
|
617
|
+
effects: controller.show({
|
|
618
|
+
pos: props.placement === 'below' ? line.to : line.from,
|
|
619
|
+
placement: props.placement,
|
|
620
|
+
onMount: props.onMount,
|
|
621
|
+
onUnmount: props.onUnmount,
|
|
622
|
+
}),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private updateInlinePanel(
|
|
627
|
+
props: NonNullable<CypherEditorProps['inlinePanel']>,
|
|
628
|
+
): void {
|
|
629
|
+
this.inlinePanelController?.updateCallbacks({
|
|
630
|
+
onMount: props.onMount,
|
|
631
|
+
onUnmount: props.onUnmount,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private closeInlinePanel(): void {
|
|
636
|
+
const view = this.editorView.current;
|
|
637
|
+
const controller = this.inlinePanelController;
|
|
638
|
+
if (!view || !controller) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
view.dispatch({ effects: controller.hide() });
|
|
556
642
|
}
|
|
557
643
|
|
|
558
644
|
componentDidUpdate(prevProps: CypherEditorProps): void {
|
|
@@ -642,6 +728,30 @@ export class CypherEditor extends Component<
|
|
|
642
728
|
});
|
|
643
729
|
}
|
|
644
730
|
|
|
731
|
+
const prevPanel = prevProps.inlinePanel;
|
|
732
|
+
const nextPanel = this.props.inlinePanel;
|
|
733
|
+
if (prevPanel !== nextPanel) {
|
|
734
|
+
if (!nextPanel) {
|
|
735
|
+
this.closeInlinePanel();
|
|
736
|
+
} else if (
|
|
737
|
+
!prevPanel ||
|
|
738
|
+
prevPanel.pos !== nextPanel.pos ||
|
|
739
|
+
prevPanel.placement !== nextPanel.placement
|
|
740
|
+
) {
|
|
741
|
+
this.openInlinePanel(nextPanel);
|
|
742
|
+
} else {
|
|
743
|
+
this.updateInlinePanel(nextPanel);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (prevProps.diff?.original !== this.props.diff?.original) {
|
|
748
|
+
this.editorView.current.dispatch({
|
|
749
|
+
effects: diffCompartment.reconfigure(
|
|
750
|
+
this.props.diff ? createDiffExtension(this.props.diff) : [],
|
|
751
|
+
),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
645
755
|
if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
|
|
646
756
|
this.editorView.current.dispatch({
|
|
647
757
|
effects: domEventHandlerCompartment.reconfigure(
|
package/src/diffView.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { unifiedMergeView } from '@codemirror/merge';
|
|
2
|
+
import type { Extension } from '@codemirror/state';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for rendering an inline diff in the editor.
|
|
6
|
+
*
|
|
7
|
+
* The diff is computed between {@link DiffProps.original} and the *current*
|
|
8
|
+
* editor document, so streaming/external updates to the document re-diff
|
|
9
|
+
* automatically against the same original.
|
|
10
|
+
*/
|
|
11
|
+
export type DiffProps = {
|
|
12
|
+
/** The baseline document the current editor content is compared against. */
|
|
13
|
+
original: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createDiffExtension({ original }: DiffProps): Extension {
|
|
17
|
+
return unifiedMergeView({
|
|
18
|
+
original,
|
|
19
|
+
highlightChanges: true,
|
|
20
|
+
syntaxHighlightDeletions: true,
|
|
21
|
+
mergeControls: false,
|
|
22
|
+
gutter: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -131,11 +131,6 @@ test('Semantic errors are correctly accumulated', async ({ page, mount }) => {
|
|
|
131
131
|
|
|
132
132
|
await mount(<CypherEditor value={query} />);
|
|
133
133
|
|
|
134
|
-
await editorPage.checkErrorMessage(
|
|
135
|
-
'MATCH (n)',
|
|
136
|
-
'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).',
|
|
137
|
-
);
|
|
138
|
-
|
|
139
134
|
await editorPage.checkErrorMessage(
|
|
140
135
|
'-1',
|
|
141
136
|
"Invalid input. '-1' is not a valid value. Must be a positive integer.",
|
|
@@ -151,11 +146,6 @@ test('Multiline errors are correctly placed', async ({ page, mount }) => {
|
|
|
151
146
|
|
|
152
147
|
await mount(<CypherEditor value={query} />);
|
|
153
148
|
|
|
154
|
-
await editorPage.checkErrorMessage(
|
|
155
|
-
'MATCH (n)',
|
|
156
|
-
'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).',
|
|
157
|
-
);
|
|
158
|
-
|
|
159
149
|
await editorPage.checkErrorMessage(
|
|
160
150
|
'-1',
|
|
161
151
|
"Invalid input. '-1' is not a valid value. Must be a positive integer.",
|
|
@@ -171,11 +161,6 @@ test('Validation errors are correctly overlapped', async ({ page, mount }) => {
|
|
|
171
161
|
|
|
172
162
|
await mount(<CypherEditor value={query} />);
|
|
173
163
|
|
|
174
|
-
await editorPage.checkErrorMessage(
|
|
175
|
-
'-1',
|
|
176
|
-
'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).',
|
|
177
|
-
);
|
|
178
|
-
|
|
179
164
|
await editorPage.checkErrorMessage(
|
|
180
165
|
'-1',
|
|
181
166
|
"Invalid input. '-1' is not a valid value. Must be a positive integer.",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * as LanguageSupport from '@neo4j-cypher/language-support';
|
|
2
2
|
export { CypherEditor } from './CypherEditor';
|
|
3
|
+
export type { InlinePanelProps } from './CypherEditor';
|
|
4
|
+
export type { DiffProps } from './diffView';
|
|
3
5
|
export { cypher } from './lang-cypher/langCypher';
|
|
4
6
|
export { darkThemeConstants, lightThemeConstants } from './themes';
|
|
@@ -0,0 +1,145 @@
|
|
|
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 buildDecoration = (options: InlinePanelShowOptions): DecorationSet =>
|
|
83
|
+
Decoration.set([
|
|
84
|
+
Decoration.widget({
|
|
85
|
+
widget: new InlinePanelWidget(options),
|
|
86
|
+
block: true,
|
|
87
|
+
side: options.placement === 'below' ? 1 : -1,
|
|
88
|
+
}).range(options.pos),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const readOptions = (
|
|
92
|
+
decorations: DecorationSet,
|
|
93
|
+
): InlinePanelShowOptions | null => {
|
|
94
|
+
const widget = decorations.iter().value?.spec?.widget;
|
|
95
|
+
return widget instanceof InlinePanelWidget ? widget.options : null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const field = StateField.define<DecorationSet>({
|
|
99
|
+
create() {
|
|
100
|
+
return Decoration.none;
|
|
101
|
+
},
|
|
102
|
+
update(decorations, transaction) {
|
|
103
|
+
for (const effect of transaction.effects) {
|
|
104
|
+
if (effect.is(showEffect)) {
|
|
105
|
+
return effect.value === null
|
|
106
|
+
? Decoration.none
|
|
107
|
+
: buildDecoration(effect.value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Nothing to do when the panel is closed, or when the document is unchanged
|
|
112
|
+
if (decorations.size === 0 || !transaction.docChanged) {
|
|
113
|
+
return decorations.map(transaction.changes);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Re-anchor manually because CodeMirror's default mapping silently drops
|
|
117
|
+
// block-widget decorations when their anchor line is deleted
|
|
118
|
+
const options = readOptions(decorations);
|
|
119
|
+
if (options === null) {
|
|
120
|
+
return decorations.map(transaction.changes);
|
|
121
|
+
}
|
|
122
|
+
const side = options.placement === 'below' ? 1 : -1;
|
|
123
|
+
const lineNumber = Math.min(
|
|
124
|
+
transaction.startState.doc.lineAt(options.pos).number,
|
|
125
|
+
transaction.newDoc.lines,
|
|
126
|
+
);
|
|
127
|
+
const line = transaction.newDoc.line(lineNumber);
|
|
128
|
+
|
|
129
|
+
return buildDecoration({
|
|
130
|
+
...options,
|
|
131
|
+
pos: side === 1 ? line.to : line.from,
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
provide: (f) => EditorView.decorations.from(f),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
extension: field,
|
|
139
|
+
show: (options) => showEffect.of(options),
|
|
140
|
+
hide: () => showEffect.of(null),
|
|
141
|
+
updateCallbacks: (callbacks) => {
|
|
142
|
+
callbacksRef = callbacks;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -18,6 +18,17 @@ import {
|
|
|
18
18
|
upArrowSvg,
|
|
19
19
|
} from './themeIcons';
|
|
20
20
|
|
|
21
|
+
export interface DiffColors {
|
|
22
|
+
/** Background fill for the whole inserted/changed line. */
|
|
23
|
+
insertedLine: string;
|
|
24
|
+
/** Highlight for the exact inserted text within a changed line. */
|
|
25
|
+
insertedText: string;
|
|
26
|
+
/** Background fill for the whole deleted line widget. */
|
|
27
|
+
deletedLine: string;
|
|
28
|
+
/** Highlight for the exact deleted text within a deleted chunk. */
|
|
29
|
+
deletedText: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export interface ThemeOptions {
|
|
22
33
|
dark: boolean;
|
|
23
34
|
editorSettings: {
|
|
@@ -40,6 +51,7 @@ export interface ThemeOptions {
|
|
|
40
51
|
};
|
|
41
52
|
highlightStyles: Partial<Record<HighlightedCypherTokenTypes, string>>;
|
|
42
53
|
inheritBgColor?: boolean;
|
|
54
|
+
diffColors?: DiffColors;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export const createCypherTheme = ({
|
|
@@ -47,6 +59,7 @@ export const createCypherTheme = ({
|
|
|
47
59
|
editorSettings: settings,
|
|
48
60
|
highlightStyles,
|
|
49
61
|
inheritBgColor,
|
|
62
|
+
diffColors,
|
|
50
63
|
}: ThemeOptions): Extension => {
|
|
51
64
|
const themeOptions: Record<string, StyleSpec> = {
|
|
52
65
|
'&': {
|
|
@@ -218,6 +231,26 @@ export const createCypherTheme = ({
|
|
|
218
231
|
},
|
|
219
232
|
},
|
|
220
233
|
},
|
|
234
|
+
...(diffColors && {
|
|
235
|
+
'&.cm-merge-b .cm-changedLine': {
|
|
236
|
+
backgroundColor: diffColors.insertedLine,
|
|
237
|
+
},
|
|
238
|
+
'&.cm-merge-b .cm-changedText': {
|
|
239
|
+
background: diffColors.insertedText,
|
|
240
|
+
},
|
|
241
|
+
'&.cm-merge-b .cm-deletedChunk': {
|
|
242
|
+
backgroundColor: diffColors.deletedLine,
|
|
243
|
+
},
|
|
244
|
+
'&.cm-merge-b .cm-deletedChunk .cm-deletedText': {
|
|
245
|
+
background: diffColors.deletedText,
|
|
246
|
+
},
|
|
247
|
+
// Hide the empty deletion widget that unifiedMergeView renders
|
|
248
|
+
// when the original document is empty (its only line is an empty <del>).
|
|
249
|
+
'&.cm-merge-b .cm-deletedChunk:has(> .cm-deletedLine:only-child > del > br:only-child)':
|
|
250
|
+
{
|
|
251
|
+
display: 'none',
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
221
254
|
};
|
|
222
255
|
|
|
223
256
|
const themeExtension = EditorView.theme(themeOptions, { dark });
|