diff --git a/example/example.code-workspace b/example/example.code-workspace index b170d51..79f6d13 100644 --- a/example/example.code-workspace +++ b/example/example.code-workspace @@ -6,6 +6,7 @@ ], "settings": { "coverage-gutters.showLineCoverage": true, + "coverage-gutters.showHitCounts": true, "coverage-gutters.coverageReportFileName": "index.html", "coverage-gutters.remotePathResolve": ["/var/www/", "./"] } diff --git a/package.json b/package.json index 9a646e2..d1292ea 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,12 @@ "default": "rgba(163, 0, 0, 0.4)", "description": "dark theme partial highlight for code coverage" }, + "coverage-gutters.hitCountColor": { + "type": "string", + "scope": "resource", + "default": "rgba(255, 255, 255, 0.8)", + "description": "color for hit count numbers in the gutter" + }, "coverage-gutters.showLineCoverage": { "type": "boolean", "default": false, @@ -89,6 +95,12 @@ "default": true, "description": "show or hide the gutter coverage" }, + "coverage-gutters.showHitCounts": { + "type": "boolean", + "scope": "resource", + "default": false, + "description": "show hit counts as numbers in the gutter" + }, "coverage-gutters.ignoredPathGlobs": { "type": "string", "scope": "resource", diff --git a/src/coverage-system/renderer.ts b/src/coverage-system/renderer.ts index 6edf767..1d015dd 100644 --- a/src/coverage-system/renderer.ts +++ b/src/coverage-system/renderer.ts @@ -2,6 +2,7 @@ import { Section } from "lcov-parse"; import { Range, TextEditor, + DecorationOptions } from "vscode"; import { Config } from "../extension/config"; import { SectionFinder } from "./sectionfinder"; @@ -10,11 +11,13 @@ export interface ICoverageLines { full: Range[]; partial: Range[]; none: Range[]; + hitCounts: Map; } export class Renderer { private configStore: Config; private sectionFinder: SectionFinder; + private maxHitCount: number = 0; constructor( configStore: Config, @@ -37,6 +40,7 @@ export class Renderer { full: [], none: [], partial: [], + hitCounts: new Map(), }; textEditors.forEach((textEditor) => { @@ -49,6 +53,8 @@ export class Renderer { coverageLines.full = []; coverageLines.none = []; coverageLines.partial = []; + coverageLines.hitCounts = new Map(); + this.maxHitCount = 0; // find the section(s) (or undefined) by looking relatively at each workspace // users can also optional use absolute instead of relative for this @@ -73,6 +79,9 @@ export class Renderer { this.configStore.partialCoverageDecorationType, [], ); + + // Clean up hit count decorations if they exist + editor.setDecorations(this.configStore.hitCountDecorationType, []); } public setDecorationsForEditor( @@ -92,6 +101,65 @@ export class Renderer { this.configStore.partialCoverageDecorationType, coverage.partial, ); + + // Apply hit count decorations if enabled + if (this.configStore.showHitCounts) { + this.setHitCountDecorations(editor, coverage); + } + } + + private setHitCountDecorations( + editor: TextEditor, + coverage: ICoverageLines, + ) { + const hitCountDecorations: DecorationOptions[] = []; + + const paddingWidth = this.maxHitCount.toString().length; + + const addEmptyPadding = (startLine: number, endLine: number) => { + for (let line = startLine; line <= endLine; line++) { + hitCountDecorations.push({ + range: new Range(line, 0, line, 0), + renderOptions: { + before: { + // We use this special invisible character for the margin since spaces are trimmed + contentText: '\u00A0'.repeat(paddingWidth) + '\u00A0', + } + } + }); + } + }; + + let lastLineNumber = -1; + + // Create decorations for all lines with coverage data + coverage.hitCounts.forEach((hitCount, lineNumber) => { + if (lineNumber > lastLineNumber + 1) { + addEmptyPadding(lastLineNumber + 1, lineNumber - 1); + } + + const range = new Range(lineNumber, 0, lineNumber, 0); + const displayValue = hitCount > 1000 ? '>1000' : hitCount.toString(); + const paddedHitCount = displayValue.padStart(paddingWidth, '\u00A0'); + + hitCountDecorations.push({ + range, + renderOptions: { + before: { + contentText: paddedHitCount + '\u00A0', + } + } + }); + + lastLineNumber = lineNumber; + }); + + // Fill gap at end of file if needed + if (lastLineNumber < editor.document.lineCount - 1) { + addEmptyPadding(lastLineNumber + 1, editor.document.lineCount - 1); + } + + editor.setDecorations(this.configStore.hitCountDecorationType, hitCountDecorations); } /** @@ -120,6 +188,11 @@ export class Renderer { .filter((detail) => detail.line > 0) .forEach((detail) => { const lineRange = new Range(detail.line - 1, 0, detail.line - 1, 0); + + // Store hit count for this line + coverageLines.hitCounts.set(detail.line - 1, detail.hit); + this.maxHitCount = Math.max(this.maxHitCount, detail.hit); + if (detail.hit > 0) { // Evaluates to true if at least one element in range is equal to LineRange if (coverageLines.none.some((range) => range.isEqual(lineRange))) { diff --git a/src/extension/config.ts b/src/extension/config.ts index d6a6b7d..9503df9 100644 --- a/src/extension/config.ts +++ b/src/extension/config.ts @@ -16,11 +16,13 @@ export class Config { public fullCoverageDecorationType!: TextEditorDecorationType; public partialCoverageDecorationType!: TextEditorDecorationType; public noCoverageDecorationType!: TextEditorDecorationType; + public hitCountDecorationType!: TextEditorDecorationType; public showStatusBarToggler!: boolean; public ignoredPathGlobs!: string; public remotePathResolve!: string[]; public manualCoverageFilePaths!: string[]; public watchOnActivate!: boolean; + public showHitCounts!: boolean; private context: ExtensionContext; @@ -71,6 +73,8 @@ export class Config { const showGutterCoverage = rootConfig.get("showGutterCoverage") as string; const showLineCoverage = rootConfig.get("showLineCoverage") as string; const showRulerCoverage = rootConfig.get("showRulerCoverage") as string; + this.showHitCounts = rootConfig.get("showHitCounts") as boolean; + const hitCountColor = rootConfig.get("hitCountColor") as string; const makeIcon = (colour: string): string | Uri => { colour = colour @@ -138,12 +142,19 @@ export class Config { overviewRulerLane: OverviewRulerLane.Full, }; + const hitCountDecoration: DecorationRenderOptions = { + before: { + color: hitCountColor, + } + }; + this.cleanupEmptyGutterIcons(fullDecoration, partialDecoration, noDecoration); // Generate decorations this.noCoverageDecorationType = window.createTextEditorDecorationType(noDecoration); this.partialCoverageDecorationType = window.createTextEditorDecorationType(partialDecoration); this.fullCoverageDecorationType = window.createTextEditorDecorationType(fullDecoration); + this.hitCountDecorationType = window.createTextEditorDecorationType(hitCountDecoration); // Assign the key and resolved fragment this.remotePathResolve = rootConfig.get("remotePathResolve") as string[];