From 7dcce3e993af0e311a964df6aa4a941fb1ab45d0 Mon Sep 17 00:00:00 2001 From: Remi Schnekenburger Date: Wed, 20 Dec 2023 15:27:52 +0100 Subject: [PATCH] [vscode] Support TestMessage#contextValue (#13176) Also adds the menu mapping for testing/message/context vscode menu extension. contributed on behalf of STMicroelectronics Signed-off-by: Remi Schnekenburger --- CHANGELOG.md | 1 + packages/plugin-ext/src/common/test-types.ts | 20 +++++++++++ .../menus/plugin-menu-command-adapter.ts | 29 +++++++++++++++ .../menus/vscode-theia-menu-mappings.ts | 3 ++ packages/plugin-ext/src/plugin/tests.ts | 26 +++++++++++++- .../plugin-ext/src/plugin/type-converters.ts | 3 +- packages/plugin-ext/src/plugin/types-impl.ts | 1 + packages/plugin/src/theia.d.ts | 31 ++++++++++++++++ packages/test/src/browser/test-service.ts | 8 +++++ .../browser/view/test-context-key-service.ts | 36 +++++++++++++++++++ .../src/browser/view/test-output-ui-model.ts | 10 +++++- .../test/src/browser/view/test-run-widget.tsx | 11 ++++-- .../browser/view/test-view-frontend-module.ts | 2 ++ 13 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 packages/test/src/browser/view/test-context-key-service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 412473d665f88..2dbc881f452a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/plugin-ext/src/common/test-types.ts b/packages/plugin-ext/src/common/test-types.ts index c1d74d23ba8e5..f73d099266e5d 100644 --- a/packages/plugin-ext/src/common/test-types.ts +++ b/packages/plugin-ext/src/common/test-types.ts @@ -84,6 +84,7 @@ export interface TestMessageDTO { readonly actual?: string; readonly location?: Location; readonly message: string | MarkdownString; + readonly contextValue?: string; } export interface TestItemDTO { @@ -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(arg) + && isObject(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 + }; + } +} diff --git a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts index 8dab5d8a80068..0a3cccc5e4593 100644 --- a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts +++ b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts @@ -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[]; @@ -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); @@ -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], @@ -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, diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index c7149296b9b43..bf6ea3df16029 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -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']; @@ -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' @@ -83,6 +85,7 @@ export const codeToTheiaMappings = new Map([ ['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]], diff --git a/packages/plugin-ext/src/plugin/tests.ts b/packages/plugin-ext/src/plugin/tests.ts index e58bc432d91da..9f1083651123b 100644 --- a/packages/plugin-ext/src/plugin/tests.ts +++ b/packages/plugin-ext/src/plugin/tests.ts @@ -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; type ResolveHandler = (item: theia.TestItem | undefined) => theia.Thenable | void; @@ -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; } @@ -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) { diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 405d6873b7050..ba0d8140c3435 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -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 }]; } } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 5e3f05a21e665..69e74af05355f 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -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); diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 31847322fd15f..7451907b61cb5 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -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. diff --git a/packages/test/src/browser/test-service.ts b/packages/test/src/browser/test-service.ts index e7aba373d23b8..cc0bc6f0dfaa2 100644 --- a/packages/test/src/browser/test-service.ts +++ b/packages/test/src/browser/test-service.ts @@ -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(obj) && (MarkdownString.is(obj.message) || typeof obj.message === 'string'); + } } export interface TestState { @@ -136,6 +143,7 @@ export interface TestItem { readonly controller: TestController | undefined; readonly canResolveChildren: boolean; resolveChildren(): void; + readonly path: string[]; } export namespace TestItem { diff --git a/packages/test/src/browser/view/test-context-key-service.ts b/packages/test/src/browser/view/test-context-key-service.ts new file mode 100644 index 0000000000000..866b048f747ac --- /dev/null +++ b/packages/test/src/browser/view/test-context-key-service.ts @@ -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; + get contextValue(): ContextKey { + return this._contextValue; + } + + @postConstruct() + protected init(): void { + this._contextValue = this.contextKeyService.createKey('testMessage', undefined); + } + +} diff --git a/packages/test/src/browser/view/test-output-ui-model.ts b/packages/test/src/browser/view/test-output-ui-model.ts index 87d6f586bb053..f9520d71b337e 100644 --- a/packages/test/src/browser/view/test-output-ui-model.ts +++ b/packages/test/src/browser/view/test-output-ui-model.ts @@ -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; @@ -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(); @@ -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); } } diff --git a/packages/test/src/browser/view/test-run-widget.tsx b/packages/test/src/browser/view/test-run-widget.tsx index 296573ffb63e9..c45a6454be7e8 100644 --- a/packages/test/src/browser/view/test-run-widget.tsx +++ b/packages/test/src/browser/view/test-run-widget.tsx @@ -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'; @@ -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 []; } diff --git a/packages/test/src/browser/view/test-view-frontend-module.ts b/packages/test/src/browser/view/test-view-frontend-module.ts index 7040fa6384f01..5ba3d8b860731 100644 --- a/packages/test/src/browser/view/test-view-frontend-module.ts +++ b/packages/test/src/browser/view/test-view-frontend-module.ts @@ -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 }) => ({