From 78d51a8c6d2e45d6abc3f0ae41e2564dc3bc6ab5 Mon Sep 17 00:00:00 2001 From: Kuldeep Singh Chouhan <40597687+kool7@users.noreply.github.com> Date: Sun, 3 May 2026 22:31:29 +0530 Subject: [PATCH 1/3] fix: branch coverage parsing, UI consistency, and sidebar accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse arc table for branch-coverage .coverage files (coverage.py branch=true); previously all files showed 0% because line_bits is empty in that mode - Fix inferMissingLines to track bracket depth, eliminating false red highlights on continuation lines inside __all__, multi-line function calls, dict literals - Run pytest via child_process.spawn instead of a VS Code terminal; eliminates ^C corruption from the Python extension auto-activating the venv in terminals - Debounce file-watcher reloads and guard handleNoCoverage with a mutex to prevent concurrent prompts on rapid file changes - Filter test files from Coverage tree and dashboard (excludeTestFiles setting) - Recompute filtered totals in tree Overall, status bar, and dashboard ring so all three surfaces always show the same percentage - Remove info-message toast on every reload; status bar is sufficient - Dashboard header icon inlined as SVG so currentColor inherits VS Code theme - Panel tab icon uses ThemeIcon graph — works on light and dark themes - Coverage tree moved back to Explorer panel for discoverability - Add activity bar SVG icon and dashboard icon assets --- assets/icon-dashboard.svg | 8 ++ package.json | 9 ++- src/config.ts | 2 + src/extension.ts | 119 +++++++++++++++++++++++++++--- src/providers/codeLensProvider.ts | 10 ++- src/providers/treeProvider.ts | 17 ++++- src/ui/statusBar.ts | 5 +- 7 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 assets/icon-dashboard.svg diff --git a/assets/icon-dashboard.svg b/assets/icon-dashboard.svg new file mode 100644 index 0000000..23b1d4f --- /dev/null +++ b/assets/icon-dashboard.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index ac40e8e..01f9edb 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,13 @@ "name": "coverage-visualizer", "displayName": "Python Coverage Visualizer", "description": "Visualize Python test coverage inline in VS Code — highlights, CodeLens, dashboard, and sidebar tree view", - "version": "0.0.4", + "version": "0.0.5", "publisher": "kool7", "engines": { "vscode": "^1.90.0" }, "license": "MIT", - "icon": "assets/icon-128.png", + "icon": "assets/icon-256.png", "categories": [ "Testing", "Visualization", @@ -104,6 +104,11 @@ "type": "string", "default": "coverage.json", "description": "Path to coverage.json relative to workspace root. Generate with: pytest --cov=. --cov-report=json" + }, + "coverageVisualizer.excludeTestFiles": { + "type": "boolean", + "default": true, + "description": "Skip decorations and CodeLens on test files (test_*.py, *_test.py, files inside tests/ directories)." } } } diff --git a/src/config.ts b/src/config.ts index 233fddc..dbc06b4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,6 +8,7 @@ export interface Config { enableCodeLens: boolean; enableHoverMessages: boolean; autoReloadOnChange: boolean; + excludeTestFiles: boolean; } export function getConfig(): Config { @@ -20,5 +21,6 @@ export function getConfig(): Config { enableCodeLens: cfg.get('enableCodeLens', true), enableHoverMessages: cfg.get('enableHoverMessages', true), autoReloadOnChange: cfg.get('autoReloadOnChange', true), + excludeTestFiles: cfg.get('excludeTestFiles', true), }; } diff --git a/src/extension.ts b/src/extension.ts index fb09070..4273eaf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; +import { exec, spawn } from 'child_process'; import { parseCoverageJson, parseCoverageXml, @@ -20,6 +21,10 @@ import { CoverageTreeProvider } from './providers/treeProvider.js'; let coveredDecoration: vscode.TextEditorDecorationType; let uncoveredDecoration: vscode.TextEditorDecorationType; let currentReport: CoverageReport | undefined; +let coverageRunInProgress = false; +let noCoveragePromptActive = false; +let reloadTimer: ReturnType | undefined; +let coverageOutputChannel: vscode.OutputChannel | undefined; const codeLensProvider = new CoverageCodeLensProvider(); const hoverProvider = new CoverageHoverProvider(); @@ -85,14 +90,19 @@ function setupWatchers(context: vscode.ExtensionContext) { const root = vscode.workspace.workspaceFolders?.[0]; if (!root) return; + const debouncedReload = () => { + if (!getConfig().autoReloadOnChange) return; + clearTimeout(reloadTimer); + reloadTimer = setTimeout(() => loadAndApply(), 500); + }; + const patterns = ['coverage.json', 'coverage.xml', '.coverage']; patterns.forEach(pattern => { const w = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(root, pattern) ); - const reload = () => { if (getConfig().autoReloadOnChange) loadAndApply(); }; - w.onDidCreate(reload); - w.onDidChange(reload); + w.onDidCreate(debouncedReload); + w.onDidChange(debouncedReload); w.onDidDelete(() => { if (!findAnyCoverageFile(root.uri.fsPath)) clearCoverage(); }); @@ -106,11 +116,12 @@ async function loadAndApply() { const result = await detectAndParse(workspaceFolder); if (!result) { - vscode.window.showWarningMessage( - 'Coverage Visualizer: No coverage file found. Run: pytest --cov=. --cov-report=json' - ); + if (!coverageRunInProgress && !findAnyCoverageFile(workspaceFolder)) { + await handleNoCoverage(workspaceFolder); + } return; } + coverageRunInProgress = false; const { report, formatUsed } = result; currentReport = report; @@ -120,13 +131,87 @@ async function loadAndApply() { treeProvider.setReport(report); vscode.window.visibleTextEditors.forEach(editor => applyToEditor(editor, currentReport!)); - updateStatusBar(report); + + // Compute stats from the same filtered file set the dashboard shows, + // so the status bar number stays consistent with what's on screen. + const { excludeTestFiles } = getConfig(); + const filteredFiles = Object.entries(report.files) + .filter(([p]) => !excludeTestFiles || !isTestFile(p)) + .map(([, d]) => d); + const filteredCovered = filteredFiles.reduce((n, f) => n + f.executedLines.length, 0); + const filteredTotal = filteredFiles.reduce((n, f) => n + f.executedLines.length + f.missingLines.length, 0); + updateStatusBar({ + percentCovered: filteredTotal > 0 ? (filteredCovered / filteredTotal) * 100 : 0, + coveredStatements: filteredCovered, + numStatements: filteredTotal, + }); + updateDashboard(report); +} - const { percentCovered, coveredStatements, numStatements } = report.totals; - vscode.window.showInformationMessage( - `Coverage [${formatUsed}]: ${percentCovered.toFixed(1)}% — ${coveredStatements}/${numStatements} statements` - ); +function resolvePython(workspaceFolder: string): string { + const candidates = [ + path.join(workspaceFolder, '.venv', 'bin', 'python'), + path.join(workspaceFolder, '.venv', 'Scripts', 'python.exe'), + ]; + for (const p of candidates) { + if (fs.existsSync(p)) return p; + } + return 'python'; +} + +function checkPython(python: string, cwd: string, code: string): Promise { + return new Promise(resolve => { + exec(`"${python}" -c "${code}"`, { cwd }, err => resolve(!err)); + }); +} + +async function handleNoCoverage(workspaceFolder: string) { + if (noCoveragePromptActive) return; + noCoveragePromptActive = true; + + try { + const python = resolvePython(workspaceFolder); + const [hasPytestCov, hasCoverage] = await Promise.all([ + checkPython(python, workspaceFolder, 'import pytest_cov'), + checkPython(python, workspaceFolder, 'import coverage'), + ]); + + if (!hasCoverage) { + vscode.window.showWarningMessage('coverage is not installed — add it as a dev dependency to enable auto-run.'); + return; + } + + const choice = await vscode.window.showInformationMessage( + 'No coverage found.', + 'Run pytest', + 'Cancel' + ); + if (choice !== 'Run pytest') return; + + // Use spawn with the venv Python directly — no terminal, no VS Code Python + // extension interference. The extension auto-activates venv in *terminals*; + // background processes are unaffected. + const args = hasPytestCov + ? ['-m', 'pytest', '--cov=.', '--cov-report=json'] + : ['-m', 'coverage', 'run', '-m', 'pytest']; + + coverageRunInProgress = true; + coverageOutputChannel ??= vscode.window.createOutputChannel('Coverage Run'); + coverageOutputChannel.clear(); + coverageOutputChannel.show(true); + coverageOutputChannel.appendLine(`$ ${python} ${args.join(' ')}\n`); + + const proc = spawn(python, args, { cwd: workspaceFolder }); + proc.stdout.on('data', (d: Buffer) => coverageOutputChannel!.append(d.toString())); + proc.stderr.on('data', (d: Buffer) => coverageOutputChannel!.append(d.toString())); + proc.on('close', code => { + coverageRunInProgress = false; + coverageOutputChannel!.appendLine(`\n[exited ${code ?? '?'}]`); + }); + } finally { + noCoveragePromptActive = false; + } } async function detectAndParse( @@ -181,7 +266,18 @@ function clearCoverage() { } +function isTestFile(fsPath: string): boolean { + const basename = path.basename(fsPath); + return basename.startsWith('test_') || basename.endsWith('_test.py') || + fsPath.split(path.sep).some(seg => seg === 'tests' || seg === 'test'); +} + function applyToEditor(editor: vscode.TextEditor, report: CoverageReport) { + if (getConfig().excludeTestFiles && isTestFile(editor.document.uri.fsPath)) { + editor.setDecorations(coveredDecoration, []); + editor.setDecorations(uncoveredDecoration, []); + return; + } const fileCoverage = findFileInReport(report, editor.document.uri.fsPath); if (!fileCoverage) { editor.setDecorations(coveredDecoration, []); @@ -201,5 +297,6 @@ function linesToDecorations(lines: number[]): vscode.DecorationOptions[] { export function deactivate() { coveredDecoration?.dispose(); uncoveredDecoration?.dispose(); + coverageOutputChannel?.dispose(); currentReport = undefined; } diff --git a/src/providers/codeLensProvider.ts b/src/providers/codeLensProvider.ts index 08ba851..fc2e41e 100644 --- a/src/providers/codeLensProvider.ts +++ b/src/providers/codeLensProvider.ts @@ -1,11 +1,18 @@ import * as vscode from 'vscode'; +import * as path from 'path'; import { CoverageReport, findFileInReport } from '../parsers/coverageParser.js'; import { getConfig } from '../config.js'; +function isTestFile(fsPath: string): boolean { + const basename = path.basename(fsPath); + return basename.startsWith('test_') || basename.endsWith('_test.py') || + fsPath.split(path.sep).some(seg => seg === 'tests' || seg === 'test'); +} + export class CoverageCodeLensProvider implements vscode.CodeLensProvider { private report: CoverageReport | undefined; private _onDidChangeCodeLenses = new vscode.EventEmitter(); - readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event; + readonly onDidChangeCodeLenses = _onDidChangeCodeLenses.event; setReport(report: CoverageReport | undefined) { this.report = report; @@ -15,6 +22,7 @@ export class CoverageCodeLensProvider implements vscode.CodeLensProvider { provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { const cfg = getConfig(); if (!cfg.enableCodeLens || !this.report) return []; + if (cfg.excludeTestFiles && isTestFile(document.uri.fsPath)) return []; const fileCoverage = findFileInReport(this.report, document.uri.fsPath); if (!fileCoverage) return []; diff --git a/src/providers/treeProvider.ts b/src/providers/treeProvider.ts index ff0070d..15956d6 100644 --- a/src/providers/treeProvider.ts +++ b/src/providers/treeProvider.ts @@ -3,6 +3,12 @@ import * as path from 'path'; import { CoverageReport } from '../parsers/coverageParser.js'; import { getConfig } from '../config.js'; +function isTestFile(fsPath: string): boolean { + const basename = path.basename(fsPath); + return basename.startsWith('test_') || basename.endsWith('_test.py') || + fsPath.split(/[\\/]/).some(seg => seg === 'tests' || seg === 'test'); +} + export class CoverageTreeProvider implements vscode.TreeDataProvider { private report: CoverageReport | undefined; private _onDidChangeTreeData = new vscode.EventEmitter(); @@ -28,9 +34,16 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider !cfg.excludeTestFiles || !isTestFile(p)) + .map(([, d]) => d); + const coveredStatements = filteredFiles.reduce((n, f) => n + f.executedLines.length, 0); + const numStatements = filteredFiles.reduce((n, f) => n + f.executedLines.length + f.missingLines.length, 0); + const percentCovered = numStatements > 0 ? (coveredStatements / numStatements) * 100 : 0; + const summaryIcon = percentCovered >= cfg.thresholdGood ? 'shield' : percentCovered >= cfg.thresholdWarn ? 'warning' : 'error'; const summary = new vscode.TreeItem( @@ -39,8 +52,10 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider !cfg.excludeTestFiles || !isTestFile(filePath)) .sort(([, a], [, b]) => a.percentCovered - b.percentCovered) .map(([filePath, data]) => { const label = filePath.split('/').pop() ?? filePath; diff --git a/src/ui/statusBar.ts b/src/ui/statusBar.ts index 0266d3e..641c544 100644 --- a/src/ui/statusBar.ts +++ b/src/ui/statusBar.ts @@ -1,5 +1,4 @@ import * as vscode from 'vscode'; -import { CoverageReport } from '../parsers/coverageParser.js'; import { getConfig } from '../config.js'; let statusBarItem: vscode.StatusBarItem | undefined; @@ -11,9 +10,9 @@ export function initStatusBar(context: vscode.ExtensionContext) { context.subscriptions.push(statusBarItem); } -export function updateStatusBar(report: CoverageReport) { +export function updateStatusBar(stats: { percentCovered: number; coveredStatements: number; numStatements: number }) { if (!statusBarItem) return; - const { percentCovered } = report.totals; + const { percentCovered } = stats; const { thresholdGood, thresholdWarn } = getConfig(); const icon = percentCovered >= thresholdGood ? '$(shield)' : percentCovered >= thresholdWarn ? '$(warning)' : '$(error)'; From e2e6d64b4970050f675c9d8fd3f5fb4713f6a868 Mon Sep 17 00:00:00 2001 From: Kuldeep Singh Chouhan <40597687+kool7@users.noreply.github.com> Date: Sun, 3 May 2026 22:33:43 +0530 Subject: [PATCH 2/3] fix: add coverageParser and dashboardPanel changes --- src/parsers/coverageParser.ts | 120 ++++++++++++++++++++++++++++------ src/ui/dashboardPanel.ts | 55 ++++++++++++++-- 2 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/parsers/coverageParser.ts b/src/parsers/coverageParser.ts index efa28d8..adf0251 100644 --- a/src/parsers/coverageParser.ts +++ b/src/parsers/coverageParser.ts @@ -34,7 +34,7 @@ export interface RawCoverageJson { }; } -// ── JSON (coverage.json) ────────────────────────────────────────────────────── +// ── JSON (coverage.json) ───────────────────────────────────────────── export function parseCoverageJson(raw: RawCoverageJson): CoverageReport { const files: Record = {}; @@ -112,7 +112,7 @@ export function parseCoverageXml(xmlContent: string): CoverageReport { }; } -// ── SQLite (.coverage) ─────────────────────────────────────────────────────── +// ── SQLite (.coverage) ─────────────────────────────────────────────────────────────── // Reads the .coverage SQLite file directly using sql.js (pure JS/WASM). // coverage.py stores executed line numbers as a compact bitmap (numbits BLOB). @@ -130,20 +130,70 @@ function decodeNumBits(buf: Uint8Array): number[] { return lines; } -// Approximate missing lines by reading the source file (non-blank, non-comment lines). +// Approximate missing lines by reading the source file. +// Tracks two kinds of state across lines: +// 1. Triple-quoted strings — interior lines are not executable statements. +// 2. Open brackets — lines inside an unclosed ( [ { are continuation lines +// of the statement that opened the bracket, not independent statements. +// This prevents false "missing" markers on multi-line list/dict literals, +// __all__ definitions, multi-line function calls, etc. function inferMissingLines(filePath: string, executedSet: Set, workspaceRoot: string): number[] { const resolved = path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath); if (!fs.existsSync(resolved)) return []; - const lines = fs.readFileSync(resolved, 'utf-8').split('\n'); + + const sourceLines = fs.readFileSync(resolved, 'utf-8').split('\n'); const missing: number[] = []; - for (let i = 0; i < lines.length; i++) { + let inTripleString = false; + let tripleDelim = ''; + let bracketDepth = 0; + + for (let i = 0; i < sourceLines.length; i++) { const lineNum = i + 1; - if (executedSet.has(lineNum)) continue; - const trimmed = lines[i].trim(); - if (!trimmed || trimmed.startsWith('#') || - trimmed.startsWith('"""') || trimmed.startsWith("'''")) continue; - missing.push(lineNum); + const trimmed = sourceLines[i].trim(); + + // ── Inside a triple-quoted string ──────────────────────────────────────────── + if (inTripleString) { + if (trimmed.includes(tripleDelim)) inTripleString = false; + continue; // never a standalone executable statement + } + + if (!trimmed || trimmed.startsWith('#')) continue; + + // ── Inside an open bracket expression (continuation line) ──────────────────── + // e.g. the items of __all__ = [...], multi-line function args, dict literals. + if (bracketDepth > 0) { + for (const ch of trimmed) { + if (ch === '(' || ch === '[' || ch === '{') bracketDepth++; + else if (ch === ')' || ch === ']' || ch === '}') bracketDepth--; + } + continue; // continuation line — not an independent statement + } + + // ── Detect multi-line triple-quoted string opening ────────────────────────── + const dqIdx = trimmed.indexOf('"""'); + const sqIdx = trimmed.indexOf("'''"); + const hasTriple = dqIdx !== -1 || sqIdx !== -1; + if (hasTriple) { + const delim = (dqIdx !== -1 && (sqIdx === -1 || dqIdx < sqIdx)) ? '"""' : "'''"; + const openAt = trimmed.indexOf(delim); + const closeAt = trimmed.indexOf(delim, openAt + 3); + if (closeAt === -1) { + inTripleString = true; + tripleDelim = delim; + continue; // opening line of a multiline docstring — skip + } + // Same-line open+close → single-line string expression, IS a statement. Fall through. + } + + // ── Update bracket depth for this statement line ──────────────────────────── + for (const ch of trimmed) { + if (ch === '(' || ch === '[' || ch === '{') bracketDepth++; + else if (ch === ')' || ch === ']' || ch === '}') bracketDepth--; + } + + if (!executedSet.has(lineNum)) missing.push(lineNum); } + return missing; } @@ -176,21 +226,51 @@ export async function parseCoverageSqlite(coveragePath: string, workspaceRoot: s const fileRows = db.exec('SELECT id, path FROM file')[0]; if (!fileRows) return { files: {}, totals: { numStatements: 0, coveredStatements: 0, percentCovered: 0 }, source: 'sqlite' }; - for (const [id, filePath] of fileRows.values as [number, string][]) { - const stmt = db.prepare('SELECT numbits FROM line_bits WHERE file_id = ?'); - stmt.bind([id]); + // Detect whether this .coverage was collected with branch tracking (arc table) + // or simple line tracking (line_bits). branch=true in pyproject.toml writes only + // to arc; branch=false (the default) writes only to line_bits. + // The arc table may not exist at all in older coverage.py versions, so guard with try/catch. + let useArcs = false; + try { + const arcCount = db.exec('SELECT COUNT(*) FROM arc')[0]?.values[0][0] as number ?? 0; + useArcs = arcCount > 0; + } catch { + useArcs = false; + } + for (const [id, filePath] of fileRows.values as [number, string][]) { const executedSet = new Set(); - while (stmt.step()) { - const row = stmt.getAsObject() as { numbits: Uint8Array }; - for (const line of decodeNumBits(row.numbits)) executedSet.add(line); + + if (useArcs) { + // Branch-coverage mode: each row is a (fromno, tono) arc between lines. + // Negative values are virtual entry/exit markers, not real line numbers. + // Any positive value in either column means that line was executed. + const stmt = db.prepare('SELECT fromno, tono FROM arc WHERE file_id = ?'); + stmt.bind([id]); + while (stmt.step()) { + const { fromno, tono } = stmt.getAsObject() as { fromno: number; tono: number }; + if (fromno > 0) executedSet.add(fromno); + if (tono > 0) executedSet.add(tono); + } + stmt.free(); + } else { + // Line-coverage mode: each row is a compressed bitmap of executed line numbers. + const stmt = db.prepare('SELECT numbits FROM line_bits WHERE file_id = ?'); + stmt.bind([id]); + while (stmt.step()) { + const row = stmt.getAsObject() as { numbits: Uint8Array }; + for (const line of decodeNumBits(row.numbits)) executedSet.add(line); + } + stmt.free(); } - stmt.free(); const executedLines = [...executedSet].sort((a, b) => a - b); const missingLines = inferMissingLines(filePath as string, executedSet, workspaceRoot); const total = executedLines.length + missingLines.length; + // Skip files with no trackable lines (empty files, pure comments, __init__.py stubs, etc.) + if (total === 0) continue; + totalStmts += total; totalCovered += executedLines.length; @@ -198,7 +278,7 @@ export async function parseCoverageSqlite(coveragePath: string, workspaceRoot: s executedLines, missingLines, excludedLines: [], - percentCovered: total > 0 ? (executedLines.length / total) * 100 : 100, + percentCovered: (executedLines.length / total) * 100, }; } @@ -207,7 +287,7 @@ export async function parseCoverageSqlite(coveragePath: string, workspaceRoot: s totals: { numStatements: totalStmts, coveredStatements: totalCovered, - percentCovered: totalStmts > 0 ? (totalCovered / totalStmts) * 100 : 100, + percentCovered: totalStmts > 0 ? (totalCovered / totalStmts) * 100 : 0, }, source: 'sqlite', }; @@ -216,7 +296,7 @@ export async function parseCoverageSqlite(coveragePath: string, workspaceRoot: s } } -// ── Shared utilities ────────────────────────────────────────────────────────── +// ── Shared utilities ─────────────────────────────────────────────────────────────── export function findFileInReport( report: CoverageReport, diff --git a/src/ui/dashboardPanel.ts b/src/ui/dashboardPanel.ts index ad99625..eb4d9ca 100644 --- a/src/ui/dashboardPanel.ts +++ b/src/ui/dashboardPanel.ts @@ -3,6 +3,12 @@ import * as path from 'path'; import { CoverageReport } from '../parsers/coverageParser.js'; import { getConfig } from '../config.js'; +function isTestFile(fsPath: string): boolean { + const basename = path.basename(fsPath); + return basename.startsWith('test_') || basename.endsWith('_test.py') || + fsPath.split(/[\\/]/).some(seg => seg === 'tests' || seg === 'test'); +} + let panel: vscode.WebviewPanel | undefined; export function showDashboard(report: CoverageReport, context: vscode.ExtensionContext) { @@ -13,12 +19,18 @@ export function showDashboard(report: CoverageReport, context: vscode.ExtensionC 'coverageDashboard', 'Coverage Dashboard', vscode.ViewColumn.Beside, - { enableScripts: true, retainContextWhenHidden: true } + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'assets')], + } ); + // ThemeIcon is supported for WebviewPanel.iconPath since VS Code 1.87 (we target 1.90+). + panel.iconPath = new vscode.ThemeIcon('graph'); panel.onDidDispose(() => { panel = undefined; }, null, context.subscriptions); } - panel.webview.html = buildHtml(report); + panel.webview.html = buildHtml(report, panel.webview, context.extensionUri); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; @@ -35,21 +47,48 @@ export function showDashboard(report: CoverageReport, context: vscode.ExtensionC } export function updateDashboard(report: CoverageReport) { - if (panel) panel.webview.html = buildHtml(report); + if (panel) panel.webview.html = buildHtml(report, panel.webview, undefined); +} + +function buildHeaderIcon(extensionUri?: vscode.Uri): string { + if (extensionUri) { + const fs = require('fs') as typeof import('fs'); + const svgUri = vscode.Uri.joinPath(extensionUri, 'assets', 'icon-dashboard.svg'); + if (fs.existsSync(svgUri.fsPath)) { + let svg = fs.readFileSync(svgUri.fsPath, 'utf-8') as string; + // Inject class onto the root element so CSS can size/color it. + // Inlining lets currentColor inherit VS Code's foreground — works on both themes. + svg = svg.replace(/^(\s* + + + + + `; } function colorClass(pct: number, thresholdGood: number, thresholdWarn: number): string { return pct >= thresholdGood ? 'good' : pct >= thresholdWarn ? 'warn' : 'bad'; } -function buildHtml(report: CoverageReport): string { - const { percentCovered, coveredStatements, numStatements } = report.totals; - const { thresholdGood, thresholdWarn } = getConfig(); +function buildHtml(report: CoverageReport, webview?: vscode.Webview, extensionUri?: vscode.Uri): string { + const { thresholdGood, thresholdWarn, excludeTestFiles } = getConfig(); const files = Object.entries(report.files) + .filter(([filePath]) => !excludeTestFiles || !isTestFile(filePath)) .map(([filePath, data]) => ({ filePath, ...data })) .sort((a, b) => a.percentCovered - b.percentCovered); + // Recompute totals from the filtered file list so the ring and counts + // match what's shown in the table (e.g. when excludeTestFiles is on). + const coveredStatements = files.reduce((n, f) => n + f.executedLines.length, 0); + const numStatements = files.reduce((n, f) => n + f.executedLines.length + f.missingLines.length, 0); + const percentCovered = numStatements > 0 ? (coveredStatements / numStatements) * 100 : 0; + const fileRows = files.map(f => { const pct = f.percentCovered.toFixed(1); const cls = colorClass(f.percentCovered, thresholdGood, thresholdWarn); @@ -158,10 +197,12 @@ function buildHtml(report: CoverageReport): string { .pct.good, .pct.warn, .pct.bad { background: none; } .no-results { padding: 16px 10px; opacity: 0.5; font-style: italic; display: none; } .source-tag { margin-top: 20px; opacity: 0.4; font-size: 0.78em; } + h1 { display: flex; align-items: center; gap: 10px; } + .h1-icon { width: 28px; height: 28px; flex-shrink: 0; } -

Coverage Dashboard

+

${buildHeaderIcon(extensionUri)}Coverage Dashboard

From 1ec1eba318bf1f546c536480521e2a77de301b92 Mon Sep 17 00:00:00 2001 From: Kuldeep Singh Chouhan <40597687+kool7@users.noreply.github.com> Date: Sun, 3 May 2026 22:36:25 +0530 Subject: [PATCH 3/3] fix: resolve merge conflicts with main (incorporate PR2 changes, our version supersedes) --- src/ui/dashboardPanel.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ui/dashboardPanel.ts b/src/ui/dashboardPanel.ts index eb4d9ca..81cc4fd 100644 --- a/src/ui/dashboardPanel.ts +++ b/src/ui/dashboardPanel.ts @@ -263,17 +263,14 @@ function buildHtml(report: CoverageReport, webview?: vscode.Webview, extensionUr