diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 97d981ba530..e160ed65305 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. */ @@ -3567,6 +3582,7 @@ declare module 'vscode' { * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ export function showInformationMessage(message: string, ...items: string[]): Thenable; + export function showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; /** * Show an information message. @@ -3578,6 +3594,7 @@ declare module 'vscode' { * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ export function showInformationMessage(message: string, ...items: T[]): Thenable; + export function showInformationMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show a warning message. @@ -3589,6 +3606,7 @@ declare module 'vscode' { * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ export function showWarningMessage(message: string, ...items: string[]): Thenable; + export function showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; /** * Show a warning message. @@ -3600,6 +3618,7 @@ declare module 'vscode' { * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ export function showWarningMessage(message: string, ...items: T[]): Thenable; + export function showWarningMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show an error message. @@ -3611,6 +3630,7 @@ declare module 'vscode' { * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ export function showErrorMessage(message: string, ...items: string[]): Thenable; + export function showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; /** * Show an error message. @@ -3622,6 +3642,7 @@ declare module 'vscode' { * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ export function showErrorMessage(message: string, ...items: T[]): Thenable; + 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 c3e38145462..234e7a82ca5 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -255,6 +255,18 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ } }; + const emptyMessageOptions: vscode.MessageOptions = Object.create(null); + + function parseMessageArguments(args: any[]): { options: vscode.MessageOptions; items: any[]; } { + const [first, ...rest] = args; + + if (first && (typeof first === 'string' || first.title)) { + return { options: emptyMessageOptions, items: args }; + } else { + return { options: first || emptyMessageOptions, items: rest }; + } + } + // namespace: window const window: typeof vscode.window = { get activeTextEditor() { @@ -287,14 +299,17 @@ 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, ...args) { + const { options, items } = parseMessageArguments(args); + return extHostMessageService.showMessage(Severity.Info, message, options, items); }, - showWarningMessage(message, ...items) { - return extHostMessageService.showMessage(Severity.Warning, message, items); + showWarningMessage(message, ...args) { + const { options, items } = parseMessageArguments(args); + return extHostMessageService.showMessage(Severity.Warning, message, options, items); }, - showErrorMessage(message, ...items) { - return extHostMessageService.showMessage(Severity.Error, message, items); + showErrorMessage(message, ...args) { + const { options, items } = parseMessageArguments(args); + return extHostMessageService.showMessage(Severity.Error, message, options, items); }, showQuickPick(items: any, options: vscode.QuickPickOptions, token?: vscode.CancellationToken) { return extHostQuickOpen.showQuickPick(items, options, token); @@ -325,7 +340,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 f08c6498385..24e77896c5d 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -176,7 +176,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 ec7c2c0e27c..b02a13f349c 100644 --- a/src/vs/workbench/api/node/extHostMessageService.ts +++ b/src/vs/workbench/api/node/extHostMessageService.ts @@ -17,7 +17,7 @@ export class ExtHostMessageService { this._proxy = threadService.get(MainContext.MainThreadMessageService); } - showMessage(severity: Severity, message: string, commands: (string | vscode.MessageItem)[]): Thenable { + showMessage(severity: Severity, message: string, options: vscode.MessageOptions, commands: (string | vscode.MessageItem)[]): Thenable { const items: { title: string; isCloseAffordance: boolean; handle: number; }[] = []; @@ -33,7 +33,7 @@ export class ExtHostMessageService { } } - return this._proxy.$showMessage(severity, message, items).then(handle => { + return this._proxy.$showMessage(severity, message, options, items).then(handle => { if (typeof handle === 'number') { return commands[handle]; } diff --git a/src/vs/workbench/api/node/mainThreadMessageService.ts b/src/vs/workbench/api/node/mainThreadMessageService.ts index fb053564dc7..99255d2d361 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 closeAffordanceIndex = -1; + + const options = commands.map((command, index) => { + if (command.isCloseAffordance === true) { + closeAffordanceIndex = index; + } + + return command.title; + }); + + if (closeAffordanceIndex === -1) { + 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); + }); + }); + }); });