configurationEditingService.ts 9.1 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import nls = require('vs/nls');
9 10 11 12
import {TPromise} from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import * as json from 'vs/base/common/json';
import * as encoding from 'vs/base/node/encoding';
13
import strings = require('vs/base/common/strings');
14 15 16 17 18
import {getConfigurationKeys} from 'vs/platform/configuration/common/model';
import {setProperty} from 'vs/base/common/jsonEdit';
import {applyEdits} from 'vs/base/common/jsonFormatter';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IEnvironmentService} from 'vs/platform/environment/common/environment';
19
import {ITextFileService} from 'vs/workbench/services/textfile/common/textfiles';
20 21
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {WORKSPACE_CONFIG_DEFAULT_PATH} from 'vs/workbench/services/configuration/common/configuration';
22
import {IFileService} from 'vs/platform/files/common/files';
23
import {IConfigurationEditingService, ConfigurationEditingErrorCode, IConfigurationEditingError, ConfigurationTarget, IConfigurationValue} from 'vs/workbench/services/configuration/common/configurationEditing';
24

25 26 27 28 29 30 31 32 33 34
export const WORKSPACE_STANDALONE_CONFIGURATIONS = {
	'tasks': '.vscode/tasks.json',
	'launch': '.vscode/launch.json'
};

interface IConfigurationEditOperation extends IConfigurationValue {
	target: URI;
	isWorkspaceStandalone?: boolean;
}

35
interface IValidationResult {
36
	error?: ConfigurationEditingErrorCode;
37 38 39 40 41 42 43 44 45 46 47 48
	exists?: boolean;
	contents?: string;
}

export class ConfigurationEditingService implements IConfigurationEditingService {

	public _serviceBrand: any;

	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IEnvironmentService private environmentService: IEnvironmentService,
49
		@IFileService private fileService: IFileService,
50
		@ITextFileService private textFileService: ITextFileService
51 52 53
	) {
	}

54 55
	public writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue): TPromise<void> {
		const operation = this.getConfigurationEditOperation(target, value);
56 57

		// First validate before making any edits
58
		return this.validate(target, operation).then(validation => {
59
			if (typeof validation.error === 'number') {
60
				return this.wrapError(validation.error, target);
61 62 63
			}

			// Create configuration file if missing
64
			const resource = operation.target;
65 66 67 68
			let ensureConfigurationFile = TPromise.as(null);
			let contents: string;
			if (!validation.exists) {
				contents = '{}';
69
				ensureConfigurationFile = this.fileService.updateContent(resource, contents, { encoding: encoding.UTF8 });
70 71 72 73 74 75 76
			} else {
				contents = validation.contents;
			}

			return ensureConfigurationFile.then(() => {

				// Apply all edits to the configuration file
77
				const result = this.applyEdits(contents, operation);
78

79
				return this.fileService.updateContent(resource, result, { encoding: encoding.UTF8 }).then(() => {
80 81

					// Reload the configuration so that we make sure all parties are updated
82
					return this.configurationService.reloadConfiguration().then(() => void 0);
83
				});
84 85 86 87
			});
		});
	}

88 89
	private wrapError(code: ConfigurationEditingErrorCode, target: ConfigurationTarget): TPromise<any> {
		const message = this.toErrorMessage(code, target);
90 91 92 93 94 95 96 97

		return TPromise.wrapError<IConfigurationEditingError>({
			code,
			message,
			toString: () => message
		});
	}

98
	private toErrorMessage(error: ConfigurationEditingErrorCode, target: ConfigurationTarget): string {
99
		switch (error) {
100 101

			// API constraints
102
			case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to the configuration file (Unknown Key)");
103
			case ConfigurationEditingErrorCode.ERROR_INVALID_TARGET: return nls.localize('errorInvalidTarget', "Unable to write to the configuration file (Invalid Target)");
104 105

			// User issues
106
			case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorNoWorkspaceOpened', "Unable to write settings because no folder is opened. Please open a folder first and try again.");
107 108 109 110 111 112 113 114 115 116 117 118 119 120
			case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: {
				if (target === ConfigurationTarget.USER) {
					return nls.localize('errorInvalidConfiguration', "Unable to write settings. Please open **User Settings** to correct errors/warnings in the file and try again.");
				}

				return nls.localize('errorInvalidConfigurationWorkspace', "Unable to write settings. Please open **Workspace Settings** to correct errors/warnings in the file and try again.");
			};
			case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: {
				if (target === ConfigurationTarget.USER) {
					return nls.localize('errorConfigurationFileDirty', "Unable to write settings because the file is dirty. Please save the **User Settings** file and try again.");
				}

				return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write settings because the file is dirty. Please save the **Workspace Settings** file and try again.");
			};
121 122 123
		}
	}

124
	private applyEdits(content: string, edit: IConfigurationEditOperation): string {
125 126 127
		const {tabSize, insertSpaces} = this.configurationService.getConfiguration<{ tabSize: number; insertSpaces: boolean }>('editor');
		const {eol} = this.configurationService.getConfiguration<{ eol: string }>('files');

128
		const {key, value} = edit;
129

130 131 132
		// Without key, the entire settings file is being replaced, so we just use JSON.stringify
		if (!key) {
			return JSON.stringify(value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t');
133 134
		}

135 136 137
		const edits = setProperty(content, [key], value, { tabSize, insertSpaces, eol });
		content = applyEdits(content, edits);

138 139 140
		return content;
	}

141 142 143 144 145 146 147 148 149
	private validate(target: ConfigurationTarget, operation: IConfigurationEditOperation): TPromise<IValidationResult> {

		// Any key must be a known setting from the registry (unless this is a standalone config)
		if (!operation.isWorkspaceStandalone) {
			const validKeys = getConfigurationKeys();
			if (validKeys.indexOf(operation.key) < 0) {
				return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY });
			}
		}
150

151 152 153
		// Target cannot be user if is standalone
		if (operation.isWorkspaceStandalone && target === ConfigurationTarget.USER) {
			return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_INVALID_TARGET });
154 155
		}

156
		// Target cannot be workspace if no workspace opened
157
		if (target === ConfigurationTarget.WORKSPACE && !this.contextService.getWorkspace()) {
158
			return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED });
159 160
		}

161 162
		// Target cannot be dirty
		const resource = operation.target;
163 164 165
		if (this.textFileService.isDirty(resource)) {
			return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY });
		}
166

167 168 169 170
		return this.fileService.existsFile(resource).then(exists => {
			if (!exists) {
				return { exists };
			}
171

172
			return this.fileService.resolveContent(resource, { acceptTextOnly: true, encoding: encoding.UTF8 }).then(content => {
173

174 175 176 177 178
				// If we write to a workspace standalone file and replace the entire contents (no key provided)
				// we can return here because any parse errors can safely be ignored since all contents are replaced
				if (operation.isWorkspaceStandalone && !operation.key) {
					return { exists, contents: content.value };
				}
179

180 181 182 183 184 185
				// Target cannot contain JSON errors
				const parseErrors = [];
				json.parse(content.value, parseErrors);
				if (parseErrors.length > 0) {
					return { error: ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION };
				}
186

187
				return { exists, contents: content.value };
188 189 190 191
			});
		});
	}

192 193 194 195 196 197 198 199 200
	private getConfigurationEditOperation(target: ConfigurationTarget, config: IConfigurationValue): IConfigurationEditOperation {

		// Check for standalone workspace configurations
		if (config.key) {
			const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS);
			for (let i = 0; i < standaloneConfigurationKeys.length; i++) {
				const key = standaloneConfigurationKeys[i];
				const target = this.contextService.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[key]);

201 202 203 204 205 206 207
				// Check for prefix
				if (config.key === key) {
					return { key: '', value: config.value, target, isWorkspaceStandalone: true };
				}

				// Check for prefix.<setting>
				const keyPrefix = `${key}.`;
208 209 210 211 212 213
				if (config.key.indexOf(keyPrefix) === 0) {
					return { key: config.key.substr(keyPrefix.length), value: config.value, target, isWorkspaceStandalone: true };
				}
			}
		}

214
		if (target === ConfigurationTarget.USER) {
215
			return { key: config.key, value: config.value, target: URI.file(this.environmentService.appSettingsPath) };
216 217
		}

218
		return { key: config.key, value: config.value, target: this.contextService.toResource(WORKSPACE_CONFIG_DEFAULT_PATH) };
219 220
	}
}