From 43abcc20b3b47e1153e116ee98636a6ec9c3a68d Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Fri, 9 Jun 2017 22:26:56 +0200 Subject: [PATCH] Fixes #28379: Run Task should be sorted using MRU list --- src/vs/base/common/linkedMap.ts | 306 ++++++++++++++++++ .../parts/tasks/browser/quickOpen.ts | 56 ++-- .../parts/tasks/common/taskService.ts | 2 + .../electron-browser/task.contribution.ts | 41 ++- 4 files changed, 376 insertions(+), 29 deletions(-) create mode 100644 src/vs/base/common/linkedMap.ts diff --git a/src/vs/base/common/linkedMap.ts b/src/vs/base/common/linkedMap.ts new file mode 100644 index 00000000000..65eaa0e6c85 --- /dev/null +++ b/src/vs/base/common/linkedMap.ts @@ -0,0 +1,306 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +interface Item { + previous: Item | undefined; + next: Item | undefined; + key: K; + value: V; +} + +export namespace Touch { + export const None: 0 = 0; + export const First: 1 = 1; + export const Last: 2 = 2; +} + +export type Touch = 0 | 1 | 2; + +export class LinkedMap { + + private _map: Map>; + private _head: Item | undefined; + private _tail: Item | undefined; + private _size: number; + + constructor() { + this._map = new Map>(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + } + + public clear(): void { + this._map.clear(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + } + + public isEmpty(): boolean { + return !this._head && !this._tail; + } + + public get size(): number { + return this._size; + } + + public has(key: K): boolean { + return this._map.has(key); + } + + public get(key: K): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + return item.value; + } + + public set(key: K, value: V, touch: Touch = Touch.None): void { + let item = this._map.get(key); + if (item) { + item.value = value; + if (touch !== Touch.None) { + this.touch(item, touch); + } + } else { + item = { key, value, next: undefined, previous: undefined }; + switch (touch) { + case Touch.None: + this.addItemLast(item); + break; + case Touch.First: + this.addItemFirst(item); + break; + case Touch.Last: + this.addItemLast(item); + break; + default: + this.addItemLast(item); + break; + } + this._map.set(key, item); + this._size++; + } + } + + public delete(key: K): boolean { + const item = this._map.get(key); + if (!item) { + return false; + } + this._map.delete(key); + this.removeItem(item); + this._size--; + return true; + } + + public shift(): V | undefined { + if (!this._head && !this._tail) { + return undefined; + } + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + const item = this._head; + this._map.delete(item.key); + this.removeItem(item); + this._size--; + return item.value; + } + + public forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + let current = this._head; + while (current) { + if (thisArg) { + callbackfn.bind(thisArg)(current.value, current.key, this); + } else { + callbackfn(current.value, current.key, this); + } + current = current.next; + } + } + + public forEachReverse(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + let current = this._tail; + while (current) { + if (thisArg) { + callbackfn.bind(thisArg)(current.value, current.key, this); + } else { + callbackfn(current.value, current.key, this); + } + current = current.previous; + } + } + + public values(): V[] { + let result: V[] = []; + let current = this._head; + while (current) { + result.push(current.value); + current = current.next; + } + return result; + } + + public keys(): K[] { + let result: K[] = []; + let current = this._head; + while (current) { + result.push(current.key); + current = current.next; + } + return result; + } + + /* VS Code / Monaco editor runs on es5 which has no Symbol.iterator + public keys(): IterableIterator { + let current = this._head; + let iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next():IteratorResult { + if (current) { + let result = { value: current.key, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + public values(): IterableIterator { + let current = this._head; + let iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next():IteratorResult { + if (current) { + let result = { value: current.value, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + */ + + private addItemFirst(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._tail = item; + } else if (!this._head) { + throw new Error('Invalid list'); + } else { + item.next = this._head; + this._head.previous = item; + } + this._head = item; + } + + private addItemLast(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._head = item; + } else if (!this._tail) { + throw new Error('Invalid list'); + } else { + item.previous = this._tail; + this._tail.next = item; + } + this._tail = item; + } + + private removeItem(item: Item): void { + if (item === this._head && item === this._tail) { + this._head = undefined; + this._tail = undefined; + } + else if (item === this._head) { + this._head = item.next; + } + else if (item === this._tail) { + this._tail = item.previous; + } + else { + const next = item.next; + const previous = item.previous; + if (!next || !previous) { + throw new Error('Invalid list'); + } + next.previous = previous; + previous.next = next; + } + } + + private touch(item: Item, touch: Touch): void { + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + if ((touch !== Touch.First && touch !== Touch.Last)) { + return; + } + + if (touch === Touch.First) { + if (item === this._head) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item + if (item === this._tail) { + // previous must be defined since item was not head but is tail + // So there are more than on item in the map + previous!.next = undefined; + this._tail = previous; + } + else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + + // Insert the node at head + item.previous = undefined; + item.next = this._head; + this._head.previous = item; + this._head = item; + } else if (touch === Touch.Last) { + if (item === this._tail) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item. + if (item === this._head) { + // next must be defined since item was not tail but is head + // So there are more than on item in the map + next!.previous = undefined; + this._head = next; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + item.next = undefined; + item.previous = this._tail; + this._tail.next = item; + this._tail = item; + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/tasks/browser/quickOpen.ts b/src/vs/workbench/parts/tasks/browser/quickOpen.ts index c8661e0d52c..9368b61de9a 100644 --- a/src/vs/workbench/parts/tasks/browser/quickOpen.ts +++ b/src/vs/workbench/parts/tasks/browser/quickOpen.ts @@ -8,6 +8,8 @@ import nls = require('vs/nls'); import Filters = require('vs/base/common/filters'); import { TPromise } from 'vs/base/common/winjs.base'; import { Action, IAction } from 'vs/base/common/actions'; +import { IStringDictionary } from 'vs/base/common/collections'; + import Quickopen = require('vs/workbench/browser/quickopen'); import QuickOpen = require('vs/base/parts/quickopen/common/quickOpen'); import Model = require('vs/base/parts/quickopen/browser/quickOpenModel'); @@ -49,7 +51,7 @@ export abstract class QuickOpenHandler extends Quickopen.QuickOpenHandler { constructor( protected quickOpenService: IQuickOpenService, - protected taskService: ITaskService, + protected taskService: ITaskService ) { super(); @@ -71,39 +73,39 @@ export abstract class QuickOpenHandler extends Quickopen.QuickOpenHandler { if (tasks.length === 0) { return new Model.QuickOpenModel(entries); } - tasks = tasks.sort((a, b) => { - let aKind = a._source.kind; - let bKind = b._source.kind; - if (aKind === bKind) { - if (aKind === TaskSourceKind.Extension) { - let compare = a._source.label.localeCompare(b._source.label); - if (compare !== 0) { - return compare; - } - } - return a._label.localeCompare(b._label); - } - if (aKind === TaskSourceKind.Workspace) { - return -1; - } else { - return +1; + let recentlyUsedTasks = this.taskService.getRecentlyUsedTasks(); + let recent: Task[] = []; + let others: Task[] = []; + let taskMap: IStringDictionary = Object.create(null); + tasks.forEach(task => taskMap[task.identifier] = task); + recentlyUsedTasks.keys().forEach(key => { + let task = taskMap[key]; + if (task) { + recent.push(task); } }); - let firstWorkspace: boolean = true; - let firstExtension: boolean = true; - let hadWorkspace: boolean = false; for (let task of tasks) { + if (!recentlyUsedTasks.has(task.identifier)) { + others.push(task); + } + } + others = others.sort((a, b) => a._source.label.localeCompare(b._source.label)); + let sortedTasks = recent.concat(others); + let recentlyUsed: boolean = recentlyUsedTasks.has(tasks[0].identifier); + let otherTasks: boolean = !recentlyUsedTasks.has(tasks[tasks.length - 1].identifier); + let hasRecentlyUsed: boolean = false; + for (let task of sortedTasks) { let highlights = Filters.matchesContiguousSubString(input, task._label); if (!highlights) { continue; } - if (task._source.kind === TaskSourceKind.Workspace && firstWorkspace) { - firstWorkspace = false; - hadWorkspace = true; - entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('configured', 'Configured Tasks'), false)); - } else if (task._source.kind === TaskSourceKind.Extension && firstExtension) { - firstExtension = false; - entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('detected', 'Detected Tasks'), hadWorkspace)); + if (recentlyUsed) { + recentlyUsed = false; + hasRecentlyUsed = true; + entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('recentlyUsed', 'recently used'), false)); + } else if (!recentlyUsedTasks.has(task.identifier) && otherTasks) { + otherTasks = false; + entries.push(new TaskGroupEntry(this.createEntry(this.taskService, task, highlights), nls.localize('other tasks', 'other tasks'), hasRecentlyUsed)); } else { entries.push(this.createEntry(this.taskService, task, highlights)); } diff --git a/src/vs/workbench/parts/tasks/common/taskService.ts b/src/vs/workbench/parts/tasks/common/taskService.ts index b410569a283..89ab0e6d57c 100644 --- a/src/vs/workbench/parts/tasks/common/taskService.ts +++ b/src/vs/workbench/parts/tasks/common/taskService.ts @@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; import { IEventEmitter } from 'vs/base/common/eventEmitter'; import { TerminateResponse } from 'vs/base/common/processes'; +import { LinkedMap } from 'vs/base/common/linkedMap'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Task, TaskSet } from 'vs/workbench/parts/tasks/common/tasks'; import { ITaskSummary, TaskEvent, TaskType } from 'vs/workbench/parts/tasks/common/taskSystem'; @@ -43,6 +44,7 @@ export interface ITaskService extends IEventEmitter { terminateAll(): TPromise; tasks(): TPromise; getTasksForGroup(group: string): TPromise; + getRecentlyUsedTasks(): LinkedMap; customize(task: Task, openConfig?: boolean): TPromise; diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index ee12526316b..10b8f78aba5 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -29,6 +29,7 @@ import { TerminateResponse, TerminateResponseCode } from 'vs/base/common/process import * as strings from 'vs/base/common/strings'; import { ValidationStatus, ValidationState } from 'vs/base/common/parsers'; import * as UUID from 'vs/base/common/uuid'; +import { LinkedMap, Touch } from 'vs/base/common/linkedMap'; import { Registry } from 'vs/platform/platform'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -45,7 +46,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ProblemMatcherRegistry } from 'vs/platform/markers/common/problemMatcher'; - +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -516,6 +517,7 @@ interface WorkspaceConfigurationResult { class TaskService extends EventEmitter implements ITaskService { // private static autoDetectTelemetryName: string = 'taskServer.autoDetect'; + private static RecentlyUsedTasks_Key = 'workbench.tasks.recentlyUsedTasks'; public _serviceBrand: any; public static SERVICE_ID: string = 'taskService'; @@ -544,6 +546,7 @@ class TaskService extends EventEmitter implements ITaskService { private _taskSystem: ITaskSystem; private _taskSystemListeners: IDisposable[]; + private _recentlyUsedTasks: LinkedMap; private _outputChannel: IOutputChannel; @@ -559,7 +562,8 @@ class TaskService extends EventEmitter implements ITaskService { @IEnvironmentService private environmentService: IEnvironmentService, @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, @ITerminalService private terminalService: ITerminalService, - @IWorkbenchEditorService private workbenchEditorService: IWorkbenchEditorService + @IWorkbenchEditorService private workbenchEditorService: IWorkbenchEditorService, + @IStorageService private storageService: IStorageService ) { super(); @@ -689,6 +693,37 @@ class TaskService extends EventEmitter implements ITaskService { return TPromise.as(this._taskSystem.getActiveTasks()); } + public getRecentlyUsedTasks(): LinkedMap { + if (this._recentlyUsedTasks) { + return this._recentlyUsedTasks; + } + this._recentlyUsedTasks = new LinkedMap(); + let storageValue = this.storageService.get(TaskService.RecentlyUsedTasks_Key, StorageScope.WORKSPACE); + if (storageValue) { + try { + let values: string[] = JSON.parse(storageValue); + if (Array.isArray(values)) { + for (let value of values) { + this._recentlyUsedTasks.set(value, value); + } + } + } catch (error) { + // Ignore. We use the empty result + } + } + return this._recentlyUsedTasks; + } + + private saveRecentlyUsedTasks(): void { + if (!this._recentlyUsedTasks) { + return; + } + let values = this._recentlyUsedTasks.values(); + if (values.length > 30) { + values = values.slice(0, 30); + } + this.storageService.store(TaskService.RecentlyUsedTasks_Key, JSON.stringify(values), StorageScope.WORKSPACE); + } public build(): TPromise { return this.getTaskSets().then((values) => { @@ -896,6 +931,7 @@ class TaskService extends EventEmitter implements ITaskService { throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is already a task running. Terminate it first before executing another task.'), TaskErrors.RunningTask); } } + this.getRecentlyUsedTasks().set(task.identifier, task.identifier, Touch.First); return executeResult.promise; }); }); @@ -1248,6 +1284,7 @@ class TaskService extends EventEmitter implements ITaskService { } public beforeShutdown(): boolean | TPromise { + this.saveRecentlyUsedTasks(); if (this._taskSystem && this._taskSystem.isActiveSync()) { if (this._taskSystem.canAutoTerminate() || this.messageService.confirm({ message: nls.localize('TaskSystem.runningTask', 'There is a task running. Do you want to terminate it?'), -- GitLab