Init extension
This commit is contained in:
commit
377b29ceac
30
.eslintrc.json
Normal file
30
.eslintrc.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "import",
|
||||
"format": [ "camelCase", "PascalCase" ]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": "warn",
|
||||
"curly": "warn",
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"out",
|
||||
"dist",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
5
.vscode-test.mjs
Normal file
5
.vscode-test.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
||||
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"out": false // set this to true to hide the "out" folder with the compiled JS files
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
20
.vscode/tasks.json
vendored
Normal file
20
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
.vscodeignore
Normal file
11
.vscodeignore
Normal file
@ -0,0 +1,11 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
||||
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to the "coverage-tool" extension will be documented in this file.
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Initial release
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Justin Bossis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
34
README.md
Normal file
34
README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# `Coverage.py` Highlighter (coveragetool)
|
||||
|
||||
An extension to highlight lines according to a `coverage.json` generated by `Coverage.py`
|
||||
|
||||
## Features
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
* Python `coverage` package
|
||||
* A `coverage.json` file generated by `coverage`
|
||||
|
||||
---
|
||||
|
||||
## Extension Settings
|
||||
|
||||
This extension contributes the following settings:
|
||||
* `coveragetool.coverageFileName`: Name of the json file containing coverage data
|
||||
* `coveragetool.coverageFilePath`: Path to the folder containing the coverage JSON file
|
||||
* `coveragetool.replacePath`: Substring in paths that needs to be replaced
|
||||
* `coveragetool.replacePathWith`: Substring to replace with in the paths
|
||||
* `coveragetool.colors.executedColor`: Background color value for executed lines
|
||||
* `coveragetool.colors.excludedColor`: Background color value for excluded lines
|
||||
* `coveragetool.colors.missingColor`: Background color value for missing lines
|
||||
|
||||
## Commands
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
* Decorations do not reload when coverage file changes.
|
||||
* No warnings when coverage data is outdated.
|
||||
257
coverage.json
Normal file
257
coverage.json
Normal file
@ -0,0 +1,257 @@
|
||||
{
|
||||
"meta": {
|
||||
"format": 2,
|
||||
"version": "7.5.1",
|
||||
"timestamp": "2024-09-01T12:02:33.377012",
|
||||
"branch_coverage": false,
|
||||
"show_contexts": false
|
||||
},
|
||||
"files": {
|
||||
"views.py": {
|
||||
"executed_lines": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
17,
|
||||
18,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
38,
|
||||
39,
|
||||
41,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
50,
|
||||
57,
|
||||
61,
|
||||
62,
|
||||
69,
|
||||
70,
|
||||
91,
|
||||
92,
|
||||
105,
|
||||
106,
|
||||
117,
|
||||
118,
|
||||
145,
|
||||
146,
|
||||
168,
|
||||
169,
|
||||
180,
|
||||
181,
|
||||
189,
|
||||
190,
|
||||
201,
|
||||
202,
|
||||
214,
|
||||
215,
|
||||
216,
|
||||
217,
|
||||
218,
|
||||
220,
|
||||
227,
|
||||
231,
|
||||
232,
|
||||
233,
|
||||
234,
|
||||
235,
|
||||
238,
|
||||
239,
|
||||
240,
|
||||
241,
|
||||
242,
|
||||
243,
|
||||
245,
|
||||
247,
|
||||
252,
|
||||
259,
|
||||
264,
|
||||
265,
|
||||
266,
|
||||
267,
|
||||
272,
|
||||
273,
|
||||
275,
|
||||
276,
|
||||
279,
|
||||
280,
|
||||
284,
|
||||
286,
|
||||
287,
|
||||
288,
|
||||
289,
|
||||
290,
|
||||
291,
|
||||
300,
|
||||
301,
|
||||
302,
|
||||
307,
|
||||
309,
|
||||
310,
|
||||
311,
|
||||
313,
|
||||
317,
|
||||
322,
|
||||
323,
|
||||
324,
|
||||
325,
|
||||
326,
|
||||
328,
|
||||
341,
|
||||
345,
|
||||
346,
|
||||
350,
|
||||
351,
|
||||
356,
|
||||
357,
|
||||
358,
|
||||
359,
|
||||
360,
|
||||
362,
|
||||
367
|
||||
],
|
||||
"summary": {
|
||||
"covered_lines": 119,
|
||||
"num_statements": 223,
|
||||
"percent_covered": 53.36322869955157,
|
||||
"percent_covered_display": "53",
|
||||
"missing_lines": 104,
|
||||
"excluded_lines": 0
|
||||
},
|
||||
"missing_lines": [
|
||||
51,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
58,
|
||||
59,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
71,
|
||||
72,
|
||||
88,
|
||||
89,
|
||||
93,
|
||||
94,
|
||||
97,
|
||||
98,
|
||||
99,
|
||||
100,
|
||||
101,
|
||||
102,
|
||||
103,
|
||||
107,
|
||||
108,
|
||||
109,
|
||||
110,
|
||||
111,
|
||||
112,
|
||||
113,
|
||||
114,
|
||||
115,
|
||||
119,
|
||||
120,
|
||||
125,
|
||||
142,
|
||||
143,
|
||||
147,
|
||||
148,
|
||||
164,
|
||||
165,
|
||||
166,
|
||||
170,
|
||||
171,
|
||||
172,
|
||||
173,
|
||||
176,
|
||||
178,
|
||||
182,
|
||||
183,
|
||||
184,
|
||||
185,
|
||||
186,
|
||||
187,
|
||||
191,
|
||||
192,
|
||||
197,
|
||||
198,
|
||||
199,
|
||||
203,
|
||||
204,
|
||||
210,
|
||||
211,
|
||||
221,
|
||||
222,
|
||||
223,
|
||||
224,
|
||||
225,
|
||||
228,
|
||||
229,
|
||||
244,
|
||||
246,
|
||||
248,
|
||||
249,
|
||||
250,
|
||||
251,
|
||||
253,
|
||||
254,
|
||||
255,
|
||||
256,
|
||||
257,
|
||||
258,
|
||||
281,
|
||||
282,
|
||||
283,
|
||||
319,
|
||||
329,
|
||||
336,
|
||||
337,
|
||||
338,
|
||||
339,
|
||||
342,
|
||||
343,
|
||||
347,
|
||||
348,
|
||||
352,
|
||||
353,
|
||||
363,
|
||||
364,
|
||||
365,
|
||||
368,
|
||||
374
|
||||
],
|
||||
"excluded_lines": []
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"covered_lines": 1133,
|
||||
"num_statements": 1602,
|
||||
"percent_covered": 70.72409488139826,
|
||||
"percent_covered_display": "71",
|
||||
"missing_lines": 469,
|
||||
"excluded_lines": 0
|
||||
}
|
||||
}
|
||||
3266
package-lock.json
generated
Normal file
3266
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
97
package.json
Normal file
97
package.json
Normal file
@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "coveragetool",
|
||||
"displayName": "Coverage Tool",
|
||||
"publisher": "justinbossis",
|
||||
"description": "Highlights coverage.py lines in VSCode",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"vscode": "^1.92.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.justinbossis.fr/JustinBossis/CoverageTool"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://git.justinbossis.fr/JustinBossis/CoverageTool/issues"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:python",
|
||||
"workspaceContains:**/coverage.json"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "Coverage Tool",
|
||||
"properties": {
|
||||
"coveragetool.coverageFileName": {
|
||||
"type": "string",
|
||||
"default": "coverage.json",
|
||||
"description": "Name of the JSON file containing coverage information.",
|
||||
"markdownDescription": "Name of the JSON file containing coverage information."
|
||||
},
|
||||
"coveragetool.coverageFilePath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Path containing the coverage.json file.",
|
||||
"markdownDescription": "Path containing the coverage.json file."
|
||||
},
|
||||
"coveragetool.replacePath": {
|
||||
"type": "string",
|
||||
"description": "Substring in file paths to replace.",
|
||||
"markdownDescription": "Substring in file paths to replace."
|
||||
},
|
||||
"coveragetool.replacePathWith": {
|
||||
"type": "string",
|
||||
"description": "Substring to replace with in file paths.",
|
||||
"markdownDescription": "Substring to replace with in file paths."
|
||||
},
|
||||
"coveragetool.colors.executedColor": {
|
||||
"type": "string",
|
||||
"description": "Background color value for executed lines.",
|
||||
"markdownDescription": "Background color value for executed lines in any valid CSS format (`#RRGGBBAA`, `rgba(...)`)",
|
||||
"default": "rgba(0, 255, 0, 0)"
|
||||
},
|
||||
"coveragetool.colors.excludedColor": {
|
||||
"type": "string",
|
||||
"description": "Background color value for excluded lines.",
|
||||
"markdownDescription": "Background color value for excluded lines in any valid CSS format (`#RRGGBBAA`, `rgba(...)`)",
|
||||
"default": "rgba(255, 255, 0, 0)"
|
||||
},
|
||||
"coveragetool.colors.missingColor": {
|
||||
"type": "string",
|
||||
"description": "Background color value for missing lines.",
|
||||
"markdownDescription": "Background color value for missing lines in any valid CSS format (`#RRGGBBAA`, `rgba(...)`)",
|
||||
"default": "rgba(255, 0, 0, 0.15)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "coverage-tool.helloWorld",
|
||||
"title": "Hello World"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"pretest": "npm run compile && npm run lint",
|
||||
"lint": "eslint src --ext ts",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.92.0",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"eslint": "^8.57.0",
|
||||
"typescript": "^5.4.5",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0"
|
||||
}
|
||||
}
|
||||
116
src/config/ConfigProvider.ts
Normal file
116
src/config/ConfigProvider.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import {
|
||||
window,
|
||||
workspace,
|
||||
TextEditorDecorationType,
|
||||
WorkspaceConfiguration,
|
||||
OverviewRulerLane,
|
||||
} from 'vscode';
|
||||
import Configs from './Configs';
|
||||
import Logger from '../util/Logger';
|
||||
|
||||
// Create a TextEditor decoration from given bgColor
|
||||
function createDecor(bgColor: string) {
|
||||
return window.createTextEditorDecorationType(
|
||||
{
|
||||
backgroundColor: bgColor,
|
||||
isWholeLine: true,
|
||||
overviewRulerLane: OverviewRulerLane.Full,
|
||||
overviewRulerColor: bgColor.replace(RegExp(/0\.(\d+)/), '0.8'),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Get the config from a given path or use default
|
||||
function getConfig <Type>(
|
||||
root: WorkspaceConfiguration,
|
||||
path: string,
|
||||
defaultVal: Type,
|
||||
): Type {
|
||||
return root.get(path) || defaultVal;
|
||||
}
|
||||
|
||||
// Essentially a Singleton
|
||||
export default class ConfigProvider {
|
||||
private static instance: ConfigProvider;
|
||||
|
||||
public coverageFileName!: string;
|
||||
public coverageFilePath!: string;
|
||||
public replacePath!: string;
|
||||
public replacePathWith!: string;
|
||||
|
||||
// TextEditorDecorations
|
||||
public excludedDecor!: TextEditorDecorationType;
|
||||
public executedDecor!: TextEditorDecorationType;
|
||||
public missingDecor!: TextEditorDecorationType;
|
||||
|
||||
constructor() {
|
||||
Logger.log('[Initialising] ConfigProvider');
|
||||
this.setupConfigs();
|
||||
// Bind the setup method to re-run and update cached configs
|
||||
workspace.onDidChangeConfiguration(this.setupConfigs.bind(this));
|
||||
}
|
||||
|
||||
public static getInstance(): ConfigProvider {
|
||||
if (!ConfigProvider.instance) {
|
||||
ConfigProvider.instance = new ConfigProvider();
|
||||
}
|
||||
|
||||
return ConfigProvider.instance;
|
||||
}
|
||||
|
||||
private setupConfigs() {
|
||||
Logger.log('[Updating] ConfigProvider');
|
||||
|
||||
// WorkspaceConfiguration(s)
|
||||
const rootConfig = workspace.getConfiguration(Configs.root);
|
||||
const colorsConfig = workspace.getConfiguration(Configs.colorsRoot);
|
||||
|
||||
// Updating class memebers
|
||||
this.coverageFileName = getConfig<string>(
|
||||
rootConfig,
|
||||
Configs.coverageFileName.path,
|
||||
Configs.coverageFileName.default,
|
||||
);
|
||||
|
||||
this.coverageFilePath = getConfig<string>(
|
||||
rootConfig,
|
||||
Configs.coverageFilePath.path,
|
||||
Configs.coverageFilePath.default,
|
||||
);
|
||||
|
||||
this.replacePath = getConfig<string>(
|
||||
rootConfig,
|
||||
Configs.replacePath.path,
|
||||
Configs.replacePath.default,
|
||||
);
|
||||
|
||||
this.replacePathWith = getConfig<string>(
|
||||
rootConfig,
|
||||
Configs.replacePathWith.path,
|
||||
Configs.replacePathWith.default,
|
||||
);
|
||||
|
||||
// Colors
|
||||
const excludedColor = getConfig<string>(
|
||||
colorsConfig,
|
||||
Configs.excludedColor.path,
|
||||
Configs.excludedColor.default,
|
||||
);
|
||||
|
||||
const executedColor = getConfig<string>(
|
||||
colorsConfig,
|
||||
Configs.executedColor.path,
|
||||
Configs.executedColor.default,
|
||||
);
|
||||
|
||||
const missingColor = getConfig<string>(
|
||||
colorsConfig,
|
||||
Configs.missingColor.path,
|
||||
Configs.missingColor.default,
|
||||
);
|
||||
|
||||
this.excludedDecor = createDecor(excludedColor);
|
||||
this.executedDecor = createDecor(executedColor);
|
||||
this.missingDecor = createDecor(missingColor);
|
||||
}
|
||||
}
|
||||
40
src/config/Configs.ts
Normal file
40
src/config/Configs.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export default class Configs {
|
||||
// Base
|
||||
public static extensionName = 'CoverageTool';
|
||||
|
||||
// ConfigPaths
|
||||
public static root = 'coverageTool';
|
||||
public static colorsRoot = `${Configs.root}.colors`;
|
||||
|
||||
// Root config
|
||||
public static coverageFileName = {
|
||||
path: 'coverageFileName',
|
||||
default: 'coverage.json',
|
||||
};
|
||||
public static coverageFilePath = {
|
||||
path: 'coverageFilePath',
|
||||
default: '',
|
||||
};
|
||||
public static replacePath = {
|
||||
path: 'replacePath',
|
||||
default: '',
|
||||
};
|
||||
public static replacePathWith = {
|
||||
path: 'replacePathWith',
|
||||
default: '',
|
||||
};
|
||||
|
||||
// Colors nesting
|
||||
public static excludedColor = {
|
||||
path: 'excludedColor',
|
||||
default: 'rgba(255, 255, 0, 0)',
|
||||
};
|
||||
public static executedColor = {
|
||||
path: 'executedColor',
|
||||
default: 'rgba(0, 255, 0, 0)',
|
||||
};
|
||||
public static missingColor = {
|
||||
path: 'missingColor',
|
||||
default: 'rgba(255, 0, 0, 0.15)',
|
||||
};
|
||||
}
|
||||
229
src/extension.ts
Normal file
229
src/extension.ts
Normal file
@ -0,0 +1,229 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import * as vscode from 'vscode';
|
||||
import { readFile, watch, accessSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { platform } from 'os';
|
||||
import ConfigProvider from './config/ConfigProvider';
|
||||
import CoverageStatusBarItem from './statusBar/CoverageStatusBarItem';
|
||||
import { CoverageStats, ICoverageCache, ICoverageStatsJson, IFileDecorationCache, IFileDecorationRange } from './models';
|
||||
import Logger from './util/Logger';
|
||||
import Configs from './config/Configs';
|
||||
|
||||
// Promisified Functions
|
||||
const readFileAsync = promisify(readFile);
|
||||
|
||||
const config = ConfigProvider.getInstance();
|
||||
const statusBarItem = CoverageStatusBarItem.getInstance();
|
||||
const isWindows = platform() === 'win32';
|
||||
|
||||
// Caches/Dynamic
|
||||
let fileWatchers: vscode.FileSystemWatcher[] = [];
|
||||
let COV_CACHE: ICoverageCache = {};
|
||||
let FILE_CACHE: IFileDecorationCache = {};
|
||||
|
||||
|
||||
// Functions
|
||||
async function getCoverageFileUri() : Promise<vscode.Uri> {
|
||||
let fileUri: vscode.Uri = vscode.Uri.file(config.coverageFileName);
|
||||
|
||||
if (config.coverageFilePath) {
|
||||
const filePathUri = vscode.Uri.file(config.coverageFilePath);
|
||||
fileUri = vscode.Uri.joinPath(filePathUri, config.coverageFileName);
|
||||
}
|
||||
Logger.log(`[CoverageFile][FromConfig] ${fileUri.path}`);
|
||||
|
||||
return fileUri;
|
||||
}
|
||||
|
||||
async function getCoverageFileData(files: vscode.Uri[]): Promise<any> {
|
||||
const promises = Promise.all(
|
||||
files.map((file, i) => {
|
||||
Logger.log(`[CoverageFile][Reading] (${i + 1}/${files.length}) ${file.fsPath}`);
|
||||
return readFileAsync(file.fsPath);
|
||||
}),
|
||||
);
|
||||
|
||||
const mergedCovData = (await promises)
|
||||
.reduce(
|
||||
(acc, content, i, arr) => {
|
||||
Logger.log(`[CoverageFile][Parsing] ${i + 1}/${arr.length}`);
|
||||
const jsonData = JSON.parse(content.toString());
|
||||
Logger.log(`[CoverageFile][Parsed] ${i + 1}/${arr.length}`);
|
||||
return Object.assign(acc, jsonData);
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return mergedCovData;
|
||||
}
|
||||
|
||||
function processJsonCoverage(json: any) {
|
||||
const covData: ICoverageCache = {};
|
||||
|
||||
if (json && json.files) {
|
||||
// Look for the 'files' key in coverage JSON, and iterate through each file
|
||||
Object.keys(json.files).forEach((file: string) => {
|
||||
// Create CoverageStats for each file and assign to covData
|
||||
const data: ICoverageStatsJson = json.files[file];
|
||||
const stats = new CoverageStats(file, data);
|
||||
|
||||
covData[stats.replacedPath] = stats;
|
||||
covData[stats.replacedPath.toLowerCase()] = stats;
|
||||
|
||||
if (config.replacePath) {
|
||||
const replacedStats = new CoverageStats(
|
||||
file, data,
|
||||
config.replacePath,
|
||||
config.replacePathWith,
|
||||
);
|
||||
covData[replacedStats.replacedPath] = replacedStats;
|
||||
covData[replacedStats.replacedPath.toLowerCase()] = replacedStats;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return covData;
|
||||
}
|
||||
|
||||
async function updateCache(files: vscode.Uri) {
|
||||
const existingFile = await vscode.workspace.findFiles(`**${files.path}`);
|
||||
if (existingFile.length > 0) {
|
||||
Logger.log('[Updating][CoverageCache]');
|
||||
const data = await getCoverageFileData(existingFile);
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
FILE_CACHE = {};
|
||||
COV_CACHE = processJsonCoverage(data);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.log('No data found, could not update cache');
|
||||
}
|
||||
}
|
||||
|
||||
function updateDecorations(editor: vscode.TextEditor, coverage: IFileDecorationRange) {
|
||||
editor.setDecorations(config.excludedDecor, coverage.excludedRanges);
|
||||
editor.setDecorations(config.missingDecor, coverage.missingRanges);
|
||||
editor.setDecorations(config.executedDecor, coverage.executedRanges);
|
||||
}
|
||||
|
||||
function updateFileHighlight(editor: vscode.TextEditor) {
|
||||
statusBarItem.update({ loading: true });
|
||||
|
||||
const fullPath = editor.document.uri.fsPath;
|
||||
const fullPathLower = fullPath.toLowerCase();
|
||||
const { path } = editor.document.uri;
|
||||
const pathLower = path.toLowerCase();
|
||||
|
||||
if (fullPath in FILE_CACHE) {
|
||||
Logger.log(`[Updating][FileHighlight][FoundInCache] ${fullPath}`);
|
||||
const cachedDecorations = FILE_CACHE[fullPath];
|
||||
const cachedCoverage = COV_CACHE[fullPath];
|
||||
|
||||
updateDecorations(editor, cachedDecorations);
|
||||
statusBarItem.update({ loading: false, stats: cachedCoverage });
|
||||
return;
|
||||
}
|
||||
|
||||
const decorations: IFileDecorationRange = {
|
||||
excludedRanges: [],
|
||||
executedRanges: [],
|
||||
missingRanges: [],
|
||||
};
|
||||
|
||||
const matchingFile = Object.keys(COV_CACHE).find((file) => {
|
||||
// If current file matches the path
|
||||
if (file === path || file === fullPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If file is a substring of the path
|
||||
// For cases when coverage.json does not have full paths
|
||||
if (path.includes(file) || fullPath.includes(file)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
// Process file names to remove ambiguity
|
||||
const fileLow = file.toLowerCase();
|
||||
if (pathLower.includes(fileLow) || fullPathLower.includes(fileLow)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
let covStats;
|
||||
if (matchingFile) {
|
||||
covStats = COV_CACHE[matchingFile];
|
||||
}
|
||||
|
||||
if (covStats) {
|
||||
Logger.log(`[Updating][FileHighlight] ${fullPath}`);
|
||||
decorations.excludedRanges = covStats.excludedLines.map(
|
||||
(lineNum) => editor.document.lineAt(lineNum - 1).range,
|
||||
);
|
||||
decorations.executedRanges = covStats.executedLines.map(
|
||||
(lineNum) => editor.document.lineAt(lineNum - 1).range,
|
||||
);
|
||||
decorations.missingRanges = covStats.missingLines.map(
|
||||
(lineNum) => editor.document.lineAt(lineNum - 1).range,
|
||||
);
|
||||
|
||||
updateDecorations(editor, decorations);
|
||||
FILE_CACHE[fullPath] = decorations;
|
||||
statusBarItem.update({ loading: false, stats: covStats });
|
||||
} else {
|
||||
statusBarItem.update({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
function setupFileWatchers(files: vscode.Uri) {
|
||||
Logger.log(`[FileWatchers][Disposing] ${fileWatchers.length}`);
|
||||
fileWatchers.map((watcher) => watcher.dispose());
|
||||
fileWatchers = [];
|
||||
|
||||
Logger.log(`[FileWatchers][Creating] ${files.path}`);
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||
`**${files.path}`,
|
||||
false, false, false,
|
||||
);
|
||||
|
||||
watcher.onDidChange(() => updateCache(files));
|
||||
watcher.onDidCreate(() => updateCache(files));
|
||||
watcher.onDidDelete(() => updateCache(files));
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
async function setupCacheAndWatchers() {
|
||||
const files = await getCoverageFileUri();
|
||||
await updateCache(files);
|
||||
setupFileWatchers(files);
|
||||
}
|
||||
|
||||
// This method is called when your extension is activated
|
||||
// Your extension is activated the very first time the command is executed
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Use the console to output diagnostic information (console.log) and errors (console.error)
|
||||
// This line of code will only be executed once when your extension is activated
|
||||
console.log(`[Activating] ${Configs.extensionName}`);
|
||||
await setupCacheAndWatchers();
|
||||
|
||||
if (vscode.window.activeTextEditor) {
|
||||
updateFileHighlight(vscode.window.activeTextEditor);
|
||||
}
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor((editor) => {
|
||||
if (editor && !/extension-output-#\d/.test(editor.document.fileName)) {
|
||||
updateFileHighlight(editor);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export function deactivate() {}
|
||||
85
src/models.ts
Normal file
85
src/models.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Range } from 'vscode';
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
export interface ICoverageStatsJson {
|
||||
executed_lines: number[];
|
||||
missing_lines: number[];
|
||||
excluded_lines: number[];
|
||||
summary: {
|
||||
covered_lines: number;
|
||||
num_statements: number;
|
||||
percent_covered: number;
|
||||
missing_lines: number;
|
||||
excluded_lines: number;
|
||||
// If branch is enabled
|
||||
num_branches?: number;
|
||||
num_partial_branches?: number;
|
||||
covered_branches?: number;
|
||||
missing_branches?: number;
|
||||
};
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
export interface ICoverageCache {
|
||||
[key: string]: CoverageStats;
|
||||
}
|
||||
|
||||
export interface IFileDecorationRange {
|
||||
excludedRanges: Range[];
|
||||
executedRanges: Range[];
|
||||
missingRanges: Range[];
|
||||
}
|
||||
|
||||
export interface IFileDecorationCache {
|
||||
[key: string]: IFileDecorationRange;
|
||||
}
|
||||
|
||||
export class CoverageStats {
|
||||
public path: string;
|
||||
public replacedPath: string;
|
||||
public excludedLines: number[];
|
||||
public executedLines: number[];
|
||||
public missingLines: number[];
|
||||
public summary: {
|
||||
coveredLines: number;
|
||||
excludedLines: number;
|
||||
missingLines: number;
|
||||
numStatements: number;
|
||||
percentCovered: number;
|
||||
// If branch is enabled
|
||||
numBranches: number;
|
||||
numPartialBranches: number;
|
||||
coveredBranches: number;
|
||||
missingBranches: number;
|
||||
};
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
stats: ICoverageStatsJson,
|
||||
pathToReplace: string = '',
|
||||
replacePathWith: string = '',
|
||||
) {
|
||||
this.path = path;
|
||||
|
||||
if (pathToReplace && replacePathWith) {
|
||||
this.replacedPath = path.replace(pathToReplace, replacePathWith);
|
||||
} else {
|
||||
this.replacedPath = path;
|
||||
}
|
||||
this.excludedLines = stats.excluded_lines;
|
||||
this.executedLines = stats.executed_lines;
|
||||
this.missingLines = stats.missing_lines;
|
||||
this.summary = {
|
||||
coveredLines: stats.summary.covered_lines,
|
||||
excludedLines: stats.summary.excluded_lines,
|
||||
missingLines: stats.summary.missing_lines,
|
||||
numStatements: stats.summary.num_statements,
|
||||
percentCovered: stats.summary.percent_covered,
|
||||
// If branch is enabled
|
||||
numBranches: stats.summary?.num_branches ?? 0,
|
||||
numPartialBranches: stats.summary?.num_partial_branches ?? 0,
|
||||
coveredBranches: stats.summary?.covered_branches ?? 0,
|
||||
missingBranches: stats.summary?.missing_branches ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
79
src/statusBar/CoverageStatusBarItem.ts
Normal file
79
src/statusBar/CoverageStatusBarItem.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import {
|
||||
window,
|
||||
StatusBarAlignment,
|
||||
StatusBarItem,
|
||||
} from 'vscode';
|
||||
import { CoverageStats } from '../models';
|
||||
import Logger from '../util/Logger';
|
||||
|
||||
export default class CoverageStatusBarItem {
|
||||
private static instance: CoverageStatusBarItem;
|
||||
private statusBarItem: StatusBarItem;
|
||||
|
||||
// TODO: add configProvider in constructor if configs required
|
||||
constructor() {
|
||||
Logger.log('[Initialising] ConfigProvider');
|
||||
|
||||
this.statusBarItem = window.createStatusBarItem(
|
||||
StatusBarAlignment.Left,
|
||||
);
|
||||
this.update({ loading: true });
|
||||
}
|
||||
|
||||
public static getInstance(): CoverageStatusBarItem {
|
||||
if (!CoverageStatusBarItem.instance) {
|
||||
CoverageStatusBarItem.instance = new CoverageStatusBarItem();
|
||||
}
|
||||
|
||||
return CoverageStatusBarItem.instance;
|
||||
}
|
||||
|
||||
public update(
|
||||
{ loading = true, stats }:
|
||||
{ loading?: boolean, stats?: CoverageStats },
|
||||
) {
|
||||
const staticIcon = '$(globe)';
|
||||
const spinningIcon = '$(globe~spin)';
|
||||
const coveredIcon = '$(pass)';
|
||||
const missingIcon = '$(stop)';
|
||||
const excludedIcon = '$(stop-circle)';
|
||||
|
||||
if (loading) {
|
||||
this.statusBarItem.text = `${spinningIcon}`;
|
||||
this.statusBarItem.tooltip = '';
|
||||
this.statusBarItem.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (stats) {
|
||||
const percent = stats.summary.percentCovered.toFixed(2);
|
||||
const {
|
||||
coveredLines, missingLines, excludedLines,
|
||||
numBranches, coveredBranches, missingBranches,
|
||||
numPartialBranches: partialBranches,
|
||||
} = stats.summary;
|
||||
|
||||
const textItems = [
|
||||
`${staticIcon} ${percent}%`,
|
||||
`${coveredIcon} ${coveredLines}`,
|
||||
`${missingIcon} ${missingLines}`,
|
||||
`${excludedIcon} ${excludedLines}`,
|
||||
];
|
||||
const tooltipLines = [
|
||||
`Coverage: ${percent}%`,
|
||||
`- Covered: ${coveredLines}`,
|
||||
`- Missing: ${missingLines}`,
|
||||
`- Excluded: ${excludedLines}`,
|
||||
];
|
||||
|
||||
this.statusBarItem.text = textItems.join(' ');
|
||||
this.statusBarItem.tooltip = tooltipLines.join('\n');
|
||||
this.statusBarItem.show();
|
||||
return;
|
||||
}
|
||||
|
||||
this.statusBarItem.text = `${staticIcon} Coverage`;
|
||||
this.statusBarItem.tooltip = '';
|
||||
this.statusBarItem.show();
|
||||
}
|
||||
}
|
||||
15
src/test/extension.test.ts
Normal file
15
src/test/extension.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||
});
|
||||
});
|
||||
10
src/util/Logger.ts
Normal file
10
src/util/Logger.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { window } from 'vscode';
|
||||
import Configs from '../config/Configs';
|
||||
|
||||
export default class Logger {
|
||||
private static outputChannel = window.createOutputChannel(Configs.extensionName);
|
||||
|
||||
public static log(data: string) {
|
||||
this.outputChannel.appendLine(data);
|
||||
}
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2022",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
}
|
||||
}
|
||||
43
vsc-extension-quickstart.md
Normal file
43
vsc-extension-quickstart.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Welcome to your VS Code Extension
|
||||
|
||||
## What's in the folder
|
||||
|
||||
* This folder contains all of the files necessary for your extension.
|
||||
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
|
||||
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||
|
||||
## Get up and running straight away
|
||||
|
||||
* Press `F5` to open a new window with your extension loaded.
|
||||
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
|
||||
* Find output from your extension in the debug console.
|
||||
|
||||
## Make changes
|
||||
|
||||
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
|
||||
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||
|
||||
## Explore the API
|
||||
|
||||
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||
|
||||
## Run tests
|
||||
|
||||
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
||||
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
|
||||
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
||||
* See the output of the test result in the Test Results view.
|
||||
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
|
||||
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
|
||||
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||
|
||||
## Go further
|
||||
|
||||
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
|
||||
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
|
||||
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
||||
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
||||
Loading…
Reference in New Issue
Block a user