From 09c3b4f5221c26c81baa171b710f0cfa4a05f2db Mon Sep 17 00:00:00 2001 From: Ryan Luker Date: Sun, 30 Apr 2017 23:40:57 -0700 Subject: [PATCH] Add status icon feature and cleanup (#56) * 54: remove process env GA method * 55: use rel sf compare by default add metric to judge the number of people swapping back * 53: collapse watch commands and add remove * cleanup tslint * 53: add statusbar item and toggler * 51: switch to DI for gutters * cleanup tool tip naming * add more tests to gutters * cleanup textEditor being null * cleanup error case on render coverage --- README.md | 2 +- package.json | 42 +++++++++--------- src/config.ts | 3 ++ src/extension.ts | 31 +++++++++---- src/gutters.ts | 96 +++++++++++++++++++++++------------------ src/reporter.ts | 8 ++-- src/statusbartoggler.ts | 38 ++++++++++++++++ test/gutters.test.ts | 66 ++++++++++++++++++++++++---- test/reporter.test.ts | 10 ++--- 9 files changed, 207 insertions(+), 89 deletions(-) create mode 100644 src/statusbartoggler.ts diff --git a/README.md b/README.md index ef1c932..e1b67de 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ |Setting | Description |--------|------------ |`coverage-gutters.lcovname`|Allows specification of a custom lcov file name -|`coverage-gutters.altSfCompare`|Uses a relative method of comparing lcov source file paths +|`coverage-gutters.altSfCompare`|Uses a relative method of comparing lcov source file paths (default true) |`coverage-gutters.highlightlight`|Changes the highlight for light themes |`coverage-gutters.highlightdark`|Changes the highlight for dark themes |`coverage-gutters.partialHighlightLight`|Changes the partial highlight for light themes diff --git a/package.json b/package.json index 44d8959..ad336b2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "coverage-gutters.altSfCompare": { "type": "boolean", - "default": false, + "default": true, "description": "uses a relative method of comparing lcov source file paths" }, "coverage-gutters.highlightlight": { @@ -123,15 +123,15 @@ "default": true, "description": "enable or disable the displayCoverage command in the editor/context menu" }, - "coverage-gutters.customizable.menus-editor-context-watchLcovFile-enabled": { + "coverage-gutters.customizable.menus-editor-context-watchLcovAndVisibleEditors-enabled": { "type": "boolean", "default": true, - "description": "enable or disable the watchLcovFile command in the editor/context menu" + "description": "enable or disable the watchLcovAndVisibleEditors command in the editor/context menu" }, - "coverage-gutters.customizable.menus-editor-context-watchVisibleEditors-enabled": { + "coverage-gutters.customizable.menus-editor-context-removeWatch-enabled": { "type": "boolean", "default": true, - "description": "enable or disable the watchVisibleEditors command in the editor/context menu" + "description": "enable or disable the removeWatch command in the editor/context menu" }, "coverage-gutters.customizable.menus-editor-context-removeCoverage-enabled": { "type": "boolean", @@ -143,15 +143,15 @@ "default": true, "description": "enable or disable the keybinding shortcut for enabling coverage on the active file" }, - "coverage-gutters.customizable.keybindings-watchLcovFile-enabled": { + "coverage-gutters.customizable.keybindings-watchLcovAndVisibleEditors-enabled": { "type": "boolean", "default": true, - "description": "enable or disable the keybinding shortcut for watching the lcov file" + "description": "enable or disable the keybinding shortcut for watching the lcov file and visible editors" }, - "coverage-gutters.customizable.keybindings-watchVisibleEditors-enabled": { + "coverage-gutters.customizable.keybindings-removeWatch-enabled": { "type": "boolean", "default": true, - "description": "enable or disable the keybinding shortcut for watching visible editors" + "description": "enable or disable the keybinding shortcut for removing the watch on lcov and editors" }, "coverage-gutters.customizable.keybindings-removeCoverage-enabled": { "type": "boolean", @@ -166,12 +166,12 @@ "title": "Coverage Gutters: Display File Coverage" }, { - "command": "extension.watchLcovFile", - "title": "Coverage Gutters: Watch Lcov and Render Changes" + "command": "extension.watchLcovAndVisibleEditors", + "title": "Coverage Gutters: Watch Lcov File and Visible Editors" }, { - "command": "extension.watchVisibleEditors", - "title": "Coverage Gutters: Watch Visible Editors and Render Coverage" + "command": "extension.removeWatch", + "title": "Coverage Gutters: Remove Lcov and Editor Watch" }, { "command": "extension.removeCoverage", @@ -186,16 +186,16 @@ "when": "config.coverage-gutters.customizable.keybindings-displayCoverage-enabled" }, { - "command": "extension.watchLcovFile", + "command": "extension.watchLcovAndVisibleEditors", "key": "ctrl+shift+8", "mac": "shift+cmd+8", - "when": "config.coverage-gutters.customizable.keybindings-watchLcovFile-enabled" + "when": "config.coverage-gutters.customizable.keybindings-watchLcovAndVisibleEditors-enabled" }, { - "command": "extension.watchVisibleEditors", + "command": "extension.removeWatch", "key": "ctrl+shift+9", "mac": "shift+cmd+9", - "when": "config.coverage-gutters.customizable.keybindings-watchVisibleEditors-enabled" + "when": "config.coverage-gutters.customizable.keybindings-removeWatch-enabled" }, { "command": "extension.removeCoverage", @@ -212,13 +212,13 @@ "group": "Coverage-Gutters@1" }, { - "when": "config.coverage-gutters.customizable.menus-editor-context-watchLcovFile-enabled", - "command": "extension.watchLcovFile", + "when": "config.coverage-gutters.customizable.menus-editor-context-watchLcovAndVisibleEditors-enabled", + "command": "extension.watchLcovAndVisibleEditors", "group": "Coverage-Gutters@2" }, { - "when": "config.coverage-gutters.customizable.menus-editor-context-watchVisibleEditors-enabled", - "command": "extension.watchVisibleEditors", + "when": "config.coverage-gutters.customizable.menus-editor-context-removeWatch-enabled", + "command": "extension.removeWatch", "group": "Coverage-Gutters@3" }, { diff --git a/src/config.ts b/src/config.ts index 8379e58..c92b8a9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,7 @@ export class Config { this.vscode = vscode; this.context = context; this.reporter = reporter; + this.setup(); } public get(): IConfigStore { @@ -60,6 +61,8 @@ export class Config { // Basic configurations this.lcovFileName = rootConfig.get("lcovname") as string; this.altSfCompare = rootConfig.get("altSfCompare") as boolean; + this.reporter.sendEvent("config", "lcovFileName", this.lcovFileName); + this.reporter.sendEvent("config", "altSfCompare", this.altSfCompare.toString()); // Themes and icons const coverageLightBackgroundColour = rootConfig.get("highlightlight") as string; diff --git a/src/extension.ts b/src/extension.ts index ddece26..7eab880 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,24 +1,39 @@ import * as vscode from "vscode"; +import {Config} from "./config"; import {Gutters} from "./gutters"; +import {Indicators} from "./indicators"; +import {Lcov} from "./lcov"; import {Reporter} from "./reporter"; +import {StatusBarToggler} from "./statusbartoggler"; +import {Fs} from "./wrappers/fs"; +import {LcovParse} from "./wrappers/lcov-parse"; import {Request} from "./wrappers/request"; import {Uuid} from "./wrappers/uuid"; +import {Vscode} from "./wrappers/vscode"; + +const fsImpl = new Fs(); +const parseImpl = new LcovParse(); +const vscodeImpl = new Vscode(); export function activate(context: vscode.ExtensionContext) { const enableMetrics = vscode.workspace.getConfiguration("telemetry").get("enableTelemetry") as boolean; - const reporter = new Reporter(new Request(), new Uuid(), enableMetrics); - const gutters = new Gutters(context, reporter); + const reporter = new Reporter(new Request(), new Uuid(), "", enableMetrics); + const statusBarToggler = new StatusBarToggler(); + const configStore = new Config(vscodeImpl, context, reporter).get(); + const lcov = new Lcov(configStore, vscodeImpl, fsImpl); + const indicators = new Indicators(parseImpl, vscodeImpl, configStore); + const gutters = new Gutters(configStore, lcov, indicators, reporter, statusBarToggler); const display = vscode.commands.registerCommand("extension.displayCoverage", () => { gutters.displayCoverageForActiveFile(); }); - const watchLcovFile = vscode.commands.registerCommand("extension.watchLcovFile", () => { - gutters.watchLcovFile(); + const watch = vscode.commands.registerCommand("extension.watchLcovAndVisibleEditors", () => { + gutters.watchLcovAndVisibleEditors(); }); - const watchVisibleEditors = vscode.commands.registerCommand("extension.watchVisibleEditors", () => { - gutters.watchVisibleEditors(); + const removeWatch = vscode.commands.registerCommand("extension.removeWatch", () => { + gutters.removeWatch(); }); const remove = vscode.commands.registerCommand("extension.removeCoverage", () => { @@ -27,7 +42,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(remove); context.subscriptions.push(display); - context.subscriptions.push(watchLcovFile); - context.subscriptions.push(watchVisibleEditors); + context.subscriptions.push(watch); + context.subscriptions.push(removeWatch); context.subscriptions.push(gutters); } diff --git a/src/gutters.ts b/src/gutters.ts index 8c06343..47b7132 100644 --- a/src/gutters.ts +++ b/src/gutters.ts @@ -1,36 +1,45 @@ import { - ExtensionContext, + Disposable, FileSystemWatcher, + StatusBarItem, TextEditor, version, window, } from "vscode"; -import {Fs} from "./wrappers/fs"; -import {LcovParse} from "./wrappers/lcov-parse"; import {Vscode} from "./wrappers/vscode"; -import {Config, IConfigStore} from "./config"; +import {IConfigStore} from "./config"; import {Indicators} from "./indicators"; import {Lcov} from "./lcov"; import {Reporter} from "./reporter"; +import {StatusBarToggler} from "./statusbartoggler"; const vscodeImpl = new Vscode(); -const fsImpl = new Fs(); -const parseImpl = new LcovParse(); export class Gutters { private configStore: IConfigStore; private lcovWatcher: FileSystemWatcher; + private editorWatcher: Disposable; + private statusBarItem: StatusBarItem; private lcov: Lcov; private indicators: Indicators; private reporter: Reporter; + private statusBar: StatusBarToggler; - constructor(context: ExtensionContext, reporter: Reporter) { - this.configStore = new Config(vscodeImpl, context, reporter).setup(); - this.lcov = new Lcov(this.configStore, vscodeImpl, fsImpl); - this.indicators = new Indicators(parseImpl, vscodeImpl, this.configStore); + constructor( + configStore: IConfigStore, + lcov: Lcov, + indicators: Indicators, + reporter: Reporter, + statusBar: StatusBarToggler, + ) { + this.configStore = configStore; + this.lcov = lcov; + this.indicators = indicators; + this.statusBar = statusBar; this.reporter = reporter; + this.reporter.sendEvent("user", "start"); this.reporter.sendEvent("user", "vscodeVersion", version); } @@ -38,73 +47,70 @@ export class Gutters { public async displayCoverageForActiveFile() { const textEditor = window.activeTextEditor; try { + if (!textEditor) { return; } const lcovPath = await this.lcov.find(); await this.loadAndRenderCoverage(textEditor, lcovPath); + this.reporter.sendEvent("user", "display-coverage"); } catch (error) { this.handleError(error); } } - /** - * Watch the lcov file and iterate over textEditors when changes occur - */ - public async watchLcovFile() { - if (this.lcovWatcher) { return; } + public async watchLcovAndVisibleEditors() { + if (this.lcovWatcher && this.editorWatcher) { return; } try { const lcovPath = await this.lcov.find(); this.lcovWatcher = vscodeImpl.watchFile(lcovPath); - this.lcovWatcher.onDidChange(async (event) => { - window.visibleTextEditors.forEach(async (editor) => { - await this.loadAndRenderCoverage(editor, lcovPath); - }); - }); - this.reporter.sendEvent("user", "watch-lcov"); + this.lcovWatcher.onDidChange((event) => this.renderCoverageOnVisible(lcovPath)); + this.editorWatcher = window.onDidChangeVisibleTextEditors( + (event) => this.renderCoverageOnVisible(lcovPath)); + this.statusBar.toggle(); + + this.reporter.sendEvent("user", "watch-lcov-editors"); } catch (error) { this.handleError(error); } } - /** - * Watch the visible editors and render coverage when changes occur - */ - public async watchVisibleEditors() { - try { - const lcovPath = await this.lcov.find(); - window.onDidChangeVisibleTextEditors(async (event) => { - window.visibleTextEditors.forEach(async (editor) => { - await this.loadAndRenderCoverage(editor, lcovPath); - }); - }); - this.reporter.sendEvent("user", "watch-editors"); - } catch (error) { - this.handleError(error); - } + public removeWatch() { + this.lcovWatcher.dispose(); + this.editorWatcher.dispose(); + this.lcovWatcher = null; + this.editorWatcher = null; + this.statusBar.toggle(); + + this.reporter.sendEvent("user", "remove-watch"); } public removeCoverageForActiveFile() { const activeEditor = window.activeTextEditor; this.removeDecorationsForTextEditor(activeEditor); + this.reporter.sendEvent("user", "remove-coverage"); } public dispose() { this.lcovWatcher.dispose(); + this.editorWatcher.dispose(); + this.statusBar.dispose(); + this.reporter.sendEvent("cleanup", "dispose"); } private handleError(error: Error) { const message = error.message ? error.message : error; window.showWarningMessage(message.toString()); + this.reporter.sendEvent("error", message.toString()); } - private removeDecorationsForTextEditor(editor: TextEditor) { - if (!editor) { return; } - editor.setDecorations(this.configStore.fullCoverageDecorationType, []); - editor.setDecorations(this.configStore.partialCoverageDecorationType, []); - editor.setDecorations(this.configStore.noCoverageDecorationType, []); + private removeDecorationsForTextEditor(textEditor: TextEditor) { + if (!textEditor) { return; } + textEditor.setDecorations(this.configStore.fullCoverageDecorationType, []); + textEditor.setDecorations(this.configStore.partialCoverageDecorationType, []); + textEditor.setDecorations(this.configStore.noCoverageDecorationType, []); } private async loadAndRenderCoverage(textEditor: TextEditor, lcovPath: string): Promise { @@ -112,6 +118,14 @@ export class Gutters { const file = textEditor.document.fileName; const coveredLines = await this.indicators.extract(lcovFile, file); await this.indicators.renderToTextEditor(coveredLines, textEditor); + this.reporter.sendEvent("user", "loadAndRenderCoverage"); } + + private renderCoverageOnVisible(lcovPath: string) { + window.visibleTextEditors.forEach(async (editor) => { + if (!editor) { return; } + await this.loadAndRenderCoverage(editor, lcovPath); + }); + } } diff --git a/src/reporter.ts b/src/reporter.ts index 857950e..da23cd5 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -7,11 +7,11 @@ const EXT_VERSION = "0.5.0"; export class Reporter { private readonly cid: string; private readonly enableMetrics: boolean; - private readonly GA_TRACKING_ID: string; + private readonly gaTrackingId: string; private readonly request: Request; - constructor(request: Request, uuid: Uuid, enableMetrics: boolean) { - this.GA_TRACKING_ID = process.env.GA_TRACKING_ID || ""; + constructor(request: Request, uuid: Uuid, gaTrackingId: string, enableMetrics: boolean) { + this.gaTrackingId = gaTrackingId; this.request = request; this.cid = uuid.get(); this.enableMetrics = enableMetrics; @@ -33,7 +33,7 @@ export class Reporter { el: label, ev: value, t: "event", - tid: this.GA_TRACKING_ID, + tid: this.gaTrackingId, v: "1", }; diff --git a/src/statusbartoggler.ts b/src/statusbartoggler.ts new file mode 100644 index 0000000..d4425c9 --- /dev/null +++ b/src/statusbartoggler.ts @@ -0,0 +1,38 @@ +import {Disposable, StatusBarItem, window} from "vscode"; + +export class StatusBarToggler implements Disposable { + public static readonly watchCommand = "extension.watchLcovAndVisibleEditors"; + public static readonly removeCommand = "extension.removeWatch"; + public static readonly watchText = "$(list-ordered) Watch Lcov and Editors"; + public static readonly removeText = "$(list-ordered) Remove Watch"; + public static readonly toolTip = "Coverage Gutters: Watch and Remove Helper"; + private statusBarItem: StatusBarItem; + + constructor() { + this.statusBarItem = window.createStatusBarItem(); + this.statusBarItem.command = StatusBarToggler.watchCommand; + this.statusBarItem.text = StatusBarToggler.watchText; + this.statusBarItem.tooltip = StatusBarToggler.toolTip; + this.statusBarItem.show(); + } + + /** + * Toggles the status bar item from watch to remove and vice versa + */ + public toggle() { + if (this.statusBarItem.command === StatusBarToggler.watchCommand) { + this.statusBarItem.command = StatusBarToggler.removeCommand; + this.statusBarItem.text = StatusBarToggler.removeText; + } else { + this.statusBarItem.command = StatusBarToggler.watchCommand; + this.statusBarItem.text = StatusBarToggler.watchText; + } + } + + /** + * Cleans up the statusBarItem if asked to dispose + */ + public dispose() { + this.statusBarItem.dispose(); + } +} diff --git a/test/gutters.test.ts b/test/gutters.test.ts index ac95577..12700e9 100644 --- a/test/gutters.test.ts +++ b/test/gutters.test.ts @@ -1,28 +1,78 @@ import * as assert from "assert"; import * as vscode from "vscode"; +import {IConfigStore} from "../src/config"; import {Gutters} from "../src/gutters"; +import {Indicators} from "../src/indicators"; +import {Lcov} from "../src/lcov"; import {Reporter} from "../src/reporter"; +import {StatusBarToggler} from "../src/statusbartoggler"; suite("Gutters Tests", function() { + this.timeout(4000); + test("Should setup gutters based on config values with no errors", function(done) { - this.timeout(12000); try { - const ctx: vscode.ExtensionContext = { - asAbsolutePath() { - return "test"; - }, - subscriptions: [], - } as any; const reporter: Reporter = { sendEvent() { return; }, } as any; + const statusbar: StatusBarToggler = { + dispose() { + return; + }, + } as any; + const lcov: Lcov = {} as any; + const indicators: Indicators = {} as any; + const configStore: IConfigStore = {} as any; - const gutters = new Gutters(ctx, reporter); + const gutters = new Gutters(configStore, lcov, indicators, reporter, statusbar); return done(); } catch (e) { return done(e); } }); + + test("Should not error when trying to render coverage on empty editor", async function() { + let sendEventTimes = 0; + try { + const reporter: Reporter = { + sendEvent(cat, action) { + sendEventTimes++; + return; + }, + } as any; + const statusbar: StatusBarToggler = { + dispose() { + return; + }, + } as any; + const lcov: Lcov = { + find() { + return Promise.resolve("tempPath"); + }, + load(path) { + assert.equal(path, "tempPath"); + return Promise.resolve("filehere"); + }, + } as any; + const indicators: Indicators = { + extract(file) { + assert.equal(file, "filehere"); + return Promise.resolve([1, 2, 3]); + }, + renderToTextEditor(lines) { + assert.equal(lines, [1, 2, 3]); + return Promise.resolve(); + }, + } as any; + const configStore: IConfigStore = {} as any; + + const gutters = new Gutters(configStore, lcov, indicators, reporter, statusbar); + await gutters.displayCoverageForActiveFile(); + assert.equal(sendEventTimes, 2); + } catch (error) { + throw error; + } + }); }); diff --git a/test/reporter.test.ts b/test/reporter.test.ts index 759692e..8c1fdfa 100644 --- a/test/reporter.test.ts +++ b/test/reporter.test.ts @@ -17,7 +17,7 @@ suite("Reporter Tests", function() { }, }; - const reporter = new Reporter(fakeRequest, fakeUuid, false); + const reporter = new Reporter(fakeRequest, fakeUuid, "", false); reporter.sendEvent("test", "action"); }); @@ -36,7 +36,7 @@ suite("Reporter Tests", function() { }, }; - const reporter = new Reporter(fakeRequest, fakeUuid, true); + const reporter = new Reporter(fakeRequest, fakeUuid, "", true); reporter.sendEvent("test", "action"); }); @@ -55,13 +55,11 @@ suite("Reporter Tests", function() { }, }; - const reporter = new Reporter(fakeRequest, fakeUuid, true); + const reporter = new Reporter(fakeRequest, fakeUuid, "", true); reporter.sendEvent("test", "action"); }); test("GA tracking id should be set by env variable", function() { - process.env.GA_TRACKING_ID = "123"; - const fakeRequest = { post(uri: string, options?: IOptions) { // tslint:disable-next-line:no-string-literal @@ -76,7 +74,7 @@ suite("Reporter Tests", function() { }, }; - const reporter = new Reporter(fakeRequest, fakeUuid, true); + const reporter = new Reporter(fakeRequest, fakeUuid, "123", true); reporter.sendEvent("test", "action"); }); });