Skip to content

Commit d51605b

Browse files
author
Benjamin Pasero
committed
multi root - carry over UI state to workspace
1 parent 485c18d commit d51605b

9 files changed

Lines changed: 537 additions & 29 deletions

File tree

src/vs/code/electron-main/windows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,7 @@ export class WindowsManager implements IWindowsMainService {
11881188
window.focus();
11891189

11901190
// Only open workspace when the window has not vetoed this
1191-
return this.lifecycleService.unload(window, UnloadReason.RELOAD).done(veto => {
1191+
return this.lifecycleService.unload(window, UnloadReason.RELOAD, workspace).done(veto => {
11921192
if (!veto) {
11931193

11941194
// Register window for backups and migrate current backups over

src/vs/platform/lifecycle/common/lifecycle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const ILifecycleService = createDecorator<ILifecycleService>('lifecycleSe
2121
export interface ShutdownEvent {
2222
veto(value: boolean | TPromise<boolean>): void;
2323
reason: ShutdownReason;
24+
payload?: object;
2425
}
2526

2627
export enum ShutdownReason {

src/vs/platform/lifecycle/electron-main/lifecycleMain.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export interface ILifecycleService {
6161
ready(): void;
6262
registerWindow(window: ICodeWindow): void;
6363

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

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

@@ -179,7 +179,7 @@ export class LifecycleService implements ILifecycleService {
179179
});
180180
}
181181

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

184184
// Always allow to unload a window that is not yet ready
185185
if (window.readyState !== ReadyState.READY) {
@@ -191,7 +191,7 @@ export class LifecycleService implements ILifecycleService {
191191
const windowUnloadReason = this.quitRequested ? UnloadReason.QUIT : reason;
192192

193193
// first ask the window itself if it vetos the unload
194-
return this.doUnloadWindowInRenderer(window, windowUnloadReason).then(veto => {
194+
return this.doUnloadWindowInRenderer(window, windowUnloadReason, payload).then(veto => {
195195
if (veto) {
196196
return this.handleVeto(veto);
197197
}
@@ -213,7 +213,7 @@ export class LifecycleService implements ILifecycleService {
213213
return veto;
214214
}
215215

216-
private doUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason): TPromise<boolean /* veto */> {
216+
private doUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason, payload?: object): TPromise<boolean /* veto */> {
217217
return new TPromise<boolean>((c) => {
218218
const oneTimeEventToken = this.oneTimeListenerTokenGenerator++;
219219
const okChannel = `vscode:ok${oneTimeEventToken}`;
@@ -227,7 +227,7 @@ export class LifecycleService implements ILifecycleService {
227227
c(true); // veto
228228
});
229229

230-
window.send('vscode:beforeUnload', { okChannel, cancelChannel, reason });
230+
window.send('vscode:beforeUnload', { okChannel, cancelChannel, reason, payload });
231231
});
232232
}
233233

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
import { IStorage, StorageService } from "vs/platform/storage/common/storageService";
9+
import { endsWith, startsWith, rtrim } from "vs/base/common/strings";
10+
import URI from "vs/base/common/uri";
11+
import { IWorkspaceIdentifier } from "vs/platform/workspaces/common/workspaces";
12+
13+
/**
14+
* We currently store local storage with the following format:
15+
*
16+
* [Global]
17+
* storage://global/<key>
18+
*
19+
* [Workspace]
20+
* storage://workspace/<folder>/<key>
21+
* storage://workspace/empty:<id>/<key>
22+
* storage://workspace/root:<id>/<key>
23+
*
24+
* <folder>
25+
* macOS/Linux: /some/folder/path
26+
* Windows: c%3A/Users/name/folder (normal path)
27+
* file://localhost/c%24/name/folder (unc path)
28+
*
29+
* [no workspace]
30+
* storage://workspace/__$noWorkspace__<key>
31+
* => no longer being used (used for empty workspaces previously)
32+
*/
33+
34+
const EMPTY_WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/empty:`;
35+
const MULTI_ROOT_WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/root:`;
36+
37+
export type StorageObject = { [key: string]: string };
38+
39+
export interface IParsedStorage {
40+
global: Map<string, string>;
41+
multiRoot: Map<string, StorageObject>;
42+
folder: Map<string, StorageObject>;
43+
empty: Map<string, StorageObject>;
44+
}
45+
46+
/**
47+
* Parses the local storage implementation into global, multi root, folder and empty storage.
48+
*/
49+
export function parseStorage(storage: IStorage): IParsedStorage {
50+
const globalStorage = new Map<string, string>();
51+
const folderWorkspacesStorage = new Map<string /* workspace file resource */, StorageObject>();
52+
const emptyWorkspacesStorage = new Map<string /* empty workspace id */, StorageObject>();
53+
const multiRootWorkspacesStorage = new Map<string /* multi root workspace id */, StorageObject>();
54+
55+
const workspaces: { prefix: string; resource: string; }[] = [];
56+
for (let i = 0; i < storage.length; i++) {
57+
const key = storage.key(i);
58+
59+
// Workspace Storage (storage://workspace/)
60+
if (startsWith(key, StorageService.WORKSPACE_PREFIX)) {
61+
62+
// We are looking for key: storage://workspace/<folder>/workspaceIdentifier to be able to find all folder
63+
// paths that are known to the storage. is the only way how to parse all folder paths known in storage.
64+
if (endsWith(key, StorageService.WORKSPACE_IDENTIFIER)) {
65+
66+
// storage://workspace/<folder>/workspaceIdentifier => <folder>/
67+
let workspace = key.substring(StorageService.WORKSPACE_PREFIX.length, key.length - StorageService.WORKSPACE_IDENTIFIER.length);
68+
69+
// macOS/Unix: Users/name/folder/
70+
// Windows: c%3A/Users/name/folder/
71+
if (!startsWith(workspace, 'file:')) {
72+
workspace = `file:///${rtrim(workspace, '/')}`;
73+
}
74+
75+
// Windows UNC path: file://localhost/c%3A/Users/name/folder/
76+
else {
77+
workspace = rtrim(workspace, '/');
78+
}
79+
80+
// storage://workspace/<folder>/workspaceIdentifier => storage://workspace/<folder>/
81+
const prefix = key.substr(0, key.length - StorageService.WORKSPACE_IDENTIFIER.length);
82+
workspaces.push({ prefix, resource: workspace });
83+
}
84+
85+
// Empty workspace key: storage://workspace/empty:<id>/<key>
86+
else if (startsWith(key, EMPTY_WORKSPACE_PREFIX)) {
87+
88+
// storage://workspace/empty:<id>/<key> => <id>
89+
const emptyWorkspaceId = key.substring(EMPTY_WORKSPACE_PREFIX.length, key.indexOf('/', EMPTY_WORKSPACE_PREFIX.length));
90+
const emptyWorkspaceResource = URI.from({ path: emptyWorkspaceId, scheme: 'empty' }).toString();
91+
92+
let emptyWorkspaceStorage = emptyWorkspacesStorage.get(emptyWorkspaceResource);
93+
if (!emptyWorkspaceStorage) {
94+
emptyWorkspaceStorage = Object.create(null);
95+
emptyWorkspacesStorage.set(emptyWorkspaceResource, emptyWorkspaceStorage);
96+
}
97+
98+
// storage://workspace/empty:<id>/someKey => someKey
99+
const storageKey = key.substr(EMPTY_WORKSPACE_PREFIX.length + emptyWorkspaceId.length + 1 /* trailing / */);
100+
101+
emptyWorkspaceStorage[storageKey] = storage.getItem(key);
102+
}
103+
104+
// Multi root workspace key: storage://workspace/root:<id>/<key>
105+
else if (startsWith(key, MULTI_ROOT_WORKSPACE_PREFIX)) {
106+
107+
// storage://workspace/root:<id>/<key> => <id>
108+
const multiRootWorkspaceId = key.substring(MULTI_ROOT_WORKSPACE_PREFIX.length, key.indexOf('/', MULTI_ROOT_WORKSPACE_PREFIX.length));
109+
const multiRootWorkspaceResource = URI.from({ path: multiRootWorkspaceId, scheme: 'root' }).toString();
110+
111+
let multiRootWorkspaceStorage = multiRootWorkspacesStorage.get(multiRootWorkspaceResource);
112+
if (!multiRootWorkspaceStorage) {
113+
multiRootWorkspaceStorage = Object.create(null);
114+
multiRootWorkspacesStorage.set(multiRootWorkspaceResource, multiRootWorkspaceStorage);
115+
}
116+
117+
// storage://workspace/root:<id>/someKey => someKey
118+
const storageKey = key.substr(MULTI_ROOT_WORKSPACE_PREFIX.length + multiRootWorkspaceId.length + 1 /* trailing / */);
119+
120+
multiRootWorkspaceStorage[storageKey] = storage.getItem(key);
121+
}
122+
}
123+
124+
// Global Storage (storage://global)
125+
else if (startsWith(key, StorageService.GLOBAL_PREFIX)) {
126+
127+
// storage://global/someKey => someKey
128+
const globalStorageKey = key.substr(StorageService.GLOBAL_PREFIX.length);
129+
if (startsWith(globalStorageKey, StorageService.COMMON_PREFIX)) {
130+
continue; // filter out faulty keys that have the form storage://something/storage://
131+
}
132+
133+
globalStorage.set(globalStorageKey, storage.getItem(key));
134+
}
135+
}
136+
137+
// With all the folder paths known we can now extract storage for each path. We have to go through all workspaces
138+
// from the longest path first to reliably extract the storage. The reason is that one folder path can be a parent
139+
// of another folder path and as such a simple indexOf check is not enough.
140+
const workspacesByLength = workspaces.sort((w1, w2) => w1.prefix.length >= w2.prefix.length ? -1 : 1);
141+
const handledKeys = new Map<string, boolean>();
142+
workspacesByLength.forEach(workspace => {
143+
for (let i = 0; i < storage.length; i++) {
144+
const key = storage.key(i);
145+
146+
if (handledKeys.has(key) || !startsWith(key, workspace.prefix)) {
147+
continue; // not part of workspace prefix or already handled
148+
}
149+
150+
handledKeys.set(key, true);
151+
152+
let folderWorkspaceStorage = folderWorkspacesStorage.get(workspace.resource);
153+
if (!folderWorkspaceStorage) {
154+
folderWorkspaceStorage = Object.create(null);
155+
folderWorkspacesStorage.set(workspace.resource, folderWorkspaceStorage);
156+
}
157+
158+
// storage://workspace/<folder>/someKey => someKey
159+
const storageKey = key.substr(workspace.prefix.length);
160+
161+
folderWorkspaceStorage[storageKey] = storage.getItem(key);
162+
}
163+
});
164+
165+
return {
166+
global: globalStorage,
167+
multiRoot: multiRootWorkspacesStorage,
168+
folder: folderWorkspacesStorage,
169+
empty: emptyWorkspacesStorage
170+
};
171+
}
172+
173+
export function migrateStorageToMultiRootWorkspace(fromWorkspaceId: string, toWorkspaceId: IWorkspaceIdentifier, storage: IStorage): void {
174+
const parsed = parseStorage(storage);
175+
176+
const newStorageKey = URI.from({ path: toWorkspaceId.id, scheme: 'root' }).toString();
177+
178+
// Find in which location the workspace storage is to be migrated rom
179+
let storageForWorkspace: StorageObject;
180+
if (parsed.multiRoot.has(fromWorkspaceId)) {
181+
storageForWorkspace = parsed.multiRoot.get(fromWorkspaceId);
182+
} else if (parsed.empty.has(fromWorkspaceId)) {
183+
storageForWorkspace = parsed.empty.get(fromWorkspaceId);
184+
} else if (parsed.folder.has(fromWorkspaceId)) {
185+
storageForWorkspace = parsed.folder.get(fromWorkspaceId);
186+
}
187+
188+
// Migrate existing storage to new workspace id
189+
if (storageForWorkspace) {
190+
Object.keys(storageForWorkspace).forEach(key => {
191+
if (key === StorageService.WORKSPACE_IDENTIFIER) {
192+
return; // make sure to never migrate the workspace identifier
193+
}
194+
195+
storage.setItem(`${StorageService.WORKSPACE_PREFIX}${newStorageKey}/${key}`, storageForWorkspace[key]);
196+
});
197+
}
198+
}

src/vs/platform/storage/common/storageService.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,25 @@ export class StorageService implements IStorageService {
2323

2424
public _serviceBrand: any;
2525

26-
private static COMMON_PREFIX = 'storage://';
27-
private static GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`;
28-
private static WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`;
29-
private static WORKSPACE_IDENTIFIER = 'workspaceIdentifier';
30-
private static NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__';
26+
public static COMMON_PREFIX = 'storage://';
27+
public static GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`;
28+
public static WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`;
29+
public static WORKSPACE_IDENTIFIER = 'workspaceidentifier';
30+
public static NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__';
3131

32-
private workspaceStorage: IStorage;
33-
private globalStorage: IStorage;
32+
private _workspaceStorage: IStorage;
33+
private _globalStorage: IStorage;
3434

3535
private workspaceKey: string;
3636

3737
constructor(
3838
globalStorage: IStorage,
3939
workspaceStorage: IStorage,
40-
workspaceId?: string,
40+
private workspaceId?: string,
4141
legacyWorkspaceId?: number
4242
) {
43-
this.globalStorage = globalStorage;
44-
this.workspaceStorage = workspaceStorage || globalStorage;
43+
this._globalStorage = globalStorage;
44+
this._workspaceStorage = workspaceStorage || globalStorage;
4545

4646
// Calculate workspace storage key
4747
this.workspaceKey = this.getWorkspaceKey(workspaceId);
@@ -53,6 +53,18 @@ export class StorageService implements IStorageService {
5353
}
5454
}
5555

56+
public get storageId(): string {
57+
return this.workspaceId;
58+
}
59+
60+
public get globalStorage(): IStorage {
61+
return this._globalStorage;
62+
}
63+
64+
public get workspaceStorage(): IStorage {
65+
return this._workspaceStorage;
66+
}
67+
5668
private getWorkspaceKey(id?: string): string {
5769
if (!id) {
5870
return StorageService.NO_WORKSPACE_IDENTIFIER;
@@ -77,10 +89,10 @@ export class StorageService implements IStorageService {
7789
if (types.isNumber(id) && workspaceUid !== id) {
7890
const keyPrefix = this.toStorageKey('', StorageScope.WORKSPACE);
7991
const toDelete: string[] = [];
80-
const length = this.workspaceStorage.length;
92+
const length = this._workspaceStorage.length;
8193

8294
for (let i = 0; i < length; i++) {
83-
const key = this.workspaceStorage.key(i);
95+
const key = this._workspaceStorage.key(i);
8496
if (key.indexOf(StorageService.WORKSPACE_PREFIX) < 0) {
8597
continue; // ignore stored things that don't belong to storage service or are defined globally
8698
}
@@ -93,7 +105,7 @@ export class StorageService implements IStorageService {
93105

94106
// Run the delete
95107
toDelete.forEach((keyToDelete) => {
96-
this.workspaceStorage.removeItem(keyToDelete);
108+
this._workspaceStorage.removeItem(keyToDelete);
97109
});
98110
}
99111

@@ -104,12 +116,12 @@ export class StorageService implements IStorageService {
104116
}
105117

106118
public clear(): void {
107-
this.globalStorage.clear();
108-
this.workspaceStorage.clear();
119+
this._globalStorage.clear();
120+
this._workspaceStorage.clear();
109121
}
110122

111123
public store(key: string, value: any, scope = StorageScope.GLOBAL): void {
112-
const storage = (scope === StorageScope.GLOBAL) ? this.globalStorage : this.workspaceStorage;
124+
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
113125

114126
if (types.isUndefinedOrNull(value)) {
115127
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 {
127139
}
128140

129141
public get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string {
130-
const storage = (scope === StorageScope.GLOBAL) ? this.globalStorage : this.workspaceStorage;
142+
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
131143

132144
const value = storage.getItem(this.toStorageKey(key, scope));
133145
if (types.isUndefinedOrNull(value)) {
@@ -138,7 +150,7 @@ export class StorageService implements IStorageService {
138150
}
139151

140152
public remove(key: string, scope = StorageScope.GLOBAL): void {
141-
const storage = (scope === StorageScope.GLOBAL) ? this.globalStorage : this.workspaceStorage;
153+
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
142154
const storageKey = this.toStorageKey(key, scope);
143155

144156
// Remove

0 commit comments

Comments
 (0)