import { Directive , Injectable , Input , OnDestroy , OnInit } from '@angular/core' ; import * as CodeMirror from 'codemirror' ; import { Subscription } from 'rxjs' ; import { filter } from 'rxjs/operators' ; import { Streams , StreamsSubtype } from '@coast-team/mute-core' ; import { DocService } from '../../doc.service' ; import { NetworkService } from '../../network' ; import { RichCollaborator , RichCollaboratorsService } from '../../rich-collaborators' ; import { CollaboratorCursor } from './CollaboratorCursor' ; import * as proto from './cursor_proto' ; const cursorMoveKeys = [ 'ArrowLeft' , 'ArrowDown' , 'ArrowUp' , 'ArrorRight' , 'PageDown' , 'PageUp' , 'Home' , 'End' ] ; @ Directive ( { selector : '[muteCursors]' , } ) @ Injectable ( ) export class CursorsDirective implements OnInit , OnDestroy { @ Input ( ) cm : CodeMirror . Editor ; private subs : Subscription [ ] ; private cmDoc : CodeMirror . Doc ; private cursors : Map < number , CollaboratorCursor > ; private protoCursor : proto . Cursor ; private protoAnchor : proto . Position ; private protoHead : proto . Position ; constructor ( private docService , private collabService , private network ) { this . protoCursor = proto . Cursor . create ( ) ; this . protoAnchor = proto . Position . create ( ) ; this . protoHead = proto . Position . create ( ) ; this . cursors = new Map ( ) ; this . subs = [ ] ; } ngOnInit ( ) { this . cmDoc = this . cm . getDoc ( ) ; this . subs . push ( this . collabService . onJoin . subscribe ( ( collab ) => { this . cursors . set ( collab . id , new CollaboratorCursor ( this . cm , collab ) ) ; if ( this . cm . hasFocus ( ) ) { this . sendMyCursorPos ( ) ; } } ) ) ; this . subs . push ( this . collabService . onLeave . subscribe ( ( id ) => { const cursor = this . cursors . get ( id ) ; if ( cursor !== undefined ) { cursor . clean ( ) ; this . cursors . delete ( id ) ; } } ) ) ; this . subs . push ( this . collabService . onUpdate . subscribe ( ( collab ) => { const cursor = this . cursors . get ( collab . id ) ; if ( cursor !== undefined ) { cursor . updateDisplayName ( collab . displayName ) ; } } ) ) ; let cursorPosBeforeBlur ; CodeMirror . on ( this . cm , 'blur' , ( ) => { this . protoCursor . anchor = undefined ; this . protoCursor . head = undefined ; this . sendMyCursorPosition ( ) ; cursorPosBeforeBlur = this . cmDoc . getCursor ( 'head' ) ; } ) ; CodeMirror . on ( this . cm , 'focus' , ( ) => { const head = this . cmDoc . getCursor ( 'head' ) ; if ( ! cursorPosBeforeBlur || ( cursorPosBeforeBlur . line === head . line && cursorPosBeforeBlur . ch === head . ch ) ) { this . sendMyCursorPos ( ) ; } } ) ; this . listenEventsForCursorChange ( ) ; this . subs [ this . subs . length ] = this . network . messageOut . pipe ( filter ( ( { streamId } ) => streamId . type === Streams . CURSOR ) ) . subscribe ( ( { senderId , content } ) => { try { const protoCursor = proto . Cursor . decode ( content ) ; const cursor = this . cursors . get ( senderId ) ; if ( cursor ) { if ( protoCursor . head ) { const headPos = this . protoPos2codemirrorPos ( protoCursor . head ) ; if ( protoCursor . anchor ) { const anchorPos = this . protoPos2codemirrorPos ( protoCursor . anchor ) ; cursor . updateSelection ( anchorPos , headPos ) ; } else { cursor . removeSelection ( ) ; cursor . updateCursor ( headPos ) ; } } else { cursor . removeCursor ( ) ; } } } catch ( err ) { log . warn ( 'Cursor error: ' , err . message ) ; } } ) ; this . subs . push ( this . docService . doc . remoteContentChanges . subscribe ( ( ops ) => { ops . forEach ( ( { collaborator } ) => { if ( collaborator ) { const cursor = this . cursors . get ( collaborator . id ) ; if ( cursor ) { cursor . resetDisplayNameTimeout ( ) ; } } } ) ; } ) ) ; } ngOnDestroy ( ) { this . subs . forEach ( ( sub ) => sub . unsubscribe ( ) ) ; } private protoPos2codemirrorPos ( pos ) { if ( pos . id && pos . id . tuples ) { return this . cmDoc . posFromIndex ( this . docService . indexFromId ( pos . id as any ) + pos . index ) ; } else { const lastLine = this . cmDoc . lastLine ( ) ; return { line : lastLine , ch : this . cmDoc . getLine ( lastLine ) . length } ; } } private sendMyCursorPos ( ) { const anchor = this . cmDoc . getCursor ( 'anchor' ) ; const head = this . cmDoc . getCursor ( 'head' ) ; if ( head . line !== anchor . line || head . ch !== anchor . ch ) { const muteCoreAnchor = this . docService . positionFromIndex ( this . cmDoc . indexFromPos ( anchor ) ) ; if ( muteCoreAnchor ) { this . protoAnchor . id = muteCoreAnchor . id ; this . protoAnchor . index = muteCoreAnchor . index ; } else { this . protoAnchor . id = undefined ; this . protoAnchor . index = undefined ; } this . protoCursor . anchor = this . protoAnchor ; } else { this . protoCursor . anchor = undefined ; } const muteCoreHead = this . docService . positionFromIndex ( this . cmDoc . indexFromPos ( head ) ) ; if ( muteCoreHead ) { this . protoHead . id = muteCoreHead . id ; this . protoHead . index = muteCoreHead . index ; this . protoCursor . head = this . protoHead ; } else { this . protoHead . id = undefined ; this . protoHead . index = undefined ; } this . protoCursor . head = this . protoHead ; this . sendMyCursorPosition ( ) ; } private listenEventsForCursorChange ( ) { let cursorShouldBeSent = false ; CodeMirror . on ( this . cm , 'cursorActivity' , ( ) => { if ( cursorShouldBeSent ) { this . sendMyCursorPos ( ) ; } } ) ; CodeMirror . on ( this . cm , 'keydown' , ( instance , event ) => { cursorShouldBeSent = cursorMoveKeys . includes ( event . key ) ; } ) ; CodeMirror . on ( this . cm , 'mousedown' , ( ) => ( cursorShouldBeSent = true ) ) ; CodeMirror . on ( this . cm , 'touchstart' , ( ) => { cursorShouldBeSent = true ; } ) ; CodeMirror . on ( this . cm , 'keyHandled' , ( instance , name , event ) => { const anchor = this . cmDoc . getCursor ( 'anchor' ) ; const head = this . cmDoc . getCursor ( 'head' ) ; if ( anchor . line !== head . line || anchor . ch !== head . ch ) { this . sendMyCursorPos ( ) ; } } ) ; } private sendMyCursorPosition ( ) { this . network . send ( { type : Streams . CURSOR , subtype : StreamsSubtype . CURSOR } , proto . Cursor . encode ( this . protoCursor ) . finish ( ) ) ; } }