From 7f9ff2b1c89755a408aa43bbd353f2296e3fafd1 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 5 Oct 2017 15:35:23 +0200 Subject: [PATCH] add FileDecorationsService and use it with markers --- .../workbench/electron-browser/workbench.ts | 5 + .../parts/files/browser/views/explorerView.ts | 18 ++- .../files/browser/views/explorerViewer.ts | 7 +- .../markers/browser/markersFileDecorations.ts | 64 +++++++++++ .../browser/markersWorkbenchContributions.ts | 4 +- .../browser/fileDecorations.ts | 53 +++++++++ .../browser/fileDecorationsService.ts | 104 ++++++++++++++++++ 7 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/parts/markers/browser/markersFileDecorations.ts create mode 100644 src/vs/workbench/services/fileDecorations/browser/fileDecorations.ts create mode 100644 src/vs/workbench/services/fileDecorations/browser/fileDecorationsService.ts diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index c565be4c877..0a894a8df22 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -98,6 +98,8 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { WorkspaceEditingService } from 'vs/workbench/services/workspace/node/workspaceEditingService'; import URI from 'vs/base/common/uri'; +import { FileDecorationsService } from 'vs/workbench/services/fileDecorations/browser/fileDecorationsService'; +import { IFileDecorationsService } from 'vs/workbench/services/fileDecorations/browser/fileDecorations'; export const MessagesVisibleContext = new RawContextKey('globalMessageVisible', false); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); @@ -583,6 +585,9 @@ export class Workbench implements IPartService { // Text File Service serviceCollection.set(ITextFileService, new SyncDescriptor(TextFileService)); + // File Decorations + serviceCollection.set(IFileDecorationsService, new SyncDescriptor(FileDecorationsService)); + // SCM Service serviceCollection.set(ISCMService, new SyncDescriptor(SCMService)); diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index eb9f3dbdc9b..20fc00c0ff2 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -45,6 +45,7 @@ import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/th import { isLinux } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { attachListStyler } from 'vs/platform/theme/common/styler'; +import { IFileDecorationsService } from 'vs/workbench/services/fileDecorations/browser/fileDecorations'; export interface IExplorerViewOptions extends IViewletViewOptions { viewletState: FileViewletState; @@ -98,7 +99,8 @@ export class ExplorerView extends ViewsViewletPanel { @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService private configurationService: IConfigurationService, @IWorkbenchThemeService private themeService: IWorkbenchThemeService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IFileDecorationsService private fileDecorationsService: IFileDecorationsService ) { super({ ...(options as IViewOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService); @@ -166,6 +168,7 @@ export class ExplorerView extends ViewsViewletPanel { this.disposables.push(this.themeService.onDidFileIconThemeChange(onFileIconThemeChange)); this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.refreshFromEvent(e.added))); this.disposables.push(this.contextService.onDidChangeWorkbenchState(e => this.refreshFromEvent())); + this.disposables.push(this.fileDecorationsService.onDidChangeFileDecoration(this.onDidChangeFileDecorations, this)); onFileIconThemeChange(this.themeService.getFileIconTheme()); } @@ -681,6 +684,19 @@ export class ExplorerView extends ViewsViewletPanel { })); } + private onDidChangeFileDecorations(uris: URI[]): void { + let seen = new Set(); + let stack = uris.map(uri => this.model.findClosest(uri)); + while (stack.length > 0) { + let stat = stack.shift(); + if (stat && !seen.has(stat)) { + this.explorerViewer.refresh(stat, false); + stack.push(stat.parent); + seen.add(stat); + } + } + } + private refreshFromEvent(newRoots: IWorkspaceFolder[] = []): void { if (this.isVisible()) { this.explorerRefreshDelayer.trigger(() => { diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index d60aa33d4d8..a2eaf2bdb45 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -58,6 +58,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { getPathLabel } from 'vs/base/common/labels'; import { extractResources } from 'vs/base/browser/dnd'; import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; +import { IFileDecorationsService } from 'vs/workbench/services/fileDecorations/browser/fileDecorations'; export class FileDataSource implements IDataSource { constructor( @@ -290,7 +291,8 @@ export class FileRenderer implements IRenderer { state: FileViewletState, @IContextViewService private contextViewService: IContextViewService, @IInstantiationService private instantiationService: IInstantiationService, - @IThemeService private themeService: IThemeService + @IThemeService private themeService: IThemeService, + @IFileDecorationsService private decorationsService: IFileDecorationsService ) { this.state = state; } @@ -324,6 +326,9 @@ export class FileRenderer implements IRenderer { extraClasses.push('nonexistent-root'); } templateData.label.setFile(stat.resource, { hidePath: true, fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE, extraClasses }); + + let top = this.decorationsService.getTopDecoration(stat.resource, stat.isDirectory); + templateData.label.element.style.color = top ? top.color.toString() : ''; } // Input Box diff --git a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts new file mode 100644 index 00000000000..9030dd7375e --- /dev/null +++ b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { IMarkerService, IMarker } from 'vs/platform/markers/common/markers'; +import { IFileDecorationsService, DecorationType, IFileDecorationData } from 'vs/workbench/services/fileDecorations/browser/fileDecorations'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; +import { Registry } from 'vs/platform/registry/common/platform'; +import Severity from 'vs/base/common/severity'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { editorErrorForeground, editorWarningForeground } from 'vs/editor/common/view/editorColorRegistry'; + +class MarkersFileDecorations implements IWorkbenchContribution { + + private readonly _disposables: IDisposable[]; + private readonly _type: DecorationType; + + constructor( + @IMarkerService private _markerService: IMarkerService, + @IFileDecorationsService private _decorationsService: IFileDecorationsService, + @IThemeService private _themeService: IThemeService + ) { + // + this._disposables = [ + this._markerService.onMarkerChanged(this._onDidChangeMarker, this), + this._type = this._decorationsService.registerDecorationType(localize('errorAndWarnings', "Errors & Warnings")) + ]; + } + + dispose(): void { + dispose(this._disposables); + } + + getId(): string { + return 'markers.MarkersFileDecorations'; + } + + private _onDidChangeMarker(resources: URI[]): void { + for (const resource of resources) { + const markers = this._markerService.read({ resource }); + if (!isFalsyOrEmpty(markers)) { + const data = markers.map(this._toFileDecorationData, this); + this._decorationsService.setFileDecorations(this._type, resource, data); + } else { + this._decorationsService.unsetFileDecorations(this._type, resource); + } + } + } + + private _toFileDecorationData(marker: IMarker): IFileDecorationData { + const { message, severity } = marker; + const color = this._themeService.getTheme().getColor(severity === Severity.Error ? editorErrorForeground : editorWarningForeground); + return { message, severity, color }; + } +} + +Registry.as(Extensions.Workbench).registerWorkbenchContribution(MarkersFileDecorations); diff --git a/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts b/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts index d6b4f686ffd..5c20e92410b 100644 --- a/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts +++ b/src/vs/workbench/parts/markers/browser/markersWorkbenchContributions.ts @@ -17,6 +17,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { MarkersPanel } from 'vs/workbench/parts/markers/browser/markersPanel'; +import './markersFileDecorations'; + export function registerContributions(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -66,4 +68,4 @@ export function registerContributions(): void { // Retaining old action to show errors and warnings, so that custom bindings to this action for existing users works. registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleErrorsAndWarningsAction, ToggleErrorsAndWarningsAction.ID, ToggleErrorsAndWarningsAction.LABEL), 'Show Errors and Warnings'); -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/fileDecorations/browser/fileDecorations.ts b/src/vs/workbench/services/fileDecorations/browser/fileDecorations.ts new file mode 100644 index 00000000000..3bb20dba223 --- /dev/null +++ b/src/vs/workbench/services/fileDecorations/browser/fileDecorations.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Color } from 'vs/base/common/color'; +import URI from 'vs/base/common/uri'; +import Event from 'vs/base/common/event'; +import Severity from 'vs/base/common/severity'; + +export const IFileDecorationsService = createDecorator('IFileDecorationsService'); + +export interface IFileDecoration { + readonly type: DecorationType; + readonly message: string; + readonly color: Color; + readonly severity: Severity; +} + +export abstract class DecorationType { + readonly label: string; + protected constructor(label: string) { + this.label = label; + } + dispose(): void { + // + } +} + +export interface IFileDecorationData { + message: string; + color: Color; + severity: Severity; +} + +export interface IFileDecorationsService { + + readonly _serviceBrand: any; + + readonly onDidChangeFileDecoration: Event; + + registerDecorationType(label: string): DecorationType; + + setFileDecorations(type: DecorationType, target: URI, data: IFileDecorationData[]): void; + + unsetFileDecorations(type: DecorationType, target: URI): void; + + getDecorations(uri: URI, includeChildren: boolean): IFileDecoration[]; + + getTopDecoration(uri: URI, includeChildren: boolean): IFileDecoration; +} diff --git a/src/vs/workbench/services/fileDecorations/browser/fileDecorationsService.ts b/src/vs/workbench/services/fileDecorations/browser/fileDecorationsService.ts new file mode 100644 index 00000000000..392f6ece311 --- /dev/null +++ b/src/vs/workbench/services/fileDecorations/browser/fileDecorationsService.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import URI from 'vs/base/common/uri'; +import Severity from 'vs/base/common/severity'; +import Event, { Emitter, debounceEvent } from 'vs/base/common/event'; +import { IFileDecorationsService, IFileDecoration, DecorationType, IFileDecorationData } from 'vs/workbench/services/fileDecorations/browser/fileDecorations'; +import { TernarySearchTree } from 'vs/base/common/map'; +import { mergeSort, isFalsyOrEmpty } from 'vs/base/common/arrays'; + + +export class FileDecorationsService implements IFileDecorationsService { + + readonly _serviceBrand; + + private readonly _onDidChangeFileDecoration = new Emitter(); + private readonly _types = new Map>(); + + readonly onDidChangeFileDecoration: Event = debounceEvent( + this._onDidChangeFileDecoration.event, + (last, current) => { + if (!last) { + last = []; + } + last.push(current); + return last; + } + ); + + registerDecorationType(label: string): DecorationType { + const outer = this; + const type = new class extends DecorationType { + constructor() { + super(label); + } + dispose() { + outer._types.delete(type); + } + }; + this._types.set(type, TernarySearchTree.forPaths()); + return type; + } + + setFileDecorations(type: DecorationType, target: URI, data: IFileDecorationData[]): void { + let decorations = mergeSort(data.map(data => ({ type, ...data })), FileDecorationsService._compareFileDecorationsBySeverity); + this._types.get(type).set(target.toString(), decorations); + this._onDidChangeFileDecoration.fire(target); + } + + unsetFileDecorations(type: DecorationType, target: URI): void { + this._types.get(type).delete(target.toString()); + this._onDidChangeFileDecoration.fire(target); + } + + getDecorations(uri: URI, includeChildren: boolean): IFileDecoration[] { + let ret: IFileDecoration[] = []; + this._someFileDecoration(uri, includeChildren, decoration => { + ret.push(decoration); + return false; + }); + return ret; + } + + getTopDecoration(uri: URI, includeChildren: boolean): IFileDecoration { + let top: IFileDecoration; + this._someFileDecoration(uri, includeChildren, decoration => { + // top is the most severe one, + // stop as soon as an error is found + if (!top || FileDecorationsService._compareFileDecorationsBySeverity(top, decoration) > 0) { + top = decoration; + } + return top.severity === Severity.Error; + }); + return top; + } + + private _someFileDecoration(uri: URI, includeChildren: boolean, callback: (a: IFileDecoration) => boolean): void { + let key = uri.toString(); + let done = false; + this._types.forEach(tree => { + if (done) { + return; + } + if (includeChildren) { + let newTree = tree.findSuperstr(key); + if (newTree) { + newTree.forEach(([, data]) => done = done || data.some(callback)); + } + } else { + let list = tree.get(key); + if (!isFalsyOrEmpty(list)) { + done = list.some(callback); + } + } + }); + } + + private static _compareFileDecorationsBySeverity(a: IFileDecoration, b: IFileDecoration): number { + return Severity.compare(a.severity, b.severity); + } +} -- GitLab