src: prompt user to file an issue after gopls crashes

This change adds support for filing a gopls issue when the user restarts `gopls` or when `gopls` crashes. In a follow-up, we might be able to suggest attaching the `gopls` log.

The suggestion to file an issue after a manual restart is disabled by default for now. We can enable it once gopls is more stable. I'm moving the bulk of https://golang.org/cl/232863 here so that I can work on refactoring the env variables separately without causing a ton of merge conflicts.

Change-Id: I0c98bd526562dd50bdc7b127ddfb95f7de926075
GitHub-Last-Rev: df990d732515fa11bee78fdca45643b8cfef2cc8
GitHub-Pull-Request: golang/vscode-go#34
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/233325
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index 2704989..202cac8 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -14,8 +14,8 @@
 import util = require('util');
 import vscode = require('vscode');
 import {
-	Command, HandleDiagnosticsSignature, LanguageClient, ProvideCompletionItemsSignature,
-	ProvideDocumentLinksSignature, RevealOutputChannelOn
+	CloseAction, Command, ErrorAction, HandleDiagnosticsSignature, InitializeError, LanguageClient,
+	Message, ProvideCompletionItemsSignature, ProvideDocumentLinksSignature, RevealOutputChannelOn,
 } from 'vscode-languageclient';
 import WebRequest = require('web-request');
 import { GoDefinitionProvider } from './goDeclaration';
@@ -35,6 +35,7 @@
 import { GoWorkspaceSymbolProvider } from './goSymbol';
 import { getTool, Tool } from './goTools';
 import { GoTypeDefinitionProvider } from './goTypeDefinition';
+import { getFromGlobalState, updateGlobalState } from './stateUtils';
 import { getBinPath, getCurrentGoPath, getGoConfig, getToolsEnvVars } from './util';
 
 interface LanguageServerConfig {
@@ -124,7 +125,13 @@
 	// Set up the command to allow the user to manually restart the
 	// language server.
 	if (!restartCommand) {
-		restartCommand = vscode.commands.registerCommand('go.languageserver.restart', restartLanguageServer);
+		restartCommand = vscode.commands.registerCommand('go.languageserver.restart', async () => {
+			// TODO(rstambler): Enable this behavior when gopls reaches v1.0.
+			if (false) {
+				await suggestGoplsIssueReport(`Looks like you're about to manually restart the language server.`);
+			}
+			restartLanguageServer();
+		});
 		ctx.subscriptions.push(restartCommand);
 	}
 
@@ -161,6 +168,31 @@
 			},
 			outputChannel: serverOutputChannel,
 			revealOutputChannelOn: RevealOutputChannelOn.Never,
+			initializationFailedHandler: (error: WebRequest.ResponseError<InitializeError>): boolean => {
+				vscode.window.showErrorMessage(
+					`The language server is not able to serve any features. Initialization failed: ${error}. `
+				);
+				serverOutputChannel.show();
+				suggestGoplsIssueReport(`The gopls server failed to initialize.`);
+				return false;
+			},
+			errorHandler: {
+				error: (error: Error, message: Message, count: number): ErrorAction => {
+					vscode.window.showErrorMessage(
+						`Error communicating with the language server: ${error}: ${message}.`
+					);
+					// Stick with the default number of 5 crashes before shutdown.
+					if (count >= 5) {
+						return ErrorAction.Shutdown;
+					}
+					return ErrorAction.Continue;
+				},
+				closed: (): CloseAction => {
+					serverOutputChannel.show();
+					suggestGoplsIssueReport(`The connection to gopls has been closed. The gopls server may have crashed.`);
+					return CloseAction.DoNotRestart;
+				},
+			},
 			middleware: {
 				handleDiagnostics: (
 					uri: vscode.Uri,
@@ -237,14 +269,6 @@
 			}
 		}
 	);
-	c.onReady().then(() => {
-		const capabilities = languageClient.initializeResult && languageClient.initializeResult.capabilities;
-		if (!capabilities) {
-			return vscode.window.showErrorMessage(
-				'The language server is not able to serve any features at the moment.'
-			);
-		}
-	});
 	return c;
 }
 
@@ -621,3 +645,56 @@
 	}
 	return null;
 }
+
+// suggestGoplsIssueReport prompts users to file an issue with gopls.
+async function suggestGoplsIssueReport(msg: string) {
+	if (latestConfig.serverName !== 'gopls') {
+		return;
+	}
+	const promptForIssueOnGoplsRestartKey = `promptForIssueOnGoplsRestart`;
+	let saved: any;
+	try {
+		saved = JSON.parse(getFromGlobalState(promptForIssueOnGoplsRestartKey, true));
+	} catch (err) {
+		console.log(`Failed to parse as JSON ${getFromGlobalState(promptForIssueOnGoplsRestartKey, true)}: ${err}`);
+		return;
+	}
+	// If the user has already seen this prompt, they may have opted-out for
+	// the future. Only prompt again if it's been more than a year since.
+	if (saved['date'] && saved['prompt']) {
+		const dateSaved = new Date(saved['date']);
+		const prompt = <boolean>saved['prompt'];
+		if (!prompt && daysBetween(new Date(), dateSaved) <= 365) {
+			return;
+		}
+	}
+	const selected = await vscode.window.showInformationMessage(`${msg} Would you like to report a gopls issue?`, 'Yes', 'Next time', 'Never');
+	switch (selected) {
+		case 'Yes':
+			// Run the `gopls bug` command directly for now. When
+			// https://github.com/golang/go/issues/38942 is
+			// resolved, we'll be able to do this through the
+			// language client.
+
+			// Wait for the command to finish before restarting the
+			// server, but don't bother handling errors.
+			const execFile = util.promisify(cp.execFile);
+			await execFile(latestConfig.path, ['bug'], { env: getToolsEnvVars() });
+			break;
+		case 'Next time':
+			break;
+		case 'Never':
+			updateGlobalState(promptForIssueOnGoplsRestartKey, JSON.stringify({
+				prompt: false,
+				date: new Date(),
+			}));
+			break;
+	}
+}
+
+// daysBetween returns the number of days between a and b,
+// assuming that a occurs after b.
+function daysBetween(a: Date, b: Date) {
+	const ms = a.getTime() - b.getTime();
+	return ms / (1000 * 60 * 60 * 24);
+}
diff --git a/test/gopls/update.test.ts b/test/gopls/update.test.ts
index f9bf110..31f9ed9 100644
--- a/test/gopls/update.test.ts
+++ b/test/gopls/update.test.ts
@@ -4,7 +4,6 @@
  *--------------------------------------------------------*/
 
 import * as assert from 'assert';
-import moment = require('moment');
 import semver = require('semver');
 import sinon = require('sinon');
 import lsp = require('../../src/goLanguageServer');