Skip to content

Commit

Permalink
[vscode] Support TestMessage#contextValue (eclipse-theia#13176)
Browse files Browse the repository at this point in the history
Also adds the menu mapping for testing/message/context vscode menu extension.

contributed on behalf of STMicroelectronics

Signed-off-by: Remi Schnekenburger <[email protected]>
  • Loading branch information
rschnekenbu authored Dec 20, 2023
1 parent c97a496 commit 7dcce3e
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

## Unreleased

- [plugin] Support TestMessage.contextValue from vscode API [#13176](https://github.com/eclipse-theia/theia/pull/13176) - contributed on behalf of STMicroelectronics
- [terminal] Use application shell methods for expanding/collapsing bottom panel for "Terminal: Toggle Terminal" command [#13131](https://github.com/eclipse-theia/theia/pull/13131)
- [workspace] Create an empty workspace if no workspace is active on updateWorkspaceFolders [#13181](https://github.com/eclipse-theia/theia/pull/13181) - contributed on behalf of STMicroelectronics

Expand Down
20 changes: 20 additions & 0 deletions packages/plugin-ext/src/common/test-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface TestMessageDTO {
readonly actual?: string;
readonly location?: Location;
readonly message: string | MarkdownString;
readonly contextValue?: string;
}

export interface TestItemDTO {
Expand Down Expand Up @@ -131,3 +132,22 @@ export namespace TestItemReference {
}
}

export interface TestMessageArg {
testItemReference: TestItemReference | undefined,
testMessage: TestMessageDTO
}

export namespace TestMessageArg {
export function is(arg: unknown): arg is TestMessageArg {
return isObject<TestMessageArg>(arg)
&& isObject<TestMessageDTO>(arg.testMessage)
&& (MarkdownString.is(arg.testMessage.message) || typeof arg.testMessage.message === 'string');
}

export function create(testItemReference: TestItemReference | undefined, testMessageDTO: TestMessageDTO): TestMessageArg {
return {
testItemReference: testItemReference,
testMessage: testMessageDTO
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { TimelineItem } from '@theia/timeline/lib/common/timeline-model';
import { ScmCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common';
import { TestItemReference, TestMessageArg } from '../../../common/test-types';
import { PluginScmProvider, PluginScmResource, PluginScmResourceGroup } from '../scm-main';
import { TreeViewWidget } from '../view/tree-view-widget';
import { CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint } from './vscode-theia-menu-mappings';
import { TAB_BAR_TOOLBAR_CONTEXT_MENU } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { TestItem, TestMessage } from '@theia/test/lib/browser/test-service';
import { fromLocation } from '../hierarchy/hierarchy-types-converters';

export type ArgumentAdapter = (...args: unknown[]) => unknown[];

Expand Down Expand Up @@ -79,6 +82,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
@postConstruct()
protected init(): void {
const toCommentArgs: ArgumentAdapter = (...args) => this.toCommentArgs(...args);
const toTestMessageArgs: ArgumentAdapter = (...args) => this.toTestMessageArgs(...args);
const firstArgOnly: ArgumentAdapter = (...args) => [args[0]];
const noArgs: ArgumentAdapter = () => [];
const toScmArgs: ArgumentAdapter = (...args) => this.toScmArgs(...args);
Expand All @@ -100,6 +104,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
['scm/resourceGroup/context', toScmArgs],
['scm/resourceState/context', toScmArgs],
['scm/title', () => [this.toScmArg(this.scmService.selectedRepository)]],
['testing/message/context', toTestMessageArgs],
['timeline/item/context', (...args) => this.toTimelineArgs(...args)],
['view/item/context', (...args) => this.toTreeArgs(...args)],
['view/title', noArgs],
Expand Down Expand Up @@ -230,6 +235,30 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
return timelineArgs;
}

protected toTestMessageArgs(...args: any[]): any[] {
let testItem: TestItem | undefined;
let testMessage: TestMessage | undefined;
for (const arg of args) {
if (TestItem.is(arg)) {
testItem = arg;
} else if (Array.isArray(arg) && TestMessage.is(arg[0])) {
testMessage = arg[0];
}
}
if (testMessage) {
const testItemReference = (testItem && testItem.controller) ? TestItemReference.create(testItem.controller.id, testItem.path) : undefined;
const testMessageDTO = {
message: testMessage.message,
actual: testMessage.actual,
expected: testMessage.expected,
contextValue: testMessage.contextValue,
location: testMessage.location ? fromLocation(testMessage.location) : undefined
};
return [TestMessageArg.create(testItemReference, testMessageDTO)];
}
return [];
}

protected toTimelineArg(arg: TimelineItem): TimelineCommandArg {
return {
timelineHandle: arg.handle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget';
import { WEBVIEW_CONTEXT_MENU, WebviewWidget } from '../webview/webview';
import { EDITOR_LINENUMBER_CONTEXT_MENU } from '@theia/editor/lib/browser/editor-linenumber-contribution';
import { TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution';
import { TEST_RUNS_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-run-view-contribution';

export const PLUGIN_EDITOR_TITLE_MENU = ['plugin_editor/title'];
export const PLUGIN_EDITOR_TITLE_RUN_MENU = ['plugin_editor/title/run'];
Expand All @@ -57,6 +58,7 @@ export const implementedVSCodeContributionPoints = [
'scm/title',
'timeline/item/context',
'testing/item/context',
'testing/message/context',
'view/item/context',
'view/title',
'webview/context'
Expand All @@ -83,6 +85,7 @@ export const codeToTheiaMappings = new Map<ContributionPoint, MenuPath[]>([
['scm/resourceState/context', [ScmTreeWidget.RESOURCE_CONTEXT_MENU]],
['scm/title', [PLUGIN_SCM_TITLE_MENU]],
['testing/item/context', [TEST_VIEW_CONTEXT_MENU]],
['testing/message/context', [TEST_RUNS_CONTEXT_MENU]],
['timeline/item/context', [TIMELINE_ITEM_CONTEXT_MENU]],
['view/item/context', [VIEW_ITEM_CONTEXT_MENU]],
['view/title', [PLUGIN_VIEW_TITLE_MENU]],
Expand Down
26 changes: 25 additions & 1 deletion packages/plugin-ext/src/plugin/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ import { TestItemImpl, TestItemCollection } from './test-item';
import { AccumulatingTreeDeltaEmitter, TreeDelta } from '@theia/test/lib/common/tree-delta';
import {
TestItemDTO, TestOutputDTO, TestExecutionState, TestRunProfileDTO,
TestRunProfileKind, TestRunRequestDTO, TestStateChangeDTO, TestItemReference
TestRunProfileKind, TestRunRequestDTO, TestStateChangeDTO, TestItemReference, TestMessageArg, TestMessageDTO
} from '../common/test-types';
import { ChangeBatcher, observableProperty } from '@theia/test/lib/common/collections';
import { TestRunRequest } from './types-impl';
import { MarkdownString } from '../common/plugin-api-rpc-model';

type RefreshHandler = (token: theia.CancellationToken) => void | theia.Thenable<void>;
type ResolveHandler = (item: theia.TestItem | undefined) => theia.Thenable<void> | void;
Expand Down Expand Up @@ -335,6 +336,8 @@ export class TestingExtImpl implements TestingExt {
return this.toTestItem(arg);
} else if (Array.isArray(arg)) {
return arg.map(param => TestItemReference.is(param) ? this.toTestItem(param) : param);
} else if (TestMessageArg.is(arg)) {
return this.fromTestMessageArg(arg);
} else {
return arg;
}
Expand All @@ -343,6 +346,27 @@ export class TestingExtImpl implements TestingExt {

}

fromTestMessageArg(arg: TestMessageArg): { test?: theia.TestItem, message: theia.TestMessage } {
const testItem = arg.testItemReference ? this.toTestItem(arg.testItemReference) : undefined;
const message = this.toTestMessage(arg.testMessage);
return {
test: testItem,
message: message
};
}

toTestMessage(testMessage: TestMessageDTO): theia.TestMessage {
const message = MarkdownString.is(testMessage.message) ? Convert.toMarkdown(testMessage.message) : testMessage.message;

return {
message: message,
actualOutput: testMessage.actual,
expectedOutput: testMessage.expected,
contextValue: testMessage.contextValue,
location: testMessage.location ? Convert.toLocation(testMessage.location) : undefined
};
}

toTestItem(ref: TestItemReference): theia.TestItem {
const result = this.withController(ref.controllerId).items.find(ref.testPath);
if (!result) {
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-ext/src/plugin/type-converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1644,7 +1644,8 @@ export namespace TestMessage {
location: fromLocation(message.location),
message: fromMarkdown(message.message)!,
expected: message.expectedOutput,
actual: message.actualOutput
actual: message.actualOutput,
contextValue: message.contextValue
}];
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3293,6 +3293,7 @@ export class TestMessage implements theia.TestMessage {
public expectedOutput?: string;
public actualOutput?: string;
public location?: theia.Location;
public contextValue?: string;

public static diff(message: string | theia.MarkdownString, expected: string, actual: string): theia.TestMessage {
const msg = new TestMessage(message);
Expand Down
31 changes: 31 additions & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16448,6 +16448,37 @@ export module '@theia/plugin' {
*/
location?: Location;

/**
* 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;

/**
* Creates a new TestMessage that will present as a diff in the editor.
* @param message Message to display to the user.
Expand Down
8 changes: 8 additions & 0 deletions packages/test/src/browser/test-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export interface TestMessage {
readonly actual?: string;
readonly location: Location;
readonly message: string | MarkdownString;
readonly contextValue?: string;
}

export namespace TestMessage {
export function is(obj: unknown): obj is TestMessage {
return isObject<TestMessage>(obj) && (MarkdownString.is(obj.message) || typeof obj.message === 'string');
}
}

export interface TestState {
Expand Down Expand Up @@ -136,6 +143,7 @@ export interface TestItem {
readonly controller: TestController | undefined;
readonly canResolveChildren: boolean;
resolveChildren(): void;
readonly path: string[];
}

export namespace TestItem {
Expand Down
36 changes: 36 additions & 0 deletions packages/test/src/browser/view/test-context-key-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';

@injectable()
export class TestContextKeyService {

@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

protected _contextValue: ContextKey<string | undefined>;
get contextValue(): ContextKey<string | undefined> {
return this._contextValue;
}

@postConstruct()
protected init(): void {
this._contextValue = this.contextKeyService.createKey<string | undefined>('testMessage', undefined);
}

}
10 changes: 9 additions & 1 deletion packages/test/src/browser/view/test-output-ui-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
// *****************************************************************************

import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { TestController, TestOutputItem, TestRun, TestService, TestState, TestStateChangedEvent } from '../test-service';
import { TestController, TestFailure, TestOutputItem, TestRun, TestService, TestState, TestStateChangedEvent } from '../test-service';
import { Disposable, Emitter, Event } from '@theia/core';
import { TestContextKeyService } from './test-context-key-service';

export interface ActiveRunEvent {
controller: TestController;
Expand All @@ -41,6 +42,7 @@ interface ActiveTestRunInfo {

@injectable()
export class TestOutputUIModel {
@inject(TestContextKeyService) protected readonly testContextKeys: TestContextKeyService;
@inject(TestService) protected testService: TestService;

protected readonly activeRuns = new Map<string, ActiveTestRunInfo>();
Expand Down Expand Up @@ -139,6 +141,12 @@ export class TestOutputUIModel {
set selectedTestState(element: TestState | undefined) {
if (element !== this._selectedTestState) {
this._selectedTestState = element;
if (this._selectedTestState && TestFailure.is(this._selectedTestState.state)) {
const message = this._selectedTestState.state.messages[0];
this.testContextKeys.contextValue.set(message.contextValue);
} else {
this.testContextKeys.contextValue.reset();
}
this.onDidChangeSelectedTestStateEmitter.fire(element);
}
}
Expand Down
11 changes: 8 additions & 3 deletions packages/test/src/browser/view/test-run-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ContextMenuRenderer, codicon } from '@theia/core/lib/browser';
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { TestController, TestExecutionState, TestItem, TestOutputItem, TestRun, TestService } from '../test-service';
import { TestController, TestExecutionState, TestFailure, TestItem, TestMessage, TestOutputItem, TestRun, TestService } from '../test-service';
import * as React from '@theia/core/shared/react';
import { Disposable, DisposableCollection, Event, nls } from '@theia/core';
import { TestExecutionStateManager } from './test-execution-state-manager';
Expand Down Expand Up @@ -251,11 +251,16 @@ export class TestRunTreeWidget extends TreeWidget {
}
}

protected override toContextMenuArgs(node: SelectableTreeNode): (TestRun | TestItem)[] {
protected override toContextMenuArgs(node: SelectableTreeNode): (TestRun | TestItem | TestMessage[])[] {
if (node instanceof TestRunNode) {
return [node.run];
} else if (node instanceof TestItemNode) {
return [node.item];
const item = node.item;
const executionState = node.parent.run.getTestState(node.item);
if (TestFailure.is(executionState)) {
return [item, executionState.messages];
}
return [item];
}
return [];
}
Expand Down
2 changes: 2 additions & 0 deletions packages/test/src/browser/view/test-view-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ import { TestOutputUIModel } from './test-output-ui-model';
import { TestRunTree, TestRunTreeWidget } from './test-run-widget';
import { TestResultViewContribution } from './test-result-view-contribution';
import { TEST_RUNS_CONTEXT_MENU, TestRunViewContribution } from './test-run-view-contribution';
import { TestContextKeyService } from './test-context-key-service';

export default new ContainerModule(bind => {

bindContributionProvider(bind, TestContribution);
bind(TestContextKeyService).toSelf().inSingletonScope();
bind(TestService).to(DefaultTestService).inSingletonScope();

bind(WidgetFactory).toDynamicValue(({ container }) => ({
Expand Down

0 comments on commit 7dcce3e

Please sign in to comment.