Repository: microsoft/vscode (TypeScript)
Date: 2020-05-11 04:03:27+00:00
Task type: Bug Fix

Issue:
User input variables not working for launch configuration in multi-root workspace
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- Also please test using the latest insiders build to make sure your issue has not already been fixed: https://code.visualstudio.com/insiders/ -->

<!-- Use Help > Report Issue to prefill these. -->
- VSCode Version:

```
Version: 1.44.2
Commit: ff915844119ce9485abfe8aa9076ec76b5300ddd
Date: 2020-04-16T17:50:03.709Z
Electron: 7.1.11
Chrome: 78.0.3904.130
Node.js: 12.8.1
V8: 7.8.279.23-electron.0
OS: Linux x64 5.3.0-46-generic snap
```

- OS Version: ~~Ubuntu Bionic 18.04~~ Ubuntu Xenial 16.04 running on Docker instance.

Steps to Reproduce:

1. Create a multi-root workspace settings file.
2. Add a launch configuration object with the inputs and configurations.
3. Try to lunch the configuration (debugger).
4. Get an error message saying:

```
Undefined input variable 'variableNameHere' encountered. Remove or define 'variableNameHere' to continue.
```

E.g.

```json
{
  "folders": [
    {
      "path": "myLocalFolder"
    }
  ],
  "launch": {
    "version": "0.2.0",
    "inputs": [
      {
        "id": "myIndex",
        "type": "promptString",
        "default": "",
        "description": "Index of something to be processed."
      }
    ],
    "configurations": [
      {
        "name": "MyProject (Debug)",
        "type": "cppdbg",
        "request": "launch",
        "program": "${workspaceFolder:myLocalFolder}/myproject/bin/process",
        "args": [
          "-i",
          "${input:myIndex}"
        ],
        "stopAtEntry": false,
        "cwd": "${workspaceFolder:myLocalFolder}",
        "environment": [],
        "externalConsole": false,
        "preLaunchTask": "Build (MyProject)",
        "linux": {
          "MIMode": "gdb"
        },
        "osx": {
          "MIMode": "lldb"
        },
        "windows": {
          "MIMode": "gdb"
        }
      }
    ]
  }
    ...
}
```


<!-- Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Cannot test - working inside a docker container lunched by the docker extension.

p.s.: the inputs work just fine for tasks in multi-root workspace.


Issue discussion:
I am seeing the same issue. On mac error-message looks a bit different: "Variable 'VARIABLE_NAME' must be defined in an 'input' section of the debug or task configuration.".

Adding "inputs" section in launch.json solves the problem but introduces the need to check in launch.json under version control to share with the team. Having the possibility to declare "inputs" in workspace settings file would save us here. 

Solution patch:
diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts
--- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts
+++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts
@@ -699,8 +699,17 @@ abstract class AbstractLaunch {
 		if (!config || !config.configurations) {
 			return undefined;
 		}
-
-		return config.configurations.find(config => config && config.name === name);
+		const configuration = config.configurations.find(config => config && config.name === name);
+		if (configuration) {
+			if (this instanceof UserLaunch) {
+				configuration.__configurationTarget = ConfigurationTarget.USER;
+			} else if (this instanceof WorkspaceLaunch) {
+				configuration.__configurationTarget = ConfigurationTarget.WORKSPACE;
+			} else {
+				configuration.__configurationTarget = ConfigurationTarget.WORKSPACE_FOLDER;
+			}
+		}
+		return configuration;
 	}
 
 	async getInitialConfigurationContent(folderUri?: uri, type?: string, token?: CancellationToken): Promise<string> {
diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts
--- a/src/vs/workbench/contrib/debug/common/debug.ts
+++ b/src/vs/workbench/contrib/debug/common/debug.ts
@@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
 import { IDisposable } from 'vs/base/common/lifecycle';
 import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks';
 import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
-import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
+import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
 import { CancellationToken } from 'vs/base/common/cancellation';
 import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
 import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot';
@@ -538,6 +538,7 @@ export interface IConfig extends IEnvConfig {
 	linux?: IEnvConfig;
 
 	// internals
+	__configurationTarget?: ConfigurationTarget;
 	__sessionId?: string;
 	__restart?: any;
 	__autoAttach?: boolean;
diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts
--- a/src/vs/workbench/contrib/debug/common/debugger.ts
+++ b/src/vs/workbench/contrib/debug/common/debugger.ts
@@ -108,7 +108,7 @@ export class Debugger implements IDebugger {
 
 	substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {
 		return this.configurationManager.substituteVariables(this.type, folder, config).then(config => {
-			return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables);
+			return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables, config.__configurationTarget);
 		});
 	}
 
diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts
--- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts
+++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts
@@ -10,7 +10,7 @@ import { Schemas } from 'vs/base/common/network';
 import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor';
 import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections';
 import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
-import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
+import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
 import { ICommandService } from 'vs/platform/commands/common/commands';
 import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
 import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -146,8 +146,9 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR
 
 		// get all "inputs"
 		let inputs: ConfiguredInput[] = [];
-		if (folder && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) {
-			let result = this.configurationService.inspect(section, { resource: folder.uri });
+		if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) {
+			const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {};
+			let result = this.configurationService.inspect(section, overrides);
 			if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) {
 				switch (target) {
 					case ConfigurationTarget.USER: inputs = (<any>result.userValue)?.inputs; break;
@@ -155,7 +156,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR
 					default: inputs = (<any>result.workspaceFolderValue)?.inputs;
 				}
 			} else {
-				const valueResult = this.configurationService.getValue<any>(section, { resource: folder.uri });
+				const valueResult = this.configurationService.getValue<any>(section, overrides);
 				if (valueResult) {
 					inputs = valueResult.inputs;
 				}


Test patch:
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
--- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts
+++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts
@@ -448,6 +448,7 @@ suite('Configuration Resolver Service', () => {
 			assert.equal(1, mockCommandService.callCount);
 		});
 	});
+
 	test('a single prompt input variable', () => {
 
 		const configuration = {
@@ -475,6 +476,7 @@ suite('Configuration Resolver Service', () => {
 			assert.equal(0, mockCommandService.callCount);
 		});
 	});
+
 	test('a single pick input variable', () => {
 
 		const configuration = {
@@ -502,6 +504,7 @@ suite('Configuration Resolver Service', () => {
 			assert.equal(0, mockCommandService.callCount);
 		});
 	});
+
 	test('a single command input variable', () => {
 
 		const configuration = {
@@ -529,6 +532,7 @@ suite('Configuration Resolver Service', () => {
 			assert.equal(1, mockCommandService.callCount);
 		});
 	});
+
 	test('several input variables and command', () => {
 
 		const configuration = {
@@ -558,6 +562,35 @@ suite('Configuration Resolver Service', () => {
 			assert.equal(2, mockCommandService.callCount);
 		});
 	});
+
+	test('input variable with undefined workspace folder', () => {
+
+		const configuration = {
+			'name': 'Attach to Process',
+			'type': 'node',
+			'request': 'attach',
+			'processId': '${input:input1}',
+			'port': 5858,
+			'sourceMaps': false,
+			'outDir': null
+		};
+
+		return configurationResolverService!.resolveWithInteractionReplace(undefined, configuration, 'tasks').then(result => {
+
+			assert.deepEqual(result, {
+				'name': 'Attach to Process',
+				'type': 'node',
+				'request': 'attach',
+				'processId': 'resolvedEnterinput1',
+				'port': 5858,
+				'sourceMaps': false,
+				'outDir': null
+			});
+
+			assert.equal(0, mockCommandService.callCount);
+		});
+	});
+
 	test('contributed variable', () => {
 		const buildTask = 'npm: compile';
 		const variable = 'defaultBuildTask';