mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
testing: menu contribution points around messages (#190298)
- Implements the proposal in #190277 by adding a `contextValue` to TestMessages added to test runs. - Make the `FloatingClickMenu` reusable outside the editor, and uses it to implement a `testing/message/content` contribution point. With this extensions can do things like: ![](https://memes.peet.io/img/23-08-68e2f9db-abc4-4717-9da6-698b002c481c.png)
This commit is contained in:
parent
1b8729178c
commit
2d9cc42045
|
@ -40,6 +40,7 @@
|
|||
"envShellEvent",
|
||||
"testCoverage",
|
||||
"testObserver",
|
||||
"testMessageContextValue",
|
||||
"textSearchProvider",
|
||||
"timeline",
|
||||
"tokenInformation",
|
||||
|
|
|
@ -20,5 +20,6 @@ export const enum MarshalledId {
|
|||
NotebookCellActionContext,
|
||||
NotebookActionContext,
|
||||
TestItemContext,
|
||||
Date
|
||||
Date,
|
||||
TestMessageMenuArgs,
|
||||
}
|
||||
|
|
129
src/vs/platform/actions/browser/floatingMenu.ts
Normal file
129
src/vs/platform/actions/browser/floatingMenu.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $, append, clearNode } from 'vs/base/browser/dom';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { asCssVariable, asCssVariableWithDefault, buttonBackground, buttonForeground, contrastBorder, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
export class FloatingClickWidget extends Widget {
|
||||
|
||||
private readonly _onClick = this._register(new Emitter<void>());
|
||||
readonly onClick = this._onClick.event;
|
||||
|
||||
private _domNode: HTMLElement;
|
||||
|
||||
constructor(private label: string) {
|
||||
super();
|
||||
|
||||
this._domNode = $('.floating-click-widget');
|
||||
this._domNode.style.padding = '6px 11px';
|
||||
this._domNode.style.borderRadius = '2px';
|
||||
this._domNode.style.cursor = 'pointer';
|
||||
this._domNode.style.zIndex = '1';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
render() {
|
||||
clearNode(this._domNode);
|
||||
this._domNode.style.backgroundColor = asCssVariableWithDefault(buttonBackground, asCssVariable(editorBackground));
|
||||
this._domNode.style.color = asCssVariableWithDefault(buttonForeground, asCssVariable(editorForeground));
|
||||
this._domNode.style.border = `1px solid ${asCssVariable(contrastBorder)}`;
|
||||
|
||||
append(this._domNode, $('')).textContent = this.label;
|
||||
|
||||
this.onclick(this._domNode, () => this._onClick.fire());
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractFloatingClickMenu extends Disposable {
|
||||
private readonly renderEmitter = new Emitter<FloatingClickWidget>();
|
||||
protected readonly onDidRender = this.renderEmitter.event;
|
||||
private readonly menu: IMenu;
|
||||
|
||||
constructor(
|
||||
menuId: MenuId,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
this.menu = this._register(menuService.createMenu(menuId, contextKeyService));
|
||||
}
|
||||
|
||||
/** Should be called in implementation constructors after they initialized */
|
||||
protected render() {
|
||||
const menuDisposables = this._register(new DisposableStore());
|
||||
const renderMenuAsFloatingClickBtn = () => {
|
||||
menuDisposables.clear();
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
const actions: IAction[] = [];
|
||||
createAndFillInActionBarActions(this.menu, { renderShortTitle: true, shouldForwardArgs: true }, actions);
|
||||
if (actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
// todo@jrieken find a way to handle N actions, like showing a context menu
|
||||
const [first] = actions;
|
||||
const widget = this.createWidget(first, menuDisposables);
|
||||
menuDisposables.add(widget);
|
||||
menuDisposables.add(widget.onClick(() => first.run(this.getActionArg())));
|
||||
widget.render();
|
||||
};
|
||||
this._register(this.menu.onDidChange(renderMenuAsFloatingClickBtn));
|
||||
renderMenuAsFloatingClickBtn();
|
||||
}
|
||||
|
||||
protected abstract createWidget(action: IAction, disposables: DisposableStore): FloatingClickWidget;
|
||||
|
||||
protected getActionArg(): unknown {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected isVisible() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class FloatingClickMenu extends AbstractFloatingClickMenu {
|
||||
|
||||
constructor(
|
||||
private readonly options: {
|
||||
/** Element the menu should be rendered into. */
|
||||
container: HTMLElement;
|
||||
/** Menu to show. If no actions are present, the button is hidden. */
|
||||
menuId: MenuId;
|
||||
/** Argument provided to the menu action */
|
||||
getActionArg: () => void;
|
||||
},
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(options.menuId, menuService, contextKeyService);
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected override createWidget(action: IAction, disposable: DisposableStore): FloatingClickWidget {
|
||||
const w = this.instantiationService.createInstance(FloatingClickWidget, action.label);
|
||||
const node = w.getDomNode();
|
||||
this.options.container.appendChild(node);
|
||||
disposable.add(toDisposable(() => this.options.container.removeChild(node)));
|
||||
return w;
|
||||
}
|
||||
|
||||
protected override getActionArg(): unknown {
|
||||
return this.options.getActionArg();
|
||||
}
|
||||
}
|
|
@ -114,6 +114,8 @@ export class MenuId {
|
|||
static readonly StickyScrollContext = new MenuId('StickyScrollContext');
|
||||
static readonly TestItem = new MenuId('TestItem');
|
||||
static readonly TestItemGutter = new MenuId('TestItemGutter');
|
||||
static readonly TestMessageContext = new MenuId('TestMessageContext');
|
||||
static readonly TestMessageContent = new MenuId('TestMessageContent');
|
||||
static readonly TestPeekElement = new MenuId('TestPeekElement');
|
||||
static readonly TestPeekTitle = new MenuId('TestPeekTitle');
|
||||
static readonly TouchBarContext = new MenuId('TouchBarContext');
|
||||
|
|
|
@ -1521,8 +1521,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
LinkedEditingRanges: extHostTypes.LinkedEditingRanges,
|
||||
TestResultState: extHostTypes.TestResultState,
|
||||
TestRunRequest: extHostTypes.TestRunRequest,
|
||||
TestRunRequest2: extHostTypes.TestRunRequest2,
|
||||
TestMessage: extHostTypes.TestMessage,
|
||||
TestMessage2: extHostTypes.TestMessage,
|
||||
TestTag: extHostTypes.TestTag,
|
||||
TestRunProfileKind: extHostTypes.TestRunProfileKind,
|
||||
TextSearchCompleteMessageType: TextSearchCompleteMessageType,
|
||||
|
|
|
@ -17,7 +17,7 @@ import { MarshalledId } from 'vs/base/common/marshallingIds';
|
|||
import { deepFreeze } from 'vs/base/common/objects';
|
||||
import { isDefined } from 'vs/base/common/types';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtHostTestingShape, ILocationDto, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
|
@ -28,13 +28,15 @@ import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extH
|
|||
import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
|
||||
import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection';
|
||||
import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
interface ControllerInfo {
|
||||
controller: vscode.TestController;
|
||||
profiles: Map<number, vscode.TestRunProfile>;
|
||||
collection: ExtHostTestItemCollection;
|
||||
extension: Readonly<IRelaxedExtensionDescription>;
|
||||
}
|
||||
|
||||
export class ExtHostTesting implements ExtHostTestingShape {
|
||||
|
@ -58,14 +60,22 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
|
||||
commands.registerArgumentProcessor({
|
||||
processArgument: arg => {
|
||||
if (arg?.$mid !== MarshalledId.TestItemContext) {
|
||||
return arg;
|
||||
switch (arg?.$mid) {
|
||||
case MarshalledId.TestItemContext: {
|
||||
const cast = arg as ITestItemContext;
|
||||
const targetTest = cast.tests[cast.tests.length - 1].item.extId;
|
||||
const controller = this.controllers.get(TestId.root(targetTest));
|
||||
return controller?.collection.tree.get(targetTest)?.actual ?? toItemFromContext(arg);
|
||||
}
|
||||
case MarshalledId.TestMessageMenuArgs: {
|
||||
const { extId, message } = arg as ITestMessageMenuArgs;
|
||||
return {
|
||||
test: this.controllers.get(TestId.root(extId))?.collection.tree.get(extId)?.actual,
|
||||
message: Convert.TestMessage.to(message as ITestErrorMessage.Serialized),
|
||||
};
|
||||
}
|
||||
default: return arg;
|
||||
}
|
||||
|
||||
const cast = arg as ITestItemContext;
|
||||
const targetTest = cast.tests[cast.tests.length - 1].item.extId;
|
||||
const controller = this.controllers.get(TestId.root(targetTest));
|
||||
return controller?.collection.tree.get(targetTest)?.actual ?? toItemFromContext(arg);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -137,7 +147,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
return new TestItemImpl(controllerId, id, label, uri);
|
||||
},
|
||||
createTestRun: (request, name, persist = true) => {
|
||||
return this.runTracker.createTestRun(controllerId, collection, request, name, persist);
|
||||
return this.runTracker.createTestRun(extension, controllerId, collection, request, name, persist);
|
||||
},
|
||||
invalidateTestResults: items => {
|
||||
if (items === undefined) {
|
||||
|
@ -161,7 +171,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
proxy.$registerTestController(controllerId, label, !!refreshHandler);
|
||||
disposable.add(toDisposable(() => proxy.$unregisterTestController(controllerId)));
|
||||
|
||||
const info: ControllerInfo = { controller, collection, profiles: profiles };
|
||||
const info: ControllerInfo = { controller, collection, profiles: profiles, extension };
|
||||
this.controllers.set(controllerId, info);
|
||||
disposable.add(toDisposable(() => this.controllers.delete(controllerId)));
|
||||
|
||||
|
@ -310,7 +320,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
return {};
|
||||
}
|
||||
|
||||
const { collection, profiles } = lookup;
|
||||
const { collection, profiles, extension } = lookup;
|
||||
const profile = profiles.get(req.profileId);
|
||||
if (!profile) {
|
||||
return {};
|
||||
|
@ -341,6 +351,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
|||
const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun(
|
||||
publicReq,
|
||||
TestRunDto.fromInternal(req, lookup.collection),
|
||||
extension,
|
||||
token,
|
||||
);
|
||||
|
||||
|
@ -410,7 +421,12 @@ class TestRunTracker extends Disposable {
|
|||
return this.dto.id;
|
||||
}
|
||||
|
||||
constructor(private readonly dto: TestRunDto, private readonly proxy: MainThreadTestingShape, parentToken?: CancellationToken) {
|
||||
constructor(
|
||||
private readonly dto: TestRunDto,
|
||||
private readonly proxy: MainThreadTestingShape,
|
||||
private readonly extension: Readonly<IRelaxedExtensionDescription>,
|
||||
parentToken?: CancellationToken,
|
||||
) {
|
||||
super();
|
||||
this.cts = this._register(new CancellationTokenSource(parentToken));
|
||||
|
||||
|
@ -460,6 +476,10 @@ class TestRunTracker extends Disposable {
|
|||
? messages.map(Convert.TestMessage.from)
|
||||
: [Convert.TestMessage.from(messages)];
|
||||
|
||||
if (converted.some(c => c.contextValue !== undefined)) {
|
||||
checkProposedApiEnabled(this.extension, 'testMessageContextValue');
|
||||
}
|
||||
|
||||
if (test.uri && test.range) {
|
||||
const defaultLocation: ILocationDto = { range: Convert.Range.from(test.range), uri: test.uri };
|
||||
for (const message of converted) {
|
||||
|
@ -606,8 +626,8 @@ export class TestRunCoordinator {
|
|||
* `$startedExtensionTestRun` is not invoked. The run must eventually
|
||||
* be cancelled manually.
|
||||
*/
|
||||
public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, token: CancellationToken) {
|
||||
return this.getTracker(req, dto, token);
|
||||
public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, extension: Readonly<IRelaxedExtensionDescription>, token: CancellationToken) {
|
||||
return this.getTracker(req, dto, extension, token);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -635,7 +655,7 @@ export class TestRunCoordinator {
|
|||
/**
|
||||
* Implements the public `createTestRun` API.
|
||||
*/
|
||||
public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun {
|
||||
public createTestRun(extension: IRelaxedExtensionDescription, controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun {
|
||||
const existing = this.tracked.get(request);
|
||||
if (existing) {
|
||||
return existing.createRun(name);
|
||||
|
@ -655,7 +675,7 @@ export class TestRunCoordinator {
|
|||
persist
|
||||
});
|
||||
|
||||
const tracker = this.getTracker(request, dto);
|
||||
const tracker = this.getTracker(request, dto, extension);
|
||||
tracker.onEnd(() => {
|
||||
this.proxy.$finishedExtensionTestRun(dto.id);
|
||||
tracker.dispose();
|
||||
|
@ -664,8 +684,8 @@ export class TestRunCoordinator {
|
|||
return tracker.createRun(name);
|
||||
}
|
||||
|
||||
private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, token?: CancellationToken) {
|
||||
const tracker = new TestRunTracker(dto, this.proxy, token);
|
||||
private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, extension: IRelaxedExtensionDescription, token?: CancellationToken) {
|
||||
const tracker = new TestRunTracker(dto, this.proxy, extension, token);
|
||||
this.tracked.set(req, tracker);
|
||||
tracker.onEnd(() => this.tracked.delete(req));
|
||||
return tracker;
|
||||
|
|
|
@ -1799,20 +1799,22 @@ export namespace NotebookRendererScript {
|
|||
}
|
||||
|
||||
export namespace TestMessage {
|
||||
export function from(message: vscode.TestMessage): ITestErrorMessage.Serialized {
|
||||
export function from(message: vscode.TestMessage2): ITestErrorMessage.Serialized {
|
||||
return {
|
||||
message: MarkdownString.fromStrict(message.message) || '',
|
||||
type: TestMessageType.Error,
|
||||
expected: message.expectedOutput,
|
||||
actual: message.actualOutput,
|
||||
contextValue: message.contextValue,
|
||||
location: message.location && ({ range: Range.from(message.location.range), uri: message.location.uri }),
|
||||
};
|
||||
}
|
||||
|
||||
export function to(item: ITestErrorMessage.Serialized): vscode.TestMessage {
|
||||
export function to(item: ITestErrorMessage.Serialized): vscode.TestMessage2 {
|
||||
const message = new types.TestMessage(typeof item.message === 'string' ? item.message : MarkdownString.to(item.message));
|
||||
message.actualOutput = item.actual;
|
||||
message.expectedOutput = item.expected;
|
||||
message.contextValue = item.contextValue;
|
||||
message.location = item.location ? location.to(item.location) : undefined;
|
||||
return message;
|
||||
}
|
||||
|
|
|
@ -3884,15 +3884,13 @@ export class TestRunRequest implements vscode.TestRunRequest {
|
|||
) { }
|
||||
}
|
||||
|
||||
/** Back-compat for proposed API users */
|
||||
@es5ClassCompat
|
||||
export class TestRunRequest2 extends TestRunRequest { }
|
||||
|
||||
@es5ClassCompat
|
||||
export class TestMessage implements vscode.TestMessage {
|
||||
public expectedOutput?: string;
|
||||
public actualOutput?: string;
|
||||
public location?: vscode.Location;
|
||||
/** proposed: */
|
||||
public contextValue?: string;
|
||||
|
||||
public static diff(message: string | vscode.MarkdownString, expected: string, actual: string) {
|
||||
const msg = new TestMessage(message);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Iterable } from 'vs/base/common/iterator';
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import { mockObject, MockObject } from 'vs/base/test/common/mock';
|
||||
import * as editorRange from 'vs/editor/common/core/range';
|
||||
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
import { TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting';
|
||||
|
@ -594,6 +595,7 @@ suite('ExtHost Testing', () => {
|
|||
let req: TestRunRequest;
|
||||
|
||||
let dto: TestRunDto;
|
||||
const ext: IRelaxedExtensionDescription = {} as any;
|
||||
|
||||
setup(async () => {
|
||||
proxy = mockObject<MainThreadTestingShape>()();
|
||||
|
@ -621,11 +623,11 @@ suite('ExtHost Testing', () => {
|
|||
});
|
||||
|
||||
test('tracks a run started from a main thread request', () => {
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, cts.token);
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, ext, cts.token);
|
||||
assert.strictEqual(tracker.hasRunningTasks, false);
|
||||
|
||||
const task1 = c.createTestRun('ctrl', single, req, 'run1', true);
|
||||
const task2 = c.createTestRun('ctrl', single, req, 'run2', true);
|
||||
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
|
||||
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
|
||||
assert.strictEqual(proxy.$startedExtensionTestRun.called, false);
|
||||
assert.strictEqual(tracker.hasRunningTasks, true);
|
||||
|
||||
|
@ -646,8 +648,8 @@ suite('ExtHost Testing', () => {
|
|||
test('run cancel force ends after a timeout', () => {
|
||||
const clock = sinon.useFakeTimers();
|
||||
try {
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, cts.token);
|
||||
const task = c.createTestRun('ctrl', single, req, 'run1', true);
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, ext, cts.token);
|
||||
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
|
||||
const onEnded = sinon.stub();
|
||||
tracker.onEnd(onEnded);
|
||||
|
||||
|
@ -671,8 +673,8 @@ suite('ExtHost Testing', () => {
|
|||
});
|
||||
|
||||
test('run cancel force ends on second cancellation request', () => {
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, cts.token);
|
||||
const task = c.createTestRun('ctrl', single, req, 'run1', true);
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, ext, cts.token);
|
||||
const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true);
|
||||
const onEnded = sinon.stub();
|
||||
tracker.onEnd(onEnded);
|
||||
|
||||
|
@ -690,7 +692,7 @@ suite('ExtHost Testing', () => {
|
|||
});
|
||||
|
||||
test('tracks a run started from an extension request', () => {
|
||||
const task1 = c.createTestRun('ctrl', single, req, 'hello world', false);
|
||||
const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
|
||||
|
||||
const tracker = Iterable.first(c.trackers)!;
|
||||
assert.strictEqual(tracker.hasRunningTasks, true);
|
||||
|
@ -706,8 +708,8 @@ suite('ExtHost Testing', () => {
|
|||
}]
|
||||
]);
|
||||
|
||||
const task2 = c.createTestRun('ctrl', single, req, 'run2', true);
|
||||
const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true);
|
||||
const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true);
|
||||
const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true);
|
||||
|
||||
task1.end();
|
||||
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
|
||||
|
@ -721,7 +723,7 @@ suite('ExtHost Testing', () => {
|
|||
});
|
||||
|
||||
test('adds tests to run smartly', () => {
|
||||
const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false);
|
||||
const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
|
||||
const tracker = Iterable.first(c.trackers)!;
|
||||
const expectedArgs: unknown[][] = [];
|
||||
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
|
||||
|
@ -758,7 +760,7 @@ suite('ExtHost Testing', () => {
|
|||
const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt'));
|
||||
test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0));
|
||||
single.root.children.replace([test1, test2]);
|
||||
const task = c.createTestRun('ctrlId', single, req, 'hello world', false);
|
||||
const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false);
|
||||
|
||||
const message1 = new TestMessage('some message');
|
||||
message1.location = new Location(URI.file('/a.txt'), new Position(0, 0));
|
||||
|
@ -773,6 +775,7 @@ suite('ExtHost Testing', () => {
|
|||
message: 'some message',
|
||||
type: TestMessageType.Error,
|
||||
expected: undefined,
|
||||
contextValue: undefined,
|
||||
actual: undefined,
|
||||
location: convert.location.from(message1.location)
|
||||
}]
|
||||
|
@ -787,6 +790,7 @@ suite('ExtHost Testing', () => {
|
|||
[{
|
||||
message: 'some message',
|
||||
type: TestMessageType.Error,
|
||||
contextValue: undefined,
|
||||
expected: undefined,
|
||||
actual: undefined,
|
||||
location: convert.location.from({ uri: test2.uri!, range: test2.range! }),
|
||||
|
@ -795,7 +799,7 @@ suite('ExtHost Testing', () => {
|
|||
});
|
||||
|
||||
test('guards calls after runs are ended', () => {
|
||||
const task = c.createTestRun('ctrl', single, req, 'hello world', false);
|
||||
const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false);
|
||||
task.end();
|
||||
|
||||
task.failed(single.root, new TestMessage('some message'));
|
||||
|
@ -807,7 +811,7 @@ suite('ExtHost Testing', () => {
|
|||
});
|
||||
|
||||
test('excludes tests outside tree or explicitly excluded', () => {
|
||||
const task = c.createTestRun('ctrlId', single, {
|
||||
const task = c.createTestRun(ext, 'ctrlId', single, {
|
||||
profile: configuration,
|
||||
include: [single.root.children.get('id-a')!],
|
||||
exclude: [single.root.children.get('id-a')!.children.get('id-aa')!],
|
||||
|
@ -835,7 +839,7 @@ suite('ExtHost Testing', () => {
|
|||
const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined);
|
||||
testB!.children.replace([childB]);
|
||||
|
||||
const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false);
|
||||
const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false);
|
||||
const tracker = Iterable.first(c.trackers)!;
|
||||
|
||||
task1.passed(childA);
|
||||
|
|
|
@ -3,28 +3,25 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { $, append, clearNode } from 'vs/base/browser/dom';
|
||||
import { buttonBackground, buttonForeground, editorBackground, editorForeground, contrastBorder, asCssVariableWithDefault, asCssVariable } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { AbstractFloatingClickMenu, FloatingClickWidget } from 'vs/platform/actions/browser/floatingMenu';
|
||||
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export interface IRangeHighlightDecoration {
|
||||
resource: URI;
|
||||
|
@ -134,106 +131,65 @@ export class RangeHighlightDecorations extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
export class FloatingClickWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
private readonly _onClick = this._register(new Emitter<void>());
|
||||
readonly onClick = this._onClick.event;
|
||||
|
||||
private _domNode: HTMLElement;
|
||||
export class FloatingEditorClickWidget extends FloatingClickWidget implements IOverlayWidget {
|
||||
|
||||
constructor(
|
||||
private editor: ICodeEditor,
|
||||
private label: string,
|
||||
label: string,
|
||||
keyBindingAction: string | null,
|
||||
@IKeybindingService keybindingService: IKeybindingService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._domNode = $('.floating-click-widget');
|
||||
this._domNode.style.padding = '6px 11px';
|
||||
this._domNode.style.borderRadius = '2px';
|
||||
this._domNode.style.cursor = 'pointer';
|
||||
this._domNode.style.zIndex = '1';
|
||||
|
||||
if (keyBindingAction) {
|
||||
const keybinding = keybindingService.lookupKeybinding(keyBindingAction);
|
||||
if (keybinding) {
|
||||
this.label += ` (${keybinding.getLabel()})`;
|
||||
}
|
||||
}
|
||||
super(
|
||||
keyBindingAction && keybindingService.lookupKeybinding(keyBindingAction)
|
||||
? `${label} (${keybindingService.lookupKeybinding(keyBindingAction)!.getLabel()})`
|
||||
: label
|
||||
);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'editor.overlayWidget.floatingClickWidget';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
getPosition(): IOverlayWidgetPosition {
|
||||
return {
|
||||
preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
clearNode(this._domNode);
|
||||
this._domNode.style.backgroundColor = asCssVariableWithDefault(buttonBackground, asCssVariable(editorBackground));
|
||||
this._domNode.style.color = asCssVariableWithDefault(buttonForeground, asCssVariable(editorForeground));
|
||||
this._domNode.style.border = `1px solid ${asCssVariable(contrastBorder)}`;
|
||||
|
||||
append(this._domNode, $('')).textContent = this.label;
|
||||
|
||||
this.onclick(this._domNode, e => this._onClick.fire());
|
||||
|
||||
override render() {
|
||||
super.render();
|
||||
this.editor.addOverlayWidget(this);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.editor.removeOverlayWidget(this);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FloatingClickMenu extends Disposable implements IEditorContribution {
|
||||
|
||||
export class FloatingEditorClickMenu extends AbstractFloatingClickMenu implements IEditorContribution {
|
||||
static readonly ID = 'editor.contrib.floatingClickMenu';
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
private readonly editor: ICodeEditor,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
super(MenuId.EditorContent, menuService, contextKeyService);
|
||||
this.render();
|
||||
}
|
||||
|
||||
// DISABLED for embedded editors. In the future we can use a different MenuId for embedded editors
|
||||
if (!(editor instanceof EmbeddedCodeEditorWidget)) {
|
||||
const menu = menuService.createMenu(MenuId.EditorContent, contextKeyService);
|
||||
const menuDisposables = new DisposableStore();
|
||||
const renderMenuAsFloatingClickBtn = () => {
|
||||
menuDisposables.clear();
|
||||
if (!editor.hasModel() || editor.getOption(EditorOption.inDiffEditor)) {
|
||||
return;
|
||||
}
|
||||
const actions: IAction[] = [];
|
||||
createAndFillInActionBarActions(menu, { renderShortTitle: true, shouldForwardArgs: true }, actions);
|
||||
if (actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
// todo@jrieken find a way to handle N actions, like showing a context menu
|
||||
const [first] = actions;
|
||||
const widget = instantiationService.createInstance(FloatingClickWidget, editor, first.label, first.id);
|
||||
menuDisposables.add(widget);
|
||||
menuDisposables.add(widget.onClick(() => first.run(editor.getModel().uri)));
|
||||
widget.render();
|
||||
};
|
||||
this._store.add(menu);
|
||||
this._store.add(menuDisposables);
|
||||
this._store.add(menu.onDidChange(renderMenuAsFloatingClickBtn));
|
||||
renderMenuAsFloatingClickBtn();
|
||||
}
|
||||
protected override createWidget(action: IAction): FloatingClickWidget {
|
||||
return this.instantiationService.createInstance(FloatingEditorClickWidget, this.editor, action.label, action.id);
|
||||
}
|
||||
|
||||
protected override isVisible() {
|
||||
return !(this.editor instanceof EmbeddedCodeEditorWidget) && this.editor?.hasModel() && !this.editor.getOption(EditorOption.inDiffEditor);
|
||||
}
|
||||
|
||||
protected override getActionArg(): unknown {
|
||||
return this.editor.getModel()?.uri;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co
|
|||
import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { FloatingClickMenu } from 'vs/workbench/browser/codeeditor';
|
||||
import { FloatingEditorClickMenu } from 'vs/workbench/browser/codeeditor';
|
||||
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave';
|
||||
|
@ -131,7 +131,7 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).regi
|
|||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(UntitledTextEditorWorkingCopyEditorHandler, LifecyclePhase.Ready);
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DynamicEditorConfigurations, LifecyclePhase.Ready);
|
||||
|
||||
registerEditorContribution(FloatingClickMenu.ID, FloatingClickMenu, EditorContributionInstantiation.AfterFirstRender);
|
||||
registerEditorContribution(FloatingEditorClickMenu.ID, FloatingEditorClickMenu, EditorContributionInstantiation.AfterFirstRender);
|
||||
//#endregion
|
||||
|
||||
//#region Quick Access
|
||||
|
|
|
@ -18,7 +18,7 @@ import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/com
|
|||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor';
|
||||
import { FloatingEditorClickWidget } from 'vs/workbench/browser/codeeditor';
|
||||
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
|
||||
import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
|
||||
import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';
|
||||
|
@ -47,7 +47,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont
|
|||
/** @description update state */
|
||||
if (onlyWhiteSpaceChange.read(reader)) {
|
||||
const helperWidget = store.add(this._instantiationService.createInstance(
|
||||
FloatingClickWidget,
|
||||
FloatingEditorClickWidget,
|
||||
this._diffEditor.getModifiedEditor(),
|
||||
localize('hintWhitespace', "Show Whitespace Differences"),
|
||||
null
|
||||
|
|
|
@ -41,7 +41,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
|
|||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor';
|
||||
import { FloatingEditorClickWidget } from 'vs/workbench/browser/codeeditor';
|
||||
import { DebugHoverWidget, ShowDebugHoverResult } from 'vs/workbench/contrib/debug/browser/debugHover';
|
||||
import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget';
|
||||
import { CONTEXT_EXCEPTION_WIDGET_VISIBLE, IDebugConfiguration, IDebugEditorContribution, IDebugService, IDebugSession, IExceptionInfo, IExpression, IStackFrame, State } from 'vs/workbench/contrib/debug/common/debug';
|
||||
|
@ -219,7 +219,7 @@ export class DebugEditorContribution implements IDebugEditorContribution {
|
|||
private gutterIsHovered = false;
|
||||
|
||||
private exceptionWidget: ExceptionWidget | undefined;
|
||||
private configurationWidget: FloatingClickWidget | undefined;
|
||||
private configurationWidget: FloatingEditorClickWidget | undefined;
|
||||
private altListener: IDisposable | undefined;
|
||||
private altPressed = false;
|
||||
private oldDecorations = this.editor.createDecorationsCollection();
|
||||
|
|
|
@ -87,7 +87,7 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
|
|||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { NotebookPerfMarks } from 'vs/workbench/contrib/notebook/common/notebookPerformance';
|
||||
import { BaseCellEditorOptions } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions';
|
||||
import { FloatingClickMenu } from 'vs/workbench/browser/codeeditor';
|
||||
import { FloatingEditorClickMenu } from 'vs/workbench/browser/codeeditor';
|
||||
import { IDimension } from 'vs/editor/common/core/dimension';
|
||||
import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel';
|
||||
import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService';
|
||||
|
@ -107,7 +107,7 @@ export function getDefaultNotebookCreationOptions(): INotebookEditorCreationOpti
|
|||
// We inlined the id to avoid loading comment contrib in tests
|
||||
const skipContributions = [
|
||||
'editor.contrib.review',
|
||||
FloatingClickMenu.ID,
|
||||
FloatingEditorClickMenu.ID,
|
||||
'editor.contrib.dirtydiff',
|
||||
'editor.contrib.testingOutputPeek',
|
||||
'editor.contrib.testingDecorations',
|
||||
|
|
|
@ -199,6 +199,12 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.test-output-peek-message-container .floating-click-widget {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.test-output-peek-message-container,
|
||||
.test-output-peek-tree {
|
||||
height: 100%;
|
||||
|
|
|
@ -27,6 +27,7 @@ import { Iterable } from 'vs/base/common/iterator';
|
|||
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 { MarshalledId } from 'vs/base/common/marshallingIds';
|
||||
import { count } from 'vs/base/common/strings';
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { isDefined } from 'vs/base/common/types';
|
||||
|
@ -48,6 +49,7 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/mar
|
|||
import { IPeekViewService, PeekViewWidget, peekViewResultsBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
|
||||
import { FloatingClickMenu } from 'vs/platform/actions/browser/floatingMenu';
|
||||
import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { Action2, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
|
@ -88,7 +90,7 @@ import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testPro
|
|||
import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChange, TestResultItemChangeReason, maxCountPriority, resultItemParents } 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 { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
|
||||
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
|
@ -98,20 +100,30 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
|
|||
class MessageSubject {
|
||||
public readonly test: ITestItem;
|
||||
public readonly message: ITestMessage;
|
||||
public readonly messages: ITestMessage[];
|
||||
public readonly expectedUri: URI;
|
||||
public readonly actualUri: URI;
|
||||
public readonly messageUri: URI;
|
||||
public readonly revealLocation: IRichLocation | undefined;
|
||||
|
||||
public get isDiffable() {
|
||||
const message = this.messages[this.messageIndex];
|
||||
return message.type === TestMessageType.Error && isDiffable(message);
|
||||
return this.message.type === TestMessageType.Error && isDiffable(this.message);
|
||||
}
|
||||
|
||||
public get contextValue() {
|
||||
return this.message.type === TestMessageType.Error ? this.message.contextValue : undefined;
|
||||
}
|
||||
|
||||
public get context(): ITestMessageMenuArgs {
|
||||
return {
|
||||
$mid: MarshalledId.TestMessageMenuArgs,
|
||||
extId: this.test.extId,
|
||||
message: ITestMessage.serialize(this.message),
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
const messages = test.tasks[taskIndex].messages;
|
||||
this.messageIndex = messageIndex;
|
||||
|
||||
const parts = { messageIndex, resultId, taskIndex, testExtId: test.item.extId };
|
||||
|
@ -119,7 +131,7 @@ class MessageSubject {
|
|||
this.actualUri = buildTestUri({ ...parts, type: TestUriType.ResultActualOutput });
|
||||
this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage });
|
||||
|
||||
const message = this.message = this.messages[this.messageIndex];
|
||||
const message = this.message = messages[this.messageIndex];
|
||||
this.revealLocation = message.location ?? (test.item.uri && test.item.range ? { uri: test.item.uri, range: Range.lift(test.item.range) } : undefined);
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +160,7 @@ type InspectSubject = MessageSubject | TaskSubject | TestOutputSubject;
|
|||
|
||||
const equalsSubject = (a: InspectSubject, b: InspectSubject) =>
|
||||
a.resultId === b.resultId && a.taskIndex === b.taskIndex && (
|
||||
(a instanceof MessageSubject && b instanceof MessageSubject && a.messageIndex === b.messageIndex) ||
|
||||
(a instanceof MessageSubject && b instanceof MessageSubject && a.message === b.message) ||
|
||||
(a instanceof TaskSubject && b instanceof TaskSubject) ||
|
||||
(a instanceof TestOutputSubject && b instanceof TestOutputSubject && a.test === b.test)
|
||||
);
|
||||
|
@ -746,8 +758,10 @@ class TestResultsViewContent extends Disposable {
|
|||
private static lastSplitWidth?: number;
|
||||
|
||||
private readonly didReveal = this._register(new Emitter<{ subject: InspectSubject; preserveFocus: boolean }>());
|
||||
private readonly clickMenu = this._register(new MutableDisposable<FloatingClickMenu>());
|
||||
private dimension?: dom.Dimension;
|
||||
private splitView!: SplitView;
|
||||
private messageContainer!: HTMLElement;
|
||||
private contentProviders!: IPeekOutputRenderer[];
|
||||
private contentProvidersUpdateLimiter = this._register(new Limiter(1));
|
||||
|
||||
|
@ -764,6 +778,7 @@ class TestResultsViewContent extends Disposable {
|
|||
},
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ITextModelService protected readonly modelService: ITextModelService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
@ -774,7 +789,7 @@ class TestResultsViewContent extends Disposable {
|
|||
|
||||
const { historyVisible, showRevealLocationOnMessages } = this.options;
|
||||
const isInPeekView = this.editor !== undefined;
|
||||
const messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container'));
|
||||
const messageContainer = this.messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container'));
|
||||
this.contentProviders = [
|
||||
this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)),
|
||||
this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)),
|
||||
|
@ -834,14 +849,30 @@ class TestResultsViewContent extends Disposable {
|
|||
* Shows a message in-place without showing or changing the peek location.
|
||||
* This is mostly used if peeking a message without a location.
|
||||
*/
|
||||
public async reveal(opts: { subject: InspectSubject; preserveFocus: boolean }) {
|
||||
public reveal(opts: { subject: InspectSubject; preserveFocus: boolean }) {
|
||||
this.didReveal.fire(opts);
|
||||
|
||||
if (!this.current || !equalsSubject(this.current, opts.subject)) {
|
||||
this.current = opts.subject;
|
||||
await this.contentProvidersUpdateLimiter.queue(() => Promise.all(
|
||||
this.contentProviders.map(p => p.update(opts.subject))));
|
||||
if (this.current && equalsSubject(this.current, opts.subject)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.current = opts.subject;
|
||||
return this.contentProvidersUpdateLimiter.queue(async () => {
|
||||
await Promise.all(this.contentProviders.map(p => p.update(opts.subject)));
|
||||
|
||||
if (opts.subject instanceof MessageSubject) {
|
||||
const contextOverlay = this.contextKeyService.createOverlay([[TestingContextKeys.testMessageContext.key, opts.subject.contextValue]]);
|
||||
this.clickMenu.value = this.instantiationService
|
||||
.createChild(new ServiceCollection([IContextKeyService, contextOverlay]))
|
||||
.createInstance(FloatingClickMenu, {
|
||||
container: this.messageContainer,
|
||||
menuId: MenuId.TestMessageContent,
|
||||
getActionArg: () => (opts.subject as MessageSubject).context,
|
||||
});
|
||||
} else {
|
||||
this.clickMenu.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onLayoutBody(height: number, width: number) {
|
||||
|
@ -1507,11 +1538,15 @@ class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
const hintMessagePeekHeight = (msg: ITestMessage) =>
|
||||
isDiffable(msg)
|
||||
const hintMessagePeekHeight = (msg: ITestMessage) => {
|
||||
const msgHeight = isDiffable(msg)
|
||||
? Math.max(hintPeekStrHeight(msg.actual), hintPeekStrHeight(msg.expected))
|
||||
: hintPeekStrHeight(typeof msg.message === 'string' ? msg.message : msg.message.value);
|
||||
|
||||
// add 8ish lines for the size of the title and decorations in the peek.
|
||||
return msgHeight + 8;
|
||||
};
|
||||
|
||||
const firstLine = (str: string) => {
|
||||
const index = str.indexOf('\n');
|
||||
return index === -1 ? str : str.slice(0, index);
|
||||
|
@ -1519,8 +1554,7 @@ const firstLine = (str: string) => {
|
|||
|
||||
const isMultiline = (str: string | undefined) => !!str && str.includes('\n');
|
||||
|
||||
// add 5ish lines for the size of the title and decorations in the peek.
|
||||
const hintPeekStrHeight = (str: string) => Math.min(count(str, '\n') + 5, 24);
|
||||
const hintPeekStrHeight = (str: string) => Math.min(count(str, '\n'), 24);
|
||||
|
||||
class SimpleDiffEditorModel extends EditorModel {
|
||||
public readonly original = this._original.object.textEditorModel;
|
||||
|
@ -1671,13 +1705,23 @@ class TaskElement implements ITreeElement {
|
|||
|
||||
class TestMessageElement implements ITreeElement {
|
||||
public readonly type = 'message';
|
||||
public readonly context: URI;
|
||||
public readonly id: string;
|
||||
public readonly label: string;
|
||||
public readonly uri: URI;
|
||||
public readonly location?: IRichLocation;
|
||||
public readonly description?: string;
|
||||
public readonly onDidChange = Event.None;
|
||||
public readonly contextValue?: string;
|
||||
public readonly message: ITestMessage;
|
||||
|
||||
public get context(): ITestMessageMenuArgs {
|
||||
return {
|
||||
$mid: MarshalledId.TestMessageMenuArgs,
|
||||
extId: this.test.item.extId,
|
||||
message: ITestMessage.serialize(this.message),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
public readonly result: ITestResult,
|
||||
|
@ -1685,10 +1729,11 @@ class TestMessageElement implements ITreeElement {
|
|||
public readonly taskIndex: number,
|
||||
public readonly messageIndex: number,
|
||||
) {
|
||||
const m = test.tasks[taskIndex].messages[messageIndex];
|
||||
const m = this.message = test.tasks[taskIndex].messages[messageIndex];
|
||||
|
||||
this.location = m.location;
|
||||
this.uri = this.context = buildTestUri({
|
||||
this.contextValue = m.type === TestMessageType.Error ? m.contextValue : undefined;
|
||||
this.uri = buildTestUri({
|
||||
type: TestUriType.ResultMessage,
|
||||
messageIndex,
|
||||
resultId: result.id,
|
||||
|
@ -2084,7 +2129,7 @@ class TestRunElementRenderer implements ICompressibleTreeRenderer<ITreeElement,
|
|||
|
||||
const actions = this.treeActions.provideActionBar(element);
|
||||
templateData.actionBar.clear();
|
||||
templateData.actionBar.context = element;
|
||||
templateData.actionBar.context = element.context;
|
||||
templateData.actionBar.push(actions.primary, { icon: true, label: false });
|
||||
}
|
||||
}
|
||||
|
@ -2103,130 +2148,134 @@ class TreeActionsProvider {
|
|||
public provideActionBar(element: ITreeElement) {
|
||||
const test = element instanceof TestCaseElement ? element.test : undefined;
|
||||
const capabilities = test ? this.testProfileService.capabilitiesForTest(test) : 0;
|
||||
const contextOverlay = this.contextKeyService.createOverlay([
|
||||
|
||||
const contextKeys: [string, unknown][] = [
|
||||
['peek', Testing.OutputPeekContributionId],
|
||||
[TestingContextKeys.peekItemType.key, element.type],
|
||||
...getTestItemContextOverlay(test, capabilities),
|
||||
]);
|
||||
const menu = this.menuService.createMenu(MenuId.TestPeekElement, contextOverlay);
|
||||
];
|
||||
|
||||
try {
|
||||
const primary: IAction[] = [];
|
||||
const secondary: IAction[] = [];
|
||||
let id = MenuId.TestPeekElement;
|
||||
const primary: IAction[] = [];
|
||||
const secondary: IAction[] = [];
|
||||
|
||||
if (element instanceof TaskElement) {
|
||||
if (element instanceof TaskElement) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.showResultOutput',
|
||||
localize('testing.showResultOutput', "Show Result Output"),
|
||||
ThemeIcon.asClassName(Codicon.terminal),
|
||||
undefined,
|
||||
() => this.requestReveal.fire(new TaskSubject(element.results.id, element.index)),
|
||||
));
|
||||
}
|
||||
|
||||
if (element instanceof TestResultElement) {
|
||||
// only show if there are no collapsed test nodes that have more specific choices
|
||||
if (element.value.tasks.length === 1) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.showResultOutput',
|
||||
localize('testing.showResultOutput', "Show Result Output"),
|
||||
ThemeIcon.asClassName(Codicon.terminal),
|
||||
undefined,
|
||||
() => this.requestReveal.fire(new TaskSubject(element.results.id, element.index)),
|
||||
() => this.requestReveal.fire(new TaskSubject(element.value.id, 0)),
|
||||
));
|
||||
}
|
||||
|
||||
if (element instanceof TestResultElement) {
|
||||
// only show if there are no collapsed test nodes that have more specific choices
|
||||
if (element.value.tasks.length === 1) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.showResultOutput',
|
||||
localize('testing.showResultOutput', "Show Result Output"),
|
||||
ThemeIcon.asClassName(Codicon.terminal),
|
||||
undefined,
|
||||
() => this.requestReveal.fire(new TaskSubject(element.value.id, 0)),
|
||||
));
|
||||
}
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.reRunLastRun',
|
||||
localize('testing.reRunLastRun', "Rerun Test Run"),
|
||||
ThemeIcon.asClassName(icons.testingRunIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('testing.reRunLastRun', element.value.id),
|
||||
));
|
||||
|
||||
if (capabilities & TestRunProfileBitset.Debug) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.reRunLastRun',
|
||||
localize('testing.reRunLastRun', "Rerun Test Run"),
|
||||
'testing.outputPeek.debugLastRun',
|
||||
localize('testing.debugLastRun', "Debug Test Run"),
|
||||
ThemeIcon.asClassName(icons.testingDebugIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('testing.debugLastRun', element.value.id),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof TestCaseElement) {
|
||||
const extId = element.test.item.extId;
|
||||
contextKeys.push(...getTestItemContextOverlay(element.test, capabilities));
|
||||
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.goToFile',
|
||||
localize('testing.goToFile', "Go to Source"),
|
||||
ThemeIcon.asClassName(Codicon.goToFile),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('vscode.revealTest', extId),
|
||||
));
|
||||
|
||||
if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.showResultOutput',
|
||||
localize('testing.showResultOutput', "Show Result Output"),
|
||||
ThemeIcon.asClassName(Codicon.terminal),
|
||||
undefined,
|
||||
() => this.requestReveal.fire(element.outputSubject),
|
||||
));
|
||||
}
|
||||
|
||||
secondary.push(new Action(
|
||||
'testing.outputPeek.revealInExplorer',
|
||||
localize('testing.revealInExplorer', "Reveal in Test Explorer"),
|
||||
ThemeIcon.asClassName(Codicon.listTree),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('_revealTestInExplorer', extId),
|
||||
));
|
||||
|
||||
if (capabilities & TestRunProfileBitset.Run) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.runTest',
|
||||
localize('run test', 'Run Test'),
|
||||
ThemeIcon.asClassName(icons.testingRunIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('testing.reRunLastRun', element.value.id),
|
||||
() => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Run, extId),
|
||||
));
|
||||
|
||||
if (capabilities & TestRunProfileBitset.Debug) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.debugLastRun',
|
||||
localize('testing.debugLastRun', "Debug Test Run"),
|
||||
ThemeIcon.asClassName(icons.testingDebugIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('testing.debugLastRun', element.value.id),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof TestCaseElement) {
|
||||
const extId = element.test.item.extId;
|
||||
if (capabilities & TestRunProfileBitset.Debug) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.goToFile',
|
||||
localize('testing.goToFile', "Go to Source"),
|
||||
'testing.outputPeek.debugTest',
|
||||
localize('debug test', 'Debug Test'),
|
||||
ThemeIcon.asClassName(icons.testingDebugIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Debug, extId),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof TestMessageElement) {
|
||||
id = MenuId.TestMessageContext;
|
||||
contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]);
|
||||
if (this.showRevealLocationOnMessages && element.location) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.goToError',
|
||||
localize('testing.goToError', "Go to Source"),
|
||||
ThemeIcon.asClassName(Codicon.goToFile),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('vscode.revealTest', extId),
|
||||
() => this.editorService.openEditor({
|
||||
resource: element.location!.uri,
|
||||
options: {
|
||||
selection: element.location!.range,
|
||||
preserveFocus: true,
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
||||
if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.showResultOutput',
|
||||
localize('testing.showResultOutput', "Show Result Output"),
|
||||
ThemeIcon.asClassName(Codicon.terminal),
|
||||
undefined,
|
||||
() => this.requestReveal.fire(element.outputSubject),
|
||||
));
|
||||
}
|
||||
|
||||
secondary.push(new Action(
|
||||
'testing.outputPeek.revealInExplorer',
|
||||
localize('testing.revealInExplorer', "Reveal in Test Explorer"),
|
||||
ThemeIcon.asClassName(Codicon.listTree),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('_revealTestInExplorer', extId),
|
||||
));
|
||||
|
||||
if (capabilities & TestRunProfileBitset.Run) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.runTest',
|
||||
localize('run test', 'Run Test'),
|
||||
ThemeIcon.asClassName(icons.testingRunIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Run, extId),
|
||||
));
|
||||
}
|
||||
|
||||
if (capabilities & TestRunProfileBitset.Debug) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.debugTest',
|
||||
localize('debug test', 'Debug Test'),
|
||||
ThemeIcon.asClassName(icons.testingDebugIcon),
|
||||
undefined,
|
||||
() => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Debug, extId),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element instanceof TestMessageElement) {
|
||||
if (this.showRevealLocationOnMessages && element.location) {
|
||||
primary.push(new Action(
|
||||
'testing.outputPeek.goToError',
|
||||
localize('testing.goToError', "Go to Source"),
|
||||
ThemeIcon.asClassName(Codicon.goToFile),
|
||||
undefined,
|
||||
() => this.editorService.openEditor({
|
||||
resource: element.location!.uri,
|
||||
options: {
|
||||
selection: element.location!.range,
|
||||
preserveFocus: true,
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const result = { primary, secondary };
|
||||
createAndFillInActionBarActions(menu, {
|
||||
shouldForwardArgs: true,
|
||||
}, result, 'inline');
|
||||
|
||||
const contextOverlay = this.contextKeyService.createOverlay(contextKeys);
|
||||
const result = { primary, secondary };
|
||||
const menu = this.menuService.createMenu(id, contextOverlay);
|
||||
try {
|
||||
createAndFillInActionBarActions(menu, { arg: element.context }, result, 'inline');
|
||||
return result;
|
||||
} finally {
|
||||
menu.dispose();
|
||||
|
|
|
@ -156,6 +156,7 @@ export interface ITestErrorMessage {
|
|||
type: TestMessageType.Error;
|
||||
expected: string | undefined;
|
||||
actual: string | undefined;
|
||||
contextValue: string | undefined;
|
||||
location: IRichLocation | undefined;
|
||||
}
|
||||
|
||||
|
@ -165,6 +166,7 @@ export namespace ITestErrorMessage {
|
|||
type: TestMessageType.Error;
|
||||
expected: string | undefined;
|
||||
actual: string | undefined;
|
||||
contextValue: string | undefined;
|
||||
location: IRichLocation.Serialize | undefined;
|
||||
}
|
||||
|
||||
|
@ -173,6 +175,7 @@ export namespace ITestErrorMessage {
|
|||
type: TestMessageType.Error,
|
||||
expected: message.expected,
|
||||
actual: message.actual,
|
||||
contextValue: message.contextValue,
|
||||
location: message.location && IRichLocation.serialize(message.location),
|
||||
});
|
||||
|
||||
|
@ -181,6 +184,7 @@ export namespace ITestErrorMessage {
|
|||
type: TestMessageType.Error,
|
||||
expected: message.expected,
|
||||
actual: message.actual,
|
||||
contextValue: message.contextValue,
|
||||
location: message.location && IRichLocation.deserialize(message.location),
|
||||
});
|
||||
}
|
||||
|
@ -632,6 +636,18 @@ export interface ITestItemContext {
|
|||
tests: InternalTestItem.Serialized[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for actions taken in the test explorer view.
|
||||
*/
|
||||
export interface ITestMessageMenuArgs {
|
||||
/** Marshalling marker */
|
||||
$mid: MarshalledId.TestMessageMenuArgs;
|
||||
/** Tests ext ID */
|
||||
extId: string;
|
||||
/** Serialized test message */
|
||||
message: ITestMessage.Serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request from the ext host or main thread to indicate that tests have
|
||||
* changed. It's assumed that any item upserted *must* have its children
|
||||
|
|
|
@ -58,4 +58,8 @@ export namespace TestingContextKeys {
|
|||
type: 'boolean',
|
||||
description: localize('testing.testItemIsHidden', 'Boolean indicating whether the test item is hidden')
|
||||
});
|
||||
export const testMessageContext = new RawContextKey<string>('testMessage', undefined, {
|
||||
type: 'boolean',
|
||||
description: localize('testing.testMessage', 'Value set in `testMessage.contextValue`, available in editor/content and testing/message/context')
|
||||
});
|
||||
}
|
||||
|
|
|
@ -260,6 +260,16 @@ const apiMenus: IAPIMenu[] = [
|
|||
id: MenuId.TestItemGutter,
|
||||
description: localize('testing.item.gutter.title', "The menu for a gutter decoration for a test item"),
|
||||
},
|
||||
{
|
||||
key: 'testing/message/context',
|
||||
id: MenuId.TestMessageContext,
|
||||
description: localize('testing.message.context.title', "A prominent button overlaying editor content where the message is displayed"),
|
||||
},
|
||||
{
|
||||
key: 'testing/message/content',
|
||||
id: MenuId.TestMessageContent,
|
||||
description: localize('testing.message.content.title', "Context menu for the message in the results tree"),
|
||||
},
|
||||
{
|
||||
key: 'extension/context',
|
||||
id: MenuId.ExtensionContext,
|
||||
|
|
|
@ -90,6 +90,7 @@ export const allApiProposals = Object.freeze({
|
|||
terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts',
|
||||
terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts',
|
||||
testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts',
|
||||
testMessageContextValue: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testMessageContextValue.d.ts',
|
||||
testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts',
|
||||
textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts',
|
||||
timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts',
|
||||
|
|
45
src/vscode-dts/vscode.proposed.testMessageContextValue.d.ts
vendored
Normal file
45
src/vscode-dts/vscode.proposed.testMessageContextValue.d.ts
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
// https://github.com/microsoft/vscode/issues/190277
|
||||
|
||||
export class TestMessage2 extends TestMessage {
|
||||
|
||||
/**
|
||||
* Context value of the test item. This can be used to contribute message-
|
||||
* specific actions to the test peek view. The value set here can be found
|
||||
* in the `testMessage` property of the following `menus` contribution points:
|
||||
*
|
||||
* - `testing/message/context` - context menu for the message in the results tree
|
||||
* - `testing/message/content` - a prominent button overlaying editor content where
|
||||
* the message is displayed.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```json
|
||||
* "contributes": {
|
||||
* "menus": {
|
||||
* "testing/message/content": [
|
||||
* {
|
||||
* "command": "extension.deleteCommentThread",
|
||||
* "when": "testMessage == canApplyRichDiff"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The command will be called with an object containing:
|
||||
* - `test`: the {@link TestItem} the message is associated with, *if* it
|
||||
* is still present in the {@link TestController.items} collection.
|
||||
* - `message`: the {@link TestMessage} instance.
|
||||
*/
|
||||
contextValue?: string;
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue