Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testing: avoid large hovers in test coverage, show inline counts instead #202944

Merged
merged 1 commit into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@
"--vscode-terminalStickyScroll-background",
"--vscode-terminalStickyScrollHover-background",
"--vscode-testing-coverage-lineHeight",
"--vscode-testing-coverCountBadgeBackground",
"--vscode-testing-coverCountBadgeForeground",
"--vscode-testing-coveredBackground",
"--vscode-testing-coveredBorder",
"--vscode-testing-coveredGutterBackground",
Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/theme/common/colorRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { assertNever } from 'vs/base/common/assert';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Color, RGBA } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { assertNever } from 'vs/base/common/assert';
import * as nls from 'vs/nls';
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import * as platform from 'vs/platform/registry/common/platform';
import { IColorTheme } from 'vs/platform/theme/common/themeService';

Expand Down
77 changes: 40 additions & 37 deletions src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ import { Lazy } from 'vs/base/common/lazy';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable';
import { ThemeIcon } from 'vs/base/common/themables';
import { isDefined } from 'vs/base/common/types';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { IModelDecorationOptions, ITextModel, InjectedTextCursorStops } from 'vs/editor/common/model';
import { IModelDecorationOptions, ITextModel, InjectedTextCursorStops, InjectedTextOptions } from 'vs/editor/common/model';
import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation';
import { localize } from 'vs/nls';
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
Expand Down Expand Up @@ -173,12 +172,12 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
return;
}

const wasPreviouslyHovering = typeof this.hoveredSubject === 'number';
this.hoveredStore.clear();
this.hoveredSubject = lineNumber;

const todo = [{ line: lineNumber, dir: 0 }];
const toEnable = new Set<string>();
const inlineEnabled = CodeCoverageDecorations.showInline.get();
if (!CodeCoverageDecorations.showInline.get()) {
for (let i = 0; i < todo.length && i < MAX_HOVERED_LINES; i++) {
const { line, dir } = todo[i];
Expand Down Expand Up @@ -209,7 +208,9 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
});
}

this.lineHoverWidget.value.startShowingAt(lineNumber, this.details, wasPreviouslyHovering);
if (toEnable.size || inlineEnabled) {
this.lineHoverWidget.value.startShowingAt(lineNumber);
}

this.hoveredStore.add(this.editor.onMouseLeave(() => {
this.hoveredStore.clear();
Expand Down Expand Up @@ -240,7 +241,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri

model.changeDecorations(e => {
for (const detailRange of details.ranges) {
const { metadata: { detail, description }, range } = detailRange;
const { metadata: { detail, description }, range, primary } = detailRange;
if (detail.type === DetailType.Branch) {
const hits = detail.detail.branches![detail.branch].count;
const cls = hits ? CLASS_HIT : CLASS_MISS;
Expand All @@ -263,6 +264,9 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
};
} else {
target.className = `coverage-deco-inline ${cls}`;
if (primary) {
target.before = countBadge(hits);
}
}
};

Expand All @@ -282,6 +286,9 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
const applyHoverOptions = (target: IModelDecorationOptions) => {
target.className = `coverage-deco-inline ${cls}`;
target.hoverMessage = description;
if (primary) {
target.before = countBadge(detail.count);
}
};

if (showInlineByDefault) {
Expand Down Expand Up @@ -336,8 +343,21 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
}
}

const countBadge = (count: number): InjectedTextOptions | undefined => {
if (count === 0) {
return undefined;
}

return {
content: `${count > 99 ? '99+' : count}x`,
cursorStops: InjectedTextCursorStops.None,
inlineClassName: `coverage-deco-inline-count`,
inlineClassNameAffectsLetterSpacing: true,
};
};

type CoverageDetailsWithBranch = CoverageDetails | { type: DetailType.Branch; branch: number; detail: IStatementCoverage };
type DetailRange = { range: Range; metadata: { detail: CoverageDetailsWithBranch; description: IMarkdownString | undefined } };
type DetailRange = { range: Range; primary: boolean; metadata: { detail: CoverageDetailsWithBranch; description: IMarkdownString | undefined } };

export class CoverageDetailsModel {
public readonly ranges: DetailRange[] = [];
Expand All @@ -351,6 +371,7 @@ export class CoverageDetailsModel {
// the editor without ugly overlaps.
const detailRanges: DetailRange[] = details.map(detail => ({
range: tidyLocation(detail.location),
primary: true,
metadata: { detail, description: this.describe(detail, textModel) }
}));

Expand All @@ -360,6 +381,7 @@ export class CoverageDetailsModel {
const branch: CoverageDetailsWithBranch = { type: DetailType.Branch, branch: i, detail };
detailRanges.push({
range: tidyLocation(detail.branches[i].location || Range.fromPositions(range.getEndPosition())),
primary: true,
metadata: {
detail: branch,
description: this.describe(branch, textModel),
Expand Down Expand Up @@ -404,11 +426,13 @@ export class CoverageDetailsModel {
// until after the `item.range` ends.
const prev = stack[stack.length - 1];
if (prev) {
const primary = prev.primary;
const si = prev.range.setEndPosition(start.lineNumber, start.column);
prev.range = prev.range.setStartPosition(item.range.endLineNumber, item.range.endColumn);
prev.primary = false;
// discard the previous range if it became empty, e.g. a nested statement
if (prev.range.isEmpty()) { stack.pop(); }
result.push({ range: si, metadata: prev.metadata });
result.push({ range: si, primary, metadata: prev.metadata });
}

stack.push(item);
Expand Down Expand Up @@ -460,39 +484,20 @@ function tidyLocation(location: Range | Position): Range {

class LineHoverComputer implements IHoverComputer<IMarkdownString> {
public line = -1;
public textModel!: ITextModel;
public details!: CoverageDetailsModel;

constructor(@IKeybindingService private readonly keybindingService: IKeybindingService) { }

/** @inheritdoc */
public computeSync(): IMarkdownString[] {
const bestDetails: DetailRange[] = [];
let bestLine = -1;
for (const detail of this.details.ranges) {
if (detail.range.startLineNumber > this.line) {
break;
}
if (detail.range.endLineNumber < this.line) {
continue;
}
if (detail.range.startLineNumber !== bestLine) {
bestDetails.length = 0;
}
bestLine = detail.range.startLineNumber;
bestDetails.push(detail);
}
const strs: IMarkdownString[] = [];

const strs = bestDetails.map(d => d.metadata.detail.type === DetailType.Branch ? undefined : d.metadata.description).filter(isDefined);
if (strs.length) {
const s = new MarkdownString().appendMarkdown(`[${TOGGLE_INLINE_COMMAND_TEXT}](command:${TOGGLE_INLINE_COMMAND_ID})`);
s.isTrusted = true;
const binding = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID);
if (binding) {
s.appendText(` (${binding.getLabel()})`);
}
strs.push(s);
const s = new MarkdownString().appendMarkdown(`[${TOGGLE_INLINE_COMMAND_TEXT}](command:${TOGGLE_INLINE_COMMAND_ID})`);
s.isTrusted = true;
const binding = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID);
if (binding) {
s.appendText(` (${binding.getLabel()})`);
}
strs.push(s);

return strs;
}
Expand Down Expand Up @@ -556,17 +561,15 @@ class LineHoverWidget extends Disposable implements IOverlayWidget {
}

/** Shows the hover widget at the given line */
public startShowingAt(lineNumber: number, details: CoverageDetailsModel, showImmediate: boolean) {
public startShowingAt(lineNumber: number) {
this.hide();
const textModel = this.editor.getModel();
if (!textModel) {
return;
}

this.computer.line = lineNumber;
this.computer.textModel = textModel;
this.computer.details = details;
this.hoverOperation.start(showImmediate ? HoverStartMode.Immediate : HoverStartMode.Delayed);
this.hoverOperation.start(HoverStartMode.Delayed);
}

/** Hides the hover widget */
Expand Down
56 changes: 41 additions & 15 deletions src/vs/workbench/contrib/testing/browser/media/testing.css
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@

.test-explorer .computed-state.retired,
.testing-run-glyph.retired {
opacity: 0.7 !important;
opacity: 0.7 !important;
}

.test-explorer .test-is-hidden {
Expand All @@ -171,11 +171,7 @@
flex-grow: 1;
}

.monaco-workbench
.test-explorer
.monaco-action-bar
.action-item
> .action-label {
.monaco-workbench .test-explorer .monaco-action-bar .action-item > .action-label {
padding: 1px 2px;
margin-right: 2px;
}
Expand Down Expand Up @@ -358,6 +354,7 @@
.monaco-editor .testing-inline-message-severity-0 {
color: var(--vscode-testing-message-error-decorationForeground) !important;
}

.monaco-editor .testing-inline-message-severity-1 {
color: var(--vscode-testing-message-info-decorationForeground) !important;
}
Expand Down Expand Up @@ -411,8 +408,10 @@
.explorer-item-with-test-coverage .explorer-item {
flex-grow: 1;
}

.explorer-item-with-test-coverage .monaco-icon-label::after {
margin-right: 12px; /* slightly reduce because the bars handle the scrollbar margin */
margin-right: 12px;
/* slightly reduce because the bars handle the scrollbar margin */
}

/** -- coverage decorations */
Expand Down Expand Up @@ -447,14 +446,13 @@

.coverage-deco-gutter.coverage-deco-miss.coverage-deco-hit::before {
background-image: linear-gradient(45deg,
var(--vscode-testing-coveredGutterBackground) 25%,
var(--vscode-testing-uncoveredGutterBackground) 25%,
var(--vscode-testing-uncoveredGutterBackground) 50%,
var(--vscode-testing-coveredGutterBackground) 50%,
75%,
var(--vscode-testing-uncoveredGutterBackground) 75%,
var(--vscode-testing-uncoveredGutterBackground) 100%
);
var(--vscode-testing-coveredGutterBackground) 25%,
var(--vscode-testing-uncoveredGutterBackground) 25%,
var(--vscode-testing-uncoveredGutterBackground) 50%,
var(--vscode-testing-coveredGutterBackground) 50%,
75%,
var(--vscode-testing-uncoveredGutterBackground) 75%,
var(--vscode-testing-uncoveredGutterBackground) 100%);
background-size: 6px 6px;
background-color: transparent;
}
Expand Down Expand Up @@ -497,3 +495,31 @@
font: normal normal normal calc(var(--vscode-testing-coverage-lineHeight) / 2)/1 codicon;
border: 1px solid;
}

.coverage-deco-inline-count {
position: relative;
background: var(--vscode-testing-coverCountBadgeBackground);
color: var(--vscode-testing-coverCountBadgeForeground);
font-size: 0.7em;
margin: 0 0.7em 0 0.4em;
padding: 0.2em 0 0.2em 0.2em;
/* display: inline-block; */
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}

.coverage-deco-inline-count::after {
content: '';
display: block;
position: absolute;
left: 100%;
top: 0;
bottom: 0;
width: 0.5em;
background-image:
linear-gradient(to bottom left, transparent 50%, var(--vscode-testing-coverCountBadgeBackground) 0),
linear-gradient(to bottom right, var(--vscode-testing-coverCountBadgeBackground) 50%, transparent 0);
background-size: 100% 50%;
background-repeat: no-repeat;
background-position: top, bottom;
}
16 changes: 15 additions & 1 deletion src/vs/workbench/contrib/testing/browser/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Color, RGBA } from 'vs/base/common/color';
import { localize } from 'vs/nls';
import { chartsGreen, chartsRed, contrastBorder, diffInserted, diffRemoved, editorBackground, editorErrorForeground, editorForeground, editorInfoForeground, opaque, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry';
import { badgeBackground, badgeForeground, chartsGreen, chartsRed, contrastBorder, diffInserted, diffRemoved, editorBackground, editorErrorForeground, editorForeground, editorInfoForeground, opaque, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes';

Expand Down Expand Up @@ -135,6 +135,20 @@ export const testingUncoveredGutterBackground = registerColor('testing.uncovered
hcLight: chartsRed
}, localize('testing.uncoveredGutterBackground', 'Gutter color of regions where code not covered.'));

export const testingCoverCountBadgeBackground = registerColor('testing.coverCountBadgeBackground', {
dark: badgeBackground,
light: badgeBackground,
hcDark: badgeBackground,
hcLight: badgeBackground
}, localize('testing.coverCountBadgeBackground', 'Background for the badge indicating execution count'));

export const testingCoverCountBadgeForeground = registerColor('testing.coverCountBadgeForeground', {
dark: badgeForeground,
light: badgeForeground,
hcDark: badgeForeground,
hcLight: badgeForeground
}, localize('testing.coverCountBadgeForeground', 'Foreground for the badge indicating execution count'));

export const testMessageSeverityColors: {
[K in TestMessageType]: {
decorationForeground: string;
Expand Down