提交 e142962e 编写于 作者: C Christof Marti

Multi-step input API (#49340)

上级 10556272
......@@ -83,6 +83,7 @@
"name": "VS Code API Tests (single folder)",
"runtimeExecutable": "${execPath}",
"args": [
// "${workspaceFolder}", // Uncomment for running out of sources.
"${workspaceFolder}/extensions/vscode-api-tests/testWorkspace",
"--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests",
"--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/singlefolder-tests"
......
......@@ -405,6 +405,84 @@ suite('window namespace tests', () => {
return Promise.all([a, b]);
});
test('multiStepInput, two steps', async function () {
const picks = window.multiStepInput(async (input, token) => {
const pick1 = input.showQuickPick(['eins', 'zwei', 'drei']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick1, 'eins');
const pick2 = input.showQuickPick(['vier', 'fünf', 'sechs']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick2, 'vier');
return [ await pick1, await pick2 ];
});
assert.deepEqual(await picks, ['eins', 'vier']);
});
test('multiStepInput, interrupted by showQuickPick', async function () {
const picks = window.multiStepInput(async (input, token) => {
const pick1 = input.showQuickPick(['eins', 'zwei', 'drei']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick1, 'eins');
assert.ok(!token.isCancellationRequested);
const otherPick = window.showQuickPick(['sieben', 'acht', 'neun']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await otherPick, 'sieben');
assert.ok(token.isCancellationRequested);
const pick2 = input.showQuickPick(['vier', 'fünf', 'sechs']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick2, undefined);
return [ await pick1, await pick2 ];
});
assert.deepEqual(await picks, ['eins', undefined]);
});
test('multiStepInput, interrupted by multiStepInput', async function () {
const picks = window.multiStepInput(async (input, token) => {
const pick1 = input.showQuickPick(['eins', 'zwei', 'drei']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick1, 'eins');
assert.ok(!token.isCancellationRequested);
const otherPick = window.multiStepInput(async (input, token) => {
const otherPick = window.showQuickPick(['sieben', 'acht', 'neun']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await otherPick, 'sieben');
return otherPick;
});
assert.equal(await otherPick, 'sieben');
assert.ok(token.isCancellationRequested);
const pick2 = input.showQuickPick(['vier', 'fünf', 'sechs']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick2, undefined);
return [ await pick1, await pick2 ];
});
assert.deepEqual(await picks, ['eins', undefined]);
});
test('multiStepInput, interrupted by error', async function () {
try {
const picks = window.multiStepInput(async (input, token) => {
const pick1 = input.showQuickPick(['eins', 'zwei', 'drei']);
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
assert.equal(await pick1, 'eins');
throw new Error('because');
});
await picks;
assert.ok(false);
} catch (error) {
assert.equal(error.message, 'because');
}
});
test('showWorkspaceFolderPick', function () {
const p = window.showWorkspaceFolderPick(undefined);
......
......@@ -4,4 +4,5 @@
*--------------------------------------------------------------------------------------------*/
/// <reference path="../../../../src/vs/vscode.d.ts" />
/// <reference path="../../../../src/vs/vscode.proposed.d.ts" />
/// <reference types='@types/node'/>
......@@ -84,11 +84,7 @@ export interface IInputOptions {
validateInput?: (input: string) => TPromise<string>;
}
export const IQuickInputService = createDecorator<IQuickInputService>('quickInputService');
export interface IQuickInputService {
_serviceBrand: any;
export interface IQuickInput {
/**
* Opens the quick input box for selecting items and returns a promise with the user selected item(s) if any.
......@@ -99,6 +95,15 @@ export interface IQuickInputService {
* Opens the quick input box for text input and returns a promise with the user typed value if any.
*/
input(options?: IInputOptions, token?: CancellationToken): TPromise<string>;
}
export const IQuickInputService = createDecorator<IQuickInputService>('quickInputService');
export interface IQuickInputService extends IQuickInput {
_serviceBrand: any;
multiStepInput<T>(handler: (input: IQuickInput, token: CancellationToken) => Thenable<T>, token?: CancellationToken): Thenable<T>;
focus(): void;
......
......@@ -510,4 +510,28 @@ declare module 'vscode' {
}
//#endregion
//#region Multi-step input
export namespace window {
/**
* Collect multiple inputs from the user. The provided handler will be called with a
* [`QuickInput`](#QuickInput) that should be used to control the UI.
*
* @param handler The callback that will collect the inputs.
*/
export function multiStepInput<T>(handler: (input: QuickInput, token: CancellationToken) => Thenable<T>, token?: CancellationToken): Thenable<T>;
}
/**
* Controls the UI within a multi-step input session. The handler passed to [`window.multiStepInput`](#window.multiStepInput)
* should use the instance of this interface passed to it to collect all inputs.
*/
export interface QuickInput {
showQuickPick: typeof window.showQuickPick;
showInputBox: typeof window.showInputBox;
}
//#endregion
}
......@@ -6,11 +6,17 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { asWinJsPromise } from 'vs/base/common/async';
import { IPickOptions, IInputOptions, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { InputBoxOptions } from 'vscode';
import { IPickOptions, IInputOptions, IQuickInputService, IQuickInput } from 'vs/platform/quickinput/common/quickInput';
import { InputBoxOptions, CancellationToken } from 'vscode';
import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
interface MultiStepSession {
handle: number;
input: IQuickInput;
token: CancellationToken;
}
@extHostNamedCustomer(MainContext.MainThreadQuickOpen)
export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
......@@ -20,6 +26,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
private _doSetError: (error: Error) => any;
private _contents: TPromise<MyQuickPickItems[]>;
private _token: number = 0;
private _multiStep: MultiStepSession;
constructor(
extHostContext: IExtHostContext,
......@@ -32,7 +39,13 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
public dispose(): void {
}
$show(options: IPickOptions): TPromise<number | number[]> {
$show(multiStepHandle: number | undefined, options: IPickOptions): TPromise<number | number[]> {
const multiStep = typeof multiStepHandle === 'number';
if (multiStep && !(this._multiStep && multiStepHandle === this._multiStep.handle && !this._multiStep.token.isCancellationRequested)) {
return TPromise.as(undefined);
}
const input: IQuickInput = multiStep ? this._multiStep.input : this._quickInputService;
const myToken = ++this._token;
......@@ -51,7 +64,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
});
if (options.canPickMany) {
return asWinJsPromise(token => this._quickInputService.pick(this._contents, options as { canPickMany: true }, token)).then(items => {
return asWinJsPromise(token => input.pick(this._contents, options as { canPickMany: true }, token)).then(items => {
if (items) {
return items.map(item => item.handle);
}
......@@ -62,7 +75,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
}
});
} else {
return asWinJsPromise(token => this._quickInputService.pick(this._contents, options, token)).then(item => {
return asWinJsPromise(token => input.pick(this._contents, options, token)).then(item => {
if (item) {
return item.handle;
}
......@@ -91,7 +104,13 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
// ---- input
$input(options: InputBoxOptions, validateInput: boolean): TPromise<string> {
$input(multiStepHandle: number | undefined, options: InputBoxOptions, validateInput: boolean): TPromise<string> {
const multiStep = typeof multiStepHandle === 'number';
if (multiStep && !(this._multiStep && multiStepHandle === this._multiStep.handle && !this._multiStep.token.isCancellationRequested)) {
return TPromise.as(undefined);
}
const input: IQuickInput = multiStep ? this._multiStep.input : this._quickInputService;
const inputOptions: IInputOptions = Object.create(null);
......@@ -110,6 +129,27 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
};
}
return asWinJsPromise(token => this._quickInputService.input(inputOptions, token));
return asWinJsPromise(token => input.input(inputOptions, token));
}
// ---- Multi-step input
$multiStep(handle: number): TPromise<never> {
let outerReject: (err: any) => void;
let innerResolve: (value: void) => void;
const promise = new TPromise<never>((_, rej) => outerReject = rej, () => innerResolve(undefined));
this._quickInputService.multiStepInput((input, token) => {
this._multiStep = { handle, input, token };
const promise = new TPromise<void>(res => innerResolve = res);
token.onCancellationRequested(() => innerResolve(undefined));
return promise;
})
.then(() => promise.cancel(), err => outerReject(err))
.then(() => {
if (this._multiStep && this._multiStep.handle === handle) {
this._multiStep = null;
}
});
return promise;
}
}
......@@ -386,13 +386,16 @@ export function createApiFactory(
return extHostMessageService.showMessage(extension, Severity.Error, message, first, rest);
},
showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken): any {
return extHostQuickOpen.showQuickPick(items, options, token);
return extHostQuickOpen.showQuickPick(undefined, items, options, token);
},
showWorkspaceFolderPick(options: vscode.WorkspaceFolderPickOptions) {
return extHostQuickOpen.showWorkspaceFolderPick(options);
},
showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) {
return extHostQuickOpen.showInput(options, token);
return extHostQuickOpen.showInput(undefined, options, token);
},
multiStepInput<T>(handler: (input: vscode.QuickInput, token: vscode.CancellationToken) => Thenable<T>, token?: vscode.CancellationToken): Thenable<T> {
return extHostQuickOpen.multiStepInput(handler, token);
},
showOpenDialog(options) {
return extHostDialogs.showOpenDialog(options);
......
......@@ -332,10 +332,11 @@ export interface MyQuickPickItems extends IPickOpenEntry {
handle: number;
}
export interface MainThreadQuickOpenShape extends IDisposable {
$show(options: IPickOptions): TPromise<number | number[]>;
$show(multiStepHandle: number | undefined, options: IPickOptions): TPromise<number | number[]>;
$setItems(items: MyQuickPickItems[]): TPromise<any>;
$setError(error: Error): TPromise<any>;
$input(options: vscode.InputBoxOptions, validateInput: boolean): TPromise<string>;
$input(multiStepHandle: number | undefined, options: vscode.InputBoxOptions, validateInput: boolean): TPromise<string>;
$multiStep(handle: number): TPromise<never>;
}
export interface MainThreadStatusBarShape extends IDisposable {
......
......@@ -6,11 +6,12 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { wireCancellationToken, asWinJsPromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { QuickPickOptions, QuickPickItem, InputBoxOptions, WorkspaceFolderPickOptions, WorkspaceFolder } from 'vscode';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { QuickPickOptions, QuickPickItem, InputBoxOptions, WorkspaceFolderPickOptions, WorkspaceFolder, QuickInput } from 'vscode';
import { MainContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, MyQuickPickItems, IMainContext } from './extHost.protocol';
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands';
import { isPromiseCanceledError } from 'vs/base/common/errors';
export type Item = string | QuickPickItem;
......@@ -23,23 +24,25 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
private _onDidSelectItem: (handle: number) => void;
private _validateInput: (input: string) => string | Thenable<string>;
private _nextMultiStepHandle = 1;
constructor(mainContext: IMainContext, workspace: ExtHostWorkspace, commands: ExtHostCommands) {
this._proxy = mainContext.getProxy(MainContext.MainThreadQuickOpen);
this._workspace = workspace;
this._commands = commands;
}
showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable<QuickPickItem[]>, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable<QuickPickItem[] | undefined>;
showQuickPick(itemsOrItemsPromise: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string | undefined>;
showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Thenable<QuickPickItem[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<QuickPickItem | undefined>;
showQuickPick(itemsOrItemsPromise: Item[] | Thenable<Item[]>, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable<Item | Item[] | undefined> {
showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: QuickPickItem[] | Thenable<QuickPickItem[]>, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable<QuickPickItem[] | undefined>;
showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string | undefined>;
showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: QuickPickItem[] | Thenable<QuickPickItem[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<QuickPickItem | undefined>;
showQuickPick(multiStepHandle: number | undefined, itemsOrItemsPromise: Item[] | Thenable<Item[]>, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Thenable<Item | Item[] | undefined> {
// clear state from last invocation
this._onDidSelectItem = undefined;
const itemsPromise = <TPromise<Item[]>>TPromise.wrap(itemsOrItemsPromise);
const quickPickWidget = this._proxy.$show({
const quickPickWidget = this._proxy.$show(multiStepHandle, {
placeHolder: options && options.placeHolder,
matchOnDescription: options && options.matchOnDescription,
matchOnDetail: options && options.matchOnDetail,
......@@ -115,12 +118,12 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
// ---- input
showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Thenable<string> {
showInput(multiStepHandle: number | undefined, options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Thenable<string> {
// global validate fn used in callback below
this._validateInput = options && options.validateInput;
const promise = this._proxy.$input(options, typeof this._validateInput === 'function');
const promise = this._proxy.$input(multiStepHandle, options, typeof this._validateInput === 'function');
return wireCancellationToken(token, promise, true);
}
......@@ -142,4 +145,42 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
return this._workspace.getWorkspaceFolders().filter(folder => folder.uri.toString() === selectedFolder.uri.toString())[0];
});
}
// ---- Multi-step input
multiStepInput<T>(handler: (input: QuickInput, token: CancellationToken) => Thenable<T>, clientToken: CancellationToken = CancellationToken.None): Thenable<T> {
const handle = this._nextMultiStepHandle++;
const remotePromise = this._proxy.$multiStep(handle);
const cancellationSource = new CancellationTokenSource();
const handlerPromise = TPromise.wrap(handler({
showQuickPick: this.showQuickPick.bind(this, handle),
showInputBox: this.showInput.bind(this, handle)
}, cancellationSource.token));
clientToken.onCancellationRequested(() => {
remotePromise.cancel();
cancellationSource.cancel();
});
return TPromise.join<void, T>([
remotePromise.then(() => {
throw new Error('Unexpectedly fulfilled promise.');
}, err => {
if (!isPromiseCanceledError(err)) {
throw err;
}
cancellationSource.cancel();
}),
handlerPromise.then(result => {
remotePromise.cancel();
return result;
}, err => {
remotePromise.cancel();
throw err;
})
]).then(([_, result]) => result, ([remoteErr, handlerErr]) => {
throw handlerErr || remoteErr;
});
}
}
......@@ -7,7 +7,7 @@
import 'vs/css!./quickInput';
import { Component } from 'vs/workbench/common/component';
import { IQuickInputService, IPickOpenEntry, IPickOptions, IInputOptions, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput';
import { IQuickInputService, IPickOpenEntry, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickInput } from 'vs/platform/quickinput/common/quickInput';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import * as dom from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
......@@ -16,7 +16,7 @@ import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegi
import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { TPromise } from 'vs/base/common/winjs.base';
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { QuickInputList } from './quickInputList';
import { QuickInputBox } from './quickInputBox';
import { KeyCode } from 'vs/base/common/keyCodes';
......@@ -361,6 +361,7 @@ export class QuickInputService extends Component implements IQuickInputService {
private inQuickOpenContext: IContextKey<boolean>;
private controller: InputController<any>;
private multiStepHandle: CancellationTokenSource;
constructor(
@IEnvironmentService private environmentService: IEnvironmentService,
......@@ -556,7 +557,11 @@ export class QuickInputService extends Component implements IQuickInputService {
}
pick<T extends IPickOpenEntry, O extends IPickOptions>(picks: TPromise<T[]>, options: O = <O>{}, token?: CancellationToken): TPromise<O extends { canPickMany: true } ? T[] : T> {
return <any>this.show(<any>{
return this._pick(undefined, picks, options, token);
}
private _pick<T extends IPickOpenEntry, O extends IPickOptions>(handle: CancellationTokenSource | undefined, picks: TPromise<T[]>, options: O = <O>{}, token?: CancellationToken): TPromise<O extends { canPickMany: true } ? T[] : T> {
return <any>this._show(handle, <any>{
type: options.canPickMany ? 'pickMany' : 'pickOne',
picks,
placeHolder: options.placeHolder,
......@@ -567,7 +572,11 @@ export class QuickInputService extends Component implements IQuickInputService {
}
input(options: IInputOptions = {}, token?: CancellationToken): TPromise<string> {
return this.show({
return this._input(undefined, options, token);
}
private _input(handle: CancellationTokenSource | undefined, options: IInputOptions = {}, token?: CancellationToken): TPromise<string> {
return this._show(handle, {
type: 'textInput',
value: options.value,
valueSelection: options.valueSelection,
......@@ -579,9 +588,17 @@ export class QuickInputService extends Component implements IQuickInputService {
}, token);
}
show<T extends IPickOpenEntry, P extends PickOneParameters<T> | PickManyParameters<T>>(parameters: P, token?: CancellationToken): TPromise<P extends PickManyParameters<T> ? T[] : T>;
show(parameters: TextInputParameters, token?: CancellationToken): TPromise<string>;
show<R>(parameters: InputParameters, token: CancellationToken = CancellationToken.None): TPromise<R> {
private _show<T extends IPickOpenEntry, P extends PickOneParameters<T> | PickManyParameters<T>>(multiStepHandle: CancellationTokenSource | undefined, parameters: P, token?: CancellationToken): TPromise<P extends PickManyParameters<T> ? T[] : T>;
private _show(multiStepHandle: CancellationTokenSource | undefined, parameters: TextInputParameters, token?: CancellationToken): TPromise<string>;
private _show<R>(multiStepHandle: CancellationTokenSource | undefined, parameters: InputParameters, token: CancellationToken = CancellationToken.None): TPromise<R> {
if (multiStepHandle && multiStepHandle !== this.multiStepHandle) {
multiStepHandle.cancel();
return TPromise.as(undefined);
}
if (!multiStepHandle && this.multiStepHandle) {
this.multiStepHandle.cancel();
}
this.create();
this.quickOpenService.close();
if (this.controller) {
......@@ -644,6 +661,17 @@ export class QuickInputService extends Component implements IQuickInputService {
}
}
multiStepInput<T>(handler: (input: IQuickInput, token: CancellationToken) => Thenable<T>, token = CancellationToken.None): Thenable<T> {
if (this.multiStepHandle) {
this.multiStepHandle.cancel();
}
this.multiStepHandle = new CancellationTokenSource();
return TPromise.wrap(handler({
pick: this._pick.bind(this, this.multiStepHandle),
input: this._input.bind(this, this.multiStepHandle)
}, this.multiStepHandle.token));
}
focus() {
if (this.isDisplayed()) {
this.ui.inputBox.setFocus();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册