// ***************************************************************************** // 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-only 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 { Event, Emitter } from '@theia/core/lib/common/event'; import { Tree, TreeNode } 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 '../../common/problem-preferences'; import { ProblemUtils } from './problem-utils'; import { LabelProvider } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; /** * @deprecated since 1.25.0 * URI-based decorators should implement `DecorationsProvider` and contribute decorations via the `DecorationsService`. */ @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>; constructor(@inject(ProblemManager) protected readonly problemManager: ProblemManager) { this.emitter = new Emitter(); this.problemManager.onDidChangeMarkers(() => this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree))); } @postConstruct() protected init(): void { this.problemPreferences.onPreferenceChanged(event => { if (event.preferenceName === 'problems.decorations.enabled') { this.fireDidChangeDecorations(tree => this.collectDecorators(tree)); } }); this.workspaceService.onWorkspaceChanged(() => { this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); }); this.workspaceService.onWorkspaceLocationChanged(() => { this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); }); } async decorations(tree: Tree): Promise> { return this.collectDecorators(tree); } get onDidChangeDecorations(): Event<(tree: Tree) => Map> { return this.emitter.event; } protected fireDidChangeDecorations(event: (tree: Tree) => Map): void { this.emitter.fire(event); } protected collectDecorators(tree: Tree): Map { const decorations = new Map(); // If the tree root is undefined or the preference for the decorations is disabled, return an empty result map. if (!tree.root || !this.problemPreferences['problems.decorations.enabled']) { return decorations; } const baseDecorations = this.collectMarkers(tree); for (const node of new DepthFirstTreeIterator(tree.root)) { const nodeUri = this.getUriFromNode(node); if (nodeUri) { const decorator = baseDecorations.get(nodeUri); if (decorator) { this.appendContainerMarkers(node, decorator, decorations); } if (decorator) { decorations.set(node.id, decorator); } } } return decorations; } protected generateCaptionSuffix(nodeURI: URI): string { 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?.fsPath() ?? ''; separator = filePathString && workspacePrefixString ? ' \u2022 ' : ''; // add a bullet point between workspace and path } else { workspacePrefixString = nodeURIDir.path.fsPath(); } return `${workspacePrefixString}${separator}${filePathString}`; } /** * Traverses up the tree from the given node and attaches decorations to any parents. */ protected appendContainerMarkers(node: TreeNode, decoration: TreeDecoration.Data, decorations: Map): void { let parent = node?.parent; while (parent) { const existing = decorations.get(parent.id); // Make sure the highest diagnostic severity (smaller number) will be propagated to the container directory. if (existing === undefined || this.compareDecorators(existing, decoration) < 0) { decorations.set(parent.id, decoration); parent = parent.parent; } else { break; } } } /** * @returns a map matching stringified URI's to a decoration whose features reflect the highest-severity problem found * and the number of problems found (based on {@link ProblemDecorator.toDecorator }) */ protected collectMarkers(tree: Tree): Map { const decorationsForUri = new Map(); const compare = this.compare.bind(this); const filter = this.filterMarker.bind(this); for (const [, markers] of this.problemManager.getMarkersByUri()) { const relevant = markers.findMarkers({}).filter(filter).sort(compare); if (relevant.length) { decorationsForUri.set(relevant[0].uri, this.toDecorator(relevant)); } } return decorationsForUri; } protected toDecorator(markers: Marker[]): TreeDecoration.Data { const color = this.getColor(markers[0]); const priority = this.getPriority(markers[0]); return { priority, fontData: { color, }, tailDecorations: [{ color, data: markers.length.toString(), }], }; } protected getColor(marker: Marker): TreeDecoration.Color { const { severity } = marker.data; switch (severity) { case 1: return 'var(--theia-list-errorForeground)'; case 2: return 'var(--theia-list-warningForeground)'; default: return 'var(--theia-successBackground)'; } } /** * Get the decoration for a given marker diagnostic. * Markers with higher severity have a higher priority and should be displayed. * @param marker the diagnostic marker. */ protected getPriority(marker: Marker): number { const { severity } = marker.data; switch (severity) { case 1: return 30; // Errors. case 2: return 20; // Warnings. case 3: return 10; // Infos. 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: Marker): boolean { const { severity } = marker.data; return severity === DiagnosticSeverity.Error || severity === DiagnosticSeverity.Warning || severity === DiagnosticSeverity.Information; } protected getUriFromNode(node: TreeNode): string | undefined { return FileStatNode.getUri(node); } protected compare(left: Marker, right: Marker): number { return ProblemDecorator.severityCompare(left, right); } protected compareDecorators(left: TreeDecoration.Data, right: TreeDecoration.Data): number { return TreeDecoration.Data.comparePriority(left, right); } } export namespace ProblemDecorator { // Highest severities (errors) come first, then the others. Undefined severities treated as the last ones. export const severityCompare = ProblemUtils.severityCompareMarker; }