testing: hotkey for opening output peek

This commit is contained in:
Connor Peet 2021-05-27 12:51:19 -07:00
parent dd61b26f33
commit 31aef10814
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
3 changed files with 178 additions and 29 deletions

View file

@ -1152,6 +1152,26 @@ export class SearchForTestExtension extends Action2 {
}
}
export class OpenOutputPeek extends Action2 {
public static readonly ID = 'testing.openOutputPeek';
constructor() {
super({
id: OpenOutputPeek.ID,
title: localize('testing.openOutputPeek', "Peek Output"),
f1: true,
category,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.US_SEMICOLON, KeyCode.KEY_M),
},
});
}
public async run(accessor: ServicesAccessor) {
accessor.get(ITestingPeekOpener).open();
}
}
export const allTestActions = [
AutoRunOffAction,
AutoRunOnAction,
@ -1167,6 +1187,7 @@ export const allTestActions = [
DebugSelectedAction,
GoToTest,
HideTestAction,
OpenOutputPeek,
RefreshTestsAction,
ReRunFailedTests,
ReRunLastRun,

View file

@ -32,6 +32,7 @@ import { EditorAction2 } from 'vs/editor/browser/editorExtensions';
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 { 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';
@ -48,6 +49,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
@ -57,13 +59,14 @@ import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/brow
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { IRichLocation, ITestItem, ITestMessage, ITestRunTask, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { IRichLocation, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { getPathForTestInResult, ITestResult, maxCountPriority, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService';
import { getAllTestsInHierarchy, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
class TestDto {
@ -86,46 +89,66 @@ class TestDto {
}
}
type TestUriWithDocument = ParsedTestUri & { documentUri: URI };
export class TestingPeekOpener extends Disposable implements ITestingPeekOpener {
declare _serviceBrand: undefined;
private lastUri?: TestUriWithDocument;
constructor(
@IConfigurationService private readonly configuration: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@ITestResultService testResults: ITestResultService,
@ITestResultService private readonly testResults: ITestResultService,
@ITestService private readonly testService: ITestService,
) {
super();
this._register(testResults.onTestChanged(this.openPeekOnFailure, this));
}
/** @inheritdoc */
public async open() {
let uri: TestUriWithDocument | undefined;
const active = this.editorService.activeTextEditorControl;
if (isCodeEditor(active) && active.getModel()?.uri) {
const modelUri = active.getModel()?.uri;
if (modelUri) {
uri = await this.getFileCandidateMessage(modelUri, active.getPosition());
}
}
if (!uri) {
uri = this.lastUri;
}
if (!uri) {
uri = this.getAnyCandidateMessage();
}
if (!uri) {
return false;
}
return this.showPeekFromUri(uri);
}
/** @inheritdoc */
public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>) {
const candidate = this.getCandidateMessage(test);
const candidate = this.getFailedCandidateMessage(test);
if (!candidate) {
return false;
}
const message = candidate.message;
const pane = await this.editorService.openEditor({
resource: message.location!.uri,
options: { selection: message.location!.range, revealIfOpened: true, ...options }
});
const control = pane?.getControl();
if (!isCodeEditor(control)) {
return false;
}
TestingOutputPeekController.get(control).show(buildTestUri({
return this.showPeekFromUri({
type: TestUriType.ResultMessage,
documentUri: message.location!.uri,
taskIndex: candidate.taskId,
messageIndex: candidate.index,
resultId: result.id,
testExtId: test.item.extId,
}));
return true;
}, { selection: message.location!.range, ...options });
}
/** @inheritdoc */
@ -135,6 +158,22 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
}
}
private async showPeekFromUri(uri: TestUriWithDocument, options?: ITextEditorOptions) {
const pane = await this.editorService.openEditor({
resource: uri.documentUri,
options: { revealIfOpened: true, ...options }
});
const control = pane?.getControl();
if (!isCodeEditor(control)) {
return false;
}
this.lastUri = uri;
TestingOutputPeekController.get(control).show(buildTestUri(this.lastUri));
return true;
}
/**
* Opens the peek view on a test failure, based on user preferences.
*/
@ -143,7 +182,7 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
return;
}
const candidate = this.getCandidateMessage(evt.item);
const candidate = this.getFailedCandidateMessage(evt.item);
if (!candidate) {
return;
}
@ -170,25 +209,109 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
this.tryPeekFirstError(evt.result, evt.item);
}
private getCandidateMessage(test: TestResultItem) {
for (let taskId = 0; taskId < test.tasks.length; taskId++) {
const { messages, state } = test.tasks[taskId];
if (!isFailedState(state)) {
continue;
/**
* Gets the message closest to the given position from a test in the file.
*/
private async getFileCandidateMessage(uri: URI, position: Position | null) {
const tests = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri);
try {
await getAllTestsInHierarchy(tests.object);
let best: TestUriWithDocument | undefined;
let bestDistance = Infinity;
// Get all tests for the document. In those, find one that has a test
// message closest to the cursor position.
for (const test of tests.object.all) {
const result = this.testResults.getStateById(test.item.extId);
if (!result) {
continue;
}
mapFindTestMessage(result[1], (_task, message, messageIndex, taskIndex) => {
if (!message.location || message.location.uri.toString() !== uri.toString()) {
return;
}
const distance = position ? Math.abs(position.lineNumber - message.location.range.startLineNumber) : 0;
if (!best || distance <= bestDistance) {
bestDistance = distance;
best = {
type: TestUriType.ResultMessage,
testExtId: result[1].item.extId,
resultId: result[0].id,
taskIndex,
messageIndex,
documentUri: uri,
};
}
});
}
const index = messages.findIndex(m => !!m.location);
if (index === -1) {
continue;
}
return best;
} finally {
tests.dispose();
}
}
return { taskId, index, message: messages[index] };
/**
* Gets any possible still-relevant message from the results.
*/
private getAnyCandidateMessage() {
const seen = new Set<string>();
for (const result of this.testResults.results) {
for (const test of result.tests) {
if (seen.has(test.item.extId)) {
continue;
}
seen.add(test.item.extId);
const found = mapFindTestMessage(test, (task, message, messageIndex, taskIndex) => (
message.location && {
type: TestUriType.ResultMessage,
testExtId: test.item.extId,
resultId: result.id,
taskIndex,
messageIndex,
documentUri: message.location.uri,
}
));
if (found) {
return found;
}
}
}
return undefined;
}
/**
* Gets the first failed message that can be displayed from the result.
*/
private getFailedCandidateMessage(test: TestResultItem) {
return mapFindTestMessage(test, (task, message, messageIndex, taskId) =>
isFailedState(task.state) && message.location
? { taskId, index: messageIndex, message }
: undefined
);
}
}
const mapFindTestMessage = <T>(test: TestResultItem, fn: (task: ITestTaskState, message: ITestMessage, messageIndex: number, taskIndex: number) => T | undefined) => {
for (let taskIndex = 0; taskIndex < test.tasks.length; taskIndex++) {
const task = test.tasks[taskIndex];
for (let messageIndex = 0; messageIndex < task.messages.length; messageIndex++) {
const r = fn(task, task.messages[messageIndex], messageIndex, taskIndex);
if (r !== undefined) {
return r;
}
}
}
return undefined;
};
/**
* Adds output/message peek functionality to code editors.
*/

View file

@ -17,6 +17,11 @@ export interface ITestingPeekOpener {
*/
tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>): Promise<boolean>;
/**
* Opens the peek. Shows any available message.
*/
open(): void;
/**
* Closes peeks for all visible editors.
*/