goSurvey: pull survey logic out of goLanguageServer.ts

This is not intended to include any functional changes, just moving
code around.

Change-Id: I8f03eda081c3c19dad357efd256a0be86c62c597
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/315749
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index a6107a6..966483b 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -59,7 +59,7 @@
 import { GoWorkspaceSymbolProvider } from './goSymbol';
 import { getTool, Tool } from './goTools';
 import { GoTypeDefinitionProvider } from './goTypeDefinition';
-import { getFromGlobalState, getFromWorkspaceState, updateGlobalState, updateWorkspaceState } from './stateUtils';
+import { getFromGlobalState, updateGlobalState, updateWorkspaceState } from './stateUtils';
 import {
 	getBinPath,
 	getCheckForToolsUpdatesConfig,
@@ -73,6 +73,7 @@
 import WebRequest = require('web-request');
 import { FoldingContext } from 'vscode';
 import { ProvideFoldingRangeSignature } from 'vscode-languageclient/lib/common/foldingRange';
+import { daysBetween, getStateConfig, maybePromptForSurvey, timeDay, timeMinute } from './goSurvey';
 
 export interface LanguageServerConfig {
 	serverName: string;
@@ -116,7 +117,7 @@
 
 // lastUserAction is the time of the last user-triggered change.
 // A user-triggered change is a didOpen, didChange, didSave, or didClose event.
-let lastUserAction: Date = new Date();
+export let lastUserAction: Date = new Date();
 
 // startLanguageServerWithFallback starts the language server, if enabled,
 // or falls back to the default language providers.
@@ -1256,226 +1257,6 @@
 	return null;
 }
 
-// SurveyConfig is the set of global properties used to determine if
-// we should prompt a user to take the gopls survey.
-export interface SurveyConfig {
-	// prompt is true if the user can be prompted to take the survey.
-	// It is false if the user has responded "Never" to the prompt.
-	prompt?: boolean;
-
-	// promptThisMonth is true if we have used a random number generator
-	// to determine if the user should be prompted this month.
-	// It is undefined if we have not yet made the determination.
-	promptThisMonth?: boolean;
-
-	// dateToPromptThisMonth is the date on which we should prompt the user
-	// this month.
-	dateToPromptThisMonth?: Date;
-
-	// dateComputedPromptThisMonth is the date on which the values of
-	// promptThisMonth and dateToPromptThisMonth were set.
-	dateComputedPromptThisMonth?: Date;
-
-	// lastDatePrompted is the most recent date that the user has been prompted.
-	lastDatePrompted?: Date;
-
-	// lastDateAccepted is the most recent date that the user responded "Yes"
-	// to the survey prompt. The user need not have completed the survey.
-	lastDateAccepted?: Date;
-}
-
-function maybePromptForSurvey() {
-	const now = new Date();
-	let cfg = shouldPromptForSurvey(now, getSurveyConfig());
-	if (!cfg) {
-		return;
-	}
-	flushSurveyConfig(cfg);
-	if (!cfg.dateToPromptThisMonth) {
-		return;
-	}
-	const callback = async () => {
-		const currentTime = new Date();
-
-		// Make sure the user has been idle for at least a minute.
-		if (minutesBetween(lastUserAction, currentTime) < 1) {
-			setTimeout(callback, 5 * timeMinute);
-			return;
-		}
-		cfg = await promptForSurvey(cfg, now);
-		if (cfg) {
-			flushSurveyConfig(cfg);
-		}
-	};
-	const ms = msBetween(now, cfg.dateToPromptThisMonth);
-	setTimeout(callback, ms);
-}
-
-export function shouldPromptForSurvey(now: Date, cfg: SurveyConfig): SurveyConfig {
-	// If the prompt value is not set, assume we haven't prompted the user
-	// and should do so.
-	if (cfg.prompt === undefined) {
-		cfg.prompt = true;
-	}
-	if (!cfg.prompt) {
-		return;
-	}
-
-	// Check if the user has taken the survey in the last year.
-	// Don't prompt them if they have been.
-	if (cfg.lastDateAccepted) {
-		if (daysBetween(now, cfg.lastDateAccepted) < 365) {
-			return;
-		}
-	}
-
-	// Check if the user has been prompted for the survey in the last 90 days.
-	// Don't prompt them if they have been.
-	if (cfg.lastDatePrompted) {
-		if (daysBetween(now, cfg.lastDatePrompted) < 90) {
-			return;
-		}
-	}
-
-	// Check if the extension has been activated this month.
-	if (cfg.dateComputedPromptThisMonth) {
-		// The extension has been activated this month, so we should have already
-		// decided if the user should be prompted.
-		if (daysBetween(now, cfg.dateComputedPromptThisMonth) < 30) {
-			return cfg;
-		}
-	}
-	// This is the first activation this month (or ever), so decide if we
-	// should prompt the user. This is done by generating a random number in
-	// the range [0, 1) and checking if it is < probability, which varies
-	// depending on whether the default or the nightly is in use.
-	// We then randomly pick a day in the rest of the month on which to prompt
-	// the user.
-	let probability = 0.01; // lower probability for the regular extension
-	if (isInPreviewMode()) {
-		probability = 0.0275;
-	}
-	cfg.promptThisMonth = Math.random() < probability;
-	if (cfg.promptThisMonth) {
-		// end is the last day of the month, day is the random day of the
-		// month on which to prompt.
-		const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
-		const day = randomIntInRange(now.getUTCDate(), end.getUTCDate());
-		cfg.dateToPromptThisMonth = new Date(now.getFullYear(), now.getMonth(), day);
-	} else {
-		cfg.dateToPromptThisMonth = undefined;
-	}
-	cfg.dateComputedPromptThisMonth = now;
-	return cfg;
-}
-
-async function promptForSurvey(cfg: SurveyConfig, now: Date): Promise<SurveyConfig> {
-	const selected = await vscode.window.showInformationMessage(
-		`Looks like you are using the Go extension for VS Code.
-Could you help us improve this extension by filling out a 1-2 minute survey about your experience with it?`,
-		'Yes',
-		'Not now',
-		'Never'
-	);
-
-	// Update the time last asked.
-	cfg.lastDatePrompted = now;
-
-	switch (selected) {
-		case 'Yes':
-			{
-				cfg.lastDateAccepted = now;
-				cfg.prompt = true;
-				const goplsEnabled = latestConfig.enabled;
-				const usersGoplsVersion = await getLocalGoplsVersion(latestConfig);
-				await vscode.env.openExternal(
-					vscode.Uri.parse(
-						`https://google.qualtrics.com/jfe/form/SV_ekAdHVcVcvKUojX?usingGopls=${goplsEnabled}&gopls=${usersGoplsVersion}&extid=${extensionId}`
-					)
-				);
-			}
-			break;
-		case 'Not now':
-			cfg.prompt = true;
-
-			vscode.window.showInformationMessage("No problem! We'll ask you again another time.");
-			break;
-		case 'Never':
-			cfg.prompt = false;
-
-			vscode.window.showInformationMessage("No problem! We won't ask again.");
-			break;
-		default:
-			// If the user closes the prompt without making a selection, treat it
-			// like a "Not now" response.
-			cfg.prompt = true;
-
-			break;
-	}
-	return cfg;
-}
-
-export const goplsSurveyConfig = 'goplsSurveyConfig';
-
-function getSurveyConfig(): SurveyConfig {
-	return getStateConfig(goplsSurveyConfig) as SurveyConfig;
-}
-
-export function resetSurveyConfig() {
-	flushSurveyConfig(null);
-}
-
-function flushSurveyConfig(cfg: SurveyConfig) {
-	if (cfg) {
-		updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
-	} else {
-		updateGlobalState(goplsSurveyConfig, null); // reset
-	}
-}
-
-function getStateConfig(globalStateKey: string, workspace?: boolean): any {
-	let saved: any;
-	if (workspace === true) {
-		saved = getFromWorkspaceState(globalStateKey);
-	} else {
-		saved = getFromGlobalState(globalStateKey);
-	}
-	if (saved === undefined) {
-		return {};
-	}
-	try {
-		const cfg = JSON.parse(saved, (key: string, value: any) => {
-			// Make sure values that should be dates are correctly converted.
-			if (key.toLowerCase().includes('date') || key.toLowerCase().includes('timestamp')) {
-				return new Date(value);
-			}
-			return value;
-		});
-		return cfg || {};
-	} catch (err) {
-		console.log(`Error parsing JSON from ${saved}: ${err}`);
-		return {};
-	}
-}
-
-export async function showSurveyConfig() {
-	outputChannel.appendLine('Gopls Survey Configuration');
-	outputChannel.appendLine(JSON.stringify(getSurveyConfig(), null, 2));
-	outputChannel.show();
-
-	const selected = await vscode.window.showInformationMessage('Prompt for survey?', 'Yes', 'Maybe', 'No');
-	switch (selected) {
-		case 'Yes':
-			promptForSurvey(getSurveyConfig(), new Date());
-			break;
-		case 'Maybe':
-			maybePromptForSurvey();
-			break;
-		default:
-			break;
-	}
-}
-
 // errorKind refers to the different possible kinds of gopls errors.
 enum errorKind {
 	initializationFailure,
@@ -1635,31 +1416,6 @@
 	}
 }
 
-// randomIntInRange returns a random integer between min and max, inclusive.
-function randomIntInRange(min: number, max: number): number {
-	const low = Math.ceil(min);
-	const high = Math.floor(max);
-	return Math.floor(Math.random() * (high - low + 1)) + low;
-}
-
-export const timeMinute = 1000 * 60;
-const timeHour = timeMinute * 60;
-const timeDay = timeHour * 24;
-
-// daysBetween returns the number of days between a and b.
-function daysBetween(a: Date, b: Date): number {
-	return msBetween(a, b) / timeDay;
-}
-
-// minutesBetween returns the number of minutes between a and b.
-function minutesBetween(a: Date, b: Date): number {
-	return msBetween(a, b) / timeMinute;
-}
-
-function msBetween(a: Date, b: Date): number {
-	return Math.abs(a.getTime() - b.getTime());
-}
-
 export function showServerOutputChannel() {
 	if (!languageServerIsRunning) {
 		vscode.window.showInformationMessage('gopls is not running');
diff --git a/src/goMain.ts b/src/goMain.ts
index 5e1a3c6..0fcd4e5 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -47,11 +47,8 @@
 import {
 	isInPreviewMode,
 	languageServerIsRunning,
-	resetSurveyConfig,
 	showServerOutputChannel,
-	showSurveyConfig,
 	startLanguageServerWithFallback,
-	timeMinute,
 	watchLanguageServerConfiguration
 } from './goLanguageServer';
 import { lintCode } from './goLint';
@@ -104,6 +101,7 @@
 import semver = require('semver');
 import vscode = require('vscode');
 import { getFormatTool } from './goFormat';
+import { resetSurveyConfig, showSurveyConfig, timeMinute } from './goSurvey';
 
 export let buildDiagnosticCollection: vscode.DiagnosticCollection;
 export let lintDiagnosticCollection: vscode.DiagnosticCollection;
diff --git a/src/goSurvey.ts b/src/goSurvey.ts
new file mode 100644
index 0000000..eb02760
--- /dev/null
+++ b/src/goSurvey.ts
@@ -0,0 +1,258 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+'use strict';
+
+import vscode = require('vscode');
+import { getLocalGoplsVersion, isInPreviewMode, lastUserAction, latestConfig } from './goLanguageServer';
+import { outputChannel } from './goStatus';
+import { extensionId } from './const';
+import { getFromGlobalState, getFromWorkspaceState, updateGlobalState } from './stateUtils';
+
+// SurveyConfig is the set of global properties used to determine if
+// we should prompt a user to take the gopls survey.
+export interface SurveyConfig {
+	// prompt is true if the user can be prompted to take the survey.
+	// It is false if the user has responded "Never" to the prompt.
+	prompt?: boolean;
+
+	// promptThisMonth is true if we have used a random number generator
+	// to determine if the user should be prompted this month.
+	// It is undefined if we have not yet made the determination.
+	promptThisMonth?: boolean;
+
+	// dateToPromptThisMonth is the date on which we should prompt the user
+	// this month.
+	dateToPromptThisMonth?: Date;
+
+	// dateComputedPromptThisMonth is the date on which the values of
+	// promptThisMonth and dateToPromptThisMonth were set.
+	dateComputedPromptThisMonth?: Date;
+
+	// lastDatePrompted is the most recent date that the user has been prompted.
+	lastDatePrompted?: Date;
+
+	// lastDateAccepted is the most recent date that the user responded "Yes"
+	// to the survey prompt. The user need not have completed the survey.
+	lastDateAccepted?: Date;
+}
+
+export function maybePromptForSurvey() {
+	const now = new Date();
+	let cfg = shouldPromptForSurvey(now, getSurveyConfig());
+	if (!cfg) {
+		return;
+	}
+	flushSurveyConfig(cfg);
+	if (!cfg.dateToPromptThisMonth) {
+		return;
+	}
+	const callback = async () => {
+		const currentTime = new Date();
+
+		// Make sure the user has been idle for at least a minute.
+		if (minutesBetween(lastUserAction, currentTime) < 1) {
+			setTimeout(callback, 5 * timeMinute);
+			return;
+		}
+		cfg = await promptForSurvey(cfg, now);
+		if (cfg) {
+			flushSurveyConfig(cfg);
+		}
+	};
+	const ms = msBetween(now, cfg.dateToPromptThisMonth);
+	setTimeout(callback, ms);
+}
+
+export function shouldPromptForSurvey(now: Date, cfg: SurveyConfig): SurveyConfig {
+	// If the prompt value is not set, assume we haven't prompted the user
+	// and should do so.
+	if (cfg.prompt === undefined) {
+		cfg.prompt = true;
+	}
+	if (!cfg.prompt) {
+		return;
+	}
+
+	// Check if the user has taken the survey in the last year.
+	// Don't prompt them if they have been.
+	if (cfg.lastDateAccepted) {
+		if (daysBetween(now, cfg.lastDateAccepted) < 365) {
+			return;
+		}
+	}
+
+	// Check if the user has been prompted for the survey in the last 90 days.
+	// Don't prompt them if they have been.
+	if (cfg.lastDatePrompted) {
+		if (daysBetween(now, cfg.lastDatePrompted) < 90) {
+			return;
+		}
+	}
+
+	// Check if the extension has been activated this month.
+	if (cfg.dateComputedPromptThisMonth) {
+		// The extension has been activated this month, so we should have already
+		// decided if the user should be prompted.
+		if (daysBetween(now, cfg.dateComputedPromptThisMonth) < 30) {
+			return cfg;
+		}
+	}
+	// This is the first activation this month (or ever), so decide if we
+	// should prompt the user. This is done by generating a random number in
+	// the range [0, 1) and checking if it is < probability, which varies
+	// depending on whether the default or the nightly is in use.
+	// We then randomly pick a day in the rest of the month on which to prompt
+	// the user.
+	let probability = 0.01; // lower probability for the regular extension
+	if (isInPreviewMode()) {
+		probability = 0.0275;
+	}
+	cfg.promptThisMonth = Math.random() < probability;
+	if (cfg.promptThisMonth) {
+		// end is the last day of the month, day is the random day of the
+		// month on which to prompt.
+		const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
+		const day = randomIntInRange(now.getUTCDate(), end.getUTCDate());
+		cfg.dateToPromptThisMonth = new Date(now.getFullYear(), now.getMonth(), day);
+	} else {
+		cfg.dateToPromptThisMonth = undefined;
+	}
+	cfg.dateComputedPromptThisMonth = now;
+	return cfg;
+}
+
+// randomIntInRange returns a random integer between min and max, inclusive.
+function randomIntInRange(min: number, max: number): number {
+	const low = Math.ceil(min);
+	const high = Math.floor(max);
+	return Math.floor(Math.random() * (high - low + 1)) + low;
+}
+
+async function promptForSurvey(cfg: SurveyConfig, now: Date): Promise<SurveyConfig> {
+	const selected = await vscode.window.showInformationMessage(
+		`Looks like you are using the Go extension for VS Code.
+Could you help us improve this extension by filling out a 1-2 minute survey about your experience with it?`,
+		'Yes',
+		'Not now',
+		'Never'
+	);
+
+	// Update the time last asked.
+	cfg.lastDatePrompted = now;
+
+	switch (selected) {
+		case 'Yes':
+			{
+				cfg.lastDateAccepted = now;
+				cfg.prompt = true;
+				const goplsEnabled = latestConfig.enabled;
+				const usersGoplsVersion = await getLocalGoplsVersion(latestConfig);
+				await vscode.env.openExternal(
+					vscode.Uri.parse(
+						`https://google.qualtrics.com/jfe/form/SV_ekAdHVcVcvKUojX?usingGopls=${goplsEnabled}&gopls=${usersGoplsVersion}&extid=${extensionId}`
+					)
+				);
+			}
+			break;
+		case 'Not now':
+			cfg.prompt = true;
+
+			vscode.window.showInformationMessage("No problem! We'll ask you again another time.");
+			break;
+		case 'Never':
+			cfg.prompt = false;
+
+			vscode.window.showInformationMessage("No problem! We won't ask again.");
+			break;
+		default:
+			// If the user closes the prompt without making a selection, treat it
+			// like a "Not now" response.
+			cfg.prompt = true;
+
+			break;
+	}
+	return cfg;
+}
+
+export const goplsSurveyConfig = 'goplsSurveyConfig';
+
+function getSurveyConfig(): SurveyConfig {
+	return getStateConfig(goplsSurveyConfig) as SurveyConfig;
+}
+
+export function resetSurveyConfig() {
+	flushSurveyConfig(null);
+}
+
+function flushSurveyConfig(cfg: SurveyConfig) {
+	if (cfg) {
+		updateGlobalState(goplsSurveyConfig, JSON.stringify(cfg));
+	} else {
+		updateGlobalState(goplsSurveyConfig, null); // reset
+	}
+}
+
+export function getStateConfig(globalStateKey: string, workspace?: boolean): any {
+	let saved: any;
+	if (workspace === true) {
+		saved = getFromWorkspaceState(globalStateKey);
+	} else {
+		saved = getFromGlobalState(globalStateKey);
+	}
+	if (saved === undefined) {
+		return {};
+	}
+	try {
+		const cfg = JSON.parse(saved, (key: string, value: any) => {
+			// Make sure values that should be dates are correctly converted.
+			if (key.toLowerCase().includes('date') || key.toLowerCase().includes('timestamp')) {
+				return new Date(value);
+			}
+			return value;
+		});
+		return cfg || {};
+	} catch (err) {
+		console.log(`Error parsing JSON from ${saved}: ${err}`);
+		return {};
+	}
+}
+
+export async function showSurveyConfig() {
+	outputChannel.appendLine('Gopls Survey Configuration');
+	outputChannel.appendLine(JSON.stringify(getSurveyConfig(), null, 2));
+	outputChannel.show();
+
+	const selected = await vscode.window.showInformationMessage('Prompt for survey?', 'Yes', 'Maybe', 'No');
+	switch (selected) {
+		case 'Yes':
+			promptForSurvey(getSurveyConfig(), new Date());
+			break;
+		case 'Maybe':
+			maybePromptForSurvey();
+			break;
+		default:
+			break;
+	}
+}
+
+export const timeMinute = 1000 * 60;
+const timeHour = timeMinute * 60;
+export const timeDay = timeHour * 24;
+
+// daysBetween returns the number of days between a and b.
+export function daysBetween(a: Date, b: Date): number {
+	return msBetween(a, b) / timeDay;
+}
+
+// minutesBetween returns the number of minutes between a and b.
+function minutesBetween(a: Date, b: Date): number {
+	return msBetween(a, b) / timeMinute;
+}
+
+export function msBetween(a: Date, b: Date): number {
+	return Math.abs(a.getTime() - b.getTime());
+}
diff --git a/test/gopls/survey.test.ts b/test/gopls/survey.test.ts
index f065510..3a32954 100644
--- a/test/gopls/survey.test.ts
+++ b/test/gopls/survey.test.ts
@@ -7,11 +7,12 @@
 import sinon = require('sinon');
 import vscode = require('vscode');
 import goLanguageServer = require('../../src/goLanguageServer');
+import goSurvey = require('../../src/goSurvey');
 
 suite('gopls survey tests', () => {
 	test('prompt for survey', () => {
 		// global state -> offer survey
-		const testCases: [goLanguageServer.SurveyConfig, boolean][] = [
+		const testCases: [goSurvey.SurveyConfig, boolean][] = [
 			// User who is activating the extension for the first time.
 			[{}, true],
 			// User who has already taken the survey.
@@ -70,7 +71,7 @@
 			sinon.replace(Math, 'random', () => 0);
 
 			const now = new Date('2020-04-29');
-			const gotPrompt = goLanguageServer.shouldPromptForSurvey(now, testConfig);
+			const gotPrompt = goSurvey.shouldPromptForSurvey(now, testConfig);
 			if (wantPrompt) {
 				assert.ok(gotPrompt, `prompt determination failed for ${i}`);
 			} else {