From d51605bed8c82080280511e5eb96aad05e91c329 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 21 Aug 2017 10:19:00 +0200 Subject: [PATCH] multi root - carry over UI state to workspace --- src/vs/code/electron-main/windows.ts | 2 +- src/vs/platform/lifecycle/common/lifecycle.ts | 1 + .../lifecycle/electron-main/lifecycleMain.ts | 10 +- src/vs/platform/storage/common/migration.ts | 198 ++++++++++++++ .../platform/storage/common/storageService.ts | 48 ++-- .../storage/test/browser/migration.test.ts | 252 ++++++++++++++++++ .../platform/workspaces/common/workspaces.ts | 6 + src/vs/workbench/electron-browser/shell.ts | 40 ++- .../electron-browser/lifecycleService.ts | 9 +- 9 files changed, 537 insertions(+), 29 deletions(-) create mode 100644 src/vs/platform/storage/common/migration.ts create mode 100644 src/vs/platform/storage/test/browser/migration.test.ts diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 441a142d55e9c..286539c8d8a5c 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -1188,7 +1188,7 @@ export class WindowsManager implements IWindowsMainService { window.focus(); // Only open workspace when the window has not vetoed this - return this.lifecycleService.unload(window, UnloadReason.RELOAD).done(veto => { + return this.lifecycleService.unload(window, UnloadReason.RELOAD, workspace).done(veto => { if (!veto) { // Register window for backups and migrate current backups over diff --git a/src/vs/platform/lifecycle/common/lifecycle.ts b/src/vs/platform/lifecycle/common/lifecycle.ts index e4a776067be2f..0a413c08ce955 100644 --- a/src/vs/platform/lifecycle/common/lifecycle.ts +++ b/src/vs/platform/lifecycle/common/lifecycle.ts @@ -21,6 +21,7 @@ export const ILifecycleService = createDecorator('lifecycleSe export interface ShutdownEvent { veto(value: boolean | TPromise): void; reason: ShutdownReason; + payload?: object; } export enum ShutdownReason { diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index ac9c926716844..6bc8ab87990bf 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -61,7 +61,7 @@ export interface ILifecycleService { ready(): void; registerWindow(window: ICodeWindow): void; - unload(window: ICodeWindow, reason: UnloadReason): TPromise; + unload(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise; relaunch(options?: { addArgs?: string[], removeArgs?: string[] }); @@ -179,7 +179,7 @@ export class LifecycleService implements ILifecycleService { }); } - public unload(window: ICodeWindow, reason: UnloadReason): TPromise { + public unload(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise { // Always allow to unload a window that is not yet ready if (window.readyState !== ReadyState.READY) { @@ -191,7 +191,7 @@ export class LifecycleService implements ILifecycleService { const windowUnloadReason = this.quitRequested ? UnloadReason.QUIT : reason; // first ask the window itself if it vetos the unload - return this.doUnloadWindowInRenderer(window, windowUnloadReason).then(veto => { + return this.doUnloadWindowInRenderer(window, windowUnloadReason, payload).then(veto => { if (veto) { return this.handleVeto(veto); } @@ -213,7 +213,7 @@ export class LifecycleService implements ILifecycleService { return veto; } - private doUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason): TPromise { + private doUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise { return new TPromise((c) => { const oneTimeEventToken = this.oneTimeListenerTokenGenerator++; const okChannel = `vscode:ok${oneTimeEventToken}`; @@ -227,7 +227,7 @@ export class LifecycleService implements ILifecycleService { c(true); // veto }); - window.send('vscode:beforeUnload', { okChannel, cancelChannel, reason }); + window.send('vscode:beforeUnload', { okChannel, cancelChannel, reason, payload }); }); } diff --git a/src/vs/platform/storage/common/migration.ts b/src/vs/platform/storage/common/migration.ts new file mode 100644 index 0000000000000..500a500208cd2 --- /dev/null +++ b/src/vs/platform/storage/common/migration.ts @@ -0,0 +1,198 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IStorage, StorageService } from "vs/platform/storage/common/storageService"; +import { endsWith, startsWith, rtrim } from "vs/base/common/strings"; +import URI from "vs/base/common/uri"; +import { IWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; + +/** + * We currently store local storage with the following format: + * + * [Global] + * storage://global/ + * + * [Workspace] + * storage://workspace// + * storage://workspace/empty:/ + * storage://workspace/root:/ + * + * + * macOS/Linux: /some/folder/path + * Windows: c%3A/Users/name/folder (normal path) + * file://localhost/c%24/name/folder (unc path) + * + * [no workspace] + * storage://workspace/__$noWorkspace__ + * => no longer being used (used for empty workspaces previously) + */ + +const EMPTY_WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/empty:`; +const MULTI_ROOT_WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/root:`; + +export type StorageObject = { [key: string]: string }; + +export interface IParsedStorage { + global: Map; + multiRoot: Map; + folder: Map; + empty: Map; +} + +/** + * Parses the local storage implementation into global, multi root, folder and empty storage. + */ +export function parseStorage(storage: IStorage): IParsedStorage { + const globalStorage = new Map(); + const folderWorkspacesStorage = new Map(); + const emptyWorkspacesStorage = new Map(); + const multiRootWorkspacesStorage = new Map(); + + const workspaces: { prefix: string; resource: string; }[] = []; + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + + // Workspace Storage (storage://workspace/) + if (startsWith(key, StorageService.WORKSPACE_PREFIX)) { + + // We are looking for key: storage://workspace//workspaceIdentifier to be able to find all folder + // paths that are known to the storage. is the only way how to parse all folder paths known in storage. + if (endsWith(key, StorageService.WORKSPACE_IDENTIFIER)) { + + // storage://workspace//workspaceIdentifier => / + let workspace = key.substring(StorageService.WORKSPACE_PREFIX.length, key.length - StorageService.WORKSPACE_IDENTIFIER.length); + + // macOS/Unix: Users/name/folder/ + // Windows: c%3A/Users/name/folder/ + if (!startsWith(workspace, 'file:')) { + workspace = `file:///${rtrim(workspace, '/')}`; + } + + // Windows UNC path: file://localhost/c%3A/Users/name/folder/ + else { + workspace = rtrim(workspace, '/'); + } + + // storage://workspace//workspaceIdentifier => storage://workspace// + const prefix = key.substr(0, key.length - StorageService.WORKSPACE_IDENTIFIER.length); + workspaces.push({ prefix, resource: workspace }); + } + + // Empty workspace key: storage://workspace/empty:/ + else if (startsWith(key, EMPTY_WORKSPACE_PREFIX)) { + + // storage://workspace/empty:/ => + const emptyWorkspaceId = key.substring(EMPTY_WORKSPACE_PREFIX.length, key.indexOf('/', EMPTY_WORKSPACE_PREFIX.length)); + const emptyWorkspaceResource = URI.from({ path: emptyWorkspaceId, scheme: 'empty' }).toString(); + + let emptyWorkspaceStorage = emptyWorkspacesStorage.get(emptyWorkspaceResource); + if (!emptyWorkspaceStorage) { + emptyWorkspaceStorage = Object.create(null); + emptyWorkspacesStorage.set(emptyWorkspaceResource, emptyWorkspaceStorage); + } + + // storage://workspace/empty:/someKey => someKey + const storageKey = key.substr(EMPTY_WORKSPACE_PREFIX.length + emptyWorkspaceId.length + 1 /* trailing / */); + + emptyWorkspaceStorage[storageKey] = storage.getItem(key); + } + + // Multi root workspace key: storage://workspace/root:/ + else if (startsWith(key, MULTI_ROOT_WORKSPACE_PREFIX)) { + + // storage://workspace/root:/ => + const multiRootWorkspaceId = key.substring(MULTI_ROOT_WORKSPACE_PREFIX.length, key.indexOf('/', MULTI_ROOT_WORKSPACE_PREFIX.length)); + const multiRootWorkspaceResource = URI.from({ path: multiRootWorkspaceId, scheme: 'root' }).toString(); + + let multiRootWorkspaceStorage = multiRootWorkspacesStorage.get(multiRootWorkspaceResource); + if (!multiRootWorkspaceStorage) { + multiRootWorkspaceStorage = Object.create(null); + multiRootWorkspacesStorage.set(multiRootWorkspaceResource, multiRootWorkspaceStorage); + } + + // storage://workspace/root:/someKey => someKey + const storageKey = key.substr(MULTI_ROOT_WORKSPACE_PREFIX.length + multiRootWorkspaceId.length + 1 /* trailing / */); + + multiRootWorkspaceStorage[storageKey] = storage.getItem(key); + } + } + + // Global Storage (storage://global) + else if (startsWith(key, StorageService.GLOBAL_PREFIX)) { + + // storage://global/someKey => someKey + const globalStorageKey = key.substr(StorageService.GLOBAL_PREFIX.length); + if (startsWith(globalStorageKey, StorageService.COMMON_PREFIX)) { + continue; // filter out faulty keys that have the form storage://something/storage:// + } + + globalStorage.set(globalStorageKey, storage.getItem(key)); + } + } + + // With all the folder paths known we can now extract storage for each path. We have to go through all workspaces + // from the longest path first to reliably extract the storage. The reason is that one folder path can be a parent + // of another folder path and as such a simple indexOf check is not enough. + const workspacesByLength = workspaces.sort((w1, w2) => w1.prefix.length >= w2.prefix.length ? -1 : 1); + const handledKeys = new Map(); + workspacesByLength.forEach(workspace => { + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + + if (handledKeys.has(key) || !startsWith(key, workspace.prefix)) { + continue; // not part of workspace prefix or already handled + } + + handledKeys.set(key, true); + + let folderWorkspaceStorage = folderWorkspacesStorage.get(workspace.resource); + if (!folderWorkspaceStorage) { + folderWorkspaceStorage = Object.create(null); + folderWorkspacesStorage.set(workspace.resource, folderWorkspaceStorage); + } + + // storage://workspace//someKey => someKey + const storageKey = key.substr(workspace.prefix.length); + + folderWorkspaceStorage[storageKey] = storage.getItem(key); + } + }); + + return { + global: globalStorage, + multiRoot: multiRootWorkspacesStorage, + folder: folderWorkspacesStorage, + empty: emptyWorkspacesStorage + }; +} + +export function migrateStorageToMultiRootWorkspace(fromWorkspaceId: string, toWorkspaceId: IWorkspaceIdentifier, storage: IStorage): void { + const parsed = parseStorage(storage); + + const newStorageKey = URI.from({ path: toWorkspaceId.id, scheme: 'root' }).toString(); + + // Find in which location the workspace storage is to be migrated rom + let storageForWorkspace: StorageObject; + if (parsed.multiRoot.has(fromWorkspaceId)) { + storageForWorkspace = parsed.multiRoot.get(fromWorkspaceId); + } else if (parsed.empty.has(fromWorkspaceId)) { + storageForWorkspace = parsed.empty.get(fromWorkspaceId); + } else if (parsed.folder.has(fromWorkspaceId)) { + storageForWorkspace = parsed.folder.get(fromWorkspaceId); + } + + // Migrate existing storage to new workspace id + if (storageForWorkspace) { + Object.keys(storageForWorkspace).forEach(key => { + if (key === StorageService.WORKSPACE_IDENTIFIER) { + return; // make sure to never migrate the workspace identifier + } + + storage.setItem(`${StorageService.WORKSPACE_PREFIX}${newStorageKey}/${key}`, storageForWorkspace[key]); + }); + } +} \ No newline at end of file diff --git a/src/vs/platform/storage/common/storageService.ts b/src/vs/platform/storage/common/storageService.ts index a5e2c7fa5322f..ca3e61f326182 100644 --- a/src/vs/platform/storage/common/storageService.ts +++ b/src/vs/platform/storage/common/storageService.ts @@ -23,25 +23,25 @@ export class StorageService implements IStorageService { public _serviceBrand: any; - private static COMMON_PREFIX = 'storage://'; - private static GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`; - private static WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`; - private static WORKSPACE_IDENTIFIER = 'workspaceIdentifier'; - private static NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__'; + public static COMMON_PREFIX = 'storage://'; + public static GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`; + public static WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`; + public static WORKSPACE_IDENTIFIER = 'workspaceidentifier'; + public static NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__'; - private workspaceStorage: IStorage; - private globalStorage: IStorage; + private _workspaceStorage: IStorage; + private _globalStorage: IStorage; private workspaceKey: string; constructor( globalStorage: IStorage, workspaceStorage: IStorage, - workspaceId?: string, + private workspaceId?: string, legacyWorkspaceId?: number ) { - this.globalStorage = globalStorage; - this.workspaceStorage = workspaceStorage || globalStorage; + this._globalStorage = globalStorage; + this._workspaceStorage = workspaceStorage || globalStorage; // Calculate workspace storage key this.workspaceKey = this.getWorkspaceKey(workspaceId); @@ -53,6 +53,18 @@ export class StorageService implements IStorageService { } } + public get storageId(): string { + return this.workspaceId; + } + + public get globalStorage(): IStorage { + return this._globalStorage; + } + + public get workspaceStorage(): IStorage { + return this._workspaceStorage; + } + private getWorkspaceKey(id?: string): string { if (!id) { return StorageService.NO_WORKSPACE_IDENTIFIER; @@ -77,10 +89,10 @@ export class StorageService implements IStorageService { if (types.isNumber(id) && workspaceUid !== id) { const keyPrefix = this.toStorageKey('', StorageScope.WORKSPACE); const toDelete: string[] = []; - const length = this.workspaceStorage.length; + const length = this._workspaceStorage.length; for (let i = 0; i < length; i++) { - const key = this.workspaceStorage.key(i); + const key = this._workspaceStorage.key(i); if (key.indexOf(StorageService.WORKSPACE_PREFIX) < 0) { continue; // ignore stored things that don't belong to storage service or are defined globally } @@ -93,7 +105,7 @@ export class StorageService implements IStorageService { // Run the delete toDelete.forEach((keyToDelete) => { - this.workspaceStorage.removeItem(keyToDelete); + this._workspaceStorage.removeItem(keyToDelete); }); } @@ -104,12 +116,12 @@ export class StorageService implements IStorageService { } public clear(): void { - this.globalStorage.clear(); - this.workspaceStorage.clear(); + this._globalStorage.clear(); + this._workspaceStorage.clear(); } public store(key: string, value: any, scope = StorageScope.GLOBAL): void { - const storage = (scope === StorageScope.GLOBAL) ? this.globalStorage : this.workspaceStorage; + const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage; if (types.isUndefinedOrNull(value)) { this.remove(key, scope); // we cannot store null or undefined, in that case we remove the key @@ -127,7 +139,7 @@ export class StorageService implements IStorageService { } public get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string { - const storage = (scope === StorageScope.GLOBAL) ? this.globalStorage : this.workspaceStorage; + const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage; const value = storage.getItem(this.toStorageKey(key, scope)); if (types.isUndefinedOrNull(value)) { @@ -138,7 +150,7 @@ export class StorageService implements IStorageService { } public remove(key: string, scope = StorageScope.GLOBAL): void { - const storage = (scope === StorageScope.GLOBAL) ? this.globalStorage : this.workspaceStorage; + const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage; const storageKey = this.toStorageKey(key, scope); // Remove diff --git a/src/vs/platform/storage/test/browser/migration.test.ts b/src/vs/platform/storage/test/browser/migration.test.ts new file mode 100644 index 0000000000000..f14337c8bb50c --- /dev/null +++ b/src/vs/platform/storage/test/browser/migration.test.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { StorageService } from "vs/platform/storage/common/storageService"; +import { parseStorage, migrateStorageToMultiRootWorkspace } from "vs/platform/storage/common/migration"; +import URI from "vs/base/common/uri"; +import { StorageScope } from "vs/platform/storage/common/storage"; +import { startsWith } from "vs/base/common/strings"; + +suite('Storage Migration', () => { + let storage = window.localStorage; + + setup(() => { + storage.clear(); + }); + + teardown(() => { + storage.clear(); + }); + + test('Parse Storage (Global)', () => { + const service = createService(); + + const parsed = parseStorage(storage); + + assert.equal(parsed.global.size, 4); + assert.equal(parsed.global.get('key1'), service.get('key1')); + assert.equal(parsed.global.get('key2.something'), service.get('key2.something')); + assert.equal(parsed.global.get('key3/special'), service.get('key3/special')); + assert.equal(parsed.global.get('key4 space'), service.get('key4 space')); + }); + + test('Parse Storage (mixed)', () => { + + // Fill the storage with multiple workspaces of all kinds (empty, root, folders) + const workspaceIds = [ + + // Multi Root Workspace + URI.from({ path: '1500007676869', scheme: 'root' }).toString(), + URI.from({ path: '2500007676869', scheme: 'root' }).toString(), + URI.from({ path: '3500007676869', scheme: 'root' }).toString(), + + // Empty Workspace + URI.from({ path: '4500007676869', scheme: 'empty' }).toString(), + URI.from({ path: '5500007676869', scheme: 'empty' }).toString(), + URI.from({ path: '6500007676869', scheme: 'empty' }).toString(), + + // Unix Paths + URI.file('/some/folder/folder1').toString(), + URI.file('/some/folder/folder2').toString(), + URI.file('/some/folder/folder3').toString(), + URI.file('/some/folder/folder1/sub1').toString(), + URI.file('/some/folder/folder2/sub2').toString(), + URI.file('/some/folder/folder3/sub3').toString(), + + // Windows Paths + URI.file('c:\\some\\folder\\folder1').toString(), + URI.file('c:\\some\\folder\\folder2').toString(), + URI.file('c:\\some\\folder\\folder3').toString(), + URI.file('c:\\some\\folder\\folder1\\sub1').toString(), + URI.file('c:\\some\\folder\\folder2\\sub2').toString(), + URI.file('c:\\some\\folder\\folder3\\sub3').toString(), + + // UNC Paths + 'file://localhost/c%3A/some/folder/folder1', + 'file://localhost/c%3A/some/folder/folder2', + 'file://localhost/c%3A/some/folder/folder3', + 'file://localhost/c%3A/some/folder/folder1/sub1', + 'file://localhost/c%3A/some/folder/folder2/sub2', + 'file://localhost/c%3A/some/folder/folder3/sub3' + ]; + + const services = workspaceIds.map(id => createService(id)); + + const parsed = parseStorage(storage); + + services.forEach((service, index) => { + let expectedKeyCount = 4; + let storageToTest; + + const workspaceId = workspaceIds[index]; + if (startsWith(workspaceId, 'file:')) { + storageToTest = parsed.folder.get(workspaceId); + expectedKeyCount++; // workspaceIdentifier gets added! + } else if (startsWith(workspaceId, 'empty:')) { + storageToTest = parsed.empty.get(workspaceId); + } else if (startsWith(workspaceId, 'root:')) { + storageToTest = parsed.multiRoot.get(workspaceId); + } + + assert.equal(Object.keys(storageToTest).length, expectedKeyCount); + assert.equal(storageToTest['key1'], service.get('key1', StorageScope.WORKSPACE)); + assert.equal(storageToTest['key2.something'], service.get('key2.something', StorageScope.WORKSPACE)); + assert.equal(storageToTest['key3/special'], service.get('key3/special', StorageScope.WORKSPACE)); + assert.equal(storageToTest['key4 space'], service.get('key4 space', StorageScope.WORKSPACE)); + }); + }); + + test('Parse Storage (handle subfolders properly)', () => { + const ws1 = URI.file('/some/folder/folder1').toString(); + const ws2 = URI.file('/some/folder/folder1/sub1').toString(); + + const s1 = new StorageService(storage, storage, ws1, Date.now()); + const s2 = new StorageService(storage, storage, ws2, Date.now()); + + s1.store('s1key1', 'value1', StorageScope.WORKSPACE); + s1.store('s1key2.something', JSON.stringify({ foo: 'bar' }), StorageScope.WORKSPACE); + s1.store('s1key3/special', true, StorageScope.WORKSPACE); + s1.store('s1key4 space', 4, StorageScope.WORKSPACE); + + s2.store('s2key1', 'value1', StorageScope.WORKSPACE); + s2.store('s2key2.something', JSON.stringify({ foo: 'bar' }), StorageScope.WORKSPACE); + s2.store('s2key3/special', true, StorageScope.WORKSPACE); + s2.store('s2key4 space', 4, StorageScope.WORKSPACE); + + const parsed = parseStorage(storage); + + const s1Storage = parsed.folder.get(ws1); + assert.equal(Object.keys(s1Storage).length, 5); + assert.equal(s1Storage['s1key1'], s1.get('s1key1', StorageScope.WORKSPACE)); + assert.equal(s1Storage['s1key2.something'], s1.get('s1key2.something', StorageScope.WORKSPACE)); + assert.equal(s1Storage['s1key3/special'], s1.get('s1key3/special', StorageScope.WORKSPACE)); + assert.equal(s1Storage['s1key4 space'], s1.get('s1key4 space', StorageScope.WORKSPACE)); + + const s2Storage = parsed.folder.get(ws2); + assert.equal(Object.keys(s2Storage).length, 5); + assert.equal(s2Storage['s2key1'], s2.get('s2key1', StorageScope.WORKSPACE)); + assert.equal(s2Storage['s2key2.something'], s2.get('s2key2.something', StorageScope.WORKSPACE)); + assert.equal(s2Storage['s2key3/special'], s2.get('s2key3/special', StorageScope.WORKSPACE)); + assert.equal(s2Storage['s2key4 space'], s2.get('s2key4 space', StorageScope.WORKSPACE)); + }); + + function createService(workspaceId?: string): StorageService { + const service = new StorageService(storage, storage, workspaceId, workspaceId && startsWith(workspaceId, 'file:') ? Date.now() : void 0); + + // Unrelated + storage.setItem('foo', 'bar'); + storage.setItem('storage://foo', 'bar'); + storage.setItem('storage://global/storage://foo', 'bar'); + + // Global + service.store('key1', 'value1'); + service.store('key2.something', JSON.stringify({ foo: 'bar' })); + service.store('key3/special', true); + service.store('key4 space', 4); + + // Workspace + service.store('key1', 'value1', StorageScope.WORKSPACE); + service.store('key2.something', JSON.stringify({ foo: 'bar' }), StorageScope.WORKSPACE); + service.store('key3/special', true, StorageScope.WORKSPACE); + service.store('key4 space', 4, StorageScope.WORKSPACE); + + return service; + } + + test('Migrate Storage (folder (Unix) => multi root)', () => { + const workspaceToMigrateFrom = URI.file('/some/folder/folder1').toString(); + createService(workspaceToMigrateFrom); + + const workspaceToMigrateTo = URI.from({ path: '1500007676869', scheme: 'root' }).toString(); + + migrateStorageToMultiRootWorkspace(workspaceToMigrateFrom, { id: '1500007676869', configPath: null }, storage); + + const s2 = new StorageService(storage, storage, workspaceToMigrateTo); + const parsed = parseStorage(storage); + + assert.equal(parsed.empty.size, 0); + assert.equal(parsed.folder.size, 1); + assert.equal(parsed.multiRoot.size, 1); + + const migratedStorage = parsed.multiRoot.get(workspaceToMigrateTo); + assert.equal(Object.keys(migratedStorage).length, 4); + assert.equal(migratedStorage['key1'], s2.get('key1', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key2.something'], s2.get('key2.something', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key3/special'], s2.get('key3/special', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key4 space'], s2.get('key4 space', StorageScope.WORKSPACE)); + }); + + test('Migrate Storage (folder (Windows) => multi root)', () => { + const workspaceToMigrateFrom = URI.file('c:\\some\\folder\\folder1').toString(); + createService(workspaceToMigrateFrom); + + const workspaceToMigrateTo = URI.from({ path: '1500007676869', scheme: 'root' }).toString(); + + migrateStorageToMultiRootWorkspace(workspaceToMigrateFrom, { id: '1500007676869', configPath: null }, storage); + + const s2 = new StorageService(storage, storage, workspaceToMigrateTo); + const parsed = parseStorage(storage); + + assert.equal(parsed.empty.size, 0); + assert.equal(parsed.folder.size, 1); + assert.equal(parsed.multiRoot.size, 1); + + const migratedStorage = parsed.multiRoot.get(workspaceToMigrateTo); + assert.equal(Object.keys(migratedStorage).length, 4); + assert.equal(migratedStorage['key1'], s2.get('key1', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key2.something'], s2.get('key2.something', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key3/special'], s2.get('key3/special', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key4 space'], s2.get('key4 space', StorageScope.WORKSPACE)); + }); + + test('Migrate Storage (folder (Windows UNC) => multi root)', () => { + const workspaceToMigrateFrom = 'file://localhost/c%3A/some/folder/folder1'; + createService(workspaceToMigrateFrom); + + const workspaceToMigrateTo = URI.from({ path: '1500007676869', scheme: 'root' }).toString(); + + migrateStorageToMultiRootWorkspace(workspaceToMigrateFrom, { id: '1500007676869', configPath: null }, storage); + + const s2 = new StorageService(storage, storage, workspaceToMigrateTo); + const parsed = parseStorage(storage); + + assert.equal(parsed.empty.size, 0); + assert.equal(parsed.folder.size, 1); + assert.equal(parsed.multiRoot.size, 1); + + const migratedStorage = parsed.multiRoot.get(workspaceToMigrateTo); + assert.equal(Object.keys(migratedStorage).length, 4); + assert.equal(migratedStorage['key1'], s2.get('key1', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key2.something'], s2.get('key2.something', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key3/special'], s2.get('key3/special', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key4 space'], s2.get('key4 space', StorageScope.WORKSPACE)); + }); + + test('Migrate Storage (empty => multi root)', () => { + const workspaceToMigrateFrom = URI.from({ path: '1500007676869', scheme: 'empty' }).toString(); + createService(workspaceToMigrateFrom); + + const workspaceToMigrateTo = URI.from({ path: '2500007676869', scheme: 'root' }).toString(); + + migrateStorageToMultiRootWorkspace(workspaceToMigrateFrom, { id: '2500007676869', configPath: null }, storage); + + const s2 = new StorageService(storage, storage, workspaceToMigrateTo); + const parsed = parseStorage(storage); + + assert.equal(parsed.empty.size, 1); + assert.equal(parsed.folder.size, 0); + assert.equal(parsed.multiRoot.size, 1); + + const migratedStorage = parsed.multiRoot.get(workspaceToMigrateTo); + assert.equal(Object.keys(migratedStorage).length, 4); + assert.equal(migratedStorage['key1'], s2.get('key1', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key2.something'], s2.get('key2.something', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key3/special'], s2.get('key3/special', StorageScope.WORKSPACE)); + assert.equal(migratedStorage['key4 space'], s2.get('key4 space', StorageScope.WORKSPACE)); + }); +}); \ No newline at end of file diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d9d67cea8c26f..afbca78f21504 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -87,4 +87,10 @@ export function getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFold export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier { return typeof obj === 'string'; +} + +export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier { + const workspaceIdentifier = obj as IWorkspaceIdentifier; + + return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && typeof workspaceIdentifier.configPath === 'string'; } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 593ee95af9c38..5cc9207223731 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -55,7 +55,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase, ShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IMessageService, IChoiceService, Severity } from 'vs/platform/message/common/message'; @@ -91,6 +91,10 @@ import { foreground, selectionBackground, focusBorder, scrollbarShadow, scrollba import { TextMateService } from 'vs/workbench/services/textMate/electron-browser/TMSyntax'; import { ITextMateService } from 'vs/workbench/services/textMate/electron-browser/textMateService'; import { IBroadcastService, BroadcastService } from "vs/platform/broadcast/electron-browser/broadcastService"; +import { isWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces"; +import { StorageService } from "vs/platform/storage/common/storageService"; +import { migrateStorageToMultiRootWorkspace } from "vs/platform/storage/common/migration"; +import { once } from "vs/base/common/event"; /** * Services that we require for the Shell @@ -131,6 +135,7 @@ export class WorkbenchShell { private previousErrorTime: number; private content: HTMLElement; private contentsContainer: Builder; + private shutdownListener: IDisposable; private configuration: IWindowConfiguration; private workbench: Workbench; @@ -314,6 +319,7 @@ export class WorkbenchShell { const lifecycleService = instantiationService.createInstance(LifecycleService); this.toUnbind.push(lifecycleService.onShutdown(reason => dispose(disposables))); this.toUnbind.push(lifecycleService.onShutdown(reason => saveFontInfo(this.storageService))); + this.toUnbind.push(lifecycleService.onWillShutdown(event => this.onWillShutdown(event))); serviceCollection.set(ILifecycleService, lifecycleService); disposables.push(lifecycleTelemetry(this.telemetryService, lifecycleService)); this.lifecycleService = lifecycleService; @@ -438,6 +444,33 @@ export class WorkbenchShell { return this.workbench.joinCreation(); } + private onWillShutdown(event: ShutdownEvent): void { + + // The shutdown sequence could have been stopped due to a veto. Make sure to + // always dispose the shutdown listener if we are called again in the same session. + if (this.shutdownListener) { + this.shutdownListener.dispose(); + this.shutdownListener = void 0; + } + + if (event.reason === ShutdownReason.RELOAD) { + const workspace = event.payload; + + // We are transitioning into a workspace from an empty workspace or folder workspace + // As such we want to migrate UI state from the current workspace to the new one. Since + // many components write to storage only on shutdown, we register a shutdown listener + // very late to be called as the last one. + if (isWorkspaceIdentifier(workspace) && !this.contextService.hasMultiFolderWorkspace()) { + this.shutdownListener = once(this.lifecycleService.onShutdown)(() => { + + // TODO@Ben revisit this when we move away from local storage to a file based approach + const storageImpl = this.storageService as StorageService; + migrateStorageToMultiRootWorkspace(storageImpl.storageId, workspace, storageImpl.workspaceStorage); + }); + } + } + } + public dispose(): void { // Workbench @@ -450,6 +483,11 @@ export class WorkbenchShell { // Listeners this.toUnbind = dispose(this.toUnbind); + if (this.shutdownListener) { + this.shutdownListener.dispose(); + this.shutdownListener = void 0; + } + // Container $(this.container).empty(); } diff --git a/src/vs/workbench/services/lifecycle/electron-browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-browser/lifecycleService.ts index 3db16b44b0eb7..f043ae7dba4da 100644 --- a/src/vs/workbench/services/lifecycle/electron-browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-browser/lifecycleService.ts @@ -76,12 +76,12 @@ export class LifecycleService implements ILifecycleService { const windowId = this._windowService.getCurrentWindowId(); // Main side indicates that window is about to unload, check for vetos - ipc.on('vscode:beforeUnload', (event, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason }) => { + ipc.on('vscode:beforeUnload', (event, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason, payload: object }) => { this.phase = LifecyclePhase.ShuttingDown; this._storageService.store(LifecycleService._lastShutdownReasonKey, JSON.stringify(reply.reason), StorageScope.WORKSPACE); // trigger onWillShutdown events and veto collecting - this.onBeforeUnload(reply.reason).done(veto => { + this.onBeforeUnload(reply.reason, reply.payload).done(veto => { if (veto) { this._storageService.remove(LifecycleService._lastShutdownReasonKey, StorageScope.WORKSPACE); this.phase = LifecyclePhase.Running; // reset this flag since the shutdown has been vetoed! @@ -94,14 +94,15 @@ export class LifecycleService implements ILifecycleService { }); } - private onBeforeUnload(reason: ShutdownReason): TPromise { + private onBeforeUnload(reason: ShutdownReason, payload?: object): TPromise { const vetos: (boolean | TPromise)[] = []; this._onWillShutdown.fire({ veto(value) { vetos.push(value); }, - reason + reason, + payload }); return handleVetos(vetos, err => this._messageService.show(Severity.Error, toErrorMessage(err)));