src/goDebugConfiguration: resolve relative paths used in cwd, output, program

If relative paths are used, translate them to be relative to the workspace folder when
using dlv-dap.

The description in the package.json says cwd is a workspace relative or absolute path, but
this seems to be broken in the old adapter (I.e., when cwd=., the old adapter simply used it
as --wd value and launched the headless server in the program directory. As a result, '.'
is translated as the program or package source directory).
This CL doesn't attempt to fix or change the behavior of the old adapter though, but
applies the translation only when dlv-dap is used.

This changes the default cwd value (when users attempt to add cwd to their launch config)
to be '' which is treated as if 'cwd' attribute was undefined. Users who want to use the
workspace folder can use `${workspaceFolder}` or  `.`.

This change doesn't change 'cwd' in attach mode because this is currently used for
different purpose in the legacy adapter, and it will become irrelevant in dlv-dap.

Updates golang/vscode-go#1348

Change-Id: Ieb15f6bbb470a17d2e7350ccf1d8a003cbb92eeb
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/317210
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/package.json b/package.json
index aca14f9..b6c0bd9 100644
--- a/package.json
+++ b/package.json
@@ -576,8 +576,8 @@
               },
               "cwd": {
                 "type": "string",
-                "description": "Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.",
-                "default": "."
+                "description": "Workspace relative or absolute path to the working directory of the program being debugged if a non-empty value is specified. The 'program' folder is used as the working directory if it is omitted or empty.",
+                "default": ""
               },
               "env": {
                 "type": "object",
diff --git a/src/goDebugConfiguration.ts b/src/goDebugConfiguration.ts
index 76759dc..e86819b 100644
--- a/src/goDebugConfiguration.ts
+++ b/src/goDebugConfiguration.ts
@@ -23,7 +23,7 @@
 import { getTool, getToolAtVersion } from './goTools';
 import { pickProcess, pickProcessByName } from './pickProcess';
 import { getFromGlobalState, updateGlobalState } from './stateUtils';
-import { getBinPath, getGoVersion } from './util';
+import { getBinPath, getGoVersion, getWorkspaceFolderPath, resolvePath } from './util';
 import { parseEnvFiles } from './utils/envUtils';
 import { resolveHomeDir } from './utils/pathUtils';
 
@@ -389,6 +389,22 @@
 		debugConfiguration['env'] = Object.assign(goToolsEnvVars, fileEnvs, env);
 		debugConfiguration['envFile'] = undefined; // unset, since we already processed.
 
+		const entriesWithRelativePaths = ['cwd', 'output', 'program'].filter(
+			(attr) => debugConfiguration[attr] && !path.isAbsolute(debugConfiguration[attr])
+		);
+		if (debugConfiguration['debugAdapter'] === 'dlv-dap' && entriesWithRelativePaths.length > 0) {
+			const workspaceRoot = folder?.uri.fsPath;
+			if (!workspaceRoot) {
+				this.showWarning(
+					'relativePathsWithoutWorkspaceFolder',
+					'Relative paths without a workspace folder for `cwd`, `program`, or `output` are not allowed.'
+				);
+				return null;
+			}
+			entriesWithRelativePaths.forEach((attr) => {
+				debugConfiguration[attr] = path.join(workspaceRoot, debugConfiguration[attr]);
+			});
+		}
 		return debugConfiguration;
 	}
 
diff --git a/src/goDebugFactory.ts b/src/goDebugFactory.ts
index e019f26..a247e30 100644
--- a/src/goDebugFactory.ts
+++ b/src/goDebugFactory.ts
@@ -412,10 +412,10 @@
 
 	logConsole(`Starting: ${dlvPath} ${dlvArgs.join(' ')}\n`);
 
-	// TODO(hyangah): determine the directories:
-	//    run `dlv` => where dlv will create the default __debug_bin. (This won't work if the directory is not writable. Fix it)
-	//    build program => 'program' directory. (This won't work for multimodule workspace. Fix it)
-	//    run program => cwd (If test, make sure to run in the package directory.)
+	// TODO(hyangah): In module-module workspace mode, the program should be build in the super module directory
+	// where go.work (gopls.mod) file is present. Where dlv runs determines the build directory currently. Two options:
+	//  1) launch dlv in the super-module module directory and adjust launchArgs.cwd (--wd).
+	//  2) introduce a new buildDir launch attribute.
 	return new Promise<ChildProcess>((resolve, reject) => {
 		const p = spawn(dlvPath, dlvArgs, {
 			cwd: dir,
diff --git a/test/integration/goDebugConfiguration.test.ts b/test/integration/goDebugConfiguration.test.ts
index 515559a..8199bbd 100644
--- a/test/integration/goDebugConfiguration.test.ts
+++ b/test/integration/goDebugConfiguration.test.ts
@@ -439,6 +439,112 @@
 	});
 });
 
+suite('Debug Configuration Converts Relative Paths', () => {
+	const debugConfigProvider = new GoDebugConfigurationProvider();
+
+	function debugConfig(adapter: string) {
+		return {
+			name: 'Launch',
+			type: 'go',
+			request: 'launch',
+			mode: 'auto',
+			debugAdapter: adapter,
+			program: path.join('foo', 'bar.go'),
+			cwd: '.',
+			output: 'debug'
+		};
+	}
+
+	test('resolve relative paths with workspace root in dlv-dap mode', () => {
+		const config = debugConfig('dlv-dap');
+		const workspaceFolder = {
+			uri: vscode.Uri.file(os.tmpdir()),
+			name: 'test',
+			index: 0
+		};
+		const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
+			workspaceFolder,
+			config
+		);
+		assert.deepStrictEqual(
+			{ program, cwd, output },
+			{
+				program: path.join(os.tmpdir(), 'foo/bar.go'),
+				cwd: os.tmpdir(),
+				output: path.join(os.tmpdir(), 'debug')
+			}
+		);
+	});
+
+	test('empty, undefined paths are not affected', () => {
+		const config = debugConfig('dlv-dap');
+		config.program = undefined;
+		config.cwd = '';
+		delete config.output;
+
+		const workspaceFolder = {
+			uri: vscode.Uri.file(os.tmpdir()),
+			name: 'test',
+			index: 0
+		};
+		const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
+			workspaceFolder,
+			config
+		);
+		assert.deepStrictEqual(
+			{ program, cwd, output },
+			{
+				program: undefined,
+				cwd: '',
+				output: undefined
+			}
+		);
+	});
+
+	test('disallow relative paths with no workspace root', () => {
+		const config = debugConfig('dlv-dap');
+		const got = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, config);
+		assert.strictEqual(got, null);
+	});
+
+	test('do not affect relative paths (workspace) in legacy mode', () => {
+		const config = debugConfig('legacy');
+		const workspaceFolder = {
+			uri: vscode.Uri.file(os.tmpdir()),
+			name: 'test',
+			index: 0
+		};
+		const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
+			workspaceFolder,
+			config
+		);
+		assert.deepStrictEqual(
+			{ program, cwd, output },
+			{
+				program: path.join('foo', 'bar.go'),
+				cwd: '.',
+				output: 'debug'
+			}
+		);
+	});
+
+	test('do not affect relative paths (no workspace) in legacy mode', () => {
+		const config = debugConfig('legacy');
+		const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
+			undefined,
+			config
+		);
+		assert.deepStrictEqual(
+			{ program, cwd, output },
+			{
+				program: path.join('foo', 'bar.go'),
+				cwd: '.',
+				output: 'debug'
+			}
+		);
+	});
+});
+
 suite('Debug Configuration Auto Mode', () => {
 	const debugConfigProvider = new GoDebugConfigurationProvider();
 	test('resolve auto to debug with non-test file', () => {