@hereugo/open-collaboration-monaco 0.3.3

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.
@@ -0,0 +1,688 @@
1
+ // ******************************************************************************
2
+ // Copyright 2024 TypeFox GmbH
3
+ // This program and the accompanying materials are made available under the
4
+ // terms of the MIT License, which is available in the project root.
5
+ // ******************************************************************************
6
+
7
+ import { Deferred, DisposableCollection, ProtocolBroadcastConnection, encodeUserPermission, getUserPermissionKey, resolveReadonly } from '@hereugo/open-collaboration-protocol';
8
+ import * as Y from 'yjs';
9
+ import * as monaco from 'monaco-editor';
10
+ import * as awarenessProtocol from 'y-protocols/awareness';
11
+ import * as types from '@hereugo/open-collaboration-protocol';
12
+ import { LOCAL_ORIGIN, OpenCollaborationYjsProvider, YTextChange, YTextChangeDelta } from '@hereugo/open-collaboration-yjs';
13
+ import { createMutex } from 'lib0/mutex';
14
+ import { debounce } from 'lodash';
15
+ import { MonacoCollabCallbacks } from './monaco-api.js';
16
+ import { DisposablePeer } from './collaboration-peer.js';
17
+
18
+ export type UsersChangeEvent = () => void;
19
+ export type FileNameChangeEvent = (fileName: string) => void;
20
+
21
+ export interface Disposable {
22
+ dispose(): void;
23
+ }
24
+
25
+ export interface CollaborationInstanceOptions {
26
+ connection: ProtocolBroadcastConnection;
27
+ host: boolean;
28
+ callbacks: MonacoCollabCallbacks;
29
+ editor?: monaco.editor.IStandaloneCodeEditor;
30
+ roomClaim: types.CreateRoomResponse | types.JoinRoomResponse;
31
+ }
32
+
33
+ export class CollaborationInstance implements Disposable {
34
+ protected readonly yjs: Y.Doc = new Y.Doc();
35
+ protected readonly yjsAwareness: awarenessProtocol.Awareness;
36
+ protected readonly yjsProvider: OpenCollaborationYjsProvider;
37
+ protected readonly yjsMutex = createMutex();
38
+
39
+ protected readonly identity = new Deferred<types.Peer>();
40
+ protected readonly documentDisposables = new Map<string, DisposableCollection>();
41
+ protected readonly peers = new Map<string, DisposablePeer>();
42
+ protected readonly throttles = new Map<string, () => void>();
43
+ protected readonly decorations = new Map<DisposablePeer, monaco.editor.IEditorDecorationsCollection>();
44
+ protected readonly usersChangedCallbacks: UsersChangeEvent[] = [];
45
+ protected readonly fileNameChangeCallbacks: FileNameChangeEvent[] = [];
46
+ protected _permissions: types.Permissions = { readonly: false };
47
+ protected effectiveReadonly = false;
48
+
49
+ protected currentPath?: string;
50
+ protected stopPropagation = false;
51
+ protected _following?: string;
52
+ protected _fileName: string;
53
+ protected previousFileName?: string;
54
+ protected _workspaceName: string;
55
+
56
+ protected connection: ProtocolBroadcastConnection;
57
+
58
+ get following(): string | undefined {
59
+ return this._following;
60
+ }
61
+
62
+ get connectedUsers(): DisposablePeer[] {
63
+ return Array.from(this.peers.values());
64
+ }
65
+
66
+ get ownUserData(): Promise<types.Peer> {
67
+ return this.identity.promise;
68
+ }
69
+
70
+ get isHost(): boolean {
71
+ return this.options.host;
72
+ }
73
+
74
+ get host(): types.Peer | undefined {
75
+ return 'host' in this.options.roomClaim ? this.options.roomClaim.host : undefined;
76
+ }
77
+
78
+ get roomId(): string {
79
+ return this.options.roomClaim.roomId;
80
+ }
81
+
82
+ get fileName(): string {
83
+ return this._fileName;
84
+ }
85
+
86
+ get workspaceName(): string {
87
+ return this._workspaceName;
88
+ }
89
+
90
+ set workspaceName(_workspaceName: string) {
91
+ this._workspaceName = _workspaceName;
92
+ }
93
+
94
+ /**
95
+ * access token for the room. allow to join or reconnect as host
96
+ */
97
+ get roomToken(): string {
98
+ return this.options.roomClaim.roomToken;
99
+ }
100
+
101
+ onUsersChanged(callback: UsersChangeEvent) {
102
+ this.usersChangedCallbacks.push(callback);
103
+ }
104
+
105
+ onFileNameChange(callback: FileNameChangeEvent) {
106
+ this.fileNameChangeCallbacks.push(callback);
107
+ }
108
+
109
+ constructor(protected options: CollaborationInstanceOptions) {
110
+ this.connection = options.connection;
111
+ this.yjsAwareness = new awarenessProtocol.Awareness(this.yjs);
112
+ this.yjsProvider = new OpenCollaborationYjsProvider(this.options.connection, this.yjs, this.yjsAwareness, {
113
+ resyncTimer: 10_000
114
+ });
115
+ this.yjsProvider.connect();
116
+
117
+ this._fileName = 'myFile.txt';
118
+ this._workspaceName = this.roomId;
119
+
120
+ this.setupConnectionHandlers();
121
+ this.setupFileSystemHandlers();
122
+ this.options.editor && this.registerEditorEvents();
123
+ }
124
+
125
+ private setupConnectionHandlers(): void {
126
+ this.connection.peer.onJoinRequest(async (_, user) => {
127
+ const result = await this.options.callbacks.onUserRequestsAccess(user);
128
+ return result ? {
129
+ workspace: {
130
+ name: this.workspaceName,
131
+ folders: [this.workspaceName]
132
+ }
133
+ } : undefined;
134
+ });
135
+
136
+ this.connection.room.onJoin(async (_, peer) => {
137
+ this.peers.set(peer.id, new DisposablePeer(this.yjsAwareness, peer));
138
+ const initData: types.InitData = {
139
+ protocol: '0.0.1',
140
+ host: await this.identity.promise,
141
+ guests: Array.from(this.peers.values()).map(e => e.peer),
142
+ capabilities: {},
143
+ permissions: {
144
+ ...this._permissions,
145
+ [getUserPermissionKey(peer.id)]: encodeUserPermission(true)
146
+ },
147
+ workspace: {
148
+ name: this.workspaceName,
149
+ folders: [this.workspaceName]
150
+ }
151
+ };
152
+ this.connection.peer.init(peer.id, initData);
153
+ this.notifyUsersChanged();
154
+ });
155
+
156
+ this.connection.room.onLeave(async (_, peer) => {
157
+ const disposable = this.peers.get(peer.id);
158
+ if (disposable) {
159
+ this.peers.delete(peer.id);
160
+ this.notifyUsersChanged();
161
+ }
162
+ this.rerenderPresence();
163
+ });
164
+
165
+ this.connection.peer.onInfo((_, peer) => {
166
+ this.yjsAwareness.setLocalStateField('peer', peer.id);
167
+ this.identity.resolve(peer);
168
+ });
169
+
170
+ this.connection.peer.onInit(async (_, initData) => {
171
+ await this.initialize(initData);
172
+ });
173
+
174
+ this.connection.room.onPermissions((_, permissions) => {
175
+ void this.applyPermissionsUpdate(permissions);
176
+ });
177
+ }
178
+
179
+ private async applyPermissionsUpdate(permissions: types.Permissions): Promise<void> {
180
+ this._permissions = permissions;
181
+ const peer = await this.identity.promise;
182
+ const readonly = resolveReadonly(permissions, peer.id);
183
+ this.effectiveReadonly = readonly;
184
+ this.updateEditorReadonly(readonly);
185
+ }
186
+
187
+ private updateEditorReadonly(readonly: boolean): void {
188
+ if (this.options.editor) {
189
+ this.options.editor.updateOptions({ readOnly: readonly });
190
+ }
191
+ }
192
+
193
+ private setupFileSystemHandlers(): void {
194
+ this.connection.fs.onReadFile(this.handleReadFile.bind(this));
195
+ this.connection.fs.onStat(this.handleStat.bind(this));
196
+ this.connection.fs.onReaddir(this.handleReaddir.bind(this));
197
+ this.connection.fs.onChange(this.handleFileChange.bind(this));
198
+ }
199
+
200
+ private async handleReadFile(_: unknown, path: string): Promise<{ content: Uint8Array }> {
201
+ if (path === this._fileName && this.options.editor) {
202
+ const text = this.options.editor.getModel()?.getValue();
203
+ const encoder = new TextEncoder();
204
+ const content = encoder.encode(text);
205
+ return { content };
206
+ }
207
+ throw new Error('Could not read file');
208
+ }
209
+
210
+ private async handleStat(_: unknown, path: string): Promise<{ type: types.FileType; mtime: number; ctime: number; size: number }> {
211
+ return {
212
+ type: path === this.workspaceName ? types.FileType.Directory : types.FileType.File,
213
+ mtime: 0,
214
+ ctime: 0,
215
+ size: 0
216
+ };
217
+ }
218
+
219
+ private async handleReaddir(_: unknown, path: string): Promise<Record<string, types.FileType>> {
220
+ const uri = this.getResourceUri(path);
221
+ if (uri) {
222
+ return {
223
+ [this._fileName]: types.FileType.File
224
+ };
225
+ }
226
+ throw new Error('Could not read directory');
227
+ }
228
+
229
+ private handleFileChange(_: unknown, change: types.FileChangeEvent): void {
230
+ const deleteChange = change.changes.find(c => c.type === types.FileChangeEventType.Delete);
231
+ const createChange = change.changes.find(c => c.type === types.FileChangeEventType.Create);
232
+ if (deleteChange && createChange) {
233
+ this._fileName = createChange.path;
234
+ const model = this.options.editor?.getModel();
235
+ if (model) {
236
+ this.registerTextDocument(model);
237
+ }
238
+ }
239
+ }
240
+
241
+ private notifyUsersChanged(): void {
242
+ this.usersChangedCallbacks.forEach(callback => callback());
243
+ }
244
+
245
+ private notifyFileNameChanged(fileName: string): void {
246
+ this.fileNameChangeCallbacks.forEach(callback => callback(fileName));
247
+ }
248
+
249
+ setEditor(editor: monaco.editor.IStandaloneCodeEditor): void {
250
+ this.options.editor = editor;
251
+ this.updateEditorReadonly(this.effectiveReadonly);
252
+ this.registerEditorEvents();
253
+ }
254
+
255
+ async setFileName(fileName: string): Promise<void> {
256
+ const oldFileName = this._fileName;
257
+ this._fileName = fileName;
258
+ const model = this.options.editor?.getModel();
259
+ if (model) {
260
+ await this.registerTextDocument(model);
261
+ this.connection.fs.change({
262
+ changes: [
263
+ {
264
+ type: types.FileChangeEventType.Create,
265
+ path: fileName
266
+ },
267
+ {
268
+ type: types.FileChangeEventType.Delete,
269
+ path: oldFileName
270
+ }
271
+ ]
272
+ });
273
+ }
274
+ }
275
+
276
+ dispose() {
277
+ this.peers.clear();
278
+ this.documentDisposables.forEach(e => e.dispose());
279
+ this.documentDisposables.clear();
280
+ }
281
+
282
+ leaveRoom() {
283
+ this.options.connection.room.leave();
284
+ }
285
+
286
+ getCurrentConnection(): ProtocolBroadcastConnection {
287
+ return this.options.connection;
288
+ }
289
+
290
+ protected pushDocumentDisposable(path: string, disposable: Disposable) {
291
+ let disposables = this.documentDisposables.get(path);
292
+ if (!disposables) {
293
+ disposables = new DisposableCollection();
294
+ this.documentDisposables.set(path, disposables);
295
+ }
296
+ disposables.push(disposable);
297
+ }
298
+
299
+ protected registerEditorEvents(): void {
300
+ if (!this.options.editor) {
301
+ return;
302
+ }
303
+ const text = this.options.editor.getModel();
304
+ if (text) {
305
+ this.registerTextDocument(text);
306
+ }
307
+
308
+ this.options.editor.onDidChangeModelContent(event => {
309
+ if (text && !this.stopPropagation) {
310
+ this.updateTextDocument(event, text);
311
+ }
312
+ });
313
+
314
+ this.options.editor.onDidChangeCursorSelection(() => {
315
+ if (this.options.editor && !this.stopPropagation) {
316
+ this.updateTextSelection(this.options.editor);
317
+ }
318
+ });
319
+
320
+ const awarenessDebounce = debounce(() => {
321
+ this.rerenderPresence();
322
+ }, 2000);
323
+
324
+ this.yjsAwareness.on('change', async (_: unknown, origin: string) => {
325
+ if (origin !== LOCAL_ORIGIN) {
326
+ this.updateFollow();
327
+ this.rerenderPresence();
328
+ awarenessDebounce();
329
+ }
330
+ });
331
+ }
332
+
333
+ followUser(id?: string) {
334
+ this._following = id;
335
+ if (id) {
336
+ this.updateFollow();
337
+ }
338
+ }
339
+
340
+ protected updateFollow(): void {
341
+ if (this._following) {
342
+ let userState: types.ClientAwareness | undefined = undefined;
343
+ const states = this.yjsAwareness.getStates() as Map<number, types.ClientAwareness>;
344
+ for (const state of states.values()) {
345
+ const peer = this.peers.get(state.peer);
346
+ if (peer?.peer.id === this._following) {
347
+ userState = state;
348
+ }
349
+ }
350
+ if (userState) {
351
+ if (types.ClientTextSelection.is(userState.selection)) {
352
+ this.followSelection(userState.selection);
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ protected async followSelection(selection: types.ClientTextSelection): Promise<void> {
359
+ if (!this.options.editor) {
360
+ return;
361
+ }
362
+ const uri = this.getResourceUri(selection.path);
363
+ const text = this.yjs.getText(selection.path);
364
+
365
+ const prevPath = this.currentPath;
366
+ this.currentPath = selection.path;
367
+ if (prevPath !== selection.path) {
368
+ this.stopPropagation = true;
369
+ this.options.editor.setValue(text.toString());
370
+ this.stopPropagation = false;
371
+ }
372
+
373
+ const filename = this.getHostPath(selection.path);
374
+ if (this._fileName !== filename) {
375
+ this._fileName = filename;
376
+ this.previousFileName = filename;
377
+ this.notifyFileNameChanged(this._fileName);
378
+ }
379
+
380
+ this.registerTextObserver(selection.path, this.options.editor.getModel()!, text);
381
+ if (uri && selection.visibleRanges && selection.visibleRanges.length > 0) {
382
+ const visibleRange = selection.visibleRanges[0];
383
+ const range = new monaco.Range(visibleRange.start.line, visibleRange.start.character, visibleRange.end.line, visibleRange.end.character);
384
+ this.options.editor && this.options.editor.revealRange(range);
385
+ }
386
+ }
387
+
388
+ protected updateTextSelection(editor: monaco.editor.IStandaloneCodeEditor): void {
389
+ const document = editor.getModel();
390
+ const selections = editor.getSelections();
391
+ if (!document || !selections) {
392
+ return;
393
+ }
394
+ const path = this.currentPath;
395
+ if (path) {
396
+ const ytext = this.yjs.getText(path);
397
+ const textSelections: types.RelativeTextSelection[] = [];
398
+ for (const selection of selections) {
399
+ const start = document.getOffsetAt(selection.getStartPosition());
400
+ const end = document.getOffsetAt(selection.getEndPosition());
401
+ const direction = selection.getDirection() === monaco.SelectionDirection.RTL
402
+ ? types.SelectionDirection.RightToLeft
403
+ : types.SelectionDirection.LeftToRight;
404
+ const editorSelection: types.RelativeTextSelection = {
405
+ start: Y.createRelativePositionFromTypeIndex(ytext, start),
406
+ end: Y.createRelativePositionFromTypeIndex(ytext, end),
407
+ direction
408
+ };
409
+ textSelections.push(editorSelection);
410
+ }
411
+ const textSelection: types.ClientTextSelection = {
412
+ path,
413
+ textSelections,
414
+ visibleRanges: editor.getVisibleRanges().map(range => ({
415
+ start: {
416
+ line: range.startLineNumber,
417
+ character: range.startColumn
418
+ },
419
+ end: {
420
+ line: range.endLineNumber,
421
+ character: range.endColumn
422
+ }
423
+ }))
424
+ };
425
+ this.setSharedSelection(textSelection);
426
+ }
427
+ }
428
+
429
+ protected async registerTextDocument(document: monaco.editor.ITextModel): Promise<void> {
430
+ const uri = this.getResourceUri(`${this._workspaceName}/${this._fileName}`);
431
+ const path = this.getProtocolPath(uri);
432
+ if (!this.currentPath || this.currentPath !== path) {
433
+ this.currentPath = path;
434
+ }
435
+ if (path) {
436
+ const text = document.getValue();
437
+ const yjsText = this.yjs.getText(path);
438
+ let ytextContent = '';
439
+ if (this.isHost) {
440
+ this.yjs.transact(() => {
441
+ yjsText.delete(0, yjsText.length);
442
+ yjsText.insert(0, text);
443
+ });
444
+ ytextContent = yjsText.toString();
445
+ } else {
446
+ ytextContent = await this.readFile();
447
+ if (this._fileName !== this.previousFileName) {
448
+ this.previousFileName = this._fileName;
449
+ this.notifyFileNameChanged(this._fileName);
450
+ }
451
+ }
452
+ if (text !== ytextContent) {
453
+ this.yjsMutex(() => {
454
+ document.setValue(ytextContent);
455
+ });
456
+ }
457
+ this.registerTextObserver(path, document, yjsText);
458
+ }
459
+ }
460
+
461
+ protected registerTextObserver(path: string, document: monaco.editor.ITextModel, yjsText: Y.Text): void {
462
+ const textObserver = this.documentDisposables.get(path);
463
+ if (textObserver) {
464
+ textObserver.dispose();
465
+ }
466
+
467
+ const resyncThrottle = this.getOrCreateThrottle(path, document);
468
+ const observer = (textEvent: Y.YTextEvent) => {
469
+ this.yjsMutex(async () => {
470
+ if (this.options.editor) {
471
+ const changes = YTextChangeDelta.toChanges(textEvent.delta);
472
+ const edits = this.createEditsFromTextEvent(changes, document);
473
+ this.updateDocument(document, edits);
474
+ resyncThrottle();
475
+ }
476
+ });
477
+ };
478
+ yjsText.observe(observer);
479
+ this.pushDocumentDisposable(path, { dispose: () => yjsText.unobserve(observer) });
480
+ }
481
+
482
+ protected updateDocument(document: monaco.editor.ITextModel, edits: monaco.editor.IIdentifiedSingleEditOperation[]): void {
483
+ document.pushStackElement();
484
+ document.pushEditOperations(null, edits, () => null);
485
+ document.pushStackElement();
486
+ }
487
+
488
+ private createEditsFromTextEvent(changes: YTextChange[], document: monaco.editor.ITextModel): monaco.editor.IIdentifiedSingleEditOperation[] {
489
+ const edits: monaco.editor.IIdentifiedSingleEditOperation[] = [];
490
+ changes.forEach(change => {
491
+ const start = document.getPositionAt(change.start);
492
+ const end = document.getPositionAt(change.end);
493
+ edits.push({
494
+ range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column),
495
+ text: change.text
496
+ });
497
+ });
498
+ return edits;
499
+ }
500
+
501
+ protected updateTextDocument(event: monaco.editor.IModelContentChangedEvent, document: monaco.editor.ITextModel): void {
502
+ if (this.effectiveReadonly) {
503
+ return;
504
+ }
505
+ const path = this.currentPath;
506
+ if (path) {
507
+ this.yjsMutex(() => {
508
+ const ytext = this.yjs.getText(path);
509
+ this.yjs.transact(() => {
510
+ for (const change of event.changes) {
511
+ ytext.delete(change.rangeOffset, change.rangeLength);
512
+ ytext.insert(change.rangeOffset, change.text);
513
+ }
514
+ });
515
+ this.getOrCreateThrottle(path, document)();
516
+ });
517
+ }
518
+ }
519
+
520
+ protected getOrCreateThrottle(path: string, document: monaco.editor.ITextModel): () => void {
521
+ let value = this.throttles.get(path);
522
+ if (!value) {
523
+ value = debounce(() => {
524
+ this.yjsMutex(() => {
525
+ const yjsText = this.yjs.getText(path);
526
+ const newContent = yjsText.toString();
527
+ if (newContent !== document.getValue()) {
528
+ this.updateDocumentContent(document, newContent);
529
+ }
530
+ });
531
+ }, 100, {
532
+ leading: false,
533
+ trailing: true
534
+ });
535
+ this.throttles.set(path, value);
536
+ }
537
+ return value;
538
+ }
539
+
540
+ private updateDocumentContent(document: monaco.editor.ITextModel, newContent: string): void {
541
+ this.yjsMutex(() => {
542
+ if (this.options.editor) {
543
+ const edits: monaco.editor.IIdentifiedSingleEditOperation[] = [{
544
+ range: document.getFullModelRange(),
545
+ text: newContent
546
+ }];
547
+ this.updateDocument(document, edits);
548
+ }
549
+ });
550
+ }
551
+
552
+ protected rerenderPresence() {
553
+ const states = this.yjsAwareness.getStates() as Map<number, types.ClientAwareness>;
554
+ for (const [clientID, state] of states.entries()) {
555
+ if (clientID === this.yjs.clientID) {
556
+ // Ignore own awareness state
557
+ continue;
558
+ }
559
+ const peerId = state.peer;
560
+ const peer = this.peers.get(peerId);
561
+ if (!state.selection || !peer) {
562
+ continue;
563
+ }
564
+ if (!types.ClientTextSelection.is(state.selection)) {
565
+ continue;
566
+ }
567
+ const { path, textSelections } = state.selection;
568
+ const selection = textSelections[0];
569
+ if (!selection) {
570
+ continue;
571
+ }
572
+ const uri = this.getResourceUri(path);
573
+ if (uri && this.options.editor) {
574
+ const model = this.options.editor.getModel();
575
+ const forward = selection.direction === 1;
576
+ let startIndex = Y.createAbsolutePositionFromRelativePosition(selection.start, this.yjs);
577
+ let endIndex = Y.createAbsolutePositionFromRelativePosition(selection.end, this.yjs);
578
+ if (model && startIndex && endIndex) {
579
+ if (startIndex.index > endIndex.index) {
580
+ [startIndex, endIndex] = [endIndex, startIndex];
581
+ }
582
+ const start = model.getPositionAt(startIndex.index);
583
+ const end = model.getPositionAt(endIndex.index);
584
+ const inverted = (forward && end.lineNumber === 1) || (!forward && start.lineNumber === 1);
585
+ const range: monaco.IRange = {
586
+ startLineNumber: start.lineNumber,
587
+ startColumn: start.column,
588
+ endLineNumber: end.lineNumber,
589
+ endColumn: end.column
590
+ };
591
+ const contentClassNames: string[] = [peer.decoration.cursorClassName];
592
+ if (inverted) {
593
+ contentClassNames.push(peer.decoration.cursorInvertedClassName);
594
+ }
595
+ this.setDecorations(peer, [{
596
+ range,
597
+ options: {
598
+ className: peer.decoration.selectionClassName,
599
+ beforeContentClassName: !forward ? contentClassNames.join(' ') : undefined,
600
+ afterContentClassName: forward ? contentClassNames.join(' ') : undefined,
601
+ stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
602
+ }
603
+ }]);
604
+ }
605
+ }
606
+ }
607
+ }
608
+
609
+ protected setDecorations(peer: DisposablePeer, decorations: monaco.editor.IModelDeltaDecoration[]): void {
610
+ if (this.decorations.has(peer)) {
611
+ this.decorations.get(peer)?.set(decorations);
612
+ } else if (this.options.editor) {
613
+ this.decorations.set(peer, this.options.editor.createDecorationsCollection(decorations));
614
+ }
615
+ }
616
+
617
+ protected setSharedSelection(selection?: types.ClientSelection): void {
618
+ this.yjsAwareness.setLocalStateField('selection', selection);
619
+ }
620
+
621
+ protected updateSelectionPath(newPath: string): void {
622
+ const currentState = this.yjsAwareness.getLocalState() as types.ClientAwareness;
623
+ if (currentState?.selection && types.ClientTextSelection.is(currentState.selection)) {
624
+ const newSelection: types.ClientTextSelection = {
625
+ ...currentState.selection,
626
+ path: newPath
627
+ };
628
+ this.setSharedSelection(newSelection);
629
+ }
630
+ }
631
+
632
+ protected createSelectionFromRelative(selection: types.RelativeTextSelection, model: monaco.editor.ITextModel): monaco.Selection | undefined {
633
+ const start = Y.createAbsolutePositionFromRelativePosition(selection.start, this.yjs);
634
+ const end = Y.createAbsolutePositionFromRelativePosition(selection.end, this.yjs);
635
+ if (start && end) {
636
+ let anchor = model.getPositionAt(start.index);
637
+ let head = model.getPositionAt(end.index);
638
+ if (selection.direction === types.SelectionDirection.RightToLeft) {
639
+ [anchor, head] = [head, anchor];
640
+ }
641
+ return new monaco.Selection(anchor.lineNumber, anchor.column, head.lineNumber, head.column);
642
+ }
643
+ return undefined;
644
+ }
645
+
646
+ protected getHostPath(path: string): string {
647
+ // When creating a URI as a guest, we always prepend it with the name of the workspace
648
+ // This just removes the workspace name from the path to get the path expected by the protocol
649
+ const subpath = path.substring(1).split('/');
650
+ return subpath.slice(1).join('/');
651
+ }
652
+
653
+ async initialize(data: types.InitData): Promise<void> {
654
+ for (const peer of [data.host, ...data.guests]) {
655
+ this.peers.set(peer.id, new DisposablePeer(this.yjsAwareness, peer));
656
+ }
657
+ await this.applyPermissionsUpdate(data.permissions);
658
+ this.workspaceName = data.workspace.name;
659
+ this.notifyUsersChanged();
660
+ }
661
+
662
+ getProtocolPath(uri?: monaco.Uri): string | undefined {
663
+ if (!uri) {
664
+ return undefined;
665
+ }
666
+ return uri.path.startsWith('/') ? uri.path.substring(1) : uri.path;
667
+ }
668
+
669
+ getResourceUri(path?: string): monaco.Uri | undefined {
670
+ return new monaco.Uri().with({ path });
671
+ }
672
+
673
+ async readFile(): Promise<string> {
674
+ if (!this.currentPath) {
675
+ return '';
676
+ }
677
+ const path = this.getHostPath(this.currentPath);
678
+
679
+ if (this.yjs.share.has(path)) {
680
+ const stringValue = this.yjs.getText(path);
681
+ return stringValue.toString();
682
+ } else {
683
+ const file = await this.connection.fs.readFile(this.host?.id, path);
684
+ const decoder = new TextDecoder();
685
+ return decoder.decode(file.content);
686
+ }
687
+ }
688
+ }