@@ -3,6 +3,7 @@ import * as React from 'react';
33import { TextOperation } from 'ot' ;
44import { debounce } from 'lodash' ;
55import { getModulePath } from 'common/sandbox/modules' ;
6+ import { css } from 'glamor' ;
67
78import getTemplate from 'common/templates' ;
89import type {
@@ -48,6 +49,50 @@ function indexToLineAndColumn(lines, index) {
4849 return { lineNumber : lines . length , column : lines [ lines . length - 1 ] + 2 } ;
4950}
5051
52+ const fadeIn = css . keyframes ( 'fadeIn' , {
53+ // optional name
54+ '0%' : { opacity : 0 } ,
55+ '100%' : { opacity : 1 } ,
56+ } ) ;
57+
58+ function lineAndColumnToIndex ( lines , lineNumber , column ) {
59+ let currentLine = 0 ;
60+ let index = 0 ;
61+
62+ while ( currentLine + 1 < lineNumber ) {
63+ index += lines [ currentLine ] . length ;
64+ index += 1 ; // Linebreak character
65+ currentLine += 1 ;
66+ }
67+
68+ index += column - 1 ;
69+
70+ return index ;
71+ }
72+
73+ function getSelection ( lines , selection ) {
74+ const startSelection = lineAndColumnToIndex (
75+ lines ,
76+ selection . startLineNumber ,
77+ selection . startColumn
78+ ) ;
79+ const endSelection = lineAndColumnToIndex (
80+ lines ,
81+ selection . endLineNumber ,
82+ selection . endColumn
83+ ) ;
84+
85+ return {
86+ selection :
87+ startSelection === endSelection ? [ ] : [ startSelection , endSelection ] ,
88+ cursorPosition : lineAndColumnToIndex (
89+ lines ,
90+ selection . positionLineNumber ,
91+ selection . positionColumn
92+ ) ,
93+ } ;
94+ }
95+
5196let modelCache = { } ;
5297
5398const fontFamilies = ( ...families ) =>
@@ -232,6 +277,32 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
232277 }
233278 } ) ;
234279
280+ editor . onDidChangeCursorSelection ( selectionChange => {
281+ const { onSelectionChanged, isLive } = this . props ;
282+ // Reason 3 is update by mouse or arrow keys
283+ if (
284+ isLive &&
285+ ( selectionChange . reason === 3 ||
286+ /* alt + shift + arrow keys */ selectionChange . source ===
287+ 'moveWordCommand' ||
288+ /* click inside a selection */ selectionChange . source === 'api' ) &&
289+ onSelectionChanged
290+ ) {
291+ const lines = editor . getModel ( ) . getLinesContent ( ) ;
292+ const data = {
293+ primary : getSelection ( lines , selectionChange . selection ) ,
294+ secondary : selectionChange . secondarySelections . map ( s =>
295+ getSelection ( lines , s )
296+ ) ,
297+ } ;
298+
299+ onSelectionChanged ( {
300+ selection : data ,
301+ moduleShortid : this . currentModule . shortid ,
302+ } ) ;
303+ }
304+ } ) ;
305+
235306 if ( this . props . onInitialized ) {
236307 this . disposeInitializer = this . props . onInitialized ( this ) ;
237308 }
@@ -350,17 +421,12 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
350421 . map ( change => {
351422 const startPos = change . range . getStartPosition ( ) ;
352423 const lines = code . split ( '\n' ) ;
353- let index = 0 ;
354424 const totalLength = code . length ;
355- let currentLine = 0 ;
356-
357- while ( currentLine + 1 < startPos . lineNumber ) {
358- index += lines [ currentLine ] . length ;
359- index += 1 ; // Linebreak character
360- currentLine += 1 ;
361- }
362-
363- index += startPos . column - 1 ;
425+ let index = lineAndColumnToIndex (
426+ lines ,
427+ startPos . lineNumber ,
428+ startPos . column
429+ ) ;
364430
365431 const operation = new TextOperation ( ) ;
366432 if ( index ) {
@@ -395,6 +461,149 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
395461 this . changes = { code : '' , changes : [ ] } ;
396462 } ;
397463
464+ userClassesGenerated = { } ;
465+ userSelectionDecorations = { } ;
466+ userSelections = [ ] ;
467+ updateUserSelections = (
468+ userSelections : Array < {
469+ userId : string ,
470+ name : string ,
471+ selection : any ,
472+ color : Array < number > ,
473+ } >
474+ ) => {
475+ this. userSelections = userSelections ;
476+ const lines = this . editor . getModel ( ) . getLinesContent ( ) ;
477+
478+ userSelections . forEach ( data => {
479+ const decorations = [ ] ;
480+ const { selection, color, userId, name } = data ;
481+
482+ if ( selection ) {
483+ const addCursor = ( position , className ) => {
484+ const cursorPos = indexToLineAndColumn ( lines , position ) ;
485+
486+ decorations . push ( {
487+ range : new this . monaco . Range (
488+ cursorPos . lineNumber ,
489+ cursorPos . column ,
490+ cursorPos . lineNumber ,
491+ cursorPos . column
492+ ) ,
493+ options : {
494+ className : this . userClassesGenerated [ className ] ,
495+ } ,
496+ } ) ;
497+ } ;
498+
499+ const addSelection = ( start , end , className ) => {
500+ const from = indexToLineAndColumn ( lines , start ) ;
501+ const to = indexToLineAndColumn ( lines , end ) ;
502+
503+ decorations . push ( {
504+ range : new this . monaco . Range (
505+ from . lineNumber ,
506+ from . column ,
507+ to . lineNumber ,
508+ to . column
509+ ) ,
510+ options : {
511+ className : this . userClassesGenerated [ className ] ,
512+ } ,
513+ } ) ;
514+ } ;
515+
516+ const cursorClassName = userId + '-cursor' ;
517+ const secondaryCursorClassName = userId + '-secondary-cursor' ;
518+ const selectionClassName = userId + '-selection' ;
519+ const secondarySelectionClassName = userId + '-secondary-selection' ;
520+
521+ if ( ! this . userClassesGenerated [ cursorClassName ] ) {
522+ this . userClassesGenerated [ cursorClassName ] = `${ css ( {
523+ backgroundColor : `rgba(${ color [ 0 ] } , ${ color [ 1 ] } , ${ color [ 2 ] } , 0.8)` ,
524+ width : '2px !important' ,
525+ cursor : 'text' ,
526+ zIndex : 30 ,
527+ ':hover' : {
528+ ':before' : {
529+ animation : `${ fadeIn } 0.3s` ,
530+ animationFillMode : 'forwards' ,
531+ opacity : 0 ,
532+ content : name ,
533+ position : 'absolute' ,
534+ top : - 20 ,
535+ backgroundColor : `rgb(${ color [ 0 ] } , ${ color [ 1 ] } , ${ color [ 2 ] } )` ,
536+ zIndex : 20 ,
537+ color :
538+ color [ 0 ] + color [ 1 ] + color [ 2 ] > 500
539+ ? 'rgba(0, 0, 0, 0.8)'
540+ : 'white' ,
541+ padding : '2px 6px' ,
542+ borderRadius : 2 ,
543+ borderBottomLeftRadius : 0 ,
544+ fontSize : '.875rem' ,
545+ fontWeight : 800 ,
546+ } ,
547+ } ,
548+ } ) } `;
549+ }
550+
551+ if ( ! this . userClassesGenerated [ secondaryCursorClassName ] ) {
552+ this . userClassesGenerated [ secondaryCursorClassName ] = `${ css ( {
553+ backgroundColor : `rgba(${ color [ 0 ] } , ${ color [ 1 ] } , ${ color [ 2 ] } , 0.6)` ,
554+ width : '2px !important' ,
555+ } ) } `;
556+ }
557+
558+ if ( ! this . userClassesGenerated [ selectionClassName ] ) {
559+ this . userClassesGenerated [ selectionClassName ] = `${ css ( {
560+ backgroundColor : `rgba(${ color [ 0 ] } , ${ color [ 1 ] } , ${ color [ 2 ] } , 0.3)` ,
561+ borderRadius : '3px' ,
562+ } ) } `;
563+ }
564+
565+ if ( ! this . userClassesGenerated [ secondarySelectionClassName ] ) {
566+ this . userClassesGenerated [ secondarySelectionClassName ] = `${ css ( {
567+ backgroundColor : `rgba(${ color [ 0 ] } , ${ color [ 1 ] } , ${ color [ 2 ] } , 0.2)` ,
568+ borderRadius : '3px' ,
569+ } ) } `;
570+ }
571+
572+ addCursor ( selection . primary . cursorPosition , cursorClassName ) ;
573+ if ( selection . primary . selection . length ) {
574+ addSelection (
575+ selection . primary . selection [ 0 ] ,
576+ selection . primary . selection [ 1 ] ,
577+ selectionClassName
578+ ) ;
579+ }
580+
581+ if ( selection . secondary . length ) {
582+ selection . secondary . forEach ( s => {
583+ addCursor ( s . cursorPosition , secondaryCursorClassName ) ;
584+
585+ if ( s . selection . length ) {
586+ addSelection (
587+ s . selection [ 0 ] ,
588+ s . selection [ 1 ] ,
589+ secondarySelectionClassName
590+ ) ;
591+ }
592+ } ) ;
593+ }
594+ }
595+
596+ this . userSelectionDecorations [
597+ this . currentModule . shortid + userId
598+ ] = this . editor . deltaDecorations (
599+ this . userSelectionDecorations [ this . currentModule . shortid + userId ] ||
600+ [ ] ,
601+ decorations ,
602+ userId
603+ ) ;
604+ } ) ;
605+ } ;
606+
398607 changeSandbox = (
399608 newSandbox : Sandbox ,
400609 newCurrentModule : Module ,
@@ -458,6 +667,7 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
458667 column
459668 ) ,
460669 text : op ,
670+ forceMoveMarkers : true ,
461671 } ,
462672 ] ) ;
463673 index += op . length ;
0 commit comments