add cancellation support for showInput and showQuickPick, #9377

This commit is contained in:
Johannes Rieken 2016-08-08 15:50:57 +02:00
parent b0c702dd2f
commit ff061902a7
11 changed files with 106 additions and 49 deletions

View file

@ -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);
});
});
});

View file

@ -40,8 +40,15 @@ export function asWinJsPromise<T>(callback: (token: CancellationToken) => T | Th
/**
* Hook a cancellation token to a WinJS Promise
*/
export function wireCancellationToken<T>(token: CancellationToken, promise: TPromise<T>): Thenable<T> {
export function wireCancellationToken<T>(token: CancellationToken, promise: TPromise<T>, resolveAsUndefinedWhenCancelled?: boolean): Thenable<T> {
token.onCancellationRequested(() => promise.cancel());
if (resolveAsUndefinedWhenCancelled) {
return promise.then(undefined, err => {
if (!errors.isPromiseCanceledError(err)) {
return TPromise.wrapError(err);
}
});
}
return promise;
}

View file

@ -12,6 +12,11 @@ export interface CancellationToken {
onCancellationRequested: Event<any>;
}
const shortcutEvent: Event<any> = 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<any> = Object.freeze(function(callback, context?) {
let handle = setTimeout(callback.bind(context), 0);
return { dispose() { clearTimeout(handle); } };
});
class MutableToken implements CancellationToken {
private _isCancelled: boolean = false;

View file

@ -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);
}

9
src/vs/vscode.d.ts vendored
View file

@ -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<string[]>, options?: QuickPickOptions): Thenable<string>;
export function showQuickPick(items: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string>;
/**
* 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<T extends QuickPickItem>(items: T[] | Thenable<T[]>, options?: QuickPickOptions): Thenable<T>;
export function showQuickPick<T extends QuickPickItem>(items: T[] | Thenable<T[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<T>;
/**
* 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<string>;
export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string>;
/**
* Create a new [output channel](#OutputChannel) with the given name.

View file

@ -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 {

View file

@ -153,7 +153,7 @@ export abstract class MainThreadQuickOpenShape {
$show(options: IPickOptions): Thenable<number> { throw ni(); }
$setItems(items: MyQuickPickItems[]): Thenable<any> { throw ni(); }
$setError(error: Error): Thenable<any> { throw ni(); }
$input(options: vscode.InputBoxOptions, validateInput: boolean): Thenable<string> { throw ni(); }
$input(options: vscode.InputBoxOptions, validateInput: boolean): TPromise<string> { throw ni(); }
}
export abstract class MainThreadStatusBarShape {

View file

@ -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<Item[]>, options?: QuickPickOptions): Thenable<Item> {
showQuickPick(itemsOrItemsPromise: Item[] | Thenable<Item[]>, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable<Item> {
// clear state from last invocation
this._onDidSelectItem = undefined;
let itemsPromise: Thenable<Item[]>;
if (!Array.isArray(itemsOrItemsPromise)) {
itemsPromise = itemsOrItemsPromise;
} else {
itemsPromise = TPromise.as(itemsOrItemsPromise);
}
const itemsPromise = <TPromise<Item[]>> 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<string> {
showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Thenable<string> {
// 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<string> {

View file

@ -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<string> {
$input(options: InputBoxOptions, validateInput: boolean): TPromise<string> {
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));
}
}

View file

@ -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<string> {
public input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): TPromise<string> {
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<string[]>, options?: IPickOptions): TPromise<string>;
public pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions): TPromise<string>;
public pick(picks: string[], options?: IPickOptions): TPromise<string>;
public pick<T extends IPickOpenEntry>(picks: T[], options?: IPickOptions): TPromise<T>;
public pick(arg1: string[] | TPromise<string[]> | IPickOpenEntry[] | TPromise<IPickOpenEntry[]>, options?: IPickOptions): TPromise<string | IPickOpenEntry> {
public pick(picks: TPromise<string[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string>;
public pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string>;
public pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise<string>;
public pick<T extends IPickOpenEntry>(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise<T>;
public pick(arg1: string[] | TPromise<string[]> | IPickOpenEntry[] | TPromise<IPickOpenEntry[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string | IPickOpenEntry> {
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<IPickOpenEntry[]>, options: IInternalPickOptions): TPromise<IPickOpenEntry> {
private doPick(picksPromise: TPromise<IPickOpenEntry[]>, options: IInternalPickOptions, token: CancellationToken = CancellationToken.None): TPromise<IPickOpenEntry> {
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<IPickOpenEntry | string>((complete, error, progress) => {
// hide widget when being cancelled
token.onCancellationRequested(e => this.pickOpenWidget.hide(HideReason.CANCELED));
let picksPromiseDone = false;
// Resolve picks

View file

@ -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<string[]>, options?: IPickOptions): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions): TPromise<T>;
pick(picks: string[], options?: IPickOptions): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: T[], options?: IPickOptions): TPromise<T>;
pick(picks: TPromise<string[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<T>;
pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise<T>;
/**
* 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<string>;
input(options?: IInputOptions, token?: CancellationToken): TPromise<string>;
/**
* Allows to register on the event that quick open is showing