src/goCover.go: add go.coverMode to display coverage data and counts

Provide an option, go.coverMode, to change the value for -covermode.
Provide the option for the user to display borders around covered and
uncovered code. These are named 'coveredBorderColor' and
'unocveredBorderColor' and default to the same colors used as
backgrounds. The boolean option go.coverShowCounts show how often each
block of code was executed by the tests.

Fixes golang/vscode-go#256.

Change-Id: I551e0682d0db1e31dc07a2ae9b44ad94f66bbf18
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/247769
Run-TryBot: Peter Weinberger <pjw@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/docs/settings.md b/docs/settings.md
index 04ba5f7..cd4881d 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -61,9 +61,17 @@
 
 If true, shows test coverage when Go: Test Package command is run.
 
+### `go.coverMode`
+
+When generating code coverage, the value for -covermode: 'set', 'count', 'atomic'
+
+### `go.coverShowCounts`
+
+When generating code coverage, should counts be shown as --374--
+
 ### `go.coverageDecorator`
 
-This option lets you choose the way to display code coverage. Choose either to highlight the complete line or to show a decorator in the gutter. You can customize the color for the former and the style for the latter.
+This option lets you choose the way to display code coverage. Choose either to highlight the complete line or to show a decorator in the gutter. You can customize the color and border for the former and the style for the latter.
 
 ### `go.coverageOptions`
 
diff --git a/package.json b/package.json
index 4ea6b76..8d0df68 100644
--- a/package.json
+++ b/package.json
@@ -1309,6 +1309,22 @@
           "default": false,
           "description": "If true, shows test coverage when Go: Test Single File command is run."
         },
+        "go.coverMode": {
+          "type": "string",
+          "enum:": [
+            "set",
+            "count",
+            "atomic"
+          ],
+          "default": "set",
+          "description": "When generating code coverage, the value for -covermode: 'set', 'count', 'atomic'",
+          "scope": "resource"
+        },
+        "go.coverShowCounts": {
+          "type": "boolean",
+          "default": false,
+          "description": "When generating code coverage, should counts be shown as --374--"
+        },
         "go.coverageOptions": {
           "type": "string",
           "enum": [
@@ -1325,7 +1341,6 @@
           "properties": {
             "type": {
               "type": "string",
-              "default": "highlight",
               "enum": [
                 "highlight",
                 "gutter"
@@ -1333,17 +1348,22 @@
             },
             "coveredHighlightColor": {
               "type": "string",
-              "default": "rgba(64,128,128,0.5)",
               "description": "Color in the rgba format to use to highlight covered code."
             },
             "uncoveredHighlightColor": {
               "type": "string",
-              "default": "rgba(128,64,64,0.25)",
               "description": "Color in the rgba format to use to highlight uncovered code."
             },
+            "coveredBorderColor": {
+              "type": "string",
+              "description": "Color to use for the border of covered code."
+            },
+            "uncoveredBorderColor": {
+              "type": "string",
+              "description": "Color to use for the border of uncovered code."
+            },
             "coveredGutterStyle": {
               "type": "string",
-              "default": "blockblue",
               "enum": [
                 "blockblue",
                 "blockred",
@@ -1362,7 +1382,6 @@
             },
             "uncoveredGutterStyle": {
               "type": "string",
-              "default": "blockblue",
               "enum": [
                 "blockblue",
                 "blockred",
@@ -1385,10 +1404,12 @@
             "type": "highlight",
             "coveredHighlightColor": "rgba(64,128,128,0.5)",
             "uncoveredHighlightColor": "rgba(128,64,64,0.25)",
+            "coveredBorderColor": "rgba(64,128,128,0.5)",
+            "uncoveredBorderColor": "rgba(128,64,64,0.25)",
             "coveredGutterStyle": "blockblue",
             "uncoveredGutterStyle": "slashyellow"
           },
-          "description": "This option lets you choose the way to display code coverage. Choose either to highlight the complete line or to show a decorator in the gutter. You can customize the color for the former and the style for the latter.",
+          "description": "This option lets you choose the way to display code coverage. Choose either to highlight the complete line or to show a decorator in the gutter. You can customize the colors for the former and the style for the latter.",
           "scope": "resource"
         },
         "go.testTimeout": {
@@ -2011,4 +2032,4 @@
       ]
     }
   }
-}
+}
\ No newline at end of file
diff --git a/src/goCover.ts b/src/goCover.ts
index b1d3994..b0d6c8d 100644
--- a/src/goCover.ts
+++ b/src/goCover.ts
@@ -13,19 +13,28 @@
 import { getTestFlags, goTest, showTestOutput, TestConfig } from './testUtils';
 import { getGoConfig } from './util';
 
-let gutterSvgs: { [key: string]: string };
+let gutterSvgs: { [key: string]: string; };
+
+interface Highlight {
+	top: vscode.TextEditorDecorationType;
+	mid: vscode.TextEditorDecorationType;
+	bot: vscode.TextEditorDecorationType;
+	all: vscode.TextEditorDecorationType;
+}
 let decorators: {
-	type: string;
-	coveredGutterDecorator: vscode.TextEditorDecorationType;
-	uncoveredGutterDecorator: vscode.TextEditorDecorationType;
-	coveredHighlightDecorator: vscode.TextEditorDecorationType;
-	uncoveredHighlightDecorator: vscode.TextEditorDecorationType;
+	type: 'highlight' | 'gutter';
+	coveredGutter: vscode.TextEditorDecorationType;
+	uncoveredGutter: vscode.TextEditorDecorationType;
+	coveredHighlight: Highlight;
+	uncoveredHighlight: Highlight;
 };
 let decoratorConfig: {
 	[key: string]: any;
-	type: string;
+	type: 'highlight' | 'gutter';
 	coveredHighlightColor: string;
 	uncoveredHighlightColor: string;
+	coveredBorderColor: string;
+	uncoveredBorderColor: string;
 	coveredGutterStyle: string;
 	uncoveredGutterStyle: string;
 };
@@ -57,6 +66,7 @@
 	};
 
 	// Update the coverageDecorator in User config, if they are using the old style.
+	// Maybe it is time to deprecate the old style, and send warnings for a release or two.
 	const goConfig = getGoConfig();
 	const inspectResult = goConfig.inspect('coverageDecorator');
 	if (inspectResult) {
@@ -94,23 +104,29 @@
 export function updateCodeCoverageDecorators(coverageDecoratorConfig: any) {
 	// These defaults are chosen to be distinguishable in nearly any color scheme (even Red)
 	// as well as by people who have difficulties with color perception.
-	// (how do these relate the defaults in package.json?)
-	// and where do the defaults actually come from? (raised as issue #256)
+	// It appears that the contributions in package.json are only used to check what users
+	// put in settings.json, while the defaults come from the defaults section of
+	// go.coverageDecorator in package.json.
 	decoratorConfig = {
 		type: 'highlight',
 		coveredHighlightColor: 'rgba(64,128,128,0.5)',
+		coveredBorderColor: 'rgba(64,128,128,1.0)',
 		uncoveredHighlightColor: 'rgba(128,64,64,0.25)',
+		uncoveredBorderColor: 'rgba(128,64,64,1.0)',
 		coveredGutterStyle: 'blockblue',
 		uncoveredGutterStyle: 'slashyellow'
 	};
 
-	// Update from configuration
-	if (typeof coverageDecoratorConfig === 'string') {
+	// Update from configuration. First case is obsolete; we should warn the user.
+	if (typeof coverageDecoratorConfig === 'string' &&
+		(coverageDecoratorConfig === 'highlight' || coverageDecoratorConfig === 'gutter')) {
 		decoratorConfig.type = coverageDecoratorConfig;
 	} else {
 		for (const k in coverageDecoratorConfig) {
 			if (coverageDecoratorConfig.hasOwnProperty(k)) {
 				decoratorConfig[k] = coverageDecoratorConfig[k];
+			} else {
+				vscode.window.showWarningMessage(`unknown coverage parameter ${k}`);
 			}
 		}
 	}
@@ -120,20 +136,53 @@
 
 function setDecorators() {
 	disposeDecorators();
+	if (!decorators) { initForTest(); } // only happens in tests
+	const f = (x: { overviewRulerColor: string, backgroundColor: string; }, arg: string) => {
+		const y = {
+			overviewRulerLane: 2,
+			borderStyle: arg,
+			borderWidth: '2px',
+		};
+		return Object.assign(y, x);
+	};
+	const cov = {
+		overviewRulerColor: 'green',
+		backgroundColor: decoratorConfig.coveredHighlightColor,
+		borderColor: decoratorConfig.coveredBorderColor
+	};
+	const uncov = {
+		overviewRulerColor: 'red',
+		backgroundColor: decoratorConfig.uncoveredHighlightColor,
+		borderColor: decoratorConfig.uncoveredBorderColor
+	};
+	const ctop = f(cov, 'solid solid none solid');
+	const cmid = f(cov, 'none solid none solid');
+	const cbot = f(cov, 'none solid solid solid');
+	const cone = f(cov, 'solid solid solid solid');
+	const utop = f(uncov, 'solid solid none solid');
+	const umid = f(uncov, 'none solid none solid');
+	const ubot = f(uncov, 'none solid solid solid');
+	const uone = f(uncov, 'solid solid solid solid');
 	decorators = {
 		type: decoratorConfig.type,
-		coveredGutterDecorator: vscode.window.createTextEditorDecorationType({
+		coveredGutter: vscode.window.createTextEditorDecorationType({
 			gutterIconPath: gutterSvgs[decoratorConfig.coveredGutterStyle]
 		}),
-		uncoveredGutterDecorator: vscode.window.createTextEditorDecorationType({
+		uncoveredGutter: vscode.window.createTextEditorDecorationType({
 			gutterIconPath: gutterSvgs[decoratorConfig.uncoveredGutterStyle]
 		}),
-		coveredHighlightDecorator: vscode.window.createTextEditorDecorationType({
-			backgroundColor: decoratorConfig.coveredHighlightColor
-		}),
-		uncoveredHighlightDecorator: vscode.window.createTextEditorDecorationType({
-			backgroundColor: decoratorConfig.uncoveredHighlightColor
-		})
+		coveredHighlight: {
+			all: vscode.window.createTextEditorDecorationType(cone),
+			top: vscode.window.createTextEditorDecorationType(ctop),
+			mid: vscode.window.createTextEditorDecorationType(cmid),
+			bot: vscode.window.createTextEditorDecorationType(cbot),
+		},
+		uncoveredHighlight: {
+			all: vscode.window.createTextEditorDecorationType(uone),
+			top: vscode.window.createTextEditorDecorationType(utop),
+			mid: vscode.window.createTextEditorDecorationType(umid),
+			bot: vscode.window.createTextEditorDecorationType(ubot)
+		},
 	};
 }
 
@@ -142,21 +191,34 @@
  */
 function disposeDecorators() {
 	if (decorators) {
-		decorators.coveredGutterDecorator.dispose();
-		decorators.uncoveredGutterDecorator.dispose();
-		decorators.coveredHighlightDecorator.dispose();
-		decorators.uncoveredHighlightDecorator.dispose();
+		decorators.coveredGutter.dispose();
+		decorators.uncoveredGutter.dispose();
+		decorators.coveredHighlight.all.dispose();
+		decorators.coveredHighlight.top.dispose();
+		decorators.coveredHighlight.mid.dispose();
+		decorators.coveredHighlight.bot.dispose();
+		decorators.uncoveredHighlight.all.dispose();
+		decorators.uncoveredHighlight.top.dispose();
+		decorators.uncoveredHighlight.mid.dispose();
+		decorators.uncoveredHighlight.bot.dispose();
 	}
 }
 
 interface CoverageData {
-	uncoveredRange: vscode.Range[];
-	coveredRange: vscode.Range[];
+	uncoveredOptions: vscode.DecorationOptions[];
+	coveredOptions: vscode.DecorationOptions[];
 }
 
-let coverageData: { [key: string]: CoverageData } = {};  // actual file path to the coverage data.
+let coverageData: { [key: string]: CoverageData; } = {};  // actual file path to the coverage data.
 let isCoverageApplied: boolean = false;
 
+function emptyCoverageData(): CoverageData {
+	return {
+		uncoveredOptions: [],
+		coveredOptions: []
+	};
+}
+
 /**
  * Clear the coverage on all files
  */
@@ -175,6 +237,7 @@
 export function applyCodeCoverageToAllEditors(coverProfilePath: string, testDir?: string): Promise<void> {
 	const v = new Promise<void>((resolve, reject) => {
 		try {
+			const showCounts = getGoConfig().get('coverShowCounts') as boolean;
 			const coveragePath = new Map<string, CoverageData>();  // <filename> from the cover profile to the coverage data.
 
 			// Clear existing coverage files
@@ -200,33 +263,31 @@
 				}
 
 				// and fill in coveragePath
-				const coverage = coveragePath.get(parse[1]) || { coveredRange: [], uncoveredRange: [] };
+				const coverage = coveragePath.get(parse[1]) || emptyCoverageData();
 				const range = new vscode.Range(
-					// Start Line converted to zero based
+					// Convert lines and columns to 0-based
 					parseInt(parse[2], 10) - 1,
-					// Start Column converted to zero based
 					parseInt(parse[3], 10) - 1,
-					// End Line converted to zero based
 					parseInt(parse[4], 10) - 1,
-					// End Column converted to zero based
 					parseInt(parse[5], 10) - 1
 				);
+				const counts = parseInt(parse[7], 10);
 				// If is Covered (CoverCount > 0)
-				if (parseInt(parse[7], 10) > 0) {
-					coverage.coveredRange.push(range);
+				if (counts > 0) {
+					coverage.coveredOptions.push(...elaborate(range, counts, showCounts));
 				} else {
-					coverage.uncoveredRange.push(range);
+					coverage.uncoveredOptions.push(...elaborate(range, counts, showCounts));
 				}
 				coveragePath.set(parse[1], coverage);
 			});
 
 			getImportPathToFolder([...seenPaths], testDir)
 				.then((pathsToDirs) => {
-				createCoverageData(pathsToDirs, coveragePath);
-				setDecorators();
-				vscode.window.visibleTextEditors.forEach(applyCodeCoverage);
-				resolve();
-			});
+					createCoverageData(pathsToDirs, coveragePath);
+					setDecorators();
+					vscode.window.visibleTextEditors.forEach(applyCodeCoverage);
+					resolve();
+				});
 		} catch (e) {
 			vscode.window.showInformationMessage(e.msg);
 			reject(e);
@@ -235,6 +296,28 @@
 	return v;
 }
 
+// add decorations to the range
+function elaborate(r: vscode.Range, count: number, showCounts: boolean): vscode.DecorationOptions[] {
+	// irrelevant for "gutter"
+	if (!decorators || decorators.type === 'gutter') { return [{ range: r }]; }
+	const ans: vscode.DecorationOptions[] = [];
+	const dc = decoratorConfig;
+	const backgroundColor = [dc.uncoveredHighlightColor, dc.coveredHighlightColor];
+	const txt: vscode.ThemableDecorationAttachmentRenderOptions = {
+		contentText: count > 0 && showCounts ? `--${count}--` : '',
+		backgroundColor: backgroundColor[count === 0 ? 0 : 1]
+	};
+	const v: vscode.DecorationOptions = {
+		range: r,
+		hoverMessage: `${count} executions`,
+		renderOptions: {
+			before: txt,
+		}
+	};
+	ans.push(v);
+	return ans;
+}
+
 function createCoverageData(
 	pathsToDirs: Map<string, string>,
 	coveragePath: Map<string, CoverageData>) {
@@ -291,30 +374,71 @@
 	const cfg = getGoConfig(editor.document.uri);
 	const coverageOptions = cfg['coverageOptions'];
 	for (const filename in coverageData) {
-		if (editor.document.uri.fsPath.endsWith(filename)) {
-			isCoverageApplied = true;
-			const cd = coverageData[filename];
-			if (coverageOptions === 'showCoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
-				editor.setDecorations(
-					decorators.type === 'gutter'
-						? decorators.coveredGutterDecorator
-						: decorators.coveredHighlightDecorator,
-					cd.coveredRange
-				);
+		if (!editor.document.uri.fsPath.endsWith(filename)) {
+			continue;
+		}
+		isCoverageApplied = true;
+		const cd = coverageData[filename];
+		if (coverageOptions === 'showCoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
+			if (decorators.type === 'gutter') {
+				editor.setDecorations(decorators.coveredGutter, cd.coveredOptions);
+			} else {
+				detailed(editor, decorators.coveredHighlight, cd.coveredOptions);
 			}
+		}
 
-			if (coverageOptions === 'showUncoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
-				editor.setDecorations(
-					decorators.type === 'gutter'
-						? decorators.uncoveredGutterDecorator
-						: decorators.uncoveredHighlightDecorator,
-					cd.uncoveredRange
-				);
+		if (coverageOptions === 'showUncoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
+			if (decorators.type === 'gutter') {
+				editor.setDecorations(decorators.uncoveredGutter, cd.uncoveredOptions);
+			} else {
+				detailed(editor, decorators.uncoveredHighlight, cd.uncoveredOptions);
 			}
 		}
 	}
 }
 
+function detailed(editor: vscode.TextEditor, h: Highlight, opts: vscode.DecorationOptions[]) {
+	const tops: vscode.DecorationOptions[] = [];
+	const mids: vscode.DecorationOptions[] = [];
+	const bots: vscode.DecorationOptions[] = [];
+	const alls: vscode.DecorationOptions[] = [];
+	opts.forEach((opt) => {
+		const r = opt.range;
+		if (r.start.line === r.end.line) {
+			alls.push(opt);
+			return;
+		}
+		for (let line = r.start.line; line <= r.end.line; line++) {
+			if (line === r.start.line) {
+				const use: vscode.DecorationOptions = {
+					range: editor.document.validateRange(
+						new vscode.Range(line, r.start.character, line, Number.MAX_SAFE_INTEGER)),
+					hoverMessage: opt.hoverMessage,
+					renderOptions: opt.renderOptions
+				};
+				tops.push(use);
+			} else if (line < r.end.line) {
+				const use = {
+					range: editor.document.validateRange(
+						new vscode.Range(line, 0, line, Number.MAX_SAFE_INTEGER)),
+					hoverMessage: opt.hoverMessage
+				};
+				mids.push(use);
+			} else {
+				const use = {
+					range: new vscode.Range(line, 0, line, r.end.character),
+					hoverMessage: opt.hoverMessage
+				};
+				bots.push(use);
+			}
+		}
+	});
+	if (tops.length > 0) { editor.setDecorations(h.top, tops); }
+	if (mids.length > 0) { editor.setDecorations(h.mid, mids); }
+	if (bots.length > 0) { editor.setDecorations(h.bot, bots); }
+	if (alls.length > 0) { editor.setDecorations(h.all, alls); }
+}
+
 /**
  * Listener for file save that clears potential stale coverage data.
  * Local cache tracks files with changes outside of comments to determine
@@ -410,7 +534,7 @@
 
 // These routines enable testing without starting an editing session.
 
-export function coverageFilesForTest():  { [key: string]: CoverageData; } {
+export function coverageFilesForTest(): { [key: string]: CoverageData; } {
 	return coverageData;
 }
 
@@ -426,6 +550,8 @@
 			type: 'highlight',
 			coveredHighlightColor: x,
 			uncoveredHighlightColor: x,
+			coveredBorderColor: x,
+			uncoveredBorderColor: x,
 			coveredGutterStyle: x,
 			uncoveredGutterStyle: x
 		};
diff --git a/src/testUtils.ts b/src/testUtils.ts
index cd00956..07d9e3d 100644
--- a/src/testUtils.ts
+++ b/src/testUtils.ts
@@ -9,7 +9,6 @@
 
 import { applyCodeCoverageToAllEditors } from './goCover';
 import { toolExecutionEnvironment } from './goEnv';
-import { getCurrentPackage } from './goModules';
 import { GoDocumentSymbolProvider } from './goOutline';
 import { getNonVendorPackages } from './goPackages';
 import {
@@ -269,7 +268,17 @@
 		} else {
 			args.push('-timeout', testconfig.goConfig['testTimeout']);
 			if (testconfig.applyCodeCoverage) {
+				let coverMode = testconfig.goConfig['coverMode'];
+				switch (coverMode) {
+					case 'set': case 'count': case 'atomic': break;
+					default:
+						vscode.window.showWarningMessage(
+							`go.coverMode=${coverMode} is illegal. Use 'set', 'count', atomic'`
+						);
+						coverMode = 'set';
+				}
 				args.push('-coverprofile=' + tmpCoverPath);
+				args.push('-covermode', coverMode);
 			}
 		}
 		if (testTags && testconfig.flags.indexOf('-tags') === -1) {