| /* eslint-disable no-useless-escape */ |
| /* eslint-disable no-prototype-builtins */ |
| /* eslint-disable @typescript-eslint/no-explicit-any */ |
| /*--------------------------------------------------------- |
| * Copyright (C) Microsoft Corporation. All rights reserved. |
| * Licensed under the MIT License. See LICENSE in the project root for license information. |
| *--------------------------------------------------------*/ |
| |
| 'use strict'; |
| |
| import fs = require('fs'); |
| import path = require('path'); |
| import vscode = require('vscode'); |
| import { getGoConfig } from './config'; |
| import { isModSupported } from './goModules'; |
| import { getImportPathToFolder } from './goPackages'; |
| import { getTestFlags, goTest, showTestOutput, TestConfig } from './testUtils'; |
| import { fixDriveCasingInWindows } from './utils/pathUtils'; |
| |
| let gutterSvgs: { [key: string]: string }; |
| |
| interface Highlight { |
| top: vscode.TextEditorDecorationType; |
| mid: vscode.TextEditorDecorationType; |
| bot: vscode.TextEditorDecorationType; |
| all: vscode.TextEditorDecorationType; |
| } |
| |
| let decorators: { |
| type: 'highlight' | 'gutter'; |
| coveredGutter: vscode.TextEditorDecorationType; |
| uncoveredGutter: vscode.TextEditorDecorationType; |
| coveredHighlight: Highlight; |
| uncoveredHighlight: Highlight; |
| }; |
| |
| let decoratorConfig: { |
| [key: string]: any; |
| type: 'highlight' | 'gutter'; |
| coveredHighlightColor: string; |
| uncoveredHighlightColor: string; |
| coveredBorderColor: string; |
| uncoveredBorderColor: string; |
| coveredGutterStyle: string; |
| uncoveredGutterStyle: string; |
| }; |
| |
| // a list of modified, unsaved go files with actual code edits (rather than comment edits) |
| let modifiedFiles: { |
| [key: string]: boolean; |
| } = {}; |
| |
| /** |
| * Initializes the decorators used for Code coverage. |
| * @param ctx The extension context |
| */ |
| export function initCoverageDecorators(ctx: vscode.ExtensionContext) { |
| // Initialize gutter svgs |
| gutterSvgs = { |
| blockred: ctx.asAbsolutePath('media/gutter-blockred.svg'), |
| blockgreen: ctx.asAbsolutePath('media/gutter-blockgreen.svg'), |
| blockblue: ctx.asAbsolutePath('media/gutter-blockblue.svg'), |
| blockyellow: ctx.asAbsolutePath('media/gutter-blockyellow.svg'), |
| slashred: ctx.asAbsolutePath('media/gutter-slashred.svg'), |
| slashgreen: ctx.asAbsolutePath('media/gutter-slashgreen.svg'), |
| slashblue: ctx.asAbsolutePath('media/gutter-slashblue.svg'), |
| slashyellow: ctx.asAbsolutePath('media/gutter-slashyellow.svg'), |
| verticalred: ctx.asAbsolutePath('media/gutter-vertred.svg'), |
| verticalgreen: ctx.asAbsolutePath('media/gutter-vertgreen.svg'), |
| verticalblue: ctx.asAbsolutePath('media/gutter-vertblue.svg'), |
| verticalyellow: ctx.asAbsolutePath('media/gutter-vertyellow.svg') |
| }; |
| |
| const goConfig = getGoConfig(); |
| updateCodeCoverageDecorators(goConfig.get('coverageDecorator')); |
| } |
| |
| /** |
| * Updates the decorators used for Code coverage. |
| * @param coverageDecoratorConfig The coverage decorated as configured by the user |
| */ |
| export function updateCodeCoverageDecorators(coverageDecoratorConfig: any) { |
| // These defaults are chosen to be distinguishable in nearly any color scheme (even Red) |
| // as well as by people who have difficulties with color perception. |
| // It appears that the contributions in package.json are only used to check what users |
| // put in settings.json, while the defaults come from the defaults section of |
| // go.coverageDecorator in package.json. |
| decoratorConfig = { |
| type: 'highlight', |
| coveredHighlightColor: 'rgba(64,128,128,0.5)', |
| coveredBorderColor: 'rgba(64,128,128,1.0)', |
| uncoveredHighlightColor: 'rgba(128,64,64,0.25)', |
| uncoveredBorderColor: 'rgba(128,64,64,1.0)', |
| coveredGutterStyle: 'blockblue', |
| uncoveredGutterStyle: 'slashyellow' |
| }; |
| |
| // Update from configuration. |
| if (typeof coverageDecoratorConfig !== 'object') { |
| vscode.window.showWarningMessage("invalid go.coverageDecorator type, expected an 'object'"); |
| } else { |
| for (const k in coverageDecoratorConfig) { |
| if (coverageDecoratorConfig.hasOwnProperty(k)) { |
| decoratorConfig[k] = coverageDecoratorConfig[k]; |
| } else { |
| vscode.window.showWarningMessage(`invalid coverage parameter ${k}`); |
| } |
| } |
| } |
| setDecorators(); |
| vscode.window.visibleTextEditors.forEach(applyCodeCoverage); |
| } |
| |
| function setDecorators() { |
| disposeDecorators(); |
| if (!decorators) { |
| initForTest(); |
| } // only happens in tests |
| const f = (x: { overviewRulerColor: string; backgroundColor: string }, arg: string) => { |
| const y = { |
| overviewRulerLane: 2, |
| borderStyle: arg, |
| borderWidth: '2px' |
| }; |
| return Object.assign(y, x); |
| }; |
| const cov = { |
| overviewRulerColor: 'green', |
| backgroundColor: decoratorConfig.coveredHighlightColor, |
| borderColor: decoratorConfig.coveredBorderColor |
| }; |
| const uncov = { |
| overviewRulerColor: 'red', |
| backgroundColor: decoratorConfig.uncoveredHighlightColor, |
| borderColor: decoratorConfig.uncoveredBorderColor |
| }; |
| const ctop = f(cov, 'solid solid none solid'); |
| const cmid = f(cov, 'none solid none solid'); |
| const cbot = f(cov, 'none solid solid solid'); |
| const cone = f(cov, 'solid solid solid solid'); |
| const utop = f(uncov, 'solid solid none solid'); |
| const umid = f(uncov, 'none solid none solid'); |
| const ubot = f(uncov, 'none solid solid solid'); |
| const uone = f(uncov, 'solid solid solid solid'); |
| decorators = { |
| type: decoratorConfig.type, |
| coveredGutter: vscode.window.createTextEditorDecorationType({ |
| gutterIconPath: gutterSvgs[decoratorConfig.coveredGutterStyle] |
| }), |
| uncoveredGutter: vscode.window.createTextEditorDecorationType({ |
| gutterIconPath: gutterSvgs[decoratorConfig.uncoveredGutterStyle] |
| }), |
| coveredHighlight: { |
| all: vscode.window.createTextEditorDecorationType(cone), |
| top: vscode.window.createTextEditorDecorationType(ctop), |
| mid: vscode.window.createTextEditorDecorationType(cmid), |
| bot: vscode.window.createTextEditorDecorationType(cbot) |
| }, |
| uncoveredHighlight: { |
| all: vscode.window.createTextEditorDecorationType(uone), |
| top: vscode.window.createTextEditorDecorationType(utop), |
| mid: vscode.window.createTextEditorDecorationType(umid), |
| bot: vscode.window.createTextEditorDecorationType(ubot) |
| } |
| }; |
| } |
| |
| /** |
| * Disposes decorators so that the current coverage is removed from the editor. |
| */ |
| function disposeDecorators() { |
| if (decorators) { |
| decorators.coveredGutter.dispose(); |
| decorators.uncoveredGutter.dispose(); |
| decorators.coveredHighlight.all.dispose(); |
| decorators.coveredHighlight.top.dispose(); |
| decorators.coveredHighlight.mid.dispose(); |
| decorators.coveredHighlight.bot.dispose(); |
| decorators.uncoveredHighlight.all.dispose(); |
| decorators.uncoveredHighlight.top.dispose(); |
| decorators.uncoveredHighlight.mid.dispose(); |
| decorators.uncoveredHighlight.bot.dispose(); |
| } |
| } |
| |
| interface CoverageData { |
| uncoveredOptions: vscode.DecorationOptions[]; |
| coveredOptions: vscode.DecorationOptions[]; |
| } |
| |
| let coverageData: { [key: string]: CoverageData } = {}; // actual file path to the coverage data. |
| let isCoverageApplied = false; |
| |
| function emptyCoverageData(): CoverageData { |
| return { |
| uncoveredOptions: [], |
| coveredOptions: [] |
| }; |
| } |
| |
| /** |
| * Clear the coverage on all files |
| */ |
| function clearCoverage() { |
| coverageData = {}; |
| disposeDecorators(); |
| isCoverageApplied = false; |
| } |
| |
| /** |
| * Extract the coverage data from the given cover profile & apply them on the files in the open editors. |
| * @param coverProfilePath Path to the file that has the cover profile data |
| * @param packageDirPath Absolute path of the package for which the coverage was calculated |
| * @param dir Directory to execute go list in |
| */ |
| export function applyCodeCoverageToAllEditors(coverProfilePath: string, dir: string): Promise<void> { |
| const v = new Promise<void>((resolve, reject) => { |
| try { |
| const showCounts = getGoConfig().get('coverShowCounts') as boolean; |
| const coveragePath = new Map<string, CoverageData>(); // <filename> from the cover profile to the coverage data. |
| |
| // Clear existing coverage files |
| clearCoverage(); |
| |
| // collect the packages named in the coverage file |
| const seenPaths = new Set<string>(); |
| // for now read synchronously and hope for no errors |
| const contents = fs.readFileSync(coverProfilePath).toString(); |
| contents.split('\n').forEach((line) => { |
| // go test coverageprofile generates output: |
| // filename:StartLine.StartColumn,EndLine.EndColumn Hits CoverCount |
| // where the filename is either the import path + '/' + base file name, or |
| // the actual file path (either absolute or starting with .) |
| // See https://golang.org/issues/40251. |
| // |
| // The first line will be like "mode: set" which we will ignore. |
| // TODO: port https://golang.org/cl/179377 for faster parsing. |
| |
| const parse = line.match(/^(\S+)\:(\d+)\.(\d+)\,(\d+)\.(\d+)\s(\d+)\s(\d+)/); |
| if (!parse) { |
| return; |
| } |
| |
| let filename = parse[1]; |
| if (filename.startsWith('.' + path.sep)) { |
| // If it's a relative file path, convert it to an absolute path. |
| // From now on, we can assume that it's a real file name if it is |
| // an absolute path. |
| filename = path.resolve(filename); |
| } |
| // If this is not a real file name, that's package_path + file name, |
| // Record it in seenPaths for `go list` call to resolve package path -> |
| // directory mapping. |
| if (!path.isAbsolute(filename)) { |
| const lastSlash = filename.lastIndexOf('/'); |
| if (lastSlash !== -1) { |
| seenPaths.add(filename.slice(0, lastSlash)); |
| } |
| } |
| |
| // and fill in coveragePath |
| const coverage = coveragePath.get(parse[1]) || emptyCoverageData(); |
| const range = new vscode.Range( |
| // Convert lines and columns to 0-based |
| parseInt(parse[2], 10) - 1, |
| parseInt(parse[3], 10) - 1, |
| parseInt(parse[4], 10) - 1, |
| parseInt(parse[5], 10) - 1 |
| ); |
| const counts = parseInt(parse[7], 10); |
| // If is Covered (CoverCount > 0) |
| if (counts > 0) { |
| coverage.coveredOptions.push(...elaborate(range, counts, showCounts)); |
| } else { |
| coverage.uncoveredOptions.push(...elaborate(range, counts, showCounts)); |
| } |
| |
| coveragePath.set(filename, coverage); |
| }); |
| |
| getImportPathToFolder([...seenPaths], dir).then((pathsToDirs) => { |
| createCoverageData(pathsToDirs, coveragePath); |
| setDecorators(); |
| vscode.window.visibleTextEditors.forEach(applyCodeCoverage); |
| resolve(); |
| }); |
| } catch (e) { |
| vscode.window.showInformationMessage(e.msg); |
| reject(e); |
| } |
| }); |
| return v; |
| } |
| |
| // add decorations to the range |
| function elaborate(r: vscode.Range, count: number, showCounts: boolean): vscode.DecorationOptions[] { |
| // irrelevant for "gutter" |
| if (!decorators || decorators.type === 'gutter') { |
| return [{ range: r }]; |
| } |
| const ans: vscode.DecorationOptions[] = []; |
| const dc = decoratorConfig; |
| const backgroundColor = [dc.uncoveredHighlightColor, dc.coveredHighlightColor]; |
| const txt: vscode.ThemableDecorationAttachmentRenderOptions = { |
| contentText: count > 0 && showCounts ? `--${count}--` : '', |
| backgroundColor: backgroundColor[count === 0 ? 0 : 1] |
| }; |
| const v: vscode.DecorationOptions = { |
| range: r, |
| hoverMessage: `${count} executions`, |
| renderOptions: { |
| before: txt |
| } |
| }; |
| ans.push(v); |
| return ans; |
| } |
| |
| function createCoverageData(pathsToDirs: Map<string, string>, coveragePath: Map<string, CoverageData>) { |
| coveragePath.forEach((cd, ip) => { |
| if (path.isAbsolute(ip)) { |
| setCoverageDataByFilePath(ip, cd); |
| return; |
| } |
| |
| const lastSlash = ip.lastIndexOf('/'); |
| if (lastSlash === -1) { |
| setCoverageDataByFilePath(ip, cd); |
| return; |
| } |
| |
| const maybePkgPath = ip.slice(0, lastSlash); |
| const fileDir = pathsToDirs.get(maybePkgPath) || path.resolve(maybePkgPath); |
| const file = fileDir + path.sep + ip.slice(lastSlash + 1); |
| setCoverageDataByFilePath(file, cd); |
| }); |
| } |
| |
| /** |
| * Set the object that holds the coverage data for given file path. |
| * @param filePath |
| * @param data |
| */ |
| function setCoverageDataByFilePath(filePath: string, data: CoverageData) { |
| if (filePath.startsWith('_')) { |
| filePath = filePath.substr(1); |
| } |
| if (process.platform === 'win32') { |
| const parts = filePath.split('/'); |
| if (parts.length) { |
| filePath = parts.join(path.sep); |
| } |
| } |
| coverageData[filePath] = data; |
| } |
| |
| /** |
| * Apply the code coverage highlighting in given editor |
| * @param editor |
| */ |
| export function applyCodeCoverage(editor: vscode.TextEditor) { |
| if (!editor || editor.document.languageId !== 'go' || editor.document.fileName.endsWith('_test.go')) { |
| return; |
| } |
| let doc = editor.document.fileName; |
| if (path.isAbsolute(doc)) { |
| doc = fixDriveCasingInWindows(doc); |
| } |
| |
| const cfg = getGoConfig(editor.document.uri); |
| const coverageOptions = cfg['coverageOptions']; |
| for (const filename in coverageData) { |
| if (doc !== fixDriveCasingInWindows(filename)) { |
| continue; |
| } |
| isCoverageApplied = true; |
| const cd = coverageData[filename]; |
| if (coverageOptions === 'showCoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') { |
| if (decorators.type === 'gutter') { |
| editor.setDecorations(decorators.coveredGutter, cd.coveredOptions); |
| } else { |
| detailed(editor, decorators.coveredHighlight, cd.coveredOptions); |
| } |
| } |
| |
| if (coverageOptions === 'showUncoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') { |
| if (decorators.type === 'gutter') { |
| editor.setDecorations(decorators.uncoveredGutter, cd.uncoveredOptions); |
| } else { |
| detailed(editor, decorators.uncoveredHighlight, cd.uncoveredOptions); |
| } |
| } |
| } |
| } |
| |
| function detailed(editor: vscode.TextEditor, h: Highlight, opts: vscode.DecorationOptions[]) { |
| const tops: vscode.DecorationOptions[] = []; |
| const mids: vscode.DecorationOptions[] = []; |
| const bots: vscode.DecorationOptions[] = []; |
| const alls: vscode.DecorationOptions[] = []; |
| opts.forEach((opt) => { |
| const r = opt.range; |
| if (r.start.line === r.end.line) { |
| alls.push(opt); |
| return; |
| } |
| for (let line = r.start.line; line <= r.end.line; line++) { |
| if (line === r.start.line) { |
| const use: vscode.DecorationOptions = { |
| range: editor.document.validateRange( |
| new vscode.Range(line, r.start.character, line, Number.MAX_SAFE_INTEGER) |
| ), |
| hoverMessage: opt.hoverMessage, |
| renderOptions: opt.renderOptions |
| }; |
| tops.push(use); |
| } else if (line < r.end.line) { |
| const use = { |
| range: editor.document.validateRange(new vscode.Range(line, 0, line, Number.MAX_SAFE_INTEGER)), |
| hoverMessage: opt.hoverMessage |
| }; |
| mids.push(use); |
| } else { |
| const use = { |
| range: new vscode.Range(line, 0, line, r.end.character), |
| hoverMessage: opt.hoverMessage |
| }; |
| bots.push(use); |
| } |
| } |
| }); |
| if (tops.length > 0) { |
| editor.setDecorations(h.top, tops); |
| } |
| if (mids.length > 0) { |
| editor.setDecorations(h.mid, mids); |
| } |
| if (bots.length > 0) { |
| editor.setDecorations(h.bot, bots); |
| } |
| if (alls.length > 0) { |
| editor.setDecorations(h.all, alls); |
| } |
| } |
| |
| /** |
| * Listener for file save that clears potential stale coverage data. |
| * Local cache tracks files with changes outside of comments to determine |
| * files for which the save event can cause stale coverage data. |
| * @param e TextDocument |
| */ |
| export function removeCodeCoverageOnFileSave(e: vscode.TextDocument) { |
| if (e.languageId !== 'go' || !isCoverageApplied) { |
| return; |
| } |
| |
| if (vscode.window.visibleTextEditors.every((editor) => editor.document !== e)) { |
| return; |
| } |
| |
| if (modifiedFiles[e.fileName]) { |
| clearCoverage(); |
| modifiedFiles = {}; // reset the list of modified files |
| } |
| } |
| |
| /** |
| * Listener for file change that tracks files with changes outside of comments |
| * to determine files for which an eventual save can cause stale coverage data. |
| * @param e TextDocumentChangeEvent |
| */ |
| export function trackCodeCoverageRemovalOnFileChange(e: vscode.TextDocumentChangeEvent) { |
| if (e.document.languageId !== 'go' || !e.contentChanges.length || !isCoverageApplied) { |
| return; |
| } |
| |
| if (vscode.window.visibleTextEditors.every((editor) => editor.document !== e.document)) { |
| return; |
| } |
| |
| if (isPartOfComment(e)) { |
| return; |
| } |
| |
| modifiedFiles[e.document.fileName] = true; |
| } |
| |
| /** |
| * If current editor has Code coverage applied, then remove it. |
| * Else run tests to get the coverage and apply. |
| */ |
| export async function toggleCoverageCurrentPackage() { |
| const editor = vscode.window.activeTextEditor; |
| if (!editor) { |
| vscode.window.showInformationMessage('No editor is active.'); |
| return; |
| } |
| |
| if (isCoverageApplied) { |
| clearCoverage(); |
| return; |
| } |
| |
| const goConfig = getGoConfig(); |
| const cwd = path.dirname(editor.document.uri.fsPath); |
| |
| const testFlags = getTestFlags(goConfig); |
| const isMod = await isModSupported(editor.document.uri); |
| const testConfig: TestConfig = { |
| goConfig, |
| dir: cwd, |
| flags: testFlags, |
| background: true, |
| isMod, |
| applyCodeCoverage: true |
| }; |
| |
| return goTest(testConfig).then((success) => { |
| if (!success) { |
| showTestOutput(); |
| } |
| }); |
| } |
| |
| export function isPartOfComment(e: vscode.TextDocumentChangeEvent): boolean { |
| return e.contentChanges.every((change) => { |
| // We cannot be sure with using just regex on individual lines whether a multi line change is part of a comment or not |
| // So play it safe and treat it as not a comment |
| if (!change.range.isSingleLine || change.text.includes('\n')) { |
| return false; |
| } |
| |
| const text = e.document.lineAt(change.range.start).text; |
| const idx = text.search('//'); |
| return idx > -1 && idx <= change.range.start.character; |
| }); |
| } |
| |
| // These routines enable testing without starting an editing session. |
| |
| export function coverageFilesForTest(): { [key: string]: CoverageData } { |
| return coverageData; |
| } |
| |
| export function initForTest() { |
| if (!decoratorConfig) { |
| // this code is unnecessary except for testing, where there may be no workspace |
| // nor the normal flow of initializations |
| const x = 'rgba(0,0,0,0)'; |
| if (!gutterSvgs) { |
| gutterSvgs = { x }; |
| } |
| decoratorConfig = { |
| type: 'highlight', |
| coveredHighlightColor: x, |
| uncoveredHighlightColor: x, |
| coveredBorderColor: x, |
| uncoveredBorderColor: x, |
| coveredGutterStyle: x, |
| uncoveredGutterStyle: x |
| }; |
| } |
| } |