[dev.go2go] goLanguageServer.ts: add support for .go2 files with gopls

This change checks if the user has a version of go that supports go2go,
and if so, translates URIs as they go between the extension and the
language server.

It is definitely excessive to stat files up to 2 times every time they
are requested, but caching known files gets us into difficult territory
with deletions, renames, etc. I'd rather avoid it and sacrifice a little
speed.

Change-Id: I8c1e2a1973e9715c16a772bffe46fe7d3cb4b341
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/238244
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/package.json b/package.json
index 1dc7e8d..74cf405 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,8 @@
       {
         "id": "go",
         "extensions": [
-          ".go"
+          ".go",
+          "go2"
         ],
         "aliases": [
           "Go"
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index f6fac7f..9c5a86c 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -39,7 +39,7 @@
 import { getTool, Tool } from './goTools';
 import { GoTypeDefinitionProvider } from './goTypeDefinition';
 import { getFromGlobalState, updateGlobalState } from './stateUtils';
-import { getBinPath, getCurrentGoPath, getGoConfig } from './util';
+import { getBinPath, getCurrentGoPath, getGoConfig, getGoVersion } from './util';
 
 interface LanguageServerConfig {
 	serverName: string;
@@ -84,22 +84,43 @@
 	if (activation && cfg.enabled && cfg.serverName === 'gopls') {
 		const tool = getTool(cfg.serverName);
 		if (tool) {
-			const versionToUpdate = await shouldUpdateLanguageServer(tool, cfg.path, cfg.checkForUpdates);
-			if (versionToUpdate) {
-				promptForUpdatingTool(tool.name, versionToUpdate);
-			} else if (goplsSurveyOn) {
-				// Only prompt users to fill out the gopls survey if we are not
-				// also prompting them to update (both would be too much).
-				const timeout = 1000 * 60 * 60; // 1 hour
-				setTimeout(async () => {
-					const surveyCfg = await maybePromptForGoplsSurvey();
-					flushSurveyConfig(surveyCfg);
-				}, timeout);
+			// Skip the update prompt - the user should have a special generics
+			// version of gopls.
+			if (false) {
+				const versionToUpdate = await shouldUpdateLanguageServer(tool, cfg.path, cfg.checkForUpdates);
+				if (versionToUpdate) {
+					promptForUpdatingTool(tool.name, versionToUpdate);
+				} else if (goplsSurveyOn) {
+					// Only prompt users to fill out the gopls survey if we are not
+					// also prompting them to update (both would be too much).
+					const timeout = 1000 * 60 * 60; // 1 hour
+					setTimeout(async () => {
+						const surveyCfg = await maybePromptForGoplsSurvey();
+						flushSurveyConfig(surveyCfg);
+					}, timeout);
+				}
 			}
 		}
 	}
 
-	const started = await startLanguageServer(ctx, cfg);
+	// Run `go tool go2go help` to check if the user is working with a version
+	// of Go that supports the generics prototype. This will error either way,
+	// but we can check the error message to see if the command was recognized.
+	let usingGo2Go: boolean = false;
+	const goRuntimePath = getBinPath('go');
+	if (goRuntimePath) {
+		const execFile = util.promisify(cp.execFile);
+		try {
+			await execFile(goRuntimePath, ['tool', 'go2go', 'help'], { env: cfg.env });
+		} catch (err) {
+			const errStr = `${err}`;
+			if (errStr.indexOf('Usage: go2go <command> [arguments]') !== -1) {
+				usingGo2Go = true;
+			}
+		}
+	}
+
+	const started = await startLanguageServer(ctx, cfg, usingGo2Go);
 
 	// If the server has been disabled, or failed to start,
 	// fall back to the default providers, while making sure not to
@@ -109,7 +130,8 @@
 	}
 }
 
-async function startLanguageServer(ctx: vscode.ExtensionContext, config: LanguageServerConfig): Promise<boolean> {
+async function startLanguageServer(
+	ctx: vscode.ExtensionContext, config: LanguageServerConfig, usingGo2Go: boolean): Promise<boolean> {
 	// If the client has already been started, make sure to clear existing
 	// diagnostics and stop it.
 	if (languageClient) {
@@ -128,7 +150,7 @@
 		// Track the latest config used to start the language server,
 		// and rebuild the language client.
 		latestConfig = config;
-		languageClient = await buildLanguageClient(config);
+		languageClient = await buildLanguageClient(config, usingGo2Go);
 	}
 
 	// If the user has not enabled the language server, return early.
@@ -158,7 +180,7 @@
 	return true;
 }
 
-async function buildLanguageClient(config: LanguageServerConfig): Promise<LanguageClient> {
+async function buildLanguageClient(config: LanguageServerConfig, usingGo2Go: boolean): Promise<LanguageClient> {
 	// Reuse the same output channel for each instance of the server.
 	if (config.enabled && !serverOutputChannel) {
 		serverOutputChannel = vscode.window.createOutputChannel(config.serverName);
@@ -173,12 +195,39 @@
 		},
 		{
 			initializationOptions: {},
-			documentSelector: ['go', 'go.mod', 'go.sum'],
+			documentSelector: ['go', 'go2', 'go.mod', 'go.sum'],
 			uriConverters: {
 				// Apply file:/// scheme to all file paths.
-				code2Protocol: (uri: vscode.Uri): string =>
-					(uri.scheme ? uri : uri.with({ scheme: 'file' })).toString(),
-				protocol2Code: (uri: string) => vscode.Uri.parse(uri)
+				code2Protocol: (uri: vscode.Uri): string => {
+					if (usingGo2Go) {
+						uri = (uri.scheme ? uri : uri.with({ scheme: 'file' }));
+						// If the file has a *.go2 suffix, try stripping it.
+						const uriPath = uri.path.replace('.go2', '.go');
+						uri = uri.with({ path: uriPath });
+						return uri.toString();
+					}
+					return (uri.scheme ? uri : uri.with({ scheme: 'file' })).toString();
+				},
+				protocol2Code: (uri: string) => {
+					if (usingGo2Go) {
+						const parsed = vscode.Uri.parse(uri);
+						try {
+							fs.statSync(parsed.fsPath);
+							return parsed;
+						} catch (err) {
+							// Try adding a 'go2' suffix to a Go file and see if it exists.
+							const uriPath = parsed.fsPath.replace('.go', '.go2');
+							try {
+								fs.statSync(uriPath);
+								return parsed.with({ path: parsed.path.replace('.go', '.go2') });
+							} catch (err) {
+								// do nothing?
+							}
+						}
+						return parsed;
+					}
+					return vscode.Uri.parse(uri);
+				},
 			},
 			outputChannel: serverOutputChannel,
 			revealOutputChannelOn: RevealOutputChannelOn.Never,
@@ -494,7 +543,7 @@
 
 	// If the user's version does not contain a timestamp,
 	// default to a semver comparison of the two versions.
-	const usersVersionSemver = semver.coerce(usersVersion, {includePrerelease: true, loose: true});
+	const usersVersionSemver = semver.coerce(usersVersion, { includePrerelease: true, loose: true });
 	return semver.lt(usersVersionSemver, latestVersion) ? latestVersion : null;
 }