src/vs/workbench/common/notifications.ts
818 LOC · 688 covered · 130 uncovered · 125 ranges · 6 concepts · 6 introducers · 3 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
notifications.ts ×58
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties, IPromptChoiceWithMenu, NotificationPriority, INotificationSource, isNotificationSource, IStatusHandle } from '../../platform/notification/common/notification.js';
import { toErrorMessage, isErrorWithActions } from '../../base/common/errorMessage.js';
import { Event, Emitter } from '../../base/common/event.js';
import { Disposable } from '../../base/common/lifecycle.js';
import { isCancellationError } from '../../base/common/errors.js';
import { Action } from '../../base/common/actions.js';
import { equals } from '../../base/common/arrays.js';
import { parseLinkedText, LinkedText } from '../../base/common/linkedText.js';
import { mapsStrictEqualIgnoreOrder } from '../../base/common/map.js';
import { IConfigurationService } from '../../platform/configuration/common/configuration.js';
export interface INotificationsModel {
//#region Notifications as Toasts/Center
readonly notifications: INotificationViewItem[];
readonly onDidChangeNotification: Event<INotificationChangeEvent>;
readonly onDidChangeFilter: Event<Partial<INotificationsFilter>>;
addNotification(notification: INotification): INotificationHandle;
setFilter(filter: Partial<INotificationsFilter>): void;
//#endregion
//#region Notifications as Status
readonly statusMessage: IStatusMessageViewItem | undefined;
readonly onDidChangeStatusMessage: Event<IStatusMessageChangeEvent>;
showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle;
//#endregion
}
export const enum NotificationChangeType {
/**
* A notification was added.
*/
ADD,
/**
* A notification changed. Check `detail` property
* on the event for additional information.
*/
CHANGE,
/**
* A notification expanded or collapsed.
*/
EXPAND_COLLAPSE,
/**
* A notification was removed.
*/
REMOVE
}
export interface INotificationChangeEvent {
/**
* The index this notification has in the list of notifications.
*/
index: number;
/**
* The notification this change is about.
*/
item: INotificationViewItem;
/**
* The kind of notification change.
*/
kind: NotificationChangeType;
/**
* Additional detail about the item change. Only applies to
* `NotificationChangeType.CHANGE`.
*/
detail?: NotificationViewItemContentChangeKind;
}
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 extends Disposable implements INotificationHandle {
private readonly _onDidClose = this._register(new Emitter<void>());
readonly onDidClose = this._onDidClose.event;
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
constructor(private readonly item: INotificationViewItem, private readonly onClose: (item: INotificationViewItem) => void) {
this.registerListeners();
}
private registerListeners(): void {
// Visibility
this._register(this.item.onDidChangeVisibility(visible => this._onDidChangeVisibility.fire(visible)));
// Closing
Event.once(this.item.onDidClose)(() => {
this._onDidClose.fire();
this.dispose();
});
}
get progress(): INotificationProgress {
}
updateSeverity(severity: Severity): void {
}
updateMessage(message: NotificationMessage): void {
}
updateActions(actions?: INotificationActions): void {
}
close(): void {
this.dispose();
}
export interface INotificationsFilter {
readonly global: NotificationsFilter;
readonly sources: Map<string, NotificationsFilter>;
}
export class NotificationsModel extends Disposable implements INotificationsModel {
private static readonly NO_OP_NOTIFICATION = new NoOpNotification();
private readonly _onDidChangeNotification = this._register(new Emitter<INotificationChangeEvent>());
readonly onDidChangeNotification = this._onDidChangeNotification.event;
private readonly _onDidChangeStatusMessage = this._register(new Emitter<IStatusMessageChangeEvent>());
readonly onDidChangeStatusMessage = this._onDidChangeStatusMessage.event;
private readonly _onDidChangeFilter = this._register(new Emitter<Partial<INotificationsFilter>>());
readonly onDidChangeFilter = this._onDidChangeFilter.event;
private readonly _notifications: INotificationViewItem[] = [];
get notifications(): INotificationViewItem[] { return this._notifications; }
private _statusMessage: IStatusMessageViewItem | undefined;
get statusMessage(): IStatusMessageViewItem | undefined { return this._statusMessage; }
private readonly filter = {
global: NotificationsFilter.OFF,
sources: new Map<string, NotificationsFilter>()
};
setFilter(filter: Partial<INotificationsFilter>): void {
let globalChanged = false;
if (typeof filter.global === 'number') {
globalChanged = this.filter.global !== filter.global;
this.filter.global = filter.global;
}
let sourcesChanged = false;
if (filter.sources) {
sourcesChanged = !mapsStrictEqualIgnoreOrder(this.filter.sources, filter.sources);
this.filter.sources = filter.sources;
}
if (globalChanged || sourcesChanged) {
this._onDidChangeFilter.fire({
global: globalChanged ? filter.global : undefined,
sources: sourcesChanged ? filter.sources : undefined
});
}
}
addNotification(notification: INotification): INotificationHandle {
if (!item) {
return NotificationsModel.NO_OP_NOTIFICATION; // return early if this is a no-op
}
// Deduplicate
const duplicate = this.findNotification(item);
duplicate?.close();
// Add to list as first entry
this._notifications.splice(0, 0, item);
// Events
this._onDidChangeNotification.fire({ item, index: 0, kind: NotificationChangeType.ADD });
// Wrap into handle
return new NotificationHandle(item, item => this.onClose(item));
}
private onClose(item: INotificationViewItem): void {
if (liveItem && liveItem !== item) {
liveItem.close(); // item could have been replaced with another one, make sure to close the live item
} else {
item.close(); // otherwise just close the item that was passed in
}
}
private findNotification(item: INotificationViewItem): INotificationViewItem | undefined {
return this._notifications.find(notification => notification.equals(item));
notifications.ts ×23
}
private createViewItem(notification: INotification): INotificationViewItem | undefined {
if (!item) {
return undefined;
}
// Item Events
const fireNotificationChangeEvent = (kind: NotificationChangeType, detail?: NotificationViewItemContentChangeKind) => {
const index = this._notifications.indexOf(item);
if (index >= 0) {
this._onDidChangeNotification.fire({ item, index, kind, detail });
}
};
const itemExpansionChangeListener = item.onDidChangeExpansion(() => fireNotificationChangeEvent(NotificationChangeType.EXPAND_COLLAPSE));
const itemContentChangeListener = item.onDidChangeContent(e => fireNotificationChangeEvent(NotificationChangeType.CHANGE, e.kind));
Event.once(item.onDidClose)(() => {
itemExpansionChangeListener.dispose();
itemContentChangeListener.dispose();
const index = this._notifications.indexOf(item);
if (index >= 0) {
this._notifications.splice(index, 1);
this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.REMOVE });
}
});
return item;
}
showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle {
if (!item) {
return { close: () => { } };
}
this._statusMessage = item;
this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.ADD, item });
return {
close: () => {
if (this._statusMessage === item) {
this._statusMessage = undefined;
this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item });
}
}
};
}
export interface INotificationViewItem {
readonly id: string | undefined;
readonly severity: Severity;
readonly sticky: boolean;
readonly priority: NotificationPriority;
readonly message: INotificationMessage;
readonly source: string | undefined;
readonly sourceId: string | undefined;
readonly actions: INotificationActions | undefined;
readonly progress: INotificationViewItemProgress;
readonly expanded: boolean;
readonly visible: boolean;
readonly canCollapse: boolean;
readonly hasProgress: boolean;
readonly onDidChangeExpansion: Event<void>;
readonly onDidChangeVisibility: Event<boolean>;
readonly onDidChangeContent: Event<INotificationViewItemContentChangeEvent>;
readonly onDidClose: Event<void>;
expand(): void;
collapse(skipEvents?: boolean): void;
toggle(): void;
updateSeverity(severity: Severity): void;
updateMessage(message: NotificationMessage): void;
updateActions(actions?: INotificationActions): void;
updateVisibility(visible: boolean): void;
close(): void;
equals(item: INotificationViewItem): boolean;
}
export function isNotificationViewItem(obj: unknown): obj is INotificationViewItem {
return obj instanceof NotificationViewItem;
}
export const enum NotificationViewItemContentChangeKind {
SEVERITY,
MESSAGE,
ACTIONS,
PROGRESS
}
export interface INotificationViewItemContentChangeEvent {
kind: NotificationViewItemContentChangeKind;
}
export interface INotificationViewItemProgressState {
infinite?: boolean;
total?: number;
worked?: number;
done?: boolean;
}
export interface INotificationViewItemProgress extends INotificationProgress {
readonly state: INotificationViewItemProgressState;
dispose(): void;
}
export class NotificationViewItemProgress extends Disposable implements INotificationViewItemProgress {
private readonly _state: INotificationViewItemProgressState;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
constructor() {
this._state = Object.create(null);
}
get state(): INotificationViewItemProgressState {
return this._state;
}
infinite(): void {
return;
}
this._state.infinite = true;
this._state.total = undefined;
this._state.worked = undefined;
this._state.done = undefined;
this._onDidChange.fire();
}
done(): void {
return;
}
this._state.done = true;
this._state.infinite = undefined;
this._state.total = undefined;
this._state.worked = undefined;
this._onDidChange.fire();
}
total(value: number): void {
if (this._state.total === value) {
return;
}
this._state.total = value;
this._state.infinite = undefined;
this._state.done = undefined;
this._onDidChange.fire();
}
worked(value: number): void {
if (typeof this._state.worked === 'number') {
this._state.worked += value;
} else {
this._state.worked = value;
}
this._state.infinite = undefined;
this._state.done = undefined;
this._onDidChange.fire();
}
export interface IMessageLink {
href: string;
name: string;
title: string;
offset: number;
length: number;
}
export interface INotificationMessage {
raw: string;
original: NotificationMessage;
linkedText: LinkedText;
}
export class NotificationViewItem extends Disposable implements INotificationViewItem {
private static readonly MAX_MESSAGE_LENGTH = 1000;
private _expanded: boolean | undefined;
private _visible: boolean = false;
private _actions: INotificationActions | undefined;
private _progress: NotificationViewItemProgress | undefined;
private readonly _onDidChangeExpansion = this._register(new Emitter<void>());
readonly onDidChangeExpansion = this._onDidChangeExpansion.event;
private readonly _onDidClose = this._register(new Emitter<void>());
readonly onDidClose = this._onDidClose.event;
private readonly _onDidChangeContent = this._register(new Emitter<INotificationViewItemContentChangeEvent>());
readonly onDidChangeContent = this._onDidChangeContent.event;
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
static create(notification: INotification, filter: INotificationsFilter): INotificationViewItem | undefined {
if (!notification?.message || isCancellationError(notification.message)) {
}
let severity: Severity;
if (typeof notification.severity === 'number') {
severity = notification.severity;
} else {
severity = Severity.Info;
}
const message = NotificationViewItem.parseNotificationMessage(notification.message);
if (!message) {
return undefined; // we need a message to show
}
let actions: INotificationActions | undefined;
if (notification.actions) {
}
let priority = notification.priority ?? NotificationPriority.DEFAULT;
if ((priority === NotificationPriority.DEFAULT || priority === NotificationPriority.OPTIONAL) && severity !== Severity.Error) {
} else if (isNotificationSource(notification.source) && filter.sources.get(notification.source.id) === NotificationsFilter.ERROR) {
notifications.ts ×25
}
return new NotificationViewItem(notification.id, severity, notification.sticky, priority, message, notification.source, notification.progress, actions);
}
private static parseNotificationMessage(input: NotificationMessage): INotificationMessage | undefined {
let message: string | undefined;
if (input instanceof Error) {
message = input;
}
if (!message) {
return undefined; // we need a message to show
}
const raw = message;
// Make sure message is in the limits
if (message.length > NotificationViewItem.MAX_MESSAGE_LENGTH) {
message = `${message.substr(0, NotificationViewItem.MAX_MESSAGE_LENGTH)}...`;
}
// Remove newlines from messages as we do not support that and it makes link parsing hard
message = message.replace(/(\r\n|\n|\r)/gm, ' ').trim();
// Parse Links
const linkedText = parseLinkedText(message);
return { raw, linkedText, original: input };
}
private constructor(
readonly id: string | undefined,
private _severity: Severity,
private _sticky: boolean | undefined,
private _priority: NotificationPriority,
private _message: INotificationMessage,
private _source: string | INotificationSource | undefined,
progress: INotificationProgressProperties | undefined,
actions?: INotificationActions
) {
super();
if (progress) {
}
this.setActions(actions);
}
private setProgress(progress: INotificationProgressProperties): void {
this.progress.infinite();
} else if (progress.total) {
this.progress.total(progress.total);
if (progress.worked) {
this.progress.worked(progress.worked);
}
}
private setActions(actions: INotificationActions = { primary: [], secondary: [] }): void {
this._actions = {
primary: Array.isArray(actions.primary) ? actions.primary : [],
secondary: Array.isArray(actions.secondary) ? actions.secondary : []
};
this._expanded = actions.primary && actions.primary.length > 0;
}
get canCollapse(): boolean {
}
get expanded(): boolean {
}
get severity(): Severity {
}
get sticky(): boolean {
if (this._sticky) {
return true; // explicitly sticky
}
const hasActions = this.hasActions;
if (
(hasActions && this._severity === Severity.Error) || // notification errors with actions are sticky
(!hasActions && this._expanded) || // notifications that got expanded are sticky
(this._progress && !this._progress.state.done) // notifications with running progress are sticky
) {
return true;
}
return false; // not sticky
}
get priority(): NotificationPriority {
}
private get hasActions(): boolean {
return false;
}
if (!this._actions.primary) {
return false;
}
return this._actions.primary.length > 0;
}
get hasProgress(): boolean {
}
get progress(): INotificationViewItemProgress {
this._progress = this._register(new NotificationViewItemProgress());
this._register(this._progress.onDidChange(() => this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.PROGRESS })));
}
return this._progress;
}
get message(): INotificationMessage {
}
get source(): string | undefined {
return typeof this._source === 'string' ? this._source : (this._source ? this._source.label : undefined);
notifications.ts ×25
}
get sourceId(): string | undefined {
return (this._source && typeof this._source !== 'string' && 'id' in this._source) ? this._source.id : undefined;
}
get actions(): INotificationActions | undefined {
}
get visible(): boolean {
return this._visible;
}
updateSeverity(severity: Severity): void {
if (severity === this._severity) {
}
this._severity = severity;
this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.SEVERITY });
updateMessage(input: NotificationMessage): void {
const message = NotificationViewItem.parseNotificationMessage(input);
if (!message || message.raw === this._message.raw) {
}
this._message = message;
this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.MESSAGE });
updateActions(actions?: INotificationActions): void {
this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.ACTIONS });
}
updateVisibility(visible: boolean): void {
this._visible = visible;
this._onDidChangeVisibility.fire(visible);
}
}
expand(): void {
}
this._expanded = true;
this._onDidChangeExpansion.fire();
}
collapse(skipEvents?: boolean): void {
return;
}
this._expanded = false;
if (!skipEvents) {
this._onDidChangeExpansion.fire();
}
}
toggle(): void {
if (this._expanded) {
this.collapse();
} else {
this.expand();
}
}
close(): void {
this._onDidClose.fire();
this.dispose();
}
equals(other: INotificationViewItem): boolean {
}
if (typeof this.id === 'string' || typeof other.id === 'string') {
}
if (typeof this._source === 'object') {
if (this._source.label !== other.source || this._source.id !== other.sourceId) {
return false;
}
return false;
}
if (this._message.raw !== other.message.raw) {
}
const primaryActions = this._actions?.primary || [];
const otherPrimaryActions = other.actions?.primary || [];
return equals(primaryActions, otherPrimaryActions, (action, otherAction) => (action.id + action.label) === (otherAction.id + otherAction.label));
}
export class ChoiceAction extends Action {
private readonly _onDidRun = this._register(new Emitter<void>());
readonly onDidRun = this._onDidRun.event;
private readonly _keepOpen: boolean;
private readonly _menu: ChoiceAction[] | undefined;
constructor(id: string, choice: IPromptChoice) {
super(id, choice.label, undefined, true, async () => {
// Pass to runner
choice.run();
// Emit Event
this._onDidRun.fire();
});
this._keepOpen = !!choice.keepOpen;
this._menu = !choice.isSecondary && (<IPromptChoiceWithMenu>choice).menu ? (<IPromptChoiceWithMenu>choice).menu.map((c, index) => new ChoiceAction(`${id}.${index}`, c)) : undefined;
}
get menu(): ChoiceAction[] | undefined {
return this._menu;
}
get keepOpen(): boolean {
return this._keepOpen;
}
class StatusMessageViewItem {
static create(notification: NotificationMessage, options?: IStatusMessageOptions): IStatusMessageViewItem | undefined {
return undefined; // we need a message to show
}
let message: string | undefined;
if (notification instanceof Error) {
message = toErrorMessage(notification, false);
message = notification;
}
if (!message) {
return undefined; // we need a message to show
}
return { message, options };
}
export const enum NotificationsSettings {
NOTIFICATIONS_POSITION = 'workbench.notifications.position',
NOTIFICATIONS_BUTTON = 'workbench.notifications.showInTitleBar'
}
export const enum NotificationsPosition {
BOTTOM_RIGHT = 'bottom-right',
BOTTOM_LEFT = 'bottom-left',
TOP_RIGHT = 'top-right'
}
export function getNotificationsPosition(configurationService: IConfigurationService): NotificationsPosition {
const position = configurationService.getValue<NotificationsPosition>(NotificationsSettings.NOTIFICATIONS_POSITION);
if (position === NotificationsPosition.BOTTOM_LEFT || position === NotificationsPosition.TOP_RIGHT) {
return position;
}
return NotificationsPosition.BOTTOM_RIGHT;
}