[release-v0.56] extension/test/gopls: add implement interface test

Make vscode command "go.impl.cursor" await the completion of the
document edit before resolving.

This test is testing the behavior for the vscode command execution
using the third party tool impl. The test is added to make sure the
behavior remain mostly the same before and after the future change.

For golang/vscode-go#4063

Change-Id: Ib1d8c40c92ce878c35ec153dfbe1aa06ef8c8a36
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/815540
Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Madeline Kalil <mkalil@google.com>
(cherry picked from commit 0264783fd77c2a59aa8a0f149c45cf3ce0c0ce35)
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/820340
diff --git a/extension/src/goImpl.ts b/extension/src/goImpl.ts
index 61ac926..576bf83 100644
--- a/extension/src/goImpl.ts
+++ b/extension/src/goImpl.ts
@@ -18,6 +18,8 @@
 // Supports only passing interface, see TODO in implCursor to finish
 const inputRegex = /^(\w+\ \*?\w+\ )?([\w\.\-\/]+)$/;
 
+// implCursor generates method stubs for implementing the provided interface
+// based on the type defined at cursor.
 export const implCursor: CommandFactory = () => () => {
 	const editor = vscode.window.activeTextEditor;
 	if (!editor) {
@@ -44,33 +46,40 @@
 			// if matches[1] is undefined then detect receiver type
 			// take first character and use as receiver name
 
-			runGoImpl([matches[1], matches[2]], cursor.start, editor);
+			return runGoImpl([matches[1], matches[2]], cursor.start, editor);
 		});
 };
 
-function runGoImpl(args: string[], insertPos: vscode.Position, editor: vscode.TextEditor) {
-	const goimpl = getBinPath('impl');
-	const p = cp.execFile(
-		goimpl,
-		args,
-		{ env: toolExecutionEnvironment(), cwd: dirname(editor.document.fileName) },
-		(err, stdout, stderr) => {
-			if (err && (<any>err).code === 'ENOENT') {
-				promptForMissingTool('impl');
-				return;
-			}
+function runGoImpl(args: string[], insertPos: vscode.Position, editor: vscode.TextEditor): Promise<void> {
+	return new Promise((resolve) => {
+		const goimpl = getBinPath('impl');
+		const p = cp.execFile(
+			goimpl,
+			args,
+			{ env: toolExecutionEnvironment(), cwd: dirname(editor.document.fileName) },
+			(err, stdout, stderr) => {
+				if (err && (<any>err).code === 'ENOENT') {
+					void promptForMissingTool('impl');
+					return resolve();
+				}
 
-			if (err) {
-				vscode.window.showInformationMessage(`Cannot stub interface: ${stderr}`);
-				return;
-			}
+				if (err) {
+					vscode.window.showInformationMessage(`Cannot stub interface: ${stderr}`);
+					return resolve();
+				}
 
-			editor.edit((editBuilder) => {
-				editBuilder.insert(insertPos, stdout);
-			});
+				editor
+					.edit((editBuilder) => {
+						editBuilder.insert(insertPos, stdout);
+					})
+					.then(
+						() => resolve(),
+						() => resolve()
+					);
+			}
+		);
+		if (p.pid) {
+			p.stdin?.end();
 		}
-	);
-	if (p.pid) {
-		p.stdin?.end();
-	}
+	});
 }
diff --git a/extension/test/gopls/interactive.test.ts b/extension/test/gopls/interactive.test.ts
index bf7e62e..2f738f4 100644
--- a/extension/test/gopls/interactive.test.ts
+++ b/extension/test/gopls/interactive.test.ts
@@ -11,6 +11,7 @@
 import { Env } from './goplsTestEnv.utils';
 import { updateGoVarsFromConfig } from '../../src/goInstallTools';
 import { addTags } from '../../src/goModifytags';
+import { implCursor } from '../../src/goImpl';
 import { MockExtensionContext } from '../mocks/MockContext';
 
 suite('Interactive Refactoring', function () {
@@ -147,4 +148,24 @@
 		assert.match(docText, /Foo string `json:"foo,omitempty" xml:"foo"`/);
 		ctx.teardown();
 	});
+
+	test('Implement interface via implCursor', async () => {
+		const ctx = MockExtensionContext.new();
+		const editor = await vscode.window.showTextDocument(document);
+
+		// type Foo struct {
+		//	Foo string //@loc(editor.selection, "Foo string")
+		// }
+		editor.selection = new vscode.Selection(3, 1, 3, 11);
+
+		sandbox.stub(vscode.window, 'showInputBox').resolves('f *Foo net.Error');
+
+		await implCursor(ctx, env.goCtx)();
+
+		const docText = document.getText();
+		assert.match(docText, /func \(f \*Foo\) Error\(\) string/);
+		assert.match(docText, /func \(f \*Foo\) Temporary\(\) bool/);
+		assert.match(docText, /func \(f \*Foo\) Timeout\(\) bool/);
+		ctx.teardown();
+	});
 });
diff --git a/extension/tools/installtools/main.go b/extension/tools/installtools/main.go
index f954817..cdb41db 100644
--- a/extension/tools/installtools/main.go
+++ b/extension/tools/installtools/main.go
@@ -34,19 +34,16 @@
 	path          string
 	dest          string
 	preferPreview bool
-	// versions is a list of supportedVersions sorted by
-	// goMinorVersion. If we want to pin a tool's version
-	// add a fake entry with a large goMinorVersion
-	// value and the pinned tool version as the last entry.
-	// Nil of empty list indicates we can use the `latest` version.
-	versions []finalVersion
+	version       string // pinned version, or empty string to use "latest" (or preview if preferPreview)
 }{
 	// TODO: auto-generate based on allTools.ts.in.
-	{"golang.org/x/tools/gopls", "", true, nil},
-	{"github.com/cweill/gotests/gotests", "", false, nil},
-	{"github.com/haya14busa/goplay/cmd/goplay", "", false, nil},
-	{"honnef.co/go/tools/cmd/staticcheck", "", false, []finalVersion{{21, "v0.4.7"}}},
-	{"github.com/go-delve/delve/cmd/dlv", "", false, nil},
+	{"golang.org/x/tools/gopls", "", true, ""},
+	{"github.com/cweill/gotests/gotests", "", false, ""},
+	{"github.com/haya14busa/goplay/cmd/goplay", "", false, ""},
+	{"honnef.co/go/tools/cmd/staticcheck", "", false, ""},
+	// TODO(hxjiang): remove from test environment later.
+	{"github.com/josharian/impl", "", false, "v1.5.0"},
+	{"github.com/go-delve/delve/cmd/dlv", "", false, ""},
 }
 
 // pickVersion returns the version to install based on the supported
@@ -134,7 +131,10 @@
 	// For tools installation, ensure GOTOOLCHAIN=auto.
 	env := append(os.Environ(), "GO111MODULE=on", "GOTOOLCHAIN=auto")
 	for _, tool := range tools {
-		ver := pickVersion(goMinorVersion, tool.versions, pickLatest(tool.path, tool.preferPreview))
+		ver := tool.version
+		if ver == "" {
+			ver = pickLatest(tool.path, tool.preferPreview)
+		}
 		path := tool.path + "@" + ver
 		cmd := exec.Command("go", installCmd, path)
 		cmd.Env = env