Skip to content

Commit 987c016

Browse files
committed
Handle multi model decorations
1 parent 9a0ea03 commit 987c016

File tree

6 files changed

+190
-130
lines changed

6 files changed

+190
-130
lines changed

packages/app/src/app/overmind/effects/vscode/ModelsHandler.ts

Lines changed: 144 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
EditorSelection,
44
Module,
55
Sandbox,
6-
UserSelection,
76
} from '@codesandbox/common/lib/types';
87
import { indexToLineAndColumn } from 'app/overmind/utils/common';
98
import { actions, dispatch } from 'codesandbox-api';
@@ -198,9 +197,36 @@ export class ModelsHandler {
198197
this.isApplyingOperation = false;
199198
}
200199

200+
public clearUserSelections(userId: string) {
201+
const decorations = Object.keys(this.userSelectionDecorations).filter(d =>
202+
d.startsWith(userId)
203+
);
204+
Object.keys(this.moduleModels).forEach(async key => {
205+
const moduleModel = this.moduleModels[key];
206+
207+
if (!moduleModel.model) {
208+
return;
209+
}
210+
211+
const model = await moduleModel.model;
212+
213+
decorations.forEach(decorationId => {
214+
if (decorationId.startsWith(userId + model.id)) {
215+
this.userSelectionDecorations[decorationId] = model.deltaDecorations(
216+
this.userSelectionDecorations[decorationId] || [],
217+
[]
218+
);
219+
}
220+
});
221+
});
222+
}
223+
224+
nameTagTimeouts: { [name: string]: number } = {};
225+
201226
public async updateUserSelections(
202227
module,
203-
userSelections: Array<UserSelection | EditorSelection>
228+
userSelections: EditorSelection[],
229+
showNameTag = true
204230
) {
205231
const moduleModel = this.getModuleModel(module);
206232

@@ -212,19 +238,15 @@ export class ModelsHandler {
212238

213239
const model = await moduleModel.model;
214240
const lines = model.getLinesContent() || [];
215-
const activeEditor = this.editorApi.getActiveCodeEditor();
216241

217-
userSelections.forEach((data: EditorSelection & UserSelection) => {
242+
userSelections.forEach((data: EditorSelection) => {
218243
const { userId } = data;
219244

220-
const decorationId = module.shortid + userId;
245+
const decorationId = userId + model.id + module.shortid;
221246
if (data.selection === null) {
222-
this.userSelectionDecorations[
223-
decorationId
224-
] = activeEditor.deltaDecorations(
247+
this.userSelectionDecorations[decorationId] = model.deltaDecorations(
225248
this.userSelectionDecorations[decorationId] || [],
226-
[],
227-
data.userId
249+
[]
228250
);
229251

230252
return;
@@ -233,82 +255,75 @@ export class ModelsHandler {
233255
const decorations = [];
234256
const { selection, color, name } = data;
235257

236-
if (selection) {
237-
const addCursor = (position, className) => {
238-
const cursorPos = indexToLineAndColumn(lines, position);
239-
240-
decorations.push({
241-
range: new this.monaco.Range(
242-
cursorPos.lineNumber,
243-
cursorPos.column,
244-
cursorPos.lineNumber,
245-
cursorPos.column
246-
),
247-
options: {
248-
beforeContentClassName: this.userClassesGenerated[className],
249-
zIndex: 2,
250-
},
251-
});
258+
const getCursorDecoration = (position, className) => {
259+
const cursorPos = indexToLineAndColumn(lines, position);
260+
261+
return {
262+
range: new this.monaco.Range(
263+
cursorPos.lineNumber,
264+
cursorPos.column,
265+
cursorPos.lineNumber,
266+
cursorPos.column
267+
),
268+
options: {
269+
className: `${this.userClassesGenerated[className]}`,
270+
},
252271
};
272+
};
253273

254-
const addSelection = (start, end, className) => {
255-
const from = indexToLineAndColumn(lines, start);
256-
const to = indexToLineAndColumn(lines, end);
274+
const getSelectionDecoration = (start, end, className) => {
275+
const from = indexToLineAndColumn(lines, start);
276+
const to = indexToLineAndColumn(lines, end);
257277

258-
decorations.push({
259-
range: new this.monaco.Range(
260-
from.lineNumber,
261-
from.column,
262-
to.lineNumber,
263-
to.column
264-
),
265-
options: {
266-
className: this.userClassesGenerated[className],
267-
},
268-
});
278+
return {
279+
range: new this.monaco.Range(
280+
from.lineNumber,
281+
from.column,
282+
to.lineNumber,
283+
to.column
284+
),
285+
options: {
286+
className: this.userClassesGenerated[className],
287+
},
269288
};
289+
};
290+
const prefix = color.join('-') + userId;
291+
const cursorClassName = prefix + '-cursor';
292+
const nameTagClassName = prefix + '-nametag';
293+
const secondaryCursorClassName = prefix + '-secondary-cursor';
294+
const selectionClassName = prefix + '-selection';
295+
const secondarySelectionClassName = prefix + '-secondary-selection';
270296

271-
const prefix = color.join('-') + userId;
272-
const cursorClassName = prefix + '-cursor';
273-
const secondaryCursorClassName = prefix + '-secondary-cursor';
274-
const selectionClassName = prefix + '-selection';
275-
const secondarySelectionClassName = prefix + '-secondary-selection';
276-
297+
if (selection) {
298+
const nameStyles = {
299+
content: name,
300+
position: 'absolute',
301+
bottom: '100%',
302+
backgroundColor: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
303+
zIndex: 200,
304+
color:
305+
color[0] + color[1] + color[2] > 500
306+
? 'rgba(0, 0, 0, 0.8)'
307+
: 'white',
308+
padding: '0 4px',
309+
borderRadius: 2,
310+
borderBottomLeftRadius: 0,
311+
fontSize: '.75rem',
312+
fontWeight: 600,
313+
userSelect: 'none',
314+
pointerEvents: 'none',
315+
width: 'max-content',
316+
fontFamily: 'dm, Menlo, monospace',
317+
};
277318
if (!this.userClassesGenerated[cursorClassName]) {
278-
const nameStyles = {
279-
content: name,
280-
position: 'absolute',
281-
bottom: '100%',
282-
backgroundColor: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
283-
zIndex: 200,
284-
color:
285-
color[0] + color[1] + color[2] > 500
286-
? 'rgba(0, 0, 0, 0.8)'
287-
: 'white',
288-
padding: '0 4px',
289-
borderRadius: 2,
290-
borderBottomLeftRadius: 0,
291-
fontSize: '.75rem',
292-
fontWeight: 600,
293-
userSelect: 'none',
294-
pointerEvents: 'none',
295-
width: 'max-content',
296-
};
297-
298319
this.userClassesGenerated[cursorClassName] = `${css({
320+
display: 'inherit',
299321
position: 'absolute',
300322
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.8)`,
301323
width: '2px !important',
302324
height: '100%',
303325
cursor: 'text',
304326
zIndex: 200,
305-
':before': {
306-
animation: `${fadeOut} 0.3s`,
307-
animationDelay: '1s',
308-
animationFillMode: 'forwards',
309-
opacity: 1,
310-
...nameStyles,
311-
},
312327
':hover': {
313328
':before': {
314329
animation: `${fadeIn} 0.3s`,
@@ -320,6 +335,18 @@ export class ModelsHandler {
320335
})}`;
321336
}
322337

338+
if (!this.userClassesGenerated[nameTagClassName]) {
339+
this.userClassesGenerated[nameTagClassName] = `${css({
340+
':before': {
341+
animation: `${fadeOut} 0.3s`,
342+
animationDelay: '1s',
343+
animationFillMode: 'forwards',
344+
opacity: 1,
345+
...nameStyles,
346+
},
347+
})}`;
348+
}
349+
323350
if (!this.userClassesGenerated[secondaryCursorClassName]) {
324351
this.userClassesGenerated[secondaryCursorClassName] = `${css({
325352
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.6)`,
@@ -343,48 +370,65 @@ export class ModelsHandler {
343370
})}`;
344371
}
345372

346-
// These types are not working, have to figure this out
347-
// @ts-ignore
348-
addCursor(selection.primary.cursorPosition, cursorClassName);
349-
// @ts-ignore
373+
decorations.push(
374+
getCursorDecoration(selection.primary.cursorPosition, cursorClassName)
375+
);
376+
350377
if (selection.primary.selection.length) {
351-
addSelection(
352-
// @ts-ignore
353-
selection.primary.selection[0],
354-
// @ts-ignore
355-
selection.primary.selection[1],
356-
selectionClassName
378+
decorations.push(
379+
getSelectionDecoration(
380+
// @ts-ignore
381+
selection.primary.selection[0],
382+
// @ts-ignore
383+
selection.primary.selection[1],
384+
selectionClassName
385+
)
357386
);
358387
}
359388

360-
// @ts-ignore
361389
if (selection.secondary.length) {
362-
// @ts-ignore
363390
selection.secondary.forEach(s => {
364-
addCursor(s.cursorPosition, secondaryCursorClassName);
391+
decorations.push(
392+
getCursorDecoration(s.cursorPosition, secondaryCursorClassName)
393+
);
365394

366395
if (s.selection.length) {
367-
addSelection(
368-
s.selection[0],
369-
s.selection[1],
370-
secondarySelectionClassName
396+
decorations.push(
397+
getSelectionDecoration(
398+
s.selection[0],
399+
s.selection[1],
400+
secondarySelectionClassName
401+
)
371402
);
372403
}
373404
});
374405
}
375406
}
376407

377-
// Allow new model to attach in case it's attaching
378-
// Should ideally verify this, this is hacky
379-
requestAnimationFrame(() => {
380-
this.userSelectionDecorations[
381-
decorationId
382-
] = activeEditor.deltaDecorations(
383-
this.userSelectionDecorations[decorationId] || [],
384-
decorations,
385-
userId
408+
this.userSelectionDecorations[decorationId] = model.deltaDecorations(
409+
this.userSelectionDecorations[decorationId] || [],
410+
decorations
411+
);
412+
413+
if (this.nameTagTimeouts[decorationId]) {
414+
clearTimeout(this.nameTagTimeouts[decorationId]);
415+
}
416+
if (showNameTag) {
417+
const decoration = model.deltaDecorations(
418+
[],
419+
[
420+
getCursorDecoration(
421+
selection.primary.cursorPosition,
422+
nameTagClassName
423+
),
424+
]
386425
);
387-
});
426+
this.userSelectionDecorations[decorationId].push(decoration);
427+
this.nameTagTimeouts[decorationId] = window.setTimeout(() => {
428+
// And now hide the nametag after 1.5s
429+
model.deltaDecorations([decoration], []);
430+
}, 1500);
431+
}
388432
});
389433
}
390434

packages/app/src/app/overmind/effects/vscode/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,15 +263,23 @@ export class VSCodeEffect {
263263
}
264264
}
265265

266-
public updateUserSelections(userSelections: EditorSelection[]) {
266+
public clearUserSelections(userId: string) {
267267
if (!this.modelsHandler) {
268268
return;
269269
}
270270

271-
this.modelsHandler.updateUserSelections(
272-
this.options.getCurrentModule(),
273-
userSelections
274-
);
271+
this.modelsHandler.clearUserSelections(userId);
272+
}
273+
274+
public updateUserSelections(
275+
module: Module,
276+
userSelections: EditorSelection[]
277+
) {
278+
if (!this.modelsHandler) {
279+
return;
280+
}
281+
282+
this.modelsHandler.updateUserSelections(module, userSelections);
275283
}
276284

277285
public setReadOnly(enabled: boolean) {
@@ -868,6 +876,13 @@ export class VSCodeEffect {
868876
),
869877
};
870878

879+
if (selectionChange.source === 'modelChange') {
880+
// Don't update the cursor pos on model change, as it will
881+
// be updated automatically by vscode (they handle cursor
882+
// location changes really well)
883+
return;
884+
}
885+
871886
if (
872887
selectionChange.reason === 3 ||
873888
/* alt + shift + arrow keys */ selectionChange.source ===

packages/app/src/app/overmind/namespaces/editor/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ export const moduleSelected: Action<{
370370

371371
if (state.live.isLive) {
372372
effects.vscode.updateUserSelections(
373+
module,
373374
actions.live.internal.getSelectionsForModule(module)
374375
);
375376
state.live.liveUser.currentModuleShortid = module.shortid;

0 commit comments

Comments
 (0)