diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index c7e14b8054e..b421bbe2913 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -79,7 +79,7 @@ import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views import { isWorkspaceFolder, ITaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQuickPick, QUICKOPEN_SKIP_CONFIG, configureTaskIcon } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; import { ILogService } from 'vs/platform/log/common/log'; import { once } from 'vs/base/common/functional'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys'; import { Schemas } from 'vs/base/common/network'; @@ -94,7 +94,7 @@ export namespace ConfigureTaskAction { export const TEXT = nls.localize('ConfigureTaskRunnerAction.label', "Configure Task"); } -type TaskQuickPickEntryType = (IQuickPickItem & { task: Task }) | (IQuickPickItem & { folder: IWorkspaceFolder }) | (IQuickPickItem & { settingType: string }); +export type TaskQuickPickEntryType = (IQuickPickItem & { task: Task }) | (IQuickPickItem & { folder: IWorkspaceFolder }) | (IQuickPickItem & { settingType: string }); class ProblemReporter implements TaskConfig.IProblemReporter { @@ -256,7 +256,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILogService private readonly _logService: ILogService + @ILogService private readonly _logService: ILogService, + @IThemeService private readonly _themeService: IThemeService ) { super(); @@ -2519,7 +2520,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private async _showTwoLevelQuickPick(placeHolder: string, defaultEntry?: ITaskQuickPickEntry) { - return TaskQuickPick.show(this, this._configurationService, this._quickInputService, this._notificationService, this._dialogService, placeHolder, defaultEntry); + return TaskQuickPick.show(this, this._configurationService, this._quickInputService, this._notificationService, this._dialogService, this._themeService, placeHolder, defaultEntry); } private async _showQuickPick(tasks: Promise | Task[], placeHolder: string, defaultEntry?: ITaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, additionalEntries?: ITaskQuickPickEntry[]): Promise { @@ -3143,7 +3144,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._isTaskEntry(selection)) { this._configureTask(selection.task); } else if (this._isSettingEntry(selection)) { - const taskQuickPick = new TaskQuickPick(this, this._configurationService, this._quickInputService, this._notificationService, this._dialogService); + const taskQuickPick = new TaskQuickPick(this, this._configurationService, this._quickInputService, this._notificationService, this._themeService, this._dialogService); taskQuickPick.handleSettingOption(selection.settingType); } else if (selection.folder && (this._contextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this._openTaskFile(selection.folder.toResource('.vscode/tasks.json'), TaskSourceKind.Workspace); @@ -3201,7 +3202,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { tasks = tasks.sort((a, b) => a._label.localeCompare(b._label)); for (const task of tasks) { - entries.push({ label: task._label, task, description: this.getTaskDescription(task), detail: this._showDetail() ? task.configurationProperties.detail : undefined }); + const entry = { label: TaskQuickPick.getTaskLabelWithIcon(task), task, description: this.getTaskDescription(task), detail: this._showDetail() ? task.configurationProperties.detail : undefined }; + TaskQuickPick.applyColorStyles(task, entry, this._themeService); + entries.push(entry); if (!ContributedTask.is(task)) { configuredCount++; } diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index 615a8a7e236..0d307a071df 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -9,16 +9,18 @@ import { Task, ContributedTask, CustomTask, ConfiguringTask, TaskSorter, KeyedTa import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import * as Types from 'vs/base/common/types'; import { ITaskService, IWorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; -import { IQuickPickItem, QuickPickInput, IQuickPick, IQuickInputButton } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, QuickPickInput, IQuickPick, IQuickInputButton, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Codicon } from 'vs/base/common/codicons'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { getColorClass, getColorStyleElement, getStandardColors } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; +import { TaskQuickPickEntryType } from 'vs/workbench/contrib/tasks/browser/abstractTaskService'; export const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; export const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip'; @@ -49,6 +51,7 @@ export class TaskQuickPick extends Disposable { private _configurationService: IConfigurationService, private _quickInputService: IQuickInputService, private _notificationService: INotificationService, + private _themeService: IThemeService, private _dialogService: IDialogService) { super(); this._sorter = this._taskService.createSorter(); @@ -61,7 +64,7 @@ export class TaskQuickPick extends Disposable { private _guessTaskLabel(task: Task | ConfiguringTask): string { if (task._label) { - return task._label; + return TaskQuickPick.getTaskLabelWithIcon(task); } if (ConfiguringTask.is(task)) { let label: string = task.configures.type; @@ -74,9 +77,29 @@ export class TaskQuickPick extends Disposable { return ''; } + public static getTaskLabelWithIcon(task: Task | ConfiguringTask): string { + return task.configurationProperties.icon ? `$(${task.configurationProperties.icon}) ${task._label}` : task.configurationProperties.color ? `$(${Codicon.tools.id}) ${task._label}` : `${task._label}`; + } + + public static applyColorStyles(task: Task | ConfiguringTask, entry: TaskQuickPickEntryType | ITaskTwoLevelQuickPickEntry, themeService: IThemeService): void { + if (task.configurationProperties.color) { + const colorTheme = themeService.getColorTheme(); + const styleElement = getColorStyleElement(colorTheme); + entry.iconClasses = [getColorClass(task.configurationProperties.color)]; + document.body.appendChild(styleElement); + } + } + private _createTaskEntry(task: Task | ConfiguringTask, extraButtons: IQuickInputButton[] = []): ITaskTwoLevelQuickPickEntry { + const customizeIconButton = { iconClass: ThemeIcon.asClassName(Codicon.pencil), tooltip: nls.localize('setIconAndColor', "Choose color and icon") }; const entry: ITaskTwoLevelQuickPickEntry = { label: this._guessTaskLabel(task), description: this._taskService.getTaskDescription(task), task, detail: this._showDetail() ? task.configurationProperties.detail : undefined }; - entry.buttons = [{ iconClass: ThemeIcon.asClassName(configureTaskIcon), tooltip: nls.localize('configureTask', "Configure Task") }, ...extraButtons]; + entry.buttons = []; + if (CustomTask.is(task)) { + entry.buttons.push(customizeIconButton); + } + entry.buttons.push({ iconClass: ThemeIcon.asClassName(configureTaskIcon), tooltip: nls.localize('configureTask', "Configure Task") }); + entry.buttons.push(...extraButtons); + TaskQuickPick.applyColorStyles(task, entry, this._themeService); return entry; } @@ -211,6 +234,9 @@ export class TaskQuickPick extends Disposable { if (indexToRemove >= 0) { picker.items = [...picker.items.slice(0, indexToRemove), ...picker.items.slice(indexToRemove + 1)]; } + } else if (context.button.iconClass = ThemeIcon.asClassName(Codicon.pencil)) { + await this._setColor(task); + await this._setIcon(task); } else { this._quickInputService.cancel(); if (ContributedTask.is(task)) { @@ -265,6 +291,74 @@ export class TaskQuickPick extends Disposable { return; } + private async _setColor(task: Task | ConfiguringTask | null | string | undefined): Promise { + if (task === undefined || task === null || typeof task === 'string') { + return; + } + const colorTheme = this._themeService.getColorTheme(); + const standardColors: string[] = getStandardColors(colorTheme); + const styleElement = getColorStyleElement(colorTheme); + const items: (IQuickPickItem | IQuickPickSeparator)[] = []; + for (const colorKey of standardColors) { + const colorClass = getColorClass(colorKey); + items.push({ + label: `$(${Codicon.circleFilled.id}) ${colorKey.replace('terminal.ansi', '')}`, id: colorKey, description: colorKey, iconClasses: [colorClass] + }); + } + items.push({ type: 'separator' }); + const showAllColorsItem = { label: 'Reset to default' }; + items.push(showAllColorsItem); + document.body.appendChild(styleElement); + + const quickPick = this._quickInputService.createQuickPick(); + quickPick.items = items; + quickPick.matchOnDescription = true; + quickPick.show(); + const disposables: IDisposable[] = []; + const result = await new Promise(r => { + disposables.push(quickPick.onDidHide(() => r(undefined))); + disposables.push(quickPick.onDidAccept(() => r(quickPick.selectedItems[0]))); + }); + dispose(disposables); + + if (result && task && typeof task !== 'string') { + task.configurationProperties.color = result.id; + } + document.body.removeChild(styleElement); + quickPick.hide(); + } + + private async _setIcon(task: Task | ConfiguringTask | null | string | undefined): Promise { + if (task === undefined || task === null || typeof task === 'string') { + return; + } + type Item = IQuickPickItem & { icon: ThemeIcon }; + const items: Item[] = []; + for (const icon of Codicon.getAll()) { + items.push({ label: `$(${icon.id})`, description: `${icon.id}`, id: icon.id, icon, iconClasses: task.configurationProperties.color ? [getColorClass(task.configurationProperties.color)] : undefined }); + } + + const quickPick = this._quickInputService.createQuickPick(); + quickPick.items = items; + quickPick.matchOnDescription = true; + quickPick.show(); + const disposables: IDisposable[] = []; + const result = await new Promise(r => { + disposables.push(quickPick.onDidHide(() => r(undefined))); + disposables.push(quickPick.onDidAccept(() => r(quickPick.selectedItems[0]))); + }); + dispose(disposables); + + if (result && task && typeof task !== 'string') { + task.configurationProperties.icon = result.id; + } + if (CustomTask.is(task) && result) { + await this._taskService.customize(task, { icon: result.id, color: task.configurationProperties.color }, false); + } + quickPick.hide(); + } + + private async _doPickerFirstLevel(picker: IQuickPick, taskQuickPickEntries: QuickPickInput[]): Promise { picker.items = taskQuickPickEntries; const firstLevelPickerResult = await new Promise(resolve => { @@ -367,8 +461,8 @@ export class TaskQuickPick extends Disposable { static async show(taskService: ITaskService, configurationService: IConfigurationService, quickInputService: IQuickInputService, notificationService: INotificationService, - dialogService: IDialogService, placeHolder: string, defaultEntry?: ITaskQuickPickEntry) { - const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService, notificationService, dialogService); + dialogService: IDialogService, themeService: IThemeService, placeHolder: string, defaultEntry?: ITaskQuickPickEntry) { + const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService, notificationService, themeService, dialogService); return taskQuickPick.show(placeHolder, defaultEntry); } } diff --git a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts index 5f9af57d660..7c3731d171d 100644 --- a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts +++ b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts @@ -17,6 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { isString } from 'vs/base/common/types'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TasksQuickAccessProvider extends PickerQuickAccessProvider { @@ -28,7 +29,8 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider = []; diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index dfe37cf6070..86acc2a6a03 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -36,6 +36,8 @@ export interface ICustomizationProperties { group?: string | { kind?: string; isDefault?: boolean }; problemMatcher?: string | string[]; isBackground?: boolean; + color?: string; + icon?: string; } export interface ITaskFilter { diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index ac7efc250fd..0620c9bc2d5 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -43,6 +43,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; interface IWorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; @@ -83,7 +84,8 @@ export class TaskService extends AbstractTaskService { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IWorkspaceTrustRequestService workspaceTrustRequestService: IWorkspaceTrustRequestService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILogService logService: ILogService) { + @ILogService logService: ILogService, + @IThemeService themeService: IThemeService) { super(configurationService, markerService, outputService, @@ -115,7 +117,8 @@ export class TaskService extends AbstractTaskService { viewDescriptorService, workspaceTrustRequestService, workspaceTrustManagementService, - logService); + logService, + themeService); this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); }