Skip to content

Commit ad5e581

Browse files
author
Benjamin Pasero
committed
web - introduce workspace provider
1 parent 1f80e39 commit ad5e581

8 files changed

Lines changed: 115 additions & 52 deletions

File tree

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

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider } from 'vs/workbench/workbench.web.api';
6+
import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace } from 'vs/workbench/workbench.web.api';
77
import { generateUuid } from 'vs/base/common/uuid';
88
import { CancellationToken } from 'vs/base/common/cancellation';
99
import { streamToBuffer } from 'vs/base/common/buffer';
1010
import { Disposable } from 'vs/base/common/lifecycle';
1111
import { request } from 'vs/base/parts/request/browser/request';
12+
import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows';
1213

1314
interface ICredential {
1415
service: string;
@@ -197,18 +198,43 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi
197198
}
198199
}
199200

200-
const options: IWorkbenchConstructionOptions = JSON.parse(document.getElementById('vscode-workbench-web-configuration')!.getAttribute('data-settings')!);
201-
options.urlCallbackProvider = new PollingURLCallbackProvider();
202-
options.credentialsProvider = new LocalStorageCredentialsProvider();
201+
class WorkspaceProvider implements IWorkspaceProvider {
203202

204-
if (options.folderUri) {
205-
options.folderUri = URI.revive(options.folderUri);
206-
}
203+
constructor(public readonly workspace: IWorkspace) { }
204+
205+
async open(workspace: IWorkspace, options?: { reuse?: boolean }): Promise<void> {
206+
let targetHref: string | undefined = undefined;
207+
208+
// Empty
209+
if (!workspace) {
210+
targetHref = `${document.location.origin}${document.location.pathname}?ew=true`;
211+
}
212+
213+
// Folder
214+
else if (isFolderToOpen(workspace)) {
215+
targetHref = `${document.location.origin}${document.location.pathname}?folder=${workspace.folderUri.path}`;
216+
}
207217

208-
if (options.workspaceUri) {
209-
options.workspaceUri = URI.revive(options.workspaceUri);
218+
// Workspace
219+
else if (isWorkspaceToOpen(workspace)) {
220+
targetHref = `${document.location.origin}${document.location.pathname}?workspace=${workspace.workspaceUri.path}`;
221+
}
222+
223+
if (targetHref) {
224+
if (options && options.reuse) {
225+
window.location.href = targetHref;
226+
} else {
227+
window.open(targetHref);
228+
}
229+
}
230+
}
210231
}
211232

233+
const options: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(document.getElementById('vscode-workbench-web-configuration')!.getAttribute('data-settings')!);
234+
options.workspaceProvider = new WorkspaceProvider(options.folderUri ? { folderUri: URI.revive(options.folderUri) } : options.workspaceUri ? { workspaceUri: URI.revive(options.workspaceUri) } : undefined);
235+
options.urlCallbackProvider = new PollingURLCallbackProvider();
236+
options.credentialsProvider = new LocalStorageCredentialsProvider();
237+
212238
if (Array.isArray(options.staticExtensions)) {
213239
options.staticExtensions.forEach(extension => {
214240
extension.extensionLocation = URI.revive(extension.extensionLocation);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const workspacesCategory = nls.localize('workspaces', "Workspaces");
200200

201201
registry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory, SupportsWorkspacesContext);
202202
registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRemoveRootFolderAction, GlobalRemoveRootFolderAction.ID, GlobalRemoveRootFolderAction.LABEL), 'Workspaces: Remove Folder from Workspace...', workspacesCategory, SupportsWorkspacesContext);
203-
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'File: Close Workspace', workspacesCategory, SupportsWorkspacesContext);
203+
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, SupportsWorkspacesContext);
204204

205205
// --- Menu Registration
206206

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ConfigurationCache } from 'vs/workbench/services/configuration/browser/
3333
import { ISignService } from 'vs/platform/sign/common/sign';
3434
import { SignService } from 'vs/platform/sign/browser/signService';
3535
import { hash } from 'vs/base/common/hash';
36-
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
36+
import { IWorkbenchConstructionOptions, IWorkspace } from 'vs/workbench/workbench.web.api';
3737
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
3838
import { BACKUPS } from 'vs/platform/environment/common/environment';
3939
import { joinPath } from 'vs/base/common/resources';
@@ -47,6 +47,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService';
4747
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';
50+
import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows';
5051

5152
class BrowserMain extends Disposable {
5253

@@ -284,15 +285,27 @@ class BrowserMain extends Disposable {
284285
}
285286

286287
private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload {
288+
let workspace: IWorkspace | undefined = undefined;
289+
if (this.configuration.workspaceProvider) {
290+
workspace = this.configuration.workspaceProvider.workspace;
291+
} else {
292+
// TODO@ben remove me once IWorkspaceProvider API is adopted
293+
const legacyConfiguration = this.configuration as { workspaceUri?: URI, folderUri?: URI };
294+
if (legacyConfiguration.workspaceUri) {
295+
workspace = { workspaceUri: legacyConfiguration.workspaceUri };
296+
} else if (legacyConfiguration.folderUri) {
297+
workspace = { folderUri: legacyConfiguration.folderUri };
298+
}
299+
}
287300

288301
// Multi-root workspace
289-
if (this.configuration.workspaceUri) {
290-
return { id: hash(this.configuration.workspaceUri.toString()).toString(16), configPath: this.configuration.workspaceUri };
302+
if (workspace && isWorkspaceToOpen(workspace)) {
303+
return { id: hash(workspace.workspaceUri.toString()).toString(16), configPath: workspace.workspaceUri };
291304
}
292305

293306
// Single-folder workspace
294-
if (this.configuration.folderUri) {
295-
return { id: hash(this.configuration.folderUri.toString()).toString(16), folder: this.configuration.folderUri };
307+
if (workspace && isFolderToOpen(workspace)) {
308+
return { id: hash(workspace.folderUri.toString()).toString(16), folder: workspace.folderUri };
296309
}
297310

298311
return { id: 'empty-window' };

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as nls from 'vs/nls';
77
import { Registry } from 'vs/platform/registry/common/platform';
8-
import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, ShowOpenedFileInNewWindow, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler } from 'vs/workbench/contrib/files/browser/fileActions';
8+
import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler } from 'vs/workbench/contrib/files/browser/fileActions';
99
import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler';
1010
import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions';
1111
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
@@ -41,7 +41,6 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(ShowActiveFileInExplor
4141
registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseExplorerView, CollapseExplorerView.ID, CollapseExplorerView.LABEL), 'File: Collapse Folders in Explorer', category.value);
4242
registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL), 'File: Refresh Explorer', category.value);
4343
registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalNewUntitledFileAction, GlobalNewUntitledFileAction.ID, GlobalNewUntitledFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N }), 'File: New Untitled File', category.value);
44-
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category.value);
4544
registry.registerWorkbenchAction(new SyncActionDescriptor(CompareWithClipboardAction, CompareWithClipboardAction.ID, CompareWithClipboardAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category.value);
4645
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category.value);
4746

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/fi
1818
import { IListService } from 'vs/platform/list/browser/listService';
1919
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2020
import { revealResourcesInOS } from 'vs/workbench/contrib/files/electron-browser/fileCommands';
21-
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
21+
import { MenuRegistry, MenuId, SyncActionDescriptor } from 'vs/platform/actions/common/actions';
2222
import { ResourceContextKey } from 'vs/workbench/common/resources';
2323
import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution';
24+
import { Registry } from 'vs/platform/registry/common/platform';
25+
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
26+
import { ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions';
2427

2528
const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS';
2629
const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder");
@@ -81,3 +84,6 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
8184

8285
const category = { value: nls.localize('filesCategory', "File"), original: 'File' };
8386
appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'Reveal in Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' }, category);
87+
88+
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
89+
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category.value);

src/vs/workbench/contrib/logs/common/logs.contribution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution {
5858

5959
const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions);
6060
const devCategory = nls.localize('developer', "Developer");
61-
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWindowSessionLogFileAction, OpenWindowSessionLogFileAction.ID, OpenWindowSessionLogFileAction.LABEL), 'Developer: Open Browser Log File (Session)...', devCategory);
61+
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWindowSessionLogFileAction, OpenWindowSessionLogFileAction.ID, OpenWindowSessionLogFileAction.LABEL), 'Developer: Open Window Log File (Session)...', devCategory);
6262
}
6363

6464
private registerNativeContributions(): void {

src/vs/workbench/services/host/browser/browserHostService.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,32 @@ import { IFileService } from 'vs/platform/files/common/files';
1515
import { ILabelService } from 'vs/platform/label/common/label';
1616
import { trackFocus } from 'vs/base/browser/dom';
1717
import { Disposable } from 'vs/base/common/lifecycle';
18+
import { URI } from 'vs/base/common/uri';
19+
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
20+
21+
/**
22+
* A workspace to open in the workbench can either be:
23+
* - a workspace file with 0-N folders (via `workspaceUri`)
24+
* - a single folder (via `folderUri`)
25+
* - empty (via `undefined`)
26+
*/
27+
export type IWorkspace = { workspaceUri: URI } | { folderUri: URI } | undefined;
28+
29+
export interface IWorkspaceProvider {
30+
31+
/**
32+
* The initial workspace to open.
33+
*/
34+
readonly workspace: IWorkspace;
35+
36+
/**
37+
* Asks to open a workspace in the current or a new window.
38+
*
39+
* @param workspace the workspace to open.
40+
* @param options wether to open inside the current window or a new window.
41+
*/
42+
open(workspace: IWorkspace, options?: { reuse?: boolean }): Promise<void>;
43+
}
1844

1945
export class BrowserHostService extends Disposable implements IHostService {
2046

@@ -27,15 +53,27 @@ export class BrowserHostService extends Disposable implements IHostService {
2753

2854
//#endregion
2955

56+
private workspaceProvider: IWorkspaceProvider;
57+
3058
constructor(
3159
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
3260
@IEditorService private readonly editorService: IEditorService,
3361
@IConfigurationService private readonly configurationService: IConfigurationService,
3462
@IFileService private readonly fileService: IFileService,
35-
@ILabelService private readonly labelService: ILabelService
63+
@ILabelService private readonly labelService: ILabelService,
64+
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
3665
) {
3766
super();
3867

68+
if (environmentService.options && environmentService.options.workspaceProvider) {
69+
this.workspaceProvider = environmentService.options.workspaceProvider;
70+
} else {
71+
this.workspaceProvider = new class implements IWorkspaceProvider {
72+
readonly workspace = undefined;
73+
async open() { }
74+
};
75+
}
76+
3977
this.registerListeners();
4078
}
4179

@@ -54,33 +92,21 @@ export class BrowserHostService extends Disposable implements IHostService {
5492
readonly windowCount = Promise.resolve(1);
5593

5694
async openInWindow(toOpen: IWindowOpenable[], options?: IOpenInWindowOptions): Promise<void> {
57-
// TODO@Ben delegate to embedder
58-
const { openFolderInNewWindow } = this.shouldOpenNewWindow(options);
5995
for (let i = 0; i < toOpen.length; i++) {
6096
const openable = toOpen[i];
6197
openable.label = openable.label || this.getRecentLabel(openable);
6298

6399
// Folder
64100
if (isFolderToOpen(openable)) {
65-
const newAddress = `${document.location.origin}${document.location.pathname}?folder=${openable.folderUri.path}`;
66-
if (openFolderInNewWindow) {
67-
window.open(newAddress);
68-
} else {
69-
window.location.href = newAddress;
70-
}
101+
this.workspaceProvider.open({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options) });
71102
}
72103

73104
// Workspace
74105
else if (isWorkspaceToOpen(openable)) {
75-
const newAddress = `${document.location.origin}${document.location.pathname}?workspace=${openable.workspaceUri.path}`;
76-
if (openFolderInNewWindow) {
77-
window.open(newAddress);
78-
} else {
79-
window.location.href = newAddress;
80-
}
106+
this.workspaceProvider.open({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options) });
81107
}
82108

83-
// File
109+
// File: open via editor service in current window
84110
else if (isFileToOpen(openable)) {
85111
const inputs: IResourceEditor[] = await pathsToEditors([openable], this.fileService);
86112
this.editorService.openEditors(inputs);
@@ -100,7 +126,7 @@ export class BrowserHostService extends Disposable implements IHostService {
100126
return this.labelService.getUriLabel(openable.fileUri);
101127
}
102128

103-
private shouldOpenNewWindow(options: IOpenInWindowOptions = {}): { openFolderInNewWindow: boolean } {
129+
private shouldReuse(options: IOpenInWindowOptions = {}): boolean {
104130
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
105131
const openFolderInNewWindowConfig = (windowConfig && windowConfig.openFoldersInNewWindow) || 'default' /* default */;
106132

@@ -109,17 +135,11 @@ export class BrowserHostService extends Disposable implements IHostService {
109135
openFolderInNewWindow = (openFolderInNewWindowConfig === 'on');
110136
}
111137

112-
return { openFolderInNewWindow };
138+
return !openFolderInNewWindow;
113139
}
114140

115141
async openEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {
116-
// TODO@Ben delegate to embedder
117-
const targetHref = `${document.location.origin}${document.location.pathname}?ew=true`;
118-
if (options && options.reuse) {
119-
window.location.href = targetHref;
120-
} else {
121-
window.open(targetHref);
122-
}
142+
this.workspaceProvider.open(undefined, { reuse: options && options.reuse });
123143
}
124144

125145
async toggleFullScreen(): Promise<void> {

src/vs/workbench/workbench.web.api.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { LogLevel } from 'vs/platform/log/common/log';
1515
import { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/browser/updateService';
1616
import { Event, Emitter } from 'vs/base/common/event';
1717
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
18+
import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/browser/browserHostService';
1819

1920
interface IWorkbenchConstructionOptions {
2021

@@ -36,14 +37,9 @@ interface IWorkbenchConstructionOptions {
3637
webviewEndpoint?: string;
3738

3839
/**
39-
* Experimental: An optional folder that is set as workspace context for the workbench.
40+
* Experimental: a handler for opening workspaces and providing the initial workspace.
4041
*/
41-
folderUri?: URI;
42-
43-
/**
44-
* Experimental: An optional workspace that is set as workspace context for the workbench.
45-
*/
46-
workspaceUri?: URI;
42+
workspaceProvider?: IWorkspaceProvider;
4743

4844
/**
4945
* Experimental: The userDataProvider is used to handle user specific application
@@ -91,7 +87,6 @@ interface IWorkbenchConstructionOptions {
9187
*/
9288
updateProvider?: IUpdateProvider;
9389

94-
9590
/**
9691
* Experimental: Support adding additional properties to telemetry.
9792
*/
@@ -127,6 +122,10 @@ export {
127122
IDisposable,
128123
Disposable,
129124

125+
// Workspace
126+
IWorkspace,
127+
IWorkspaceProvider,
128+
130129
// FileSystem
131130
IFileSystemProvider,
132131
FileSystemProviderCapabilities,

0 commit comments

Comments
 (0)