module import { createMutex } from 'lib0/mutex.js' ; import * as math from 'lib0/math.js' ; import * as Y from 'yjs' ; const typeObserver = ( binding , event ) => { binding . _mux ( ( ) => { const cm = binding . target ; cm . operation ( ( ) => { const delta = event . delta ; let index = 0 ; for ( let i = 0 ; i < event . delta . length ; i ++ ) { const d = delta [ i ] ; if ( d . retain ) { index += d . retain ; } else if ( d . insert ) { const pos = cm . posFromIndex ( index ) ; cm . replaceRange ( d . insert , pos , pos , 'prosemirror-binding' ) ; index += d . insert . length ; } else if ( d . delete ) { const start = cm . posFromIndex ( index ) ; const end = cm . posFromIndex ( index + d . delete ) ; cm . replaceRange ( '' , start , end , 'prosemirror-binding' ) ; } } } ) ; } ) ; } ; const targetObserver = ( binding , change ) => { binding . _mux ( ( ) => { const start = binding . target . indexFromPos ( change . from ) ; const delLen = change . removed . map ( s => s . length ) . reduce ( math . add ) + change . removed . length - 1 ; if ( delLen > 0 ) { binding . type . delete ( start , delLen ) ; } if ( change . text . length > 0 ) { binding . type . insert ( start , change . text . join ( '\n' ) ) ; } } ) ; } ; const createCaret = ( username , color ) => { const caret = document . createElement ( 'span' ) ; caret . classList . add ( 'remote-caret' ) ; caret . setAttribute ( 'style' , `border-color: ${ color }` ) ; const userDiv = document . createElement ( 'div' ) ; userDiv . setAttribute ( 'style' , `background-color: ${ color }` ) ; userDiv . insertBefore ( document . createTextNode ( username ) , null ) ; caret . insertBefore ( userDiv , null ) ; return caret ; } ; const jumpToLine = ( cm , i ) => { const t = cm . charCoords ( { line : i , ch : 0 } , "local" ) . top ; const middleHeight = cm . getScrollerElement ( ) . offsetHeight / 2 ; cm . scrollTo ( null , t - middleHeight - 5 ) ; } ; const updateRemoteSelection = ( y , cm , type , cursors , clientId , awareness ) => { const m = cursors . get ( clientId ) ; if ( m !== undefined ) { m . caret . clear ( ) ; if ( m . sel !== null ) { m . sel . clear ( ) ; } cursors . delete ( clientId ) ; } const aw = awareness . getStates ( ) . get ( clientId ) ; if ( aw === undefined ) { return ; } const user = aw . user || { } ; if ( user . color == null ) { user . color = '#ffa500' ; } if ( user . name == null ) { user . name = `User: ${ clientId }` ; } const cursor = aw . cursor ; if ( cursor == null || cursor . anchor == null || cursor . head == null ) { return ; } const anchor = Y . createAbsolutePositionFromRelativePosition ( JSON . parse ( cursor . anchor ) , y ) ; const head = Y . createAbsolutePositionFromRelativePosition ( JSON . parse ( cursor . head ) , y ) ; if ( anchor !== null && head !== null && anchor . type === type && head . type === type ) { const headpos = cm . posFromIndex ( head . index ) ; const anchorpos = cm . posFromIndex ( anchor . index ) ; let from , to ; if ( head . index < anchor . index ) { from = headpos ; to = anchorpos ; } else { from = anchorpos ; to = headpos ; } const caretEl = createCaret ( user . name , user . color ) ; const caret = cm . setBookmark ( headpos , { widget : caretEl , insertLeft : true } ) ; let sel = null ; if ( head . index !== anchor . index ) { sel = cm . markText ( from , to , { css : `background-color: ${ user . color }70` , inclusiveRight : true , inclusiveLeft : false } ) ; } cursors . set ( clientId , { caret , sel } ) ; if ( cm . hasFocus ( ) !== true ) { jumpToLine ( cm , headpos . line ) ; } } } ; const codemirrorCursorActivity = ( y , cm , type , cursors , username , awareness ) => { const m = cursors . get ( "current" ) ; if ( m !== undefined ) { m . caret . clear ( ) ; cursors . delete ( "current" ) ; } if ( ! cm . hasFocus ( ) ) { return ; } const newAnchor = Y . createRelativePositionFromTypeIndex ( type , cm . indexFromPos ( cm . getCursor ( 'anchor' ) ) ) ; const newHead = Y . createRelativePositionFromTypeIndex ( type , cm . indexFromPos ( cm . getCursor ( 'head' ) ) ) ; const aw = awareness . getLocalState ( ) ; let currentAnchor = null ; let currentHead = null ; if ( aw . cursor != null ) { currentAnchor = Y . createAbsolutePositionFromRelativePosition ( JSON . parse ( aw . cursor . anchor ) , y ) ; currentHead = Y . createAbsolutePositionFromRelativePosition ( JSON . parse ( aw . cursor . head ) , y ) ; } if ( aw . cursor == null || ! Y . compareRelativePositions ( currentAnchor , newAnchor ) || ! Y . compareRelativePositions ( currentHead , newHead ) ) { awareness . setLocalStateField ( 'cursor' , { anchor : JSON . stringify ( newAnchor ) , head : JSON . stringify ( newHead ) } ) ; const pos = cm . getCursor ( ) ; const caretEl = createCaret ( username , "#ffa500" ) ; const caret = cm . setBookmark ( pos , { widget : caretEl , insertLeft : true } ) ; cursors . set ( "current" , { caret } ) ; } } ; typedef { any } CodeMirror example export class CodeMirrorBinding { param { Y . Text } textType param { CodeMirror } codeMirror param { any } awareness constructor ( textType , codeMirror , username , awareness ) { const doc = textType . doc ; this . type = textType ; this . target = codeMirror ; this . username = username ; this . awareness = awareness ; private this . _mux = createMutex ( ) ; codeMirror . setValue ( textType . toString ( ) ) ; this . _typeObserver = event => typeObserver ( this , event ) ; this . _targetObserver = ( _ , change ) => targetObserver ( this , change ) ; this . _cursors = new Map ( ) ; this . _awarenessListener = event => { const f = clientId => { if ( clientId !== doc . clientID ) { updateRemoteSelection ( doc , codeMirror , textType , this . _cursors , clientId , awareness ) ; } } ; event . added . forEach ( f ) ; event . removed . forEach ( f ) ; event . updated . forEach ( f ) ; } ; this . _cursorListener = ( ) => codemirrorCursorActivity ( doc , codeMirror , textType , this . _cursors , this . username , awareness ) ; this . _blurListener = ( ) => { const m = this . _cursors . get ( "current" ) ; if ( m !== undefined ) { m . caret . clear ( ) ; this . _cursors . delete ( "current" ) ; } awareness . setLocalStateField ( 'cursor' , null ) ; } ; textType . observe ( this . _typeObserver ) ; codeMirror . on ( 'change' , this . _targetObserver ) ; if ( awareness ) { awareness . on ( 'change' , this . _awarenessListener ) ; codeMirror . on ( 'cursorActivity' , this . _cursorListener ) ; codeMirror . on ( 'blur' , this . _blurListener ) ; codeMirror . on ( 'focus' , this . _cursorListener ) ; } } destroy ( ) { this . type . unobserve ( this . _typeObserver ) ; this . target . off ( 'change' , this . _targetObserver ) ; this . target . off ( 'cursorActivity' , this . _cursorListener ) ; this . target . off ( 'focus' , this . _cursorListener ) ; this . target . off ( 'blur' , this . _blurListener ) ; this . awareness . off ( 'change' , this . _awarenessListener ) ; this . type = null ; this . target = null ; } }