Skip to content

Commit 5246984

Browse files
committed
restructure command variable resolving
1 parent c29f432 commit 5246984

7 files changed

Lines changed: 84 additions & 61 deletions

File tree

src/vs/workbench/api/node/extHostDebugService.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -584,11 +584,11 @@ export class ExtHostVariableResolverService implements IConfigurationResolverSer
584584
return this._variableResolver.resolveAny(root ? root.uri : undefined, value);
585585
}
586586

587-
public resolveAny(root: IWorkspaceFolder, value: any): any {
588-
return this._variableResolver.resolveAny(root ? root.uri : undefined, value);
587+
public resolveAny<T>(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary<string>): T {
588+
return this._variableResolver.resolveAny(root ? root.uri : undefined, value, commandMapping);
589589
}
590590

591-
resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string; }): TPromise<any, any> {
592-
throw new Error('Method not implemented.');
591+
public executeCommandVariables(configuration: any, variables: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
592+
throw new Error('findAndExecuteCommandVariables not implemented.');
593593
}
594594
}

src/vs/workbench/parts/debug/node/debugAdapter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { IOutputService } from 'vs/workbench/parts/output/common/output';
2020
import { IDebugAdapter, IAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IConfig } from 'vs/workbench/parts/debug/common/debug';
2121
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
2222
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
23+
import { IStringDictionary } from 'vs/base/common/collections';
2324

2425
/**
2526
* Abstract implementation of the low level API for a debug adapter.
@@ -409,7 +410,7 @@ export class DebugAdapter extends StreamDebugAdapter {
409410
}
410411
}
411412

412-
static substituteVariables(workspaceFolder: IWorkspaceFolder, config: IConfig, resolverService: IConfigurationResolverService): IConfig {
413+
static substituteVariables(workspaceFolder: IWorkspaceFolder, config: IConfig, resolverService: IConfigurationResolverService, commandValueMapping?: IStringDictionary<string>): IConfig {
413414

414415
const result = objects.deepClone(config) as IConfig;
415416

@@ -428,7 +429,7 @@ export class DebugAdapter extends StreamDebugAdapter {
428429
delete result.linux;
429430

430431
// substitute all variables in string values
431-
return resolverService.resolveAny(workspaceFolder, result);
432+
return resolverService.resolveAny(workspaceFolder, result, commandValueMapping);
432433
}
433434
}
434435

src/vs/workbench/parts/debug/node/debugger.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,24 @@ export class Debugger {
6363

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

66-
let configP: TPromise<IConfig>;
67-
const debugConfigs = this.configurationService.getValue<IDebugConfiguration>('debug');
68-
if (debugConfigs.extensionHostDebugAdapter) {
69-
configP = this.configurationManager.substituteVariables(this.type, folder, config);
70-
} else {
71-
try {
72-
configP = TPromise.as(DebugAdapter.substituteVariables(folder, config, this.configurationResolverService));
73-
} catch (e) {
74-
return TPromise.wrapError(e);
66+
// first resolve command variables (which might have a UI)
67+
return this.configurationResolverService.executeCommandVariables(config, this.variables).then(commandValueMapping => {
68+
69+
if (!commandValueMapping) { // cancelled by user
70+
return null;
7571
}
76-
}
7772

78-
return configP.then(result => {
79-
// substitute 'command' variables (including interactive)
80-
return this.configurationResolverService.resolveInteractiveVariables(result, this.variables);
73+
// optionally substitute in EH
74+
const inEh = this.configurationService.getValue<IDebugConfiguration>('debug').extensionHostDebugAdapter;
75+
76+
// now substitute all other variables
77+
return (inEh ? this.configurationManager.substituteVariables(this.type, folder, config) : TPromise.as(config)).then(config => {
78+
try {
79+
return TPromise.as(DebugAdapter.substituteVariables(folder, config, this.configurationResolverService, commandValueMapping));
80+
} catch (e) {
81+
return TPromise.wrapError(e);
82+
}
83+
});
8184
});
8285
}
8386

src/vs/workbench/services/configurationResolver/common/configurationResolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ export interface IConfigurationResolverService {
1616
resolve(root: IWorkspaceFolder, value: string): string;
1717
resolve(root: IWorkspaceFolder, value: string[]): string[];
1818
resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
19-
resolveAny<T>(root: IWorkspaceFolder, value: T): T;
20-
resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string }): TPromise<any>;
19+
resolveAny<T>(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary<string>): T;
20+
executeCommandVariables(value: any, variables: IStringDictionary<string>): TPromise<IStringDictionary<string>>;
2121
}

src/vs/workbench/services/configurationResolver/electron-browser/configurationResolverService.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -94,63 +94,67 @@ export class ConfigurationResolverService implements IConfigurationResolverServi
9494
return this.resolver.resolveAny(root ? root.uri : undefined, value);
9595
}
9696

97-
public resolveAny(root: IWorkspaceFolder, value: any): any {
98-
return this.resolver.resolveAny(root ? root.uri : undefined, value);
97+
public resolveAny(root: IWorkspaceFolder, value: any, commandValueMapping?: IStringDictionary<string>): any {
98+
return this.resolver.resolveAny(root ? root.uri : undefined, value, commandValueMapping);
9999
}
100100

101101
/**
102-
* Resolve all interactive variables in configuration #6569
102+
* Finds and executes all command variables (see #6569)
103103
*/
104-
public resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string }): TPromise<any> {
104+
public executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
105+
105106
if (!configuration) {
106107
return TPromise.as(null);
107108
}
108109

109-
// We need a map from interactive variables to keys because we only want to trigger an command once per key -
110-
// even though it might occur multiple times in configuration #7026.
111-
const interactiveVariablesToSubstitutes: { [interactiveVariable: string]: { object: any, key: string }[] } = Object.create(null);
112-
const findInteractiveVariables = (object: any) => {
110+
// use an array to preserve order of first appearance
111+
const commands: string[] = [];
112+
113+
const cmd_var = /\${command:(.*?)}/g;
114+
115+
const findCommandVariables = (object: any) => {
113116
Object.keys(object).forEach(key => {
114-
if (object[key] && typeof object[key] === 'object') {
115-
findInteractiveVariables(object[key]);
116-
} else if (typeof object[key] === 'string') {
117-
const matches = /\${command:(.*?)}/.exec(object[key]);
118-
if (matches && matches.length === 2) {
119-
const interactiveVariable = matches[1];
120-
if (!interactiveVariablesToSubstitutes[interactiveVariable]) {
121-
interactiveVariablesToSubstitutes[interactiveVariable] = [];
117+
const value = object[key];
118+
if (value && typeof value === 'object') {
119+
findCommandVariables(value);
120+
} else if (typeof value === 'string') {
121+
let matches;
122+
while ((matches = cmd_var.exec(value)) !== null) {
123+
if (matches.length === 2) {
124+
const command = matches[1];
125+
if (commands.indexOf(command) < 0) {
126+
commands.push(command);
127+
}
122128
}
123-
interactiveVariablesToSubstitutes[interactiveVariable].push({ object, key });
124129
}
125130
}
126131
});
127132
};
128-
findInteractiveVariables(configuration);
129-
let substitionCanceled = false;
130133

131-
const factory: { (): TPromise<any> }[] = Object.keys(interactiveVariablesToSubstitutes).map(interactiveVariable => {
134+
findCommandVariables(configuration);
135+
136+
let cancelled = false;
137+
const commandValueMapping: IStringDictionary<string> = Object.create(null);
138+
139+
const factory: { (): TPromise<any> }[] = commands.map(interactiveVariable => {
132140
return () => {
133-
let commandId: string = null;
134-
commandId = interactiveVariablesMap ? interactiveVariablesMap[interactiveVariable] : null;
141+
142+
let commandId = variableToCommandMap ? variableToCommandMap[interactiveVariable] : null;
135143
if (!commandId) {
136144
// Just launch any command if the interactive variable is not contributed by the adapter #12735
137145
commandId = interactiveVariable;
138146
}
139147

140148
return this.commandService.executeCommand<string>(commandId, configuration).then(result => {
141149
if (result) {
142-
interactiveVariablesToSubstitutes[interactiveVariable].forEach(substitute => {
143-
if (substitute.object[substitute.key].indexOf(`\${command:${interactiveVariable}}`) >= 0) {
144-
substitute.object[substitute.key] = substitute.object[substitute.key].replace(`\${command:${interactiveVariable}}`, result);
145-
}
146-
});
150+
commandValueMapping[interactiveVariable] = result;
147151
} else {
148-
substitionCanceled = true;
152+
cancelled = true;
149153
}
150154
});
151155
};
152156
});
153157

154-
return sequence(factory).then(() => substitionCanceled ? null : configuration);
158+
return sequence(factory).then(() => cancelled ? null : commandValueMapping);
155159
}
156160
}

src/vs/workbench/services/configurationResolver/node/variableResolver.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { normalizeDriveLetter } from 'vs/base/common/labels';
1212
import { localize } from 'vs/nls';
1313
import uri from 'vs/base/common/uri';
1414

15-
1615
export interface IVariableAccessor {
1716
getFolderUri(folderName: string): uri | undefined;
1817
getWorkspaceFolderCount(): number;
@@ -43,22 +42,22 @@ export class VariableResolver {
4342
}
4443
}
4544

46-
resolveAny(folderUri: uri, value: any): any {
45+
resolveAny(folderUri: uri, value: any, commandValueMapping?: IStringDictionary<string>): any {
4746
if (types.isString(value)) {
48-
return this.resolve(folderUri, value);
47+
return this.resolve(folderUri, value, commandValueMapping);
4948
} else if (types.isArray(value)) {
50-
return value.map(s => this.resolveAny(folderUri, s));
49+
return value.map(s => this.resolveAny(folderUri, s, commandValueMapping));
5150
} else if (types.isObject(value)) {
5251
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
5352
Object.keys(value).forEach(key => {
54-
result[key] = this.resolveAny(folderUri, value[key]);
53+
result[key] = this.resolveAny(folderUri, value[key], commandValueMapping);
5554
});
5655
return result;
5756
}
5857
return value;
5958
}
6059

61-
resolve(folderUri: uri, value: string): string {
60+
resolve(folderUri: uri, value: string, commandValueMapping: IStringDictionary<string>): string {
6261

6362
const filePath = this.accessor.getFilePath();
6463

@@ -100,6 +99,16 @@ export class VariableResolver {
10099
}
101100
throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match));
102101

102+
case 'command':
103+
if (argument && commandValueMapping) {
104+
const v = commandValueMapping[argument];
105+
if (typeof v === 'string') {
106+
return v;
107+
}
108+
throw new Error(localize('noValueForCommand', "'{0}' can not be resolved because the command has no value.", match));
109+
}
110+
return match;
111+
103112
default: {
104113

105114
// common error handling for all variables that require an open folder and accept a folder name argument
@@ -197,7 +206,7 @@ export class VariableResolver {
197206
if (ep) {
198207
return ep;
199208
}
200-
throw new Error(localize('canNotResolveExecPath', "'{0}' can not be resolved.", match));
209+
return match;
201210

202211
default:
203212
return match;

src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,11 @@ suite('Configuration Resolver Service', () => {
240240
interactiveVariables['interactiveVariable1'] = 'command1';
241241
interactiveVariables['interactiveVariable2'] = 'command2';
242242

243-
configurationResolverService.resolveInteractiveVariables(configuration, interactiveVariables).then(resolved => {
244-
assert.deepEqual(resolved, {
243+
configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => {
244+
245+
const result = configurationResolverService.resolveAny(undefined, configuration, mapping);
246+
247+
assert.deepEqual(result, {
245248
'name': 'Attach to Process',
246249
'type': 'node',
247250
'request': 'attach',
@@ -272,8 +275,11 @@ suite('Configuration Resolver Service', () => {
272275
interactiveVariables['interactiveVariable1'] = 'command1';
273276
interactiveVariables['interactiveVariable2'] = 'command2';
274277

275-
configurationResolverService.resolveInteractiveVariables(configuration, interactiveVariables).then(resolved => {
276-
assert.deepEqual(resolved, {
278+
configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => {
279+
280+
const result = configurationResolverService.resolveAny(undefined, configuration, mapping);
281+
282+
assert.deepEqual(result, {
277283
'name': 'Attach to Process',
278284
'type': 'node',
279285
'request': 'attach',

0 commit comments

Comments
 (0)