/********************************************************************************
 * Copyright (C) 2018 TypeFox 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 { inject , injectable , postConstruct } from '@theia/core/shared/inversify' ; import { Diagnostic , DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol' ; import URI from '@theia/core/lib/common/uri' ; import { notEmpty } from '@theia/core/lib/common/objects' ; import { Event , Emitter } from '@theia/core/lib/common/event' ; import { Tree } from '@theia/core/lib/browser/tree/tree' ; import { DepthFirstTreeIterator } from '@theia/core/lib/browser/tree/tree-iterator' ; import { TreeDecorator , TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator' ; import { FileStatNode } from '@theia/filesystem/lib/browser' ; import { Marker } from '../../common/marker' ; import { ProblemManager } from './problem-manager' ; import { ProblemPreferences } from './problem-preferences' ; import { ProblemUtils } from './problem-utils' ; import { OpenEditorNode } from '@theia/navigator/lib/browser/open-editors-widget/navigator-open-editors-tree-model' ; import { LabelProvider } from '@theia/core/lib/browser' ; import { WorkspaceService } from '@theia/workspace/lib/browser' ; import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration' ; @ injectable ( ) export class ProblemDecorator implements TreeDecorator { @ inject ( ProblemPreferences ) protected problemPreferences : ProblemPreferences ; @ inject ( WorkspaceService ) protected readonly workspaceService : WorkspaceService ; @ inject ( LabelProvider ) protected readonly labelProvider : LabelProvider ; readonly id = 'theia-problem-decorator' ; protected readonly emitter : Emitter < ( tree : Tree ) => Map < string , TreeDecoration . Data > > ; constructor ( @ inject ( ProblemManager ) protected readonly problemManager ) { this . emitter = new Emitter ( ) ; this . problemManager . onDidChangeMarkers ( ( ) => this . fireDidChangeDecorations ( ( tree ) => this . collectDecorators ( tree ) ) ) ; } @ postConstruct ( ) protected init ( ) { this . problemPreferences . onPreferenceChanged ( event => { if ( event . preferenceName === 'problems.decorations.enabled' ) { this . fireDidChangeDecorations ( tree => this . collectDecorators ( tree ) ) ; } } ) ; this . workspaceService . onWorkspaceChanged ( ( ) => { this . fireDidChangeDecorations ( ( tree ) => this . collectDecorators ( tree ) ) ; } ) ; this . workspaceService . onWorkspaceLocationChanged ( ( ) => { this . fireDidChangeDecorations ( ( tree ) => this . collectDecorators ( tree ) ) ; } ) ; } async decorations ( tree ) { return this . collectDecorators ( tree ) ; } get onDidChangeDecorations ( ) : Event < ( tree : Tree ) => Map < string , TreeDecoration . Data > > { return this . emitter . event ; } protected fireDidChangeDecorations ( event ) { this . emitter . fire ( event ) ; } protected collectDecorators ( tree ) { const decorations = new Map < string , TreeDecoration . Data > ( ) ; if ( tree . root === undefined || ! this . problemPreferences [ 'problems.decorations.enabled' ] ) { return decorations ; } const markers = this . appendContainerMarkers ( tree , this . collectMarkers ( tree ) ) ; for ( const node of new DepthFirstTreeIterator ( tree . root ) ) { const nodeUri = FileStatNode . getUri ( node ) ; if ( nodeUri ) { const marker = markers . get ( nodeUri ) ; let decorator ; if ( marker ) { decorator = this . toDecorator ( marker ) ; } if ( OpenEditorNode . is ( node ) ) { decorator = this . appendSuffixDecoration ( node . uri , decorator ) ; } if ( decorator ) { decorations . set ( node . id , decorator ) ; } } } return decorations ; } protected appendSuffixDecoration ( nodeURI , existingDecorations ? ) { const workspaceAndPath = this . generateCaptionSuffix ( nodeURI ) ; const color = existingDecorations ?. fontData ?. color ; const captionSuffix = { data : workspaceAndPath } ; if ( color ) { Object . assign ( captionSuffix , { fontData : { color } } ) ; } const suffixDecorations = { captionSuffixes : [ captionSuffix ] } ; const decorator = existingDecorations ?? { } ; return Object . assign ( decorator , suffixDecorations ) ; } protected generateCaptionSuffix ( nodeURI ) { const workspaceRoots = this . workspaceService . tryGetRoots ( ) ; const parentWorkspace = this . workspaceService . getWorkspaceRootUri ( nodeURI ) ; let workspacePrefixString = '' ; let separator = '' ; let filePathString = '' ; const nodeURIDir = nodeURI . parent ; if ( parentWorkspace ) { const relativeDirFromWorkspace = parentWorkspace . relative ( nodeURIDir ) ; workspacePrefixString = workspaceRoots . length > 1 ? this . labelProvider . getName ( parentWorkspace ) : '' ; filePathString = relativeDirFromWorkspace ?. toString ( ) ?? '' ; separator = filePathString && workspacePrefixString ? ' \u2022 ' : '' ; } else { workspacePrefixString = nodeURIDir . path . toString ( ) ; } return `${ workspacePrefixString }${ separator }${ filePathString }` ; } protected appendContainerMarkers ( tree , markers ) { const result = new Map ( ) ; for ( const [ uri , marker ] of new Map ( markers . map ( m => [ new URI ( m . uri ) , m ] as [ URI , Marker < Diagnostic > ] ) ) . entries ( ) ) { const uriString = uri . toString ( ) ; result . set ( uriString , marker ) ; let parentUri = uri . parent ; while ( parentUri && ! parentUri . path . isRoot ) { const parentUriString = parentUri . toString ( ) ; const existing = result . get ( parentUriString ) ; if ( existing === undefined || this . compare ( marker , existing ) < 0 ) { result . set ( parentUriString , { data : marker . data , uri : parentUriString , owner : marker . owner , kind : marker . kind } ) ; parentUri = parentUri . parent ; } else { parentUri = undefined ; } } } return result ; } protected collectMarkers ( tree ) { return Array . from ( this . problemManager . getUris ( ) ) . map ( uri => new URI ( uri ) ) . map ( uri => this . problemManager . findMarkers ( { uri } ) ) . map ( markers => markers . sort ( this . compare . bind ( this ) ) ) . map ( markers => markers . shift ( ) ) . filter ( notEmpty ) . filter ( this . filterMarker . bind ( this ) ) ; } protected toDecorator ( marker ) { const position = TreeDecoration . IconOverlayPosition . BOTTOM_RIGHT ; const icon = this . getOverlayIcon ( marker ) ; const color = this . getOverlayIconColor ( marker ) ; const priority = this . getPriority ( marker ) ; return { priority , fontData : { color , } , iconOverlay : { position , icon , color , background : { shape : 'circle' , color : 'transparent' } } , } ; } protected getOverlayIcon ( marker ) { const { severity } = marker . data ; switch ( severity ) { case 1 : return 'times-circle' ; case 2 : return 'exclamation-circle' ; case 3 : return 'info-circle' ; default : return 'hand-o-up' ; } } protected getOverlayIconColor ( marker ) { const { severity } = marker . data ; switch ( severity ) { case 1 : return 'var(--theia-editorError-foreground)' ; case 2 : return 'var(--theia-editorWarning-foreground)' ; case 3 : return 'var(--theia-editorInfo-foreground)' ; default : return 'var(--theia-successBackground)' ; } } param marker protected getPriority ( marker ) { const { severity } = marker . data ; switch ( severity ) { case 1 : return 30 ; case 2 : return 20 ; case 3 : return 10 ; default : return 0 ; } } /**
     * Returns `true` if the diagnostic (`data`) of the marker argument has `Error`, `Warning`, or `Information` severity.
     * Otherwise, returns `false`.
     */ protected filterMarker ( marker ) { const { severity } = marker . data ; return severity === DiagnosticSeverity . Error || severity === DiagnosticSeverity . Warning || severity === DiagnosticSeverity . Information ; } protected compare ( left , right ) { return ProblemDecorator . severityCompare ( left , right ) ; } } export namespace ProblemDecorator { export const severityCompare = ProblemUtils . severityCompareMarker ; }