Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/common/notifications.ts
3291 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties, IPromptChoiceWithMenu, NotificationPriority, INotificationSource, isNotificationSource, IStatusHandle } from '../../platform/notification/common/notification.js';
7
import { toErrorMessage, isErrorWithActions } from '../../base/common/errorMessage.js';
8
import { Event, Emitter } from '../../base/common/event.js';
9
import { Disposable } from '../../base/common/lifecycle.js';
10
import { isCancellationError } from '../../base/common/errors.js';
11
import { Action } from '../../base/common/actions.js';
12
import { equals } from '../../base/common/arrays.js';
13
import { parseLinkedText, LinkedText } from '../../base/common/linkedText.js';
14
import { mapsStrictEqualIgnoreOrder } from '../../base/common/map.js';
15
16
export interface INotificationsModel {
17
18
//#region Notifications as Toasts/Center
19
20
readonly notifications: INotificationViewItem[];
21
22
readonly onDidChangeNotification: Event<INotificationChangeEvent>;
23
readonly onDidChangeFilter: Event<Partial<INotificationsFilter>>;
24
25
addNotification(notification: INotification): INotificationHandle;
26
27
setFilter(filter: Partial<INotificationsFilter>): void;
28
29
//#endregion
30
31
32
//#region Notifications as Status
33
34
readonly statusMessage: IStatusMessageViewItem | undefined;
35
36
readonly onDidChangeStatusMessage: Event<IStatusMessageChangeEvent>;
37
38
showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle;
39
40
//#endregion
41
}
42
43
export const enum NotificationChangeType {
44
45
/**
46
* A notification was added.
47
*/
48
ADD,
49
50
/**
51
* A notification changed. Check `detail` property
52
* on the event for additional information.
53
*/
54
CHANGE,
55
56
/**
57
* A notification expanded or collapsed.
58
*/
59
EXPAND_COLLAPSE,
60
61
/**
62
* A notification was removed.
63
*/
64
REMOVE
65
}
66
67
export interface INotificationChangeEvent {
68
69
/**
70
* The index this notification has in the list of notifications.
71
*/
72
index: number;
73
74
/**
75
* The notification this change is about.
76
*/
77
item: INotificationViewItem;
78
79
/**
80
* The kind of notification change.
81
*/
82
kind: NotificationChangeType;
83
84
/**
85
* Additional detail about the item change. Only applies to
86
* `NotificationChangeType.CHANGE`.
87
*/
88
detail?: NotificationViewItemContentChangeKind;
89
}
90
91
export const enum StatusMessageChangeType {
92
ADD,
93
REMOVE
94
}
95
96
export interface IStatusMessageViewItem {
97
message: string;
98
options?: IStatusMessageOptions;
99
}
100
101
export interface IStatusMessageChangeEvent {
102
103
/**
104
* The status message item this change is about.
105
*/
106
item: IStatusMessageViewItem;
107
108
/**
109
* The kind of status message change.
110
*/
111
kind: StatusMessageChangeType;
112
}
113
114
export class NotificationHandle extends Disposable implements INotificationHandle {
115
116
private readonly _onDidClose = this._register(new Emitter<void>());
117
readonly onDidClose = this._onDidClose.event;
118
119
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
120
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
121
122
constructor(private readonly item: INotificationViewItem, private readonly onClose: (item: INotificationViewItem) => void) {
123
super();
124
125
this.registerListeners();
126
}
127
128
private registerListeners(): void {
129
130
// Visibility
131
this._register(this.item.onDidChangeVisibility(visible => this._onDidChangeVisibility.fire(visible)));
132
133
// Closing
134
Event.once(this.item.onDidClose)(() => {
135
this._onDidClose.fire();
136
137
this.dispose();
138
});
139
}
140
141
get progress(): INotificationProgress {
142
return this.item.progress;
143
}
144
145
updateSeverity(severity: Severity): void {
146
this.item.updateSeverity(severity);
147
}
148
149
updateMessage(message: NotificationMessage): void {
150
this.item.updateMessage(message);
151
}
152
153
updateActions(actions?: INotificationActions): void {
154
this.item.updateActions(actions);
155
}
156
157
close(): void {
158
this.onClose(this.item);
159
160
this.dispose();
161
}
162
}
163
164
export interface INotificationsFilter {
165
readonly global: NotificationsFilter;
166
readonly sources: Map<string, NotificationsFilter>;
167
}
168
169
export class NotificationsModel extends Disposable implements INotificationsModel {
170
171
private static readonly NO_OP_NOTIFICATION = new NoOpNotification();
172
173
private readonly _onDidChangeNotification = this._register(new Emitter<INotificationChangeEvent>());
174
readonly onDidChangeNotification = this._onDidChangeNotification.event;
175
176
private readonly _onDidChangeStatusMessage = this._register(new Emitter<IStatusMessageChangeEvent>());
177
readonly onDidChangeStatusMessage = this._onDidChangeStatusMessage.event;
178
179
private readonly _onDidChangeFilter = this._register(new Emitter<Partial<INotificationsFilter>>());
180
readonly onDidChangeFilter = this._onDidChangeFilter.event;
181
182
private readonly _notifications: INotificationViewItem[] = [];
183
get notifications(): INotificationViewItem[] { return this._notifications; }
184
185
private _statusMessage: IStatusMessageViewItem | undefined;
186
get statusMessage(): IStatusMessageViewItem | undefined { return this._statusMessage; }
187
188
private readonly filter = {
189
global: NotificationsFilter.OFF,
190
sources: new Map<string, NotificationsFilter>()
191
};
192
193
setFilter(filter: Partial<INotificationsFilter>): void {
194
let globalChanged = false;
195
if (typeof filter.global === 'number') {
196
globalChanged = this.filter.global !== filter.global;
197
this.filter.global = filter.global;
198
}
199
200
let sourcesChanged = false;
201
if (filter.sources) {
202
sourcesChanged = !mapsStrictEqualIgnoreOrder(this.filter.sources, filter.sources);
203
this.filter.sources = filter.sources;
204
}
205
206
if (globalChanged || sourcesChanged) {
207
this._onDidChangeFilter.fire({
208
global: globalChanged ? filter.global : undefined,
209
sources: sourcesChanged ? filter.sources : undefined
210
});
211
}
212
}
213
214
addNotification(notification: INotification): INotificationHandle {
215
const item = this.createViewItem(notification);
216
if (!item) {
217
return NotificationsModel.NO_OP_NOTIFICATION; // return early if this is a no-op
218
}
219
220
// Deduplicate
221
const duplicate = this.findNotification(item);
222
duplicate?.close();
223
224
// Add to list as first entry
225
this._notifications.splice(0, 0, item);
226
227
// Events
228
this._onDidChangeNotification.fire({ item, index: 0, kind: NotificationChangeType.ADD });
229
230
// Wrap into handle
231
return new NotificationHandle(item, item => this.onClose(item));
232
}
233
234
private onClose(item: INotificationViewItem): void {
235
const liveItem = this.findNotification(item);
236
if (liveItem && liveItem !== item) {
237
liveItem.close(); // item could have been replaced with another one, make sure to close the live item
238
} else {
239
item.close(); // otherwise just close the item that was passed in
240
}
241
}
242
243
private findNotification(item: INotificationViewItem): INotificationViewItem | undefined {
244
return this._notifications.find(notification => notification.equals(item));
245
}
246
247
private createViewItem(notification: INotification): INotificationViewItem | undefined {
248
const item = NotificationViewItem.create(notification, this.filter);
249
if (!item) {
250
return undefined;
251
}
252
253
// Item Events
254
const fireNotificationChangeEvent = (kind: NotificationChangeType, detail?: NotificationViewItemContentChangeKind) => {
255
const index = this._notifications.indexOf(item);
256
if (index >= 0) {
257
this._onDidChangeNotification.fire({ item, index, kind, detail });
258
}
259
};
260
261
const itemExpansionChangeListener = item.onDidChangeExpansion(() => fireNotificationChangeEvent(NotificationChangeType.EXPAND_COLLAPSE));
262
const itemContentChangeListener = item.onDidChangeContent(e => fireNotificationChangeEvent(NotificationChangeType.CHANGE, e.kind));
263
264
Event.once(item.onDidClose)(() => {
265
itemExpansionChangeListener.dispose();
266
itemContentChangeListener.dispose();
267
268
const index = this._notifications.indexOf(item);
269
if (index >= 0) {
270
this._notifications.splice(index, 1);
271
this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.REMOVE });
272
}
273
});
274
275
return item;
276
}
277
278
showStatusMessage(message: NotificationMessage, options?: IStatusMessageOptions): IStatusHandle {
279
const item = StatusMessageViewItem.create(message, options);
280
if (!item) {
281
return { close: () => { } };
282
}
283
284
this._statusMessage = item;
285
this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.ADD, item });
286
287
return {
288
close: () => {
289
if (this._statusMessage === item) {
290
this._statusMessage = undefined;
291
this._onDidChangeStatusMessage.fire({ kind: StatusMessageChangeType.REMOVE, item });
292
}
293
}
294
};
295
}
296
}
297
298
export interface INotificationViewItem {
299
readonly id: string | undefined;
300
readonly severity: Severity;
301
readonly sticky: boolean;
302
readonly priority: NotificationPriority;
303
readonly message: INotificationMessage;
304
readonly source: string | undefined;
305
readonly sourceId: string | undefined;
306
readonly actions: INotificationActions | undefined;
307
readonly progress: INotificationViewItemProgress;
308
309
readonly expanded: boolean;
310
readonly visible: boolean;
311
readonly canCollapse: boolean;
312
readonly hasProgress: boolean;
313
314
readonly onDidChangeExpansion: Event<void>;
315
readonly onDidChangeVisibility: Event<boolean>;
316
readonly onDidChangeContent: Event<INotificationViewItemContentChangeEvent>;
317
readonly onDidClose: Event<void>;
318
319
expand(): void;
320
collapse(skipEvents?: boolean): void;
321
toggle(): void;
322
323
updateSeverity(severity: Severity): void;
324
updateMessage(message: NotificationMessage): void;
325
updateActions(actions?: INotificationActions): void;
326
327
updateVisibility(visible: boolean): void;
328
329
close(): void;
330
331
equals(item: INotificationViewItem): boolean;
332
}
333
334
export function isNotificationViewItem(obj: unknown): obj is INotificationViewItem {
335
return obj instanceof NotificationViewItem;
336
}
337
338
export const enum NotificationViewItemContentChangeKind {
339
SEVERITY,
340
MESSAGE,
341
ACTIONS,
342
PROGRESS
343
}
344
345
export interface INotificationViewItemContentChangeEvent {
346
kind: NotificationViewItemContentChangeKind;
347
}
348
349
export interface INotificationViewItemProgressState {
350
infinite?: boolean;
351
total?: number;
352
worked?: number;
353
done?: boolean;
354
}
355
356
export interface INotificationViewItemProgress extends INotificationProgress {
357
readonly state: INotificationViewItemProgressState;
358
359
dispose(): void;
360
}
361
362
export class NotificationViewItemProgress extends Disposable implements INotificationViewItemProgress {
363
private readonly _state: INotificationViewItemProgressState;
364
365
private readonly _onDidChange = this._register(new Emitter<void>());
366
readonly onDidChange = this._onDidChange.event;
367
368
constructor() {
369
super();
370
371
this._state = Object.create(null);
372
}
373
374
get state(): INotificationViewItemProgressState {
375
return this._state;
376
}
377
378
infinite(): void {
379
if (this._state.infinite) {
380
return;
381
}
382
383
this._state.infinite = true;
384
385
this._state.total = undefined;
386
this._state.worked = undefined;
387
this._state.done = undefined;
388
389
this._onDidChange.fire();
390
}
391
392
done(): void {
393
if (this._state.done) {
394
return;
395
}
396
397
this._state.done = true;
398
399
this._state.infinite = undefined;
400
this._state.total = undefined;
401
this._state.worked = undefined;
402
403
this._onDidChange.fire();
404
}
405
406
total(value: number): void {
407
if (this._state.total === value) {
408
return;
409
}
410
411
this._state.total = value;
412
413
this._state.infinite = undefined;
414
this._state.done = undefined;
415
416
this._onDidChange.fire();
417
}
418
419
worked(value: number): void {
420
if (typeof this._state.worked === 'number') {
421
this._state.worked += value;
422
} else {
423
this._state.worked = value;
424
}
425
426
this._state.infinite = undefined;
427
this._state.done = undefined;
428
429
this._onDidChange.fire();
430
}
431
}
432
433
export interface IMessageLink {
434
href: string;
435
name: string;
436
title: string;
437
offset: number;
438
length: number;
439
}
440
441
export interface INotificationMessage {
442
raw: string;
443
original: NotificationMessage;
444
linkedText: LinkedText;
445
}
446
447
export class NotificationViewItem extends Disposable implements INotificationViewItem {
448
449
private static readonly MAX_MESSAGE_LENGTH = 1000;
450
451
private _expanded: boolean | undefined;
452
private _visible: boolean = false;
453
454
private _actions: INotificationActions | undefined;
455
private _progress: NotificationViewItemProgress | undefined;
456
457
private readonly _onDidChangeExpansion = this._register(new Emitter<void>());
458
readonly onDidChangeExpansion = this._onDidChangeExpansion.event;
459
460
private readonly _onDidClose = this._register(new Emitter<void>());
461
readonly onDidClose = this._onDidClose.event;
462
463
private readonly _onDidChangeContent = this._register(new Emitter<INotificationViewItemContentChangeEvent>());
464
readonly onDidChangeContent = this._onDidChangeContent.event;
465
466
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
467
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
468
469
static create(notification: INotification, filter: INotificationsFilter): INotificationViewItem | undefined {
470
if (!notification || !notification.message || isCancellationError(notification.message)) {
471
return undefined; // we need a message to show
472
}
473
474
let severity: Severity;
475
if (typeof notification.severity === 'number') {
476
severity = notification.severity;
477
} else {
478
severity = Severity.Info;
479
}
480
481
const message = NotificationViewItem.parseNotificationMessage(notification.message);
482
if (!message) {
483
return undefined; // we need a message to show
484
}
485
486
let actions: INotificationActions | undefined;
487
if (notification.actions) {
488
actions = notification.actions;
489
} else if (isErrorWithActions(notification.message)) {
490
actions = { primary: notification.message.actions };
491
}
492
493
let priority = notification.priority ?? NotificationPriority.DEFAULT;
494
if ((priority === NotificationPriority.DEFAULT || priority === NotificationPriority.OPTIONAL) && severity !== Severity.Error) {
495
if (filter.global === NotificationsFilter.ERROR) {
496
priority = NotificationPriority.SILENT; // filtered globally
497
} else if (isNotificationSource(notification.source) && filter.sources.get(notification.source.id) === NotificationsFilter.ERROR) {
498
priority = NotificationPriority.SILENT; // filtered by source
499
}
500
}
501
502
return new NotificationViewItem(notification.id, severity, notification.sticky, priority, message, notification.source, notification.progress, actions);
503
}
504
505
private static parseNotificationMessage(input: NotificationMessage): INotificationMessage | undefined {
506
let message: string | undefined;
507
if (input instanceof Error) {
508
message = toErrorMessage(input, false);
509
} else if (typeof input === 'string') {
510
message = input;
511
}
512
513
if (!message) {
514
return undefined; // we need a message to show
515
}
516
517
const raw = message;
518
519
// Make sure message is in the limits
520
if (message.length > NotificationViewItem.MAX_MESSAGE_LENGTH) {
521
message = `${message.substr(0, NotificationViewItem.MAX_MESSAGE_LENGTH)}...`;
522
}
523
524
// Remove newlines from messages as we do not support that and it makes link parsing hard
525
message = message.replace(/(\r\n|\n|\r)/gm, ' ').trim();
526
527
// Parse Links
528
const linkedText = parseLinkedText(message);
529
530
return { raw, linkedText, original: input };
531
}
532
533
private constructor(
534
readonly id: string | undefined,
535
private _severity: Severity,
536
private _sticky: boolean | undefined,
537
private _priority: NotificationPriority,
538
private _message: INotificationMessage,
539
private _source: string | INotificationSource | undefined,
540
progress: INotificationProgressProperties | undefined,
541
actions?: INotificationActions
542
) {
543
super();
544
545
if (progress) {
546
this.setProgress(progress);
547
}
548
549
this.setActions(actions);
550
}
551
552
private setProgress(progress: INotificationProgressProperties): void {
553
if (progress.infinite) {
554
this.progress.infinite();
555
} else if (progress.total) {
556
this.progress.total(progress.total);
557
558
if (progress.worked) {
559
this.progress.worked(progress.worked);
560
}
561
}
562
}
563
564
private setActions(actions: INotificationActions = { primary: [], secondary: [] }): void {
565
this._actions = {
566
primary: Array.isArray(actions.primary) ? actions.primary : [],
567
secondary: Array.isArray(actions.secondary) ? actions.secondary : []
568
};
569
570
this._expanded = actions.primary && actions.primary.length > 0;
571
}
572
573
get canCollapse(): boolean {
574
return !this.hasActions;
575
}
576
577
get expanded(): boolean {
578
return !!this._expanded;
579
}
580
581
get severity(): Severity {
582
return this._severity;
583
}
584
585
get sticky(): boolean {
586
if (this._sticky) {
587
return true; // explicitly sticky
588
}
589
590
const hasActions = this.hasActions;
591
if (
592
(hasActions && this._severity === Severity.Error) || // notification errors with actions are sticky
593
(!hasActions && this._expanded) || // notifications that got expanded are sticky
594
(this._progress && !this._progress.state.done) // notifications with running progress are sticky
595
) {
596
return true;
597
}
598
599
return false; // not sticky
600
}
601
602
get priority(): NotificationPriority {
603
return this._priority;
604
}
605
606
private get hasActions(): boolean {
607
if (!this._actions) {
608
return false;
609
}
610
611
if (!this._actions.primary) {
612
return false;
613
}
614
615
return this._actions.primary.length > 0;
616
}
617
618
get hasProgress(): boolean {
619
return !!this._progress;
620
}
621
622
get progress(): INotificationViewItemProgress {
623
if (!this._progress) {
624
this._progress = this._register(new NotificationViewItemProgress());
625
this._register(this._progress.onDidChange(() => this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.PROGRESS })));
626
}
627
628
return this._progress;
629
}
630
631
get message(): INotificationMessage {
632
return this._message;
633
}
634
635
get source(): string | undefined {
636
return typeof this._source === 'string' ? this._source : (this._source ? this._source.label : undefined);
637
}
638
639
get sourceId(): string | undefined {
640
return (this._source && typeof this._source !== 'string' && 'id' in this._source) ? this._source.id : undefined;
641
}
642
643
get actions(): INotificationActions | undefined {
644
return this._actions;
645
}
646
647
get visible(): boolean {
648
return this._visible;
649
}
650
651
updateSeverity(severity: Severity): void {
652
if (severity === this._severity) {
653
return;
654
}
655
656
this._severity = severity;
657
this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.SEVERITY });
658
}
659
660
updateMessage(input: NotificationMessage): void {
661
const message = NotificationViewItem.parseNotificationMessage(input);
662
if (!message || message.raw === this._message.raw) {
663
return;
664
}
665
666
this._message = message;
667
this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.MESSAGE });
668
}
669
670
updateActions(actions?: INotificationActions): void {
671
this.setActions(actions);
672
this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.ACTIONS });
673
}
674
675
updateVisibility(visible: boolean): void {
676
if (this._visible !== visible) {
677
this._visible = visible;
678
679
this._onDidChangeVisibility.fire(visible);
680
}
681
}
682
683
expand(): void {
684
if (this._expanded || !this.canCollapse) {
685
return;
686
}
687
688
this._expanded = true;
689
this._onDidChangeExpansion.fire();
690
}
691
692
collapse(skipEvents?: boolean): void {
693
if (!this._expanded || !this.canCollapse) {
694
return;
695
}
696
697
this._expanded = false;
698
699
if (!skipEvents) {
700
this._onDidChangeExpansion.fire();
701
}
702
}
703
704
toggle(): void {
705
if (this._expanded) {
706
this.collapse();
707
} else {
708
this.expand();
709
}
710
}
711
712
close(): void {
713
this._onDidClose.fire();
714
715
this.dispose();
716
}
717
718
equals(other: INotificationViewItem): boolean {
719
if (this.hasProgress || other.hasProgress) {
720
return false;
721
}
722
723
if (typeof this.id === 'string' || typeof other.id === 'string') {
724
return this.id === other.id;
725
}
726
727
if (typeof this._source === 'object') {
728
if (this._source.label !== other.source || this._source.id !== other.sourceId) {
729
return false;
730
}
731
} else if (this._source !== other.source) {
732
return false;
733
}
734
735
if (this._message.raw !== other.message.raw) {
736
return false;
737
}
738
739
const primaryActions = (this._actions && this._actions.primary) || [];
740
const otherPrimaryActions = (other.actions && other.actions.primary) || [];
741
return equals(primaryActions, otherPrimaryActions, (action, otherAction) => (action.id + action.label) === (otherAction.id + otherAction.label));
742
}
743
}
744
745
export class ChoiceAction extends Action {
746
747
private readonly _onDidRun = this._register(new Emitter<void>());
748
readonly onDidRun = this._onDidRun.event;
749
750
private readonly _keepOpen: boolean;
751
private readonly _menu: ChoiceAction[] | undefined;
752
753
constructor(id: string, choice: IPromptChoice) {
754
super(id, choice.label, undefined, true, async () => {
755
756
// Pass to runner
757
choice.run();
758
759
// Emit Event
760
this._onDidRun.fire();
761
});
762
763
this._keepOpen = !!choice.keepOpen;
764
this._menu = !choice.isSecondary && (<IPromptChoiceWithMenu>choice).menu ? (<IPromptChoiceWithMenu>choice).menu.map((c, index) => new ChoiceAction(`${id}.${index}`, c)) : undefined;
765
}
766
767
get menu(): ChoiceAction[] | undefined {
768
return this._menu;
769
}
770
771
get keepOpen(): boolean {
772
return this._keepOpen;
773
}
774
}
775
776
class StatusMessageViewItem {
777
778
static create(notification: NotificationMessage, options?: IStatusMessageOptions): IStatusMessageViewItem | undefined {
779
if (!notification || isCancellationError(notification)) {
780
return undefined; // we need a message to show
781
}
782
783
let message: string | undefined;
784
if (notification instanceof Error) {
785
message = toErrorMessage(notification, false);
786
} else if (typeof notification === 'string') {
787
message = notification;
788
}
789
790
if (!message) {
791
return undefined; // we need a message to show
792
}
793
794
return { message, options };
795
}
796
}
797
798