import { debounce , includes } from 'lodash' ; import { Models } from 'omnisharp-client' ; import { Observable , Subject , Subscription } from 'rxjs' ; import { CompositeDisposable , Disposable , IDisposable } from 'ts-disposables' ; import { Omni } from '../server/omni' ; interface IDecoration { destroy ( ) : any ; getMarker ( ) : Atom . Marker ; getProperties ( ) : any ; setProperties ( props ) : any ; } class CodeLens implements IFeature { public required = false ; public title = 'Code Lens' ; public description = 'Adds support for displaying references in the editor.' ; private disposable : CompositeDisposable ; private decorations = new WeakMap < Atom . TextEditor , Set < Lens > > ( ) ; public activate ( ) { this . disposable = new CompositeDisposable ( ) ; this . disposable . add ( Omni . eachEditor ( ( editor , cd ) => { cd . add ( Disposable . create ( ( ) => { const markers = this . decorations . get ( editor ) ; if ( markers ) { markers . forEach ( marker => marker . dispose ( ) ) ; } this . decorations . delete ( editor ) ; } ) ) ; cd . add ( atom . config . observe ( 'editor.fontSize' , ( size ) => { const decorations = this . decorations . get ( editor ) ; const lineHeight = editor . getLineHeightInPixels ( ) ; if ( decorations && lineHeight ) { decorations . forEach ( decoration => decoration . updateTop ( lineHeight ) ) ; } } ) ) ; } ) ) ; this . disposable . add ( Omni . switchActiveEditor ( ( editor , cd ) => { const items = this . decorations . get ( editor ) ; if ( ! items ) { this . decorations . set ( editor , new Set < Lens > ( ) ) ; } const subject = new Subject < boolean > ( ) ; cd . add ( subject . filter ( x => ! ! x && ! editor . isDestroyed ( ) ) . distinctUntilChanged ( x => ! ! x ) . debounceTime ( 500 ) . switchMap ( ( ) => this . updateCodeLens ( editor ) ) . subscribe ( ) ) ; const bindDidChange = ( ) => { const didChange = editor . getBuffer ( ) . onDidChange ( ( ) => { didChange . dispose ( ) ; cd . remove ( didChange ) ; subject . next ( false ) ; } ) ; cd . add ( didChange ) ; } ; cd . add ( editor . getBuffer ( ) . onDidStopChanging ( debounce ( ( ) => { if ( ! subject . closed ) { subject . next ( true ) ; } bindDidChange ( ) ; } , 5000 ) ) ) ; cd . add ( editor . getBuffer ( ) . onDidSave ( ( ) => subject . next ( true ) ) ) ; cd . add ( editor . getBuffer ( ) . onDidReload ( ( ) => subject . next ( true ) ) ) ; cd . add ( Observable . timer ( 1000 ) . subscribe ( ( ) => subject . next ( true ) ) ) ; cd . add ( editor . onDidChangeScrollTop ( ( ) => this . updateDecoratorVisiblility ( editor ) ) ) ; cd . add ( atom . commands . onWillDispatch ( ( event ) => { if ( includes ( [ 'omnisharp-atom:toggle-dock' , 'omnisharp-atom:show-dock' , 'omnisharp-atom:hide-dock' ] , event . type ) ) { this . updateDecoratorVisiblility ( editor ) ; } } ) ) ; cd . add ( subject ) ; this . updateDecoratorVisiblility ( editor ) ; } ) ) ; } public updateDecoratorVisiblility ( editor ) { if ( ! this . decorations . has ( editor ) ) { this . decorations . set ( editor , new Set < Lens > ( ) ) ; } const decorations = this . decorations . get ( editor ) ; decorations . forEach ( decoration => decoration . updateVisible ( ) ) ; } public dispose ( ) { this . disposable . dispose ( ) ; } public updateCodeLens ( editor ) { if ( ! this . decorations . has ( editor ) ) { this . decorations . set ( editor , new Set < Lens > ( ) ) ; } const decorations = this . decorations . get ( editor ) ; const updated = new WeakSet < Lens > ( ) ; if ( editor . isDestroyed ( ) ) { return Observable . empty < number > ( ) ; } return Omni . request ( editor , solution => solution . currentfilemembersasflat ( { Buffer : null , Changes : null } ) ) . filter ( fileMembers => ! ! fileMembers ) . flatMap ( fileMembers => fileMembers ) . concatMap ( fileMember => { const range = < any > editor . getBuffer ( ) . rangeForRow ( fileMember . Line , false ) ; const marker = ( < any > editor ) . markBufferRange ( range , { invalidate : 'inside' } ) ; let lens ; const iteratee = decorations . values ( ) ; let decoration = iteratee . next ( ) ; while ( ! decoration . done ) { if ( decoration . value . isEqual ( marker ) ) { lens = decoration . value ; break ; } decoration = iteratee . next ( ) ; } if ( lens ) { updated . add ( lens ) ; lens . invalidate ( ) ; } else { lens = new Lens ( editor , fileMember , marker , range , Disposable . create ( ( ) => { decorations . delete ( lens ) ; } ) ) ; updated . add ( lens ) ; decorations . add ( lens ) ; } return lens . updateVisible ( ) ; } ) . do ( { complete : ( ) => { decorations . forEach ( lens => { if ( lens && ! updated . has ( lens ) ) { lens . dispose ( ) ; } } ) ; } } ) ; } } function isLineVisible ( editor , line ) { const element = atom . views . getView ( editor ) ; const top = element . getFirstVisibleScreenRow ( ) ; const bottom = element . getLastVisibleScreenRow ( ) ; if ( line <= top || line >= bottom ) { return false ; } return true ; } export class Lens implements IDisposable { public loaded : boolean = false ; private _update : Subject < boolean > ; private _row : number ; private _decoration : IDecoration ; private _disposable = new CompositeDisposable ( ) ; private _element : HTMLDivElement ; private _updateObservable : Observable < number > ; private _path : string ; private _issueUpdate = debounce ( ( isVisible ) => { if ( ! this . _update . closed ) { this . _update . next ( isVisible ) ; } } , 250 ) ; public constructor ( private _editor , private _member , private _marker , private _range , disposer ) { this . _row = _range . getRows ( ) [ 0 ] ; this . _update = new Subject < any > ( ) ; this . _disposable . add ( this . _update ) ; this . _path = _editor . getPath ( ) ; this . _updateObservable = this . _update . filter ( x => ! ! x ) . flatMap ( ( ) => Omni . request ( this . _editor , solution => solution . findusages ( { FileName : this . _path , Column : this . _member . Column + 1 , Line : this . _member . Line , Buffer : null , Changes : null } , { silent : true } ) ) ) . filter ( x => x && x . QuickFixes && ! ! x . QuickFixes . length ) . map ( x => x && x . QuickFixes && x . QuickFixes . length - 1 ) . share ( ) ; this . _disposable . add ( this . _updateObservable . take ( 1 ) . filter ( x => x > 0 ) . do ( ( ) => this . loaded = true ) . subscribe ( x => this . _decorate ( x ) ) ) ; this . _disposable . add ( disposer ) ; this . _disposable . add ( this . _marker . onDidDestroy ( ( ) => { this . dispose ( ) ; } ) ) ; } public updateVisible ( ) { const isVisible = this . _isVisible ( ) ; this . _updateDecoration ( isVisible ) ; let result ; if ( isVisible ) { result = this . _updateObservable . take ( 1 ) ; } else { result = Observable . empty < number > ( ) ; } this . _issueUpdate ( isVisible ) ; return result ; } public updateTop ( lineHeight ) { if ( this . _element ) { this . _element . style . top = `-${ lineHeight }px` ; } } public invalidate ( ) { const self = this . _updateObservable . take ( 1 ) . do ( ( ) => this . _disposable . remove ( self ) ) . subscribe ( x => { if ( x <= 0 ) { this . dispose ( ) ; } else { if ( this . _element ) { ( this . _element . textContent = x . toString ( ) ) ; } } } ) ; this . _disposable . add ( self ) ; } public isEqual ( marker ) { return this . _marker . isEqual ( < any > marker ) ; } public dispose ( ) { return this . _disposable . dispose ( ) ; } private _isVisible ( ) { return isLineVisible ( this . _editor , this . _row ) ; } private _updateDecoration ( isVisible ) { if ( this . _decoration && this . _element ) { const element = this . _element ; if ( isVisible && element . style . display === 'none' ) { element . style . display = '' ; } else if ( element . style . display !== 'none' ) { element . style . display = 'none' ; } } } private _decorate ( count ) { const lineHeight = this . _editor . getLineHeightInPixels ( ) ; const element = this . _element = document . createElement ( 'div' ) ; element . style . position = 'relative' ; element . style . top = `-${ lineHeight }px` ; element . style . left = '16px' ; element . classList . add ( 'highlight-info' , 'badge' , 'badge-small' ) ; element . textContent = count . toString ( ) ; element . onclick = ( ) => Omni . request ( this . _editor , s => s . findusages ( { FileName : this . _path , Column : this . _member . Column + 1 , Line : this . _member . Line , Buffer : null , Changes : null } ) ) ; this . _decoration = < any > this . _editor . decorateMarker ( this . _marker , { type : 'overlay' , class : `codelens` , item : this . _element , position : 'head' } ) ; this . _disposable . add ( Disposable . create ( ( ) => { this . _element . remove ( ) ; if ( this . _decoration ) { this . _decoration . destroy ( ) ; } this . _element = null ; } ) ) ; const isVisible = isLineVisible ( this . _editor , this . _row ) ; if ( ! isVisible ) { element . style . display = 'none' ; } return this . _decoration ; } } export const codeLens = new CodeLens ( ) ;