diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1e2d2df6c2f..b5fd59b9962 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -121,6 +121,7 @@ export class MenuId { static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu'); + static readonly TestItem = new MenuId('TestItem'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TunnelContext = new MenuId('TunnelContext'); diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 7122754cfc6..9f032ea1bde 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -166,6 +166,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('notebook.cell.title', "The contributed notebook cell title menu"), proposed: true }, + { + key: 'testing/item/context', + id: MenuId.TestItem, + description: localize('testing.item.title', "The contributed test item menu"), + proposed: true + }, { key: 'extension/context', id: MenuId.ExtensionContext, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 23f50add8f6..08eb41d1ddb 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -10,9 +10,8 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ITreeContextMenuEvent, ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction } from 'vs/base/common/actions'; import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async'; import { Color, RGBA } from 'vs/base/common/color'; @@ -22,15 +21,15 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { splitGlobAware } from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/testing'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { localize } from 'vs/nls'; -import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -256,8 +255,8 @@ export class TestingExplorerViewModel extends Disposable { listContainer: HTMLElement, onDidChangeVisibility: Event, private listener: TestSubscriptionListener | undefined, - @ICommandService commandService: ICommandService, - @IThemeService themeService: IThemeService, + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, @ITestService private readonly testService: ITestService, @ITestExplorerFilterState filterState: TestExplorerFilterState, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -304,6 +303,8 @@ export class TestingExplorerViewModel extends Disposable { this.tree.refilter(); })); + this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); + this._register(editorService.onDidActiveEditorChange(() => { if (filterState.currentDocumentOnly.value && editorService.activeEditor?.resource) { if (this.projection.hasTestInDocument(editorService.activeEditor.resource)) { @@ -450,6 +451,20 @@ export class TestingExplorerViewModel extends Disposable { : false; } + private onContextMenu(evt: ITreeContextMenuEvent) { + if (!evt.element) { + return; + } + + const actions = getTestItemActions(this.instantiationService, this.contextKeyService, this.menuService, evt.element); + this.contextMenuService.showContextMenu({ + getAnchor: () => evt.anchor, + getActions: () => actions.value.secondary, + getActionsContext: () => evt.element?.test?.item.extId, + onHide: () => actions.dispose(), + }); + } + private handleExecuteKeypress(evt: IKeyboardEvent) { const focused = this.tree.getFocus(); const selected = this.tree.getSelection(); @@ -747,25 +762,32 @@ interface TestTemplateData { label: IResourceLabel; icon: HTMLElement; actionBar: ActionBar; + elementDisposable: IDisposable[]; + templateDisposable: IDisposable[]; } -class TestsRenderer implements ITreeRenderer { +class TestsRenderer extends Disposable implements ITreeRenderer { public static readonly ID = 'testExplorer'; constructor( private labels: ResourceLabels, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService - ) { } - - renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: TestTemplateData): void { - const element = node.element.elements[node.element.elements.length - 1]; - this.renderElementDirect(element, templateData); + ) { + super(); } + /** + * @inheritdoc + */ get templateId(): string { return TestsRenderer.ID; } + /** + * @inheritdoc + */ public renderTemplate(container: HTMLElement): TestTemplateData { const wrapper = dom.append(container, dom.$('.test-item')); @@ -780,14 +802,13 @@ class TestsRenderer implements ITreeRenderer, index: number, data: TestTemplateData): void { - this.renderElementDirect(node.element, data); - } - - private renderElementDirect(element: ITestTreeElement, data: TestTemplateData) { + /** + * @inheritdoc + */ + public renderElement({ element }: ITreeNode, _: number, data: TestTemplateData): void { const label: IResourceLabelProps = { name: element.label }; const options: IResourceLabelOptions = {}; data.actionBar.clear(); @@ -816,30 +837,65 @@ class TestsRenderer implements ITreeRenderer, _: number, templateData: TestTemplateData): void { + dispose(templateData.elementDisposable); + templateData.elementDisposable = []; + } + + private fillActionBar(element: ITestTreeElement, data: TestTemplateData) { + const actions = getTestItemActions(this.instantiationService, this.contextKeyService, this.menuService, element); + data.elementDisposable.push(actions); + data.actionBar.clear(); + data.actionBar.push(actions.value.primary, { icon: true, label: false }); } } +const getTestItemActions = (instantionService: IInstantiationService, contextKeyService: IContextKeyService, menuService: IMenuService, element: ITestTreeElement) => { + const contextOverlay = contextKeyService.createOverlay([ + ['view', Testing.ExplorerViewId], + [TestingContextKeys.testItemExtId.key, element.test?.item.extId] + ]); + const menu = menuService.createMenu(MenuId.TestItem, contextOverlay); + + try { + const primary: IAction[] = []; + const running = element.state === TestRunState.Running; + if (!Iterable.isEmpty(element.runnable)) { + primary.push(instantionService.createInstance(RunAction, element.runnable, running)); + } + + if (!Iterable.isEmpty(element.debuggable)) { + primary.push(instantionService.createInstance(DebugAction, element.debuggable, running)); + } + + const secondary: IAction[] = []; + const result = { primary, secondary }; + const actionsDisposable = createAndFillInActionBarActions(menu, { + arg: element.test?.item.extId, + shouldForwardArgs: true, + }, result, g => /^inline/.test(g)); + + return { value: result, dispose: () => actionsDisposable.dispose }; + } finally { + menu.dispose(); + } +}; + type CountSummary = ReturnType; const collectCounts = (count: TestStateCount) => { diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 6f951ef63ac..c2a97327607 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants'; @@ -18,4 +19,8 @@ export namespace TestingContextKeys { export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false); export const explorerLocation = new RawContextKey('testing.explorerLocation', ViewContainerLocation.Sidebar); export const autoRun = new RawContextKey('testing.autoRun', false); + export const testItemExtId = new RawContextKey('testId', undefined, { + type: 'string', + description: localize('testing.testId', 'ID of the current test item, set when creating or opening menus on test items') + }); }