Skip to content
This repository was archived by the owner on Dec 31, 2020. It is now read-only.

Commit dda8d0f

Browse files
roblourensmeganrogge
authored andcommitted
Restore terminal UI state and layout when reconnecting to remote terminals
Fix microsoft#109244 Co-authored-by: Megan Rogge <[email protected]>
1 parent 371e350 commit dda8d0f

File tree

8 files changed

+219
-46
lines changed

8 files changed

+219
-46
lines changed

src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
1515
import { ILogService } from 'vs/platform/log/common/log';
1616
import { IRemoteTerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
1717
import { IRemoteTerminalProcessExecCommandEvent, IShellLaunchConfigDto, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
18-
import { IProcessDataEvent, IRemoteTerminalAttachTarget, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
18+
import { IProcessDataEvent, IRemoteTerminalAttachTarget, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/workbench/contrib/terminal/common/terminal';
1919
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
2020

2121
export class RemoteTerminalService extends Disposable implements IRemoteTerminalService {
@@ -69,6 +69,22 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal
6969
};
7070
});
7171
}
72+
73+
public setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise<void> {
74+
if (!this._remoteTerminalChannel) {
75+
throw new Error(`Cannot call setActiveInstanceId when there is no remote`);
76+
}
77+
78+
return this._remoteTerminalChannel.setTerminalLayoutInfo(layout);
79+
}
80+
81+
public getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined> {
82+
if (!this._remoteTerminalChannel) {
83+
throw new Error(`Cannot call getActiveInstanceId when there is no remote`);
84+
}
85+
86+
return this._remoteTerminalChannel.getTerminalLayoutInfo();
87+
}
7288
}
7389

7490
export class RemoteTerminalProcess extends Disposable implements ITerminalChildProcess {
@@ -115,7 +131,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP
115131
}
116132
}
117133

118-
public async start(): Promise<ITerminalLaunchError | undefined> {
134+
public async start(): Promise<ITerminalLaunchError | { remoteTerminalId: number } | undefined> {
119135
// Fetch the environment to check shell permissions
120136
const env = await this._remoteAgentService.getEnvironment();
121137
if (!env) {
@@ -166,7 +182,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP
166182
}
167183

168184
this._startBarrier.open();
169-
return undefined;
185+
return { remoteTerminalId: this._remoteTerminalId };
170186
}
171187

172188
public shutdown(immediate: boolean): void {

src/vs/workbench/contrib/terminal/browser/terminal.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Terminal as XTermTerminal } from 'xterm';
77
import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search';
88
import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11';
99
import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl';
10-
import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError, ITerminalNativeWindowsDelegate, LinuxDistro, IRemoteTerminalAttachTarget } from 'vs/workbench/contrib/terminal/common/terminal';
10+
import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError, ITerminalNativeWindowsDelegate, LinuxDistro, IRemoteTerminalAttachTarget, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ITerminalTabLayoutInfoById } from 'vs/workbench/contrib/terminal/common/terminal';
1111
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1212
import { IProcessEnvironment, Platform } from 'vs/base/common/platform';
1313
import { Event } from 'vs/base/common/event';
@@ -58,16 +58,17 @@ export interface ITerminalTab {
5858
title: string;
5959
onDisposed: Event<ITerminalTab>;
6060
onInstancesChanged: Event<void>;
61-
6261
focusPreviousPane(): void;
6362
focusNextPane(): void;
6463
resizePane(direction: Direction): void;
64+
resizePanes(relativeSizes: number[]): void;
6565
setActiveInstanceByIndex(index: number): void;
6666
attachToElement(element: HTMLElement): void;
6767
setVisible(visible: boolean): void;
6868
layout(width: number, height: number): void;
6969
addDisposable(disposable: IDisposable): void;
7070
split(shellLaunchConfig: IShellLaunchConfig): ITerminalInstance;
71+
getLayoutInfo(isActive: boolean): ITerminalTabLayoutInfoById;
7172
}
7273

7374
export const enum TerminalConnectionState {
@@ -186,11 +187,12 @@ export interface ITerminalService {
186187

187188
export interface IRemoteTerminalService {
188189
readonly _serviceBrand: undefined;
189-
190190
dispose(): void;
191-
192191
listTerminals(isInitialization?: boolean): Promise<IRemoteTerminalAttachTarget[]>;
193192
createRemoteTerminalProcess(terminalId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, configHelper: ITerminalConfigHelper,): Promise<ITerminalChildProcess>;
193+
194+
setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise<void>;
195+
getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>;
194196
}
195197

196198
/**
@@ -261,6 +263,12 @@ export interface ITerminalInstance {
261263
*/
262264
processId: number | undefined;
263265

266+
/**
267+
* The id of a terminal on the remote server. Defined if this is a terminal created
268+
* by the RemoteTerminalService.
269+
*/
270+
readonly remoteTerminalId: number | undefined;
271+
264272
/**
265273
* An event that fires when the terminal instance's title changes.
266274
*/

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
369369
return TerminalInstance._lastKnownCanvasDimensions;
370370
}
371371

372+
public get remoteTerminalId(): number | undefined { return this._processManager.remoteTerminalId; }
373+
372374
private async _getXtermConstructor(): Promise<typeof XTermTerminal> {
373375
if (xtermConstructor) {
374376
return xtermConstructor;

src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
8383
public get onEnvironmentVariableInfoChanged(): Event<IEnvironmentVariableInfo> { return this._onEnvironmentVariableInfoChange.event; }
8484

8585
public get environmentVariableInfo(): IEnvironmentVariableInfo | undefined { return this._environmentVariableInfo; }
86+
private _remoteTerminalId: number | undefined;
87+
public get remoteTerminalId(): number | undefined { return this._remoteTerminalId; }
8688

8789
constructor(
8890
private readonly _terminalId: number,
@@ -208,9 +210,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
208210
}
209211
}, LAUNCHING_DURATION);
210212

211-
const error = await this._process.start();
212-
if (error) {
213-
return error;
213+
const result = await this._process.start();
214+
if (result && 'remoteTerminalId' in result) {
215+
this._remoteTerminalId = result.remoteTerminalId;
216+
} else if (result) {
217+
// Error
218+
return result;
214219
}
215220

216221
return undefined;

src/vs/workbench/contrib/terminal/browser/terminalService.ts

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { timeout } from 'vs/base/common/async';
7+
import { debounce } from 'vs/base/common/decorators';
78
import { Emitter, Event } from 'vs/base/common/event';
89
import { IDisposable } from 'vs/base/common/lifecycle';
910
import { basename } from 'vs/base/common/path';
@@ -17,14 +18,13 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
1718
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1819
import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
1920
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
20-
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
2121
import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
2222
import { TerminalConnectionState, IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalTab, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal';
2323
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
2424
import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
2525
import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab';
2626
import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView';
27-
import { IAvailableShellsRequest, IRemoteTerminalAttachTarget, IShellDefinition, IShellLaunchConfig, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalLaunchError, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
27+
import { IAvailableShellsRequest, IRemoteTerminalAttachTarget, IShellDefinition, IShellLaunchConfig, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalLaunchError, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, ITerminalsLayoutInfoById, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
2828
import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
2929
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
3030
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@@ -117,8 +117,7 @@ export class TerminalService implements ITerminalService {
117117
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService,
118118
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
119119
@IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService,
120-
@ITelemetryService private readonly _telemetryService: ITelemetryService,
121-
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
120+
@ITelemetryService private readonly _telemetryService: ITelemetryService
122121
) {
123122
this._activeTabIndex = 0;
124123
this._isShuttingDown = false;
@@ -135,7 +134,6 @@ export class TerminalService implements ITerminalService {
135134
this._onActiveInstanceChanged.fire(instance ? instance : undefined);
136135
});
137136
this.onInstanceLinksReady(instance => this._setInstanceLinkProviders(instance));
138-
139137
this._handleInstanceContextKeys();
140138
this._processSupportContextKey = KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED.bindTo(this._contextKeyService);
141139
this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null);
@@ -147,36 +145,74 @@ export class TerminalService implements ITerminalService {
147145
this._connectionState = TerminalConnectionState.Connecting;
148146
} else {
149147
this._connectionState = TerminalConnectionState.Connected;
148+
this.attachRemoteListeners();
150149
}
151150
}
152151

153152
private async _reconnectToRemoteTerminals(): Promise<void> {
154-
const remoteTerms = await this._remoteTerminalService.listTerminals(true);
155-
const workspace = this._workspaceContextService.getWorkspace();
156-
const unattachedWorkspaceRemoteTerms = remoteTerms
157-
.filter(term => term.workspaceId === workspace.id)
158-
.filter(term => !this.isAttachedToTerminal(term));
159-
153+
// Reattach to all remote terminals
154+
const layoutInfo = await this._remoteTerminalService.getTerminalLayoutInfo();
155+
let reconnectCounter = 0;
156+
let activeTab: ITerminalTab | undefined;
157+
if (layoutInfo) {
158+
layoutInfo.tabs.forEach((tabLayout) => {
159+
const terminalLayouts = tabLayout.terminals.filter(t => t.terminal && t.terminal.isOrphan);
160+
if (terminalLayouts.length) {
161+
reconnectCounter += terminalLayouts.length;
162+
let terminalInstance: ITerminalInstance | undefined;
163+
let tab: ITerminalTab | undefined;
164+
terminalLayouts.forEach((terminalLayout) => {
165+
if (!terminalInstance) {
166+
// create tab and terminal
167+
terminalInstance = this.createTerminal({ remoteAttach: terminalLayout.terminal! });
168+
tab = this._getTabForInstance(terminalInstance);
169+
if (tabLayout.isActive) {
170+
activeTab = tab;
171+
}
172+
} else {
173+
// add split terminals to this tab
174+
this.splitInstance(terminalInstance, { remoteAttach: terminalLayout.terminal! });
175+
}
176+
});
177+
const activeInstance = this.terminalInstances.find(t => t.shellLaunchConfig.remoteAttach?.pid === tabLayout.activeTerminalProcessId);
178+
if (activeInstance) {
179+
this.setActiveInstance(activeInstance);
180+
}
181+
tab?.resizePanes(tabLayout.terminals.map(terminal => terminal.relativeSize));
182+
}
183+
});
184+
if (layoutInfo.tabs.length) {
185+
this.setActiveTabByIndex(activeTab ? this.terminalTabs.indexOf(activeTab) : 0);
186+
}
187+
}
160188
/* __GDPR__
161189
"terminalReconnection" : {
162190
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
163191
}
164192
*/
165193
const data = {
166-
count: unattachedWorkspaceRemoteTerms.length
194+
count: reconnectCounter
167195
};
168196
this._telemetryService.publicLog('terminalReconnection', data);
169-
if (unattachedWorkspaceRemoteTerms.length > 0) {
170-
// Reattach to all remote terminals
171-
for (let term of unattachedWorkspaceRemoteTerms) {
172-
this.createTerminal({ remoteAttach: term });
173-
}
174-
}
175-
176197
this._connectionState = TerminalConnectionState.Connected;
198+
// now that terminals have been restored,
199+
// attach listeners to update remote when terminals are changed
200+
this.attachRemoteListeners();
177201
this._onDidChangeConnectionState.fire();
178202
}
179203

204+
private attachRemoteListeners(): void {
205+
this.onActiveTabChanged(() => {
206+
this._updateRemoteState();
207+
});
208+
this.onActiveInstanceChanged(() => {
209+
this._updateRemoteState();
210+
});
211+
this.onInstancesChanged(() => {
212+
this._updateRemoteState();
213+
});
214+
}
215+
180216
public setNativeWindowsDelegate(delegate: ITerminalNativeWindowsDelegate): void {
181217
this._nativeWindowsDelegate = delegate;
182218
}
@@ -253,7 +289,7 @@ export class TerminalService implements ITerminalService {
253289
}
254290

255291
private async _onBeforeShutdownAsync(): Promise<boolean> {
256-
// veto if configured to show confirmation and the user choosed not to exit
292+
// veto if configured to show confirmation and the user chose not to exit
257293
const veto = await this._showTerminalCloseConfirmation();
258294
if (!veto) {
259295
this._isShuttingDown = true;
@@ -274,6 +310,16 @@ export class TerminalService implements ITerminalService {
274310
return this._findState;
275311
}
276312

313+
@debounce(500)
314+
private _updateRemoteState(): void {
315+
if (!!this._environmentService.remoteAuthority) {
316+
const state: ITerminalsLayoutInfoById = {
317+
tabs: this.terminalTabs.map(t => t.getLayoutInfo(t === this.getActiveTab()))
318+
};
319+
this._remoteTerminalService.setTerminalLayoutInfo(state);
320+
}
321+
}
322+
277323
private _removeTab(tab: ITerminalTab): void {
278324
// Get the index of the tab and remove it from the list
279325
const index = this._terminalTabs.indexOf(tab);
@@ -467,6 +513,7 @@ export class TerminalService implements ITerminalService {
467513
}
468514

469515
const instance = tab.split(shellLaunchConfig);
516+
470517
this._initInstanceListeners(instance);
471518
this._onInstancesChanged.fire();
472519

@@ -479,7 +526,10 @@ export class TerminalService implements ITerminalService {
479526
instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged));
480527
instance.addDisposable(instance.onProcessIdReady(this._onInstanceProcessIdReady.fire, this._onInstanceProcessIdReady));
481528
instance.addDisposable(instance.onLinksReady(this._onInstanceLinksReady.fire, this._onInstanceLinksReady));
482-
instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance)));
529+
instance.addDisposable(instance.onDimensionsChanged(() => {
530+
this._onInstanceDimensionsChanged.fire(instance);
531+
this._updateRemoteState();
532+
}));
483533
instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onInstanceMaximumDimensionsChanged.fire(instance)));
484534
instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged));
485535
}
@@ -746,7 +796,7 @@ export class TerminalService implements ITerminalService {
746796
}
747797
}
748798

749-
public setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void {
799+
public async setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): Promise<void> {
750800
this._configHelper.panelContainer = panelContainer;
751801
this._terminalContainer = terminalContainer;
752802
this._terminalTabs.forEach(tab => tab.attachToElement(terminalContainer));

0 commit comments

Comments
 (0)