diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 7eff695f79a..88faa4175f6 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -34,7 +34,7 @@ import { normalizeNFC } from 'vs/base/common/normalization'; import { URI } from 'vs/base/common/uri'; import { Queue, timeout } from 'vs/base/common/async'; import { exists } from 'vs/base/node/pfs'; -import { getComparisonKey, isEqual, normalizePath } from 'vs/base/common/resources'; +import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename } from 'vs/base/common/resources'; import { endsWith } from 'vs/base/common/strings'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; @@ -1474,7 +1474,7 @@ export class WindowsManager implements IWindowsMainService { return this.workspacesManager.saveAndEnterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); } - enterWorkspace(win: ICodeWindow, path: string): Promise { + enterWorkspace(win: ICodeWindow, path: URI): Promise { return this.workspacesManager.enterWorkspace(win, path).then(result => this.doEnterWorkspace(win, result)); } @@ -1968,14 +1968,14 @@ class WorkspacesManager { } saveAndEnterWorkspace(window: ICodeWindow, path: string): Promise { - if (!window || !window.win || !window.isReady || !window.openedWorkspace || !path || !this.isValidTargetWorkspacePath(window, path)) { + if (!window || !window.win || !window.isReady || !window.openedWorkspace || !path || !this.isValidTargetWorkspacePath(window, URI.file(path))) { return Promise.resolve(null); // return early if the window is not ready or disposed or does not have a workspace } return this.doSaveAndOpenWorkspace(window, window.openedWorkspace, path); } - enterWorkspace(window: ICodeWindow, path: string): Promise { + enterWorkspace(window: ICodeWindow, path: URI): Promise { if (!window || !window.win || !window.isReady) { return Promise.resolve(null); // return early if the window is not ready or disposed } @@ -1984,20 +1984,18 @@ class WorkspacesManager { if (!isValid) { return null; // return early if the workspace is not valid } - - return this.workspacesMainService.resolveWorkspace(path).then(workspace => { - return this.doOpenWorkspace(window, workspace); - }); + const workspaceIdentifier = this.workspacesMainService.getWorkspaceIdentifier(path); + return this.doOpenWorkspace(window, workspaceIdentifier); }); } createAndEnterWorkspace(window: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): Promise { - if (!window || !window.win || !window.isReady) { + if (!window || !window.win || !window.isReady || !path) { return Promise.resolve(null); // return early if the window is not ready or disposed } - return this.isValidTargetWorkspacePath(window, path).then(isValid => { + return this.isValidTargetWorkspacePath(window, URI.file(path)).then(isValid => { if (!isValid) { return null; // return early if the workspace is not valid } @@ -2008,22 +2006,22 @@ class WorkspacesManager { }); } - private isValidTargetWorkspacePath(window: ICodeWindow, path?: string): Promise { + private isValidTargetWorkspacePath(window: ICodeWindow, path?: URI): Promise { if (!path) { return Promise.resolve(true); } - if (window.openedWorkspace && window.openedWorkspace.configPath === path) { + if (window.openedWorkspace && isEqual(URI.file(window.openedWorkspace.configPath), path)) { return Promise.resolve(false); // window is already opened on a workspace with that path } // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesMainService.getWorkspaceId(path), configPath: path })) { + if (findWindowOnWorkspace(this.windowsMainService.getWindows(), this.workspacesMainService.getWorkspaceIdentifier(path))) { const options: Electron.MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", resourcesBasename(path)), detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), noLink: true }; diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 2d1761c2446..78292433ce5 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -114,7 +114,7 @@ export interface IWindowsService { openDevTools(windowId: number, options?: IDevToolsOptions): Promise; toggleDevTools(windowId: number): Promise; closeWorkspace(windowId: number): Promise; - enterWorkspace(windowId: number, path: string): Promise; + enterWorkspace(windowId: number, path: URI): Promise; createAndEnterWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], path?: string): Promise; saveAndEnterWorkspace(windowId: number, path: string): Promise; toggleFullScreen(windowId: number): Promise; @@ -207,7 +207,7 @@ export interface IWindowService { toggleDevTools(): Promise; closeWorkspace(): Promise; updateTouchBar(items: ISerializableCommandAction[][]): Promise; - enterWorkspace(path: string): Promise; + enterWorkspace(path: URI): Promise; createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): Promise; saveAndEnterWorkspace(path: string): Promise; toggleFullScreen(): Promise; diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 60457efd10c..040f396f8d9 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -89,7 +89,7 @@ export class WindowService extends Disposable implements IWindowService { return this.windowsService.closeWorkspace(this.windowId); } - enterWorkspace(path: string): Promise { + enterWorkspace(path: URI): Promise { return this.windowsService.enterWorkspace(this.windowId, path); } diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 839ba5b7b6f..b8756fd9130 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -94,7 +94,7 @@ export interface IWindowsMainService { // methods ready(initialUserEnv: IProcessEnvironment): void; reload(win: ICodeWindow, cli?: ParsedArgs): void; - enterWorkspace(win: ICodeWindow, path: string): Promise; + enterWorkspace(win: ICodeWindow, path: URI): Promise; createAndEnterWorkspace(win: ICodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): Promise; saveAndEnterWorkspace(win: ICodeWindow, path: string): Promise; closeWorkspace(win: ICodeWindow): void; diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 8e3998a11e3..0f116f6bc9c 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -138,7 +138,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return this.withWindow(windowId, codeWindow => this.windowsMainService.closeWorkspace(codeWindow)); } - async enterWorkspace(windowId: number, path: string): Promise { + async enterWorkspace(windowId: number, path: URI): Promise { this.logService.trace('windowsService#enterWorkspace', windowId); return this.withWindow(windowId, codeWindow => this.windowsMainService.enterWorkspace(codeWindow, path)); diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/node/windowsIpc.ts index f590f6c7832..ddd42188cd6 100644 --- a/src/vs/platform/windows/node/windowsIpc.ts +++ b/src/vs/platform/windows/node/windowsIpc.ts @@ -56,7 +56,7 @@ export class WindowsChannel implements IServerChannel { case 'openDevTools': return this.service.openDevTools(arg[0], arg[1]); case 'toggleDevTools': return this.service.toggleDevTools(arg); case 'closeWorkspace': return this.service.closeWorkspace(arg); - case 'enterWorkspace': return this.service.enterWorkspace(arg[0], arg[1]); + case 'enterWorkspace': return this.service.enterWorkspace(arg[0], URI.revive(arg[1])); case 'createAndEnterWorkspace': { const rawFolders: IWorkspaceFolderCreationData[] = arg[1]; let folders: IWorkspaceFolderCreationData[] | undefined = undefined; @@ -179,7 +179,7 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('closeWorkspace', windowId); } - enterWorkspace(windowId: number, path: string): Promise { + enterWorkspace(windowId: number, path: URI): Promise { return this.channel.call('enterWorkspace', [windowId, path]); } diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d913586c132..7b8a8a09b23 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -84,8 +84,6 @@ export interface IWorkspacesMainService extends IWorkspacesService { createWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; - resolveWorkspace(path: string): Promise; - resolveWorkspaceSync(path: string): IResolvedWorkspace | null; isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; @@ -95,6 +93,8 @@ export interface IWorkspacesMainService extends IWorkspacesService { getUntitledWorkspacesSync(): IWorkspaceIdentifier[]; getWorkspaceId(workspacePath: string): string; + + getWorkspaceIdentifier(workspacePath: URI): IWorkspaceIdentifier; } export interface IWorkspacesService { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 2828f1268ae..c7521c466ab 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -6,7 +6,7 @@ import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { isParent } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { extname, join, dirname, isAbsolute, resolve } from 'path'; +import { join, dirname, isAbsolute, resolve, extname } from 'path'; import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs'; import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; @@ -23,6 +23,7 @@ import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; +import { fsPath, dirname as resourcesDirname } from 'vs/base/common/resources'; export interface IStoredWorkspace { folders: IStoredWorkspaceFolder[]; @@ -49,14 +50,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain this.workspacesHome = environmentService.workspacesHome; } - resolveWorkspace(path: string): Promise { - if (!this.isWorkspacePath(path)) { - return Promise.resolve(null); // does not look like a valid workspace config file - } - - return readFile(path, 'utf8').then(contents => this.doResolveWorkspace(path, contents)); - } - resolveWorkspaceSync(path: string): IResolvedWorkspace | null { if (!this.isWorkspacePath(path)) { return null; // does not look like a valid workspace config file @@ -69,21 +62,21 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return null; // invalid workspace } - return this.doResolveWorkspace(path, contents); + return this.doResolveWorkspace(URI.file(path), contents); } private isWorkspacePath(path: string): boolean { return this.isInsideWorkspacesHome(path) || extname(path) === `.${WORKSPACE_EXTENSION}`; } - private doResolveWorkspace(path: string, contents: string): IResolvedWorkspace | null { + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { try { const workspace = this.doParseStoredWorkspace(path, contents); - + const workspaceIdentifier = this.getWorkspaceIdentifier(path); return { - id: this.getWorkspaceId(path), - configPath: path, - folders: toWorkspaceFolders(workspace.folders, URI.file(dirname(path))) + id: workspaceIdentifier.id, + configPath: workspaceIdentifier.configPath, + folders: toWorkspaceFolders(workspace.folders, resourcesDirname(path)!) }; } catch (error) { this.logService.warn(error.toString()); @@ -92,7 +85,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return null; } - private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace { + private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { // Parse workspace file let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser @@ -104,7 +97,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain // Validate if (!Array.isArray(storedWorkspace.folders)) { - throw new Error(`${path} looks like an invalid workspace file.`); + throw new Error(`${path.toString()} looks like an invalid workspace file.`); } return storedWorkspace; @@ -148,7 +141,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain // File URI if (folderResource.scheme === Schemas.file) { - storedWorkspace = { path: massageFolderPathForWorkspace(folderResource.fsPath, untitledWorkspaceConfigFolder, []) }; + storedWorkspace = { path: massageFolderPathForWorkspace(fsPath(folderResource), URI.file(untitledWorkspaceConfigFolder), []) }; } // Any URI @@ -182,6 +175,21 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return createHash('md5').update(workspaceConfigPath).digest('hex'); } + getWorkspaceIdentifier(workspacePath: URI): IWorkspaceIdentifier { + if (workspacePath.scheme === Schemas.file) { + const configPath = fsPath(workspacePath); + return { + configPath, + id: this.getWorkspaceId(configPath) + }; + } + throw new Error('Not yet supported'); + /*return { + configPath: workspacePath + id: this.getWorkspaceId(workspacePath.toString()); + };*/ + } + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { return this.isInsideWorkspacesHome(workspace.configPath); } @@ -198,7 +206,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain const rawWorkspaceContents = raw.toString(); let storedWorkspace: IStoredWorkspace; try { - storedWorkspace = this.doParseStoredWorkspace(workspace.configPath, rawWorkspaceContents); + storedWorkspace = this.doParseStoredWorkspace(URI.file(workspace.configPath), rawWorkspaceContents); } catch (error) { return Promise.reject(error); } @@ -214,7 +222,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain if (!isAbsolute(folder.path)) { folder.path = resolve(sourceConfigFolder, folder.path); // relative paths get resolved against the workspace location } - folder.path = massageFolderPathForWorkspace(folder.path, targetConfigFolder, storedWorkspace.folders); + folder.path = massageFolderPathForWorkspace(folder.path, URI.file(targetConfigFolder), storedWorkspace.folders); } }); diff --git a/src/vs/platform/workspaces/node/workspaces.ts b/src/vs/platform/workspaces/node/workspaces.ts index 723d8ea2499..d34df96d54d 100644 --- a/src/vs/platform/workspaces/node/workspaces.ts +++ b/src/vs/platform/workspaces/node/workspaces.ts @@ -5,9 +5,12 @@ import { IStoredWorkspaceFolder, isRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { isWindows, isLinux } from 'vs/base/common/platform'; -import { isAbsolute, relative } from 'path'; -import { isEqualOrParent, normalize } from 'vs/base/common/paths'; +import { isAbsolute, relative, posix } from 'path'; +import { normalize, isEqualOrParent } from 'vs/base/common/paths'; import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { URI } from 'vs/base/common/uri'; +import { fsPath } from 'vs/base/common/resources'; +import { Schemas } from 'vs/base/common/network'; const SLASH = '/'; @@ -19,27 +22,33 @@ const SLASH = '/'; * @param targetConfigFolder the folder where the workspace is living in * @param existingFolders a set of existing folders of the workspace */ -export function massageFolderPathForWorkspace(absoluteFolderPath: string, targetConfigFolder: string, existingFolders: IStoredWorkspaceFolder[]): string { - const useSlashesForPath = shouldUseSlashForPath(existingFolders); +export function massageFolderPathForWorkspace(absoluteFolderPath: string, targetConfigFolderURI: URI, existingFolders: IStoredWorkspaceFolder[]): string { - // Convert path to relative path if the target config folder - // is a parent of the path. - if (isEqualOrParent(absoluteFolderPath, targetConfigFolder, !isLinux)) { - absoluteFolderPath = relative(targetConfigFolder, absoluteFolderPath) || '.'; - } + if (targetConfigFolderURI.scheme === Schemas.file) { + const targetFolderPath = fsPath(targetConfigFolderURI); + // Convert path to relative path if the target config folder + // is a parent of the path. + if (isEqualOrParent(absoluteFolderPath, targetFolderPath, !isLinux)) { + absoluteFolderPath = relative(targetFolderPath, absoluteFolderPath) || '.'; + } - // Windows gets special treatment: - // - normalize all paths to get nice casing of drive letters - // - convert to slashes if we want to use slashes for paths - if (isWindows) { - if (isAbsolute(absoluteFolderPath)) { - if (useSlashesForPath) { - absoluteFolderPath = normalize(absoluteFolderPath, false /* do not use OS path separator */); + // Windows gets special treatment: + // - normalize all paths to get nice casing of drive letters + // - convert to slashes if we want to use slashes for paths + if (isWindows) { + if (isAbsolute(absoluteFolderPath)) { + if (shouldUseSlashForPath(existingFolders)) { + absoluteFolderPath = normalize(absoluteFolderPath, false /* do not use OS path separator */); + } + + absoluteFolderPath = normalizeDriveLetter(absoluteFolderPath); + } else if (shouldUseSlashForPath(existingFolders)) { + absoluteFolderPath = absoluteFolderPath.replace(/[\\]/g, SLASH); } - - absoluteFolderPath = normalizeDriveLetter(absoluteFolderPath); - } else if (useSlashesForPath) { - absoluteFolderPath = absoluteFolderPath.replace(/[\\]/g, SLASH); + } + } else { + if (isEqualOrParent(absoluteFolderPath, targetConfigFolderURI.path)) { + absoluteFolderPath = posix.relative(absoluteFolderPath, targetConfigFolderURI.path) || '.'; } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 14baea58a97..6a435ed4660 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -245,5 +245,5 @@ CommandsRegistry.registerCommand('_workbench.enterWorkspace', async function (ac } } - return workspaceEditingService.enterWorkspace(workspace.fsPath); + return workspaceEditingService.enterWorkspace(workspace); }); diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index e37b7cdeb80..7da39edb68c 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -151,7 +151,7 @@ export class SaveWorkspaceAsAction extends Action { return this.workspaceEditingService.createAndEnterWorkspace(folders, configPath); case WorkbenchState.WORKSPACE: - return this.workspaceEditingService.saveAndEnterWorkspace(configPath); + return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri); } } }); diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 1148a763638..e07ca65249a 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { dirname } from 'path'; import * as assert from 'vs/base/common/assert'; import { Event, Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; @@ -36,7 +35,7 @@ import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/works import { UserConfiguration } from 'vs/platform/configuration/node/configuration'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { localize } from 'vs/nls'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { @@ -162,8 +161,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat if (foldersToAdd.length) { // Recompute current workspace folders if we have folders to add - const workspaceConfigFolder = dirname(this.getWorkspace().configuration.fsPath); - currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, URI.file(workspaceConfigFolder)); + const workspaceConfigFolder = dirname(this.getWorkspace().configuration); + currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, workspaceConfigFolder); const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri); const storedFoldersToAdd: IStoredWorkspaceFolder[] = []; @@ -342,7 +341,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this.workspaceConfiguration.load({ id: workspaceIdentifier.id, configPath: URI.file(workspaceIdentifier.configPath) }) .then(() => { const workspaceConfigPath = URI.file(workspaceIdentifier.configPath); - const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(workspaceConfigPath.fsPath))); + const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), dirname(workspaceConfigPath)); const workspaceId = workspaceIdentifier.id; return new Workspace(workspaceId, workspaceFolders, workspaceConfigPath); }); @@ -535,7 +534,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private onWorkspaceConfigurationChanged(): Promise { if (this.workspace && this.workspace.configuration && this._configuration) { const workspaceConfigurationChangeEvent = this._configuration.compareAndUpdateWorkspaceConfiguration(this.workspaceConfiguration.getConfiguration()); - let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(this.workspace.configuration.fsPath))); + let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), dirname(this.workspace.configuration)); const changes = this.compareFolders(this.workspace.folders, configuredFolders); if (changes.added.length || changes.removed.length || changes.changed.length) { this.workspace.folders = configuredFolders; diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index db73788241f..8bd26a6efc3 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -34,7 +34,7 @@ export interface IWorkspaceEditingService { /** * enters the workspace with the provided path. */ - enterWorkspace(path: string): Promise; + enterWorkspace(path: URI): Promise; /** * creates a new workspace with the provided folders and opens it. if path is provided @@ -45,7 +45,7 @@ export interface IWorkspaceEditingService { /** * saves the workspace to the provided path and opens it. requires a workspace to be opened. */ - saveAndEnterWorkspace(path: string): Promise; + saveAndEnterWorkspace(path: URI): Promise; /** * copies current workspace settings to the target workspace. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 38bc550917e..2e9e7d8791d 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -7,9 +7,9 @@ import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IWindowService, IEnterWorkspaceResult } from 'vs/platform/windows/common/windows'; +import { IWindowService, IEnterWorkspaceResult, MessageBoxOptions, IWindowsService } from 'vs/platform/windows/common/windows'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IStoredWorkspace, isStoredWorkspaceFolder, isRawFileWorkspaceFolder, isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -21,9 +21,15 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct } from 'vs/base/common/arrays'; -import { isLinux } from 'vs/base/common/platform'; -import { isEqual } from 'vs/base/common/resources'; +import { isLinux, isMacintosh } from 'vs/base/common/platform'; +import { isEqual, dirname, basename } from 'vs/base/common/resources'; +import * as json from 'vs/base/common/json'; +import * as jsonEdit from 'vs/base/common/jsonEdit'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IFileService } from 'vs/platform/files/common/files'; +import { isAbsolute, resolve } from 'path'; +import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; +import { Schemas } from 'vs/base/common/network'; export class WorkspaceEditingService implements IWorkspaceEditingService { @@ -38,7 +44,9 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { @IExtensionService private readonly extensionService: IExtensionService, @IBackupFileService private readonly backupFileService: IBackupFileService, @INotificationService private readonly notificationService: INotificationService, - @ICommandService private readonly commandService: ICommandService + @ICommandService private readonly commandService: ICommandService, + @IFileService private readonly fileSystemService: IFileService, + @IWindowsService private readonly windowsService: IWindowsService, ) { } @@ -140,7 +148,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { return false; } - enterWorkspace(path: string): Promise { + enterWorkspace(path: URI): Promise { return this.doEnterWorkspace(() => this.windowService.enterWorkspace(path)); } @@ -148,8 +156,104 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path)); } - saveAndEnterWorkspace(path: string): Promise { - return this.doEnterWorkspace(() => this.windowService.saveAndEnterWorkspace(path)); + async saveAndEnterWorkspace(path: URI): Promise { + if (!this.isValidTargetWorkspacePath(path)) { + return Promise.reject(null); + } + const currentWorkspaceIdentifier = toWorkspaceIdentifier(this.contextService.getWorkspace()); + if (!isWorkspaceIdentifier(currentWorkspaceIdentifier)) { + return Promise.reject(null); + } + await this.saveWorkspace(currentWorkspaceIdentifier, path); + + return this.enterWorkspace(path); + } + + + async isValidTargetWorkspacePath(path: URI): Promise { + + const windows = await this.windowsService.getWindows(); + + // Prevent overwriting a workspace that is currently opened in another window + if (windows.some(window => window.workspace && isEqual(URI.file(window.workspace.configPath), path))) { + const options: MessageBoxOptions = { + type: 'info', + buttons: [nls.localize('ok', "OK")], + message: nls.localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), + detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + return this.windowService.showMessageBox(options).then(() => false); + } + + return Promise.resolve(true); // OK + } + + private saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise { + const configPathURI = URI.file(workspace.configPath); + + // Return early if target is same as source + if (isEqual(configPathURI, targetConfigPathURI)) { + return Promise.resolve(targetConfigPathURI); + } + + // Read the contents of the workspace file and resolve it + return this.fileSystemService.resolveFile(configPathURI).then(raw => { + const rawWorkspaceContents = raw.toString(); + let storedWorkspace: IStoredWorkspace; + try { + storedWorkspace = this.doParseStoredWorkspace(configPathURI, rawWorkspaceContents); + } catch (error) { + return Promise.reject(error); + } + + const sourceConfigFolder = dirname(configPathURI); + const targetConfigFolder = dirname(targetConfigPathURI); + + // Rewrite absolute paths to relative paths if the target workspace folder + // is a parent of the location of the workspace file itself. Otherwise keep + // using absolute paths. + storedWorkspace.folders.forEach(folder => { + if (isRawFileWorkspaceFolder(folder)) { + if (sourceConfigFolder.scheme === Schemas.file) { + if (!isAbsolute(folder.path)) { + folder.path = resolve(sourceConfigFolder.path, folder.path); // relative paths get resolved against the workspace location + } + folder.path = massageFolderPathForWorkspace(folder.path, targetConfigFolder, storedWorkspace.folders); + } + } + }); + + // Preserve as much of the existing workspace as possible by using jsonEdit + // and only changing the folders portion. + let newRawWorkspaceContents = rawWorkspaceContents; + const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], storedWorkspace.folders, { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' }); + edits.forEach(edit => { + newRawWorkspaceContents = jsonEdit.applyEdit(rawWorkspaceContents, edit); + }); + + return this.fileSystemService.createFile(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }).then(() => { + return targetConfigPathURI; + }); + }); + } + + private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { + + // Parse workspace file + let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser + + // Filter out folders which do not have a path or uri set + if (Array.isArray(storedWorkspace.folders)) { + storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); + } + + // Validate + if (!Array.isArray(storedWorkspace.folders)) { + throw new Error(`${path} looks like an invalid workspace file.`); + } + + return storedWorkspace; } private handleWorkspaceConfigurationEditingError(error: JSONEditingError): Promise { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 354243890f3..cef11f04692 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -1054,7 +1054,7 @@ export class TestWindowService implements IWindowService { return Promise.resolve(); } - enterWorkspace(_path: string): Promise { + enterWorkspace(_path: URI): Promise { return Promise.resolve(); } @@ -1223,7 +1223,7 @@ export class TestWindowsService implements IWindowsService { return Promise.resolve(); } - enterWorkspace(_windowId: number, _path: string): Promise { + enterWorkspace(_windowId: number, _path: URI): Promise { return Promise.resolve(); }