Skip to content

Commit da3617b

Browse files
authored
Create coverage UI (#122)
1 parent 8e2f122 commit da3617b

File tree

6 files changed

+443
-2
lines changed

6 files changed

+443
-2
lines changed

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@
5252
"title": "Disable Codelens",
5353
"command": "codelens-kani.disableCodeLens",
5454
"category": "CodeLens Sample"
55+
},
56+
{
57+
"command": "codelens-kani.highlightCoverage",
58+
"title": "Highlight Coverage"
59+
},
60+
{
61+
"command": "codelens-kani.dehighlightCoverage",
62+
"title": "De-Highlight Coverage"
5563
}
5664
],
5765
"configuration": {
@@ -61,6 +69,11 @@
6169
"type": "boolean",
6270
"default": true
6371
},
72+
"codelens-kani.highlightCoverage": {
73+
"type": "boolean",
74+
"default": false,
75+
"description": "Controls the visibility of the `generate coverage` button by default."
76+
},
6477
"Kani.showOutputWindow": {
6578
"type": "boolean",
6679
"default": false,

src/extension.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
import { CodelensProvider } from './ui/CodeLensProvider';
2020
import { callConcretePlayback } from './ui/concrete-playback/concretePlayback';
2121
import { runKaniPlayback } from './ui/concrete-playback/kaniPlayback';
22+
import CoverageConfig from './ui/coverage/config';
23+
import { CoverageRenderer, runCodeCoverageAction } from './ui/coverage/coverageInfo';
2224
import { callViewerReport } from './ui/reportView/callReport';
2325
import { showInformationMessage } from './ui/showMessage';
2426
import { SourceCodeParser } from './ui/sourceCodeParser';
@@ -64,6 +66,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
6466

6567
// create a uri for the root folder
6668
context.subscriptions.push(controller);
69+
// Store coverage objects in a global cache when highlighting. When de-highlighting, the same objects need to be disposed
70+
const coverageConfig = new CoverageConfig(context);
71+
const globalConfig = GlobalConfig.getInstance();
6772
const crateURI: Uri = getRootDirURI();
6873
const treeRoot: vscode.TestItem = controller.createTestItem(
6974
'Kani proofs',
@@ -225,20 +230,49 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
225230
vscode.workspace.getConfiguration('codelens-kani').update('enableCodeLens', true, true);
226231
});
227232

228-
// Allows VSCode to disable VSCode globally
233+
// Allows VSCode to disable code lens globally
229234
vscode.commands.registerCommand('codelens-kani.disableCodeLens', () => {
230235
vscode.workspace.getConfiguration('codelens-kani').update('enableCodeLens', false, true);
231236
});
232237

238+
// Allows VSCode to enable highlighting globally
239+
vscode.commands.registerCommand('codelens-kani.enableCoverage', () => {
240+
vscode.workspace.getConfiguration('codelens-kani').update('highlightCoverage', true, true);
241+
});
242+
243+
// Allows VSCode to disable highlighting globally
244+
vscode.commands.registerCommand('codelens-kani.disableCoverage', () => {
245+
vscode.workspace.getConfiguration('codelens-kani').update('highlightCoverage', false, true);
246+
});
247+
233248
// Register the command for the code lens Kani test runner function
234249
vscode.commands.registerCommand('codelens-kani.codelensAction', (args: any) => {
235250
runKaniPlayback(args);
236251
});
237252

253+
// Separate rendering logic and re-use everywhere to highlight and de-highlight
254+
const renderer = new CoverageRenderer(coverageConfig);
255+
256+
// Register a command to de-highlight the coverage in the active editor
257+
const dehighlightCoverageCommand = vscode.commands.registerCommand(
258+
'codelens-kani.dehighlightCoverage',
259+
() => {
260+
globalConfig.setCoverage(new Map());
261+
renderer.renderInterface(vscode.window.visibleTextEditors, new Map());
262+
},
263+
);
264+
238265
// Update the test tree with proofs whenever a test case is opened
239266
context.subscriptions.push(
240267
vscode.workspace.onDidOpenTextDocument(updateNodeForDocument),
241268
vscode.workspace.onDidSaveTextDocument(async (e) => await updateNodeForDocument(e)),
269+
vscode.window.onDidChangeActiveTextEditor((editor) => {
270+
// VS Code resets highlighting whenever a document is changed or switched.
271+
// In order to work around this issue, the highlighting needs to be rendered each time.
272+
// To keep the rendering time short, we store the coverage metadata as a global cache.
273+
const cache = globalConfig.getCoverage();
274+
renderer.renderInterfaceForFile(editor!, cache);
275+
}),
242276
);
243277

244278
context.subscriptions.push(runKani);
@@ -251,4 +285,13 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
251285
connectToDebugger(programName),
252286
),
253287
);
288+
// Register the command for running the coverage action on a harness
289+
context.subscriptions.push(
290+
vscode.commands.registerCommand('codelens-kani.highlightCoverage', (args: any) => {
291+
runCodeCoverageAction(renderer, args);
292+
}),
293+
);
294+
295+
// Register the command for de-highlighting kani's coverage action on a harness
296+
context.subscriptions.push(dehighlightCoverageCommand);
254297
}

src/globalConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
class GlobalConfig {
44
private static instance: GlobalConfig;
55
private filePath: string;
6+
public coverageMap: any;
67

78
private constructor() {
89
this.filePath = '';
@@ -15,6 +16,16 @@ class GlobalConfig {
1516
return GlobalConfig.instance;
1617
}
1718

19+
// Store coverage as a cache to be accessed whenever a new text page is opened or switched
20+
public setCoverage(coverageMap: any): void {
21+
this.coverageMap = coverageMap;
22+
}
23+
24+
// Retrieve coverage as a cache to be accessed whenever a new text page is opened or switched
25+
public getCoverage(): any {
26+
return this.coverageMap;
27+
}
28+
1829
public setFilePath(filePath: string): void {
1930
this.filePath = filePath;
2031
}

src/ui/CodeLensProvider.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class CodelensProvider implements vscode.CodeLensProvider {
4444
const function_item_name = item.at(0);
4545

4646
if (function_item_name === undefined) {
47-
return [];
47+
continue;
4848
}
4949

5050
const startPosition = item.at(1);
@@ -77,6 +77,43 @@ export class CodelensProvider implements vscode.CodeLensProvider {
7777
this.codeLenses.push(debugTestCodelens);
7878
}
7979
}
80+
81+
// The setting for the coverage code lens needs to be switched on for the mechanism
82+
// to be enabled
83+
if (vscode.workspace.getConfiguration('codelens-kani').get('highlightCoverage', true)) {
84+
// Retrieve harness metadata from the tree-sitter which will be used to place the
85+
// `Get coverage info` code lens button.
86+
const kani_harnesses = await SourceCodeParser.getAttributeFromRustFile(text);
87+
88+
for (const harness of kani_harnesses) {
89+
const harness_name = harness.harnessName;
90+
91+
// If the harness is empty or undefined, skip to the next iteration
92+
if (harness_name === undefined || harness_name === '') {
93+
continue;
94+
}
95+
96+
const startPosition = harness.endPosition;
97+
98+
// This is the metadata that VSCode needs to place the codelens button
99+
const line = document.lineAt(startPosition.row);
100+
const indexOf = line.text.indexOf(harness_name);
101+
const position = new vscode.Position(line.lineNumber, indexOf);
102+
const range = document.getWordRangeAtPosition(position);
103+
104+
const codeCoverageAction = {
105+
title: '$(play) Get coverage info',
106+
tooltip: 'Highlight code with coverage information generated by Kani',
107+
command: 'codelens-kani.highlightCoverage',
108+
arguments: [harness_name],
109+
};
110+
111+
if (range) {
112+
const codeCoverageCodelens = new vscode.CodeLens(range, codeCoverageAction);
113+
this.codeLenses.push(codeCoverageCodelens);
114+
}
115+
}
116+
}
80117
return this.codeLenses;
81118
}
82119

src/ui/coverage/config.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
import { DecorationRenderOptions, ExtensionContext, TextEditorDecorationType, window } from "vscode";
5+
6+
// Takes the extension context and stores the specified decoration (VS Code highliging API) values for that context
7+
// By storing the decoration values per context, we allow users to de-highlight the same contexts.
8+
// Without caching these values as a global cache, VS Code does not de-highlight.
9+
class CoverageConfig {
10+
public covered!: TextEditorDecorationType;
11+
public partialcovered!: TextEditorDecorationType;
12+
public uncovered!: TextEditorDecorationType;
13+
14+
private context: ExtensionContext;
15+
16+
constructor(context: ExtensionContext) {
17+
this.context = context;
18+
this.setup();
19+
}
20+
21+
private setup(): void {
22+
const fullDecoration: DecorationRenderOptions = {
23+
backgroundColor: 'rgba(0, 255, 0, 0.2)', // Green background
24+
isWholeLine: false
25+
};
26+
27+
const partialDecoration: DecorationRenderOptions = {
28+
backgroundColor: 'rgba(255, 255, 0, 0.2)', // Yellow background
29+
isWholeLine: false
30+
};
31+
32+
const noDecoration: DecorationRenderOptions = {
33+
backgroundColor: 'rgba(255, 0, 0, 0.2)', // Red background
34+
isWholeLine: false
35+
};
36+
37+
this.covered = window.createTextEditorDecorationType(fullDecoration);
38+
this.partialcovered = window.createTextEditorDecorationType(partialDecoration);
39+
this.uncovered = window.createTextEditorDecorationType(noDecoration);
40+
}
41+
}
42+
43+
export default CoverageConfig;

0 commit comments

Comments
 (0)