Skip to content

Commit

Permalink
multi root - carry over UI state to workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Aug 21, 2017
1 parent 485c18d commit d51605b
Show file tree
Hide file tree
Showing 9 changed files with 537 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/vs/code/electron-main/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/lifecycle/common/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const ILifecycleService = createDecorator<ILifecycleService>('lifecycleSe
export interface ShutdownEvent {
veto(value: boolean | TPromise<boolean>): void;
reason: ShutdownReason;
payload?: object;
}

export enum ShutdownReason {
Expand Down
10 changes: 5 additions & 5 deletions src/vs/platform/lifecycle/electron-main/lifecycleMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface ILifecycleService {
ready(): void;
registerWindow(window: ICodeWindow): void;

unload(window: ICodeWindow, reason: UnloadReason): TPromise<boolean /* veto */>;
unload(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise<boolean /* veto */>;

relaunch(options?: { addArgs?: string[], removeArgs?: string[] });

Expand Down Expand Up @@ -179,7 +179,7 @@ export class LifecycleService implements ILifecycleService {
});
}

public unload(window: ICodeWindow, reason: UnloadReason): TPromise<boolean /* veto */> {
public unload(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise<boolean /* veto */> {

// Always allow to unload a window that is not yet ready
if (window.readyState !== ReadyState.READY) {
Expand All @@ -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);
}
Expand All @@ -213,7 +213,7 @@ export class LifecycleService implements ILifecycleService {
return veto;
}

private doUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason): TPromise<boolean /* veto */> {
private doUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise<boolean /* veto */> {
return new TPromise<boolean>((c) => {
const oneTimeEventToken = this.oneTimeListenerTokenGenerator++;
const okChannel = `vscode:ok${oneTimeEventToken}`;
Expand All @@ -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 });
});
}

Expand Down
198 changes: 198 additions & 0 deletions src/vs/platform/storage/common/migration.ts
Original file line number Diff line number Diff line change
@@ -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/<key>
*
* [Workspace]
* storage://workspace/<folder>/<key>
* storage://workspace/empty:<id>/<key>
* storage://workspace/root:<id>/<key>
*
* <folder>
* 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__<key>
* => 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<string, string>;
multiRoot: Map<string, StorageObject>;
folder: Map<string, StorageObject>;
empty: Map<string, StorageObject>;
}

/**
* Parses the local storage implementation into global, multi root, folder and empty storage.
*/
export function parseStorage(storage: IStorage): IParsedStorage {
const globalStorage = new Map<string, string>();
const folderWorkspacesStorage = new Map<string /* workspace file resource */, StorageObject>();
const emptyWorkspacesStorage = new Map<string /* empty workspace id */, StorageObject>();
const multiRootWorkspacesStorage = new Map<string /* multi root workspace id */, StorageObject>();

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/<folder>/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/<folder>/workspaceIdentifier => <folder>/
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/<folder>/workspaceIdentifier => storage://workspace/<folder>/
const prefix = key.substr(0, key.length - StorageService.WORKSPACE_IDENTIFIER.length);
workspaces.push({ prefix, resource: workspace });
}

// Empty workspace key: storage://workspace/empty:<id>/<key>
else if (startsWith(key, EMPTY_WORKSPACE_PREFIX)) {

// storage://workspace/empty:<id>/<key> => <id>
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:<id>/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:<id>/<key>
else if (startsWith(key, MULTI_ROOT_WORKSPACE_PREFIX)) {

// storage://workspace/root:<id>/<key> => <id>
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:<id>/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<string, boolean>();
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/<folder>/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]);
});
}
}
48 changes: 30 additions & 18 deletions src/vs/platform/storage/common/storageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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
}
Expand All @@ -93,7 +105,7 @@ export class StorageService implements IStorageService {

// Run the delete
toDelete.forEach((keyToDelete) => {
this.workspaceStorage.removeItem(keyToDelete);
this._workspaceStorage.removeItem(keyToDelete);
});
}

Expand All @@ -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
Expand All @@ -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)) {
Expand All @@ -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
Expand Down
Loading

0 comments on commit d51605b

Please sign in to comment.