diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index ccd4bb4ad99..37486e3a92b 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -653,4 +653,8 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer public executeCommandVariables(configuration: any, variables: IStringDictionary): TPromise> { throw new Error('findAndExecuteCommandVariables not implemented.'); } + + public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise { + throw new Error('resolveWithCommands not implemented.'); + } } diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index c2edd66679e..d4c9d95f46e 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -74,21 +74,10 @@ export class Debugger { public substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { - // first resolve command variables (which might have a UI) - return this.configurationResolverService.executeCommandVariables(config, this.variables).then(commandValueMapping => { + // first try to substitute variables in EH + return (this.inEH() ? this.configurationManager.substituteVariables(this.type, folder, config) : TPromise.as(config)).then(config => { - if (!commandValueMapping) { // cancelled by user - return null; - } - - // now substitute all other variables - return (this.inEH() ? this.configurationManager.substituteVariables(this.type, folder, config) : TPromise.as(config)).then(config => { - try { - return TPromise.as(DebugAdapter.substituteVariables(folder, config, this.configurationResolverService, commandValueMapping)); - } catch (e) { - return TPromise.wrapError(e); - } - }); + return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); }); } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index ec8a9eab100..4b7eaf8f931 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -17,5 +17,5 @@ export interface IConfigurationResolverService { resolve(root: IWorkspaceFolder, value: string[]): string[]; resolve(root: IWorkspaceFolder, value: IStringDictionary): IStringDictionary; resolveAny(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary): T; - executeCommandVariables(value: any, variables: IStringDictionary): TPromise>; + resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise; } diff --git a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts index a0ca4baff20..1948ae812de 100644 --- a/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts @@ -4,31 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import uri from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; import * as paths from 'vs/base/common/paths'; +import * as platform from 'vs/base/common/platform'; +import * as objects from 'vs/base/common/objects'; import { Schemas } from 'vs/base/common/network'; import { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; import { toResource } from 'vs/workbench/common/editor'; -import { IStringDictionary } from 'vs/base/common/collections'; +import { IStringDictionary, size } from 'vs/base/common/collections'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IProcessEnvironment } from 'vs/base/common/platform'; import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { localize } from 'vs/nls'; export class ConfigurationResolverService implements IConfigurationResolverService { + _serviceBrand: any; private resolver: VariableResolver; constructor( - envVariables: IProcessEnvironment, + envVariables: platform.IProcessEnvironment, @IEditorService editorService: IEditorService, @IEnvironmentService environmentService: IEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @@ -93,10 +95,53 @@ export class ConfigurationResolverService implements IConfigurationResolverServi return this.resolver.resolveAny(root ? root.uri : undefined, value, commandValueMapping); } + public resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary): TPromise { + + // then substitute remaining variables in VS Code core + config = this.substituteVariables(folder, config); + + // now evaluate command variables (which might have a UI) + return this.executeCommandVariables(config, variables).then(commandValueMapping => { + + if (!commandValueMapping) { // cancelled by user + return null; + } + + // finally substitute evaluated command variables (if there are any) + if (size(commandValueMapping) > 0) { + return this.substituteVariables(folder, config, commandValueMapping); + } else { + return config; + } + }); + } + + private substituteVariables(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary): any { + + const result = objects.deepClone(config) as any; + + // hoist platform specific attributes to top level + if (platform.isWindows && result.windows) { + Object.keys(result.windows).forEach(key => result[key] = result.windows[key]); + } else if (platform.isMacintosh && result.osx) { + Object.keys(result.osx).forEach(key => result[key] = result.osx[key]); + } else if (platform.isLinux && result.linux) { + Object.keys(result.linux).forEach(key => result[key] = result.linux[key]); + } + + // delete all platform specific sections + delete result.windows; + delete result.osx; + delete result.linux; + + // substitute all variables in string values + return this.resolveAny(workspaceFolder, result, commandValueMapping); + } + /** * Finds and executes all command variables (see #6569) */ - public executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { + private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary): TPromise> { if (!configuration) { return TPromise.as(null); @@ -131,22 +176,22 @@ export class ConfigurationResolverService implements IConfigurationResolverServi let cancelled = false; const commandValueMapping: IStringDictionary = Object.create(null); - const factory: { (): TPromise }[] = commands.map(interactiveVariable => { + const factory: { (): TPromise }[] = commands.map(commandVariable => { return () => { - let commandId = variableToCommandMap ? variableToCommandMap[interactiveVariable] : null; + let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : null; if (!commandId) { // Just launch any command if the interactive variable is not contributed by the adapter #12735 - commandId = interactiveVariable; + commandId = commandVariable; } return this.commandService.executeCommand(commandId, configuration).then(result => { if (typeof result === 'string') { - commandValueMapping[interactiveVariable] = result; + commandValueMapping[commandVariable] = result; } else if (isUndefinedOrNull(result)) { cancelled = true; } else { - throw new Error(localize('stringsOnlySupported', "Command {0} did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandId)); + throw new Error(nls.localize('stringsOnlySupported', "Command '{0}' did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandVariable)); } }); }; diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 57ba15ae14c..2c0ede219c9 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -239,29 +239,25 @@ suite('Configuration Resolver Service', () => { assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz')); }); - test('interactive variable simple', () => { + test('a single command variable', () => { + const configuration = { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': '${command:interactiveVariable1}', + 'processId': '${command:command1}', 'port': 5858, 'sourceMaps': false, 'outDir': null }; - const interactiveVariables = Object.create(null); - interactiveVariables['interactiveVariable1'] = 'command1'; - interactiveVariables['interactiveVariable2'] = 'command2'; - configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => { - - const result = configurationResolverService.resolveAny(undefined, configuration, mapping); + return configurationResolverService.resolveWithCommands(undefined, configuration).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': 'command1', + 'processId': 'command1-result', 'port': 5858, 'sourceMaps': false, 'outDir': null @@ -271,43 +267,96 @@ suite('Configuration Resolver Service', () => { }); }); - test('interactive variable complex', () => { + test('an old style command variable', () => { const configuration = { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': '${command:interactiveVariable1}', - 'port': '${command:interactiveVariable2}', + 'processId': '${command:commandVariable1}', + 'port': 5858, 'sourceMaps': false, - 'outDir': 'src/${command:interactiveVariable2}', - 'env': { - 'processId': '__${command:interactiveVariable2}__', - } + 'outDir': null }; - const interactiveVariables = Object.create(null); - interactiveVariables['interactiveVariable1'] = 'command1'; - interactiveVariables['interactiveVariable2'] = 'command2'; + const commandVariables = Object.create(null); + commandVariables['commandVariable1'] = 'command1'; - configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => { - - const result = configurationResolverService.resolveAny(undefined, configuration, mapping); + return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { assert.deepEqual(result, { 'name': 'Attach to Process', 'type': 'node', 'request': 'attach', - 'processId': 'command1', - 'port': 'command2', + 'processId': 'command1-result', + 'port': 5858, 'sourceMaps': false, - 'outDir': 'src/command2', + 'outDir': null + }); + + assert.equal(1, mockCommandService.callCount); + }); + }); + + test('multiple new and old-style command variables', () => { + + const configuration = { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': '${command:commandVariable1}', + 'pid': '${command:command2}', + 'sourceMaps': false, + 'outDir': 'src/${command:command2}', + 'env': { + 'processId': '__${command:command2}__', + } + }; + const commandVariables = Object.create(null); + commandVariables['commandVariable1'] = 'command1'; + + return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + + assert.deepEqual(result, { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': 'command1-result', + 'pid': 'command2-result', + 'sourceMaps': false, + 'outDir': 'src/command2-result', 'env': { - 'processId': '__command2__', + 'processId': '__command2-result__', } }); assert.equal(2, mockCommandService.callCount); }); }); + + test('a command variable that relies on resolved env vars', () => { + + const configuration = { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': '${command:commandVariable1}', + 'value': '${env:key1}' + }; + const commandVariables = Object.create(null); + commandVariables['commandVariable1'] = 'command1'; + + return configurationResolverService.resolveWithCommands(undefined, configuration, commandVariables).then(result => { + + assert.deepEqual(result, { + 'name': 'Attach to Process', + 'type': 'node', + 'request': 'attach', + 'processId': 'Value for key1', + 'value': 'Value for key1' + }); + + assert.equal(1, mockCommandService.callCount); + }); + }); }); @@ -345,6 +394,14 @@ class MockCommandService implements ICommandService { onWillExecuteCommand = () => ({ dispose: () => { } }); public executeCommand(commandId: string, ...args: any[]): TPromise { this.callCount++; - return TPromise.as(commandId); + + let result = `${commandId}-result`; + if (args.length >= 1) { + if (args[0] && args[0].value) { + result = args[0].value; + } + } + + return TPromise.as(result); } }