diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index bc15b0e4420..4c913345b26 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1411,6 +1411,21 @@ declare module 'vscode' { isCloseAffordance?: boolean; } + /** + * Options to configure the behavior of the message. + * + * @see [showInformationMessage](#window.showInformationMessage) + * @see [showWarningMessage](#window.showWarningMessage) + * @see [showErrorMessage](#window.showErrorMessage) + */ + export interface MessageOptions { + + /** + * Indicates that this message should be modal. + */ + modal?: boolean; + } + /** * Options to configure the behavior of the input box UI. */ @@ -3586,6 +3601,17 @@ declare module 'vscode' { */ export function showInformationMessage(message: string, ...items: string[]): Thenable; + /** + * Show an information message to users. Optionally provide an array of items which will be presented as + * clickable buttons. + * + * @param message The message to show. + * @param options Configures the behaviour of the message. + * @param items A set of items that will be rendered as actions in the message. + * @return A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + export function showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + /** * Show an information message. * @@ -3597,6 +3623,18 @@ declare module 'vscode' { */ export function showInformationMessage(message: string, ...items: T[]): Thenable; + /** + * Show an information message. + * + * @see [showInformationMessage](#window.showInformationMessage) + * + * @param message The message to show. + * @param options Configures the behaviour of the message. + * @param items A set of items that will be rendered as actions in the message. + * @return A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + export function showInformationMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + /** * Show a warning message. * @@ -3608,6 +3646,18 @@ declare module 'vscode' { */ export function showWarningMessage(message: string, ...items: string[]): Thenable; + /** + * Show a warning message. + * + * @see [showInformationMessage](#window.showInformationMessage) + * + * @param message The message to show. + * @param options Configures the behaviour of the message. + * @param items A set of items that will be rendered as actions in the message. + * @return A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + export function showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + /** * Show a warning message. * @@ -3619,6 +3669,18 @@ declare module 'vscode' { */ export function showWarningMessage(message: string, ...items: T[]): Thenable; + /** + * Show a warning message. + * + * @see [showInformationMessage](#window.showInformationMessage) + * + * @param message The message to show. + * @param options Configures the behaviour of the message. + * @param items A set of items that will be rendered as actions in the message. + * @return A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + export function showWarningMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + /** * Show an error message. * @@ -3630,6 +3692,18 @@ declare module 'vscode' { */ export function showErrorMessage(message: string, ...items: string[]): Thenable; + /** + * Show an error message. + * + * @see [showInformationMessage](#window.showInformationMessage) + * + * @param message The message to show. + * @param options Configures the behaviour of the message. + * @param items A set of items that will be rendered as actions in the message. + * @return A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + export function showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + /** * Show an error message. * @@ -3641,6 +3715,18 @@ declare module 'vscode' { */ export function showErrorMessage(message: string, ...items: T[]): Thenable; + /** + * Show an error message. + * + * @see [showInformationMessage](#window.showInformationMessage) + * + * @param message The message to show. + * @param options Configures the behaviour of the message. + * @param items A set of items that will be rendered as actions in the message. + * @return A thenable that resolves to the selected item or `undefined` when being dismissed. + */ + export function showErrorMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + /** * Shows a selection list. * diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 1837c23b996..267ed86f9cb 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -291,14 +291,14 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ onDidCloseTerminal(listener, thisArg?, disposables?) { return extHostTerminalService.onDidCloseTerminal(listener, thisArg, disposables); }, - showInformationMessage(message, ...items) { - return extHostMessageService.showMessage(Severity.Info, message, items); + showInformationMessage(message, first, ...rest) { + return extHostMessageService.showMessage(Severity.Info, message, first, rest); }, - showWarningMessage(message, ...items) { - return extHostMessageService.showMessage(Severity.Warning, message, items); + showWarningMessage(message, first, ...rest) { + return extHostMessageService.showMessage(Severity.Warning, message, first, rest); }, - showErrorMessage(message, ...items) { - return extHostMessageService.showMessage(Severity.Error, message, items); + showErrorMessage(message, first, ...rest) { + return extHostMessageService.showMessage(Severity.Error, message, first, rest); }, showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken) { return extHostQuickOpen.showQuickPick(items, options, token); @@ -329,7 +329,7 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ }, // proposed API sampleFunction: proposedApiFunction(extension, () => { - return extHostMessageService.showMessage(Severity.Info, 'Hello Proposed Api!', []); + return extHostMessageService.showMessage(Severity.Info, 'Hello Proposed Api!', {}, []); }), registerTreeExplorerNodeProvider: proposedApiFunction(extension, (providerId: string, provider: vscode.TreeExplorerNodeProvider) => { return extHostExplorers.registerTreeExplorerNodeProvider(providerId, provider); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 9246fe5d4d9..f5d14ea492c 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -177,7 +177,7 @@ export abstract class MainThreadLanguagesShape { } export abstract class MainThreadMessageServiceShape { - $showMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Thenable { throw ni(); } + $showMessage(severity: Severity, message: string, options: vscode.MessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Thenable { throw ni(); } } export abstract class MainThreadOutputServiceShape { diff --git a/src/vs/workbench/api/node/extHostMessageService.ts b/src/vs/workbench/api/node/extHostMessageService.ts index 9b87ced7c11..e044c6c01e0 100644 --- a/src/vs/workbench/api/node/extHostMessageService.ts +++ b/src/vs/workbench/api/node/extHostMessageService.ts @@ -9,6 +9,20 @@ import Severity from 'vs/base/common/severity'; import vscode = require('vscode'); import { MainContext, MainThreadMessageServiceShape } from './extHost.protocol'; +const emptyMessageOptions: vscode.MessageOptions = Object.create(null); + +function isMessageItem(item: any): item is vscode.MessageItem { + return item && item.title; +} + +function parseMessageArguments(first: vscode.MessageOptions | string | vscode.MessageItem, rest: (string | vscode.MessageItem)[]): { options: vscode.MessageOptions; items: (string | vscode.MessageItem)[]; } { + if (typeof first === 'string' || isMessageItem(first)) { + return { options: emptyMessageOptions, items: [first, ...rest] }; + } else { + return { options: first || emptyMessageOptions, items: rest }; + } +} + export class ExtHostMessageService { private _proxy: MainThreadMessageServiceShape; @@ -17,25 +31,25 @@ export class ExtHostMessageService { this._proxy = threadService.get(MainContext.MainThreadMessageService); } - showMessage(severity: Severity, message: string, commands: (string | vscode.MessageItem)[]): Thenable { + showMessage(severity: Severity, message: string, optionsOrFirstItem: vscode.MessageOptions | string | vscode.MessageItem, rest: (string | vscode.MessageItem)[]): Thenable { + const { options, items } = parseMessageArguments(optionsOrFirstItem, rest); + const commands: { title: string; isCloseAffordance: boolean; handle: number; }[] = []; - const items: { title: string; isCloseAffordance: boolean; handle: number; }[] = []; - - for (let handle = 0; handle < commands.length; handle++) { - let command = commands[handle]; + for (let handle = 0; handle < items.length; handle++) { + let command = items[handle]; if (typeof command === 'string') { - items.push({ title: command, handle, isCloseAffordance: false }); + commands.push({ title: command, handle, isCloseAffordance: false }); } else if (typeof command === 'object') { let {title, isCloseAffordance} = command; - items.push({ title, isCloseAffordance, handle }); + commands.push({ title, isCloseAffordance, handle }); } else { console.warn('Invalid message item:', command); } } - return this._proxy.$showMessage(severity, message, items).then(handle => { + return this._proxy.$showMessage(severity, message, options, commands).then(handle => { if (typeof handle === 'number') { - return commands[handle]; + return items[handle]; } return undefined; }); diff --git a/src/vs/workbench/api/node/mainThreadMessageService.ts b/src/vs/workbench/api/node/mainThreadMessageService.ts index fb053564dc7..c8d9c07996e 100644 --- a/src/vs/workbench/api/node/mainThreadMessageService.ts +++ b/src/vs/workbench/api/node/mainThreadMessageService.ts @@ -5,22 +5,31 @@ 'use strict'; import nls = require('vs/nls'); -import { IMessageService } from 'vs/platform/message/common/message'; +import { IMessageService, IChoiceService } from 'vs/platform/message/common/message'; import Severity from 'vs/base/common/severity'; import { Action } from 'vs/base/common/actions'; import { TPromise as Promise } from 'vs/base/common/winjs.base'; import { MainThreadMessageServiceShape } from './extHost.protocol'; +import * as vscode from 'vscode'; export class MainThreadMessageService extends MainThreadMessageServiceShape { - private _messageService: IMessageService; - - constructor( @IMessageService messageService: IMessageService) { + constructor( + @IMessageService private _messageService: IMessageService, + @IChoiceService private _choiceService: IChoiceService + ) { super(); - this._messageService = messageService; } - $showMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Thenable { + $showMessage(severity: Severity, message: string, options: vscode.MessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Thenable { + if (options.modal) { + return this.showModalMessage(severity, message, commands); + } else { + return this.showMessage(severity, message, commands); + } + } + + private showMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Thenable { return new Promise(resolve => { @@ -58,4 +67,27 @@ export class MainThreadMessageService extends MainThreadMessageServiceShape { }); }); } + + private showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Thenable { + let hasCloseAffordance = false; + + const options = commands.map((command, index) => { + if (command.isCloseAffordance === true) { + hasCloseAffordance = true; + } + + return command.title; + }); + + if (!hasCloseAffordance) { + if (options.length > 0) { + options.push(nls.localize('cancel', "Cancel")); + } else { + options.push(nls.localize('ok', "OK")); + } + } + + return this._choiceService.choose(severity, message, options, true) + .then(result => result === commands.length ? undefined : commands[result].handle); + } } diff --git a/src/vs/workbench/test/node/api/extHostMessagerService.test.ts b/src/vs/workbench/test/node/api/extHostMessagerService.test.ts index d25a5e08e57..d6a10c576a7 100644 --- a/src/vs/workbench/test/node/api/extHostMessagerService.test.ts +++ b/src/vs/workbench/test/node/api/extHostMessagerService.test.ts @@ -8,8 +8,9 @@ import * as assert from 'assert'; import { Action } from 'vs/base/common/actions'; import { MainThreadMessageService } from 'vs/workbench/api/node/mainThreadMessageService'; +import { TPromise as Promise } from 'vs/base/common/winjs.base'; -suite('ExtHostMessageService', function () { +suite('`ExtHostMessageService`', function () { test('propagte handle on select', function () { @@ -19,9 +20,13 @@ suite('ExtHostMessageService', function () { setImmediate(() => m.actions[0].run()); return () => { }; } + }, { + choose() { + throw new Error('not implemented'); + } }); - return service.$showMessage(1, 'h', [{ handle: 42, title: 'a thing', isCloseAffordance: true }]).then(handle => { + return service.$showMessage(1, 'h', {}, [{ handle: 42, title: 'a thing', isCloseAffordance: true }]).then(handle => { assert.equal(handle, 42); }); }); @@ -33,17 +38,21 @@ suite('ExtHostMessageService', function () { show(sev: number, m: { message; actions: Action[] }) { actions = m.actions; } + }, { + choose() { + throw new Error('not implemented'); + } }); // default close action - service.$showMessage(1, '', [{ title: 'a thing', isCloseAffordance: false, handle: 0 }]); + service.$showMessage(1, '', {}, [{ title: 'a thing', isCloseAffordance: false, handle: 0 }]); assert.equal(actions.length, 2); let [first, second] = actions; assert.equal(first.label, 'a thing'); assert.equal(second.label, 'Close'); // override close action - service.$showMessage(1, '', [{ title: 'a thing', isCloseAffordance: true, handle: 0 }]); + service.$showMessage(1, '', {}, [{ title: 'a thing', isCloseAffordance: true, handle: 0 }]); assert.equal(actions.length, 1); first = actions[0]; assert.equal(first.label, 'a thing'); @@ -61,12 +70,71 @@ suite('ExtHostMessageService', function () { c += 1; }; } + }, { + choose() { + throw new Error('not implemented'); + } }); - service.$showMessage(1, '', [{ title: 'a thing', isCloseAffordance: true, handle: 0 }]); + service.$showMessage(1, '', {}, [{ title: 'a thing', isCloseAffordance: true, handle: 0 }]); assert.equal(actions.length, 1); actions[0].run(); assert.equal(c, 1); }); + + suite('modal', () => { + test('calls choice service', () => { + const service = new MainThreadMessageService({ + show(sev: number, m: { message; actions: Action[] }) { + throw new Error('not implemented'); + } + }, { + choose(severity, message, options, modal) { + assert.equal(severity, 1); + assert.equal(message, 'h'); + assert.equal(options.length, 2); + assert.equal(options[1], 'Cancel'); + return Promise.as(0); + } + }); + + return service.$showMessage(1, 'h', { modal: true }, [{ handle: 42, title: 'a thing', isCloseAffordance: false }]).then(handle => { + assert.equal(handle, 42); + }); + }); + + test('returns undefined when cancelled', () => { + const service = new MainThreadMessageService({ + show(sev: number, m: { message; actions: Action[] }) { + throw new Error('not implemented'); + } + }, { + choose(severity, message, options, modal) { + return Promise.as(1); + } + }); + + return service.$showMessage(1, 'h', { modal: true }, [{ handle: 42, title: 'a thing', isCloseAffordance: false }]).then(handle => { + assert.equal(handle, undefined); + }); + }); + + test('hides Cancel button when not needed', () => { + const service = new MainThreadMessageService({ + show(sev: number, m: { message; actions: Action[] }) { + throw new Error('not implemented'); + } + }, { + choose(severity, message, options, modal) { + assert.equal(options.length, 1); + return Promise.as(0); + } + }); + + return service.$showMessage(1, 'h', { modal: true }, [{ handle: 42, title: 'a thing', isCloseAffordance: true }]).then(handle => { + assert.equal(handle, 42); + }); + }); + }); });