mirror of
https://github.com/Microsoft/vscode
synced 2024-08-25 03:56:03 +00:00
testing: peek and explorer ux feedback
This commit is contained in:
parent
9126e31644
commit
3f9cd31660
|
@ -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' }
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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[]) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue