/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IBackupFileService, toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup'; import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct } from 'vs/base/common/arrays'; import { isEqual, getComparisonKey } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IHostService } from 'vs/workbench/services/host/browser/host'; export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditingService { _serviceBrand: undefined; constructor( @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, @IWorkspaceContextService private readonly contextService: WorkspaceService, @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @IBackupFileService private readonly backupFileService: IBackupFileService, @INotificationService private readonly notificationService: INotificationService, @ICommandService private readonly commandService: ICommandService, @IFileService private readonly fileService: IFileService, @ITextFileService private readonly textFileService: ITextFileService, @IWorkspacesService protected readonly workspacesService: IWorkspacesService, @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IDialogService protected readonly dialogService: IDialogService, @IHostService protected readonly hostService: IHostService ) { } pickNewWorkspacePath(): Promise { return this.fileDialogService.showSaveDialog({ saveLabel: mnemonicButtonLabel(nls.localize('save', "Save")), title: nls.localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, defaultUri: this.fileDialogService.defaultWorkspacePath() }); } updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { const folders = this.contextService.getWorkspace().folders; let foldersToDelete: URI[] = []; if (typeof deleteCount === 'number') { foldersToDelete = folders.slice(index, index + deleteCount).map(f => f.uri); } const wantsToDelete = foldersToDelete.length > 0; const wantsToAdd = Array.isArray(foldersToAdd) && foldersToAdd.length > 0; if (!wantsToAdd && !wantsToDelete) { return Promise.resolve(); // return early if there is nothing to do } // Add Folders if (wantsToAdd && !wantsToDelete && Array.isArray(foldersToAdd)) { return this.doAddFolders(foldersToAdd, index, donotNotifyError); } // Delete Folders if (wantsToDelete && !wantsToAdd) { return this.removeFolders(foldersToDelete); } // Add & Delete Folders else { // if we are in single-folder state and the folder is replaced with // other folders, we handle this specially and just enter workspace // mode with the folders that are being added. if (this.includesSingleFolderWorkspace(foldersToDelete)) { return this.createAndEnterWorkspace(foldersToAdd!); } // if we are not in workspace-state, we just add the folders if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) { return this.doAddFolders(foldersToAdd!, index, donotNotifyError); } // finally, update folders within the workspace return this.doUpdateFolders(foldersToAdd!, foldersToDelete, index, donotNotifyError); } } private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise { try { await this.contextService.updateFolders(foldersToAdd, foldersToDelete, index); } catch (error) { if (donotNotifyError) { throw error; } this.handleWorkspaceConfigurationEditingError(error); } } addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): Promise { return this.doAddFolders(foldersToAdd, undefined, donotNotifyError); } private async doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise { const state = this.contextService.getWorkbenchState(); if (this.environmentService.configuration.remoteAuthority) { // Do not allow workspace folders with scheme different than the current remote scheme const schemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme); if (schemas.length && foldersToAdd.some(f => schemas.indexOf(f.uri.scheme) === -1)) { return Promise.reject(new Error(nls.localize('differentSchemeRoots', "Workspace folders from different providers are not allowed in the same workspace."))); } } // If we are in no-workspace or single-folder workspace, adding folders has to // enter a workspace. if (state !== WorkbenchState.WORKSPACE) { let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd); newWorkspaceFolders = distinct(newWorkspaceFolders, folder => getComparisonKey(folder.uri)); if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) { return; // return if the operation is a no-op for the current state } return this.createAndEnterWorkspace(newWorkspaceFolders); } // Delegate addition of folders to workspace service otherwise try { await this.contextService.addFolders(foldersToAdd, index); } catch (error) { if (donotNotifyError) { throw error; } this.handleWorkspaceConfigurationEditingError(error); } } async removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise { // If we are in single-folder state and the opened folder is to be removed, // we create an empty workspace and enter it. if (this.includesSingleFolderWorkspace(foldersToRemove)) { return this.createAndEnterWorkspace([]); } // Delegate removal of folders to workspace service otherwise try { await this.contextService.removeFolders(foldersToRemove); } catch (error) { if (donotNotifyError) { throw error; } this.handleWorkspaceConfigurationEditingError(error); } } private includesSingleFolderWorkspace(folders: URI[]): boolean { if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { const workspaceFolder = this.contextService.getWorkspace().folders[0]; return (folders.some(folder => isEqual(folder, workspaceFolder.uri))); } return false; } async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise { if (path && !await this.isValidTargetWorkspacePath(path)) { return; } const remoteAuthority = this.environmentService.configuration.remoteAuthority; const untitledWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority); if (path) { await this.saveWorkspaceAs(untitledWorkspace, path); } else { path = untitledWorkspace.configPath; } return this.enterWorkspace(path); } async saveAndEnterWorkspace(path: URI): Promise { if (!await this.isValidTargetWorkspacePath(path)) { return; } const workspaceIdentifier = this.getCurrentWorkspaceIdentifier(); if (!workspaceIdentifier) { return; } await this.saveWorkspaceAs(workspaceIdentifier, path); return this.enterWorkspace(path); } async isValidTargetWorkspacePath(path: URI): Promise { return true; // OK } protected async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise { const configPathURI = workspace.configPath; // Return early if target is same as source if (isEqual(configPathURI, targetConfigPathURI)) { return; } // Read the contents of the workspace file, update it to new location and save it. const raw = await this.fileService.readFile(configPathURI); const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value.toString(), configPathURI, targetConfigPathURI); await this.textFileService.create(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true }); } private handleWorkspaceConfigurationEditingError(error: JSONEditingError): void { switch (error.code) { case JSONEditingErrorCode.ERROR_INVALID_FILE: this.onInvalidWorkspaceConfigurationFileError(); break; case JSONEditingErrorCode.ERROR_FILE_DIRTY: this.onWorkspaceConfigurationFileDirtyError(); break; default: this.notificationService.error(error.message); } } private onInvalidWorkspaceConfigurationFileError(): void { const message = nls.localize('errorInvalidTaskConfiguration', "Unable to write into workspace configuration file. Please open the file to correct errors/warnings in it and try again."); this.askToOpenWorkspaceConfigurationFile(message); } private onWorkspaceConfigurationFileDirtyError(): void { const message = nls.localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file is dirty. Please save it and try again."); this.askToOpenWorkspaceConfigurationFile(message); } private askToOpenWorkspaceConfigurationFile(message: string): void { this.notificationService.prompt(Severity.Error, message, [{ label: nls.localize('openWorkspaceConfigurationFile', "Open Workspace Configuration"), run: () => this.commandService.executeCommand('workbench.action.openWorkspaceConfigFile') }] ); } async enterWorkspace(path: URI): Promise { if (!!this.environmentService.extensionTestsLocationURI) { throw new Error('Entering a new workspace is not possible in tests.'); } const workspace = await this.workspacesService.getWorkspaceIdentifier(path); // Settings migration (only if we come from a folder workspace) if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { await this.migrateWorkspaceSettings(workspace); } const workspaceImpl = this.contextService as WorkspaceService; await workspaceImpl.initialize(workspace); const result = await this.workspacesService.enterWorkspace(path); if (result) { // Migrate storage to new workspace await this.migrateStorage(result.workspace); // Reinitialize backup service this.environmentService.configuration.backupPath = result.backupPath; this.environmentService.configuration.backupWorkspaceResource = result.backupPath ? toBackupWorkspaceResource(result.backupPath, this.environmentService) : undefined; if (this.backupFileService instanceof BackupFileService) { this.backupFileService.reinitialize(); } } // TODO@aeschli: workaround until restarting works if (this.environmentService.configuration.remoteAuthority) { this.hostService.reload(); } // Restart the extension host: entering a workspace means a new location for // storage and potentially a change in the workspace.rootPath property. else { this.extensionService.restartExtensionHost(); } } private migrateStorage(toWorkspace: IWorkspaceIdentifier): Promise { return this.storageService.migrate(toWorkspace); } private migrateWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise { return this.doCopyWorkspaceSettings(toWorkspace, setting => setting.scope === ConfigurationScope.WINDOW); } copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise { return this.doCopyWorkspaceSettings(toWorkspace); } private doCopyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier, filter?: (config: IConfigurationPropertySchema) => boolean): Promise { const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); const targetWorkspaceConfiguration: any = {}; for (const key of this.configurationService.keys().workspace) { if (configurationProperties[key]) { if (filter && !filter(configurationProperties[key])) { continue; } targetWorkspaceConfiguration[key] = this.configurationService.inspect(key).workspace; } } return this.jsonEditingService.write(toWorkspace.configPath, [{ key: 'settings', value: targetWorkspaceConfiguration }], true); } protected getCurrentWorkspaceIdentifier(): IWorkspaceIdentifier | undefined { const workspace = this.contextService.getWorkspace(); if (workspace && workspace.configuration) { return { id: workspace.id, configPath: workspace.configuration }; } return undefined; } }