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>
diff --git a/extension/src/goImpl.ts b/extension/src/goImpl.ts
index 2a11d3b..b569211 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') {
-				void 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 066e80b..824939d 100644
--- a/extension/test/gopls/interactive.test.ts
+++ b/extension/test/gopls/interactive.test.ts
@@ -10,6 +10,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 () {
@@ -135,6 +136,10 @@
 	test('Add struct tags via addTags', 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('json,xml');
@@ -146,4 +151,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 81fe64d..ae568d0 100644
--- a/extension/tools/installtools/main.go
+++ b/extension/tools/installtools/main.go
@@ -37,6 +37,8 @@
 	{"honnef.co/go/tools/cmd/staticcheck", "", false, ""},
 	// For regression test: golang/vscode-go#3511
 	{"github.com/golangci/golangci-lint/v2/cmd/golangci-lint", "golangci-lint-v2", false, "v2.12.2"},
+	// TODO(hxjiang): remove from test environment later.
+	{"github.com/josharian/impl", "", false, "v1.5.0"},
 	{"github.com/go-delve/delve/cmd/dlv", "", false, ""},
 }