diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index e01be8d28fd..42656aa18b3 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -167,7 +167,7 @@ export class WindowsManager implements IWindowsMainService { } this.dialogs = new Dialogs(environmentService, telemetryService, stateService, this); - this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, this); + this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, historyMainService, this); } private getWindowsState(): IWindowsState { @@ -1955,7 +1955,8 @@ class WorkspacesManager { private workspacesMainService: IWorkspacesMainService, private backupMainService: IBackupMainService, private environmentService: IEnvironmentService, - private windowsMainService: IWindowsMainService + private historyMainService: IHistoryMainService, + private windowsMainService: IWindowsMainService, ) { } @@ -2090,7 +2091,11 @@ class WorkspacesManager { defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace) }, window).then(target => { if (target) { - return this.workspacesMainService.saveWorkspace(workspace, target).then(() => false, () => false); + return this.workspacesMainService.saveWorkspace(workspace, target).then(savedWorkspace => { + this.historyMainService.addRecentlyOpened([savedWorkspace], []); + return false; + }, + () => false); } return true; // keep veto if no target was provided diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 85ecbea3aed..1b0d20dc036 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -12,7 +12,7 @@ import { getBaseLabel, getPathLabel } from 'vs/base/common/labels'; import { IPath } from 'vs/platform/windows/common/windows'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, IWorkspacesMainService, IWorkspaceSavedEvent, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspacesMainService, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history'; import { isEqual } from 'vs/base/common/paths'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -54,18 +54,6 @@ export class HistoryMainService implements IHistoryMainService { @IEnvironmentService private readonly environmentService: IEnvironmentService ) { this.macOSRecentDocumentsUpdater = new RunOnceScheduler(() => this.updateMacOSRecentDocuments(), 800); - - this.registerListeners(); - } - - private registerListeners(): void { - this.workspacesMainService.onWorkspaceSaved(e => this.onWorkspaceSaved(e)); - } - - private onWorkspaceSaved(e: IWorkspaceSavedEvent): void { - - // Make sure to add newly saved workspaces to the list of recent workspaces - this.addRecentlyOpened([e.workspace], []); } addRecentlyOpened(workspaces: Array, files: URI[]): void { diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 2ff6aaff74a..0d1c0d017c6 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -77,7 +77,6 @@ export interface IWorkspaceFolderCreationData { export interface IWorkspacesMainService extends IWorkspacesService { _serviceBrand: any; - onWorkspaceSaved: Event; onUntitledWorkspaceDeleted: Event; saveWorkspace(workspace: IWorkspaceIdentifier, target: string): Promise; @@ -101,6 +100,8 @@ export interface IWorkspacesService { _serviceBrand: any; createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise; + + deleteIfUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; } export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index fa8455f1da1..9435ca0a714 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, 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 { join, dirname, isAbsolute, resolve, extname } from 'path'; +import { join, dirname, 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'; +import { isLinux } from 'vs/base/common/platform'; import { delSync, readdirSync, writeFileAndFlushSync } from 'vs/base/node/extfs'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,8 +17,7 @@ import { isEqual } from 'vs/base/common/paths'; import { coalesce } from 'vs/base/common/arrays'; import { createHash } from 'crypto'; import * as json from 'vs/base/common/json'; -import * as jsonEdit from 'vs/base/common/jsonEdit'; -import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; +import { massageFolderPathForWorkspace, rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/node/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -35,9 +34,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain private workspacesHome: string; - private readonly _onWorkspaceSaved = this._register(new Emitter()); - get onWorkspaceSaved(): Event { return this._onWorkspaceSaved.event; } - private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); get onUntitledWorkspaceDeleted(): Event { return this._onUntitledWorkspaceDeleted.event; } @@ -203,48 +199,12 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain // Read the contents of the workspace file and resolve it return readFile(workspace.configPath).then(raw => { - const rawWorkspaceContents = raw.toString(); - let storedWorkspace: IStoredWorkspace; - try { - storedWorkspace = this.doParseStoredWorkspace(URI.file(workspace.configPath), rawWorkspaceContents); - } catch (error) { - return Promise.reject(error); - } - - const sourceConfigFolder = dirname(workspace.configPath); - const targetConfigFolder = dirname(targetConfigPath); - - // 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 (!isAbsolute(folder.path)) { - folder.path = resolve(sourceConfigFolder, folder.path); // relative paths get resolved against the workspace location - } - folder.path = massageFolderPathForWorkspace(folder.path, URI.file(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); - }); + const targetConfigPathURI = URI.file(targetConfigPath); + const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.toString(), URI.file(workspace.configPath), targetConfigPathURI); return writeFile(targetConfigPath, newRawWorkspaceContents).then(() => { - const savedWorkspaceIdentifier = { id: this.getWorkspaceId(targetConfigPath), configPath: targetConfigPath }; - - // Event - this._onWorkspaceSaved.fire({ workspace: savedWorkspaceIdentifier, oldConfigPath: workspace.configPath }); - - // Delete untitled workspace this.deleteUntitledWorkspaceSync(workspace); - - return savedWorkspaceIdentifier; + return this.getWorkspaceIdentifier(targetConfigPathURI); }); }); } @@ -261,6 +221,11 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain this._onUntitledWorkspaceDeleted.fire(workspace); } + deleteIfUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { + this.deleteUntitledWorkspaceSync(workspace); + return Promise.resolve(); + } + private doDeleteUntitledWorkspaceSync(configPath: string): void { try { diff --git a/src/vs/platform/workspaces/node/workspacesIpc.ts b/src/vs/platform/workspaces/node/workspacesIpc.ts index 6b44c1dedea..e7a44f5bea7 100644 --- a/src/vs/platform/workspaces/node/workspacesIpc.ts +++ b/src/vs/platform/workspaces/node/workspacesIpc.ts @@ -32,6 +32,11 @@ export class WorkspacesChannel implements IServerChannel { return this.service.createUntitledWorkspace(folders); } + case 'deleteIfUntitledWorkspace': { + const rawWorkspace: IWorkspaceIdentifier = arg; + // TODO aeschli: resolve IWorkspaceIdentifier when switching to URI + return this.service.deleteIfUntitledWorkspace(rawWorkspace); + } } throw new Error(`Call not found: ${command}`); @@ -47,4 +52,8 @@ export class WorkspacesChannelClient implements IWorkspacesService { createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise { return this.channel.call('createUntitledWorkspace', folders); } + + deleteIfUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { + return this.channel.call('deleteIfUntitledWorkspace', workspace); + } } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 13bb75596aa..b863c62f175 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -12,7 +12,7 @@ import * as pfs from 'vs/base/node/pfs'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { WORKSPACE_EXTENSION, IWorkspaceSavedEvent, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { WORKSPACE_EXTENSION, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { NullLogService } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; @@ -224,16 +224,6 @@ suite('WorkspacesMainService', () => { }); test('saveWorkspace (untitled)', () => { - let savedEvent: IWorkspaceSavedEvent; - const listener = service.onWorkspaceSaved(e => { - savedEvent = e; - }); - - let deletedEvent: IWorkspaceIdentifier; - const listener2 = service.onUntitledWorkspaceDeleted(e => { - deletedEvent = e; - }); - return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => { const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`); @@ -250,14 +240,6 @@ suite('WorkspacesMainService', () => { assertPathEquals((ws.folders[1]).path, '.'); // relative assertPathEquals((ws.folders[2]).path, path.relative(path.dirname(workspaceConfigPath), path.join(os.tmpdir(), 'somefolder'))); // relative - assert.equal(savedWorkspace, savedEvent.workspace); - assertPathEquals(workspace.configPath, savedEvent.oldConfigPath); - - assert.deepEqual(deletedEvent, workspace); - - listener.dispose(); - listener2.dispose(); - extfs.delSync(workspaceConfigPath); }); }); diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index abcb3fff36f..e65b8e1c29e 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -43,7 +43,7 @@ export interface IWorkspaceEditingService { createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise; /** - * saves the workspace to the provided path and opens it. requires a workspace to be opened. + * saves the current workspace to the provided path and opens it. requires a workspace to be opened. */ saveAndEnterWorkspace(path: URI): Promise; diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 2427501df0f..9ac1f58843e 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWindowService, MessageBoxOptions, IWindowsService } from 'vs/platform/windows/common/windows'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IStoredWorkspace, isStoredWorkspaceFolder, isRawFileWorkspaceFolder, isWorkspaceIdentifier, toWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, isWorkspaceIdentifier, toWorkspaceIdentifier, IWorkspacesService } 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,15 +21,11 @@ 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, 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 { isLinux } from 'vs/base/common/platform'; +import { isEqual, basename } from 'vs/base/common/resources'; 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'; +import { rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/node/workspaces'; export class WorkspaceEditingService implements IWorkspaceEditingService { @@ -156,7 +152,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { const untitledWorkspace = await this.workspaceService.createUntitledWorkspace(folders); if (path) { - await this.saveWorkspace(untitledWorkspace, path); + await this.saveWorkspaceAs(untitledWorkspace, path); } else { path = URI.file(untitledWorkspace.configPath); } @@ -171,12 +167,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { if (!isWorkspaceIdentifier(currentWorkspaceIdentifier)) { return Promise.reject(null); } - await this.saveWorkspace(currentWorkspaceIdentifier, path); + await this.saveWorkspaceAs(currentWorkspaceIdentifier, path); return this.enterWorkspace(path); } - async isValidTargetWorkspacePath(path: URI): Promise { const windows = await this.windowsService.getWindows(); @@ -196,7 +191,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { return Promise.resolve(true); // OK } - private saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise { + private async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise { const configPathURI = URI.file(workspace.configPath); // Return early if target is same as source @@ -204,61 +199,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { return Promise.resolve(null); } - // 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 }); - }); - } - - 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; + // Read the contents of the workspace file, update it to new location and save it. + const raw = await this.fileSystemService.resolveContent(configPathURI); + const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value, configPathURI, targetConfigPathURI); + await this.fileSystemService.createFile(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }); + await this.workspaceService.deleteIfUntitledWorkspace(workspace); } private handleWorkspaceConfigurationEditingError(error: JSONEditingError): Promise {