src/goDocumentSymbols: refactor DocumentSymbolProvider to remove fallback code

This is an attempt to split out the code running go-outline
and the code that asks gopls for the document symbols.

This change also removes the fallback to go-outline if
the language server is enabled. This means that users of old gopls may
get worse results, since they cannot list packages.
This change affects identifying test functions.

Change-Id: I271db88ef4f5307791b7954f102b2f3aa859f38b
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/403894
Reviewed-by: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
diff --git a/src/goDocumentSymbols.ts b/src/goDocumentSymbols.ts
new file mode 100644
index 0000000..2da40ca
--- /dev/null
+++ b/src/goDocumentSymbols.ts
@@ -0,0 +1,102 @@
+/*---------------------------------------------------------
+ * Copyright 2022 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import vscode = require('vscode');
+import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageserver-protocol';
+import { getGoConfig } from './config';
+import { goCtx } from './goMain';
+import { GoLegacyDocumentSymbolProvider } from './language/legacy/goOutline';
+
+export function GoDocumentSymbolProvider(
+	includeImports?: boolean
+): GoplsDocumentSymbolProvider | GoLegacyDocumentSymbolProvider {
+	const { latestConfig } = goCtx;
+	if (!latestConfig?.enabled) {
+		return new GoLegacyDocumentSymbolProvider(includeImports);
+	}
+	return new GoplsDocumentSymbolProvider(includeImports);
+}
+
+const GOPLS_LIST_IMPORTS = 'gopls.list_imports';
+export class GoplsDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
+	constructor(private includeImports?: boolean) {}
+
+	public async provideDocumentSymbols(document: vscode.TextDocument): Promise<vscode.DocumentSymbol[]> {
+		if (typeof this.includeImports !== 'boolean') {
+			const gotoSymbolConfig = getGoConfig(document.uri)['gotoSymbol'];
+			this.includeImports = gotoSymbolConfig ? gotoSymbolConfig['includeImports'] : false;
+		}
+		const { languageClient, serverInfo } = goCtx;
+		if (!languageClient) {
+			return [];
+		}
+
+		const symbols: vscode.DocumentSymbol[] | undefined = await vscode.commands.executeCommand(
+			'vscode.executeDocumentSymbolProvider',
+			document.uri
+		);
+		if (!symbols || symbols.length === 0) {
+			return [];
+		}
+
+		// Stitch the results together to make the results look like
+		// go-outline.
+		let pkgDeclRng = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0));
+		let pkgName = '';
+
+		// Try to find the package statement.
+		const text = document.getText();
+		const packageStatement = new RegExp('^[ \\t]*package[ \\t]*(\\S+)', 'm');
+		const match = packageStatement.exec(text);
+		if (match && match.length === 2) {
+			const packageDecl = match[0];
+			const start = text.indexOf(packageDecl);
+			pkgDeclRng = new vscode.Range(document.positionAt(start), document.positionAt(start + packageDecl.length));
+			pkgName = packageDecl[1];
+		}
+		const packageSymbol = new vscode.DocumentSymbol(
+			pkgName,
+			'package',
+			vscode.SymbolKind.Package,
+			pkgDeclRng,
+			pkgDeclRng
+		);
+		packageSymbol.children = symbols;
+		if (this.includeImports && serverInfo?.Commands?.includes(GOPLS_LIST_IMPORTS)) {
+			try {
+				const imports = await listImports(document);
+				imports?.forEach((value) => {
+					packageSymbol.children.unshift(
+						new vscode.DocumentSymbol(
+							value.Path,
+							'import',
+							vscode.SymbolKind.Namespace,
+							new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)),
+							new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0))
+						)
+					);
+				});
+			} catch (err) {
+				console.log('Failed to list imports: {err}');
+			}
+		}
+		return [packageSymbol];
+	}
+}
+
+async function listImports(document: vscode.TextDocument): Promise<{ Path: string; Name: string }[]> {
+	const { languageClient } = goCtx;
+	const uri = languageClient?.code2ProtocolConverter.asTextDocumentIdentifier(document).uri;
+	const params: ExecuteCommandParams = {
+		command: GOPLS_LIST_IMPORTS,
+		arguments: [
+			{
+				URI: uri
+			}
+		]
+	};
+	const resp = await languageClient?.sendRequest(ExecuteCommandRequest.type, params);
+	return resp.Imports;
+}
diff --git a/src/goGenerateTests.ts b/src/goGenerateTests.ts
index 756dca8..e06354d 100644
--- a/src/goGenerateTests.ts
+++ b/src/goGenerateTests.ts
@@ -14,7 +14,7 @@
 import { getGoConfig } from './config';
 import { toolExecutionEnvironment } from './goEnv';
 import { promptForMissingTool } from './goInstallTools';
-import { GoDocumentSymbolProvider } from './language/legacy/goOutline';
+import { GoDocumentSymbolProvider } from './goDocumentSymbols';
 import { outputChannel } from './goStatus';
 import { getBinPath } from './util';
 
@@ -223,7 +223,7 @@
 }
 
 async function getFunctions(doc: vscode.TextDocument): Promise<vscode.DocumentSymbol[]> {
-	const documentSymbolProvider = new GoDocumentSymbolProvider();
+	const documentSymbolProvider = GoDocumentSymbolProvider();
 	const symbols = await documentSymbolProvider.provideDocumentSymbols(doc);
 	return symbols[0].children.filter((sym) =>
 		[vscode.SymbolKind.Function, vscode.SymbolKind.Method].includes(sym.kind)
diff --git a/src/goReferencesCodelens.ts b/src/goReferencesCodelens.ts
index b78a3a2..fc9cb67 100644
--- a/src/goReferencesCodelens.ts
+++ b/src/goReferencesCodelens.ts
@@ -10,7 +10,7 @@
 import { CancellationToken, CodeLens, Range, TextDocument } from 'vscode';
 import { getGoConfig } from './config';
 import { GoBaseCodeLensProvider } from './goBaseCodelens';
-import { GoDocumentSymbolProvider } from './language/legacy/goOutline';
+import { GoDocumentSymbolProvider } from './goDocumentSymbols';
 import { GoReferenceProvider } from './language/legacy/goReferences';
 import { getBinPath } from './util';
 import vscode = require('vscode');
@@ -89,7 +89,7 @@
 		document: TextDocument,
 		token: CancellationToken
 	): Promise<vscode.DocumentSymbol[]> {
-		const symbolProvider = new GoDocumentSymbolProvider();
+		const symbolProvider = GoDocumentSymbolProvider();
 		const isTestFile = document.fileName.endsWith('_test.go');
 		const symbols = await symbolProvider.provideDocumentSymbols(document, token);
 		return symbols[0].children.filter((symbol) => {
diff --git a/src/goRunTestCodelens.ts b/src/goRunTestCodelens.ts
index 886fccf..77847ca 100644
--- a/src/goRunTestCodelens.ts
+++ b/src/goRunTestCodelens.ts
@@ -11,7 +11,7 @@
 import { CancellationToken, CodeLens, TextDocument } from 'vscode';
 import { getGoConfig } from './config';
 import { GoBaseCodeLensProvider } from './goBaseCodelens';
-import { GoDocumentSymbolProvider } from './language/legacy/goOutline';
+import { GoDocumentSymbolProvider } from './goDocumentSymbols';
 import { getBenchmarkFunctions, getTestFunctions } from './testUtils';
 
 export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider {
@@ -36,7 +36,7 @@
 	}
 
 	private async getCodeLensForPackage(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
-		const documentSymbolProvider = new GoDocumentSymbolProvider();
+		const documentSymbolProvider = GoDocumentSymbolProvider();
 		const symbols = await documentSymbolProvider.provideDocumentSymbols(document, token);
 		if (!symbols || symbols.length === 0) {
 			return [];
diff --git a/src/goTest/explore.ts b/src/goTest/explore.ts
index 8477d50..412798e 100644
--- a/src/goTest/explore.ts
+++ b/src/goTest/explore.ts
@@ -18,7 +18,7 @@
 	WorkspaceFoldersChangeEvent
 } from 'vscode';
 import vscode = require('vscode');
-import { GoDocumentSymbolProvider } from '../language/legacy/goOutline';
+import { GoDocumentSymbolProvider } from '../goDocumentSymbols';
 import { outputChannel } from '../goStatus';
 import { dispose, disposeIfEmpty, findItem, GoTest, isInTest, Workspace } from './utils';
 import { GoTestResolver, ProvideSymbols } from './resolve';
@@ -35,7 +35,7 @@
 		if (!isVscodeTestingAPIAvailable) throw new Error('VSCode Testing API is unavailable');
 
 		const ctrl = vscode.tests.createTestController('go', 'Go');
-		const symProvider = new GoDocumentSymbolProvider(true);
+		const symProvider = GoDocumentSymbolProvider(true);
 		const inst = new this(workspace, ctrl, context.workspaceState, (doc, token) =>
 			symProvider.provideDocumentSymbols(doc, token)
 		);
diff --git a/src/language/legacy/goOutline.ts b/src/language/legacy/goOutline.ts
index 6552955..f8d4a70 100644
--- a/src/language/legacy/goOutline.ts
+++ b/src/language/legacy/goOutline.ts
@@ -8,11 +8,9 @@
 
 import cp = require('child_process');
 import vscode = require('vscode');
-import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageserver-protocol';
 import { getGoConfig } from '../../config';
 import { toolExecutionEnvironment } from '../../goEnv';
 import { promptForMissingTool, promptForUpdatingTool } from '../../goInstallTools';
-import { goCtx } from '../../goMain';
 import { getBinPath, getFileArchive, makeMemoizedByteOffsetConverter } from '../../util';
 import { killProcess } from '../../utils/processUtils';
 
@@ -193,9 +191,7 @@
 	return symbols;
 }
 
-const GOPLS_LIST_IMPORTS = 'gopls.list_imports';
-
-export class GoDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
+export class GoLegacyDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
 	constructor(private includeImports?: boolean) {}
 
 	public async provideDocumentSymbols(
@@ -206,69 +202,6 @@
 			const gotoSymbolConfig = getGoConfig(document.uri)['gotoSymbol'];
 			this.includeImports = gotoSymbolConfig ? gotoSymbolConfig['includeImports'] : false;
 		}
-
-		const { languageClient, serverInfo } = goCtx;
-		// TODO(suzmue): Check the commands available instead of the version.
-		if (languageClient && serverInfo?.Commands?.includes(GOPLS_LIST_IMPORTS)) {
-			const symbols: vscode.DocumentSymbol[] | undefined = await vscode.commands.executeCommand(
-				'vscode.executeDocumentSymbolProvider',
-				document.uri
-			);
-			if (!symbols || symbols.length === 0) {
-				return [];
-			}
-
-			// Stitch the results together to make the results look like
-			// go-outline.
-			let pkgDeclRng = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0));
-			let pkgName = '';
-
-			// Try to find the package statement.
-			const text = document.getText();
-			const packageStatement = new RegExp('^[ \\t]*package[ \\t]*(\\S+)', 'm');
-			const match = packageStatement.exec(text);
-			if (match && match.length === 2) {
-				const packageDecl = match[0];
-				const start = text.indexOf(packageDecl);
-				pkgDeclRng = new vscode.Range(
-					document.positionAt(start),
-					document.positionAt(start + packageDecl.length)
-				);
-				pkgName = packageDecl[1];
-			}
-			const packageSymbol = new vscode.DocumentSymbol(
-				pkgName,
-				'package',
-				vscode.SymbolKind.Package,
-				pkgDeclRng,
-				pkgDeclRng
-			);
-			packageSymbol.children = symbols;
-			if (this.includeImports) {
-				try {
-					const imports = await listImports(document);
-					imports?.forEach((value) => {
-						packageSymbol.children.unshift(
-							new vscode.DocumentSymbol(
-								value.Path,
-								'import',
-								vscode.SymbolKind.Namespace,
-								new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)),
-								new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0))
-							)
-						);
-					});
-				} catch (e) {
-					// Fall back to use go-outline.
-					return this.runGoOutline(document, token);
-				}
-			}
-			return [packageSymbol];
-		}
-		return this.runGoOutline(document, token);
-	}
-
-	private runGoOutline(document: vscode.TextDocument, token?: vscode.CancellationToken) {
 		const options: GoOutlineOptions = {
 			fileName: document.fileName,
 			document,
@@ -277,18 +210,3 @@
 		return documentSymbols(options, token);
 	}
 }
-
-async function listImports(document: vscode.TextDocument): Promise<{ Path: string; Name: string }[]> {
-	const { languageClient } = goCtx;
-	const uri = languageClient?.code2ProtocolConverter.asTextDocumentIdentifier(document).uri;
-	const params: ExecuteCommandParams = {
-		command: GOPLS_LIST_IMPORTS,
-		arguments: [
-			{
-				URI: uri
-			}
-		]
-	};
-	const resp = await languageClient?.sendRequest(ExecuteCommandRequest.type, params);
-	return resp.Imports;
-}
diff --git a/src/language/registerDefaultProviders.ts b/src/language/registerDefaultProviders.ts
index a5380ac..86a1d20 100644
--- a/src/language/registerDefaultProviders.ts
+++ b/src/language/registerDefaultProviders.ts
@@ -13,7 +13,7 @@
 import { GoImplementationProvider } from './legacy/goImplementations';
 import { parseLiveFile } from './legacy/goLiveErrors';
 import { GO_MODE } from '../goMode';
-import { GoDocumentSymbolProvider } from './legacy/goOutline';
+import { GoLegacyDocumentSymbolProvider } from './legacy/goOutline';
 import { GoReferenceProvider } from './legacy/goReferences';
 import { GoRenameProvider } from './legacy/goRename';
 import { GoSignatureHelpProvider } from './legacy/goSignature';
@@ -32,7 +32,7 @@
 		this._disposables.push(vscode.languages.registerDefinitionProvider(GO_MODE, new GoDefinitionProvider()));
 		this._disposables.push(vscode.languages.registerReferenceProvider(GO_MODE, new GoReferenceProvider()));
 		this._disposables.push(
-			vscode.languages.registerDocumentSymbolProvider(GO_MODE, new GoDocumentSymbolProvider())
+			vscode.languages.registerDocumentSymbolProvider(GO_MODE, new GoLegacyDocumentSymbolProvider())
 		);
 		this._disposables.push(vscode.languages.registerWorkspaceSymbolProvider(new GoWorkspaceSymbolProvider()));
 		this._disposables.push(
diff --git a/src/testUtils.ts b/src/testUtils.ts
index 1c7c2fd..8e8e42d 100644
--- a/src/testUtils.ts
+++ b/src/testUtils.ts
@@ -15,7 +15,7 @@
 import { applyCodeCoverageToAllEditors } from './goCover';
 import { toolExecutionEnvironment } from './goEnv';
 import { getCurrentPackage } from './goModules';
-import { GoDocumentSymbolProvider } from './language/legacy/goOutline';
+import { GoDocumentSymbolProvider } from './goDocumentSymbols';
 import { getNonVendorPackages } from './goPackages';
 import { getBinPath, getCurrentGoPath, getTempFilePath, LineBuffer, resolvePath } from './util';
 import { parseEnvFile } from './utils/envUtils';
@@ -144,7 +144,7 @@
 	doc: vscode.TextDocument,
 	token?: vscode.CancellationToken
 ): Promise<vscode.DocumentSymbol[] | undefined> {
-	const documentSymbolProvider = new GoDocumentSymbolProvider(true);
+	const documentSymbolProvider = GoDocumentSymbolProvider(true);
 	const symbols = await documentSymbolProvider.provideDocumentSymbols(doc, token);
 	if (!symbols || symbols.length === 0) {
 		return;
@@ -226,7 +226,7 @@
 	doc: vscode.TextDocument,
 	token?: vscode.CancellationToken
 ): Promise<vscode.DocumentSymbol[] | undefined> {
-	const documentSymbolProvider = new GoDocumentSymbolProvider();
+	const documentSymbolProvider = GoDocumentSymbolProvider();
 	const symbols = await documentSymbolProvider.provideDocumentSymbols(doc, token);
 	if (!symbols || symbols.length === 0) {
 		return;
diff --git a/test/integration/extension.test.ts b/test/integration/extension.test.ts
index 157825f..285dcb0 100644
--- a/test/integration/extension.test.ts
+++ b/test/integration/extension.test.ts
@@ -27,12 +27,8 @@
 import { updateGoVarsFromConfig } from '../../src/goInstallTools';
 import { buildLanguageServerConfig } from '../../src/language/goLanguageServer';
 import { goLint } from '../../src/goLint';
-import {
-	documentSymbols,
-	GoDocumentSymbolProvider,
-	GoOutlineImportsOptions
-} from '../../src/language/legacy/goOutline';
-import { getAllPackages } from '../../src/goPackages';
+import { documentSymbols, GoOutlineImportsOptions } from '../../src/language/legacy/goOutline';
+import { GoDocumentSymbolProvider } from '../../src/goDocumentSymbols';
 import { goPlay } from '../../src/goPlayground';
 import { GoSignatureHelpProvider } from '../../src/language/legacy/goSignature';
 import { GoCompletionItemProvider } from '../../src/language/legacy/goSuggest';
@@ -734,7 +730,7 @@
 	test('Test Outline document symbols', async () => {
 		const uri = vscode.Uri.file(path.join(fixturePath, 'outlineTest', 'test.go'));
 		const document = await vscode.workspace.openTextDocument(uri);
-		const symbolProvider = new GoDocumentSymbolProvider();
+		const symbolProvider = GoDocumentSymbolProvider();
 
 		const outlines = await symbolProvider.provideDocumentSymbols(document, dummyCancellationSource.token);
 		const packages = outlines.filter((x) => x.kind === vscode.SymbolKind.Package);