/********************************************************************************
 * Copyright (C) 2020 Red Hat, Inc. and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import { injectable } from 'inversify' ; import { CancellationToken , CancellationTokenSource , Disposable , Emitter , Event } from '../common' ; import { TernarySearchTree } from '../common/ternary-search-tree' ; import URI from '../common/uri' ; export interface DecorationsProvider { readonly onDidChange : Event < URI [ ] > ; provideDecorations ( uri , token ) : Decoration | Promise < Decoration | undefined > | undefined ; } export interface Decoration { readonly weight ? : number ; readonly colorId ? : string ; readonly letter ? : string ; readonly tooltip ? : string ; readonly bubble ? : boolean ; } export interface ResourceDecorationChangeEvent { affectsResource ( uri ) : boolean ; } export const DecorationsService = Symbol ( 'DecorationsService' ) ; export interface DecorationsService { readonly onDidChangeDecorations : Event < Map < string , Decoration > > ; registerDecorationsProvider ( provider ) : Disposable ; getDecoration ( uri , includeChildren ) : Decoration [ ] ; } class DecorationDataRequest { constructor ( readonly source , readonly thenable ) { } } class DecorationProviderWrapper { readonly data : TernarySearchTree < URI , DecorationDataRequest | Decoration | undefined > ; readonly decorations : Map < string , Decoration > = new Map ( ) ; private readonly disposable : Disposable ; constructor ( readonly provider , readonly onDidChangeDecorationsEmitter ) { this . data = TernarySearchTree . forUris < DecorationDataRequest | Decoration | undefined > ( true ) ; this . disposable = this . provider . onDidChange ( async ( uris ) => { this . decorations . clear ( ) ; if ( ! uris ) { this . data . clear ( ) ; } else { for ( const uri of uris ) { this . fetchData ( new URI ( uri . toString ( ) ) ) ; const decoration = await provider . provideDecorations ( uri , CancellationToken . None ) ; if ( decoration ) { this . decorations . set ( uri . toString ( ) , decoration ) ; } } } this . onDidChangeDecorationsEmitter . fire ( this . decorations ) ; } ) ; } dispose ( ) { this . disposable . dispose ( ) ; this . data . clear ( ) ; } knowsAbout ( uri ) { return ! ! this . data . get ( uri ) || Boolean ( this . data . findSuperstr ( uri ) ) ; } getOrRetrieve ( uri , includeChildren , callback ) { let item = this . data . get ( uri ) ; if ( item === undefined ) { item = this . fetchData ( uri ) ; } if ( item && ! ( item instanceof DecorationDataRequest ) ) { callback ( item , false ) ; } if ( includeChildren ) { const iter = this . data . findSuperstr ( uri ) ; if ( iter ) { let next = iter . next ( ) ; while ( ! next . done ) { const value = next . value ; if ( value && ! ( value instanceof DecorationDataRequest ) ) { callback ( value , true ) ; } next = iter . next ( ) ; } } } } private fetchData ( uri ) { const pendingRequest = this . data . get ( new URI ( uri . toString ( ) ) ) ; if ( pendingRequest instanceof DecorationDataRequest ) { pendingRequest . source . cancel ( ) ; this . data . delete ( uri ) ; } const source = new CancellationTokenSource ( ) ; const dataOrThenable = this . provider . provideDecorations ( new URI ( uri . toString ( ) ) , source . token ) ; if ( ! isThenable < Decoration | Promise < Decoration | undefined > | undefined > ( dataOrThenable ) ) { return this . keepItem ( uri , dataOrThenable ) ; } else { const request = new DecorationDataRequest ( source , Promise . resolve ( dataOrThenable ) . then ( data => { if ( this . data . get ( uri ) === request ) { this . keepItem ( uri , data ) ; } } ) . catch ( err => { if ( ! ( err instanceof Error && err . name === 'Canceled' && err . message === 'Canceled' ) && this . data . get ( uri ) === request ) { this . data . delete ( uri ) ; } } ) ) ; this . data . set ( uri , request ) ; return undefined ; } function isThenable < T > ( obj ) { return obj && typeof ( < Promise < any > > obj ) . then === 'function' ; } } private keepItem ( uri , data ) { const deco = data ? data : undefined ; this . data . set ( uri , deco ) ; return deco ; } } @ injectable ( ) export class DecorationsServiceImpl implements DecorationsService { private readonly data : DecorationProviderWrapper [ ] = [ ] ; private readonly onDidChangeDecorationsEmitter = new Emitter < Map < string , Decoration > > ( ) ; readonly onDidChangeDecorations = this . onDidChangeDecorationsEmitter . event ; dispose ( ) { this . onDidChangeDecorationsEmitter . dispose ( ) ; } registerDecorationsProvider ( provider ) { const wrapper = new DecorationProviderWrapper ( provider , this . onDidChangeDecorationsEmitter ) ; this . data . push ( wrapper ) ; return Disposable . create ( ( ) => { this . data . splice ( this . data . indexOf ( wrapper ) , 1 ) ; this . onDidChangeDecorationsEmitter . fire ( new Map < string , Decoration > ( ) ) ; wrapper . dispose ( ) ; } ) ; } getDecoration ( uri , includeChildren ) { const data = [ ] ; let containsChildren = false ; for ( const wrapper of this . data ) { wrapper . getOrRetrieve ( new URI ( uri . toString ( ) ) , includeChildren , ( deco , isChild ) => { if ( ! isChild || deco . bubble ) { data . push ( deco ) ; containsChildren = isChild || containsChildren ; } } ) ; } return data ; } }