Skip to content

Commit

Permalink
reverse variable substitution passes; fixes microsoft#51075
Browse files Browse the repository at this point in the history
  • Loading branch information
weinand committed Jun 12, 2018
1 parent 94a4919 commit db9865b
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 49 deletions.
4 changes: 4 additions & 0 deletions src/vs/workbench/api/node/extHostDebugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,4 +653,8 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer
public executeCommandVariables(configuration: any, variables: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
throw new Error('findAndExecuteCommandVariables not implemented.');
}

public resolveWithCommands(folder: IWorkspaceFolder, config: any): TPromise<any> {
throw new Error('resolveWithCommands not implemented.');
}
}
17 changes: 3 additions & 14 deletions src/vs/workbench/parts/debug/node/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,10 @@ export class Debugger {

public substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise<IConfig> {

// 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);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ export interface IConfigurationResolverService {
resolve(root: IWorkspaceFolder, value: string[]): string[];
resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
resolveAny<T>(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary<string>): T;
executeCommandVariables(value: any, variables: IStringDictionary<string>): TPromise<IStringDictionary<string>>;
resolveWithCommands(folder: IWorkspaceFolder, config: any, variables?: IStringDictionary<string>): TPromise<any>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string>): TPromise<any> {

// 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<string>(commandValueMapping) > 0) {
return this.substituteVariables(folder, config, commandValueMapping);
} else {
return config;
}
});
}

private substituteVariables(workspaceFolder: IWorkspaceFolder, config: any, commandValueMapping?: IStringDictionary<string>): 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<string>): TPromise<IStringDictionary<string>> {
private executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {

if (!configuration) {
return TPromise.as(null);
Expand Down Expand Up @@ -131,22 +176,22 @@ export class ConfigurationResolverService implements IConfigurationResolverServi
let cancelled = false;
const commandValueMapping: IStringDictionary<string> = Object.create(null);

const factory: { (): TPromise<any> }[] = commands.map(interactiveVariable => {
const factory: { (): TPromise<any> }[] = 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<string>(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));
}
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,29 +239,54 @@ 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 => {
return configurationResolverService.resolveWithCommands(undefined, configuration).then(result => {

assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1-result',
'port': 5858,
'sourceMaps': false,
'outDir': null
});

assert.equal(1, mockCommandService.callCount);
});
});

test('an old style command variable', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:commandVariable1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
const commandVariables = Object.create(null);
commandVariables['commandVariable1'] = 'command1';

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',
'processId': 'command1-result',
'port': 5858,
'sourceMaps': false,
'outDir': null
Expand All @@ -271,43 +296,67 @@ suite('Configuration Resolver Service', () => {
});
});

test('interactive variable complex', () => {
test('multiple new and old-style command variables', () => {

const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:interactiveVariable1}',
'port': '${command:interactiveVariable2}',
'processId': '${command:commandVariable1}',
'pid': '${command:command2}',
'sourceMaps': false,
'outDir': 'src/${command:interactiveVariable2}',
'outDir': 'src/${command:command2}',
'env': {
'processId': '__${command:interactiveVariable2}__',
'processId': '__${command:command2}__',
}
};
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',
'pid': 'command2-result',
'sourceMaps': false,
'outDir': 'src/command2',
'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);
});
});
});


Expand Down Expand Up @@ -345,6 +394,14 @@ class MockCommandService implements ICommandService {
onWillExecuteCommand = () => ({ dispose: () => { } });
public executeCommand(commandId: string, ...args: any[]): TPromise<any> {
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);
}
}

0 comments on commit db9865b

Please sign in to comment.