diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index afb94d703957e298de53875f83f06eeb233c70f7..059eb29406d3eb997ccceb82cf1756297c7f6294 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -575,6 +575,10 @@ class QuickPick extends QuickInput implements IQuickPi return this.visible ? this.ui.inputBox.hasFocus() : false; } + public focusOnInput() { + this.ui.inputBox.setFocus(); + } + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 8cbf10553e75891a0372bdf1563ec94379e78244..5640fecc09bc49ec3644d0af3fdc6bf71b508590 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -209,6 +209,8 @@ export interface IQuickPick extends IQuickInput { validationMessage: string | undefined; inputHasFocus(): boolean; + + focusOnInput(): void; } export interface IInputBox extends IQuickInput { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index d5321cc69450ad5c5f1823cc2804b0554c461851..2e7290ca89492373450908ba12db65d36514fb05 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -34,7 +34,7 @@ import { IProgressService, IProgressOptions, ProgressLocation } from 'vs/platfor import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -63,7 +63,7 @@ import { getTemplates as getTaskTemplates } from 'vs/workbench/contrib/tasks/com import * as TaskConfig from '../common/taskConfiguration'; import { TerminalTaskSystem } from './terminalTaskSystem'; -import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickInput, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -80,6 +80,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { find } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IViewsService } from 'vs/workbench/common/views'; +import { ProviderProgressMananger } from 'vs/workbench/contrib/tasks/browser/providerProgressManager'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; @@ -220,6 +221,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _providers: Map; private _providerTypes: Map; protected _taskSystemInfos: Map; + private _providerProgressManager: ProviderProgressMananger | undefined; protected _workspaceTasksPromise?: Promise>; protected _areJsonTasksSupportedPromise: Promise = Promise.resolve(false); @@ -1347,36 +1349,26 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected abstract getTaskSystem(): ITaskSystem; - private async provideTasksWithWarning(provider: ITaskProvider, type: string, validTypes: IStringDictionary): Promise { + private async provideTasksWithWarning(provider: ITaskProvider, type: string, validTypes: IStringDictionary): Promise { return new Promise(async (resolve, reject) => { let isDone = false; - provider.provideTasks(validTypes).then((value) => { + let disposable: IDisposable | undefined; + const providePromise = provider.provideTasks(validTypes); + this._providerProgressManager?.addProvider(type, providePromise); + disposable = this._providerProgressManager?.canceled.token.onCancellationRequested(() => { + if (!isDone) { + resolve(); + } + }); + providePromise.then((value) => { isDone = true; + disposable?.dispose(); resolve(value); }, (e) => { isDone = true; + disposable?.dispose(); reject(e); }); - let settingValue: boolean | string[] = this.configurationService.getValue('task.slowProviderWarning'); - if ((settingValue === true) || (Types.isStringArray(settingValue) && (settingValue.indexOf(type) < 0))) { - setTimeout(() => { - if (!isDone) { - const settings: IPromptChoice = { label: nls.localize('TaskSystem.slowProvider.settings', "Settings"), run: () => this.preferencesService.openSettings(false, undefined) }; - const disableAll: IPromptChoice = { label: nls.localize('TaskSystem.slowProvider.disableAll', "Disable All"), run: () => this.configurationService.updateValue('task.autoDetect', 'off') }; - const dontShow: IPromptChoice = { - label: nls.localize('TaskSystem.slowProvider.dontShow', "Don't warn again for {0} tasks", type), run: () => { - if (!Types.isStringArray(settingValue)) { - settingValue = []; - } - settingValue.push(type); - return this.configurationService.updateValue('task.slowProviderWarning', settingValue); - } - }; - this.notificationService.prompt(Severity.Warning, nls.localize('TaskSystem.slowProvider', "The {0} task provider is slow. The extension that provides {0} tasks may provide a setting to disable it, or you can disable all tasks providers", type), - [settings, disableAll, dontShow]); - } - }, 4000); - } }); } @@ -1386,10 +1378,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); validTypes['shell'] = true; validTypes['process'] = true; + this._providerProgressManager = new ProviderProgressMananger(); return new Promise(resolve => { let result: TaskSet[] = []; let counter: number = 0; - let done = (value: TaskSet) => { + let done = (value: TaskSet | undefined) => { if (value) { result.push(value); } @@ -2085,30 +2078,69 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return entries; }); - return this.quickInputService.pick(pickEntries, { - placeHolder, - matchOnDescription: true, - onDidTriggerItemButton: context => { - let task = context.item.task; - this.quickInputService.cancel(); - if (ContributedTask.is(task)) { - this.customize(task, undefined, true); - } else if (CustomTask.is(task)) { - this.openConfig(task); - } - } - }, cancellationToken).then(async (selection) => { - if (cancellationToken.isCancellationRequested) { - // canceled when there's only one task - const task = (await pickEntries)[0]; - if ((task).task) { - selection = task; - } - } - if (!selection) { - return; + + const picker: IQuickPick = this.quickInputService.createQuickPick(); + picker.placeholder = placeHolder; + picker.matchOnDescription = true; + picker.ignoreFocusOut = true; + + picker.onDidTriggerItemButton(context => { + let task = context.item.task; + this.quickInputService.cancel(); + if (ContributedTask.is(task)) { + this.customize(task, undefined, true); + } else if (CustomTask.is(task)) { + this.openConfig(task); + } + }); + picker.busy = true; + const progressManager = this._providerProgressManager; + const progressTimeout = setTimeout(() => { + if (progressManager) { + progressManager.showProgress = (stillProviding) => { + let message = undefined; + if (stillProviding.length > 0) { + message = nls.localize('pickProgressManager.description', 'Getting tasks from extensions. {0} extension(s) remaining: {1}', stillProviding.length, stillProviding.join(', ')); + } + picker.description = message; + }; + progressManager.addOnDoneListener(() => { + picker.focusOnInput(); + picker.customButton = false; + }); + if (!progressManager.isDone) { + picker.customLabel = nls.localize('taskQuickPick.cancel', "Cancel Remaining Extensions"); + picker.onDidCustom(() => { + this._providerProgressManager?.cancel(); + }); + picker.customButton = true; + } } - return selection; + }, 1000); + pickEntries.then(entries => { + clearTimeout(progressTimeout); + progressManager?.dispose(); + picker.busy = false; + picker.items = entries; + }); + picker.show(); + + return new Promise(resolve => { + this._register(picker.onDidAccept(async () => { + let selection = picker.selectedItems ? picker.selectedItems[0] : undefined; + if (cancellationToken.isCancellationRequested) { + // canceled when there's only one task + const task = (await pickEntries)[0]; + if ((task).task) { + selection = task; + } + } + picker.dispose(); + if (!selection) { + resolve(); + } + resolve(selection); + })); }); } diff --git a/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts b/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..18d4dae5be479ce75f147582f650f7c1b64cacc3 --- /dev/null +++ b/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TaskSet } from 'vs/workbench/contrib/tasks/common/tasks'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +export class ProviderProgressMananger extends Disposable { + private _onProviderComplete: Emitter = new Emitter(); + private _stillProviding: Set = new Set(); + private _onDone: Emitter = new Emitter(); + private _isDone: boolean = false; + private _showProgress: ((remaining: string[]) => void) | undefined; + public canceled: CancellationTokenSource = new CancellationTokenSource(); + + constructor() { + super(); + this._register(this._onProviderComplete.event(taskType => { + this._stillProviding.delete(taskType); + if (this._stillProviding.size === 0) { + this._isDone = true; + this._onDone.fire(); + } + if (this._showProgress) { + this._showProgress(Array.from(this._stillProviding)); + } + })); + } + + public addProvider(taskType: string, provider: Promise) { + this._stillProviding.add(taskType); + provider.then(() => this._onProviderComplete.fire(taskType)); + } + + public addOnDoneListener(onDoneListener: () => void) { + this._register(this._onDone.event(onDoneListener)); + } + + set showProgress(progressDisplayFunction: (remaining: string[]) => void) { + this._showProgress = progressDisplayFunction; + this._showProgress(Array.from(this._stillProviding)); + } + + get isDone(): boolean { + return this._isDone; + } + + public cancel() { + this._isDone = true; + if (this._showProgress) { + this._showProgress([]); + } + this._onDone.fire(); + this.canceled.cancel(); + } +}