src/stateUtils.ts: add command to reset memento state

It may sometimes be necessary to reset the memento state,
particularly for testing the extension's behavior when the
state is different.

Change-Id: I8cfed3b8b49a0f5064b4927e86772d32ea358a54
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/285679
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/docs/commands.md b/docs/commands.md
index 7777fab..9e4cbe5 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -207,6 +207,14 @@
 
 Reset the current Go survey configuration history
 
+### `Go: Reset Workspace State`
+
+Reset keys in workspace state to undefined.
+
+### `Go: Reset Global State`
+
+Reset keys in global state to undefined.
+
 ### `Go: Toggle Workspace Trust Flag`
 
 Toggle the workspace trust flag. Workspace settings that determine tool locations are disabled by default in untrusted workspaces.
diff --git a/package.json b/package.json
index 0556c31..a46cc30 100644
--- a/package.json
+++ b/package.json
@@ -402,6 +402,16 @@
         "description": "Reset the current Go survey configuration history"
       },
       {
+        "command": "go.workspace.resetState",
+        "title": "Go: Reset Workspace State",
+        "description": "Reset keys in workspace state to undefined."
+      },
+      {
+        "command": "go.global.resetState",
+        "title": "Go: Reset Global State",
+        "description": "Reset keys in global state to undefined."
+      },
+      {
         "command": "go.workspace.isTrusted.toggle",
         "title": "Go: Toggle Workspace Trust Flag",
         "description": "Toggle the workspace trust flag. Workspace settings that determine tool locations are disabled by default in untrusted workspaces."
diff --git a/src/goMain.ts b/src/goMain.ts
index b2c0b81..890a324 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -54,7 +54,13 @@
 import { vetCode } from './goVet';
 import { pickProcess } from './pickProcess';
 import {
-	getFromGlobalState, getFromWorkspaceState, setGlobalState, setWorkspaceState, updateGlobalState,
+	getFromGlobalState,
+	getFromWorkspaceState,
+	resetGlobalState,
+	resetWorkspaceState,
+	setGlobalState,
+	setWorkspaceState,
+	updateGlobalState,
 	updateWorkspaceState
 } from './stateUtils';
 import { cancelRunningTests, showTestOutput } from './testUtils';
@@ -507,10 +513,17 @@
 		showServerOutputChannel();
 	}));
 
-	ctx.subscriptions.push(
-		vscode.commands.registerCommand('go.welcome', () => {
-			WelcomePanel.createOrShow(ctx.extensionUri);
-		}));
+	ctx.subscriptions.push(vscode.commands.registerCommand('go.welcome', () => {
+		WelcomePanel.createOrShow(ctx.extensionUri);
+	}));
+
+	ctx.subscriptions.push(vscode.commands.registerCommand('go.workspace.resetState', () => {
+		resetWorkspaceState();
+	}));
+
+	ctx.subscriptions.push(vscode.commands.registerCommand('go.global.resetState', () => {
+		resetGlobalState();
+	}));
 
 	ctx.subscriptions.push(vscode.commands.registerCommand('go.toggle.gc_details', () => {
 		if (!languageServerIsRunning) {
diff --git a/src/stateUtils.ts b/src/stateUtils.ts
index cb57157..2f93a2f 100644
--- a/src/stateUtils.ts
+++ b/src/stateUtils.ts
@@ -30,6 +30,10 @@
 	return globalState;
 }
 
+export function resetGlobalState() {
+	resetStateQuickPick(globalState, updateGlobalState);
+}
+
 export function getFromWorkspaceState(key: string, defaultValue?: any) {
 	if (!workspaceState) {
 		return defaultValue;
@@ -51,3 +55,40 @@
 export function getWorkspaceState(): vscode.Memento {
 	return workspaceState;
 }
+
+export function resetWorkspaceState() {
+	const keys = getMementoKeys(workspaceState);
+	resetStateQuickPick(workspaceState, updateWorkspaceState);
+}
+
+export function getMementoKeys(state: vscode.Memento): string[] {
+	if (!state) {
+		return [];
+	}
+	// tslint:disable-next-line: no-empty
+	if ((state as any)._value) {
+		const keys = Object.keys((state as any)._value);
+		// Filter out keys with undefined values, so they are not shown
+		// in the quick pick menu.
+		return keys.filter((key) => state.get(key) !== undefined);
+	}
+	return [];
+}
+
+async function resetStateQuickPick(state: vscode.Memento, updateFn: (key: string, value: any) => {}) {
+	const items = await vscode.window.showQuickPick(
+		getMementoKeys(state),
+		{
+			canPickMany: true,
+			placeHolder: 'Select the keys to reset.'
+		}
+	);
+	resetItemsState(items, updateFn);
+}
+
+export function resetItemsState(items: string[], updateFn: (key: string, value: any) => {}) {
+	if (!items) {
+		return;
+	}
+	items.forEach((item) => updateFn(item, undefined));
+}
diff --git a/test/integration/stateUtils.test.ts b/test/integration/stateUtils.test.ts
new file mode 100644
index 0000000..cd80b12
--- /dev/null
+++ b/test/integration/stateUtils.test.ts
@@ -0,0 +1,100 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+import { getMementoKeys, getWorkspaceState, resetItemsState, setWorkspaceState, updateWorkspaceState } from '../../src/stateUtils';
+import { MockMemento } from '../mocks/MockMemento';
+
+suite('Workspace State Modification Tests', () => {
+	let defaultMemento: vscode.Memento;
+
+	setup(async () => {
+		defaultMemento = getWorkspaceState();
+	});
+
+	teardown(async () => {
+		setWorkspaceState(defaultMemento);
+	});
+
+	test('test getMementoKeys', () => {
+		interface TestCase {
+			keys: string[];
+			values: any[];
+			want: string[];
+		}
+		const testCases: TestCase[] = [
+			{keys: [], values: [], want: []},
+			{keys: ['hello'], values: [false], want: ['hello']},
+			{keys: ['hello', 'goodbye'], values: [false, 25], want: ['hello', 'goodbye']},
+		];
+
+		testCases.forEach((tc) => {
+			setWorkspaceState(new MockMemento());
+
+			const keys = tc.keys;
+			const values = tc.values;
+			assert.strictEqual(keys.length, values.length, 'List of keys and values does not have same length');
+
+			for (let i = 0; i < keys.length; i ++) {
+				updateWorkspaceState(keys[i], values[i]);
+			}
+
+			const got = getMementoKeys(getWorkspaceState());
+			const want = tc.want;
+
+			assert.strictEqual(got.length, tc.want.length);
+			got.forEach((key) => {
+				assert.ok(want.includes(key));
+			});
+		});
+	});
+
+	test('test resetItemsState', () => {
+		interface TestCase {
+			keys: string[];
+			values: any[];
+			items: string[];
+			want: string[];
+		}
+		const testCases: TestCase[] = [
+			{keys: [], values: [], items: undefined, want: []},
+			{keys: ['hello'], values: [false], items: undefined, want: ['hello']},
+			{keys: ['hello', 'goodbye'], values: [false, 25], items: undefined, want: ['hello', 'goodbye']},
+
+			{keys: [], values: [], items: [], want: []},
+			{keys: ['hello'], values: [false], items: [], want: ['hello']},
+			{keys: ['hello', 'goodbye'], values: [false, 25], items: [], want: ['hello', 'goodbye']},
+
+			{keys: ['hello'], values: [false], items: ['hello'], want: []},
+			{keys: ['hello', 'goodbye'], values: [false, 25], items: ['hello'], want: ['goodbye']},
+
+			{keys: ['hello'], values: [false], items: ['hello'], want: []},
+			{keys: ['hello', 'goodbye'], values: [false, 25], items: ['hello', 'goodbye'], want: []},
+		];
+
+		testCases.forEach((tc) => {
+			setWorkspaceState(new MockMemento());
+
+			const keys = tc.keys;
+			const values = tc.values;
+			assert.strictEqual(keys.length, values.length, 'List of keys and values does not have same length');
+
+			for (let i = 0; i < keys.length; i ++) {
+				updateWorkspaceState(keys[i], values[i]);
+			}
+
+			resetItemsState(tc.items, updateWorkspaceState);
+			const got = getMementoKeys(getWorkspaceState());
+			const want = tc.want;
+
+			assert.strictEqual(got.length, want.length);
+			got.forEach((key) => {
+				assert.ok(want.includes(key));
+			});
+		});
+	});
+
+});