diff --git a/extensions/vscode-api-tests/src/window.test.ts b/extensions/vscode-api-tests/src/window.test.ts index 5b98e2d2067..9a02325e47a 100644 --- a/extensions/vscode-api-tests/src/window.test.ts +++ b/extensions/vscode-api-tests/src/window.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import {workspace, window, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position} from 'vscode'; +import {workspace, window, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource} from 'vscode'; import {join} from 'path'; import {cleanUp, pathEquals} from './utils'; @@ -148,8 +148,45 @@ suite('window namespace tests', () => { }); test('#7013 - input without options', function () { - - let p = window.showInputBox(); + const source = new CancellationTokenSource(); + let p = window.showInputBox(undefined, source.token); assert.ok(typeof p === 'object'); + source.dispose(); + }); + + test('showInputBox - undefined on cancel', function () { + const source = new CancellationTokenSource(); + const p = window.showInputBox(undefined, source.token); + source.cancel(); + return p.then(value => { + assert.equal(value, undefined); + }); + }); + + test('showInputBox - cancel early', function () { + const source = new CancellationTokenSource(); + source.cancel(); + const p = window.showInputBox(undefined, source.token); + return p.then(value => { + assert.equal(value, undefined); + }); + }); + + test('showQuickPick, undefined on cancel', function () { + const source = new CancellationTokenSource(); + const p = window.showQuickPick(['eins', 'zwei', 'drei'], undefined, source.token); + source.cancel(); + return p.then(value => { + assert.equal(value, undefined); + }); + }); + + test('showQuickPick, cancel early', function () { + const source = new CancellationTokenSource(); + source.cancel(); + const p = window.showQuickPick(['eins', 'zwei', 'drei'], undefined, source.token); + return p.then(value => { + assert.equal(value, undefined); + }); }); }); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 68b84190f41..41c8cf24a8d 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -40,8 +40,15 @@ export function asWinJsPromise(callback: (token: CancellationToken) => T | Th /** * Hook a cancellation token to a WinJS Promise */ -export function wireCancellationToken(token: CancellationToken, promise: TPromise): Thenable { +export function wireCancellationToken(token: CancellationToken, promise: TPromise, resolveAsUndefinedWhenCancelled?: boolean): Thenable { token.onCancellationRequested(() => promise.cancel()); + if (resolveAsUndefinedWhenCancelled) { + return promise.then(undefined, err => { + if (!errors.isPromiseCanceledError(err)) { + return TPromise.wrapError(err); + } + }); + } return promise; } diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index e8123246b91..ba8f97a834c 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -12,6 +12,11 @@ export interface CancellationToken { onCancellationRequested: Event; } +const shortcutEvent: Event = Object.freeze(function(callback, context?) { + let handle = setTimeout(callback.bind(context), 0); + return { dispose() { clearTimeout(handle); } }; +}); + export namespace CancellationToken { export const None: CancellationToken = Object.freeze({ @@ -21,15 +26,10 @@ export namespace CancellationToken { export const Cancelled: CancellationToken = Object.freeze({ isCancellationRequested: true, - onCancellationRequested: Event.None + onCancellationRequested: shortcutEvent }); } -const shortcutEvent: Event = Object.freeze(function(callback, context?) { - let handle = setTimeout(callback.bind(context), 0); - return { dispose() { clearTimeout(handle); } }; -}); - class MutableToken implements CancellationToken { private _isCancelled: boolean = false; diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index d26b70bd5db..66a9ffbee3e 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -509,11 +509,11 @@ export class TestQuickOpenService implements QuickOpenService.IQuickOpenService this.callback = callback; } - pick(arg: any, placeHolder?: string, autoFocusFirst?: boolean): Promise { + pick(arg: any, options?: any, token?: any): Promise { return TPromise.as(null); } - input(options?: any): Promise { + input(options?: any, token?: any): Promise { return TPromise.as(null); } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 8e463e7ea46..500c27d7d6c 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3324,18 +3324,20 @@ declare namespace vscode { * * @param items An array of strings, or a promise that resolves to an array of strings. * @param options Configures the behavior of the selection list. + * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selection or undefined. */ - export function showQuickPick(items: string[] | Thenable, options?: QuickPickOptions): Thenable; + export function showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** * Shows a selection list. * * @param items An array of items, or a promise that resolves to an array of items. * @param options Configures the behavior of the selection list. + * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected item or undefined. */ - export function showQuickPick(items: T[] | Thenable, options?: QuickPickOptions): Thenable; + export function showQuickPick(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; /** * Opens an input box to ask the user for input. @@ -3345,9 +3347,10 @@ declare namespace vscode { * anything but dismissed the input box with OK. * * @param options Configures the behavior of the input box. + * @param token A token that can be used to signal cancellation. * @return A promise that resolves to a string the user provided or to `undefined` in case of dismissal. */ - export function showInputBox(options?: InputBoxOptions): Thenable; + export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; /** * Create a new [output channel](#OutputChannel) with the given name. diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 82372870a41..03c22e52f3f 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -236,11 +236,11 @@ export class ExtHostAPIImplementation { showErrorMessage: (message, ...items) => { return extHostMessageService.showMessage(Severity.Error, message, items); }, - showQuickPick: (items: any, options: vscode.QuickPickOptions) => { - return extHostQuickOpen.show(items, options); + showQuickPick: (items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken) => { + return extHostQuickOpen.showQuickPick(items, options, token); }, - showInputBox(options?: vscode.InputBoxOptions) { - return extHostQuickOpen.input(options); + showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { + return extHostQuickOpen.showInput(options, token); }, createStatusBarItem(position?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 282ded66c2d..c2c8b8f6137 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -153,7 +153,7 @@ export abstract class MainThreadQuickOpenShape { $show(options: IPickOptions): Thenable { throw ni(); } $setItems(items: MyQuickPickItems[]): Thenable { throw ni(); } $setError(error: Error): Thenable { throw ni(); } - $input(options: vscode.InputBoxOptions, validateInput: boolean): Thenable { throw ni(); } + $input(options: vscode.InputBoxOptions, validateInput: boolean): TPromise { throw ni(); } } export abstract class MainThreadStatusBarShape { diff --git a/src/vs/workbench/api/node/extHostQuickOpen.ts b/src/vs/workbench/api/node/extHostQuickOpen.ts index 59ec73bbb81..7b16d9cb6a6 100644 --- a/src/vs/workbench/api/node/extHostQuickOpen.ts +++ b/src/vs/workbench/api/node/extHostQuickOpen.ts @@ -5,6 +5,8 @@ 'use strict'; import {TPromise} from 'vs/base/common/winjs.base'; +import {wireCancellationToken} from 'vs/base/common/async'; +import {CancellationToken} from 'vs/base/common/cancellation'; import {IThreadService} from 'vs/workbench/services/thread/common/threadService'; import {QuickPickOptions, QuickPickItem, InputBoxOptions} from 'vscode'; import {MainContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems} from './extHost.protocol'; @@ -22,26 +24,21 @@ export class ExtHostQuickOpen extends ExtHostQuickOpenShape { this._proxy = threadService.get(MainContext.MainThreadQuickOpen); } - show(itemsOrItemsPromise: Item[] | Thenable, options?: QuickPickOptions): Thenable { + showQuickPick(itemsOrItemsPromise: Item[] | Thenable, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable { // clear state from last invocation this._onDidSelectItem = undefined; - let itemsPromise: Thenable; - if (!Array.isArray(itemsOrItemsPromise)) { - itemsPromise = itemsOrItemsPromise; - } else { - itemsPromise = TPromise.as(itemsOrItemsPromise); - } + const itemsPromise = > TPromise.as(itemsOrItemsPromise); - let quickPickWidget = this._proxy.$show({ + const quickPickWidget = this._proxy.$show({ autoFocus: { autoFocusFirstEntry: true }, placeHolder: options && options.placeHolder, matchOnDescription: options && options.matchOnDescription, matchOnDetail: options && options.matchOnDetail }); - return itemsPromise.then(items => { + const promise = itemsPromise.then(items => { let pickItems: MyQuickPickItems[] = []; for (let handle = 0; handle < items.length; handle++) { @@ -86,6 +83,8 @@ export class ExtHostQuickOpen extends ExtHostQuickOpenShape { return TPromise.wrapError(err); }); + + return wireCancellationToken(token, promise, true); } $onItemSelected(handle: number): void { @@ -96,9 +95,13 @@ export class ExtHostQuickOpen extends ExtHostQuickOpenShape { // ---- input - input(options?: InputBoxOptions): Thenable { + showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Thenable { + + // global validate fn used in callback below this._validateInput = options && options.validateInput; - return this._proxy.$input(options, options && typeof options.validateInput === 'function'); + + const promise = this._proxy.$input(options, typeof this._validateInput === 'function'); + return wireCancellationToken(token, promise, true); } $validateInput(input: string): TPromise { diff --git a/src/vs/workbench/api/node/mainThreadQuickOpen.ts b/src/vs/workbench/api/node/mainThreadQuickOpen.ts index 12a883b2527..cdcb7f2de16 100644 --- a/src/vs/workbench/api/node/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/node/mainThreadQuickOpen.ts @@ -5,6 +5,7 @@ 'use strict'; import {TPromise} from 'vs/base/common/winjs.base'; +import {asWinJsPromise} from 'vs/base/common/async'; import {IThreadService} from 'vs/workbench/services/thread/common/threadService'; import {IQuickOpenService, IPickOptions, IInputOptions} from 'vs/workbench/services/quickopen/common/quickOpenService'; import {InputBoxOptions} from 'vscode'; @@ -46,7 +47,7 @@ export class MainThreadQuickOpen extends MainThreadQuickOpenShape { }; }); - return this._quickOpenService.pick(this._contents, options).then(item => { + return asWinJsPromise(token => this._quickOpenService.pick(this._contents, options, token)).then(item => { if (item) { return item.handle; } @@ -73,7 +74,7 @@ export class MainThreadQuickOpen extends MainThreadQuickOpenShape { // ---- input - $input(options: InputBoxOptions, validateInput: boolean): Thenable { + $input(options: InputBoxOptions, validateInput: boolean): TPromise { const inputOptions: IInputOptions = Object.create(null); @@ -90,6 +91,6 @@ export class MainThreadQuickOpen extends MainThreadQuickOpenShape { }; } - return this._quickOpenService.input(inputOptions); + return asWinJsPromise(token => this._quickOpenService.input(inputOptions, token)); } } diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 166164158bf..956e02fb53c 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -14,9 +14,10 @@ import filters = require('vs/base/common/filters'); import URI from 'vs/base/common/uri'; import uuid = require('vs/base/common/uuid'); import types = require('vs/base/common/types'); +import {CancellationToken} from 'vs/base/common/cancellation'; import {Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel} from 'vs/base/parts/quickopen/common/quickOpen'; import {QuickOpenEntryItem, QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup} from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import {QuickOpenWidget} from 'vs/base/parts/quickopen/browser/quickOpenWidget'; +import {QuickOpenWidget, HideReason} from 'vs/base/parts/quickopen/browser/quickOpenWidget'; import {ContributableActionProvider} from 'vs/workbench/browser/actionBarRegistry'; import {ITree, IElementCallback} from 'vs/base/parts/tree/browser/tree'; import labels = require('vs/base/common/labels'); @@ -123,7 +124,7 @@ export class QuickOpenController extends WorkbenchComponent implements IQuickOpe } } - public input(options: IInputOptions = {}): TPromise { + public input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): TPromise { const defaultMessage = options.prompt ? nls.localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", options.prompt) : nls.localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel"); @@ -167,7 +168,7 @@ export class QuickOpenController extends WorkbenchComponent implements IQuickOpe }); } } - }).then(resolve, reject); + }, token).then(resolve, reject); }; return new TPromise(init).then(item => { @@ -179,11 +180,11 @@ export class QuickOpenController extends WorkbenchComponent implements IQuickOpe }); } - public pick(picks: TPromise, options?: IPickOptions): TPromise; - public pick(picks: TPromise, options?: IPickOptions): TPromise; - public pick(picks: string[], options?: IPickOptions): TPromise; - public pick(picks: T[], options?: IPickOptions): TPromise; - public pick(arg1: string[] | TPromise | IPickOpenEntry[] | TPromise, options?: IPickOptions): TPromise { + public pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; + public pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; + public pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise; + public pick(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise; + public pick(arg1: string[] | TPromise | IPickOpenEntry[] | TPromise, options?: IPickOptions, token?: CancellationToken): TPromise { if (!options) { options = Object.create(null); } @@ -215,11 +216,11 @@ export class QuickOpenController extends WorkbenchComponent implements IQuickOpe return item && isAboutStrings ? item.label : item; } - this.doPick(entryPromise, options).then(item => resolve(onItem(item)), err => reject(err), item => progress(onItem(item))); + this.doPick(entryPromise, options, token).then(item => resolve(onItem(item)), err => reject(err), item => progress(onItem(item))); }); } - private doPick(picksPromise: TPromise, options: IInternalPickOptions): TPromise { + private doPick(picksPromise: TPromise, options: IInternalPickOptions, token: CancellationToken = CancellationToken.None): TPromise { let autoFocus = options.autoFocus; // Use a generated token to avoid race conditions from long running promises @@ -276,6 +277,10 @@ export class QuickOpenController extends WorkbenchComponent implements IQuickOpe } return new TPromise((complete, error, progress) => { + + // hide widget when being cancelled + token.onCancellationRequested(e => this.pickOpenWidget.hide(HideReason.CANCELED)); + let picksPromiseDone = false; // Resolve picks diff --git a/src/vs/workbench/services/quickopen/common/quickOpenService.ts b/src/vs/workbench/services/quickopen/common/quickOpenService.ts index bda3b1e786b..02fc52a8282 100644 --- a/src/vs/workbench/services/quickopen/common/quickOpenService.ts +++ b/src/vs/workbench/services/quickopen/common/quickOpenService.ts @@ -6,6 +6,7 @@ import {TPromise} from 'vs/base/common/winjs.base'; import Event from 'vs/base/common/event'; +import {CancellationToken} from 'vs/base/common/cancellation'; import {IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext} from 'vs/base/parts/quickopen/common/quickOpen'; import {createDecorator} from 'vs/platform/instantiation/common/instantiation'; @@ -108,10 +109,10 @@ export interface IQuickOpenService { * Passing in a promise will allow you to resolve the elements in the background while quick open will show a * progress bar spinning. */ - pick(picks: TPromise, options?: IPickOptions): TPromise; - pick(picks: TPromise, options?: IPickOptions): TPromise; - pick(picks: string[], options?: IPickOptions): TPromise; - pick(picks: T[], options?: IPickOptions): TPromise; + pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; + pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise; + pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise; + pick(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise; /** * Should not be used by clients. Will cause any opened quick open widget to navigate in the result set. @@ -121,7 +122,7 @@ export interface IQuickOpenService { /** * Opens the quick open box for user input and returns a promise with the user typed value if any. */ - input(options?: IInputOptions): TPromise; + input(options?: IInputOptions, token?: CancellationToken): TPromise; /** * Allows to register on the event that quick open is showing