diff --git a/src/vs/platform/statusbar/common/statusbar.ts b/src/vs/platform/statusbar/common/statusbar.ts index 92a2c0c99f5..c311a9180ff 100644 --- a/src/vs/platform/statusbar/common/statusbar.ts +++ b/src/vs/platform/statusbar/common/statusbar.ts @@ -60,6 +60,11 @@ export interface IStatusbarEntry { * Wether to show a beak above the status bar entry. */ readonly showBeak?: boolean; + + /** + * An identifier to associate with the status bar entry DOM element. + */ + readonly elementId?: string; } export interface IStatusbarService { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 1070575c2be..3081e6e8c67 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -324,6 +324,13 @@ class StatusBarEntryItem extends Disposable { private render(entry: IStatusbarEntry): void { + // Container + if (entry.elementId) { + this.container.id = entry.elementId; + } else if (this.container.id) { + delete this.container.id; + } + // Text Container let textContainer: HTMLElement; if (entry.command) { diff --git a/src/vs/workbench/contrib/tasks/common/media/status-error.svg b/src/vs/workbench/contrib/tasks/common/media/status-error.svg deleted file mode 100644 index 61b16362cb1..00000000000 --- a/src/vs/workbench/contrib/tasks/common/media/status-error.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/tasks/common/media/status-info.svg b/src/vs/workbench/contrib/tasks/common/media/status-info.svg deleted file mode 100644 index 16306356ba0..00000000000 --- a/src/vs/workbench/contrib/tasks/common/media/status-info.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/tasks/common/media/status-warning.svg b/src/vs/workbench/contrib/tasks/common/media/status-warning.svg deleted file mode 100644 index 5951d69539a..00000000000 --- a/src/vs/workbench/contrib/tasks/common/media/status-warning.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/tasks/common/media/task.contribution.css b/src/vs/workbench/contrib/tasks/common/media/task.contribution.css index 9c316230674..e9d227427eb 100644 --- a/src/vs/workbench/contrib/tasks/common/media/task.contribution.css +++ b/src/vs/workbench/contrib/tasks/common/media/task.contribution.css @@ -3,64 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.task-statusbar-item { - display: inline-block; -} - -.task-statusbar-item-icon { - background: url('task.svg') 50% 2px no-repeat; - background-size: 18px; - cursor: pointer; - height: 22px; - width: 24px; - vertical-align: top; -} - -.task-statusbar-item-building { - height: 18px; - padding: 0px 2px 0px 2px; - display: inline-block; - text-align: center; - vertical-align: top; -} - -.task-statusbar-item-label { - display: inline-block; - cursor: pointer; - padding: 0 5px 0 5px; -} - -.task-statusbar-item-label > .task-statusbar-item-label-counter { - display: inline-block; - vertical-align: top; - padding-left: 2px; - padding-right: 2px; -} - -.task-statusbar-item-label > .task-statusbar-item-label-error, -.task-statusbar-item-label > .task-statusbar-item-label-warning, -.task-statusbar-item-label > .task-statusbar-item-label-info { - display: inline-block; - padding-right: 8px; - width: 8px; - height: 22px; -} - -.task-statusbar-item-label > .task-statusbar-item-label-error { - -webkit-mask: url('status-error.svg') no-repeat 50% 50%; - -webkit-mask-size: 11px; -} - -.task-statusbar-item-label > .task-statusbar-item-label-warning { - -webkit-mask: url('status-warning.svg') no-repeat 50% 50%; - -webkit-mask-size: 11px; -} - -.task-statusbar-item-label > .task-statusbar-item-label-info { - -webkit-mask: url('status-info.svg') no-repeat 50% 50%; - -webkit-mask-size: 11px; -} - .monaco-workbench .quick-open-task-configure { background-image: url('configure.svg'); } diff --git a/src/vs/workbench/contrib/tasks/common/media/task.svg b/src/vs/workbench/contrib/tasks/common/media/task.svg deleted file mode 100644 index 7692f495b32..00000000000 --- a/src/vs/workbench/contrib/tasks/common/media/task.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts index 0349151092b..56e9863d8d6 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts @@ -14,8 +14,7 @@ import * as Objects from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { Action } from 'vs/base/common/actions'; -import * as Dom from 'vs/base/browser/dom'; -import { IDisposable, dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as Types from 'vs/base/common/types'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -51,7 +50,6 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { IStatusbarItem, IStatusbarRegistry, Extensions as StatusbarExtensions, StatusbarItemDescriptor } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; @@ -84,8 +82,6 @@ import { TerminalTaskSystem } from './terminalTaskSystem'; import { ProcessRunnerDetector } from 'vs/workbench/contrib/tasks/node/processRunnerDetector'; import { QuickOpenActionContributor } from '../browser/quickOpen'; -import { Themable, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; @@ -105,212 +101,143 @@ const actionRegistry = Registry.as(ActionExtensions.Wo actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(AllowAutomaticTaskRunning, AllowAutomaticTaskRunning.ID, AllowAutomaticTaskRunning.LABEL), 'Tasks: Allow Automatic Tasks in Folder', tasksCategory); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DisallowAutomaticTaskRunning, DisallowAutomaticTaskRunning.ID, DisallowAutomaticTaskRunning.LABEL), 'Tasks: Disallow Automatic Tasks in Folder', tasksCategory); - namespace ConfigureTaskAction { export const ID = 'workbench.action.tasks.configureTaskRunner'; export const TEXT = nls.localize('ConfigureTaskRunnerAction.label', "Configure Task"); } -class BuildStatusBarItem extends Themable implements IStatusbarItem { - private activeCount: number; - private icons: HTMLElement[]; - - constructor( - @IPanelService private readonly panelService: IPanelService, - @IMarkerService private readonly markerService: IMarkerService, - @ITaskService private readonly taskService: ITaskService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IThemeService themeService: IThemeService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService - ) { - super(themeService); - - this.activeCount = 0; - this.icons = []; - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); - } - - protected updateStyles(): void { - super.updateStyles(); - - this.icons.forEach(icon => { - icon.style.backgroundColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND); - }); - } - - public render(container: HTMLElement): IDisposable { - let callOnDispose: IDisposable[] = []; - - const element = document.createElement('div'); - const label = document.createElement('a'); - const errorIcon = document.createElement('div'); - const warningIcon = document.createElement('div'); - const infoIcon = document.createElement('div'); - const error = document.createElement('div'); - const warning = document.createElement('div'); - const info = document.createElement('div'); - const building = document.createElement('div'); - - const errorTitle = (n: number) => nls.localize('totalErrors', "{0} Errors", n); - const warningTitle = (n: number) => nls.localize('totalWarnings', "{0} Warnings", n); - const infoTitle = (n: number) => nls.localize('totalInfos', "{0} Infos", n); - - Dom.addClass(element, 'task-statusbar-item'); - element.title = nls.localize('problems', "Problems"); - - Dom.addClass(label, 'task-statusbar-item-label'); - element.appendChild(label); - - Dom.addClass(errorIcon, 'task-statusbar-item-label-error'); - Dom.addClass(errorIcon, 'mask-icon'); - label.appendChild(errorIcon); - this.icons.push(errorIcon); - - Dom.addClass(error, 'task-statusbar-item-label-counter'); - error.innerHTML = '0'; - error.title = errorIcon.title = errorTitle(0); - label.appendChild(error); - - Dom.addClass(warningIcon, 'task-statusbar-item-label-warning'); - Dom.addClass(warningIcon, 'mask-icon'); - label.appendChild(warningIcon); - this.icons.push(warningIcon); - - Dom.addClass(warning, 'task-statusbar-item-label-counter'); - warning.innerHTML = '0'; - warning.title = warningIcon.title = warningTitle(0); - label.appendChild(warning); - - Dom.addClass(infoIcon, 'task-statusbar-item-label-info'); - Dom.addClass(infoIcon, 'mask-icon'); - label.appendChild(infoIcon); - this.icons.push(infoIcon); - Dom.hide(infoIcon); - - Dom.addClass(info, 'task-statusbar-item-label-counter'); - label.appendChild(info); - Dom.hide(info); - - Dom.addClass(building, 'task-statusbar-item-building'); - element.appendChild(building); - building.innerHTML = nls.localize('building', 'Building...'); - Dom.hide(building); - - callOnDispose.push(Dom.addDisposableListener(label, 'click', (e: MouseEvent) => { - const panel = this.panelService.getActivePanel(); - if (panel && panel.getId() === Constants.MARKERS_PANEL_ID) { - this.layoutService.setPanelHidden(true); - } else { - this.panelService.openPanel(Constants.MARKERS_PANEL_ID, true); - } - })); - - const manyProblems = nls.localize('manyProblems', "10K+"); - const packNumber = (n: number) => n > 9999 ? manyProblems : n > 999 ? n.toString().charAt(0) + 'K' : n.toString(); - let updateLabel = (stats: MarkerStatistics) => { - error.innerHTML = packNumber(stats.errors); - error.title = errorIcon.title = errorTitle(stats.errors); - warning.innerHTML = packNumber(stats.warnings); - warning.title = warningIcon.title = warningTitle(stats.warnings); - if (stats.infos > 0) { - info.innerHTML = packNumber(stats.infos); - info.title = infoIcon.title = infoTitle(stats.infos); - Dom.show(info); - Dom.show(infoIcon); - } else { - Dom.hide(info); - Dom.hide(infoIcon); - } - }; - - this.markerService.onMarkerChanged((changedResources) => { - updateLabel(this.markerService.getStatistics()); - }); - - callOnDispose.push(this.taskService.onDidStateChange((event) => { - if (this.ignoreEvent(event)) { - return; - } - switch (event.kind) { - case TaskEventKind.Active: - this.activeCount++; - if (this.activeCount === 1) { - Dom.show(building); - } - break; - case TaskEventKind.Inactive: - // Since the exiting of the sub process is communicated async we can't order inactive and terminate events. - // So try to treat them accordingly. - if (this.activeCount > 0) { - this.activeCount--; - if (this.activeCount === 0) { - Dom.hide(building); - } - } - break; - case TaskEventKind.Terminated: - if (this.activeCount !== 0) { - Dom.hide(building); - this.activeCount = 0; - } - break; - } - })); - - container.appendChild(element); - - this.updateStyles(); - - return toDisposable(() => { - callOnDispose = dispose(callOnDispose); - }); - } - - private ignoreEvent(event: TaskEvent): boolean { - if (!this.taskService.inTerminal()) { - return false; - } - if (event.group !== TaskGroup.Build) { - return true; - } - if (!event.__task) { - return false; - } - return event.__task.configurationProperties.problemMatchers === undefined || event.__task.configurationProperties.problemMatchers.length === 0; - } -} - export class TaskStatusBarContributions extends Disposable implements IWorkbenchContribution { - private item: IStatusbarEntryAccessor | undefined; + private runningTasksStatusItem: IStatusbarEntryAccessor | undefined; + private problemsStatusItem: IStatusbarEntryAccessor; + + private activeTasksCount: number = 0; constructor( @ITaskService private readonly taskService: ITaskService, + @IMarkerService private readonly markerService: IMarkerService, @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); + this.problemsStatusItem = this._register(this.statusbarService.addEntry(this.getProblemsItem(), StatusbarAlignment.LEFT, 50 /* Medium Priority */)); + this.registerListeners(); } + private getProblemsItem(): IStatusbarEntry { + const problems = this.markerService.getStatistics(); + + return { + elementId: 'task-statusbar-item', + text: this.getProblemsText(problems), + tooltip: this.getProblemsTooltip(problems), + command: 'workbench.action.tasks.toggleProblems' + }; + } + + private getProblemsTooltip(stats: MarkerStatistics): string { + const errorTitle = (n: number) => nls.localize('totalErrors', "{0} Errors", n); + const warningTitle = (n: number) => nls.localize('totalWarnings', "{0} Warnings", n); + const infoTitle = (n: number) => nls.localize('totalInfos', "{0} Infos", n); + + const titles: string[] = []; + + if (stats.errors > 0) { + titles.push(errorTitle(stats.errors)); + } + + if (stats.warnings > 0) { + titles.push(warningTitle(stats.warnings)); + } + + if (stats.infos > 0) { + titles.push(infoTitle(stats.infos)); + } + + if (titles.length === 0) { + return nls.localize('noProblems', "No Problems"); + } + + return titles.join(', '); + } + + private getProblemsText(stats: MarkerStatistics): string { + const problemsText: string[] = []; + + // Errors + problemsText.push('$(error) ' + this.packNumber(stats.errors)); + + // Warnings + problemsText.push('$(warning) ' + this.packNumber(stats.warnings)); + + // Info (only if any) + if (stats.infos > 0) { + problemsText.push('$(info) ' + this.packNumber(stats.infos)); + } + + // Building (only if any running tasks) + if (this.activeTasksCount > 0) { + problemsText.push(nls.localize('building', 'Building...')); + } + + return problemsText.join(' '); + } + + private packNumber(n: number): string { + const manyProblems = nls.localize('manyProblems', "10K+"); + + return n > 9999 ? manyProblems : n > 999 ? n.toString().charAt(0) + 'K' : n.toString(); + } + private registerListeners(): void { + this.markerService.onMarkerChanged(() => this.updateProblemsStatus()); + this.taskService.onDidStateChange(event => { if (event.kind === TaskEventKind.Changed) { - this.update(); + this.updateRunningTasksStatus(); + } + + if (!this.ignoreEventForUpdateRunningTasksCount(event)) { + let needsUpdate = false; + + switch (event.kind) { + case TaskEventKind.Active: + this.activeTasksCount++; + if (this.activeTasksCount === 1) { + needsUpdate = true; + } + break; + case TaskEventKind.Inactive: + // Since the exiting of the sub process is communicated async we can't order inactive and terminate events. + // So try to treat them accordingly. + if (this.activeTasksCount > 0) { + this.activeTasksCount--; + if (this.activeTasksCount === 0) { + needsUpdate = true; + } + } + break; + case TaskEventKind.Terminated: + if (this.activeTasksCount !== 0) { + this.activeTasksCount = 0; + needsUpdate = true; + } + break; + } + + if (needsUpdate) { + this.updateProblemsStatus(); + } } }); } - private async update(): Promise { + private async updateRunningTasksStatus(): Promise { const tasks = await this.taskService.getActiveTasks(); if (tasks.length === 0) { - if (this.item) { - this.item.dispose(); - this.item = undefined; + if (this.runningTasksStatusItem) { + this.runningTasksStatusItem.dispose(); + this.runningTasksStatusItem = undefined; } } else { const itemProps: IStatusbarEntry = { @@ -319,13 +246,33 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench command: 'workbench.action.tasks.showTasks', }; - if (!this.item) { - this.item = this.statusbarService.addEntry(itemProps, StatusbarAlignment.LEFT, 50 /* Medium Priority */); + if (!this.runningTasksStatusItem) { + this.runningTasksStatusItem = this.statusbarService.addEntry(itemProps, StatusbarAlignment.LEFT, 50 /* Medium Priority */); } else { - this.item.update(itemProps); + this.runningTasksStatusItem.update(itemProps); } } } + + private updateProblemsStatus(): void { + this.problemsStatusItem.update(this.getProblemsItem()); + } + + private ignoreEventForUpdateRunningTasksCount(event: TaskEvent): boolean { + if (!this.taskService.inTerminal()) { + return false; + } + + if (event.group !== TaskGroup.Build) { + return true; + } + + if (!event.__task) { + return false; + } + + return event.__task.configurationProperties.problemMatchers === undefined || event.__task.configurationProperties.problemMatchers.length === 0; + } } workbenchRegistry.registerWorkbenchContribution(TaskStatusBarContributions, LifecyclePhase.Restored); @@ -470,7 +417,8 @@ class TaskService extends Disposable implements ITaskService { @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(); @@ -605,6 +553,15 @@ class TaskService extends Disposable implements ITaskService { CommandsRegistry.registerCommand('workbench.action.tasks.showTasks', () => { this.runShowTasks(); }); + + CommandsRegistry.registerCommand('workbench.action.tasks.toggleProblems', () => { + const panel = this.panelService.getActivePanel(); + if (panel && panel.getId() === Constants.MARKERS_PANEL_ID) { + this.layoutService.setPanelHidden(true); + } else { + this.panelService.openPanel(Constants.MARKERS_PANEL_ID, true); + } + }); } private get workspaceFolders(): IWorkspaceFolder[] { @@ -1340,7 +1297,7 @@ class TaskService extends Disposable implements ITaskService { this._taskSystem = new TerminalTaskSystem( this.terminalService, this.outputService, this.panelService, this.markerService, this.modelService, this.configurationResolverService, this.telemetryService, - this.contextService, this._environmentService, + this.contextService, this.environmentService, TaskService.OutputChannelId, (workspaceFolder: IWorkspaceFolder) => { if (!workspaceFolder) { @@ -2687,10 +2644,6 @@ quickOpenRegistry.registerQuickOpenHandler( const actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); actionBarRegistry.registerActionBarContributor(Scope.VIEWER, QuickOpenActionContributor); -// Status bar -let statusbarRegistry = Registry.as(StatusbarExtensions.Statusbar); -statusbarRegistry.registerStatusbarItem(new StatusbarItemDescriptor(BuildStatusBarItem, StatusbarAlignment.LEFT, 50 /* Medium Priority */)); - // tasks.json validation let schemaId = 'vscode://schemas/tasks'; let schema: IJSONSchema = { diff --git a/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts b/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts index 036cedba11f..194cee71776 100644 --- a/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts +++ b/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts @@ -214,7 +214,7 @@ class WelcomeOverlay extends Disposable { } private updateProblemsKey() { - const problems = document.querySelector('.task-statusbar-item'); + const problems = document.getElementById('task-statusbar-item'); const key = this._overlay.querySelector('.key.problems') as HTMLElement; if (problems instanceof HTMLElement) { const target = problems.getBoundingClientRect(); diff --git a/test/smoke/src/areas/statusbar/statusbar.ts b/test/smoke/src/areas/statusbar/statusbar.ts index b36678ea7d2..25286b033c5 100644 --- a/test/smoke/src/areas/statusbar/statusbar.ts +++ b/test/smoke/src/areas/statusbar/statusbar.ts @@ -48,7 +48,7 @@ export class StatusBar { case StatusBarElement.SYNC_STATUS: return `${this.mainSelector} ${this.leftSelector} .octicon.octicon-sync`; case StatusBarElement.PROBLEMS_STATUS: - return `${this.mainSelector} ${this.leftSelector} .task-statusbar-item[title="Problems"]`; + return `${this.mainSelector} ${this.leftSelector} div[id="task-statusbar-item"]`; case StatusBarElement.SELECTION_STATUS: return `${this.mainSelector} ${this.rightSelector} .editor-status-selection`; case StatusBarElement.INDENTATION_STATUS: