From 41347912b3cd8ebc31130b7c0ffaa77369345f2a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 Dec 2018 12:45:53 +0100 Subject: [PATCH] labels - introduce ResourceLabels and adopt in tabs --- src/vs/workbench/browser/labels.ts | 293 ++++++++++++++++++ .../browser/parts/editor/tabsTitleControl.ts | 29 +- 2 files changed, 308 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index d5afcc62217..9c760f92849 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -22,6 +22,299 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { ILabelService } from 'vs/platform/label/common/label'; import { getIconClasses, getConfiguredLangId } from 'vs/editor/common/services/getIconClasses'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export interface IResourceLabelHandle extends IDisposable { + readonly element: HTMLElement; + + setLabel(label: IResourceLabel, options?: IResourceLabelOptions): void; +} + +export class ResourceLabels extends Disposable { + private _widgets: ResourceLabelWidget[] = []; + private _labels: IResourceLabelHandle[] = []; + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @IExtensionService private extensionService: IExtensionService, + @IConfigurationService private configurationService: IConfigurationService, + @IModelService private modelService: IModelService, + @IDecorationsService protected decorationsService: IDecorationsService, + @IThemeService private themeService: IThemeService, + @ILabelService protected labelService: ILabelService + ) { + super(); + + this.registerListeners(); + } + + get labels(): IResourceLabelHandle[] { + return this._labels; + } + + get(index: number): IResourceLabelHandle { + return this._labels[index]; + } + + private registerListeners(): void { + + // notify when extensions are registered with potentially new languages + this._register(this.extensionService.onDidRegisterExtensions(() => this._widgets.forEach(widget => widget.notifyExtensionsRegistered()))); + + // notify when model mode changes + this._register(this.modelService.onModelModeChanged(e => this._widgets.forEach(widget => widget.notifyModelModeChanged(e)))); + + // notify when file decoration changes + this._register(this.decorationsService.onDidChangeDecorations(e => this._widgets.forEach(widget => widget.notifyFileDecorationsChanges(e)))); + + // notify when theme changes + this._register(this.themeService.onThemeChange(() => () => this._widgets.forEach(widget => widget.notifyThemeChange()))); + + // notify when files.associations changes + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(FILES_ASSOCIATIONS_CONFIG)) { + this._widgets.forEach(widget => widget.notifyFileAssociationsChange()); + } + })); + } + + create(container: HTMLElement, options?: IIconLabelCreationOptions): IResourceLabelHandle { + const widget = this.instantiationService.createInstance(ResourceLabelWidget, container, options); + + // Only expose a handle to the outside + const label: IResourceLabelHandle = { + element: widget.element, + setLabel: (label: IResourceLabel, options?: IResourceLabelOptions) => widget.setLabel(label, options), + dispose: () => this.disposeWidget(widget) + }; + + // Store + this._labels.push(label); + this._widgets.push(widget); + + return label; + } + + private disposeWidget(widget: ResourceLabelWidget): void { + const index = this._widgets.indexOf(widget); + if (index > -1) { + this._widgets.splice(index, 1); + this._labels.splice(index, 1); + } + + dispose(widget); + } + + clear(): void { + this._widgets = dispose(this._widgets); + this._labels = []; + } + + dispose(): void { + super.dispose(); + + this.clear(); + } +} + +class ResourceLabelWidget extends IconLabel { + + private _onDidRender = this._register(new Emitter()); + get onDidRender(): Event { return this._onDidRender.event; } + + private label: IResourceLabel; + private options: IResourceLabelOptions; + private computedIconClasses: string[]; + private lastKnownConfiguredLangId: string; + private computedPathLabel: string; + + constructor( + container: HTMLElement, + options: IIconLabelCreationOptions, + @IModeService private modeService: IModeService, + @IModelService private modelService: IModelService, + @IDecorationsService protected decorationsService: IDecorationsService, + @ILabelService protected labelService: ILabelService + ) { + super(container, options); + } + + notifyModelModeChanged(e: { model: ITextModel; oldModeId: string; }): void { + if (!this.label || !this.label.resource) { + return; // only update if label exists + } + + if (!e.model.uri) { + return; // we need the resource to compare + } + + if (e.model.uri.scheme === Schemas.file && e.oldModeId === PLAINTEXT_MODE_ID) { // todo@remote does this apply? + return; // ignore transitions in files from no mode to specific mode because this happens each time a model is created + } + + if (e.model.uri.toString() === this.label.resource.toString()) { + if (this.lastKnownConfiguredLangId !== e.model.getLanguageIdentifier().language) { + this.render(true); // update if the language id of the model has changed from our last known state + } + } + } + + notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): void { + if (!this.options || !this.label || !this.label.resource) { + return; + } + + if (this.options.fileDecorations && e.affectsResource(this.label.resource)) { + this.render(false); + } + } + + notifyExtensionsRegistered(): void { + this.render(true); + } + + notifyThemeChange(): void { + this.render(false); + } + + notifyFileAssociationsChange(): void { + this.render(true); + } + + setLabel(label: IResourceLabel, options?: IResourceLabelOptions): void { + const hasResourceChanged = this.hasResourceChanged(label, options); + + this.label = label; + this.options = options; + + if (hasResourceChanged) { + this.computedPathLabel = void 0; // reset path label due to resource change + } + + this.render(hasResourceChanged); + } + + private hasResourceChanged(label: IResourceLabel, options: IResourceLabelOptions): boolean { + const newResource = label ? label.resource : void 0; + const oldResource = this.label ? this.label.resource : void 0; + + const newFileKind = options ? options.fileKind : void 0; + const oldFileKind = this.options ? this.options.fileKind : void 0; + + if (newFileKind !== oldFileKind) { + return true; // same resource but different kind (file, folder) + } + + if (newResource && this.computedPathLabel !== this.labelService.getUriLabel(newResource)) { + return true; + } + + if (newResource && oldResource) { + return newResource.toString() !== oldResource.toString(); + } + + if (!newResource && !oldResource) { + return false; + } + + return true; + } + + clear(): void { + this.label = void 0; + this.options = void 0; + this.lastKnownConfiguredLangId = void 0; + this.computedIconClasses = void 0; + this.computedPathLabel = void 0; + + this.setValue(); + } + + private render(clearIconCache: boolean): void { + if (this.label) { + const configuredLangId = getConfiguredLangId(this.modelService, this.label.resource); + if (this.lastKnownConfiguredLangId !== configuredLangId) { + clearIconCache = true; + this.lastKnownConfiguredLangId = configuredLangId; + } + } + + if (clearIconCache) { + this.computedIconClasses = void 0; + } + + if (!this.label) { + return; + } + + const iconLabelOptions: IIconLabelValueOptions = { + title: '', + italic: this.options && this.options.italic, + matches: this.options && this.options.matches, + extraClasses: [] + }; + + const resource = this.label.resource; + const label = this.label.name; + + if (this.options && typeof this.options.title === 'string') { + iconLabelOptions.title = this.options.title; + } else if (resource && resource.scheme !== Schemas.data /* do not accidentally inline Data URIs */) { + if (!this.computedPathLabel) { + this.computedPathLabel = this.labelService.getUriLabel(resource); + } + + iconLabelOptions.title = this.computedPathLabel; + } + + if (this.options && !this.options.hideIcon) { + if (!this.computedIconClasses) { + this.computedIconClasses = getIconClasses(this.modelService, this.modeService, resource, this.options && this.options.fileKind); + } + iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); + } + if (this.options && this.options.extraClasses) { + iconLabelOptions.extraClasses.push(...this.options.extraClasses); + } + + if (this.options && this.options.fileDecorations && resource) { + const deco = this.decorationsService.getDecoration( + resource, + this.options.fileKind !== FileKind.FILE, + this.options.fileDecorations.data + ); + + if (deco) { + if (deco.tooltip) { + iconLabelOptions.title = `${iconLabelOptions.title} • ${deco.tooltip}`; + } + + if (this.options.fileDecorations.colors) { + iconLabelOptions.extraClasses.push(deco.labelClassName); + } + + if (this.options.fileDecorations.badges) { + iconLabelOptions.extraClasses.push(deco.badgeClassName); + } + } + } + + this.setValue(label, this.label.description, iconLabelOptions); + + this._onDidRender.fire(); + } + + dispose(): void { + super.dispose(); + + this.label = void 0; + this.options = void 0; + this.lastKnownConfiguredLangId = void 0; + this.computedIconClasses = void 0; + this.computedPathLabel = void 0; + } +} export interface IResourceLabel { name: string; diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 94e3f71e332..b740e159fce 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -10,7 +10,7 @@ import { toResource, GroupIdentifier, IEditorInput, Verbosity, EditorCommandsCon import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { ResourceLabels, IResourceLabelHandle } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -57,7 +57,7 @@ export class TabsTitleControl extends TitleControl { private tabsScrollbar: ScrollableElement; private closeOneEditorAction: CloseOneEditorAction; - private tabLabelWidgets: ResourceLabel[] = []; + private tabResourceLabels: ResourceLabels; private tabLabels: IEditorInputLabel[] = []; private tabDisposeables: IDisposable[] = []; @@ -123,6 +123,9 @@ export class TabsTitleControl extends TitleControl { addClass(breadcrumbsContainer, 'tabs-breadcrumbs'); this.titleContainer.appendChild(breadcrumbsContainer); this.createBreadcrumbsControl(breadcrumbsContainer, { showFileIcons: true, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: breadcrumbsBackground }); + + // Tab Labels + this.tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels)); } private createTabsScrollbar(scrollable: HTMLElement): ScrollableElement { @@ -295,7 +298,6 @@ export class TabsTitleControl extends TitleControl { (this.tabsContainer.lastChild as HTMLElement).remove(); // Remove associated tab label and widget - this.tabLabelWidgets.pop(); this.tabDisposeables.pop().dispose(); } @@ -311,7 +313,7 @@ export class TabsTitleControl extends TitleControl { clearNode(this.tabsContainer); this.tabDisposeables = dispose(this.tabDisposeables); - this.tabLabelWidgets = []; + this.tabResourceLabels.clear(); this.tabLabels = []; this.clearEditorActionsToolbar(); @@ -395,12 +397,12 @@ export class TabsTitleControl extends TitleControl { this.redraw(); } - private withTab(editor: IEditorInput, fn: (tabContainer: HTMLElement, tabLabelWidget: ResourceLabel, tabLabel: IEditorInputLabel) => void): void { + private withTab(editor: IEditorInput, fn: (tabContainer: HTMLElement, tabLabelWidget: IResourceLabelHandle, tabLabel: IEditorInputLabel) => void): void { const editorIndex = this.group.getIndexOfEditor(editor); const tabContainer = this.tabsContainer.children[editorIndex] as HTMLElement; if (tabContainer) { - fn(tabContainer, this.tabLabelWidgets[editorIndex], this.tabLabels[editorIndex]); + fn(tabContainer, this.tabResourceLabels.get(editorIndex), this.tabLabels[editorIndex]); } } @@ -422,8 +424,7 @@ export class TabsTitleControl extends TitleControl { tabContainer.appendChild(tabBorderTopContainer); // Tab Editor Label - const editorLabel = this.instantiationService.createInstance(ResourceLabel, tabContainer, void 0); - this.tabLabelWidgets.push(editorLabel); + const editorLabel = this.tabResourceLabels.create(tabContainer); // Tab Close Button const tabCloseContainer = document.createElement('div'); @@ -806,16 +807,16 @@ export class TabsTitleControl extends TitleControl { this.layout(this.dimension); } - private forEachTab(fn: (editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel, tabLabel: IEditorInputLabel) => void): void { + private forEachTab(fn: (editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabelHandle, tabLabel: IEditorInputLabel) => void): void { this.group.editors.forEach((editor, index) => { const tabContainer = this.tabsContainer.children[index] as HTMLElement; if (tabContainer) { - fn(editor, index, tabContainer, this.tabLabelWidgets[index], this.tabLabels[index]); + fn(editor, index, tabContainer, this.tabResourceLabels.get(index), this.tabLabels[index]); } }); } - private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel, tabLabel: IEditorInputLabel): void { + private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabelHandle, tabLabel: IEditorInputLabel): void { // Label this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel); @@ -848,7 +849,7 @@ export class TabsTitleControl extends TitleControl { this.redrawEditorActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget); } - private redrawLabel(editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel, tabLabel: IEditorInputLabel): void { + private redrawLabel(editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabelHandle, tabLabel: IEditorInputLabel): void { const name = tabLabel.name; const description = tabLabel.description || ''; const title = tabLabel.title || ''; @@ -861,7 +862,7 @@ export class TabsTitleControl extends TitleControl { tabLabelWidget.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) }); } - private redrawEditorActiveAndDirty(isGroupActive: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel): void { + private redrawEditorActiveAndDirty(isGroupActive: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabelHandle): void { const isTabActive = this.group.isActive(editor); const hasModifiedBorderTop = this.doRedrawEditorDirty(isGroupActive, isTabActive, editor, tabContainer); @@ -869,7 +870,7 @@ export class TabsTitleControl extends TitleControl { this.doRedrawEditorActive(isGroupActive, !hasModifiedBorderTop, editor, tabContainer, tabLabelWidget); } - private doRedrawEditorActive(isGroupActive: boolean, allowBorderTop: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: ResourceLabel): void { + private doRedrawEditorActive(isGroupActive: boolean, allowBorderTop: boolean, editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabelHandle): void { // Tab is active if (this.group.isActive(editor)) { -- GitLab