Skip to content

Commit ce62108

Browse files
authored
Task quick pick improvement (microsoft#93479)
Part of microsoft#90087
1 parent 106c205 commit ce62108

8 files changed

Lines changed: 419 additions & 215 deletions

File tree

src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts

Lines changed: 143 additions & 140 deletions
Large diffs are not rendered by default.

src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as nls from 'vs/nls';
7+
import * as Objects from 'vs/base/common/objects';
8+
import { Task, ContributedTask, CustomTask, ConfiguringTask, TaskSorter } from 'vs/workbench/contrib/tasks/common/tasks';
9+
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
10+
import * as Types from 'vs/base/common/types';
11+
import { ITaskService, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService';
12+
import { IQuickPickItem, QuickPickInput, IQuickPick } from 'vs/base/parts/quickinput/common/quickInput';
13+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
14+
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
15+
import { Disposable } from 'vs/base/common/lifecycle';
16+
import { Event } from 'vs/base/common/event';
17+
18+
export const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail';
19+
export const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip';
20+
21+
export function isWorkspaceFolder(folder: IWorkspace | IWorkspaceFolder): folder is IWorkspaceFolder {
22+
return 'uri' in folder;
23+
}
24+
25+
export interface TaskQuickPickEntry extends IQuickPickItem {
26+
task: Task | undefined | null;
27+
}
28+
export interface TaskTwoLevelQuickPickEntry extends IQuickPickItem {
29+
task: Task | ConfiguringTask | string | undefined | null;
30+
}
31+
32+
const SHOW_ALL: string = nls.localize('taskQuickPick.showAll', "Show All Tasks...");
33+
34+
export class TaskQuickPick extends Disposable {
35+
private sorter: TaskSorter;
36+
private constructor(
37+
private taskService: ITaskService,
38+
private configurationService: IConfigurationService,
39+
private quickInputService: IQuickInputService) {
40+
super();
41+
this.sorter = this.taskService.createSorter();
42+
}
43+
44+
private showDetail(): boolean {
45+
return this.configurationService.getValue<boolean>(QUICKOPEN_DETAIL_CONFIG);
46+
}
47+
48+
private guessTaskLabel(task: Task | ConfiguringTask): string {
49+
if (task._label) {
50+
return task._label;
51+
}
52+
if (ConfiguringTask.is(task)) {
53+
let label: string = task.configures.type;
54+
const configures = Objects.deepClone(task.configures);
55+
delete configures['_key'];
56+
delete configures['type'];
57+
Object.keys(configures).forEach(key => label += `: ${configures[key]}`);
58+
return label;
59+
}
60+
return '';
61+
}
62+
63+
private createTaskEntry(task: Task | ConfiguringTask): TaskTwoLevelQuickPickEntry {
64+
let entryLabel = this.guessTaskLabel(task);
65+
let commonKey = task._id.split('|');
66+
if (commonKey.length > 1) {
67+
entryLabel = entryLabel + ' (' + commonKey[1] + ')';
68+
}
69+
const entry: TaskTwoLevelQuickPickEntry = { label: entryLabel, description: this.taskService.getTaskDescription(task), task, detail: this.showDetail() ? task.configurationProperties.detail : undefined };
70+
entry.buttons = [{ iconClass: 'codicon-gear', tooltip: nls.localize('configureTask', "Configure Task") }];
71+
return entry;
72+
}
73+
74+
private createEntriesForGroup(entries: QuickPickInput<TaskTwoLevelQuickPickEntry>[], tasks: (Task | ConfiguringTask)[], groupLabel: string) {
75+
entries.push({ type: 'separator', label: groupLabel });
76+
tasks.forEach(task => {
77+
entries.push(this.createTaskEntry(task));
78+
});
79+
}
80+
81+
private createTypeEntries(entries: QuickPickInput<TaskTwoLevelQuickPickEntry>[], types: string[]) {
82+
entries.push({ type: 'separator', label: nls.localize('contributedTasks', "contributed") });
83+
types.forEach(type => {
84+
entries.push({ label: `$(folder) ${type}`, task: type });
85+
});
86+
entries.push({ label: SHOW_ALL, task: SHOW_ALL });
87+
}
88+
89+
private handleFolderTaskResult(result: Map<string, WorkspaceFolderTaskResult>): (Task | ConfiguringTask)[] {
90+
let tasks: (Task | ConfiguringTask)[] = [];
91+
Array.from(result).forEach(([key, folderTasks]) => {
92+
if (folderTasks.set) {
93+
tasks.push(...folderTasks.set.tasks);
94+
}
95+
if (folderTasks.configurations) {
96+
for (const configuration in folderTasks.configurations.byIdentifier) {
97+
tasks.push(folderTasks.configurations.byIdentifier[configuration]);
98+
}
99+
}
100+
});
101+
return tasks;
102+
}
103+
104+
private dedupeConfiguredAndRecent(recentTasks: (Task | ConfiguringTask)[], configuredTasks: (Task | ConfiguringTask)[]): (Task | ConfiguringTask)[] {
105+
let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = [];
106+
for (let j = 0; j < configuredTasks.length; j++) {
107+
const workspaceFolder = configuredTasks[j].getWorkspaceFolder()?.uri.toString();
108+
const definition = configuredTasks[j].getDefinition()?._key;
109+
const recentKey = configuredTasks[j].getRecentlyUsedKey();
110+
if (!recentTasks.find((value) => {
111+
return (workspaceFolder && definition && value.getWorkspaceFolder()?.uri.toString() === workspaceFolder && value.getDefinition()?._key === definition)
112+
|| (recentKey && value.getRecentlyUsedKey() === recentKey);
113+
})) {
114+
dedupedConfiguredTasks.push(configuredTasks[j]);
115+
}
116+
}
117+
dedupedConfiguredTasks = dedupedConfiguredTasks.sort((a, b) => this.sorter.compare(a, b));
118+
return dedupedConfiguredTasks;
119+
}
120+
121+
private async createTopLevelEntries(defaultEntry?: TaskQuickPickEntry): Promise<{ entries: QuickPickInput<TaskTwoLevelQuickPickEntry>[], isSingleConfigured?: Task | ConfiguringTask }> {
122+
const recentTasks: (Task | ConfiguringTask)[] = (await this.taskService.readRecentTasks()).reverse();
123+
const configuredTasks: (Task | ConfiguringTask)[] = this.handleFolderTaskResult(await this.taskService.getWorkspaceTasks());
124+
const extensionTaskTypes = this.taskService.taskTypes();
125+
const taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[] = [];
126+
if (recentTasks.length > 0) {
127+
this.createEntriesForGroup(taskQuickPickEntries, recentTasks, nls.localize('recentlyUsed', 'recently used'));
128+
}
129+
if (configuredTasks.length > 0) {
130+
let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = this.dedupeConfiguredAndRecent(recentTasks, configuredTasks);
131+
if (dedupedConfiguredTasks.length > 0) {
132+
this.createEntriesForGroup(taskQuickPickEntries, dedupedConfiguredTasks, nls.localize('configured', 'configured'));
133+
}
134+
}
135+
136+
if (defaultEntry && (configuredTasks.length === 0)) {
137+
taskQuickPickEntries.push({ type: 'separator', label: nls.localize('configured', 'configured') });
138+
taskQuickPickEntries.push(defaultEntry);
139+
}
140+
141+
if (extensionTaskTypes.length > 0) {
142+
this.createTypeEntries(taskQuickPickEntries, extensionTaskTypes);
143+
}
144+
return { entries: taskQuickPickEntries, isSingleConfigured: configuredTasks.length === 1 ? configuredTasks[0] : undefined };
145+
}
146+
147+
private async show(placeHolder: string, defaultEntry?: TaskQuickPickEntry): Promise<Task | undefined | null> {
148+
const picker: IQuickPick<TaskTwoLevelQuickPickEntry> = this.quickInputService.createQuickPick();
149+
picker.placeholder = placeHolder;
150+
picker.matchOnDescription = true;
151+
picker.ignoreFocusOut = false;
152+
picker.show();
153+
154+
picker.onDidTriggerItemButton(context => {
155+
let task = context.item.task;
156+
this.quickInputService.cancel();
157+
if (ContributedTask.is(task)) {
158+
this.taskService.customize(task, undefined, true);
159+
} else if (CustomTask.is(task)) {
160+
this.taskService.openConfig(task);
161+
}
162+
});
163+
164+
// First show recent tasks configured tasks. Other tasks will be available at a second level
165+
const topLevelEntriesResult = await this.createTopLevelEntries(defaultEntry);
166+
if (topLevelEntriesResult.isSingleConfigured && this.configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) {
167+
picker.dispose();
168+
return this.toTask(topLevelEntriesResult.isSingleConfigured);
169+
}
170+
const taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[] = topLevelEntriesResult.entries;
171+
172+
do {
173+
const firstLevelTask = await this.doPickerFirstLevel(picker, taskQuickPickEntries);
174+
if (Types.isString(firstLevelTask)) {
175+
// Proceed to second level of quick pick
176+
const selectedEntry = await this.doPickerSecondLevel(picker, firstLevelTask);
177+
if (selectedEntry && selectedEntry.task === null) {
178+
// The user has chosen to go back to the first level
179+
continue;
180+
} else {
181+
picker.dispose();
182+
return (selectedEntry?.task && !Types.isString(selectedEntry?.task)) ? this.toTask(selectedEntry?.task) : undefined;
183+
}
184+
} else if (firstLevelTask) {
185+
picker.dispose();
186+
return this.toTask(firstLevelTask);
187+
} else {
188+
return;
189+
}
190+
} while (1);
191+
return;
192+
}
193+
194+
private async doPickerFirstLevel(picker: IQuickPick<TaskTwoLevelQuickPickEntry>, taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[]): Promise<Task | ConfiguringTask | string | null | undefined> {
195+
picker.items = taskQuickPickEntries;
196+
const firstLevelPickerResult = await new Promise<TaskTwoLevelQuickPickEntry | undefined | null>(resolve => {
197+
Event.once(picker.onDidAccept)(async () => {
198+
resolve(picker.selectedItems ? picker.selectedItems[0] : undefined);
199+
});
200+
});
201+
return firstLevelPickerResult?.task;
202+
}
203+
204+
private async doPickerSecondLevel(picker: IQuickPick<TaskTwoLevelQuickPickEntry>, type: string) {
205+
picker.busy = true;
206+
picker.value = '';
207+
if (type === SHOW_ALL) {
208+
picker.items = (await this.taskService.tasks()).sort((a, b) => this.sorter.compare(a, b)).map(task => this.createTaskEntry(task));
209+
} else {
210+
picker.items = await this.getEntriesForProvider(type);
211+
}
212+
picker.busy = false;
213+
const secondLevelPickerResult = await new Promise<TaskTwoLevelQuickPickEntry | undefined | null>(resolve => {
214+
Event.once(picker.onDidAccept)(async () => {
215+
resolve(picker.selectedItems ? picker.selectedItems[0] : undefined);
216+
});
217+
});
218+
219+
return secondLevelPickerResult;
220+
}
221+
222+
private async getEntriesForProvider(type: string): Promise<QuickPickInput<TaskTwoLevelQuickPickEntry>[]> {
223+
const tasks = (await this.taskService.tasks({ type })).sort((a, b) => this.sorter.compare(a, b));
224+
let taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[];
225+
if (tasks.length > 0) {
226+
taskQuickPickEntries = tasks.map(task => this.createTaskEntry(task));
227+
taskQuickPickEntries.unshift({
228+
label: nls.localize('TaskQuickPick.goBack', 'Go back...'),
229+
task: null
230+
});
231+
} else {
232+
taskQuickPickEntries = [{
233+
label: nls.localize('TaskQuickPick.noTasksForType', 'No {0} tasks found. Go back...', type),
234+
task: null
235+
}];
236+
}
237+
return taskQuickPickEntries;
238+
}
239+
240+
private async toTask(task: Task | ConfiguringTask): Promise<Task | undefined> {
241+
if (!ConfiguringTask.is(task)) {
242+
return task;
243+
}
244+
245+
return this.taskService.tryResolveTask(task);
246+
}
247+
248+
static async show(taskService: ITaskService, configurationService: IConfigurationService, quickInputService: IQuickInputService, placeHolder: string, defaultEntry?: TaskQuickPickEntry) {
249+
const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService);
250+
return taskQuickPick.show(placeHolder, defaultEntry);
251+
}
252+
}

src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ export class TerminalTaskSystem implements ITaskSystem {
436436
let promises: Promise<ITaskSummary>[] = [];
437437
if (task.configurationProperties.dependsOn) {
438438
for (const dependency of task.configurationProperties.dependsOn) {
439-
let dependencyTask = resolver.resolve(dependency.uri, dependency.task!);
439+
let dependencyTask = await resolver.resolve(dependency.uri, dependency.task!);
440440
if (dependencyTask) {
441441
let key = dependencyTask.getMapKey();
442442
let promise = this.activeTasks[key] ? this.activeTasks[key].promise : undefined;

src/vs/workbench/contrib/tasks/common/taskConfiguration.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,18 +1242,20 @@ namespace GroupKind {
12421242
}
12431243

12441244
namespace TaskDependency {
1245+
function uriFromSource(context: ParseContext, source: TaskConfigSource): URI | string {
1246+
switch (source) {
1247+
case TaskConfigSource.User: return USER_TASKS_GROUP_KEY;
1248+
case TaskConfigSource.TasksJson: return context.workspaceFolder.uri;
1249+
default: return context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri;
1250+
}
1251+
}
1252+
12451253
export function from(this: void, external: string | TaskIdentifier, context: ParseContext, source: TaskConfigSource): Tasks.TaskDependency | undefined {
12461254
if (Types.isString(external)) {
1247-
let uri: URI | string;
1248-
if (source === TaskConfigSource.User) {
1249-
uri = USER_TASKS_GROUP_KEY;
1250-
} else {
1251-
uri = context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri;
1252-
}
1253-
return { uri, task: external };
1255+
return { uri: uriFromSource(context, source), task: external };
12541256
} else if (TaskIdentifier.is(external)) {
12551257
return {
1256-
uri: context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri,
1258+
uri: uriFromSource(context, source),
12571259
task: Tasks.TaskDefinition.createTaskIdentifier(external as Tasks.TaskIdentifier, context.problemReporter)
12581260
};
12591261
} else {

src/vs/workbench/contrib/tasks/common/taskService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,20 @@ export interface ITaskService {
6969
terminate(task: Task): Promise<TaskTerminateResponse>;
7070
terminateAll(): Promise<TaskTerminateResponse[]>;
7171
tasks(filter?: TaskFilter): Promise<Task[]>;
72+
taskTypes(): string[];
7273
getWorkspaceTasks(runSource?: TaskRunSource): Promise<Map<string, WorkspaceFolderTaskResult>>;
74+
readRecentTasks(): Promise<(Task | ConfiguringTask)[]>;
7375
/**
7476
* @param alias The task's name, label or defined identifier.
7577
*/
7678
getTask(workspaceFolder: IWorkspace | IWorkspaceFolder | string, alias: string | TaskIdentifier, compareId?: boolean): Promise<Task | undefined>;
79+
tryResolveTask(configuringTask: ConfiguringTask): Promise<Task | undefined>;
7780
getTasksForGroup(group: string): Promise<Task[]>;
7881
getRecentlyUsedTasks(): LinkedMap<string, string>;
7982
migrateRecentTasks(tasks: Task[]): void;
8083
createSorter(): TaskSorter;
8184

82-
getTaskDescription(task: Task): string | undefined;
85+
getTaskDescription(task: Task | ConfiguringTask): string | undefined;
8386
canCustomize(task: ContributedTask | CustomTask): boolean;
8487
customize(task: ContributedTask | CustomTask, properties?: {}, openConfig?: boolean): Promise<void>;
8588
openConfig(task: CustomTask | undefined): Promise<void>;

src/vs/workbench/contrib/tasks/common/taskSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export interface ITaskExecuteResult {
9494
}
9595

9696
export interface ITaskResolver {
97-
resolve(uri: URI | string, identifier: string | KeyedTaskIdentifier | undefined): Task | undefined;
97+
resolve(uri: URI | string, identifier: string | KeyedTaskIdentifier | undefined): Promise<Task | undefined>;
9898
}
9999

100100
export interface TaskTerminateResponse extends TerminateResponse {

0 commit comments

Comments
 (0)