src/goTest: Add command Test Function At Cursor or Test Previous

This runs `Test Function At Cursor` if a unit test is found at the
cursor, otherwise it turns `Test Previous`.

Fixes golang/vscode-go#1508

Change-Id: I91305797dfeccd79c1f5ea14298a60803f24492b
GitHub-Last-Rev: 005415566932b373a994d7d39730f6996c97dab0
GitHub-Pull-Request: golang/vscode-go#1509
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/320769
Trust: Michael Knyszek <mknyszek@google.com>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/docs/commands.md b/docs/commands.md
index 6486617..23e0821 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -35,6 +35,10 @@
 
 Runs a unit test at the cursor.
 
+### `Go: Test Function At Cursor or Test Previous`
+
+Runs a unit test at the cursor if one is found, otherwise re-runs the last executed test.
+
 ### `Go: Subtest At Cursor`
 
 Runs a sub test at the cursor.
diff --git a/package.json b/package.json
index faab923..c620403 100644
--- a/package.json
+++ b/package.json
@@ -202,6 +202,11 @@
         "description": "Runs a unit test at the cursor."
       },
       {
+        "command": "go.test.cursorOrPrevious",
+        "title": "Go: Test Function At Cursor or Test Previous",
+        "description": "Runs a unit test at the cursor if one is found, otherwise re-runs the last executed test."
+      },
+      {
         "command": "go.subtest.cursor",
         "title": "Go: Subtest At Cursor",
         "description": "Runs a sub test at the cursor."
diff --git a/src/goMain.ts b/src/goMain.ts
index d03f863..b215f07 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -64,6 +64,7 @@
 	debugPrevious,
 	subTestAtCursor,
 	testAtCursor,
+	testAtCursorOrPrevious,
 	testCurrentFile,
 	testCurrentPackage,
 	testPrevious,
@@ -317,6 +318,13 @@
 	);
 
 	ctx.subscriptions.push(
+		vscode.commands.registerCommand('go.test.cursorOrPrevious', (args) => {
+			const goConfig = getGoConfig();
+			testAtCursorOrPrevious(goConfig, 'test', args);
+		})
+	);
+
+	ctx.subscriptions.push(
 		vscode.commands.registerCommand('go.subtest.cursor', (args) => {
 			const goConfig = getGoConfig();
 			subTestAtCursor(goConfig, args);
diff --git a/src/goTest.ts b/src/goTest.ts
index e25c9d7..bbf95c8 100644
--- a/src/goTest.ts
+++ b/src/goTest.ts
@@ -31,50 +31,68 @@
 
 export type TestAtCursorCmd = 'debug' | 'test' | 'benchmark';
 
+class NotFoundError extends Error {}
+
+async function _testAtCursor(goConfig: vscode.WorkspaceConfiguration, cmd: TestAtCursorCmd, args: any) {
+	const editor = vscode.window.activeTextEditor;
+	if (!editor) {
+		throw new NotFoundError('No editor is active.');
+	}
+	if (!editor.document.fileName.endsWith('_test.go')) {
+		throw new NotFoundError('No tests found. Current file is not a test file.');
+	}
+
+	const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
+	const testFunctions = await getFunctions(editor.document, null);
+	// We use functionName if it was provided as argument
+	// Otherwise find any test function containing the cursor.
+	const testFunctionName =
+		args && args.functionName
+			? args.functionName
+			: testFunctions.filter((func) => func.range.contains(editor.selection.start)).map((el) => el.name)[0];
+	if (!testFunctionName) {
+		throw new NotFoundError('No test function found at cursor.');
+	}
+
+	await editor.document.save();
+
+	if (cmd === 'debug') {
+		return debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
+	} else if (cmd === 'benchmark' || cmd === 'test') {
+		return runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
+	} else {
+		throw new Error(`Unsupported command: ${cmd}`);
+	}
+}
+
 /**
  * Executes the unit test at the primary cursor using `go test`. Output
  * is sent to the 'Go' channel.
  * @param goConfig Configuration for the Go extension.
- * @param cmd Whether the command is test , benchmark or debug.
+ * @param cmd Whether the command is test, benchmark, or debug.
  * @param args
  */
 export function testAtCursor(goConfig: vscode.WorkspaceConfiguration, cmd: TestAtCursorCmd, args: any) {
-	const editor = vscode.window.activeTextEditor;
-	if (!editor) {
-		vscode.window.showInformationMessage('No editor is active.');
-		return;
-	}
-	if (!editor.document.fileName.endsWith('_test.go')) {
-		vscode.window.showInformationMessage('No tests found. Current file is not a test file.');
-		return;
-	}
+	_testAtCursor(goConfig, cmd, args).catch((err) => {
+		if (err instanceof NotFoundError) {
+			vscode.window.showInformationMessage(err.message);
+		} else {
+			console.error(err);
+		}
+	});
+}
 
-	const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
-
-	editor.document.save().then(async () => {
-		try {
-			const testFunctions = await getFunctions(editor.document, null);
-			// We use functionName if it was provided as argument
-			// Otherwise find any test function containing the cursor.
-			const testFunctionName =
-				args && args.functionName
-					? args.functionName
-					: testFunctions
-							.filter((func) => func.range.contains(editor.selection.start))
-							.map((el) => el.name)[0];
-			if (!testFunctionName) {
-				vscode.window.showInformationMessage('No test function found at cursor.');
-				return;
-			}
-
-			if (cmd === 'debug') {
-				await debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
-			} else if (cmd === 'benchmark' || cmd === 'test') {
-				await runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
-			} else {
-				throw new Error('Unsupported command.');
-			}
-		} catch (err) {
+/**
+ * Executes the unit test at the primary cursor if found, otherwise re-runs the previous test.
+ * @param goConfig Configuration for the Go extension.
+ * @param cmd Whether the command is test, benchmark, or debug.
+ * @param args
+ */
+export function testAtCursorOrPrevious(goConfig: vscode.WorkspaceConfiguration, cmd: TestAtCursorCmd, args: any) {
+	_testAtCursor(goConfig, cmd, args).catch((err) => {
+		if (err instanceof NotFoundError) {
+			testPrevious();
+		} else {
 			console.error(err);
 		}
 	});