Skip to content

Commit ff026e0

Browse files
author
Benjamin Pasero
committed
Web: support for untitled workspaces (fixes microsoft#82599)
1 parent 030af95 commit ff026e0

11 files changed

Lines changed: 201 additions & 158 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "code-oss-dev",
33
"version": "1.40.0",
4-
"distro": "c4406f67d23097ea3ddc5e388290c06ad37021eb",
4+
"distro": "6864b7d5658c713e1a5d54baefafe5ff90818aee",
55
"author": {
66
"name": "Microsoft Corporation"
77
},

src/vs/code/browser/workbench/workbench.ts

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ class WorkspaceProvider implements IWorkspaceProvider {
206206
static QUERY_PARAM_FOLDER = 'folder';
207207
static QUERY_PARAM_WORKSPACE = 'workspace';
208208

209+
static QUERY_PARAM_PAYLOAD = 'payload';
210+
209211
constructor(
210212
public readonly workspace: IWorkspace,
211213
public readonly payload: object
@@ -216,6 +218,22 @@ class WorkspaceProvider implements IWorkspaceProvider {
216218
return; // return early if workspace and environment is not changing and we are reusing window
217219
}
218220

221+
const targetHref = this.createTargetUrl(workspace, options);
222+
if (targetHref) {
223+
if (options?.reuse) {
224+
window.location.href = targetHref;
225+
} else {
226+
if (isStandalone) {
227+
window.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window!
228+
} else {
229+
window.open(targetHref);
230+
}
231+
}
232+
}
233+
}
234+
235+
private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): string | undefined {
236+
219237
// Empty
220238
let targetHref: string | undefined = undefined;
221239
if (!workspace) {
@@ -224,30 +242,20 @@ class WorkspaceProvider implements IWorkspaceProvider {
224242

225243
// Folder
226244
else if (isFolderToOpen(workspace)) {
227-
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${workspace.folderUri.path}`;
245+
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`;
228246
}
229247

230248
// Workspace
231249
else if (isWorkspaceToOpen(workspace)) {
232-
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${workspace.workspaceUri.path}`;
250+
targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`;
233251
}
234252

235-
// Environment
253+
// Append payload if any
236254
if (options?.payload) {
237-
targetHref += `&payload=${encodeURIComponent(JSON.stringify(options.payload))}`;
255+
targetHref += `&${WorkspaceProvider.QUERY_PARAM_PAYLOAD}=${encodeURIComponent(JSON.stringify(options.payload))}`;
238256
}
239257

240-
if (targetHref) {
241-
if (options?.reuse) {
242-
window.location.href = targetHref;
243-
} else {
244-
if (isStandalone) {
245-
window.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window!
246-
} else {
247-
window.open(targetHref);
248-
}
249-
}
250-
}
258+
return targetHref;
251259
}
252260

253261
private isSame(workspaceA: IWorkspace, workspaceB: IWorkspace): boolean {
@@ -276,32 +284,49 @@ class WorkspaceProvider implements IWorkspaceProvider {
276284
throw new Error('Missing web configuration element');
277285
}
278286

279-
const options: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute);
280-
281-
// Determine workspace to open
287+
// Find workspace to open and payload
288+
let foundWorkspace = false;
282289
let workspace: IWorkspace;
283-
if (options.folderUri) {
284-
workspace = { folderUri: URI.revive(options.folderUri) };
285-
} else if (options.workspaceUri) {
286-
workspace = { workspaceUri: URI.revive(options.workspaceUri) };
287-
} else {
288-
workspace = undefined;
289-
}
290-
291-
// Find payload
292290
let payload = Object.create(null);
293-
if (document.location.search) {
294-
const query = document.location.search.substring(1);
295-
const vars = query.split('&');
296-
for (let p of vars) {
297-
const pair = p.split('=');
298-
if (pair.length === 2) {
299-
const [key, value] = pair;
300-
if (key === 'payload') {
301-
payload = JSON.parse(decodeURIComponent(value));
302-
break;
303-
}
304-
}
291+
292+
const query = new URL(document.location.href).searchParams;
293+
query.forEach((value, key) => {
294+
switch (key) {
295+
296+
// Folder
297+
case WorkspaceProvider.QUERY_PARAM_FOLDER:
298+
workspace = { folderUri: URI.parse(value) };
299+
foundWorkspace = true;
300+
break;
301+
302+
// Workspace
303+
case WorkspaceProvider.QUERY_PARAM_WORKSPACE:
304+
workspace = { workspaceUri: URI.parse(value) };
305+
foundWorkspace = true;
306+
break;
307+
308+
// Empty
309+
case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW:
310+
workspace = undefined;
311+
foundWorkspace = true;
312+
break;
313+
314+
// Payload
315+
case WorkspaceProvider.QUERY_PARAM_PAYLOAD:
316+
payload = JSON.parse(value);
317+
break;
318+
}
319+
});
320+
321+
// If no workspace is provided through the URL, check for config attribute from server
322+
const options: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute);
323+
if (!foundWorkspace) {
324+
if (options.folderUri) {
325+
workspace = { folderUri: URI.revive(options.folderUri) };
326+
} else if (options.workspaceUri) {
327+
workspace = { workspaceUri: URI.revive(options.workspaceUri) };
328+
} else {
329+
workspace = undefined;
305330
}
306331
}
307332

src/vs/workbench/browser/actions/workspaceActions.ts

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/c
1313
import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
1414
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
1515
import { MenuRegistry, MenuId, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
16-
import { WorkbenchStateContext, SupportsWorkspacesContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
16+
import { WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
1717
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1818
import { Registry } from 'vs/platform/registry/common/platform';
1919
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
@@ -22,6 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
2222
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
2323
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
2424
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
25+
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
2526

2627
export class OpenFileAction extends Action {
2728

@@ -195,14 +196,74 @@ export class GlobalRemoveRootFolderAction extends Action {
195196
}
196197
}
197198

199+
export class SaveWorkspaceAsAction extends Action {
200+
201+
static readonly ID = 'workbench.action.saveWorkspaceAs';
202+
static readonly LABEL = nls.localize('saveWorkspaceAsAction', "Save Workspace As...");
203+
204+
constructor(
205+
id: string,
206+
label: string,
207+
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
208+
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService
209+
210+
) {
211+
super(id, label);
212+
}
213+
214+
async run(): Promise<any> {
215+
const configPathUri = await this.workspaceEditingService.pickNewWorkspacePath();
216+
if (configPathUri) {
217+
switch (this.contextService.getWorkbenchState()) {
218+
case WorkbenchState.EMPTY:
219+
case WorkbenchState.FOLDER:
220+
const folders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri }));
221+
return this.workspaceEditingService.createAndEnterWorkspace(folders, configPathUri);
222+
case WorkbenchState.WORKSPACE:
223+
return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri);
224+
}
225+
}
226+
}
227+
}
228+
229+
export class DuplicateWorkspaceInNewWindowAction extends Action {
230+
231+
static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow';
232+
static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window");
233+
234+
constructor(
235+
id: string,
236+
label: string,
237+
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
238+
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
239+
@IHostService private readonly hostService: IHostService,
240+
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
241+
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
242+
) {
243+
super(id, label);
244+
}
245+
246+
async run(): Promise<any> {
247+
const folders = this.workspaceContextService.getWorkspace().folders;
248+
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
249+
250+
const newWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority);
251+
await this.workspaceEditingService.copyWorkspaceSettings(newWorkspace);
252+
253+
return this.hostService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true });
254+
}
255+
}
256+
198257
// --- Actions Registration
199258

200259
const registry = Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions);
201260
const workspacesCategory = nls.localize('workspaces', "Workspaces");
202261

203-
registry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory, SupportsWorkspacesContext);
204-
registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRemoveRootFolderAction, GlobalRemoveRootFolderAction.ID, GlobalRemoveRootFolderAction.LABEL), 'Workspaces: Remove Folder from Workspace...', workspacesCategory, SupportsWorkspacesContext);
205-
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, SupportsWorkspacesContext);
262+
registry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory);
263+
registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRemoveRootFolderAction, GlobalRemoveRootFolderAction.ID, GlobalRemoveRootFolderAction.LABEL), 'Workspaces: Remove Folder from Workspace...', workspacesCategory);
264+
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory);
265+
registry.registerWorkbenchAction(new SyncActionDescriptor(SaveWorkspaceAsAction, SaveWorkspaceAsAction.ID, SaveWorkspaceAsAction.LABEL), 'Workspaces: Save Workspace As...', workspacesCategory);
266+
registry.registerWorkbenchAction(new SyncActionDescriptor(DuplicateWorkspaceInNewWindowAction, DuplicateWorkspaceInNewWindowAction.ID, DuplicateWorkspaceInNewWindowAction.LABEL), 'Workspaces: Duplicate Workspace in New Window', workspacesCategory);
206267

207268
// --- Menu Registration
208269

@@ -216,8 +277,16 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
216277
id: ADD_ROOT_FOLDER_COMMAND_ID,
217278
title: nls.localize({ key: 'miAddFolderToWorkspace', comment: ['&& denotes a mnemonic'] }, "A&&dd Folder to Workspace...")
218279
},
219-
order: 1,
220-
when: SupportsWorkspacesContext
280+
order: 1
281+
});
282+
283+
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
284+
group: '3_workspace',
285+
command: {
286+
id: SaveWorkspaceAsAction.ID,
287+
title: nls.localize('miSaveWorkspaceAs', "Save Workspace As...")
288+
},
289+
order: 2
221290
});
222291

223292
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
@@ -246,5 +315,5 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
246315
title: nls.localize({ key: 'miCloseWorkspace', comment: ['&& denotes a mnemonic'] }, "Close &&Workspace")
247316
},
248317
order: 3,
249-
when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), SupportsWorkspacesContext)
318+
when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'))
250319
});

src/vs/workbench/browser/contextkeys.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ export const RemoteConnectionState = new RawContextKey<'' | 'initializing' | 'di
3838

3939
export const HasMacNativeTabsContext = new RawContextKey<boolean>('hasMacNativeTabs', false);
4040

41-
export const SupportsWorkspacesContext = new RawContextKey<boolean>('supportsWorkspaces', true);
42-
4341
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false);
4442

4543
export const WorkbenchStateContext = new RawContextKey<string>('workbenchState', undefined);
@@ -107,11 +105,6 @@ export class WorkbenchContextKeysHandler extends Disposable {
107105
// Development
108106
IsDevelopmentContext.bindTo(this.contextKeyService).set(!this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment);
109107

110-
// Workspaces Support
111-
// - web: only if already in workspace state
112-
// - desktop: always
113-
SupportsWorkspacesContext.bindTo(this.contextKeyService).set(isWeb ? this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE : true);
114-
115108
// Editors
116109
this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService);
117110
this.activeEditorIsSaveable = ActiveEditorIsSaveableContext.bindTo(this.contextKeyService);

src/vs/workbench/browser/web.main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { toLocalISOString } from 'vs/base/common/date';
4848
import { IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedDBLogProvider';
4949
import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLogProvider';
5050
import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows';
51+
import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces';
5152

5253
class BrowserMain extends Disposable {
5354

@@ -292,7 +293,7 @@ class BrowserMain extends Disposable {
292293

293294
// Multi-root workspace
294295
if (workspace && isWorkspaceToOpen(workspace)) {
295-
return { id: hash(workspace.workspaceUri.toString()).toString(16), configPath: workspace.workspaceUri };
296+
return getWorkspaceIdentifier(workspace.workspaceUri);
296297
}
297298

298299
// Single-folder workspace

src/vs/workbench/contrib/files/browser/fileActions.contribution.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { ResourceContextKey } from 'vs/workbench/common/resources';
2323
import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService';
2424
import { URI } from 'vs/base/common/uri';
2525
import { Schemas } from 'vs/base/common/network';
26-
import { SupportsWorkspacesContext, IsWebContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
26+
import { IsWebContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
2727
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
2828
import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions';
2929
import { ActiveEditorIsSaveableContext } from 'vs/workbench/common/editor';
@@ -496,7 +496,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
496496
id: ADD_ROOT_FOLDER_COMMAND_ID,
497497
title: ADD_ROOT_FOLDER_LABEL
498498
},
499-
when: ContextKeyExpr.and(ExplorerRootContext, SupportsWorkspacesContext)
499+
when: ContextKeyExpr.and(ExplorerRootContext)
500500
});
501501

502502
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
@@ -506,7 +506,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
506506
id: REMOVE_ROOT_FOLDER_COMMAND_ID,
507507
title: REMOVE_ROOT_FOLDER_LABEL
508508
},
509-
when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext, SupportsWorkspacesContext)
509+
when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext)
510510
});
511511

512512
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {

0 commit comments

Comments
 (0)