@dxos/react-ui-editor 0.3.11-main.008b7c6 → 0.3.11-main.00a28cb
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/dist/lib/browser/index.mjs +558 -629
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +12 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +4 -4
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +4 -9
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +14 -5
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +9 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -1
- package/dist/types/src/extensions/image.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts +5 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -5
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +25 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +41 -37
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +71 -67
- package/src/components/TextEditor/comments.stories.tsx +355 -0
- package/src/extensions/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +12 -14
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/defs.ts +13 -9
- package/src/extensions/automerge/semaphore.ts +10 -12
- package/src/extensions/awareness.ts +27 -33
- package/src/extensions/code.ts +28 -23
- package/src/extensions/comments.ts +131 -144
- package/src/extensions/cursor.ts +46 -0
- package/src/extensions/hr.ts +10 -12
- package/src/extensions/image.ts +40 -22
- package/src/extensions/index.ts +2 -1
- package/src/extensions/link.ts +86 -88
- package/src/extensions/markdown/highlight.ts +3 -12
- package/src/extensions/outliner.ts +12 -0
- package/src/extensions/table.ts +10 -4
- package/src/extensions/tasklist.ts +54 -79
- package/src/extensions/yjs/cursor.ts +2 -1
- package/src/extensions/yjs/yjs.test.ts +1 -1
- package/src/extensions/yjs/yjs.ts +2 -2
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useTextModel.ts +4 -7
- package/src/hooks/yjs.ts +1 -23
- package/src/index.ts +3 -0
- package/src/styles/markdown.ts +0 -2
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +51 -78
- package/dist/types/src/extensions/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/extensions/mermaid.ts +0 -11
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
- /package/src/{util/util.ts → util.ts} +0 -0
|
@@ -4,20 +4,16 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { StateField, type Extension, type Transaction } from '@codemirror/state';
|
|
8
8
|
import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '@codemirror/view';
|
|
9
9
|
|
|
10
10
|
import { next as A } from '@dxos/automerge/automerge';
|
|
11
11
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
12
12
|
|
|
13
13
|
import { cursorConverter } from './cursor';
|
|
14
|
-
import { effectType, type IDocHandle, isReconcileTx, type Value } from './defs';
|
|
14
|
+
import { effectType, type IDocHandle, isReconcileTx, semaphoreFacet, type Value } from './defs';
|
|
15
15
|
import { PatchSemaphore } from './semaphore';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
19
|
-
combine: (values) => values.at(-1)!, // Take last.
|
|
20
|
-
});
|
|
16
|
+
import { Cursor } from '../cursor';
|
|
21
17
|
|
|
22
18
|
export type AutomergeOptions = {
|
|
23
19
|
handle: IDocHandle;
|
|
@@ -25,19 +21,20 @@ export type AutomergeOptions = {
|
|
|
25
21
|
};
|
|
26
22
|
|
|
27
23
|
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
28
|
-
// TODO(burdon): Rename.
|
|
29
24
|
const stateField: StateField<Value> = StateField.define({
|
|
30
25
|
create: () => ({
|
|
31
26
|
lastHeads: A.getHeads(handle.docSync()!),
|
|
32
27
|
unreconciledTransactions: [],
|
|
33
28
|
path: path.slice(),
|
|
34
29
|
}),
|
|
30
|
+
|
|
35
31
|
update: (value: Value, tr: Transaction) => {
|
|
36
32
|
const result: Value = {
|
|
37
33
|
lastHeads: value.lastHeads,
|
|
38
34
|
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
39
35
|
path: path.slice(),
|
|
40
36
|
};
|
|
37
|
+
|
|
41
38
|
let clearUnreconciled = false;
|
|
42
39
|
for (const effect of tr.effects) {
|
|
43
40
|
if (effect.is(effectType)) {
|
|
@@ -45,6 +42,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
45
42
|
clearUnreconciled = true;
|
|
46
43
|
}
|
|
47
44
|
}
|
|
45
|
+
|
|
48
46
|
if (clearUnreconciled) {
|
|
49
47
|
result.unreconciledTransactions = [];
|
|
50
48
|
} else {
|
|
@@ -52,23 +50,23 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
52
50
|
result.unreconciledTransactions.push(tr);
|
|
53
51
|
}
|
|
54
52
|
}
|
|
53
|
+
|
|
55
54
|
return result;
|
|
56
55
|
},
|
|
57
56
|
});
|
|
58
57
|
|
|
59
|
-
const semaphore = new PatchSemaphore(stateField);
|
|
58
|
+
const semaphore = new PatchSemaphore(handle, stateField);
|
|
60
59
|
|
|
61
60
|
const viewPlugin = ViewPlugin.fromClass(
|
|
62
|
-
class
|
|
61
|
+
class AutomergeViewPlugin implements PluginValue {
|
|
63
62
|
constructor(private readonly _view: EditorView) {
|
|
64
63
|
handle.addListener('change', this._handleChange);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
update(update: ViewUpdate) {
|
|
68
67
|
if (update.transactions.length > 0 && update.transactions.some((tr) => !isReconcileTx(tr))) {
|
|
69
|
-
// TODO(burdon): This is a hack to ensure that the view is updated after the transaction is applied.
|
|
70
68
|
queueMicrotask(() => {
|
|
71
|
-
this._view.state.facet(semaphoreFacet).reconcile(
|
|
69
|
+
this._view.state.facet(semaphoreFacet).reconcile(this._view);
|
|
72
70
|
});
|
|
73
71
|
}
|
|
74
72
|
}
|
|
@@ -78,14 +76,14 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
private _handleChange = () => {
|
|
81
|
-
this._view.state.facet(semaphoreFacet).reconcile(
|
|
79
|
+
this._view.state.facet(semaphoreFacet).reconcile(this._view);
|
|
82
80
|
};
|
|
83
81
|
},
|
|
84
82
|
);
|
|
85
83
|
|
|
86
84
|
return {
|
|
87
85
|
extension: [
|
|
88
|
-
|
|
86
|
+
Cursor.converter.of(cursorConverter(handle, path)),
|
|
89
87
|
semaphoreFacet.of(semaphore),
|
|
90
88
|
stateField,
|
|
91
89
|
viewPlugin,
|
|
@@ -8,7 +8,7 @@ import type { Prop } from '@dxos/automerge/automerge';
|
|
|
8
8
|
import { next as automerge } from '@dxos/automerge/automerge';
|
|
9
9
|
|
|
10
10
|
import { type IDocHandle } from './defs';
|
|
11
|
-
import { type CursorConverter } from '
|
|
11
|
+
import { type CursorConverter } from '../cursor';
|
|
12
12
|
|
|
13
13
|
export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConverter => ({
|
|
14
14
|
// TODO(burdon): Handle assoc to associate with a previous character.
|
|
@@ -26,6 +26,7 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
|
|
|
26
26
|
// NOTE: Slice is needed because getCursor mutates the array.
|
|
27
27
|
return automerge.getCursor(doc, path.slice(), pos);
|
|
28
28
|
},
|
|
29
|
+
|
|
29
30
|
fromCursor: (cursor) => {
|
|
30
31
|
if (cursor === '') {
|
|
31
32
|
return 0;
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
|
+
// Copyright 2024 Automerge
|
|
3
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
4
5
|
//
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
Annotation,
|
|
8
|
-
StateEffect,
|
|
9
|
-
type StateField,
|
|
10
|
-
type EditorState,
|
|
11
|
-
type Transaction,
|
|
12
|
-
type TransactionSpec,
|
|
13
|
-
} from '@codemirror/state';
|
|
7
|
+
import { Annotation, StateEffect, type StateField, type EditorState, type Transaction, Facet } from '@codemirror/state';
|
|
14
8
|
|
|
15
9
|
import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
16
10
|
|
|
11
|
+
import { type PatchSemaphore } from './semaphore';
|
|
12
|
+
|
|
17
13
|
export type Value = {
|
|
18
14
|
lastHeads: Heads;
|
|
19
15
|
path: Prop[];
|
|
20
16
|
unreconciledTransactions: Transaction[];
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
type UpdateHeads = {
|
|
19
|
+
export type UpdateHeads = {
|
|
24
20
|
newHeads: Heads;
|
|
25
21
|
};
|
|
26
22
|
|
|
@@ -36,6 +32,12 @@ export const reconcileAnnotationType = Annotation.define<unknown>();
|
|
|
36
32
|
|
|
37
33
|
export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
|
|
38
34
|
|
|
35
|
+
// TODO(burdon): Rename sync/create generic def (like cursor) to decouple automerge deps.
|
|
36
|
+
export const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
37
|
+
combine: (values) => values.at(-1)!, // Take last.
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/*
|
|
39
41
|
export const makeReconcile = (tr: TransactionSpec) => {
|
|
40
42
|
if (tr.annotations != null) {
|
|
41
43
|
if (tr.annotations instanceof Array) {
|
|
@@ -46,11 +48,13 @@ export const makeReconcile = (tr: TransactionSpec) => {
|
|
|
46
48
|
} else {
|
|
47
49
|
tr.annotations = [reconcileAnnotationType.of({})];
|
|
48
50
|
}
|
|
51
|
+
|
|
49
52
|
// return {
|
|
50
53
|
// ...tr,
|
|
51
54
|
// annotations: reconcileAnnotationType.of({})
|
|
52
55
|
// }
|
|
53
56
|
};
|
|
57
|
+
*/
|
|
54
58
|
|
|
55
59
|
export type IDocHandle<T = any> = {
|
|
56
60
|
docSync(): Doc<T> | undefined;
|
|
@@ -21,27 +21,23 @@ import {
|
|
|
21
21
|
import { updateAutomerge } from './update-automerge';
|
|
22
22
|
import { updateCodeMirror } from './update-codemirror';
|
|
23
23
|
|
|
24
|
-
type Doc<T> = automerge.Doc<T>;
|
|
25
|
-
type Heads = automerge.Heads;
|
|
26
|
-
|
|
27
|
-
type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
|
|
28
|
-
|
|
29
24
|
/**
|
|
30
|
-
*
|
|
25
|
+
* Implements three-way merge (on each mutation/key-stroke).
|
|
31
26
|
*/
|
|
32
27
|
export class PatchSemaphore {
|
|
33
28
|
_inReconcile = false;
|
|
34
|
-
_queue: Array<ChangeFn> = [];
|
|
35
29
|
|
|
36
|
-
constructor(private readonly _field: StateField<Value>) {}
|
|
30
|
+
constructor(private readonly _handle: IDocHandle, private readonly _field: StateField<Value>) {}
|
|
37
31
|
|
|
38
|
-
reconcile = (
|
|
32
|
+
reconcile = (view: EditorView) => {
|
|
39
33
|
if (this._inReconcile) {
|
|
40
34
|
return;
|
|
41
35
|
}
|
|
42
36
|
this._inReconcile = true;
|
|
43
37
|
|
|
44
38
|
const path = getPath(view.state, this._field);
|
|
39
|
+
|
|
40
|
+
// Get the heads before the unreconciled transactions are applied.
|
|
45
41
|
const oldHeads = getLastHeads(view.state, this._field);
|
|
46
42
|
let selection = view.state.selection;
|
|
47
43
|
|
|
@@ -58,16 +54,18 @@ export class PatchSemaphore {
|
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
// Apply the unreconciled transactions to the document.
|
|
61
|
-
let newHeads = updateAutomerge(this._field,
|
|
57
|
+
let newHeads = updateAutomerge(this._field, this._handle, transactions, view.state);
|
|
62
58
|
|
|
63
59
|
// NOTE: null and undefined each come from automerge and repo respectively.
|
|
64
60
|
if (newHeads === null || newHeads === undefined) {
|
|
65
61
|
// TODO: @alexjg this is the call that's resetting the editor state on click
|
|
66
|
-
newHeads = automerge.getHeads(
|
|
62
|
+
newHeads = automerge.getHeads(this._handle.docSync()!);
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
// Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
|
|
70
|
-
const diff = automerge.equals(oldHeads, newHeads)
|
|
66
|
+
const diff = automerge.equals(oldHeads, newHeads)
|
|
67
|
+
? []
|
|
68
|
+
: automerge.diff(this._handle.docSync()!, oldHeads, newHeads);
|
|
71
69
|
updateCodeMirror(view, selection, path, diff);
|
|
72
70
|
|
|
73
71
|
view.dispatch({
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Event } from '@dxos/async';
|
|
17
17
|
import { Context } from '@dxos/context';
|
|
18
18
|
|
|
19
|
-
import { CursorConverter } from '
|
|
19
|
+
import { Cursor, type CursorConverter } from './cursor';
|
|
20
20
|
|
|
21
21
|
export interface AwarenessProvider {
|
|
22
22
|
remoteStateChange: Event<void>;
|
|
@@ -91,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
91
91
|
private _lastHead?: number = undefined;
|
|
92
92
|
|
|
93
93
|
constructor(view: EditorView) {
|
|
94
|
-
this._cursorConverter = view.state.facet(
|
|
94
|
+
this._cursorConverter = view.state.facet(Cursor.converter);
|
|
95
95
|
this._provider = view.state.facet(AwarenessProvider);
|
|
96
96
|
this._provider.open();
|
|
97
97
|
this._provider.remoteStateChange.on(this._ctx, () => {
|
|
@@ -156,7 +156,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
156
156
|
to: end,
|
|
157
157
|
value: Decoration.mark({
|
|
158
158
|
attributes: { style: `background-color: ${lightColor}` },
|
|
159
|
-
class: 'cm-
|
|
159
|
+
class: 'cm-collab-selection',
|
|
160
160
|
}),
|
|
161
161
|
});
|
|
162
162
|
} else {
|
|
@@ -166,7 +166,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
166
166
|
to: startLine.from + startLine.length,
|
|
167
167
|
value: Decoration.mark({
|
|
168
168
|
attributes: { style: `background-color: ${color}` },
|
|
169
|
-
class: 'cm-
|
|
169
|
+
class: 'cm-collab-selection',
|
|
170
170
|
}),
|
|
171
171
|
});
|
|
172
172
|
|
|
@@ -176,7 +176,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
176
176
|
to: end,
|
|
177
177
|
value: Decoration.mark({
|
|
178
178
|
attributes: { style: `background-color: ${color}` },
|
|
179
|
-
class: 'cm-
|
|
179
|
+
class: 'cm-collab-selection',
|
|
180
180
|
}),
|
|
181
181
|
});
|
|
182
182
|
|
|
@@ -186,7 +186,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
186
186
|
from: linePos,
|
|
187
187
|
to: linePos,
|
|
188
188
|
value: Decoration.line({
|
|
189
|
-
attributes: { style: `background-color: ${color}`, class: 'cm-
|
|
189
|
+
attributes: { style: `background-color: ${color}`, class: 'cm-collab-selectionLine' },
|
|
190
190
|
}),
|
|
191
191
|
});
|
|
192
192
|
}
|
|
@@ -208,24 +208,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
class RemoteCaretWidget extends WidgetType {
|
|
211
|
-
constructor(
|
|
211
|
+
constructor(private readonly _name: string, private readonly _color: string) {
|
|
212
212
|
super();
|
|
213
|
-
this.name = name;
|
|
214
|
-
this.color = color;
|
|
215
213
|
}
|
|
216
214
|
|
|
217
|
-
toDOM(): HTMLElement {
|
|
215
|
+
override toDOM(): HTMLElement {
|
|
218
216
|
const span = document.createElement('span');
|
|
219
|
-
span.className = 'cm-
|
|
220
|
-
span.style.backgroundColor = this.
|
|
221
|
-
span.style.borderColor = this.
|
|
217
|
+
span.className = 'cm-collab-selectionCaret';
|
|
218
|
+
span.style.backgroundColor = this._color;
|
|
219
|
+
span.style.borderColor = this._color;
|
|
222
220
|
|
|
223
221
|
const dot = document.createElement('div');
|
|
224
|
-
dot.className = 'cm-
|
|
222
|
+
dot.className = 'cm-collab-selectionCaretDot';
|
|
225
223
|
|
|
226
224
|
const info = document.createElement('div');
|
|
227
|
-
info.className = 'cm-
|
|
228
|
-
info.innerText = this.
|
|
225
|
+
info.className = 'cm-collab-selectionInfo';
|
|
226
|
+
info.innerText = this._name;
|
|
229
227
|
|
|
230
228
|
span.appendChild(document.createTextNode('\u2060'));
|
|
231
229
|
span.appendChild(dot);
|
|
@@ -236,18 +234,14 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
236
234
|
return span;
|
|
237
235
|
}
|
|
238
236
|
|
|
239
|
-
override eq(widget: this) {
|
|
240
|
-
return widget.color === this.color;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
compare(widget: this) {
|
|
244
|
-
return widget.color === this.color;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
237
|
override updateDOM() {
|
|
248
238
|
return false;
|
|
249
239
|
}
|
|
250
240
|
|
|
241
|
+
override eq(widget: this) {
|
|
242
|
+
return widget._color === this._color;
|
|
243
|
+
}
|
|
244
|
+
|
|
251
245
|
override get estimatedHeight() {
|
|
252
246
|
return -1;
|
|
253
247
|
}
|
|
@@ -257,14 +251,14 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
257
251
|
}
|
|
258
252
|
}
|
|
259
253
|
|
|
260
|
-
// TODO(burdon):
|
|
254
|
+
// TODO(burdon): Mixed case?
|
|
261
255
|
const styles = EditorView.baseTheme({
|
|
262
|
-
'.cm-
|
|
263
|
-
'.cm-
|
|
256
|
+
'.cm-collab-selection': {},
|
|
257
|
+
'.cm-collab-selectionLine': {
|
|
264
258
|
padding: 0,
|
|
265
259
|
margin: '0px 2px 0px 4px',
|
|
266
260
|
},
|
|
267
|
-
'.cm-
|
|
261
|
+
'.cm-collab-selectionCaret': {
|
|
268
262
|
position: 'relative',
|
|
269
263
|
borderLeft: '1px solid black',
|
|
270
264
|
borderRight: '1px solid black',
|
|
@@ -273,7 +267,7 @@ const styles = EditorView.baseTheme({
|
|
|
273
267
|
boxSizing: 'border-box',
|
|
274
268
|
display: 'inline',
|
|
275
269
|
},
|
|
276
|
-
'.cm-
|
|
270
|
+
'.cm-collab-selectionCaretDot': {
|
|
277
271
|
borderRadius: '50%',
|
|
278
272
|
position: 'absolute',
|
|
279
273
|
width: '.4em',
|
|
@@ -284,11 +278,11 @@ const styles = EditorView.baseTheme({
|
|
|
284
278
|
transition: 'transform .3s ease-in-out',
|
|
285
279
|
boxSizing: 'border-box',
|
|
286
280
|
},
|
|
287
|
-
'.cm-
|
|
281
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
|
|
288
282
|
transformOrigin: 'bottom center',
|
|
289
283
|
transform: 'scale(0)',
|
|
290
284
|
},
|
|
291
|
-
'.cm-
|
|
285
|
+
'.cm-collab-selectionInfo': {
|
|
292
286
|
position: 'absolute',
|
|
293
287
|
top: '-1.05em',
|
|
294
288
|
left: '-1px',
|
|
@@ -304,12 +298,12 @@ const styles = EditorView.baseTheme({
|
|
|
304
298
|
zIndex: 101,
|
|
305
299
|
transition: 'opacity .3s ease-in-out',
|
|
306
300
|
backgroundColor: 'inherit',
|
|
307
|
-
//
|
|
301
|
+
// These should be separate.
|
|
308
302
|
opacity: 0,
|
|
309
303
|
transitionDelay: '0s',
|
|
310
304
|
whiteSpace: 'nowrap',
|
|
311
305
|
},
|
|
312
|
-
'.cm-
|
|
306
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionInfo': {
|
|
313
307
|
opacity: 1,
|
|
314
308
|
transitionDelay: '0s',
|
|
315
309
|
},
|
package/src/extensions/code.ts
CHANGED
|
@@ -11,24 +11,23 @@ import {
|
|
|
11
11
|
Decoration,
|
|
12
12
|
type BlockInfo,
|
|
13
13
|
} from '@codemirror/view';
|
|
14
|
-
import get from 'lodash.get';
|
|
15
14
|
|
|
16
15
|
import { mx } from '@dxos/react-ui-theme';
|
|
17
16
|
|
|
18
|
-
import {
|
|
17
|
+
import { getToken } from '../styles';
|
|
19
18
|
|
|
20
19
|
const CODE_REGEX = /```[\s\S]*?```/gs;
|
|
21
20
|
|
|
22
21
|
// TODO(burdon): Reconcile with theme.
|
|
23
22
|
const styles = EditorView.baseTheme({
|
|
24
23
|
'& .cm-codeblock': {
|
|
25
|
-
fontFamily:
|
|
24
|
+
fontFamily: getToken('fontFamily.mono', []).join(','),
|
|
26
25
|
},
|
|
27
26
|
'&light [aria-readonly="true"] .cm-codeblock': {
|
|
28
|
-
background:
|
|
27
|
+
background: getToken('extend.colors.neutral.50'),
|
|
29
28
|
},
|
|
30
29
|
'&dark [aria-readonly="true"] .cm-codeblock': {
|
|
31
|
-
background:
|
|
30
|
+
background: getToken('extend.colors.neutral.850'),
|
|
32
31
|
},
|
|
33
32
|
'& [aria-readonly="true"] .cm-codeblock': {
|
|
34
33
|
display: 'block',
|
|
@@ -54,22 +53,20 @@ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
|
54
53
|
};
|
|
55
54
|
|
|
56
55
|
export const code = () => {
|
|
57
|
-
const
|
|
56
|
+
const buildDecorations = (view: EditorView) => {
|
|
58
57
|
const decorations: Range<Decoration>[] = [];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
}
|
|
58
|
+
const text = view.state.doc.sliceString(0);
|
|
59
|
+
const matches = text.matchAll(CODE_REGEX);
|
|
60
|
+
const blocks = view.viewportLineBlocks;
|
|
61
|
+
for (const match of matches) {
|
|
62
|
+
const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
|
|
63
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
64
|
+
const block = blocks[i];
|
|
65
|
+
decorations.push(
|
|
66
|
+
Decoration.line({
|
|
67
|
+
class: mx('cm-codeblock', i === range[0] && 'cm-codeblock-first', i === range[1] && 'cm-codeblock-last'),
|
|
68
|
+
}).range(block.from),
|
|
69
|
+
);
|
|
73
70
|
}
|
|
74
71
|
}
|
|
75
72
|
|
|
@@ -79,14 +76,22 @@ export const code = () => {
|
|
|
79
76
|
return [
|
|
80
77
|
ViewPlugin.fromClass(
|
|
81
78
|
class {
|
|
79
|
+
hasFocus = false;
|
|
82
80
|
decorations: DecorationSet;
|
|
83
81
|
constructor(view: EditorView) {
|
|
84
|
-
this.decorations =
|
|
82
|
+
this.decorations = buildDecorations(view);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
update(update: ViewUpdate) {
|
|
88
|
-
if (
|
|
89
|
-
|
|
86
|
+
if (
|
|
87
|
+
// TODO(burdon): Generalize for other extensions.
|
|
88
|
+
this.hasFocus !== update.view.hasFocus ||
|
|
89
|
+
update.startState.readOnly !== update.view.state.readOnly ||
|
|
90
|
+
update.docChanged ||
|
|
91
|
+
update.viewportChanged
|
|
92
|
+
) {
|
|
93
|
+
this.hasFocus = update.view.hasFocus;
|
|
94
|
+
this.decorations = buildDecorations(update.view);
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
},
|