From f413b81fcda270e6fbbeef96456bd2bd7774e8eb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 14 Dec 2020 09:59:07 +0100 Subject: [PATCH] windows - move state handling into own class --- .../launch/electron-main/launchMainService.ts | 6 +- .../platform/windows/electron-main/windows.ts | 4 +- .../electron-main/windowsMainService.ts | 201 ++---------- .../electron-main/windowsStateHandler.ts | 295 ++++++++++++++++++ .../electron-main/windowsStateStorage.ts | 107 ------- ...ge.test.ts => windowsStateHandler.test.ts} | 2 +- 6 files changed, 325 insertions(+), 290 deletions(-) create mode 100644 src/vs/platform/windows/electron-main/windowsStateHandler.ts delete mode 100644 src/vs/platform/windows/electron-main/windowsStateStorage.ts rename src/vs/platform/windows/test/electron-main/{windowsStateStorage.test.ts => windowsStateHandler.test.ts} (99%) diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 97d251557da..54a02fdefac 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -297,10 +297,10 @@ export class LaunchMainService implements ILaunchMainService { return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority); } - private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { + private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { return { - pid: win.webContents.getOSProcessId(), - title: win.getTitle(), + pid: window.webContents.getOSProcessId(), + title: window.getTitle(), folderURIs, remoteAuthority }; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index f5a42fb7311..9fd013da6ad 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -124,9 +124,11 @@ export interface IWindowsMainService { readonly _serviceBrand: undefined; + readonly onWindowsCountChanged: Event; + readonly onWindowOpened: Event; readonly onWindowReady: Event; - readonly onWindowsCountChanged: Event; + readonly onWindowDestroyed: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 20c111f9afc..005897bc0af 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -13,7 +13,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IStateService } from 'vs/platform/state/node/state'; import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; -import { screen, BrowserWindow, MessageBoxOptions, Display, app, WebContents } from 'electron'; +import { screen, BrowserWindow, MessageBoxOptions, Display, WebContents } from 'electron'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; @@ -30,7 +30,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { normalizePath, originalFSPath, removeTrailingPathSeparator, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; -import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData, IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsStateStorage'; +import { IWindowState, WindowsStateHandler } from 'vs/platform/windows/electron-main/windowsStateHandler'; import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -145,24 +145,22 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic declare readonly _serviceBrand: undefined; - private static readonly windowsStateStorageKey = 'windowsState'; - private static readonly WINDOWS: ICodeWindow[] = []; - private readonly windowsState: IWindowsState; - private lastClosedWindowState?: IWindowState; - - private shuttingDown = false; - private readonly _onWindowOpened = this._register(new Emitter()); readonly onWindowOpened = this._onWindowOpened.event; private readonly _onWindowReady = this._register(new Emitter()); readonly onWindowReady = this._onWindowReady.event; + private readonly _onWindowDestroyed = this._register(new Emitter()); + readonly onWindowDestroyed = this._onWindowDestroyed.event; + private readonly _onWindowsCountChanged = this._register(new Emitter()); readonly onWindowsCountChanged = this._onWindowsCountChanged.event; + private readonly windowsStateHandler = this._register(new WindowsStateHandler(this.stateService, this.lifecycleMainService, this, this.logService)); + constructor( private readonly machineId: string, private readonly initialUserEnv: IProcessEnvironment, @@ -179,166 +177,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic ) { super(); - this.windowsState = restoreWindowsState(this.stateService.getItem(WindowsMainService.windowsStateStorageKey)); this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); } private registerListeners(): void { - // When a window looses focus, save all windows state. This allows to - // prevent loss of window-state data when OS is restarted without properly - // shutting down the application (https://github.com/microsoft/vscode/issues/87171) - app.on('browser-window-blur', () => { - if (!this.shuttingDown) { - this.saveWindowsState(); - } - }); - - // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.onWindowsCountChanged(e => { - if (e.newCount - e.oldCount > 0) { - // clear last closed window state when a new window opens. this helps on macOS where - // otherwise closing the last window, opening a new window and then quitting would - // use the state of the previously closed window when restarting. - this.lastClosedWindowState = undefined; - } - }); - // Signal a window is ready after having entered a workspace - this._register(this.workspacesMainService.onWorkspaceEntered(event => { - this._onWindowReady.fire(event.window); - })); - } - - // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: - // - macOS: since the app will not quit when closing the last window, you will always first get - // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window - // - other: on other OS, closing the last window will quit the app so the order depends on the - // user interaction: closing the last window will first trigger onBeforeWindowClose() - // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() - // and then onBeforeWindowClose(). - // - // Here is the behavior on different OS depending on action taken (Electron 1.7.x): - // - // Legend - // - quit(N): quit application with N windows opened - // - close(1): close one window via the window close button - // - closeAll: close all windows via the taskbar command - // - onBeforeShutdown(N): number of windows reported in this event handler - // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler - // - // macOS - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - quit(0): onBeforeShutdown(0) - // - close(1): onBeforeWindowClose(1, false) - // - // Windows - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) - // - // Linux - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - close(1): onBeforeWindowClose(2, false)[not last window] - // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] - // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) - // - private onBeforeShutdown(): void { - this.shuttingDown = true; - - this.saveWindowsState(); - } - - private saveWindowsState(): void { - const currentWindowsState: IWindowsState = { - openedWindows: [], - lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow, - lastActiveWindow: this.lastClosedWindowState - }; - - // 1.) Find a last active window (pick any other first window otherwise) - if (!currentWindowsState.lastActiveWindow) { - let activeWindow = this.getLastActiveWindow(); - if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { - activeWindow = this.getWindows().find(window => !window.isExtensionDevelopmentHost); - } - - if (activeWindow) { - currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); - } - } - - // 2.) Find extension host window - const extensionHostWindow = this.getWindows().find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); - if (extensionHostWindow) { - currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); - } - - // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update - // - // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) - // so if we ever want to persist the UI state of the last closed window (window count === 1), it has - // to come from the stored lastClosedWindowState on Win/Linux at least - if (this.getWindowCount() > 1) { - currentWindowsState.openedWindows = this.getWindows().filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); - } - - // Persist - const state = getWindowsStateStoreData(currentWindowsState); - this.stateService.setItem(WindowsMainService.windowsStateStorageKey, state); - - if (this.shuttingDown) { - this.logService.trace('onBeforeShutdown', state); - } - } - - // See note on #onBeforeShutdown() for details how these events are flowing - private onBeforeWindowClose(win: ICodeWindow): void { - if (this.lifecycleMainService.quitRequested) { - return; // during quit, many windows close in parallel so let it be handled in the before-quit handler - } - - // On Window close, update our stored UI state of this window - const state: IWindowState = this.toWindowState(win); - if (win.isExtensionDevelopmentHost && !win.isExtensionTestHost) { - this.windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state - } - - // Any non extension host window with same workspace or folder - else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) { - this.windowsState.openedWindows.forEach(o => { - const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id; - const sameFolder = win.openedFolderUri && o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, win.openedFolderUri); - - if (sameWorkspace || sameFolder) { - o.uiState = state.uiState; - } - }); - } - - // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state - // before quitting, we need to remember the UI state of this window to be able to persist it. - // On macOS we keep the last closed window state ready in case the user wants to quit right after or - // wants to open another window, in which case we use this state over the persisted one. - if (this.getWindowCount() === 1) { - this.lastClosedWindowState = state; - } - } - - private toWindowState(win: ICodeWindow): IWindowState { - return { - workspace: win.openedWorkspace, - folderUri: win.openedFolderUri, - backupPath: win.backupPath, - remoteAuthority: win.remoteAuthority, - uiState: win.serializeWindowState() - }; + this._register(this.workspacesMainService.onWorkspaceEntered(event => this._onWindowReady.fire(event.window))); } openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { @@ -434,13 +279,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Otherwise, find a good window based on open params else { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); + const focusLastActive = this.windowsStateHandler.state.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; // 2.) focus last active window if we are not instructed to open any paths if (focusLastActive) { - const lastActiveWindow = usedWindows.filter(window => this.windowsState.lastActiveWindow && window.backupPath === this.windowsState.lastActiveWindow.backupPath); + const lastActiveWindow = usedWindows.filter(window => this.windowsStateHandler.state.lastActiveWindow && window.backupPath === this.windowsStateHandler.state.lastActiveWindow.backupPath); if (lastActiveWindow.length) { lastActiveWindow[0].focus(); focusLastOpened = false; @@ -987,10 +832,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Collect previously opened windows const openedWindows: IWindowState[] = []; if (restoreWindowsSetting !== 'one') { - openedWindows.push(...this.windowsState.openedWindows); + openedWindows.push(...this.windowsStateHandler.state.openedWindows); } - if (this.windowsState.lastActiveWindow) { - openedWindows.push(this.windowsState.lastActiveWindow); + if (this.windowsStateHandler.state.lastActiveWindow) { + openedWindows.push(this.windowsStateHandler.state.lastActiveWindow); } const windowsToOpen: IPathToOpen[] = []; @@ -1287,7 +1132,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing if (!cliArgs.length && !folderUris.length && !fileUris.length && !openConfig.cli.extensionTestsPath) { - const extensionDevelopmentWindowState = this.windowsState.lastPluginDevelopmentHostWindow; + const extensionDevelopmentWindowState = this.windowsStateHandler.state.lastPluginDevelopmentHostWindow; const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderUri); if (workspaceToOpen) { if (isSingleFolderWorkspaceIdentifier(workspaceToOpen)) { @@ -1471,7 +1316,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Window Events once(createdWindow.onReady)(() => this._onWindowReady.fire(createdWindow)); once(createdWindow.onClose)(() => this.onWindowClosed(createdWindow)); - once(createdWindow.onDestroy)(() => this.onBeforeWindowClose(createdWindow)); // try to save state before destroy because close will not fire + once(createdWindow.onDestroy)(() => this._onWindowDestroyed.fire(createdWindow)); createdWindow.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own createdWindow.win.webContents.on('devtools-reload-page', () => this.lifecycleMainService.reload(createdWindow)); @@ -1536,14 +1381,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (!configuration.extensionTestsPath) { // extension development host Window - load from stored settings if any - if (!!configuration.extensionDevelopmentPath && this.windowsState.lastPluginDevelopmentHostWindow) { - return this.windowsState.lastPluginDevelopmentHostWindow.uiState; + if (!!configuration.extensionDevelopmentPath && this.windowsStateHandler.state.lastPluginDevelopmentHostWindow) { + return this.windowsStateHandler.state.lastPluginDevelopmentHostWindow.uiState; } // Known Workspace - load from stored settings const workspace = configuration.workspace; if (workspace) { - const stateForWorkspace = this.windowsState.openedWindows.filter(o => o.workspace && o.workspace.id === workspace.id).map(o => o.uiState); + const stateForWorkspace = this.windowsStateHandler.state.openedWindows.filter(o => o.workspace && o.workspace.id === workspace.id).map(o => o.uiState); if (stateForWorkspace.length) { return stateForWorkspace[0]; } @@ -1551,7 +1396,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Known Folder - load from stored settings if (configuration.folderUri) { - const stateForFolder = this.windowsState.openedWindows.filter(o => o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, configuration.folderUri)).map(o => o.uiState); + const stateForFolder = this.windowsStateHandler.state.openedWindows.filter(o => o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, configuration.folderUri)).map(o => o.uiState); if (stateForFolder.length) { return stateForFolder[0]; } @@ -1559,14 +1404,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Empty windows with backups else if (configuration.backupPath) { - const stateForEmptyWindow = this.windowsState.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); + const stateForEmptyWindow = this.windowsStateHandler.state.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); if (stateForEmptyWindow.length) { return stateForEmptyWindow[0]; } } // First Window - const lastActiveState = this.lastClosedWindowState || this.windowsState.lastActiveWindow; + const lastActiveState = this.windowsStateHandler.lastClosedState || this.windowsStateHandler.state.lastActiveWindow; if (!lastActive && lastActiveState) { return lastActiveState.uiState; } @@ -1660,10 +1505,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return state; } - private onWindowClosed(win: ICodeWindow): void { + private onWindowClosed(window: ICodeWindow): void { // Remove from our list so that Electron can clean it up - const index = WindowsMainService.WINDOWS.indexOf(win); + const index = WindowsMainService.WINDOWS.indexOf(window); WindowsMainService.WINDOWS.splice(index, 1); // Emit diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts new file mode 100644 index 00000000000..453b786f763 --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -0,0 +1,295 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { app } from 'electron'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStateService } from 'vs/platform/state/node/state'; +import { ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState } from 'vs/platform/windows/electron-main/windows'; +import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export type WindowsStateStorageData = object; + +export interface IWindowState { + workspace?: IWorkspaceIdentifier; + folderUri?: URI; + backupPath?: string; + remoteAuthority?: string; + uiState: IWindowUIState; +} + +export interface IWindowsState { + lastActiveWindow?: IWindowState; + lastPluginDevelopmentHostWindow?: IWindowState; + openedWindows: IWindowState[]; +} + +interface ISerializedWindowsState { + readonly lastActiveWindow?: ISerializedWindowState; + readonly lastPluginDevelopmentHostWindow?: ISerializedWindowState; + readonly openedWindows: ISerializedWindowState[]; +} + +interface ISerializedWindowState { + readonly workspaceIdentifier?: { id: string; configURIPath: string }; + readonly folder?: string; + readonly backupPath?: string; + readonly remoteAuthority?: string; + readonly uiState: IWindowUIState; + + // deprecated + readonly folderUri?: UriComponents; + readonly folderPath?: string; + readonly workspace?: { id: string; configPath: string }; +} + +export class WindowsStateHandler extends Disposable { + + private static readonly windowsStateStorageKey = 'windowsState'; + + get state() { return this._windowsState; } + private readonly _windowsState: IWindowsState; + + get lastClosedState() { return this._lastClosedState; } + private _lastClosedState?: IWindowState; + + private shuttingDown = false; + + constructor( + @IStateService private readonly stateService: IStateService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @ILogService private readonly logService: ILogService + ) { + super(); + + this._windowsState = restoreWindowsState(this.stateService.getItem(WindowsStateHandler.windowsStateStorageKey)); + + this.registerListeners(); + } + + private registerListeners(): void { + + // When a window looses focus, save all windows state. This allows to + // prevent loss of window-state data when OS is restarted without properly + // shutting down the application (https://github.com/microsoft/vscode/issues/87171) + app.on('browser-window-blur', () => { + if (!this.shuttingDown) { + this.saveWindowsState(); + } + }); + + // Handle various lifecycle events around windows + this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); + this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); + this.windowsMainService.onWindowsCountChanged(e => { + if (e.newCount - e.oldCount > 0) { + // clear last closed window state when a new window opens. this helps on macOS where + // otherwise closing the last window, opening a new window and then quitting would + // use the state of the previously closed window when restarting. + this._lastClosedState = undefined; + } + }); + + // try to save state before destroy because close will not fire + this.windowsMainService.onWindowDestroyed(window => this.onBeforeWindowClose(window)); + } + + // Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS: + // - macOS: since the app will not quit when closing the last window, you will always first get + // the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window + // - other: on other OS, closing the last window will quit the app so the order depends on the + // user interaction: closing the last window will first trigger onBeforeWindowClose() + // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() + // and then onBeforeWindowClose(). + // + // Here is the behavior on different OS depending on action taken (Electron 1.7.x): + // + // Legend + // - quit(N): quit application with N windows opened + // - close(1): close one window via the window close button + // - closeAll: close all windows via the taskbar command + // - onBeforeShutdown(N): number of windows reported in this event handler + // - onBeforeWindowClose(N, M): number of windows reported and quitRequested boolean in this event handler + // + // macOS + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - quit(0): onBeforeShutdown(0) + // - close(1): onBeforeWindowClose(1, false) + // + // Windows + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - close(1): onBeforeWindowClose(2, false)[not last window] + // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // + // Linux + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - close(1): onBeforeWindowClose(2, false)[not last window] + // - close(1): onBeforeWindowClose(1, false), onBeforeShutdown(0)[last window] + // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) + // + private onBeforeShutdown(): void { + this.shuttingDown = true; + + this.saveWindowsState(); + } + + private saveWindowsState(): void { + const currentWindowsState: IWindowsState = { + openedWindows: [], + lastPluginDevelopmentHostWindow: this._windowsState.lastPluginDevelopmentHostWindow, + lastActiveWindow: this._lastClosedState + }; + + // 1.) Find a last active window (pick any other first window otherwise) + if (!currentWindowsState.lastActiveWindow) { + let activeWindow = this.windowsMainService.getLastActiveWindow(); + if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { + activeWindow = this.windowsMainService.getWindows().find(window => !window.isExtensionDevelopmentHost); + } + + if (activeWindow) { + currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); + } + } + + // 2.) Find extension host window + const extensionHostWindow = this.windowsMainService.getWindows().find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); + if (extensionHostWindow) { + currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); + } + + // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update + // + // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) + // so if we ever want to persist the UI state of the last closed window (window count === 1), it has + // to come from the stored lastClosedWindowState on Win/Linux at least + if (this.windowsMainService.getWindowCount() > 1) { + currentWindowsState.openedWindows = this.windowsMainService.getWindows().filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); + } + + // Persist + const state = getWindowsStateStoreData(currentWindowsState); + this.stateService.setItem(WindowsStateHandler.windowsStateStorageKey, state); + + if (this.shuttingDown) { + this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state); + } + } + + // See note on #onBeforeShutdown() for details how these events are flowing + private onBeforeWindowClose(window: ICodeWindow): void { + if (this.lifecycleMainService.quitRequested) { + return; // during quit, many windows close in parallel so let it be handled in the before-quit handler + } + + // On Window close, update our stored UI state of this window + const state: IWindowState = this.toWindowState(window); + if (window.isExtensionDevelopmentHost && !window.isExtensionTestHost) { + this._windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state + } + + // Any non extension host window with same workspace or folder + else if (!window.isExtensionDevelopmentHost && (!!window.openedWorkspace || !!window.openedFolderUri)) { + this._windowsState.openedWindows.forEach(openedWindow => { + const sameWorkspace = window.openedWorkspace && openedWindow.workspace && openedWindow.workspace.id === window.openedWorkspace.id; + const sameFolder = window.openedFolderUri && openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, window.openedFolderUri); + + if (sameWorkspace || sameFolder) { + openedWindow.uiState = state.uiState; + } + }); + } + + // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state + // before quitting, we need to remember the UI state of this window to be able to persist it. + // On macOS we keep the last closed window state ready in case the user wants to quit right after or + // wants to open another window, in which case we use this state over the persisted one. + if (this.windowsMainService.getWindowCount() === 1) { + this._lastClosedState = state; + } + } + + private toWindowState(window: ICodeWindow): IWindowState { + return { + workspace: window.openedWorkspace, + folderUri: window.openedFolderUri, + backupPath: window.backupPath, + remoteAuthority: window.remoteAuthority, + uiState: window.serializeWindowState() + }; + } +} + +export function restoreWindowsState(data: WindowsStateStorageData | undefined): IWindowsState { + const result: IWindowsState = { openedWindows: [] }; + const windowsState = data as ISerializedWindowsState || { openedWindows: [] }; + + if (windowsState.lastActiveWindow) { + result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); + } + + if (windowsState.lastPluginDevelopmentHostWindow) { + result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); + } + + if (Array.isArray(windowsState.openedWindows)) { + result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); + } + + return result; +} + +function restoreWindowState(windowState: ISerializedWindowState): IWindowState { + const result: IWindowState = { uiState: windowState.uiState }; + if (windowState.backupPath) { + result.backupPath = windowState.backupPath; + } + + if (windowState.remoteAuthority) { + result.remoteAuthority = windowState.remoteAuthority; + } + + if (windowState.folder) { + result.folderUri = URI.parse(windowState.folder); + } else if (windowState.folderUri) { + result.folderUri = URI.revive(windowState.folderUri); + } else if (windowState.folderPath) { + result.folderUri = URI.file(windowState.folderPath); + } + + if (windowState.workspaceIdentifier) { + result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; + } else if (windowState.workspace) { + result.workspace = { id: windowState.workspace.id, configPath: URI.file(windowState.workspace.configPath) }; + } + + return result; +} + +export function getWindowsStateStoreData(windowsState: IWindowsState): WindowsStateStorageData { + return { + lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), + lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), + openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) + }; +} + +function serializeWindowState(windowState: IWindowState): ISerializedWindowState { + return { + workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, + folder: windowState.folderUri && windowState.folderUri.toString(), + backupPath: windowState.backupPath, + remoteAuthority: windowState.remoteAuthority, + uiState: windowState.uiState + }; +} diff --git a/src/vs/platform/windows/electron-main/windowsStateStorage.ts b/src/vs/platform/windows/electron-main/windowsStateStorage.ts deleted file mode 100644 index 15a7d12dd0b..00000000000 --- a/src/vs/platform/windows/electron-main/windowsStateStorage.ts +++ /dev/null @@ -1,107 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI, UriComponents } from 'vs/base/common/uri'; -import { IWindowState as IWindowUIState } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; - -export type WindowsStateStorageData = object; - -export interface IWindowState { - workspace?: IWorkspaceIdentifier; - folderUri?: URI; - backupPath?: string; - remoteAuthority?: string; - uiState: IWindowUIState; -} - -export interface IWindowsState { - lastActiveWindow?: IWindowState; - lastPluginDevelopmentHostWindow?: IWindowState; - openedWindows: IWindowState[]; -} - -interface ISerializedWindowsState { - readonly lastActiveWindow?: ISerializedWindowState; - readonly lastPluginDevelopmentHostWindow?: ISerializedWindowState; - readonly openedWindows: ISerializedWindowState[]; -} - -interface ISerializedWindowState { - readonly workspaceIdentifier?: { id: string; configURIPath: string }; - readonly folder?: string; - readonly backupPath?: string; - readonly remoteAuthority?: string; - readonly uiState: IWindowUIState; - - // deprecated - readonly folderUri?: UriComponents; - readonly folderPath?: string; - readonly workspace?: { id: string; configPath: string }; -} - -export function restoreWindowsState(data: WindowsStateStorageData | undefined): IWindowsState { - const result: IWindowsState = { openedWindows: [] }; - const windowsState = data as ISerializedWindowsState || { openedWindows: [] }; - - if (windowsState.lastActiveWindow) { - result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); - } - - if (windowsState.lastPluginDevelopmentHostWindow) { - result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); - } - - if (Array.isArray(windowsState.openedWindows)) { - result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); - } - - return result; -} - -function restoreWindowState(windowState: ISerializedWindowState): IWindowState { - const result: IWindowState = { uiState: windowState.uiState }; - if (windowState.backupPath) { - result.backupPath = windowState.backupPath; - } - - if (windowState.remoteAuthority) { - result.remoteAuthority = windowState.remoteAuthority; - } - - if (windowState.folder) { - result.folderUri = URI.parse(windowState.folder); - } else if (windowState.folderUri) { - result.folderUri = URI.revive(windowState.folderUri); - } else if (windowState.folderPath) { - result.folderUri = URI.file(windowState.folderPath); - } - - if (windowState.workspaceIdentifier) { - result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; - } else if (windowState.workspace) { - result.workspace = { id: windowState.workspace.id, configPath: URI.file(windowState.workspace.configPath) }; - } - - return result; -} - -export function getWindowsStateStoreData(windowsState: IWindowsState): WindowsStateStorageData { - return { - lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), - lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), - openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) - }; -} - -function serializeWindowState(windowState: IWindowState): ISerializedWindowState { - return { - workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, - folder: windowState.folderUri && windowState.folderUri.toString(), - backupPath: windowState.backupPath, - remoteAuthority: windowState.remoteAuthority, - uiState: windowState.uiState - }; -} diff --git a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts similarity index 99% rename from src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts rename to src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts index 5ee6a8683a7..b5b38d68845 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateHandler.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; -import { restoreWindowsState, getWindowsStateStoreData, IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsStateStorage'; +import { restoreWindowsState, getWindowsStateStoreData, IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsStateHandler'; import { IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri';