goLanguageServer: use a webview for the survey

This change restructures a bit of the survey logic so that we check if
we should prompt for the survey once a day, regardless of activation.
The same goes for update prompts.

Also, we use a webview for the survey instead of opening a new browser
window.

Change-Id: I812bb11b78cdfed7a2086606f96dbe745c6f24b0
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/241197
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index 969ebff..6b2c53b 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -82,24 +82,12 @@
 export async function startLanguageServerWithFallback(ctx: vscode.ExtensionContext, activation: boolean) {
 	const cfg = buildLanguageServerConfig();
 
-	// If the language server is gopls, we can check if the user needs to
-	// update their gopls version. We do this only once per VS Code
-	// activation to avoid inundating the user.
-	if (activation && cfg.enabled && cfg.serverName === 'gopls') {
+	// If the language server is gopls, we enable a few additional features.
+	// These include prompting for updates and surveys.
+	if (activation && 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);
-			}
+			scheduleGoplsSuggestions(tool);
 		}
 	}
 
@@ -113,6 +101,44 @@
 	}
 }
 
+// scheduleGoplsSuggestions sets timeouts for the various gopls-specific
+// suggestions. We check user's gopls versions once per day to prompt users to
+// update to the latest version. We also check if we should prompt users to
+// fill out the survey.
+function scheduleGoplsSuggestions(tool: Tool) {
+	const minute = 1000 * 60;
+	const hour = minute * 60;
+	const day = hour * 24;
+
+	const update = async () => {
+		setTimeout(update, day);
+
+		const cfg = buildLanguageServerConfig();
+		if (!cfg.enabled) {
+			return;
+		}
+		const versionToUpdate = await shouldUpdateLanguageServer(tool, cfg);
+		if (versionToUpdate) {
+			promptForUpdatingTool(tool.name, versionToUpdate);
+		}
+	};
+	const survey = async () => {
+		setTimeout(survey, day);
+
+		const cfg = buildLanguageServerConfig();
+		if (!goplsSurveyOn || !cfg.enabled) {
+			return;
+		}
+		const surveyCfg = await maybePromptForGoplsSurvey();
+		if (surveyCfg) {
+			flushSurveyConfig(surveyCfg);
+		}
+	};
+
+	setTimeout(update, 10 * minute);
+	setTimeout(survey, hour);
+}
+
 async function startLanguageServer(ctx: vscode.ExtensionContext, config: LanguageServerConfig): Promise<boolean> {
 	// If the client has already been started, make sure to clear existing
 	// diagnostics and stop it.
@@ -465,8 +491,7 @@
 
 export async function shouldUpdateLanguageServer(
 	tool: Tool,
-	languageServerToolPath: string,
-	makeProxyCall: boolean
+	cfg: LanguageServerConfig,
 ): Promise<semver.SemVer> {
 	// Only support updating gopls for now.
 	if (tool.name !== 'gopls') {
@@ -474,7 +499,9 @@
 	}
 
 	// First, run the "gopls version" command and parse its results.
-	const usersVersion = await getLocalGoplsVersion(languageServerToolPath);
+	// TODO(rstambler): Confirm that the gopls binary's modtime matches the
+	// modtime in the config. Update it if needed.
+	const usersVersion = await getLocalGoplsVersion(cfg);
 
 	// We might have a developer version. Don't make the user update.
 	if (usersVersion === '(devel)') {
@@ -482,7 +509,7 @@
 	}
 
 	// Get the latest gopls version. If it is for nightly, using the prereleased version is ok.
-	let latestVersion = makeProxyCall ? await getLatestGoplsVersion(tool) : tool.latestVersion;
+	let latestVersion = cfg.checkForUpdates ? await getLatestGoplsVersion(tool) : tool.latestVersion;
 
 	// If we failed to get the gopls version, pick the one we know to be latest at the time of this extension's last update
 	if (!latestVersion) {
@@ -501,7 +528,8 @@
 	const usersTime = parseTimestampFromPseudoversion(usersVersion);
 	// If the user has a pseudoversion, get the timestamp for the latest gopls version and compare.
 	if (usersTime) {
-		let latestTime = makeProxyCall ? await getTimestampForVersion(tool, latestVersion) : tool.latestVersionTimestamp;
+		let latestTime = cfg.checkForUpdates ?
+			await getTimestampForVersion(tool, latestVersion) : tool.latestVersionTimestamp;
 		if (!latestTime) {
 			latestTime = tool.latestVersionTimestamp;
 		}
@@ -604,11 +632,11 @@
 // getLocalGoplsVersion returns the version of gopls that is currently
 // installed on the user's machine. This is determined by running the
 // `gopls version` command.
-export const getLocalGoplsVersion = async (goplsPath: string) => {
+export const getLocalGoplsVersion = async (cfg: LanguageServerConfig) => {
 	const execFile = util.promisify(cp.execFile);
 	let output: any;
 	try {
-		const { stdout } = await execFile(goplsPath, ['version'], { env: toolExecutionEnvironment() });
+		const { stdout } = await execFile(cfg.path, ['version'], { env: toolExecutionEnvironment() });
 		output = stdout;
 	} catch (e) {
 		// The "gopls version" command is not supported, or something else went wrong.
@@ -730,8 +758,19 @@
 			cfg.lastDateAccepted = now;
 			cfg.prompt = true;
 
-			// Open the link to the survey.
-			vscode.env.openExternal(vscode.Uri.parse('https://www.whattimeisitrightnow.com/'));
+			// Open the link to the survey in a webview.
+			const panel = vscode.window.createWebviewPanel('goplsSurvey', 'gopls survey', {
+				viewColumn: null,
+				preserveFocus: false,
+			}, {});
+
+			// TODO(rstambler): Not sure how to set the correct height here.
+			panel.webview.html = `<!DOCTYPE html>
+<html>
+<body>
+<iframe width="100%" height="500px" src="https://golang.org"></iframe>
+</body>
+</html>`;
 			break;
 		case 'Not now':
 			cfg.prompt = true;
diff --git a/test/gopls/update.test.ts b/test/gopls/update.test.ts
index bac3aaf..031bea5 100644
--- a/test/gopls/update.test.ts
+++ b/test/gopls/update.test.ts
@@ -101,7 +101,19 @@
 					return latestPrereleaseVersionTimestamp;
 				}
 			});
-			const got = await lsp.shouldUpdateLanguageServer(tool, 'bad/path/to/gopls', true);
+			const got = await lsp.shouldUpdateLanguageServer(tool, {
+				enabled: true,
+				path: 'bad/path/to/gopls',
+				checkForUpdates: true,
+				env: {},
+				features: {
+					diagnostics: true,
+					documentLink: true,
+				},
+				flags: [],
+				modtime: new Date(),
+				serverName: 'gopls',
+			});
 			assert.deepEqual(got, want, `${name}: failed (got: '${got}' ${typeof got} want: '${want}' ${typeof want})`);
 			sinon.restore();
 		}