mirror of
https://github.com/Microsoft/vscode
synced 2024-10-30 01:37:20 +00:00
testing: avoid large hovers in test coverage, show inline counts instead (#202944)
Closes #202600 I still have a hover to make the "toggle line coverage" action visible, not sure a better place to put that...
This commit is contained in:
parent
442419c7a2
commit
e244acbb17
5 changed files with 100 additions and 55 deletions
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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];
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -263,6 +264,9 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
|
|||
};
|
||||
} else {
|
||||
target.className = `coverage-deco-inline ${cls}`;
|
||||
if (primary) {
|
||||
target.before = countBadge(hits);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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[] = [];
|
||||
|
@ -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) }
|
||||
}));
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -556,7 +561,7 @@ 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) {
|
||||
|
@ -564,9 +569,7 @@ class LineHoverWidget extends Disposable implements IOverlayWidget {
|
|||
}
|
||||
|
||||
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 */
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue