mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
testing: hotkey for opening output peek
This commit is contained in:
parent
dd61b26f33
commit
31aef10814
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue