goLanguageServer: turn on all experiments in the Nightly

For users of the Go Nightly extension and gopls/v0.5.2 and greater,
enable "allExperiments" by default. Users can still provide their own
overrides, and they can still set the "allExperiments" flag to false
manually.

Fixes golang/vscode-go#818

Change-Id: Ife0db71e6a1cb7472cfe2f18ca5a3d4aa2987697
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/264317
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Trust: Rebecca Stambler <rstambler@golang.org>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index 3699e0a..f2c2481 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -15,8 +15,11 @@
 import util = require('util');
 import vscode = require('vscode');
 import {
+	CancellationToken,
 	CloseAction,
 	CompletionItemKind,
+	ConfigurationParams,
+	ConfigurationRequest,
 	ErrorAction,
 	HandleDiagnosticsSignature,
 	InitializeError,
@@ -24,6 +27,7 @@
 	ProvideCodeLensesSignature,
 	ProvideCompletionItemsSignature,
 	ProvideDocumentLinksSignature,
+	ResponseError,
 	RevealOutputChannelOn
 } from 'vscode-languageclient';
 import {
@@ -57,6 +61,7 @@
 interface LanguageServerConfig {
 	serverName: string;
 	path: string;
+	version: string;
 	modtime: Date;
 	enabled: boolean;
 	flags: string[];
@@ -171,7 +176,7 @@
 		// Track the latest config used to start the language server,
 		// and rebuild the language client.
 		latestConfig = config;
-		languageClient = buildLanguageClient(config);
+		languageClient = await buildLanguageClient(config);
 		crashCount = 0;
 	}
 
@@ -201,27 +206,28 @@
 	return true;
 }
 
-function buildLanguageClient(config: LanguageServerConfig): LanguageClient {
+async function buildLanguageClient(cfg: LanguageServerConfig): Promise<LanguageClient> {
 	// Reuse the same output channel for each instance of the server.
-	if (config.enabled) {
+	if (cfg.enabled) {
 		if (!serverOutputChannel) {
-			serverOutputChannel = vscode.window.createOutputChannel(config.serverName + ' (server)');
+			serverOutputChannel = vscode.window.createOutputChannel(cfg.serverName + ' (server)');
 		}
 		if (!serverTraceChannel) {
-			serverTraceChannel = vscode.window.createOutputChannel(config.serverName);
+			serverTraceChannel = vscode.window.createOutputChannel(cfg.serverName);
 		}
 	}
-	const goplsConfig = getGoplsConfig();
+	let goplsWorkspaceConfig = getGoplsConfig();
+	goplsWorkspaceConfig = await adjustGoplsWorkspaceConfiguration(cfg, goplsWorkspaceConfig);
 	const c = new LanguageClient(
 		'go',  // id
-		config.serverName,  // name
+		cfg.serverName,  // name
 		{
-			command: config.path,
-			args: ['-mode=stdio', ...config.flags],
-			options: { env: config.env },
+			command: cfg.path,
+			args: ['-mode=stdio', ...cfg.flags],
+			options: { env: cfg.env },
 		},
 		{
-			initializationOptions: goplsConfig,
+			initializationOptions: goplsWorkspaceConfig,
 			documentSelector: ['go', 'go.mod', 'go.sum'],
 			uriConverters: {
 				// Apply file:/// scheme to all file paths.
@@ -298,7 +304,7 @@
 					diagnostics: vscode.Diagnostic[],
 					next: HandleDiagnosticsSignature
 				) => {
-					if (!config.features.diagnostics) {
+					if (!cfg.features.diagnostics) {
 						return null;
 					}
 					return next(uri, diagnostics);
@@ -308,7 +314,7 @@
 					token: vscode.CancellationToken,
 					next: ProvideDocumentLinksSignature
 				) => {
-					if (!config.features.documentLink) {
+					if (!cfg.features.documentLink) {
 						return null;
 					}
 					return next(document, token);
@@ -399,12 +405,50 @@
 					lastUserAction = new Date();
 					next(e);
 				},
+				workspace: {
+					configuration: async (params: ConfigurationParams, token: CancellationToken, next: ConfigurationRequest.HandlerSignature): Promise<any[] | ResponseError<void>> => {
+						const configs = await next(params, token);
+						if (!Array.isArray(configs)) {
+							return configs;
+						}
+						for (let workspaceConfig of configs) {
+							workspaceConfig = await adjustGoplsWorkspaceConfiguration(cfg, workspaceConfig);
+						}
+						return configs;
+					},
+				},
 			}
 		}
 	);
 	return c;
 }
 
+// adjustGoplsWorkspaceConfiguration adds any extra options to the gopls
+// config. Right now, the only extra option is enabling experiments for the
+// Nightly extension.
+async function adjustGoplsWorkspaceConfiguration(cfg: LanguageServerConfig, config: any): Promise<any> {
+	if (!config) {
+		return config;
+	}
+	// Only modify the user's configurations for the Nightly.
+	if (extensionId !== 'golang.go-nightly') {
+		return config;
+	}
+	// allExperiments is only available with gopls/v0.5.2 and above.
+	const version = await getLocalGoplsVersion(cfg);
+	if (!version) {
+		return config;
+	}
+	const sv = semver.parse(version, true);
+	if (!sv || semver.lt(sv, 'v0.5.2')) {
+		return config;
+	}
+	if (!config['allExperiments']) {
+		config['allExperiments'] = true;
+	}
+	return config;
+}
+
 // createTestCodeLens adds the go.test.cursor and go.debug.cursor code lens
 function createTestCodeLens(lens: vscode.CodeLens): vscode.CodeLens[] {
 	// CodeLens argument signature in gopls is [fileName: string, testFunctions: string[], benchFunctions: string[]],
@@ -510,6 +554,7 @@
 	const cfg: LanguageServerConfig = {
 		serverName: '',
 		path: '',
+		version: '', // compute version lazily
 		modtime: null,
 		enabled: goConfig['useLanguageServer'] === true,
 		flags: goConfig['languageServerFlags'] || [],
@@ -748,7 +793,12 @@
 // getLocalGoplsVersion returns the version of gopls that is currently
 // installed on the user's machine. This is determined by running the
 // `gopls version` command.
+//
+// If this command has already been executed, it returns the saved result.
 export const getLocalGoplsVersion = async (cfg: LanguageServerConfig) => {
+	if (cfg.version !== '') {
+		return cfg.version;
+	}
 	const execFile = util.promisify(cp.execFile);
 	let output: any;
 	try {
@@ -802,7 +852,8 @@
 	//
 	//    v0.1.3
 	//
-	return split[1];
+	cfg.version = split[1];
+	return cfg.version;
 };
 
 async function goProxyRequest(tool: Tool, endpoint: string): Promise<any> {
diff --git a/test/gopls/update.test.ts b/test/gopls/update.test.ts
index 031bea5..2c838ff 100644
--- a/test/gopls/update.test.ts
+++ b/test/gopls/update.test.ts
@@ -104,6 +104,7 @@
 			const got = await lsp.shouldUpdateLanguageServer(tool, {
 				enabled: true,
 				path: 'bad/path/to/gopls',
+				version: '',
 				checkForUpdates: true,
 				env: {},
 				features: {