docs/settings: make the custom formatter support more visible

Custom formatter support was added in golang/vscode-go#1238.
But due to the limitation in the VS Code setting UI (which assumes
the acceptable values list is static) and the setting validation
logic (VS Code thinks it's an error to use a value outside of the
supplied enum list is invalid), this is not visible to users.

Instead, this change introduces an extra enum 'custom' for the
"go.formatTool" setting. If this is chosen, the extension uses
the tool specified as `customFormatter` in the "go.alternateTools"
setting section for formatting.

The extension expects the custom formatter to accept input as STDIN
and output the result as STDOUT. Users can also supply "go.formatFlags".

Changed the descriptions to use markdown - which allows to reference
other settings ("`#...#`"). The document generation tool does not
handle this special syntax nicely but I hope this isn't too confusing.

I wish we could add an extra validation on the allowed value for
"go.formatTool" in favor of this new 'custom' option. I am not adding
the validation in this CL because there could be users who depend
on this behavior.

For golang/vscode-go#1238
For golang/vscode-go#1603
For golang/vscode-go#2503

Change-Id: I5d9564f331845b6b07f0b54148834118404f3553
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/446298
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/docs/settings.md b/docs/settings.md
index 8958bde..ce7b8f0 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -51,9 +51,9 @@
 Alternate tools or alternate paths for the same tools used by the Go extension. Provide either absolute path or the name of the binary in GOPATH/bin, GOROOT/bin or PATH. Useful when you want to use wrapper script for the Go tools.
 | Properties | Description |
 | --- | --- |
+| `customFormatter` | Custom formatter to use instead of the language server. This should be used with the `custom` option in `#go.formatTool#`. <br/> Default: `""` |
 | `dlv` | Alternate tool to use instead of the dlv binary or alternate path to use for the dlv binary. <br/> Default: `"dlv"` |
 | `go` | Alternate tool to use instead of the go binary or alternate path to use for the go binary. <br/> Default: `"go"` |
-| `go-outline` | Alternate tool to use instead of the go-outline binary or alternate path to use for the go-outline binary. <br/> Default: `"go-outline"` |
 | `gopls` | Alternate tool to use instead of the gopls binary or alternate path to use for the gopls binary. <br/> Default: `"gopls"` |
 ### `go.autocompleteUnimportedPackages`
 
@@ -221,8 +221,16 @@
 Flags to pass to format tool (e.g. ["-s"]). Not applicable when using the language server.
 ### `go.formatTool`
 
-When the language server is enabled and one of default/gofmt/goimports/gofumpt is chosen, the language server will handle formatting. Otherwise, the extension will use the specified tool for formatting.<br/>
-Allowed Options: `default`, `gofmt`, `goimports`, `goformat`, `gofumpt`
+When the language server is enabled and one of `default`/`gofmt`/`goimports`/`gofumpt` is chosen, the language server will handle formatting. If `custom` tool is selected, the extension will use the `customFormatter` tool in the `#go.alternateTools#` section.<br/>
+Allowed Options:
+
+* `default`: If the language server is enabled, format via the language server, which already supports gofmt, goimports, goreturns, and gofumpt. Otherwise, goimports.
+* `gofmt`: Formats the file according to the standard Go style. (not applicable when the language server is enabled)
+* `goimports`: Organizes imports and formats the file with gofmt. (not applicable when the language server is enabled)
+* `goformat`: Configurable gofmt, see https://github.com/mbenkmann/goformat.
+* `gofumpt`: Stricter version of gofmt, see https://github.com/mvdan/gofumpt. . Use `#gopls.format.gofumpt#` instead)
+* `custom`: Formats using the custom tool specified as `customFormatter` in the `#go.alternateTools#` setting. The tool should take the input as STDIN and output the formatted code as STDOUT.
+
 
 Default: `"default"`
 ### `go.generateTestsFlags`
diff --git a/package.json b/package.json
index 3066e61..2c69c3f 100644
--- a/package.json
+++ b/package.json
@@ -1206,23 +1206,23 @@
         "go.formatTool": {
           "type": "string",
           "default": "default",
-          "description": "When the language server is enabled and one of default/gofmt/goimports/gofumpt is chosen, the language server will handle formatting. Otherwise, the extension will use the specified tool for formatting.",
+          "markdownDescription": "When the language server is enabled and one of `default`/`gofmt`/`goimports`/`gofumpt` is chosen, the language server will handle formatting. If `custom` tool is selected, the extension will use the `customFormatter` tool in the `#go.alternateTools#` section.",
           "scope": "resource",
           "enum": [
             "default",
             "gofmt",
             "goimports",
             "goformat",
-            "gofumpt"
+            "gofumpt",
+            "custom"
           ],
-          "additionalItems": true,
-          "enumDescriptions": [
+          "markdownEnumDescriptions": [
             "If the language server is enabled, format via the language server, which already supports gofmt, goimports, goreturns, and gofumpt. Otherwise, goimports.",
             "Formats the file according to the standard Go style. (not applicable when the language server is enabled)",
             "Organizes imports and formats the file with gofmt. (not applicable when the language server is enabled)",
             "Configurable gofmt, see https://github.com/mbenkmann/goformat.",
-            "Stricter version of gofmt, see https://github.com/mvdan/gofumpt. (not applicable when the language server is enabled)",
-            "Applies gofumpt formatting and organizes imports."
+            "Stricter version of gofmt, see https://github.com/mvdan/gofumpt. . Use `#gopls.format.gofumpt#` instead)",
+            "Formats using the custom tool specified as `customFormatter` in the `#go.alternateTools#` setting. The tool should take the input as STDIN and output the formatted code as STDOUT."
           ]
         },
         "go.formatFlags": {
@@ -2030,15 +2030,15 @@
               "default": "gopls",
               "description": "Alternate tool to use instead of the gopls binary or alternate path to use for the gopls binary."
             },
-            "go-outline": {
-              "type": "string",
-              "default": "go-outline",
-              "description": "Alternate tool to use instead of the go-outline binary or alternate path to use for the go-outline binary."
-            },
             "dlv": {
               "type": "string",
               "default": "dlv",
               "description": "Alternate tool to use instead of the dlv binary or alternate path to use for the dlv binary."
+            },
+            "customFormatter": {
+              "type": "string",
+              "default": "",
+              "markdownDescription": "Custom formatter to use instead of the language server. This should be used with the `custom` option in `#go.formatTool#`."
             }
           },
           "additionalProperties": true
diff --git a/src/language/legacy/goFormat.ts b/src/language/legacy/goFormat.ts
index 302394e..8d8164d 100644
--- a/src/language/legacy/goFormat.ts
+++ b/src/language/legacy/goFormat.ts
@@ -39,7 +39,7 @@
 		// Handle issues:
 		//  https://github.com/Microsoft/vscode-go/issues/613
 		//  https://github.com/Microsoft/vscode-go/issues/630
-		if (formatTool === 'goimports' || formatTool === 'goreturns' || formatTool === 'gofumports') {
+		if (formatTool === 'goimports' || formatTool === 'goreturns') {
 			formatFlags.push('-srcdir', filename);
 		}
 
@@ -75,10 +75,10 @@
 	): Thenable<vscode.TextEdit[]> {
 		const formatCommandBinPath = getBinPath(formatTool);
 		if (!path.isAbsolute(formatCommandBinPath)) {
+			// executable not found.
 			promptForMissingTool(formatTool);
 			return Promise.reject('failed to find tool ' + formatTool);
 		}
-
 		return new Promise<vscode.TextEdit[]>((resolve, reject) => {
 			const env = toolExecutionEnvironment();
 			const cwd = path.dirname(document.fileName);
@@ -94,7 +94,7 @@
 			p.on('error', (err) => {
 				if (err && (<any>err).code === 'ENOENT') {
 					promptForMissingTool(formatTool);
-					return reject();
+					return reject(`failed to find format tool: ${formatTool}`);
 				}
 			});
 			p.on('close', (code) => {
@@ -139,8 +139,12 @@
 }
 
 export function getFormatTool(goConfig: { [key: string]: any }): string {
-	if (goConfig['formatTool'] === 'default') {
+	const formatTool = goConfig['formatTool'];
+	if (formatTool === 'default') {
 		return 'goimports';
 	}
-	return goConfig['formatTool'];
+	if (formatTool === 'custom') {
+		return goConfig['alternateTools']['customFormatter'] || 'goimports';
+	}
+	return formatTool;
 }
diff --git a/test/gopls/extension.test.ts b/test/gopls/extension.test.ts
index 056bb9e..32245ce 100644
--- a/test/gopls/extension.test.ts
+++ b/test/gopls/extension.test.ts
@@ -286,11 +286,8 @@
 		}
 	});
 
-	test('Nonexistent formatter', async () => {
+	async function testCustomFormatter(goConfig: vscode.WorkspaceConfiguration, customFormatter: string) {
 		const config = require('../../src/config');
-		const goConfig = Object.create(getGoConfig(), {
-			formatTool: { value: 'nonexistent' } // this should make the formatter fail.
-		}) as vscode.WorkspaceConfiguration;
 		sandbox.stub(config, 'getGoConfig').returns(goConfig);
 
 		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'), goConfig);
@@ -308,7 +305,26 @@
 			);
 			assert.fail(`formatter unexpectedly succeeded and returned a result: ${JSON.stringify(result)}`);
 		} catch (e) {
-			assert(`${e}`.includes('errors when formatting with nonexistent'), `${e}`);
+			assert(`${e}`.includes(`errors when formatting with ${customFormatter}`), `${e}`);
 		}
+	}
+
+	test('Nonexistent formatter', async () => {
+		const customFormatter = 'nonexistent';
+		const goConfig = Object.create(getGoConfig(), {
+			formatTool: { value: customFormatter } // this should make the formatter fail.
+		}) as vscode.WorkspaceConfiguration;
+
+		await testCustomFormatter(goConfig, customFormatter);
+	});
+
+	test('Custom formatter', async () => {
+		const customFormatter = 'coolCustomFormatter';
+		const goConfig = Object.create(getGoConfig(), {
+			formatTool: { value: 'custom' }, // this should make the formatter fail.
+			alternateTools: { value: { customFormatter: customFormatter } } // this should make the formatter fail.
+		}) as vscode.WorkspaceConfiguration;
+
+		await testCustomFormatter(goConfig, customFormatter);
 	});
 });