diff --git a/src/vs/workbench/api/node/mainThreadConfiguration.ts b/src/vs/workbench/api/node/mainThreadConfiguration.ts index 03089d095a9..f741e86707d 100644 --- a/src/vs/workbench/api/node/mainThreadConfiguration.ts +++ b/src/vs/workbench/api/node/mainThreadConfiguration.ts @@ -33,6 +33,6 @@ export class MainThreadConfiguration extends MainThreadConfigurationShape { } $updateConfigurationOption(target: ConfigurationTarget, key: string, value: any): TPromise { - return this._configurationEditingService.writeConfiguration(target, [{ key, value }]); + return this._configurationEditingService.writeConfiguration(target, { key, value }); } } diff --git a/src/vs/workbench/services/configuration/common/configurationEditing.ts b/src/vs/workbench/services/configuration/common/configurationEditing.ts index d76af24b691..001cf2a7fb2 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditing.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditing.ts @@ -16,6 +16,11 @@ export enum ConfigurationEditingErrorCode { */ ERROR_UNKNOWN_KEY, + /** + * Error when trying to write to user target but not supported for provided key. + */ + ERROR_INVALID_TARGET, + /** * Error when trying to write to the workspace configuration without having a workspace opened. */ @@ -63,5 +68,5 @@ export interface IConfigurationEditingService { * Allows to write to either the user or workspace configuration file. The returned promise will be * in error state in any of the error cases from [ConfigurationEditingErrorCode](#ConfigurationEditingErrorCode) */ - writeConfiguration(target: ConfigurationTarget, values: IConfigurationValue[]): TPromise; + writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue): TPromise; } \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/node/configurationEditingService.ts b/src/vs/workbench/services/configuration/node/configurationEditingService.ts index ae5be45730f..15e73692ca3 100644 --- a/src/vs/workbench/services/configuration/node/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/node/configurationEditingService.ts @@ -21,6 +21,16 @@ import {IConfigurationService} from 'vs/platform/configuration/common/configurat import {WORKSPACE_CONFIG_DEFAULT_PATH} from 'vs/workbench/services/configuration/common/configuration'; import {IConfigurationEditingService, ConfigurationEditingErrorCode, IConfigurationEditingError, ConfigurationTarget, IConfigurationValue} from 'vs/workbench/services/configuration/common/configurationEditing'; +export const WORKSPACE_STANDALONE_CONFIGURATIONS = { + 'tasks': '.vscode/tasks.json', + 'launch': '.vscode/launch.json' +}; + +interface IConfigurationEditOperation extends IConfigurationValue { + target: URI; + isWorkspaceStandalone?: boolean; +} + interface IValidationResult { error?: ConfigurationEditingErrorCode; exists?: boolean; @@ -39,16 +49,17 @@ export class ConfigurationEditingService implements IConfigurationEditingService ) { } - public writeConfiguration(target: ConfigurationTarget, values: IConfigurationValue[]): TPromise { + public writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue): TPromise { + const operation = this.getConfigurationEditOperation(target, value); // First validate before making any edits - return this.validate(target, values).then(validation => { + return this.validate(target, operation).then(validation => { if (typeof validation.error === 'number') { return this.wrapError(validation.error); } // Create configuration file if missing - const resource = this.getConfigurationResource(target); + const resource = operation.target; let ensureConfigurationFile = TPromise.as(null); let contents: string; if (!validation.exists) { @@ -61,7 +72,7 @@ export class ConfigurationEditingService implements IConfigurationEditingService return ensureConfigurationFile.then(() => { // Apply all edits to the configuration file - const result = this.applyEdits(contents, values); + const result = this.applyEdits(contents, [operation]); return pfs.writeFile(resource.fsPath, result, encoding.UTF8).then(() => { @@ -85,6 +96,7 @@ export class ConfigurationEditingService implements IConfigurationEditingService private toErrorMessage(error: ConfigurationEditingErrorCode): string { switch (error) { case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to the configuration file (Unknown Key)"); + case ConfigurationEditingErrorCode.ERROR_INVALID_TARGET: return nls.localize('errorInvalidTarget', "Unable to write to the configuration file (Invalid Target)"); case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorWorkspaceOpened', "Unable to write to the configuration file (No Workspace Opened)"); case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: return nls.localize('errorInvalidConfiguration', "Unable to write to the configuration file (Invalid Configuration Found)"); case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: return nls.localize('errorConfigurationFileDirty', "Unable to write to the configuration file (Configuration File Dirty)"); @@ -105,27 +117,34 @@ export class ConfigurationEditingService implements IConfigurationEditingService return content; } - private validate(target: ConfigurationTarget, values: IConfigurationValue[]): TPromise { + private validate(target: ConfigurationTarget, operation: IConfigurationEditOperation): TPromise { - // 1.) Any key must be a known setting from the registry - const validKeys = getConfigurationKeys(); - if (values.some(v => validKeys.indexOf(v.key) < 0)) { - return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY }); + // 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 }); + } } - // 2.) Target cannot be workspace if no workspace opened + // Target cannot be user if is standalone + if (operation.isWorkspaceStandalone && target === ConfigurationTarget.USER) { + return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_INVALID_TARGET }); + } + + // Target cannot be workspace if no workspace opened if (target === ConfigurationTarget.WORKSPACE && !this.contextService.getWorkspace()) { return TPromise.as({ error: ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED }); } - // 3.) Target cannot be dirty - const resource = this.getConfigurationResource(target); + // Target cannot be dirty + const resource = operation.target; return this.editorService.createInput({ resource }).then(typedInput => { if (typedInput.isDirty()) { return { error: ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY }; } - // 4.) Target cannot contain JSON errors + // Target cannot contain JSON errors return pfs.exists(resource.fsPath).then(exists => { if (!exists) { return { exists }; @@ -146,11 +165,26 @@ export class ConfigurationEditingService implements IConfigurationEditingService }); } - private getConfigurationResource(target: ConfigurationTarget): URI { - if (target === ConfigurationTarget.USER) { - return URI.file(this.environmentService.appSettingsPath); + 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 keyPrefix = `${key}.`; + const target = this.contextService.toResource(WORKSPACE_STANDALONE_CONFIGURATIONS[key]); + + if (config.key.indexOf(keyPrefix) === 0) { + return { key: config.key.substr(keyPrefix.length), value: config.value, target, isWorkspaceStandalone: true }; + } + } } - return this.contextService.toResource(WORKSPACE_CONFIG_DEFAULT_PATH); + if (target === ConfigurationTarget.USER) { + return { key: config.key, value: config.value, target: URI.file(this.environmentService.appSettingsPath) }; + } + + return { key: config.key, value: config.value, target: this.contextService.toResource(WORKSPACE_CONFIG_DEFAULT_PATH) }; } } \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts index 4891bda547a..99f25de5b4e 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts @@ -21,7 +21,7 @@ import uuid = require('vs/base/common/uuid'); import {IConfigurationRegistry, Extensions as ConfigurationExtensions} from 'vs/platform/configuration/common/configurationRegistry'; import {WorkspaceConfigurationService} from 'vs/workbench/services/configuration/node/configurationService'; import URI from 'vs/base/common/uri'; -import {ConfigurationEditingService} from 'vs/workbench/services/configuration/node/configurationEditingService'; +import {ConfigurationEditingService, WORKSPACE_STANDALONE_CONFIGURATIONS} from 'vs/workbench/services/configuration/node/configurationEditingService'; import {ConfigurationTarget, IConfigurationEditingError, ConfigurationEditingErrorCode} from 'vs/workbench/services/configuration/common/configurationEditing'; import {IResourceInput} from 'vs/platform/editor/common/editor'; @@ -111,7 +111,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { test('errors cases - invalid key', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { return createServices(workspaceDir, globalSettingsFile, false, true /* no workspace */).then(services => { - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, [{ key: 'unknown.key', value: 'value' }]).then(res => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' }).then(res => { }, (error:IConfigurationEditingError) => { assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY); services.configurationService.dispose(); @@ -121,10 +121,23 @@ suite('WorkspaceConfigurationEditingService - Node', () => { }); }); + test('errors cases - invalid target', (done: () => void) => { + createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { + return createServices(workspaceDir, globalSettingsFile).then(services => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: 'tasks.something', value: 'value' }).then(res => { + }, (error:IConfigurationEditingError) => { + assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_TARGET); + services.configurationService.dispose(); + cleanUp(done); + }); + }); + }); + }); + test('errors cases - no workspace', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { return createServices(workspaceDir, globalSettingsFile, false, true /* no workspace */).then(services => { - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, [{ key: 'configurationEditing.service.testSetting', value: 'value' }]).then(res => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'configurationEditing.service.testSetting', value: 'value' }).then(res => { }, (error: IConfigurationEditingError) => { assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED); services.configurationService.dispose(); @@ -139,7 +152,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { return createServices(workspaceDir, globalSettingsFile).then(services => { fs.writeFileSync(globalSettingsFile, ',,,,,,,,,,,,,,'); - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, [{ key: 'configurationEditing.service.testSetting', value: 'value' }]).then(res => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }).then(res => { }, (error: IConfigurationEditingError) => { assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION); services.configurationService.dispose(); @@ -152,7 +165,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { test('errors cases - dirty', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { return createServices(workspaceDir, globalSettingsFile, true).then(services => { - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, [{ key: 'configurationEditing.service.testSetting', value: 'value' }]).then(res => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }).then(res => { }, (error: IConfigurationEditingError) => { assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY); services.configurationService.dispose(); @@ -165,7 +178,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { test('write one setting - empty file', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { return createServices(workspaceDir, globalSettingsFile).then(services => { - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, [{ key: 'configurationEditing.service.testSetting', value: 'value' }]).then(res => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }).then(res => { const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); const parsed = json.parse(contents); assert.equal(parsed['configurationEditing.service.testSetting'], 'value'); @@ -183,7 +196,7 @@ suite('WorkspaceConfigurationEditingService - Node', () => { return createServices(workspaceDir, globalSettingsFile).then(services => { fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value" }'); - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, [{ key: 'configurationEditing.service.testSetting', value: 'value' }]).then(res => { + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }).then(res => { const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); const parsed = json.parse(contents); assert.equal(parsed['configurationEditing.service.testSetting'], 'value'); @@ -199,22 +212,15 @@ suite('WorkspaceConfigurationEditingService - Node', () => { }); }); - test('write multiple settings - empty file', (done: () => void) => { + test('write workspace standalone setting - empty file', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { return createServices(workspaceDir, globalSettingsFile).then(services => { - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, [ - { key: 'configurationEditing.service.testSetting', value: 'value' }, - { key: 'configurationEditing.service.testSettingTwo', value: { complex: { value: true } } }, - { key: 'configurationEditing.service.testSettingThree', value: 55 } - ]).then(res => { - const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks.service.testSetting', value: 'value' }).then(res => { + const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); + const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); - assert.equal(parsed['configurationEditing.service.testSetting'], 'value'); - assert.equal(parsed['configurationEditing.service.testSettingTwo'].complex.value, true); - assert.equal(parsed['configurationEditing.service.testSettingThree'], 55); - - assert.equal(services.configurationService.lookup('configurationEditing.service.testSetting').value, 'value'); - assert.equal(services.configurationService.lookup('configurationEditing.service.testSettingThree').value, 55); + assert.equal(parsed['service.testSetting'], 'value'); + assert.equal(services.configurationService.lookup('tasks.service.testSetting').value, 'value'); services.configurationService.dispose(); cleanUp(done); @@ -223,27 +229,21 @@ suite('WorkspaceConfigurationEditingService - Node', () => { }); }); - test('write multiple settings - existing file', (done: () => void) => { + test('write workspace standalone setting - existing file', (done: () => void) => { createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => { return createServices(workspaceDir, globalSettingsFile).then(services => { - fs.writeFileSync(globalSettingsFile, '// some comment from me\n{ "my.super.setting": "my.super.value" }\n\n// more comments'); + const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['launch']); - return services.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, [ - { key: 'configurationEditing.service.testSetting', value: 'value' }, - { key: 'configurationEditing.service.testSettingTwo', value: { complex: { value: true } } }, - { key: 'configurationEditing.service.testSettingThree', value: 55 } - ]).then(res => { - const contents = fs.readFileSync(globalSettingsFile).toString('utf8'); + fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }'); + + return services.configurationEditingService.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'launch.service.testSetting', value: 'value' }).then(res => { + const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); - assert.equal(parsed['configurationEditing.service.testSetting'], 'value'); - assert.equal(parsed['configurationEditing.service.testSettingTwo'].complex.value, true); - assert.equal(parsed['configurationEditing.service.testSettingThree'], 55); - + assert.equal(parsed['service.testSetting'], 'value'); assert.equal(parsed['my.super.setting'], 'my.super.value'); - assert.equal(services.configurationService.lookup('my.super.setting').value, 'my.super.value'); - assert.equal(services.configurationService.lookup('configurationEditing.service.testSetting').value, 'value'); - assert.equal(services.configurationService.lookup('configurationEditing.service.testSettingThree').value, 55); + assert.equal(services.configurationService.lookup('launch.service.testSetting').value, 'value'); + assert.equal(services.configurationService.lookup('launch.my.super.setting').value, 'my.super.value'); services.configurationService.dispose(); cleanUp(done);