1
1
import { Cursor , Loro , UndoManager } from "loro-crdt" ;
2
- import { EditorState , Plugin , PluginKey , StateField , TextSelection , Transaction } from "prosemirror-state" ;
2
+ import {
3
+ EditorState ,
4
+ Plugin ,
5
+ PluginKey ,
6
+ StateField ,
7
+ TextSelection ,
8
+ Transaction ,
9
+ } from "prosemirror-state" ;
3
10
import { EditorView } from "prosemirror-view" ;
4
- import { convertPmSelectionToCursors , cursorToAbsolutePosition } from "./cursor-plugin" ;
11
+ import {
12
+ convertPmSelectionToCursors ,
13
+ cursorToAbsolutePosition ,
14
+ } from "./cursor-plugin" ;
5
15
import { loroSyncPluginKey } from "./sync-plugin" ;
6
16
7
17
export interface LoroUndoPluginProps {
8
18
doc : Loro ;
9
- undoManager ?: UndoManager
19
+ undoManager ?: UndoManager ;
10
20
}
11
21
12
- export const loroUndoPluginKey = new PluginKey < LoroUndoPluginState > ( "loro-undo" ) ;
22
+ export const loroUndoPluginKey = new PluginKey < LoroUndoPluginState > (
23
+ "loro-undo" ,
24
+ ) ;
13
25
14
26
interface LoroUndoPluginState {
15
- undoManager : UndoManager ,
16
- canUndo : boolean ,
17
- canRedo : boolean
27
+ undoManager : UndoManager ;
28
+ canUndo : boolean ;
29
+ canRedo : boolean ;
18
30
}
19
31
20
- type Cursors = { anchor : Cursor | null , focus : Cursor | null } ;
32
+ type Cursors = { anchor : Cursor | null ; focus : Cursor | null } ;
21
33
export const LoroUndoPlugin = ( props : LoroUndoPluginProps ) : Plugin => {
22
34
const undoManager = props . undoManager || new UndoManager ( props . doc , { } ) ;
23
35
let lastSelection : Cursors = {
24
36
anchor : null ,
25
- focus : null
26
- }
37
+ focus : null ,
38
+ } ;
27
39
return new Plugin ( {
28
40
key : loroUndoPluginKey ,
29
41
state : {
30
42
init : ( config , editorState ) : LoroUndoPluginState => {
31
- undoManager . addExcludeOriginPrefix ( "sys:init" )
43
+ undoManager . addExcludeOriginPrefix ( "sys:init" ) ;
32
44
return {
33
45
undoManager,
34
46
canUndo : undoManager . canUndo ( ) ,
35
47
canRedo : undoManager . canRedo ( ) ,
36
- }
48
+ } ;
37
49
} ,
38
50
apply : ( tr , state , oldEditorState , newEditorState ) => {
39
51
const undoState = loroUndoPluginKey . getState ( oldEditorState ) ;
@@ -44,104 +56,55 @@ export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
44
56
45
57
const canUndo = undoState . undoManager . canUndo ( ) ;
46
58
const canRedo = undoState . undoManager . canRedo ( ) ;
47
- const { anchor, focus } = convertPmSelectionToCursors ( oldEditorState . doc , oldEditorState . selection , loroState ) ;
48
- lastSelection = {
49
- anchor : anchor ?? null ,
50
- focus : focus ?? null
59
+ {
60
+ const { anchor, focus } = convertPmSelectionToCursors (
61
+ oldEditorState . doc ,
62
+ oldEditorState . selection ,
63
+ loroState ,
64
+ ) ;
65
+ lastSelection = {
66
+ anchor : anchor ?? null ,
67
+ focus : focus ?? null ,
68
+ } ;
51
69
}
52
70
return {
53
71
...state ,
54
72
canUndo,
55
73
canRedo,
56
- }
74
+ } ;
57
75
} ,
58
76
} as StateField < LoroUndoPluginState > ,
59
77
60
78
view : ( view : EditorView ) => {
61
- // When in the undo/redo loop, the new undo/redo stack item should restore the selection
62
- // to the state it was in before the item that was popped two steps ago from the stack.
63
- //
64
- // ┌────────────┐
65
- // │Selection 1 │
66
- // └─────┬──────┘
67
- // │ Some
68
- // ▼ ops
69
- // ┌────────────┐
70
- // │Selection 2 │
71
- // └─────┬──────┘
72
- // │ Some
73
- // ▼ ops
74
- // ┌────────────┐
75
- // │Selection 3 │◁ ─ ─ ─ ─ ─ ─ ─ Restore ─ ─ ─
76
- // └─────┬──────┘ │
77
- // │
78
- // │ │
79
- // │ ┌ ─ ─ ─ ─ ─ ─ ─
80
- // Enter the │ Undo ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ Push Redo │
81
- // undo/redo ─ ─ ─ ▶ ▼ └ ─ ─ ─ ─ ─ ─ ─
82
- // loop ┌────────────┐ │
83
- // │Selection 2 │◁─ ─ ─ Restore ─
84
- // └─────┬──────┘ │ │
85
- // │
86
- // │ │ │
87
- // │ ┌ ─ ─ ─ ─ ─ ─ ─
88
- // │ Undo ─ ─ ─ ─ ▶ Push Redo │ │
89
- // ▼ └ ─ ─ ─ ─ ─ ─ ─
90
- // ┌────────────┐ │ │
91
- // │Selection 1 │
92
- // └─────┬──────┘ │ │
93
- // │ Redo ◀ ─ ─ ─ ─ ─ ─ ─ ─
94
- // ▼ │
95
- // ┌────────────┐
96
- // ┌ Restore ─ ▷│Selection 2 │ │
97
- // └─────┬──────┘
98
- // │ │ │
99
- // ┌ ─ ─ ─ ─ ─ ─ ─ │
100
- // Push Undo │◀─ ─ ─ ─ ─ ─ ─ │ Redo ◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
101
- // └ ─ ─ ─ ─ ─ ─ ─ ▼
102
- // │ ┌────────────┐
103
- // │Selection 3 │
104
- // │ └─────┬──────┘
105
- // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶ │ Undo
106
- // ▼
107
- // ┌────────────┐
108
- // │Selection 2 │
109
- // └────────────┘
110
- //
111
- // Because users may change the selections during the undo/redo loop, it's
112
- // more stable to keep the selection stored in the last stack item
113
- // rather than using the current selection directly.
114
-
115
- let lastUndoRedoLoopSelection : Cursors | null = null ;
116
- let justPopped = false ;
117
- props . doc . subscribe ( event => {
118
- if ( event . by === "import" ) {
119
- lastUndoRedoLoopSelection = null ;
120
- }
121
- } ) ;
122
-
123
79
undoManager . setOnPush ( ( isUndo , _counterRange ) => {
124
- if ( ! justPopped ) {
125
- // A new op is being pushed to the undo stack, so it breaks the
126
- // undo/redo loop.
127
- console . assert ( isUndo ) ;
128
- lastUndoRedoLoopSelection = null ;
129
- }
130
-
131
80
const loroState = loroSyncPluginKey . getState ( view . state ) ;
132
81
if ( loroState ?. doc == null ) {
133
82
return {
134
83
value : null ,
135
- cursors : [ ]
84
+ cursors : [ ] ,
136
85
} ;
137
86
}
138
87
139
88
const cursors : Cursor [ ] = [ ] ;
140
- if ( lastSelection . anchor ) {
141
- cursors . push ( lastSelection . anchor ) ;
89
+ let selection = lastSelection ;
90
+ if ( ! isUndo ) {
91
+ const loroState = loroSyncPluginKey . getState ( view . state ) ;
92
+ if ( loroState ) {
93
+ const { anchor, focus } = convertPmSelectionToCursors (
94
+ view . state . doc ,
95
+ view . state . selection ,
96
+ loroState ,
97
+ ) ;
98
+ selection . anchor = anchor || null ;
99
+ selection . focus = focus || null ;
100
+ }
101
+ }
102
+
103
+ if ( selection . anchor ) {
104
+ cursors . push ( selection . anchor ) ;
142
105
}
143
- if ( lastSelection . focus ) {
144
- cursors . push ( lastSelection . focus ) ;
106
+ if ( selection . focus ) {
107
+ cursors . push ( selection . focus ) ;
145
108
}
146
109
147
110
return {
@@ -151,10 +114,10 @@ export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
151
114
// the cursors to their new positions. Additionally, if containers are deleted
152
115
// and recreated, they also need remapping. Remote changes to the document
153
116
// should be considered in these transformations.
154
- cursors
155
- }
156
- } )
157
- undoManager . setOnPop ( ( isUndo , meta , counterRange ) => {
117
+ cursors,
118
+ } ;
119
+ } ) ;
120
+ undoManager . setOnPop ( ( _isUndo , meta , _counterRange ) => {
158
121
// After this call, the `onPush` will be called immediately.
159
122
// The immediate `onPush` will contain the inverse operations that undone the effect caused by the current `onPop`
160
123
const loroState = loroSyncPluginKey . getState ( view . state ) ;
@@ -163,44 +126,43 @@ export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
163
126
}
164
127
165
128
const anchor = meta . cursors [ 0 ] ?? null ;
166
- const focus = meta . cursors [ 1 ] ?? null
129
+ const focus = meta . cursors [ 1 ] ?? null ;
167
130
if ( anchor == null ) {
168
131
return ;
169
132
}
170
133
171
- if ( lastUndoRedoLoopSelection ) {
172
- // We overwrite the lastSelection so that the corresponding `onPush`
173
- // will restore the selection to the state it was in before the
174
- // item that was popped two steps ago from the stack.
175
- lastSelection = lastUndoRedoLoopSelection ;
176
- }
177
-
178
- lastUndoRedoLoopSelection = {
179
- anchor,
180
- focus
181
- } ;
182
-
183
- justPopped = true ;
184
134
setTimeout ( ( ) => {
185
135
try {
186
- justPopped = false ;
187
- const anchorPos = cursorToAbsolutePosition ( anchor , loroState . doc , loroState . mapping ) [ 0 ] ;
188
- const focusPos = focus && cursorToAbsolutePosition ( focus , loroState . doc , loroState . mapping ) [ 0 ] ;
189
- const selection = TextSelection . create ( view . state . doc , anchorPos , focusPos ?? undefined )
136
+ const anchorPos = cursorToAbsolutePosition (
137
+ anchor ,
138
+ loroState . doc ,
139
+ loroState . mapping ,
140
+ ) [ 0 ] ;
141
+ const focusPos =
142
+ focus &&
143
+ cursorToAbsolutePosition (
144
+ focus ,
145
+ loroState . doc ,
146
+ loroState . mapping ,
147
+ ) [ 0 ] ;
148
+ const selection = TextSelection . create (
149
+ view . state . doc ,
150
+ anchorPos ,
151
+ focusPos ?? undefined ,
152
+ ) ;
190
153
view . dispatch ( view . state . tr . setSelection ( selection ) ) ;
191
154
} catch ( e ) {
192
155
console . error ( e ) ;
193
156
}
194
- } , 0 )
157
+ } , 0 ) ;
195
158
} ) ;
196
159
return {
197
160
destroy : ( ) => {
198
161
undoManager . setOnPop ( ) ;
199
162
undoManager . setOnPush ( ) ;
200
- }
201
- }
202
-
203
- }
163
+ } ,
164
+ } ;
165
+ } ,
204
166
} ) ;
205
167
} ;
206
168
@@ -214,34 +176,38 @@ export function canRedo(state: EditorState): boolean {
214
176
return undoState ?. undoManager . canRedo ( ) || false ;
215
177
}
216
178
217
- export function undo ( state : EditorState , dispatch : ( tr : Transaction ) => void ) : boolean {
179
+ export function undo (
180
+ state : EditorState ,
181
+ dispatch : ( tr : Transaction ) => void ,
182
+ ) : boolean {
218
183
const undoState = loroUndoPluginKey . getState ( state ) ;
219
184
if ( ! undoState || ! undoState . undoManager . canUndo ( ) ) {
220
185
return false ;
221
186
}
222
187
223
188
if ( ! undoState . undoManager . undo ( ) ) {
224
189
const emptyTr = state . tr ;
225
- dispatch ( emptyTr )
226
- return false
190
+ dispatch ( emptyTr ) ;
191
+ return false ;
227
192
}
228
193
229
- return true
194
+ return true ;
230
195
}
231
196
232
-
233
- export function redo ( state : EditorState , dispatch : ( tr : Transaction ) => void ) : boolean {
197
+ export function redo (
198
+ state : EditorState ,
199
+ dispatch : ( tr : Transaction ) => void ,
200
+ ) : boolean {
234
201
const undoState = loroUndoPluginKey . getState ( state ) ;
235
202
if ( ! undoState || ! undoState . undoManager . canRedo ( ) ) {
236
203
return false ;
237
204
}
238
205
239
206
if ( ! undoState . undoManager . redo ( ) ) {
240
207
const emptyTr = state . tr ;
241
- dispatch ( emptyTr )
242
- return false
208
+ dispatch ( emptyTr ) ;
209
+ return false ;
243
210
}
244
211
245
- return true
212
+ return true ;
246
213
}
247
-
0 commit comments