testing: peek and explorer ux feedback

This commit is contained in:
Connor Peet 2021-07-23 16:45:12 -07:00
parent 9126e31644
commit 3f9cd31660
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
5 changed files with 284 additions and 85 deletions

View file

@ -128,6 +128,7 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR
export const CATEGORIES = {
View: { value: localize('view', "View"), original: 'View' },
Help: { value: localize('help', "Help"), original: 'Help' },
Test: { value: localize('test', "Test"), original: 'Test' },
Preferences: { value: localize('preferences', "Preferences"), original: 'Preferences' },
Developer: { value: localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"), original: 'Developer' }
};

View file

@ -20,6 +20,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { FocusedViewContext } from 'vs/workbench/common/views';
import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands';
@ -41,7 +42,7 @@ import { expandAndGetTestById, IMainThreadTestCollection, ITestService, testsInF
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
const category = localize('testing.category', 'Test');
const category = CATEGORIES.Test;
const enum ActionOrder {
// Navigation:
@ -53,6 +54,7 @@ const enum ActionOrder {
// Submenu:
Collapse,
ClearResults,
DisplayMode,
Sort,
GoToTest,
@ -592,7 +594,7 @@ export class CollapseAllAction extends ViewAction<TestingExplorerView> {
menu: {
id: MenuId.ViewTitle,
order: ActionOrder.Collapse,
group: 'cikkaose',
group: 'displayAction',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
@ -619,7 +621,12 @@ export class ClearTestResultsAction extends Action2 {
}, {
id: MenuId.CommandPalette,
when: TestingContextKeys.hasAnyResults.isEqualTo(true),
},],
}, {
id: MenuId.ViewTitle,
order: ActionOrder.ClearResults,
group: 'displayAction',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}],
});
}
@ -637,10 +644,12 @@ export class GoToTest extends Action2 {
super({
id: GoToTest.ID,
title: localize('testing.editFocusedTest', "Go to Test"),
icon: Codicon.goToFile,
menu: {
id: MenuId.TestItem,
when: TestingContextKeys.testItemHasUri.isEqualTo(true),
order: ActionOrder.GoToTest,
group: 'inline',
},
keybinding: {
weight: KeybindingWeight.EditorContrib - 10,
@ -651,41 +660,9 @@ export class GoToTest extends Action2 {
}
public override async run(accessor: ServicesAccessor, element?: IActionableTestTreeElement, preserveFocus?: boolean) {
if (!element || !(element instanceof TestItemTreeElement) || !element.test.item.uri) {
return;
if (element && element instanceof TestItemTreeElement) {
accessor.get(ICommandService).executeCommand('vscode.revealTest', element.test.item.extId, preserveFocus);
}
const commandService = accessor.get(ICommandService);
const fileService = accessor.get(IFileService);
const editorService = accessor.get(IEditorService);
const { range, uri, extId } = element.test.item;
accessor.get(ITestExplorerFilterState).reveal.value = extId;
accessor.get(ITestingPeekOpener).closeAllPeeks();
let isFile = true;
try {
if (!(await fileService.resolve(uri)).isFile) {
isFile = false;
}
} catch {
// ignored
}
if (!isFile) {
await commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, uri);
return;
}
await editorService.openEditor({
resource: uri,
options: {
selection: range
? { startColumn: range.startColumn, startLineNumber: range.startLineNumber }
: undefined,
preserveFocus: preserveFocus === true,
},
});
}
/**

View file

@ -6,9 +6,10 @@
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IFileService } from 'vs/platform/files/common/files';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@ -16,11 +17,12 @@ import { IProgressService } from 'vs/platform/progress/common/progress';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views';
import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands';
import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons';
import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations';
import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { CloseTestPeek, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { ITestingOutputTerminalService, TestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
@ -36,6 +38,7 @@ import { ITestResultService, TestResultService } from 'vs/workbench/contrib/test
import { ITestResultStorage, TestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { allTestActions, discoverAndRunTests } from './testExplorerActions';
import './testingConfigurationUi';
@ -100,6 +103,8 @@ viewsRegistry.registerViews([{
}], viewContainer);
allTestActions.forEach(registerAction2);
registerAction2(GoToPreviousMessageAction);
registerAction2(GoToNextMessageAction);
registerAction2(CloseTestPeek);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored);
@ -127,9 +132,9 @@ CommandsRegistry.registerCommand({
CommandsRegistry.registerCommand({
id: 'vscode.revealTestInExplorer',
handler: async (accessor: ServicesAccessor, testId: string) => {
handler: async (accessor: ServicesAccessor, testId: string, focus?: boolean) => {
accessor.get(ITestExplorerFilterState).reveal.value = testId;
accessor.get(IViewsService).openView(Testing.ExplorerViewId);
accessor.get(IViewsService).openView(Testing.ExplorerViewId, focus);
}
});
@ -143,6 +148,51 @@ CommandsRegistry.registerCommand({
}
});
CommandsRegistry.registerCommand({
id: 'vscode.revealTest',
handler: async (accessor: ServicesAccessor, extId: string, preserveFocus?: boolean) => {
const test = accessor.get(ITestService).collection.getNodeById(extId);
if (!test) {
return;
}
const commandService = accessor.get(ICommandService);
const fileService = accessor.get(IFileService);
const editorService = accessor.get(IEditorService);
const { range, uri } = test.item;
if (!uri) {
return;
}
accessor.get(ITestExplorerFilterState).reveal.value = extId;
accessor.get(ITestingPeekOpener).closeAllPeeks();
let isFile = true;
try {
if (!(await fileService.resolve(uri)).isFile) {
isFile = false;
}
} catch {
// ignored
}
if (!isFile) {
await commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, uri);
return;
}
await editorService.openEditor({
resource: uri,
options: {
selection: range
? { startColumn: range.startColumn, startLineNumber: range.startLineNumber }
: undefined,
preserveFocus: preserveFocus === true,
},
});
}
});
CommandsRegistry.registerCommand({
id: 'vscode.runTestsById',
handler: async (accessor: ServicesAccessor, group: TestRunProfileBitset, ...testIds: string[]) => {

View file

@ -462,7 +462,7 @@ export class TestingExplorerViewModel extends Disposable {
}
}));
this._register(filterState.reveal.onDidChange(this.revealById, this));
this._register(filterState.reveal.onDidChange(id => this.revealById(id, undefined, false)));
this._register(onDidChangeVisibility(visible => {
if (visible) {
@ -471,16 +471,14 @@ export class TestingExplorerViewModel extends Disposable {
}));
this._register(this.tree.onDidChangeSelection(async evt => {
if (evt.browserEvent instanceof MouseEvent && evt.browserEvent.altKey) {
if (evt.browserEvent instanceof MouseEvent && (evt.browserEvent.altKey || evt.browserEvent.shiftKey)) {
return; // don't focus when alt-clicking to multi select
}
const selected = evt.elements[0];
if (selected && evt.browserEvent && selected instanceof TestItemTreeElement
&& selected.children.size === 0 && selected.test.expand === TestItemExpandState.NotExpandable) {
if (!(await this.tryPeekError(selected)) && selected?.test) {
this.instantiationService.invokeFunction(accessor => new GoToTest().run(accessor, selected, true));
}
await this.tryPeekError(selected);
}
}));

View file

@ -20,7 +20,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { KeyCode } from 'vs/base/common/keyCodes';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Lazy } from 'vs/base/common/lazy';
import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { clamp } from 'vs/base/common/numbers';
@ -33,7 +33,9 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
import { getOuterEditor, IPeekViewService, peekViewResultsBackground, peekViewResultsMatchForeground, peekViewResultsSelectionBackground, peekViewResultsSelectionForeground, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView';
import { localize } from 'vs/nls';
@ -41,7 +43,7 @@ import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/pla
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ContextKeyAndExpr, ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@ -50,10 +52,12 @@ import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listSe
import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display';
import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
@ -67,17 +71,17 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb
import { ITestResult, maxCountPriority, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
class TestDto {
test: ITestItem;
messageIndex: number;
messages: ITestMessage[];
expectedUri: URI;
actualUri: URI;
messageUri: URI;
public readonly test: ITestItem;
public readonly messages: ITestMessage[];
public readonly expectedUri: URI;
public readonly actualUri: URI;
public readonly messageUri: URI;
public readonly revealLocation: IRichLocation | undefined;
constructor(resultId: string, test: TestResultItem, taskIndex: number, messageIndex: number) {
constructor(public readonly resultId: string, test: TestResultItem, public readonly taskIndex: number, public readonly messageIndex: number) {
this.test = test.item;
this.messages = test.tasks[taskIndex].messages;
this.messageIndex = messageIndex;
@ -86,6 +90,22 @@ class TestDto {
this.expectedUri = buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput });
this.actualUri = buildTestUri({ ...parts, type: TestUriType.ResultActualOutput });
this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage });
const message = this.messages[this.messageIndex];
this.revealLocation = message.location ?? (test.item.uri && test.item.range ? { uri: test.item.uri, range: Range.lift(test.item.range) } : undefined);
}
}
/** Iterates through every message in every result */
function* allMessages(results: readonly ITestResult[]) {
for (const result of results) {
for (const test of result.tests) {
for (let taskIndex = 0; taskIndex < test.tasks.length; taskIndex++) {
for (let messageIndex = 0; messageIndex < test.tasks[taskIndex].messages.length; messageIndex++) {
yield { result, test, taskIndex, messageIndex };
}
}
}
}
}
@ -343,6 +363,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
constructor(
private readonly editor: ICodeEditor,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITestResultService private readonly testResults: ITestResultService,
@IContextKeyService contextKeyService: IContextKeyService,
@ -366,7 +387,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
}
/**
* Shows a peek for the message in th editor.
* Shows a peek for the message in the editor.
*/
public async show(uri: URI) {
const dto = this.retrieveTest(uri);
@ -375,11 +396,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
}
const message = dto.messages[dto.messageIndex];
if (!message?.location) {
return;
}
if (!this.peek.value) {
this.peek.value = this.instantiationService.createInstance(TestingOutputPeek, this.editor);
this.peek.value.onDidClose(() => {
@ -397,6 +413,27 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
this.currentPeekUri = uri;
}
public async openAndShow(uri: URI) {
const dto = this.retrieveTest(uri);
if (!dto) {
return;
}
if (!dto.revealLocation || dto.revealLocation.uri.toString() === this.editor.getModel()?.uri.toString()) {
return this.show(uri);
}
const otherEditor = await this.codeEditorService.openCodeEditor({
resource: dto.revealLocation.uri,
options: { pinned: false, revealIfOpened: true }
}, this.editor);
if (otherEditor) {
TestingOutputPeekController.get(otherEditor).removePeek();
return TestingOutputPeekController.get(otherEditor).show(uri);
}
}
/**
* Disposes the peek view, if any.
*/
@ -404,11 +441,67 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
this.peek.clear();
}
/**
* Shows the next message in the peek, if possible.
*/
public next() {
const dto = this.peek.value?.current;
if (!dto) {
return;
}
let found = false;
for (const { messageIndex, taskIndex, result, test } of allMessages(this.testResults.results)) {
if (found) {
this.openAndShow(buildTestUri({
type: TestUriType.ResultMessage,
messageIndex,
taskIndex,
resultId: result.id,
testExtId: test.item.extId
}));
return;
} else if (dto.test.extId === test.item.extId && dto.messageIndex === messageIndex && dto.taskIndex === taskIndex && dto.resultId === result.id) {
found = true;
}
}
}
/**
* Shows the previous message in the peek, if possible.
*/
public previous() {
const dto = this.peek.value?.current;
if (!dto) {
return;
}
let previous: { messageIndex: number, taskIndex: number, result: ITestResult, test: TestResultItem } | undefined;
for (const m of allMessages(this.testResults.results)) {
if (dto.test.extId === m.test.item.extId && dto.messageIndex === m.messageIndex && dto.taskIndex === m.taskIndex && dto.resultId === m.result.id) {
if (!previous) {
return;
}
this.openAndShow(buildTestUri({
type: TestUriType.ResultMessage,
messageIndex: previous.messageIndex,
taskIndex: previous.taskIndex,
resultId: previous.result.id,
testExtId: previous.test.item.extId
}));
return;
}
previous = m;
}
}
/**
* Removes the peek view if it's being displayed on the given test ID.
*/
public removeIfPeekingForTest(testId: string) {
if (this.peek.value?.currentTest?.extId === testId) {
if (this.peek.value?.current?.test.extId === testId) {
this.peek.clear();
}
}
@ -458,7 +551,7 @@ class TestingOutputPeek extends PeekViewWidget {
private splitView!: SplitView;
private contentProviders!: IPeekOutputRenderer[];
public currentTest?: ITestItem;
public current?: TestDto;
constructor(
editor: ICodeEditor,
@ -512,6 +605,7 @@ class TestingOutputPeek extends PeekViewWidget {
const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree'));
const tree = this._disposables.add(this.instantiationService.createInstance(
OutputPeekTree,
this.editor,
treeContainer,
this.visibilityChange.event,
this.didReveal.event,
@ -549,12 +643,21 @@ class TestingOutputPeek extends PeekViewWidget {
*/
public setModel(dto: TestDto): Promise<void> {
const message = dto.messages[dto.messageIndex];
if (!message?.location) {
const previous = this.current;
if (!dto.revealLocation && !previous) {
return Promise.resolve();
}
this.currentTest = dto.test;
this.show(message.location.range, hintDiffPeekHeight(message));
this.current = dto;
if (!dto.revealLocation) {
return this.showInPlace(dto);
}
this.show(dto.revealLocation.range, hintDiffPeekHeight(message));
this.editor.revealPositionNearTop(dto.revealLocation.range.getStartPosition(), ScrollType.Smooth);
this.editor.focus();
return this.showInPlace(dto);
}
@ -970,6 +1073,7 @@ class OutputPeekTree extends Disposable {
private readonly treeActions: TreeActionsProvider;
constructor(
editor: ICodeEditor,
container: HTMLElement,
onDidChangeVisibility: Event<boolean>,
onDidReveal: Event<TestDto>,
@ -977,7 +1081,7 @@ class OutputPeekTree extends Disposable {
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ITestResultService results: ITestResultService,
@IInstantiationService instantiationService: IInstantiationService,
@IEditorService editorService: IEditorService,
@ITestExplorerFilterState explorerFilter: ITestExplorerFilterState,
) {
super();
@ -1121,24 +1225,20 @@ class OutputPeekTree extends Disposable {
return;
}
const location = e.element.location;
if (!location) {
peekController.showInPlace(new TestDto(e.element.result.id, e.element.test, e.element.taskIndex, e.element.messageIndex));
return;
const dto = new TestDto(e.element.result.id, e.element.test, e.element.taskIndex, e.element.messageIndex);
if (!dto.revealLocation) {
peekController.showInPlace(dto);
} else {
TestingOutputPeekController.get(editor).openAndShow(dto.messageUri);
}
}));
const pane = await editorService.openEditor({
resource: location.uri,
options: {
pinned: e.editorOptions.pinned,
selection: location.range,
preserveFocus: e.editorOptions.preserveFocus,
},
}, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
const control = pane?.getControl();
if (isCodeEditor(control)) {
TestingOutputPeekController.get(control).show(e.element.uri);
this._register(this.tree.onDidChangeSelection(evt => {
for (const element of evt.elements) {
if (element && 'test' in element) {
explorerFilter.reveal.value = element.test.item.extId;
break;
}
}
}));
@ -1310,6 +1410,14 @@ class TreeActionsProvider {
if (element instanceof TestCaseElement || element instanceof TestTaskElement) {
const extId = element.test.item.extId;
primary.push(new Action(
'testing.outputPeek.goToFile',
localize('testing.goToFile', "Go to File"),
Codicon.goToFile.classNames,
undefined,
() => this.commandService.executeCommand('vscode.revealTest', extId),
));
secondary.push(new Action(
'testing.outputPeek.revealInExplorer',
localize('testing.revealInExplorer', "Reveal in Test Explorer"),
Codicon.listTree.classNames,
@ -1378,3 +1486,68 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-message-container a :hover { color: ${textLinkActiveForegroundColor}; }`);
}
});
const navWhen = ContextKeyAndExpr.create([
EditorContextKeys.focus,
TestingContextKeys.isPeekVisible,
]);
export class GoToNextMessageAction extends EditorAction2 {
public static readonly ID = 'testing.goToNextMessage';
constructor() {
super({
id: GoToNextMessageAction.ID,
f1: true,
title: localize('testing.goToNextMessage', "Go to Next Test Failure"),
icon: Codicon.chevronDown,
category: CATEGORIES.Test,
keybinding: {
primary: KeyMod.Alt | KeyCode.F8,
weight: KeybindingWeight.EditorContrib + 1,
when: navWhen,
},
menu: [{
id: MenuId.TestPeekTitle,
group: 'navigation',
order: 2,
}, {
id: MenuId.CommandPalette,
when: navWhen
}],
});
}
public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): any {
TestingOutputPeekController.get(editor).next();
}
}
export class GoToPreviousMessageAction extends EditorAction2 {
public static readonly ID = 'testing.goToPreviousMessage';
constructor() {
super({
id: GoToPreviousMessageAction.ID,
f1: true,
title: localize('testing.goToPreviousMessage', "Go to Previous Test Failure"),
icon: Codicon.chevronUp,
category: CATEGORIES.Test,
keybinding: {
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F8,
weight: KeybindingWeight.EditorContrib + 1,
when: navWhen
},
menu: [{
id: MenuId.TestPeekTitle,
group: 'navigation',
order: 1,
}, {
id: MenuId.CommandPalette,
when: navWhen
}],
});
}
public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): any {
TestingOutputPeekController.get(editor).previous();
}
}