debt - remove dependency of keybinding service to status bar service

This commit is contained in:
Benjamin Pasero 2019-06-05 11:31:21 +02:00
parent 7908cea2d8
commit fa3b4d7c28
15 changed files with 297 additions and 124 deletions

View file

@ -8,7 +8,7 @@ import * as dom from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Emitter, Event } from 'vs/base/common/event';
import { Keybinding, ResolvedKeybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes';
import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
import { OS, isLinux, isMacintosh } from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
@ -37,7 +37,7 @@ import { IKeybindingItem, KeybindingsRegistry } from 'vs/platform/keybinding/com
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label';
import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification } from 'vs/platform/notification/common/notification';
import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
import { IProgressRunner, ILocalProgressService } from 'vs/platform/progress/common/progress';
import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
@ -220,6 +220,10 @@ export class SimpleNotificationService implements INotificationService {
public prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
return SimpleNotificationService.NO_OP;
}
public status(message: string | Error, options?: IStatusMessageOptions): IDisposable {
return Disposable.None;
}
}
export class StandaloneCommandService implements ICommandService {

View file

@ -15,7 +15,6 @@ import { IKeybindingEvent, IKeybindingService, IKeyboardEvent } from 'vs/platfor
import { IResolveResult, KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
interface CurrentChord {
@ -26,45 +25,32 @@ interface CurrentChord {
export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService {
public _serviceBrand: any;
protected readonly _onDidUpdateKeybindings: Emitter<IKeybindingEvent> = this._register(new Emitter<IKeybindingEvent>());
get onDidUpdateKeybindings(): Event<IKeybindingEvent> {
return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
}
private _currentChord: CurrentChord | null;
private _currentChordChecker: IntervalTimer;
private _currentChordStatusMessage: IDisposable | null;
protected _onDidUpdateKeybindings: Emitter<IKeybindingEvent>;
private _contextKeyService: IContextKeyService;
private _statusService: IStatusbarService | undefined;
private _notificationService: INotificationService;
protected _commandService: ICommandService;
protected _telemetryService: ITelemetryService;
constructor(
contextKeyService: IContextKeyService,
commandService: ICommandService,
telemetryService: ITelemetryService,
notificationService: INotificationService,
statusService?: IStatusbarService
private _contextKeyService: IContextKeyService,
protected _commandService: ICommandService,
protected _telemetryService: ITelemetryService,
private _notificationService: INotificationService,
) {
super();
this._contextKeyService = contextKeyService;
this._commandService = commandService;
this._telemetryService = telemetryService;
this._statusService = statusService;
this._notificationService = notificationService;
this._currentChord = null;
this._currentChordChecker = new IntervalTimer();
this._currentChordStatusMessage = null;
this._onDidUpdateKeybindings = this._register(new Emitter<IKeybindingEvent>());
}
public dispose(): void {
super.dispose();
}
get onDidUpdateKeybindings(): Event<IKeybindingEvent> {
return this._onDidUpdateKeybindings ? this._onDidUpdateKeybindings.event : Event.None; // Sinon stubbing walks properties on prototype
}
protected abstract _getResolver(): KeybindingResolver;
protected abstract _documentHasFocus(): boolean;
public abstract resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
@ -128,9 +114,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
keypress: firstPart,
label: keypressLabel
};
if (this._statusService) {
this._currentChordStatusMessage = this._statusService.setStatusMessage(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
}
this._currentChordStatusMessage = this._notificationService.status(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
const chordEnterTime = Date.now();
this._currentChordChecker.cancelAndSet(() => {
@ -192,9 +176,9 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
return shouldPreventDefault;
}
if (this._statusService && this._currentChord) {
if (this._currentChord) {
if (!resolveResult || !resolveResult.commandId) {
this._statusService.setStatusMessage(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", this._currentChord.label, keypressLabel), 10 * 1000 /* 10s */);
this._notificationService.status(nls.localize('missing.chord', "The key combination ({0}, {1}) is not a command.", this._currentChord.label, keypressLabel), { hideAfter: 10 * 1000 /* 10s */ });
shouldPreventDefault = true;
}
}

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { KeyChord, KeyCode, KeyMod, Keybinding, ResolvedKeybinding, SimpleKeybinding, createKeybinding, createSimpleKeybinding } from 'vs/base/common/keyCodes';
import { IDisposable } from 'vs/base/common/lifecycle';
import { OS } from 'vs/base/common/platform';
import Severity from 'vs/base/common/severity';
import { ICommandService } from 'vs/platform/commands/common/commands';
@ -14,9 +13,9 @@ import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { INotification, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification } from 'vs/platform/notification/common/notification';
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { INotification, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
function createContext(ctx: any) {
return {
@ -35,10 +34,9 @@ suite('AbstractKeybindingService', () => {
resolver: KeybindingResolver,
contextKeyService: IContextKeyService,
commandService: ICommandService,
notificationService: INotificationService,
statusService?: IStatusbarService
notificationService: INotificationService
) {
super(contextKeyService, commandService, NullTelemetryService, notificationService, statusService);
super(contextKeyService, commandService, NullTelemetryService, notificationService);
this._resolver = resolver;
}
@ -129,7 +127,7 @@ suite('AbstractKeybindingService', () => {
};
let notificationService: INotificationService = {
_serviceBrand: undefined,
_serviceBrand: {} as ServiceIdentifier<INotificationService>,
notify: (notification: INotification) => {
showMessageCalls.push({ sev: notification.severity, message: notification.message });
return new NoOpNotification();
@ -148,13 +146,8 @@ suite('AbstractKeybindingService', () => {
},
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions) {
throw new Error('not implemented');
}
};
let statusbarService: IStatusbarService = {
_serviceBrand: undefined,
addEntry: undefined!,
setStatusMessage: (message: string, autoDisposeAfter?: number, delayBy?: number): IDisposable => {
},
status(message: string, options?: IStatusMessageOptions) {
statusMessageCalls!.push(message);
return {
dispose: () => {
@ -166,7 +159,7 @@ suite('AbstractKeybindingService', () => {
let resolver = new KeybindingResolver(items, []);
return new TestKeybindingService(resolver, contextKeyService, commandService, notificationService, statusbarService);
return new TestKeybindingService(resolver, contextKeyService, commandService, notificationService);
};
});

View file

@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import BaseSeverity from 'vs/base/common/severity';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IAction } from 'vs/base/common/actions';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
export import Severity = BaseSeverity;
@ -172,6 +173,21 @@ export interface IPromptOptions extends INotificationProperties {
onCancel?: () => void;
}
export interface IStatusMessageOptions {
/**
* An optional timeout after which the status message should show. By default
* the status message will show immediately.
*/
showAfter?: number;
/**
* An optional timeout after which the status message is to be hidden. By default
* the status message will not hide until another status message is displayed.
*/
hideAfter?: number;
}
/**
* A service to bring up notifications and non-modal prompts.
*
@ -179,7 +195,7 @@ export interface IPromptOptions extends INotificationProperties {
*/
export interface INotificationService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<INotificationService>;
/**
* Show the provided notification to the user. The returned `INotificationHandle`
@ -221,16 +237,24 @@ export interface INotificationService {
* @returns a handle on the notification to e.g. hide it or update message, buttons, etc.
*/
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle;
/**
* Shows a status message in the status area with the provied text.
*
* @param message the message to show as status
* @param options provides some optional configuration options
*
* @returns a disposable to hide the status message
*/
status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable;
}
export class NoOpNotification implements INotificationHandle {
readonly progress = new NoOpProgress();
private readonly _onDidClose: Emitter<void> = new Emitter();
get onDidClose(): Event<void> {
return this._onDidClose.event;
}
get onDidClose(): Event<void> { return this._onDidClose.event; }
updateSeverity(severity: Severity): void { }
updateMessage(message: NotificationMessage): void { }

View file

@ -3,31 +3,36 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotificationService, INotificationHandle, NoOpNotification, Severity, INotification, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
import { INotificationService, INotificationHandle, NoOpNotification, Severity, INotification, IPromptChoice, IPromptOptions, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
export class TestNotificationService implements INotificationService {
public _serviceBrand: any;
_serviceBrand: any;
private static readonly NO_OP: INotificationHandle = new NoOpNotification();
public info(message: string): INotificationHandle {
info(message: string): INotificationHandle {
return this.notify({ severity: Severity.Info, message });
}
public warn(message: string): INotificationHandle {
warn(message: string): INotificationHandle {
return this.notify({ severity: Severity.Warning, message });
}
public error(error: string | Error): INotificationHandle {
error(error: string | Error): INotificationHandle {
return this.notify({ severity: Severity.Error, message: error });
}
public notify(notification: INotification): INotificationHandle {
notify(notification: INotification): INotificationHandle {
return TestNotificationService.NO_OP;
}
public prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
return TestNotificationService.NO_OP;
}
status(message: string | Error, options?: IStatusMessageOptions): IDisposable {
return Disposable.None;
}
}

View file

@ -288,7 +288,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
// --- Toggle Statusbar Visibility
class ToggleStatusbarVisibilityAction extends Action {
export class ToggleStatusbarVisibilityAction extends Action {
static readonly ID = 'workbench.action.toggleStatusbarVisibility';
static readonly LABEL = nls.localize('toggleStatusbar', "Toggle Status Bar Visibility");

View file

@ -181,14 +181,15 @@ export class ActivitybarPart extends Part implements IActivityBarService {
createContentArea(parent: HTMLElement): HTMLElement {
this.element = parent;
const content = document.createElement('div');
addClass(content, 'content');
parent.appendChild(content);
// Top Actionbar with action items for each viewlet action
// Viewlets action bar
this.compositeBar.create(content);
// Top Actionbar with action items for each viewlet action
// Global action bar
const globalActivities = document.createElement('div');
addClass(globalActivities, 'global-activity');
content.appendChild(globalActivities);

View file

@ -3,16 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem } from 'vs/workbench/common/notifications';
import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem, IStatusMessageChangeEvent, StatusMessageChangeType, IStatusMessageViewItem } from 'vs/workbench/common/notifications';
import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { HIDE_NOTIFICATIONS_CENTER, SHOW_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands';
import { localize } from 'vs/nls';
export class NotificationsStatus extends Disposable {
private statusItem: IStatusbarEntryAccessor;
private notificationsCenterStatusItem: IStatusbarEntryAccessor;
private isNotificationsCenterVisible: boolean;
private _counter: Set<INotificationViewItem>;
private currentNotifications = new Set<INotificationViewItem>();
private currentStatusMessage: [IStatusMessageViewItem, IDisposable] | undefined;
constructor(
private model: INotificationsModel,
@ -20,29 +23,14 @@ export class NotificationsStatus extends Disposable {
) {
super();
this._counter = new Set<INotificationViewItem>();
this.updateNotificationsStatusItem();
this.updateNotificationsCenterStatusItem();
this.registerListeners();
}
private get count(): number {
return this._counter.size;
}
update(isCenterVisible: boolean): void {
if (this.isNotificationsCenterVisible !== isCenterVisible) {
this.isNotificationsCenterVisible = isCenterVisible;
// Showing the notification center resets the counter to 0
this._counter.clear();
this.updateNotificationsStatusItem();
}
}
private registerListeners(): void {
this._register(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e)));
this._register(this.model.onDidStatusMessageChange(e => this.onDidStatusMessageChange(e)));
}
private onDidNotificationChange(e: INotificationChangeEvent): void {
@ -52,29 +40,29 @@ export class NotificationsStatus extends Disposable {
// Notification got Added
if (e.kind === NotificationChangeType.ADD) {
this._counter.add(e.item);
this.currentNotifications.add(e.item);
}
// Notification got Removed
else if (e.kind === NotificationChangeType.REMOVE) {
this._counter.delete(e.item);
this.currentNotifications.delete(e.item);
}
this.updateNotificationsStatusItem();
this.updateNotificationsCenterStatusItem();
}
private updateNotificationsStatusItem(): void {
private updateNotificationsCenterStatusItem(): void {
const statusProperties: IStatusbarEntry = {
text: this.count === 0 ? '$(bell)' : `$(bell) ${this.count}`,
text: this.currentNotifications.size === 0 ? '$(bell)' : `$(bell) ${this.currentNotifications.size}`,
command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER,
tooltip: this.getTooltip(),
showBeak: this.isNotificationsCenterVisible
};
if (!this.statusItem) {
this.statusItem = this.statusbarService.addEntry(statusProperties, StatusbarAlignment.RIGHT, -1000 /* towards the far end of the right hand side */);
if (!this.notificationsCenterStatusItem) {
this.notificationsCenterStatusItem = this.statusbarService.addEntry(statusProperties, StatusbarAlignment.RIGHT, -1000 /* towards the far end of the right hand side */);
} else {
this.statusItem.update(statusProperties);
this.notificationsCenterStatusItem.update(statusProperties);
}
}
@ -87,14 +75,52 @@ export class NotificationsStatus extends Disposable {
return localize('zeroNotifications', "No Notifications");
}
if (this.count === 0) {
if (this.currentNotifications.size === 0) {
return localize('noNotifications', "No New Notifications");
}
if (this.count === 1) {
if (this.currentNotifications.size === 1) {
return localize('oneNotification', "1 New Notification");
}
return localize('notifications', "{0} New Notifications", this.count);
return localize('notifications', "{0} New Notifications", this.currentNotifications.size);
}
update(isCenterVisible: boolean): void {
if (this.isNotificationsCenterVisible !== isCenterVisible) {
this.isNotificationsCenterVisible = isCenterVisible;
// Showing the notification center resets the counter to 0
this.currentNotifications.clear();
this.updateNotificationsCenterStatusItem();
}
}
private onDidStatusMessageChange(e: IStatusMessageChangeEvent): void {
const statusItem = e.item;
switch (e.kind) {
// Show status notification
case StatusMessageChangeType.ADD:
const showAfter = statusItem.options ? statusItem.options.showAfter : undefined;
const hideAfter = statusItem.options ? statusItem.options.hideAfter : undefined;
this.currentStatusMessage = [
statusItem,
this.statusbarService.setStatusMessage(statusItem.message, hideAfter, showAfter)
];
break;
// Hide status notification (if its still the current one)
case StatusMessageChangeType.REMOVE:
if (this.currentStatusMessage && this.currentStatusMessage[0] === statusItem) {
this.currentStatusMessage[1].dispose();
this.currentStatusMessage = undefined;
}
break;
}
}
}

View file

@ -17,25 +17,27 @@ import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiat
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Action } from 'vs/base/common/actions';
import { Action, IAction } from 'vs/base/common/actions';
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService';
import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { isThemeColor } from 'vs/editor/common/editorCommon';
import { Color } from 'vs/base/common/color';
import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, clearNode, removeClass } from 'vs/base/browser/dom';
import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, clearNode, removeClass, EventType } from 'vs/base/browser/dom';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { coalesce } from 'vs/base/common/arrays';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { ToggleStatusbarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions';
interface PendingEntry { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number; accessor?: IStatusbarEntryAccessor; }
export class StatusbarPart extends Part implements IStatusbarService {
_serviceBrand: ServiceIdentifier<any>;
_serviceBrand: ServiceIdentifier<IStatusbarService>;
private static readonly PRIORITY_PROP = 'statusbar-entry-priority';
private static readonly ALIGNMENT_PROP = 'statusbar-entry-alignment';
@ -54,15 +56,20 @@ export class StatusbarPart extends Part implements IStatusbarService {
private pendingEntries: PendingEntry[] = [];
private hideStatusBarAction: ToggleStatusbarVisibilityAction;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IStorageService storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextMenuService private contextMenuService: IContextMenuService
) {
super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService);
this.hideStatusBarAction = this._register(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, nls.localize('hideStatusBar', "Hide Status Bar")));
this.registerListeners();
}
@ -161,6 +168,9 @@ export class StatusbarPart extends Part implements IStatusbarService {
createContentArea(parent: HTMLElement): HTMLElement {
this.element = parent;
// Context menu support
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e)));
// Fill in initial items that were contributed from the registry
const registry = Registry.as<IStatusbarRegistry>(Extensions.Statusbar);
@ -199,6 +209,39 @@ export class StatusbarPart extends Part implements IStatusbarService {
return this.element;
}
private showContextMenu(e: MouseEvent): void {
EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
this.contextMenuService.showContextMenu({
getAnchor: () => { return { x: event.posx, y: event.posy }; },
getActions: () => this.getContextMenuActions()
});
}
private getContextMenuActions(): IAction[] {
const actions: IAction[] = [];
// TODO@Ben collect more context menu actions
// .map(({ id, name, activityAction }) => (<IAction>{
// id,
// label: name || id,
// checked: this.isPinned(id),
// run: () => {
// if (this.isPinned(id)) {
// this.unpin(id);
// } else {
// this.pin(id, true);
// }
// }
// }));
// actions.push(new Separator());
actions.push(this.hideStatusBarAction);
return actions;
}
updateStyles(): void {
super.updateStyles();
@ -253,9 +296,9 @@ export class StatusbarPart extends Part implements IStatusbarService {
statusMessageEntry = this.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */);
showHandle = null;
}, delayBy);
let hideHandle: any;
// Dispose function takes care of timeouts and actual entry
let hideHandle: any;
const statusMessageDispose = {
dispose: () => {
if (showHandle) {

View file

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice } from 'vs/platform/notification/common/notification';
import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { Action } from 'vs/base/common/actions';
import { isErrorWithActions } from 'vs/base/common/errorsWithActions';
@ -15,10 +15,25 @@ import { localize } from 'vs/nls';
export interface INotificationsModel {
//
// Notifications as Toasts/Center
//
readonly notifications: INotificationViewItem[];
readonly onDidNotificationChange: Event<INotificationChangeEvent>;
notify(notification: INotification): INotificationHandle;
addNotification(notification: INotification): INotificationHandle;
//
// Notifications as Status
//
readonly statusMessage: IStatusMessageViewItem | undefined;
readonly onDidStatusMessageChange: Event<IStatusMessageChangeEvent>;
showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable;
}
export const enum NotificationChangeType {
@ -45,6 +60,29 @@ export interface INotificationChangeEvent {
kind: NotificationChangeType;
}
export const enum StatusMessageChangeType {
ADD,
REMOVE
}
export interface IStatusMessageViewItem {
message: string;
options?: IStatusMessageOptions;
}
export interface IStatusMessageChangeEvent {
/**
* The status message item this change is about.
*/
item: IStatusMessageViewItem;
/**
* The kind of status message change.
*/
kind: StatusMessageChangeType;
}
export class NotificationHandle implements INotificationHandle {
private readonly _onDidClose: Emitter<void> = new Emitter();
@ -90,13 +128,16 @@ export class NotificationsModel extends Disposable implements INotificationsMode
private readonly _onDidNotificationChange: Emitter<INotificationChangeEvent> = this._register(new Emitter<INotificationChangeEvent>());
get onDidNotificationChange(): Event<INotificationChangeEvent> { return this._onDidNotificationChange.event; }
private readonly _onDidStatusMessageChange: Emitter<IStatusMessageChangeEvent> = this._register(new Emitter<IStatusMessageChangeEvent>());
get onDidStatusMessageChange(): Event<IStatusMessageChangeEvent> { return this._onDidStatusMessageChange.event; }
private readonly _notifications: INotificationViewItem[] = [];
get notifications(): INotificationViewItem[] { return this._notifications; }
get notifications(): INotificationViewItem[] {
return this._notifications;
}
private _statusMessage: IStatusMessageViewItem | undefined;
get statusMessage(): IStatusMessageViewItem | undefined { return this._statusMessage; }
notify(notification: INotification): INotificationHandle {
addNotification(notification: INotification): INotificationHandle {
const item = this.createViewItem(notification);
if (!item) {
return NotificationsModel.NO_OP_NOTIFICATION; // return early if this is a no-op
@ -174,6 +215,26 @@ export class NotificationsModel extends Disposable implements INotificationsMode
return item;
}
showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable {
const item = StatusMessageViewItem.create(message, options);
if (!item) {
return Disposable.None;
}
// Remember as current status message and fire events
this._statusMessage = item;
this._onDidStatusMessageChange.fire({ kind: StatusMessageChangeType.ADD, item });
return toDisposable(() => {
// Only reset status message if the item is still the one we had remembered
if (this._statusMessage === item) {
this._statusMessage = undefined;
this._onDidStatusMessageChange.fire({ kind: StatusMessageChangeType.REMOVE, item });
}
});
}
}
export interface INotificationViewItem {
@ -621,4 +682,26 @@ export class ChoiceAction extends Action {
this._onDidRun.dispose();
}
}
class StatusMessageViewItem {
static create(notification: NotificationMessage, options?: IStatusMessageOptions): IStatusMessageViewItem | null {
if (!notification || isPromiseCanceledError(notification)) {
return null; // we need a message to show
}
let message: string | undefined;
if (notification instanceof Error) {
message = toErrorMessage(notification, false);
} else if (typeof notification === 'string') {
message = notification;
}
if (!message) {
return null; // we need a message to show
}
return { message, options };
}
}

View file

@ -186,7 +186,7 @@ suite('ConfigurationEditingService', () => {
test('do not notify error', () => {
instantiationService.stub(ITextFileService, 'isDirty', true);
const target = sinon.stub();
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: null, notify: null!, error: null!, info: null!, warn: null! });
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: null!, notify: null!, error: null!, info: null!, warn: null!, status: null! });
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true })
.then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'),
(error: ConfigurationEditingError) => {

View file

@ -28,7 +28,6 @@ import { IKeybindingItem, IKeybindingRule2, KeybindingWeight, KeybindingsRegistr
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Registry } from 'vs/platform/registry/common/platform';
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { keybindingsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
@ -277,12 +276,11 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
@ITelemetryService telemetryService: ITelemetryService,
@INotificationService notificationService: INotificationService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStatusbarService statusBarService: IStatusbarService,
@IConfigurationService configurationService: IConfigurationService,
@IWindowService private readonly windowService: IWindowService,
@IExtensionService extensionService: IExtensionService
) {
super(contextKeyService, commandService, telemetryService, notificationService, statusBarService);
super(contextKeyService, commandService, telemetryService, notificationService);
updateSchema();

View file

@ -3,15 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
import { INotificationsModel, NotificationsModel, ChoiceAction } from 'vs/workbench/common/notifications';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
export class NotificationService extends Disposable implements INotificationService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<INotificationService>;
private _model: INotificationsModel = this._register(new NotificationsModel());
@ -26,7 +27,7 @@ export class NotificationService extends Disposable implements INotificationServ
return;
}
this.model.notify({ severity: Severity.Info, message });
this.model.addNotification({ severity: Severity.Info, message });
}
warn(message: NotificationMessage | NotificationMessage[]): void {
@ -36,7 +37,7 @@ export class NotificationService extends Disposable implements INotificationServ
return;
}
this.model.notify({ severity: Severity.Warning, message });
this.model.addNotification({ severity: Severity.Warning, message });
}
error(message: NotificationMessage | NotificationMessage[]): void {
@ -46,11 +47,11 @@ export class NotificationService extends Disposable implements INotificationServ
return;
}
this.model.notify({ severity: Severity.Error, message });
this.model.addNotification({ severity: Severity.Error, message });
}
notify(notification: INotification): INotificationHandle {
return this.model.notify(notification);
return this.model.addNotification(notification);
}
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
@ -104,6 +105,10 @@ export class NotificationService extends Disposable implements INotificationServ
return handle;
}
status(message: NotificationMessage, options?: IStatusMessageOptions): IDisposable {
return this.model.showStatusMessage(message, options);
}
}
registerSingleton(INotificationService, NotificationService, true);

View file

@ -142,19 +142,19 @@ suite('Notifications', () => {
let item2Duplicate: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' };
let item3: INotification = { severity: Severity.Info, message: 'Info Message' };
let item1Handle = model.notify(item1);
let item1Handle = model.addNotification(item1);
assert.equal(lastEvent.item.severity, item1.severity);
assert.equal(lastEvent.item.message.value, item1.message);
assert.equal(lastEvent.index, 0);
assert.equal(lastEvent.kind, NotificationChangeType.ADD);
let item2Handle = model.notify(item2);
let item2Handle = model.addNotification(item2);
assert.equal(lastEvent.item.severity, item2.severity);
assert.equal(lastEvent.item.message.value, item2.message);
assert.equal(lastEvent.index, 0);
assert.equal(lastEvent.kind, NotificationChangeType.ADD);
model.notify(item3);
model.addNotification(item3);
assert.equal(lastEvent.item.severity, item3.severity);
assert.equal(lastEvent.item.message.value, item3.message);
assert.equal(lastEvent.index, 0);
@ -175,7 +175,7 @@ suite('Notifications', () => {
assert.equal(lastEvent.index, 2);
assert.equal(lastEvent.kind, NotificationChangeType.REMOVE);
model.notify(item2Duplicate);
model.addNotification(item2Duplicate);
assert.equal(model.notifications.length, 2);
assert.equal(lastEvent.item.severity, item2Duplicate.severity);
assert.equal(lastEvent.item.message.value, item2Duplicate.message);

View file

@ -6,9 +6,11 @@
import * as assert from 'assert';
import { MainThreadMessageService } from 'vs/workbench/api/browser/mainThreadMessageService';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions } from 'vs/platform/notification/common/notification';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { mock } from 'vs/workbench/test/electron-browser/api/mock';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
const emptyDialogService = new class implements IDialogService {
_serviceBrand: 'dialogService';
@ -30,7 +32,7 @@ const emptyCommandService: ICommandService = {
};
const emptyNotificationService = new class implements INotificationService {
_serviceBrand: 'notificiationService';
_serviceBrand: ServiceIdentifier<INotificationService>;
notify(...args: any[]): never {
throw new Error('not implemented');
}
@ -46,11 +48,13 @@ const emptyNotificationService = new class implements INotificationService {
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
throw new Error('not implemented');
}
status(message: string | Error, options?: IStatusMessageOptions): IDisposable {
return Disposable.None;
}
};
class EmptyNotificationService implements INotificationService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<INotificationService>;
constructor(private withNotify: (notification: INotification) => void) {
}
@ -72,6 +76,9 @@ class EmptyNotificationService implements INotificationService {
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle {
throw new Error('not implemented');
}
status(message: string, options?: IStatusMessageOptions): IDisposable {
return Disposable.None;
}
}
suite('ExtHostMessageService', function () {