From 7d6c5a0174e8b1e7fbb01bf7c419817a84b62113 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Apr 2021 12:41:16 -0700 Subject: [PATCH 1/6] Create status list Part of #120583 --- .../terminal/browser/terminalStatusList.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts new file mode 100644 index 0000000000000..a0172d97cb4a0 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import Severity from 'vs/base/common/severity'; + +export interface ITerminalStatus { + id: string; + severity: Severity; +} + +export interface ITerminalStatusList { + /** Gets the most recent, highest severity status. */ + readonly primary: ITerminalStatus | undefined; + /** Gets all active statues. */ + readonly statuses: ITerminalStatus[]; + + readonly onDidAddStatus: Event; + readonly onDidRemoveStatus: Event; + + add(status: ITerminalStatus, duration?: number): void; + remove(status: ITerminalStatus): void; + remove(statusId: string): void; + toggle(status: ITerminalStatus, value: boolean): void; +} + +export class TerminalStatusList implements ITerminalStatusList { + private readonly _statuses: Map = new Map(); + private readonly _statusTimeouts: Map = new Map(); + + private readonly _onDidAddStatus = new Emitter(); + get onDidAddStatus(): Event { return this._onDidAddStatus.event; } + private readonly _onDidRemoveStatus = new Emitter(); + get onDidRemoveStatus(): Event { return this._onDidRemoveStatus.event; } + + get primary(): ITerminalStatus | undefined { + let result: ITerminalStatus | undefined; + for (const s of this._statuses.values()) { + if (!result || s.severity > result.severity) { + result = s; + } + } + return result; + } + + get statuses(): ITerminalStatus[] { return Array.from(this._statuses.values()); } + + add(status: ITerminalStatus, duration?: number) { + this._statusTimeouts.delete(status.id); + if (duration && duration > 0) { + const timeout = window.setTimeout(() => this.remove(status), duration); + this._statusTimeouts.set(status.id, timeout); + } + if (!this._statuses.has(status.id)) { + this._statuses.set(status.id, status); + this._onDidAddStatus.fire(status); + } + } + + remove(status: ITerminalStatus): void; + remove(statusId: string): void; + remove(statusOrId: ITerminalStatus | string): void { + const status = typeof statusOrId === 'string' ? this._statuses.get(statusOrId) : statusOrId; + // Verify the status is the same as the one passed in + if (status && this._statuses.get(status.id)) { + this._statuses.delete(status.id); + this._onDidRemoveStatus.fire(status); + } + } + + toggle(status: ITerminalStatus, value: boolean) { + if (value) { + this.add(status); + } else { + this.remove(status); + } + } +} From 4377f6f19de1596437b75817805d98b576d1eecb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Apr 2021 12:45:28 -0700 Subject: [PATCH 2/6] Docs, remove old timeout --- .../terminal/browser/terminalStatusList.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index a0172d97cb4a0..b6b126aae2982 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -7,7 +7,12 @@ import { Emitter, Event } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; export interface ITerminalStatus { + /** An internal string ID used to identify the status. */ id: string; + /** + * The severity of the status, this defines both the color and how likely the status is to be + * the "primary status". + */ severity: Severity; } @@ -20,6 +25,11 @@ export interface ITerminalStatusList { readonly onDidAddStatus: Event; readonly onDidRemoveStatus: Event; + /** + * Adds a status to the list. + * @param duration An optional duration of the status, when specified the status will remove + * itself when the duration elapses unless the status gets re-added. + */ add(status: ITerminalStatus, duration?: number): void; remove(status: ITerminalStatus): void; remove(statusId: string): void; @@ -48,7 +58,11 @@ export class TerminalStatusList implements ITerminalStatusList { get statuses(): ITerminalStatus[] { return Array.from(this._statuses.values()); } add(status: ITerminalStatus, duration?: number) { - this._statusTimeouts.delete(status.id); + const outTimeout = this._statusTimeouts.get(status.id); + if (outTimeout) { + window.clearTimeout(outTimeout); + this._statusTimeouts.delete(status.id); + } if (duration && duration > 0) { const timeout = window.setTimeout(() => this.remove(status), duration); this._statusTimeouts.set(status.id, timeout); From eaf1c8c6e9db3f174eebaf9d5a1225a0fd6328e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Apr 2021 13:59:12 -0700 Subject: [PATCH 3/6] Hook up disconnect and relaunch needed statuses --- .../contrib/terminal/browser/terminalInstance.ts | 16 ++++++++++++++-- .../terminal/browser/terminalStatusList.ts | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 07bd152a0939d..3f2f78c8e3f50 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -52,6 +52,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { AutoOpenBarrier } from 'vs/base/common/async'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; +import { ITerminalStatusList, TerminalStatus, TerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -132,6 +133,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private hasHadInput: boolean; + public readonly statusList: ITerminalStatusList = new TerminalStatusList(); public disableLayout: boolean; public get instanceId(): number { return this._instanceId; } public get cols(): number { @@ -245,6 +247,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._initDimensions(); this._createProcessManager(); + this.statusList.onDidAddStatus(e => console.log('add', e)); + this.statusList.onDidRemoveStatus(e => console.log('remove', e)); this._containerReadyBarrier = new AutoOpenBarrier(Constants.WaitForContainerThreshold); this._xtermReadyPromise = this._createXterm(); @@ -1009,10 +1013,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager.onPtyDisconnect(() => { this._safeSetOption('disableStdin', true); + this.statusList.add({ id: TerminalStatus.Disconnected, severity: Severity.Error }); this._onTitleChanged.fire(this); }); this._processManager.onPtyReconnect(() => { this._safeSetOption('disableStdin', false); + this.statusList.remove(TerminalStatus.Disconnected); + // TODO: Remove title change + (disconnected) from title this._onTitleChanged.fire(this); }); } @@ -1193,6 +1200,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Dispose the environment info widget if it exists + this.statusList.remove(TerminalStatus.RelaunchNeeded); this._environmentInfo?.disposable.dispose(); this._environmentInfo = undefined; @@ -1596,9 +1604,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _refreshEnvironmentVariableInfoWidgetState(info?: IEnvironmentVariableInfo): void { // Check if the widget should not exist - if (!info || + if ( + !info || this._configHelper.config.environmentChangesIndicator === 'off' || - this._configHelper.config.environmentChangesIndicator === 'warnonly' && !info.requiresAction) { + this._configHelper.config.environmentChangesIndicator === 'warnonly' && !info.requiresAction + ) { + this.statusList.remove(TerminalStatus.RelaunchNeeded); this._environmentInfo?.disposable.dispose(); this._environmentInfo = undefined; return; @@ -1620,6 +1631,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // (Re-)create the widget + this.statusList.add({ id: TerminalStatus.RelaunchNeeded, severity: Severity.Error }); this._environmentInfo?.disposable.dispose(); const widget = this._instantiationService.createInstance(EnvironmentVariableInfoWidget, info); const disposable = this._widgetManager.attachWidget(widget); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index b6b126aae2982..01b226d5254b7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -6,6 +6,16 @@ import { Emitter, Event } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; +/** + * The set of _internal_ terminal statuses, other components building on the terminal should put + * their statuses within their component. + */ +export const enum TerminalStatus { + Bell = 'bell', + Disconnected = 'disconnected', + RelaunchNeeded = 'relaunch-needed', +} + export interface ITerminalStatus { /** An internal string ID used to identify the status. */ id: string; @@ -27,8 +37,8 @@ export interface ITerminalStatusList { /** * Adds a status to the list. - * @param duration An optional duration of the status, when specified the status will remove - * itself when the duration elapses unless the status gets re-added. + * @param duration An optional duration in milliseconds of the status, when specified the status + * will remove itself when the duration elapses unless the status gets re-added. */ add(status: ITerminalStatus, duration?: number): void; remove(status: ITerminalStatus): void; From 1e674d98414e3639e5c14ddc4f13af1a9a3c2525 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Apr 2021 14:06:52 -0700 Subject: [PATCH 4/6] Expose on did change primary status event --- .../contrib/terminal/browser/terminalStatusList.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index 01b226d5254b7..042f123b5f97f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -34,6 +34,7 @@ export interface ITerminalStatusList { readonly onDidAddStatus: Event; readonly onDidRemoveStatus: Event; + readonly onDidChangePrimaryStatus: Event; /** * Adds a status to the list. @@ -54,6 +55,8 @@ export class TerminalStatusList implements ITerminalStatusList { get onDidAddStatus(): Event { return this._onDidAddStatus.event; } private readonly _onDidRemoveStatus = new Emitter(); get onDidRemoveStatus(): Event { return this._onDidRemoveStatus.event; } + private readonly _onDidChangePrimaryStatus = new Emitter(); + get onDidChangePrimaryStatus(): Event { return this._onDidChangePrimaryStatus.event; } get primary(): ITerminalStatus | undefined { let result: ITerminalStatus | undefined; @@ -78,8 +81,13 @@ export class TerminalStatusList implements ITerminalStatusList { this._statusTimeouts.set(status.id, timeout); } if (!this._statuses.has(status.id)) { + const oldPrimary = this.primary; this._statuses.set(status.id, status); this._onDidAddStatus.fire(status); + const newPrimary = this.primary; + if (oldPrimary !== newPrimary) { + this._onDidChangePrimaryStatus.fire(newPrimary); + } } } @@ -89,8 +97,12 @@ export class TerminalStatusList implements ITerminalStatusList { const status = typeof statusOrId === 'string' ? this._statuses.get(statusOrId) : statusOrId; // Verify the status is the same as the one passed in if (status && this._statuses.get(status.id)) { + const wasPrimary = this.primary === status; this._statuses.delete(status.id); this._onDidRemoveStatus.fire(status); + if (wasPrimary) { + this._onDidChangePrimaryStatus.fire(this.primary); + } } } From b7b1e0cf8a63909d89b174831257d15e477cded1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Apr 2021 14:38:32 -0700 Subject: [PATCH 5/6] Add unit tests --- .../terminal/browser/terminalStatusList.ts | 11 +- .../test/browser/terminalStatusList.test.ts | 142 ++++++++++++++++++ 2 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index 042f123b5f97f..207abd140b373 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; /** @@ -47,21 +48,21 @@ export interface ITerminalStatusList { toggle(status: ITerminalStatus, value: boolean): void; } -export class TerminalStatusList implements ITerminalStatusList { +export class TerminalStatusList extends Disposable implements ITerminalStatusList { private readonly _statuses: Map = new Map(); private readonly _statusTimeouts: Map = new Map(); - private readonly _onDidAddStatus = new Emitter(); + private readonly _onDidAddStatus = this._register(new Emitter()); get onDidAddStatus(): Event { return this._onDidAddStatus.event; } - private readonly _onDidRemoveStatus = new Emitter(); + private readonly _onDidRemoveStatus = this._register(new Emitter()); get onDidRemoveStatus(): Event { return this._onDidRemoveStatus.event; } - private readonly _onDidChangePrimaryStatus = new Emitter(); + private readonly _onDidChangePrimaryStatus = this._register(new Emitter()); get onDidChangePrimaryStatus(): Event { return this._onDidChangePrimaryStatus.event; } get primary(): ITerminalStatus | undefined { let result: ITerminalStatus | undefined; for (const s of this._statuses.values()) { - if (!result || s.severity > result.severity) { + if (!result || s.severity >= result.severity) { result = s; } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts new file mode 100644 index 0000000000000..deca0344b0e7e --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, strictEqual } from 'assert'; +import Severity from 'vs/base/common/severity'; +import { ITerminalStatus, TerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; + +function statusesEqual(list: TerminalStatusList, expected: [string, Severity][]) { + deepStrictEqual(list.statuses.map(e => [e.id, e.severity]), expected); +} + +suite.only('Workbench - TerminalStatusList', () => { + let list: TerminalStatusList; + + setup(() => { + list = new TerminalStatusList(); + }); + + teardown(() => { + list.dispose(); + }); + + test('primary', () => { + strictEqual(list.primary?.id, undefined); + list.add({ id: 'info1', severity: Severity.Info }); + strictEqual(list.primary?.id, 'info1'); + list.add({ id: 'warning1', severity: Severity.Warning }); + strictEqual(list.primary?.id, 'warning1'); + list.add({ id: 'info2', severity: Severity.Info }); + strictEqual(list.primary?.id, 'warning1'); + list.add({ id: 'warning2', severity: Severity.Warning }); + strictEqual(list.primary?.id, 'warning2'); + list.add({ id: 'info3', severity: Severity.Info }); + strictEqual(list.primary?.id, 'warning2'); + list.add({ id: 'error1', severity: Severity.Error }); + strictEqual(list.primary?.id, 'error1'); + list.add({ id: 'warning3', severity: Severity.Warning }); + strictEqual(list.primary?.id, 'error1'); + list.add({ id: 'error2', severity: Severity.Error }); + strictEqual(list.primary?.id, 'error2'); + list.remove('error1'); + strictEqual(list.primary?.id, 'error2'); + list.remove('error2'); + strictEqual(list.primary?.id, 'warning3'); + }); + + test('statuses', () => { + strictEqual(list.statuses.length, 0); + list.add({ id: 'info', severity: Severity.Info }); + list.add({ id: 'warning', severity: Severity.Warning }); + list.add({ id: 'error', severity: Severity.Error }); + strictEqual(list.statuses.length, 3); + statusesEqual(list, [ + ['info', Severity.Info], + ['warning', Severity.Warning], + ['error', Severity.Error], + ]); + list.remove('info'); + list.remove('warning'); + list.remove('error'); + strictEqual(list.statuses.length, 0); + }); + + test('onDidAddStatus', async () => { + const result = await new Promise(r => { + list.onDidAddStatus(r); + list.add({ id: 'test', severity: Severity.Info }); + }); + deepStrictEqual(result, { id: 'test', severity: Severity.Info }); + }); + + test('onDidRemoveStatus', async () => { + const result = await new Promise(r => { + list.onDidRemoveStatus(r); + list.add({ id: 'test', severity: Severity.Info }); + list.remove('test'); + }); + deepStrictEqual(result, { id: 'test', severity: Severity.Info }); + }); + + test('onDidChangePrimaryStatus', async () => { + const result = await new Promise(r => { + list.onDidRemoveStatus(r); + list.add({ id: 'test', severity: Severity.Info }); + list.remove('test'); + }); + deepStrictEqual(result, { id: 'test', severity: Severity.Info }); + }); + + test('add', () => { + statusesEqual(list, []); + list.add({ id: 'info', severity: Severity.Info }); + statusesEqual(list, [ + ['info', Severity.Info] + ]); + list.add({ id: 'warning', severity: Severity.Warning }); + statusesEqual(list, [ + ['info', Severity.Info], + ['warning', Severity.Warning] + ]); + list.add({ id: 'error', severity: Severity.Error }); + statusesEqual(list, [ + ['info', Severity.Info], + ['warning', Severity.Warning], + ['error', Severity.Error] + ]); + }); + + test('remove', () => { + list.add({ id: 'info', severity: Severity.Info }); + list.add({ id: 'warning', severity: Severity.Warning }); + list.add({ id: 'error', severity: Severity.Error }); + statusesEqual(list, [ + ['info', Severity.Info], + ['warning', Severity.Warning], + ['error', Severity.Error] + ]); + list.remove('warning'); + statusesEqual(list, [ + ['info', Severity.Info], + ['error', Severity.Error] + ]); + list.remove('info'); + statusesEqual(list, [ + ['error', Severity.Error] + ]); + list.remove('error'); + statusesEqual(list, []); + }); + + test('toggle', () => { + const status = { id: 'info', severity: Severity.Info }; + list.toggle(status, true); + statusesEqual(list, [ + ['info', Severity.Info] + ]); + list.toggle(status, false); + statusesEqual(list, []); + }); +}); From 329804e4cb38ce0e53cb737ecd6f63bf4bfc4f5c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Apr 2021 15:19:35 -0700 Subject: [PATCH 6/6] Remove only --- .../contrib/terminal/test/browser/terminalStatusList.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts index deca0344b0e7e..08e62c688b168 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalStatusList.test.ts @@ -11,7 +11,7 @@ function statusesEqual(list: TerminalStatusList, expected: [string, Severity][]) deepStrictEqual(list.statuses.map(e => [e.id, e.severity]), expected); } -suite.only('Workbench - TerminalStatusList', () => { +suite('Workbench - TerminalStatusList', () => { let list: TerminalStatusList; setup(() => {