diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 713e9e58ce2..32b5f1a0903 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider } from 'vs/workbench/workbench.web.api'; +import { IWorkbenchConstructionOptions, create, URI, Event, Emitter, UriComponents, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace } from 'vs/workbench/workbench.web.api'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationToken } from 'vs/base/common/cancellation'; import { streamToBuffer } from 'vs/base/common/buffer'; import { Disposable } from 'vs/base/common/lifecycle'; import { request } from 'vs/base/parts/request/browser/request'; +import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; interface ICredential { service: string; @@ -197,18 +198,43 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi } } -const options: IWorkbenchConstructionOptions = JSON.parse(document.getElementById('vscode-workbench-web-configuration')!.getAttribute('data-settings')!); +class WorkspaceProvider implements IWorkspaceProvider { + + constructor(public readonly workspace: IWorkspace) { } + + async open(workspace: IWorkspace, options?: { reuse?: boolean }): Promise { + let targetHref: string | undefined = undefined; + + // Empty + if (!workspace) { + targetHref = `${document.location.origin}${document.location.pathname}?ew=true`; + } + + // Folder + else if (isFolderToOpen(workspace)) { + targetHref = `${document.location.origin}${document.location.pathname}?folder=${workspace.folderUri.path}`; + } + + // Workspace + else if (isWorkspaceToOpen(workspace)) { + targetHref = `${document.location.origin}${document.location.pathname}?workspace=${workspace.workspaceUri.path}`; + } + + if (targetHref) { + if (options && options.reuse) { + window.location.href = targetHref; + } else { + window.open(targetHref); + } + } + } +} + +const options: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(document.getElementById('vscode-workbench-web-configuration')!.getAttribute('data-settings')!); +options.workspaceProvider = new WorkspaceProvider(options.folderUri ? { folderUri: URI.revive(options.folderUri) } : options.workspaceUri ? { workspaceUri: URI.revive(options.workspaceUri) } : undefined); options.urlCallbackProvider = new PollingURLCallbackProvider(); options.credentialsProvider = new LocalStorageCredentialsProvider(); -if (options.folderUri) { - options.folderUri = URI.revive(options.folderUri); -} - -if (options.workspaceUri) { - options.workspaceUri = URI.revive(options.workspaceUri); -} - if (Array.isArray(options.staticExtensions)) { options.staticExtensions.forEach(extension => { extension.extensionLocation = URI.revive(extension.extensionLocation); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 7f30ca67b70..0b46377515b 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -200,7 +200,7 @@ const workspacesCategory = nls.localize('workspaces', "Workspaces"); registry.registerWorkbenchAction(new SyncActionDescriptor(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL), 'Workspaces: Add Folder to Workspace...', workspacesCategory, SupportsWorkspacesContext); registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalRemoveRootFolderAction, GlobalRemoveRootFolderAction.ID, GlobalRemoveRootFolderAction.LABEL), 'Workspaces: Remove Folder from Workspace...', workspacesCategory, SupportsWorkspacesContext); -registry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'File: Close Workspace', workspacesCategory, SupportsWorkspacesContext); +registry.registerWorkbenchAction(new SyncActionDescriptor(CloseWorkspaceAction, CloseWorkspaceAction.ID, CloseWorkspaceAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, SupportsWorkspacesContext); // --- Menu Registration diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 82f56ad6de5..84c46faa362 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -33,7 +33,7 @@ import { ConfigurationCache } from 'vs/workbench/services/configuration/browser/ import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/browser/signService'; import { hash } from 'vs/base/common/hash'; -import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; +import { IWorkbenchConstructionOptions, IWorkspace } from 'vs/workbench/workbench.web.api'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { BACKUPS } from 'vs/platform/environment/common/environment'; import { joinPath } from 'vs/base/common/resources'; @@ -47,6 +47,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedDBLogProvider'; import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLogProvider'; +import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; class BrowserMain extends Disposable { @@ -284,15 +285,27 @@ class BrowserMain extends Disposable { } private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload { + let workspace: IWorkspace | undefined = undefined; + if (this.configuration.workspaceProvider) { + workspace = this.configuration.workspaceProvider.workspace; + } else { + // TODO@ben remove me once IWorkspaceProvider API is adopted + const legacyConfiguration = this.configuration as { workspaceUri?: URI, folderUri?: URI }; + if (legacyConfiguration.workspaceUri) { + workspace = { workspaceUri: legacyConfiguration.workspaceUri }; + } else if (legacyConfiguration.folderUri) { + workspace = { folderUri: legacyConfiguration.folderUri }; + } + } // Multi-root workspace - if (this.configuration.workspaceUri) { - return { id: hash(this.configuration.workspaceUri.toString()).toString(16), configPath: this.configuration.workspaceUri }; + if (workspace && isWorkspaceToOpen(workspace)) { + return { id: hash(workspace.workspaceUri.toString()).toString(16), configPath: workspace.workspaceUri }; } // Single-folder workspace - if (this.configuration.folderUri) { - return { id: hash(this.configuration.folderUri.toString()).toString(16), folder: this.configuration.folderUri }; + if (workspace && isFolderToOpen(workspace)) { + return { id: hash(workspace.folderUri.toString()).toString(16), folder: workspace.folderUri }; } return { id: 'empty-window' }; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 47c9912e899..e54b26b0fd2 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -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'; +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'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; @@ -41,7 +41,6 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(ShowActiveFileInExplor registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseExplorerView, CollapseExplorerView.ID, CollapseExplorerView.LABEL), 'File: Collapse Folders in Explorer', category.value); registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL), 'File: Refresh Explorer', category.value); registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalNewUntitledFileAction, GlobalNewUntitledFileAction.ID, GlobalNewUntitledFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N }), 'File: New Untitled File', category.value); -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); 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); registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category.value); diff --git a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts index 20be6820e3f..dd9caef709a 100644 --- a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts @@ -18,9 +18,12 @@ import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/fi import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { revealResourcesInOS } from 'vs/workbench/contrib/files/electron-browser/fileCommands'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +import { ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; 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, { const category = { value: nls.localize('filesCategory', "File"), original: 'File' }; appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'Reveal in Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' }, category); + +const registry = Registry.as(ActionExtensions.WorkbenchActions); +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); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 5fcf011d725..a85738f3351 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -58,7 +58,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWindowSessionLogFileAction, OpenWindowSessionLogFileAction.ID, OpenWindowSessionLogFileAction.LABEL), 'Developer: Open Browser Log File (Session)...', devCategory); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenWindowSessionLogFileAction, OpenWindowSessionLogFileAction.ID, OpenWindowSessionLogFileAction.LABEL), 'Developer: Open Window Log File (Session)...', devCategory); } private registerNativeContributions(): void { diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 57143242756..230f1425f51 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -15,6 +15,32 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +/** + * A workspace to open in the workbench can either be: + * - a workspace file with 0-N folders (via `workspaceUri`) + * - a single folder (via `folderUri`) + * - empty (via `undefined`) + */ +export type IWorkspace = { workspaceUri: URI } | { folderUri: URI } | undefined; + +export interface IWorkspaceProvider { + + /** + * The initial workspace to open. + */ + readonly workspace: IWorkspace; + + /** + * Asks to open a workspace in the current or a new window. + * + * @param workspace the workspace to open. + * @param options wether to open inside the current window or a new window. + */ + open(workspace: IWorkspace, options?: { reuse?: boolean }): Promise; +} export class BrowserHostService extends Disposable implements IHostService { @@ -27,15 +53,27 @@ export class BrowserHostService extends Disposable implements IHostService { //#endregion + private workspaceProvider: IWorkspaceProvider; + constructor( @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { super(); + if (environmentService.options && environmentService.options.workspaceProvider) { + this.workspaceProvider = environmentService.options.workspaceProvider; + } else { + this.workspaceProvider = new class implements IWorkspaceProvider { + readonly workspace = undefined; + async open() { } + }; + } + this.registerListeners(); } @@ -54,33 +92,21 @@ export class BrowserHostService extends Disposable implements IHostService { readonly windowCount = Promise.resolve(1); async openInWindow(toOpen: IWindowOpenable[], options?: IOpenInWindowOptions): Promise { - // TODO@Ben delegate to embedder - const { openFolderInNewWindow } = this.shouldOpenNewWindow(options); for (let i = 0; i < toOpen.length; i++) { const openable = toOpen[i]; openable.label = openable.label || this.getRecentLabel(openable); // Folder if (isFolderToOpen(openable)) { - const newAddress = `${document.location.origin}${document.location.pathname}?folder=${openable.folderUri.path}`; - if (openFolderInNewWindow) { - window.open(newAddress); - } else { - window.location.href = newAddress; - } + this.workspaceProvider.open({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options) }); } // Workspace else if (isWorkspaceToOpen(openable)) { - const newAddress = `${document.location.origin}${document.location.pathname}?workspace=${openable.workspaceUri.path}`; - if (openFolderInNewWindow) { - window.open(newAddress); - } else { - window.location.href = newAddress; - } + this.workspaceProvider.open({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options) }); } - // File + // File: open via editor service in current window else if (isFileToOpen(openable)) { const inputs: IResourceEditor[] = await pathsToEditors([openable], this.fileService); this.editorService.openEditors(inputs); @@ -100,7 +126,7 @@ export class BrowserHostService extends Disposable implements IHostService { return this.labelService.getUriLabel(openable.fileUri); } - private shouldOpenNewWindow(options: IOpenInWindowOptions = {}): { openFolderInNewWindow: boolean } { + private shouldReuse(options: IOpenInWindowOptions = {}): boolean { const windowConfig = this.configurationService.getValue('window'); const openFolderInNewWindowConfig = (windowConfig && windowConfig.openFoldersInNewWindow) || 'default' /* default */; @@ -109,17 +135,11 @@ export class BrowserHostService extends Disposable implements IHostService { openFolderInNewWindow = (openFolderInNewWindowConfig === 'on'); } - return { openFolderInNewWindow }; + return !openFolderInNewWindow; } async openEmptyWindow(options?: IOpenEmptyWindowOptions): Promise { - // TODO@Ben delegate to embedder - const targetHref = `${document.location.origin}${document.location.pathname}?ew=true`; - if (options && options.reuse) { - window.location.href = targetHref; - } else { - window.open(targetHref); - } + this.workspaceProvider.open(undefined, { reuse: options && options.reuse }); } async toggleFullScreen(): Promise { diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 51baf8f108f..4833155c493 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -15,6 +15,7 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/browser/updateService'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; interface IWorkbenchConstructionOptions { @@ -36,14 +37,9 @@ interface IWorkbenchConstructionOptions { webviewEndpoint?: string; /** - * Experimental: An optional folder that is set as workspace context for the workbench. + * Experimental: a handler for opening workspaces and providing the initial workspace. */ - folderUri?: URI; - - /** - * Experimental: An optional workspace that is set as workspace context for the workbench. - */ - workspaceUri?: URI; + workspaceProvider?: IWorkspaceProvider; /** * Experimental: The userDataProvider is used to handle user specific application @@ -91,7 +87,6 @@ interface IWorkbenchConstructionOptions { */ updateProvider?: IUpdateProvider; - /** * Experimental: Support adding additional properties to telemetry. */ @@ -127,6 +122,10 @@ export { IDisposable, Disposable, + // Workspace + IWorkspace, + IWorkspaceProvider, + // FileSystem IFileSystemProvider, FileSystemProviderCapabilities,