5
5
* @license MIT
6
6
*/
7
7
8
- // TODO: support reconnects with same id
9
-
10
- import { WebSocket } from 'partysocket' ;
11
8
import Logdown from 'logdown' ;
12
- import sharedb from 'sharedb/lib/sharedb' ;
13
9
import type {
14
10
AceMultiCursorManager ,
15
11
AceMultiSelectionManager ,
16
- AceRadarView ,
17
- IRangeData
12
+ AceRadarView
18
13
} from '@convergencelabs/ace-collab-ext' ;
19
- import { IIndexRange } from '@convergencelabs/ace-collab-ext/dist/types/IndexRange' ;
20
14
import { AceViewportUtil , AceRangeUtil } from '@convergencelabs/ace-collab-ext' ;
21
15
import type { Ace , EditSession } from 'ace-builds' ;
22
16
import type { IAceEditor } from 'react-ace/lib/types' ;
23
- import type { SharedbAcePlugin , SharedbAceUser } from './types' ;
17
+ import sharedb from 'sharedb/lib/sharedb' ;
18
+ import type { PresenceUpdate , SharedbAceUser } from './types' ;
24
19
25
20
function traverse ( object : any , path : string [ ] ) {
26
21
for ( const key of path ) {
@@ -33,22 +28,12 @@ interface SharedbAceBindingOptions {
33
28
ace : IAceEditor ;
34
29
doc : sharedb . Doc ;
35
30
user : SharedbAceUser ;
36
- cursorManager : AceMultiCursorManager ;
37
- selectionManager : AceMultiSelectionManager ;
38
- radarManager : AceRadarView ;
39
- usersPresence : sharedb . Presence ;
40
- pluginWS ?: WebSocket ;
31
+ cursorManager ?: AceMultiCursorManager ;
32
+ selectionManager ?: AceMultiSelectionManager ;
33
+ radarManager ?: AceRadarView ;
34
+ usersPresence : sharedb . Presence < PresenceUpdate > ;
41
35
path : string [ ] ;
42
- plugins ?: SharedbAcePlugin [ ] ;
43
- onError : ( err : unknown ) => unknown ;
44
- }
45
-
46
- interface PresenceUpdate {
47
- user : SharedbAceUser ;
48
- cursorPos ?: Ace . Point ;
49
- selectionRange ?: IRangeData [ ] ;
50
- radarViewRows ?: IIndexRange ;
51
- radarCursorRow ?: number ;
36
+ onError ?: ( err : unknown ) => unknown ;
52
37
}
53
38
54
39
class SharedbAceBinding {
@@ -60,11 +45,11 @@ class SharedbAceBinding {
60
45
61
46
path : string [ ] ;
62
47
63
- cursorManager : AceMultiCursorManager ;
64
- selectionManager : AceMultiSelectionManager ;
65
- radarManager : AceRadarView ;
48
+ cursorManager ? : AceMultiCursorManager ;
49
+ selectionManager ? : AceMultiSelectionManager ;
50
+ radarManager ? : AceRadarView ;
66
51
67
- usersPresence : sharedb . Presence ;
52
+ usersPresence : sharedb . Presence < PresenceUpdate > ;
68
53
69
54
logger : Logdown . Logger ;
70
55
@@ -74,7 +59,18 @@ class SharedbAceBinding {
74
59
75
60
localPresence ?: sharedb . LocalPresence < PresenceUpdate > ;
76
61
77
- onError : ( err : unknown ) => unknown ;
62
+ connectedUsers : Record < string , SharedbAceUser > = { } ;
63
+
64
+ docSubmitted : sharedb . Callback = ( err ) => {
65
+ if ( err ) {
66
+ this . onError ?.( err ) ;
67
+ this . logger . log ( `*local*: op error: ${ err } ` ) ;
68
+ } else {
69
+ this . logger . log ( '*local*: op submitted' ) ;
70
+ }
71
+ } ;
72
+
73
+ onError ?: ( err : unknown ) => unknown ;
78
74
79
75
/**
80
76
* Constructs the binding object.
@@ -93,11 +89,8 @@ class SharedbAceBinding {
93
89
* the selections in the editor
94
90
* @param {Object } options.usersPresence - ShareDB presence channel
95
91
* containing information of the users, including cursor positions
96
- * @param {Object } options.pluginWS - WebSocket connection for
97
- * sharedb-ace plugins
98
92
* @param {string[] } options.path - A lens, describing the nesting
99
93
* to the JSON document. It should point to a string.
100
- * @param {Object[] } options.plugins - array of sharedb-ace plugins
101
94
* @param {?function } options.onError - a callback on error
102
95
* @example
103
96
* const binding = new SharedbAceBinding({
@@ -108,8 +101,6 @@ class SharedbAceBinding {
108
101
* selectionManager: selectionManager,
109
102
* usersPresence: usersPresence,
110
103
* path: ["path"],
111
- * plugins: [ SharedbAceMultipleCursors ],
112
- * pluginWS: "http://localhost:3108/ws",
113
104
* })
114
105
*/
115
106
constructor ( options : SharedbAceBindingOptions ) {
@@ -125,12 +116,6 @@ class SharedbAceBinding {
125
116
this . onError = options . onError ;
126
117
this . logger = Logdown ( 'shareace' ) ;
127
118
128
- // Initialize plugins
129
- if ( options . pluginWS && options . plugins ) {
130
- const { pluginWS } = options ;
131
- options . plugins . forEach ( ( plugin ) => plugin ( pluginWS , this . editor ) ) ;
132
- }
133
-
134
119
// Set value of ace document to ShareDB document value
135
120
this . setInitialValue ( ) ;
136
121
@@ -146,10 +131,12 @@ class SharedbAceBinding {
146
131
this . session . setValue ( traverse ( this . doc . data , this . path ) ) ;
147
132
this . suppress = false ;
148
133
149
- this . cursorManager . removeAll ( ) ;
150
- this . selectionManager . removeAll ( ) ;
151
- // TODO: Remove all views for radarManager
152
- // this.radarManager.removeView();
134
+ this . cursorManager ?. removeAll ( ) ;
135
+ this . selectionManager ?. removeAll ( ) ;
136
+
137
+ // @ts -expect-error hotfix to remove all views in radarManager
138
+ this . radarManager ?. removeAllViews ( ) ;
139
+
153
140
this . initializeLocalPresence ( ) ;
154
141
for ( const [ id , update ] of Object . entries ( this . usersPresence . remotePresences ) ) {
155
142
this . updatePresence ( id , update ) ;
@@ -160,30 +147,35 @@ class SharedbAceBinding {
160
147
* Listens to the changes
161
148
*/
162
149
listen = ( ) => {
163
- // TODO: Also update view on window resize
164
- // TODO: Clicking on radar indicator is not exactly accurate
165
- this . session . on ( 'change' , this . onLocalChange ) ;
166
- this . session . on ( 'changeScrollTop' , this . onLocalChangeScrollTop ) ;
167
150
this . doc . on ( 'op' , this . onRemoteChange ) ;
168
151
this . doc . on ( 'load' , this . onRemoteReload ) ;
169
152
153
+ this . session . on ( 'change' , this . onLocalChange ) ;
170
154
this . usersPresence . on ( 'receive' , this . updatePresence ) ;
171
155
this . session . selection . on ( 'changeCursor' , this . onLocalCursorChange ) ;
172
156
this . session . selection . on ( 'changeSelection' , this . onLocalSelectionChange ) ;
157
+
158
+ // Hotfix for clicking on radar indicator to update local presence
159
+ // because editor.scrollToLine does not trigger changeScrollTop
160
+ // Generates a decent amount of traffic but it's ok for now
161
+ this . editor . renderer . on ( 'afterRender' , this . onLocalViewChange ) ;
162
+
163
+ this . session . on ( 'changeMode' , this . onLocalModeChange ) ;
173
164
} ;
174
165
175
166
/**
176
167
* Stop listening to changes
177
168
*/
178
169
unlisten = ( ) => {
179
- this . session . removeListener ( 'change' , this . onLocalChange ) ;
180
- this . session . off ( 'changeScrollTop' , this . onLocalChangeScrollTop ) ;
181
170
this . doc . off ( 'op' , this . onRemoteChange ) ;
182
171
this . doc . off ( 'load' , this . onRemoteReload ) ;
183
172
173
+ this . session . removeListener ( 'change' , this . onLocalChange ) ;
184
174
this . usersPresence . off ( 'receive' , this . updatePresence ) ;
185
175
this . session . selection . off ( 'changeCursor' , this . onLocalCursorChange ) ;
186
176
this . session . selection . off ( 'changeSelection' , this . onLocalSelectionChange ) ;
177
+ this . editor . renderer . off ( 'afterRender' , this . onLocalViewChange ) ;
178
+ this . session . off ( 'changeMode' , this . onLocalModeChange ) ;
187
179
} ;
188
180
189
181
/**
@@ -194,7 +186,6 @@ class SharedbAceBinding {
194
186
* @throws {Error } throws error if delta is malformed
195
187
*/
196
188
deltaTransform = ( delta : Ace . Delta ) : sharedb . Op => {
197
- // TODO: Use SubtypeOp to declare new operations
198
189
const aceDoc = this . session . getDocument ( ) ;
199
190
const start = aceDoc . positionToIndex ( delta . start ) ;
200
191
const end = aceDoc . positionToIndex ( delta . end ) ;
@@ -225,10 +216,9 @@ class SharedbAceBinding {
225
216
* @throws {Error } throws error on malformed op
226
217
*/
227
218
opTransform = ( ops : sharedb . Op [ ] ) : Ace . Delta [ ] => {
228
- const self = this ;
229
219
const opToDelta = ( op : sharedb . Op ) : Ace . Delta => {
230
220
const index = op . p . at ( - 1 ) as number ;
231
- const pos = self . session . doc . indexToPosition ( index , 0 ) ;
221
+ const pos = this . session . doc . indexToPosition ( index , 0 ) ;
232
222
const start = pos ;
233
223
let action : 'remove' | 'insert' ;
234
224
let lines : string [ ] ;
@@ -238,7 +228,7 @@ class SharedbAceBinding {
238
228
action = 'remove' ;
239
229
lines = op . sd . split ( '\n' ) ;
240
230
const count = lines . reduce ( ( total , line ) => total + line . length , lines . length - 1 ) ;
241
- end = self . session . doc . indexToPosition ( index + count , 0 ) ;
231
+ end = this . session . doc . indexToPosition ( index + count , 0 ) ;
242
232
} else if ( 'si' in op ) {
243
233
action = 'insert' ;
244
234
lines = op . si . split ( '\n' ) ;
@@ -287,35 +277,38 @@ class SharedbAceBinding {
287
277
const op = this . deltaTransform ( delta ) ;
288
278
this . logger . log ( `*local*: transformed op: ${ JSON . stringify ( op ) } ` ) ;
289
279
290
- const docSubmitted : sharedb . Callback = ( err ) => {
291
- if ( err ) {
292
- this . onError && this . onError ( err ) ;
293
- this . logger . log ( `*local*: op error: ${ err } ` ) ;
294
- } else {
295
- this . logger . log ( '*local*: op submitted' ) ;
296
- }
297
- } ;
298
-
299
280
if ( ! this . doc . type ) {
300
281
// likely previous operation failed, we're out of sync
301
282
// don't submitOp now
302
283
return ;
303
284
}
304
285
305
- this . doc . submitOp ( op , { source : this } , docSubmitted ) ;
286
+ this . doc . submitOp ( op , { source : this } , this . docSubmitted ) ;
306
287
} catch ( err ) {
307
- this . onError && this . onError ( err ) ;
288
+ this . onError ?. ( err ) ;
308
289
}
309
290
} ;
310
291
292
+ onLocalModeChange = ( ) => {
293
+ // TODO: This is wrong
294
+ // @ts -ignore
295
+ const modeString : string = this . session . getMode ( ) . $id ;
296
+ const mode = modeString . substring ( modeString . lastIndexOf ( '/' ) + 1 ) ;
297
+
298
+ this . localPresence ?. submit ( {
299
+ user : this . user ,
300
+ newMode : mode
301
+ } ) ;
302
+ } ;
303
+
311
304
/**
312
305
* Event Listener for remote events (ShareDB)
313
306
*
314
307
* @param {Object[] } ops - array of ShareDB ops
315
308
* @param {Object } source - which sharedb-ace-binding instance
316
309
* created the op. If self, don't apply the op.
317
310
*/
318
- onRemoteChange = ( ops : sharedb . Op [ ] , source : this | false , clientId ?: string ) => {
311
+ onRemoteChange = ( ops : sharedb . Op [ ] , source : this | false ) => {
319
312
try {
320
313
this . logger . log ( `*remote*: fired ${ Date . now ( ) } ` ) ;
321
314
@@ -341,41 +334,46 @@ class SharedbAceBinding {
341
334
this . logger . log ( JSON . stringify ( this . session . getValue ( ) ) ) ;
342
335
this . logger . log ( '*remote*: delta applied' ) ;
343
336
} catch ( err ) {
344
- this . onError && this . onError ( err ) ;
337
+ this . onError ?. ( err ) ;
345
338
}
346
339
} ;
347
340
348
341
updatePresence = ( id : string , update : PresenceUpdate ) => {
349
342
// TODO: logger and error handling
350
- // TODO: separate into multiple handlers
351
343
if ( update === null ) {
352
344
try {
353
- this . cursorManager . removeCursor ( id ) ;
345
+ this . cursorManager ? .removeCursor ( id ) ;
354
346
// eslint-disable-next-line no-empty
355
347
} catch { }
356
348
357
349
try {
358
- this . selectionManager . removeSelection ( id ) ;
350
+ this . selectionManager ? .removeSelection ( id ) ;
359
351
// eslint-disable-next-line no-empty
360
352
} catch { }
361
353
362
354
try {
363
- this . radarManager . removeView ( id ) ;
355
+ this . radarManager ? .removeView ( id ) ;
364
356
// eslint-disable-next-line no-empty
365
357
} catch { }
366
358
359
+ if ( id in this . connectedUsers ) {
360
+ delete this . connectedUsers [ id ] ;
361
+ }
362
+
367
363
return ;
368
364
}
369
365
370
- if ( update . cursorPos ) {
366
+ this . connectedUsers [ id ] = update . user ;
367
+
368
+ if ( this . cursorManager && update . cursorPos ) {
371
369
try {
372
370
this . cursorManager . setCursor ( id , update . cursorPos ) ;
373
371
} catch {
374
372
this . cursorManager . addCursor ( id , update . user . name , update . user . color , update . cursorPos ) ;
375
373
}
376
374
}
377
375
378
- if ( update . selectionRange ) {
376
+ if ( this . selectionManager && update . selectionRange ) {
379
377
const ranges = AceRangeUtil . fromJson ( update . selectionRange ) ;
380
378
try {
381
379
this . selectionManager . setSelection ( id , ranges ) ;
@@ -384,7 +382,7 @@ class SharedbAceBinding {
384
382
}
385
383
}
386
384
387
- if ( update . radarViewRows ) {
385
+ if ( this . radarManager && update . radarViewRows ) {
388
386
const rows = AceViewportUtil . indicesToRows (
389
387
this . editor ,
390
388
update . radarViewRows . start ,
@@ -402,9 +400,14 @@ class SharedbAceBinding {
402
400
) ;
403
401
}
404
402
}
403
+
404
+ // TODO: This is not the right way
405
+ if ( update . newMode ) {
406
+ this . session . setMode ( update . newMode ) ;
407
+ }
405
408
} ;
406
409
407
- onLocalChangeScrollTop = ( scrollTop : number ) => {
410
+ onLocalViewChange = ( ) => {
408
411
// TODO: logger and error handling
409
412
const viewportIndices = AceViewportUtil . getVisibleIndexRange ( this . editor ) ;
410
413
this . localPresence ?. submit ( {
0 commit comments