diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index a6eba040c00..a50aa94d9f0 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -96,9 +96,9 @@ export interface INotificationProgress { export interface INotificationHandle extends IDisposable { /** - * Will be fired once the notification hides. + * Will be fired once the notification is disposed. */ - readonly onDidHide: Event; + readonly onDidDispose: Event; /** * Allows to indicate progress on the notification even after the @@ -160,10 +160,10 @@ export interface INotificationService { export class NoOpNotification implements INotificationHandle { readonly progress = new NoOpProgress(); - private _onDidHide: Emitter = new Emitter(); + private _onDidDispose: Emitter = new Emitter(); - public get onDidHide(): Event { - return this._onDidHide.event; + public get onDidDispose(): Event { + return this._onDidDispose.event; } updateSeverity(severity: Severity): void { } @@ -171,7 +171,7 @@ export class NoOpNotification implements INotificationHandle { updateActions(actions?: INotificationActions): void { } dispose(): void { - this._onDidHide.dispose(); + this._onDidDispose.dispose(); } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts b/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts index 73323ac91ee..355ad2e9b3c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadMessageService.ts @@ -11,7 +11,9 @@ import { MainThreadMessageServiceShape, MainContext, IExtHostContext, MainThread import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IChoiceService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotificationHandle } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { once } from 'vs/base/common/event'; +import { ICommandService } from 'vs/platform/commands/common/commands'; @extHostNamedCustomer(MainContext.MainThreadMessageService) export class MainThreadMessageService implements MainThreadMessageServiceShape { @@ -19,6 +21,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { constructor( extHostContext: IExtHostContext, @INotificationService private readonly _notificationService: INotificationService, + @ICommandService private readonly _commandService: ICommandService, @IChoiceService private readonly _choiceService: IChoiceService ) { // @@ -40,40 +43,41 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { return new Promise(resolve => { - let messageHandle: INotificationHandle; let actions: MessageItemAction[] = []; - let hasCloseAffordance = false; class MessageItemAction extends Action { constructor(id: string, label: string, handle: number) { super(id, label, undefined, true, () => { resolve(handle); - messageHandle.dispose(); // triggers dispose! make sure promise is already resolved return undefined; }); } - dispose(): void { - resolve(undefined); + } + + class ManageExtensionAction extends Action { + constructor(id: string, label: string, commandService: ICommandService) { + super(id, label, undefined, true, () => { + return commandService.executeCommand('_extensions.manage', id); + }); } } commands.forEach(command => { - if (command.isCloseAffordance === true) { - hasCloseAffordance = true; - } actions.push(new MessageItemAction('_extension_message_handle_' + command.handle, command.title, command.handle)); }); - if (!hasCloseAffordance) { - actions.push(new MessageItemAction('__close', nls.localize('close', "Close"), undefined)); - } - - messageHandle = this._notificationService.notify({ + const messageHandle = this._notificationService.notify({ severity, message, - actions: { primary: actions }, + actions: { primary: actions, secondary: extension ? [new ManageExtensionAction(extension.id, nls.localize('manageExtension', "Manage Extension"), this._commandService)] : [] }, source: extension && `${extension.displayName || extension.name}` }); + + // if promise has not been resolved yet, now is the time to ensure a return value + // otherwise if already resolved it means the user clicked one of the buttons + once(messageHandle.onDidDispose)(() => { + resolve(undefined); + }); }); } diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 780b0def52c..dbc2e4b31df 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -33,7 +33,7 @@ export interface INotificationChangeEvent { } export class NotificationHandle implements INotificationHandle { - private _onDidHide: Emitter = new Emitter(); + private _onDidDispose: Emitter = new Emitter(); constructor(private item: INotificationViewItem, private disposeItem: (item: INotificationViewItem) => void) { this.registerListeners(); @@ -41,13 +41,13 @@ export class NotificationHandle implements INotificationHandle { private registerListeners(): void { once(this.item.onDidDispose)(() => { - this._onDidHide.fire(); - this._onDidHide.dispose(); + this._onDidDispose.fire(); + this._onDidDispose.dispose(); }); } - public get onDidHide(): Event { - return this._onDidHide.event; + public get onDidDispose(): Event { + return this._onDidDispose.event; } public get progress(): INotificationProgress { @@ -68,7 +68,7 @@ export class NotificationHandle implements INotificationHandle { public dispose(): void { this.disposeItem(this.item); - this._onDidHide.dispose(); + this._onDidDispose.dispose(); } } diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts index 14793d13b94..3dffea57284 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogs.ts @@ -150,7 +150,7 @@ export class DialogService implements IChoiceService, IConfirmationService { handle = this.notificationService.notify({ severity, message, actions }); // Cancel promise when notification gets disposed - once(handle.onDidHide)(() => promise.cancel()); + once(handle.onDidDispose)(() => promise.cancel()); }, () => handle.dispose()); diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index b6f3560fb0e..c14367ab9b6 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -143,7 +143,7 @@ suite('Notifications', () => { assert.equal(model.notifications.length, 3); let called = 0; - item1Handle.onDidHide(() => { + item1Handle.onDidDispose(() => { called++; }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts b/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts index 33c27e7b140..428d21ddcaf 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostMessagerService.test.ts @@ -6,11 +6,11 @@ 'use strict'; import * as assert from 'assert'; -import { IAction } from 'vs/base/common/actions'; import { MainThreadMessageService } from 'vs/workbench/api/electron-browser/mainThreadMessageService'; -import { TPromise as Promise } from 'vs/base/common/winjs.base'; +import { TPromise as Promise, TPromise } from 'vs/base/common/winjs.base'; import { IChoiceService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotification } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, NoOpNotification, INotificationHandle } from 'vs/platform/notification/common/notification'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const emptyChoiceService = new class implements IChoiceService { _serviceBrand: 'choiceService'; @@ -19,6 +19,13 @@ const emptyChoiceService = new class implements IChoiceService { } }; +const emptyCommandService: ICommandService = { + _serviceBrand: undefined, + onWillExecuteCommand: () => ({ dispose: () => { } }), + executeCommand: (commandId: string, ...args: any[]): TPromise => { + return TPromise.as(void 0); + } +}; const emptyNotificationService = new class implements INotificationService { _serviceBrand: 'notificiationService'; @@ -36,74 +43,46 @@ const emptyNotificationService = new class implements INotificationService { } }; +class EmptyNotificationService implements INotificationService { + + _serviceBrand: any; + + constructor(private withNotify: (notification: INotification) => void) { + } + + notify(notification: INotification): INotificationHandle { + this.withNotify(notification); + + return new NoOpNotification(); + } + info(message: any): void { + throw new Error('Method not implemented.'); + } + warn(message: any): void { + throw new Error('Method not implemented.'); + } + error(message: any): void { + throw new Error('Method not implemented.'); + } +} + suite('ExtHostMessageService', function () { test('propagte handle on select', function () { - let service = new MainThreadMessageService(null, { - notify(m: INotification) { - assert.equal(m.actions.primary.length, 1); - setImmediate(() => m.actions.primary[0].run()); - return undefined; - } - } as INotificationService, emptyChoiceService); + let service = new MainThreadMessageService(null, new EmptyNotificationService(notification => { + assert.equal(notification.actions.primary.length, 1); + setImmediate(() => notification.actions.primary[0].run()); + }), emptyCommandService, emptyChoiceService); return service.$showMessage(1, 'h', {}, [{ handle: 42, title: 'a thing', isCloseAffordance: true }]).then(handle => { assert.equal(handle, 42); }); }); - test('isCloseAffordance', function () { - - let actions: IAction[]; - let service = new MainThreadMessageService(null, { - notify(m: INotification) { - actions = m.actions.primary; - - return undefined; - } - } as INotificationService, emptyChoiceService); - - // default close action - 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 }]); - assert.equal(actions.length, 1); - first = actions[0]; - assert.equal(first.label, 'a thing'); - }); - - test('hide on select', function () { - - let actions: IAction[]; - let c: number; - let service = new MainThreadMessageService(null, { - notify(m: INotification) { - c = 0; - actions = m.actions.primary; - return { - dispose: () => { - c += 1; - } - }; - } - } as INotificationService, emptyChoiceService); - - 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(null, emptyNotificationService, { + const service = new MainThreadMessageService(null, emptyNotificationService, emptyCommandService, { choose(severity, message, options, modal) { assert.equal(severity, 1); assert.equal(message, 'h'); @@ -119,7 +98,7 @@ suite('ExtHostMessageService', function () { }); test('returns undefined when cancelled', () => { - const service = new MainThreadMessageService(null, emptyNotificationService, { + const service = new MainThreadMessageService(null, emptyNotificationService, emptyCommandService, { choose(severity, message, options, modal) { return Promise.as(1); } @@ -131,7 +110,7 @@ suite('ExtHostMessageService', function () { }); test('hides Cancel button when not needed', () => { - const service = new MainThreadMessageService(null, emptyNotificationService, { + const service = new MainThreadMessageService(null, emptyNotificationService, emptyCommandService, { choose(severity, message, options, modal) { assert.equal(options.length, 1); return Promise.as(0);