Skip to content

Commit 31d9b11

Browse files
authored
Merge pull request #12 from situ2001/collaboration/user-model
[Stage2] Define user model
2 parents 05daf57 + fa84f06 commit 31d9b11

File tree

10 files changed

+383
-15
lines changed

10 files changed

+383
-15
lines changed

packages/collaboration/__tests__/browser/textmodel-binding.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,28 @@ import { Awareness } from 'y-protocols/awareness';
33
import { WebsocketProvider } from 'y-websocket';
44
import * as Y from 'yjs';
55

6+
import { Injector } from '@opensumi/di';
67
import { uuid } from '@opensumi/ide-core-common';
78
import { ICodeEditor } from '@opensumi/ide-monaco';
89
import * as monaco from '@opensumi/monaco-editor-core/esm/vs/editor/editor.api';
910

1011
import { TextModelBinding } from '../../src/browser/textmodel-binding';
12+
import { ICollaborationService } from '../../src/common';
13+
14+
const injector = new Injector();
15+
16+
injector.addProviders({
17+
token: ICollaborationService,
18+
useValue: {
19+
getCursorWidgetRegistry: jest.fn(),
20+
},
21+
});
1122

1223
const createBindingWithTextModel = (doc: Y.Doc, awareness: Awareness) => {
1324
const textModel = monaco.editor.createModel('');
1425
const yText = doc.getText('test');
15-
const binding = new TextModelBinding(yText, textModel, awareness);
26+
// const binding = new TextModelBinding(yText, textModel, awareness);
27+
const binding = injector.get(TextModelBinding, [yText, textModel, awareness]);
1628
return {
1729
textModel,
1830
binding,
@@ -31,6 +43,7 @@ describe('TextModelBinding test for yText and TextModel', () => {
3143
wsProvider = new WebsocketProvider('ws://127.0.0.1:12345', 'test', doc, { connect: false }); // we don't use wsProvider here
3244
user1 = createBindingWithTextModel(doc, wsProvider.awareness);
3345
user2 = createBindingWithTextModel(doc, wsProvider.awareness);
46+
jest.mock('@opensumi/di');
3447
});
3548

3649
afterEach(() => {

packages/collaboration/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@opensumi/ide-core-browser": "^2.18.7",
2929
"@opensumi/ide-editor": "2.18.7",
3030
"@opensumi/ide-monaco": "2.18.7",
31+
"@opensumi/ide-theme": "2.18.7",
3132
"@opensumi/ide-dev-tool": "^1.3.1",
3233
"y-protocols": "^1.0.2"
3334
}

packages/collaboration/src/browser/collaboration.contribution.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@ import {
66
KeybindingWeight,
77
PreferenceService,
88
} from '@opensumi/ide-core-browser';
9-
import { CommandContribution, CommandRegistry, Domain } from '@opensumi/ide-core-common';
9+
import { CommandContribution, CommandRegistry, ContributionProvider, Domain, uuid } from '@opensumi/ide-core-common';
1010

11-
import { ICollaborationService } from '../common';
11+
import { ICollaborationService, UserInfo, UserInfoForCollaborationContribution } from '../common';
1212
import { REDO, UNDO } from '../common/commands';
1313

14+
// mock user info
15+
@Domain(UserInfoForCollaborationContribution)
16+
export class MyUserInfo implements UserInfoForCollaborationContribution {
17+
info: UserInfo = {
18+
id: uuid().slice(0, 4),
19+
nickname: `${uuid().slice(0, 4)}`,
20+
};
21+
}
22+
1423
@Domain(ClientAppContribution, KeybindingContribution, CommandContribution)
1524
export class CollaborationContribution implements ClientAppContribution, KeybindingContribution, CommandContribution {
1625
@Autowired(ICollaborationService)
@@ -19,10 +28,20 @@ export class CollaborationContribution implements ClientAppContribution, Keybind
1928
@Autowired(PreferenceService)
2029
private preferenceService: PreferenceService;
2130

31+
@Autowired(UserInfoForCollaborationContribution)
32+
private readonly userInfoProvider: ContributionProvider<UserInfoForCollaborationContribution>;
33+
2234
onDidStart() {
2335
if (this.preferenceService.get('editor.askIfDiff') === true) {
2436
this.preferenceService.set('editor.askIfDiff', false);
2537
}
38+
39+
// before init
40+
const providers = this.userInfoProvider.getContributions();
41+
for (const provider of providers) {
42+
this.collaborationService.setUserInfo(provider);
43+
}
44+
2645
this.collaborationService.initialize();
2746
}
2847

packages/collaboration/src/browser/collaboration.service.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ import { WorkbenchEditorService } from '@opensumi/ide-editor';
88
import { EditorActiveResourceStateChangedEvent, EditorGroupCloseEvent } from '@opensumi/ide-editor/lib/browser';
99
import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service';
1010
import { ITextModel, ICodeEditor } from '@opensumi/ide-monaco';
11+
import { ICSSStyleService } from '@opensumi/ide-theme';
1112

1213
import {
1314
CollaborationServiceForClientPath,
1415
ICollaborationService,
1516
ICollaborationServiceForClient,
1617
ROOM_NAME,
18+
UserInfo,
19+
UserInfoForCollaborationContribution,
20+
Y_REMOTE_SELECTION,
21+
Y_REMOTE_SELECTION_HEAD,
1722
} from '../common';
1823

24+
import { getColorByClientID } from './color';
25+
import { CursorWidgetRegistry } from './cursor-widget';
1926
import { TextModelBinding } from './textmodel-binding';
2027

2128
import './styles.less';
@@ -36,6 +43,16 @@ export class CollaborationService extends WithEventBus implements ICollaboration
3643
@Autowired(WorkbenchEditorService)
3744
private workbenchEditorService: WorkbenchEditorServiceImpl;
3845

46+
@Autowired(ICSSStyleService)
47+
private cssManager: ICSSStyleService;
48+
49+
private clientIDStyleAddedSet: Set<number> = new Set();
50+
51+
// hold editor => registry
52+
private cursorRegistryMap: Map<ICodeEditor, CursorWidgetRegistry> = new Map();
53+
54+
private userInfo: UserInfo;
55+
3956
private yDoc: Y.Doc;
4057

4158
private yWebSocketProvider: WebsocketProvider;
@@ -76,15 +93,45 @@ export class CollaborationService extends WithEventBus implements ICollaboration
7693
this.yTextMap = this.yDoc.getMap();
7794
this.yWebSocketProvider = new WebsocketProvider('ws://127.0.0.1:12345', ROOM_NAME, this.yDoc); // TODO configurable uri and room name
7895
this.yTextMap.observe(this.yMapObserver);
96+
97+
// add userInfo to awareness field
98+
this.yWebSocketProvider.awareness.setLocalStateField('user-info', this.userInfo);
99+
79100
this.logger.debug('Collaboration initialized');
101+
102+
this.yWebSocketProvider.awareness.on('update', this.updateCSSManagerWhenAwarenessUpdated);
80103
}
81104

82105
destroy() {
106+
this.yWebSocketProvider.awareness.off('update', this.updateCSSManagerWhenAwarenessUpdated);
107+
this.clientIDStyleAddedSet.forEach((clientID) => {
108+
this.cssManager.removeClass(`${Y_REMOTE_SELECTION}-${clientID}`);
109+
this.cssManager.removeClass(`${Y_REMOTE_SELECTION_HEAD}-${clientID}`);
110+
this.cssManager.removeClass(`${Y_REMOTE_SELECTION_HEAD}-${clientID}::after`);
111+
});
83112
this.yTextMap.unobserve(this.yMapObserver);
84113
this.yWebSocketProvider.disconnect();
85114
this.bindingMap.forEach((binding) => binding.dispose());
86115
}
87116

117+
getUseInfo(): UserInfo {
118+
if (!this.userInfo) {
119+
throw new Error('User info is not registered');
120+
}
121+
122+
return this.userInfo;
123+
}
124+
125+
setUserInfo(contribution: UserInfoForCollaborationContribution) {
126+
if (this.userInfo) {
127+
throw new Error('User info is already registered');
128+
}
129+
130+
if (contribution.info) {
131+
this.userInfo = contribution.info;
132+
}
133+
}
134+
88135
undoOnCurrentResource() {
89136
const uri = this.workbenchEditorService.currentResource?.uri.toString();
90137
if (uri && this.bindingMap.has(uri)) {
@@ -131,11 +178,56 @@ export class CollaborationService extends WithEventBus implements ICollaboration
131178
if (binding) {
132179
binding.dispose();
133180
this.bindingMap.delete(uri);
134-
// todo ref = ref - 1 (through back service)
135181
this.logger.debug('Removed binding');
136182
}
137183
}
138184

185+
public getCursorWidgetRegistry(editor: ICodeEditor) {
186+
return this.cursorRegistryMap.get(editor);
187+
}
188+
189+
private updateCSSManagerWhenAwarenessUpdated = (changes: {
190+
added: number[];
191+
updated: number[];
192+
removed: number[];
193+
}) => {
194+
if (changes.removed.length > 0) {
195+
changes.removed.forEach((clientID) => {
196+
this.cssManager.removeClass(`${Y_REMOTE_SELECTION}-${clientID}`);
197+
this.cssManager.removeClass(`${Y_REMOTE_SELECTION_HEAD}-${clientID}`);
198+
this.cssManager.removeClass(`${Y_REMOTE_SELECTION_HEAD}-${clientID}::after`);
199+
this.clientIDStyleAddedSet.delete(clientID);
200+
});
201+
}
202+
if (changes.added.length > 0 || changes.updated.length > 0) {
203+
changes.added.forEach((clientID) => {
204+
if (!this.clientIDStyleAddedSet.has(clientID)) {
205+
const color = getColorByClientID(clientID);
206+
this.cssManager.addClass(`${Y_REMOTE_SELECTION}-${clientID}`, {
207+
backgroundColor: color,
208+
opacity: '0.25',
209+
});
210+
this.cssManager.addClass(`${Y_REMOTE_SELECTION_HEAD}-${clientID}`, {
211+
position: 'absolute',
212+
borderLeft: `${color} solid 2px`,
213+
borderBottom: `${color} solid 2px`,
214+
borderTop: `${color} solid 2px`,
215+
height: '100%',
216+
boxSizing: 'border-box',
217+
});
218+
this.cssManager.addClass(`${Y_REMOTE_SELECTION_HEAD}-${clientID}::after`, {
219+
position: 'absolute',
220+
content: ' ',
221+
border: `3px solid ${color}`,
222+
left: '-4px',
223+
top: '-5px',
224+
});
225+
this.clientIDStyleAddedSet.add(clientID);
226+
}
227+
});
228+
}
229+
};
230+
139231
@OnEvent(EditorGroupCloseEvent)
140232
private groupCloseHandler(e: EditorGroupCloseEvent) {
141233
this.logger.debug('Group close tabs', e);
@@ -167,6 +259,15 @@ export class CollaborationService extends WithEventBus implements ICollaboration
167259
const monacoEditor = this.workbenchEditorService.currentCodeEditor?.monacoEditor;
168260
const binding = this.getBinding(uri);
169261

262+
// check if editor has its widgetRegistry
263+
if (monacoEditor && !this.cursorRegistryMap.has(monacoEditor)) {
264+
const registry = this.injector.get(CursorWidgetRegistry, [monacoEditor, this.yWebSocketProvider.awareness]);
265+
this.cursorRegistryMap.set(monacoEditor, registry);
266+
monacoEditor.onDidDispose(() => {
267+
registry.destroy();
268+
});
269+
}
270+
170271
// check if there exists any binding
171272
if (!binding) {
172273
if (this.yTextMap.has(uri)) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const color = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violent'];
2+
3+
// get color by clientID
4+
export const getColorByClientID = (id: number): string => color[id % color.length];

0 commit comments

Comments
 (0)