diff --git a/assets/icon-dashboard.svg b/assets/icon-dashboard.svg new file mode 100644 index 0000000..0331caf --- /dev/null +++ b/assets/icon-dashboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/package.json b/package.json index f850be0..f4ac25b 100644 --- a/package.json +++ b/package.json @@ -8,103 +8,35 @@ "vscode": "^1.90.0" }, "license": "MIT", - "icon": "assets/icon-128.png", - "categories": [ - "Testing", - "Visualization", - "Other" - ], - "keywords": [ - "python", - "coverage", - "test coverage", - "pytest", - "pytest-cov", - "visualization" - ], - "repository": { - "type": "git", - "url": "https://github.com/kool7/coverage-visualizer" - }, - "activationEvents": [ - "onStartupFinished", - "onView:coverageVisualizer.filesView" - ], + "icon": "assets/icon-256.png", + "categories": ["Testing", "Visualization", "Other"], + "keywords": ["python", "coverage", "test coverage", "pytest", "pytest-cov", "visualization"], + "repository": { "type": "git", "url": "https://github.com/kool7/coverage-visualizer" }, + "activationEvents": ["onStartupFinished", "onView:coverageVisualizer.filesView"], "main": "./out/extension.js", "contributes": { "commands": [ - { - "command": "coverage-visualizer.show", - "title": "Coverage Visualizer: Show Coverage", - "icon": "$(shield)" - }, - { - "command": "coverage-visualizer.clear", - "title": "Coverage Visualizer: Clear Coverage", - "icon": "$(close)" - }, - { - "command": "coverage-visualizer.showDashboard", - "title": "Coverage Visualizer: Show Dashboard", - "icon": "$(graph)" - } + { "command": "coverage-visualizer.show", "title": "Coverage Visualizer: Show Coverage", "icon": "$(shield)" }, + { "command": "coverage-visualizer.clear", "title": "Coverage Visualizer: Clear Coverage", "icon": "$(close)" }, + { "command": "coverage-visualizer.showDashboard", "title": "Coverage Visualizer: Show Dashboard", "icon": "$(graph)" } ], "views": { "explorer": [ - { - "id": "coverageVisualizer.filesView", - "name": "Coverage", - "when": "true" - } + { "id": "coverageVisualizer.filesView", "name": "Coverage", "when": "true" } ] }, "configuration": { "title": "Coverage Visualizer", "properties": { - "coverageVisualizer.thresholdGood": { - "type": "number", - "default": 80, - "minimum": 0, - "maximum": 100, - "description": "Coverage % at or above which a file is considered well-covered (shown in green)." - }, - "coverageVisualizer.thresholdWarn": { - "type": "number", - "default": 50, - "minimum": 0, - "maximum": 100, - "description": "Coverage % at or above which a file is a warning (yellow). Below this is red." - }, - "coverageVisualizer.coveredHighlightColor": { - "type": "string", - "default": "rgba(0, 180, 0, 0.10)", - "description": "Background highlight color for covered lines. Any CSS color string." - }, - "coverageVisualizer.uncoveredHighlightColor": { - "type": "string", - "default": "rgba(220, 50, 50, 0.10)", - "description": "Background highlight color for uncovered lines. Any CSS color string." - }, - "coverageVisualizer.enableCodeLens": { - "type": "boolean", - "default": true, - "description": "Show coverage % above each function and class definition." - }, - "coverageVisualizer.enableHoverMessages": { - "type": "boolean", - "default": true, - "description": "Show covered / not-covered tooltip when hovering a highlighted line." - }, - "coverageVisualizer.autoReloadOnChange": { - "type": "boolean", - "default": true, - "description": "Automatically reload coverage when coverage.json / coverage.xml / .coverage changes on disk." - }, - "coverageVisualizer.coverageJsonPath": { - "type": "string", - "default": "coverage.json", - "description": "Path to coverage.json relative to workspace root. Generate with: pytest --cov=. --cov-report=json" - } + "coverageVisualizer.thresholdGood": { "type": "number", "default": 80, "minimum": 0, "maximum": 100, "description": "Coverage % at or above which a file is considered well-covered (shown in green)." }, + "coverageVisualizer.thresholdWarn": { "type": "number", "default": 50, "minimum": 0, "maximum": 100, "description": "Coverage % at or above which a file is a warning (yellow). Below this is red." }, + "coverageVisualizer.coveredHighlightColor": { "type": "string", "default": "rgba(0, 180, 0, 0.10)", "description": "Background highlight color for covered lines. Any CSS color string." }, + "coverageVisualizer.uncoveredHighlightColor": { "type": "string", "default": "rgba(220, 50, 50, 0.10)", "description": "Background highlight color for uncovered lines. Any CSS color string." }, + "coverageVisualizer.enableCodeLens": { "type": "boolean", "default": true, "description": "Show coverage % above each function and class definition." }, + "coverageVisualizer.enableHoverMessages": { "type": "boolean", "default": true, "description": "Show covered / not-covered tooltip when hovering a highlighted line." }, + "coverageVisualizer.autoReloadOnChange": { "type": "boolean", "default": true, "description": "Automatically reload coverage when coverage.json / coverage.xml / .coverage changes on disk." }, + "coverageVisualizer.coverageJsonPath": { "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)." } } } }, @@ -133,7 +65,5 @@ "ts-node": "^10.9.2", "typescript": "^5.4.0" }, - "dependencies": { - "sql.js": "^1.14.1" - } + "dependencies": { "sql.js": "^1.14.1" } } 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..c476ed7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,12 @@ const codeLensProvider = new CoverageCodeLensProvider(); const hoverProvider = new CoverageHoverProvider(); const treeProvider = new CoverageTreeProvider(); +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 function activate(context: vscode.ExtensionContext) { createDecorations(); initStatusBar(context); @@ -112,7 +118,7 @@ async function loadAndApply() { return; } - const { report, formatUsed } = result; + const { report } = result; currentReport = report; codeLensProvider.setReport(report); @@ -120,13 +126,21 @@ async function loadAndApply() { treeProvider.setReport(report); vscode.window.visibleTextEditors.forEach(editor => applyToEditor(editor, currentReport!)); - updateStatusBar(report); - updateDashboard(report); - const { percentCovered, coveredStatements, numStatements } = report.totals; - vscode.window.showInformationMessage( - `Coverage [${formatUsed}]: ${percentCovered.toFixed(1)}% — ${coveredStatements}/${numStatements} statements` - ); + const { excludeTestFiles } = getConfig(); + const filteredFiles = Object.entries(report.files) + .filter(([, d]) => d.executedLines.length + d.missingLines.length > 0) + .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); } async function detectAndParse( diff --git a/src/providers/treeProvider.ts b/src/providers/treeProvider.ts index eaa6241..f78746f 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,18 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider d.executedLines.length + d.missingLines.length > 0) + .filter(([p]) => !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,9 +54,11 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider data.executedLines.length + data.missingLines.length > 0) + .filter(([, d]) => d.executedLines.length + d.missingLines.length > 0) + .filter(([filePath]) => !cfg.excludeTestFiles || !isTestFile(filePath)) .sort(([, a], [, b]) => a.percentCovered - b.percentCovered) .map(([filePath, data]) => { const displayPath = filePath.startsWith(workspaceRoot) @@ -58,11 +75,9 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider= cfg.thresholdGood ? 'pass' : data.percentCovered >= cfg.thresholdWarn ? 'warning' : 'error' ); - const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath); - item.command = { command: 'vscode.open', title: 'Open File', diff --git a/src/ui/dashboardPanel.ts b/src/ui/dashboardPanel.ts index c2ed0b1..7088ba4 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 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, context.extensionUri); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; @@ -38,17 +50,36 @@ export function updateDashboard(report: CoverageReport) { if (panel) panel.webview.html = buildHtml(report); } +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; + // Inline SVG inherits currentColor from CSS — correct on both light and dark 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, extensionUri?: vscode.Uri): string { + const { thresholdGood, thresholdWarn, excludeTestFiles } = getConfig(); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; const files = Object.entries(report.files) .filter(([, data]) => data.executedLines.length + data.missingLines.length > 0) + .filter(([filePath]) => !excludeTestFiles || !isTestFile(filePath)) .map(([filePath, data]) => { const displayPath = filePath.startsWith(workspaceRoot) ? filePath.slice(workspaceRoot.length).replace(/^[\\/]/, '') @@ -57,6 +88,11 @@ function buildHtml(report: CoverageReport): string { }) .sort((a, b) => a.percentCovered - b.percentCovered); + // Recompute totals from the filtered list so ring and status bar always agree. + 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); @@ -95,7 +131,8 @@ function buildHtml(report: CoverageReport): string { background: var(--vscode-editor-background); padding: 24px; } - h1 { font-size: 1.3em; font-weight: 600; margin-bottom: 24px; opacity: 0.9; } + h1 { display: flex; align-items: center; gap: 10px; font-size: 1.3em; font-weight: 600; margin-bottom: 24px; opacity: 0.9; } + .h1-icon { width: 28px; height: 28px; flex-shrink: 0; } .summary { display: flex; align-items: center; gap: 40px; margin-bottom: 24px; padding: 20px 24px; @@ -115,28 +152,23 @@ function buildHtml(report: CoverageReport): string { .stat-row { display: flex; gap: 8px; align-items: baseline; } .stat-num { font-size: 1.6em; font-weight: 700; } .stat-label { opacity: 0.6; font-size: 0.85em; } - .toolbar { - display: flex; align-items: center; gap: 10px; margin-bottom: 12px; - } + .toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } .toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1; } - h2 { font-size: 1em; font-weight: 600; opacity: 0.8; - text-transform: uppercase; letter-spacing: 0.05em; } + h2 { font-size: 1em; font-weight: 600; opacity: 0.8; text-transform: uppercase; letter-spacing: 0.05em; } .file-count { opacity: 0.45; font-size: 0.82em; } .sort-btn { background: var(--vscode-button-secondaryBackground, #3c3c3c); color: var(--vscode-button-secondaryForeground, #ccc); border: 1px solid var(--vscode-widget-border, #555); border-radius: 4px; padding: 3px 9px; font-size: 0.82em; - cursor: pointer; display: flex; align-items: center; gap: 4px; - white-space: nowrap; + cursor: pointer; display: flex; align-items: center; gap: 4px; white-space: nowrap; } .sort-btn:hover { background: var(--vscode-button-secondaryHoverBackground, #4a4a4a); } .filter-input { background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, #555); color: var(--vscode-input-foreground); - border-radius: 4px; padding: 4px 10px; font-size: 0.9em; width: 200px; - outline: none; + border-radius: 4px; padding: 4px 10px; font-size: 0.9em; width: 200px; outline: none; } .filter-input:focus { border-color: var(--vscode-focusBorder, #007fd4); } table { width: 100%; border-collapse: collapse; } @@ -146,8 +178,7 @@ function buildHtml(report: CoverageReport): string { .file-row { cursor: pointer; transition: background 0.15s; } .file-row:hover { background: var(--vscode-list-hoverBackground); } .file-row.hidden { display: none; } - td { padding: 8px 10px; vertical-align: middle; - border-bottom: 1px solid var(--vscode-widget-border, #222); } + td { padding: 8px 10px; vertical-align: middle; border-bottom: 1px solid var(--vscode-widget-border, #222); } .filename { font-family: var(--vscode-editor-font-family, monospace); font-size: 0.9em; color: var(--vscode-textLink-foreground, #4daafc); @@ -168,35 +199,22 @@ function buildHtml(report: CoverageReport): string { -

Coverage Dashboard

+

${buildHeaderIcon(extensionUri)}Coverage Dashboard

- - - ${percentCovered.toFixed(1)}% - + + ${percentCovered.toFixed(1)}% covered
-
- ${coveredStatements} - / ${numStatements} statements covered -
-
- ${files.filter(f => f.percentCovered === 100).length} - files fully covered -
-
- ${files.filter(f => f.percentCovered === 0).length} - files with zero coverage -
+
${coveredStatements}/ ${numStatements} statements covered
+
${files.filter(f => f.percentCovered === 100).length}files fully covered
+
${files.filter(f => f.percentCovered === 0).length}files with zero coverage
@@ -205,43 +223,26 @@ function buildHtml(report: CoverageReport): string {

Files

${files.length} total - - - - - - - - + ${fileRows}
FileLines%
FileLines%
No files match your filter.
-

Source: ${report.source}