diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts index b191ed9edc2..92b281c4348 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts @@ -4,14 +4,45 @@ *--------------------------------------------------------------------------------------------*/ import { IAction, Separator } from 'vs/base/common/actions'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution, EditorContributionInstantiation } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export interface IGutterActionsGenerator { + (context: { lineNumber: number; editor: ICodeEditor; accessor: ServicesAccessor }, result: { push(action: IAction): void }): void; +} + +export class GutterActionsRegistryImpl { + private _registeredGutterActionsGenerators: Set = new Set(); + + /** + * + * This exists solely to allow the debug and test contributions to add actions to the gutter context menu + * which cannot be trivially expressed using when clauses and therefore cannot be statically registered. + * If you want an action to show up in the gutter context menu, you should generally use MenuId.EditorLineNumberMenu instead. + */ + public registerGutterActionsGenerator(gutterActionsGenerator: IGutterActionsGenerator): IDisposable { + this._registeredGutterActionsGenerators.add(gutterActionsGenerator); + return { + dispose: () => { + this._registeredGutterActionsGenerators.delete(gutterActionsGenerator); + } + }; + } + + public getGutterActionsGenerators(): IGutterActionsGenerator[] { + return Array.from(this._registeredGutterActionsGenerators.values()); + } +} + +Registry.add('gutterActionsRegistry', new GutterActionsRegistryImpl()); +export const GutterActionsRegistry: GutterActionsRegistryImpl = Registry.as('gutterActionsRegistry'); export class EditorLineNumberContextMenu extends Disposable implements IEditorContribution { static readonly ID = 'workbench.contrib.editorLineNumberContextMenu'; @@ -21,6 +52,7 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -33,31 +65,32 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo const menu = this.menuService.createMenu(MenuId.EditorLineNumberContext, this.contextKeyService); const model = this.editor.getModel(); - if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_LINE_NUMBERS) { + if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_LINE_NUMBERS && e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { return; } const anchor = { x: e.event.posx, y: e.event.posy }; const lineNumber = e.target.position.lineNumber; - let actions: IAction[] = []; + const actions: IAction[][] = []; - // TODO@joyceerhl refactor breakpoint and testing actions to statically contribute to this menu - const contribution = this.editor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID); - if (contribution) { - actions.push(...contribution.getContextMenuActionsAtPosition(lineNumber, model)); - } - const menuActions = menu.getActions({ arg: { lineNumber, uri: model.uri }, shouldForwardArgs: true }); - if (menuActions.length > 0) { - actions = Separator.join(...[actions], ...menuActions.map(a => a[1])); - } + this.instantiationService.invokeFunction(accessor => { + for (const generator of GutterActionsRegistry.getGutterActionsGenerators()) { + const collectedActions: IAction[] = []; + generator({ lineNumber, editor: this.editor, accessor }, { push: (action: IAction) => collectedActions.push(action) }); + actions.push(collectedActions); + } - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => actions, - menuActionOptions: { shouldForwardArgs: true }, - getActionsContext: () => ({ lineNumber, uri: model.uri }), - onHide: () => menu.dispose(), + const menuActions = menu.getActions({ arg: { lineNumber, uri: model.uri }, shouldForwardArgs: true }); + actions.push(...menuActions.map(a => a[1])); + + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => Separator.join(...actions), + menuActionOptions: { shouldForwardArgs: true }, + getActionsContext: () => ({ lineNumber, uri: model.uri }), + onHide: () => menu.dispose(), + }); }); })); } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 985a35539d5..0154a14c8f0 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -35,10 +35,11 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ILabelService } from 'vs/platform/label/common/label'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { GutterActionsRegistry } from 'vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu'; import { getBreakpointMessageAndIcon } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { BreakpointWidget } from 'vs/workbench/contrib/debug/browser/breakpointWidget'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { BreakpointWidgetContext, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DebuggerString, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, IDebugService, IDebugSession, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DebuggerString, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, IDebugService, IDebugSession, State } from 'vs/workbench/contrib/debug/common/debug'; const $ = dom.$; @@ -250,20 +251,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const uri = model.uri; if (e.event.rightButton || (env.isMacintosh && e.event.leftButton && e.event.ctrlKey)) { - if (!canSetBreakpoints) { - return; - } - - const anchor = { x: e.event.posx, y: e.event.posy }; - const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri }); - const actions = this.getContextMenuActions(breakpoints, uri, lineNumber); - - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => actions, - getActionsContext: () => breakpoints.length ? breakpoints[0] : undefined, - onHide: () => disposeIfDisposable(actions) - }); + return; } else { const breakpoints = this.debugService.getModel().getBreakpoints({ uri, lineNumber }); @@ -633,6 +621,25 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi } } +GutterActionsRegistry.registerGutterActionsGenerator(({ lineNumber, editor, accessor }, result) => { + const model = editor.getModel(); + const debugService = accessor.get(IDebugService); + if (!model || !debugService.getAdapterManager().hasEnabledDebuggers() || !debugService.canSetBreakpointsIn(model)) { + return; + } + + const breakpointEditorContribution = editor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID); + if (!breakpointEditorContribution) { + return; + } + + const actions = breakpointEditorContribution.getContextMenuActionsAtPosition(lineNumber, model); + + for (const action of actions) { + result.push(action); + } +}); + class InlineBreakpointWidget implements IContentWidget, IDisposable { // editor.IContentWidget.allowEditorOverflow diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 830d57c7bc3..bbbd84f34ac 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -36,7 +36,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { ThemeIcon } from 'vs/base/common/themables'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons'; import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; @@ -51,6 +50,7 @@ import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { getContextForTestItem, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestDiffOpType, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; +import { GutterActionsRegistry } from 'vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu'; const MAX_INLINE_MESSAGE_LENGTH = 128; @@ -205,6 +205,30 @@ export class TestingDecorationService extends Disposable implements ITestingDeco debounceInvalidate.schedule(); } })); + + this._register(GutterActionsRegistry.registerGutterActionsGenerator((context, result) => { + const model = context.editor.getModel(); + const testingDecorations = TestingDecorations.get(context.editor); + if (!model || !testingDecorations?.currentUri) { + return; + } + + const currentDecorations = this.syncDecorations(testingDecorations.currentUri); + if (!currentDecorations.size) { + return; + } + + const modelDecorations = model.getLinesDecorations(context.lineNumber, context.lineNumber); + for (const { id } of modelDecorations) { + const decoration = currentDecorations.getById(id); + if (decoration) { + const { object: actions } = decoration.getContextMenuActions(); + for (const action of actions) { + result.push(action); + } + } + } + })); } /** @inheritdoc */ @@ -371,7 +395,9 @@ export class TestingDecorations extends Disposable implements IEditorContributio return editor.getContribution(Testing.DecorationsContributionId); } - private currentUri?: URI; + public get currentUri() { return this._currentUri; } + + private _currentUri?: URI; private readonly expectedWidget = new MutableDisposable(); private readonly actualWidget = new MutableDisposable(); @@ -388,8 +414,8 @@ export class TestingDecorations extends Disposable implements IEditorContributio this.attachModel(editor.getModel()?.uri); this._register(decorations.onDidChange(() => { - if (this.currentUri) { - decorations.syncDecorations(this.currentUri); + if (this._currentUri) { + decorations.syncDecorations(this._currentUri); } })); this._register(this.editor.onDidChangeModel(e => this.attachModel(e.newModelUrl || undefined))); @@ -411,11 +437,11 @@ export class TestingDecorations extends Disposable implements IEditorContributio })); this._register(Event.accumulate(this.editor.onDidChangeModelContent, 0, this._store)(evts => { const model = editor.getModel(); - if (!this.currentUri || !model) { + if (!this._currentUri || !model) { return; } - const currentDecorations = decorations.syncDecorations(this.currentUri); + const currentDecorations = decorations.syncDecorations(this._currentUri); if (!currentDecorations.size) { return; } @@ -464,7 +490,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio uri = undefined; } - this.currentUri = uri; + this._currentUri = uri; if (!uri) { return; @@ -477,7 +503,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio // consume the iterator so that all tests in the file get expanded. Or // at least until the URI changes. If new items are requested, changes // will be trigged in the `onDidProcessDiff` callback. - if (this.currentUri !== uri) { + if (this._currentUri !== uri) { break; } } @@ -685,7 +711,6 @@ abstract class RunTestDecoration { }[], private visible: boolean, protected readonly model: ITextModel, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @ITestService protected readonly testService: ITestService, @IContextMenuService protected readonly contextMenuService: IContextMenuService, @ICommandService protected readonly commandService: ICommandService, @@ -701,15 +726,10 @@ abstract class RunTestDecoration { /** @inheritdoc */ public click(e: IEditorMouseEvent): boolean { - if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) { + if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || e.event.rightButton) { return false; } - if (e.event.rightButton) { - this.showContextMenu(e); - return true; - } - switch (getTestingConfiguration(this.configurationService, TestingConfigKeys.DefaultGutterClickAction)) { case DefaultGutterClickAction.ContextMenu: this.showContextMenu(e); @@ -757,7 +777,7 @@ abstract class RunTestDecoration { /** * Called when the decoration is clicked on. */ - protected abstract getContextMenuActions(): IReference; + abstract getContextMenuActions(): IReference; protected defaultRun() { return this.testService.runTests({ @@ -774,25 +794,9 @@ abstract class RunTestDecoration { } private showContextMenu(e: IEditorMouseEvent) { - let actions = this.getContextMenuActions(); - const editor = this.codeEditorService.listCodeEditors().find(e => e.getModel() === this.model); - if (editor) { - const contribution = editor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID); - if (contribution) { - actions = { - dispose: actions.dispose, - object: Separator.join( - actions.object, - contribution.getContextMenuActionsAtPosition(this.line, this.model) - ) - }; - } - } - this.contextMenuService.showContextMenu({ + menuId: MenuId.EditorLineNumberContext, getAnchor: () => ({ x: e.event.posx, y: e.event.posy }), - getActions: () => actions.object, - onHide: () => actions.dispose, }); } @@ -874,7 +878,7 @@ abstract class RunTestDecoration { } class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoration { - protected override getContextMenuActions() { + override getContextMenuActions() { const allActions: IAction[] = []; if (this.tests.some(({ test }) => this.testProfileService.capabilitiesForTest(test) & TestRunProfileBitset.Run)) { allActions.push(new Action('testing.gutter.runAll', localize('run all test', 'Run All Tests'), undefined, undefined, () => this.defaultRun())); @@ -929,7 +933,6 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati resultItem: TestResultItem | undefined, model: ITextModel, visible: boolean, - @ICodeEditorService codeEditorService: ICodeEditorService, @ITestService testService: ITestService, @ICommandService commandService: ICommandService, @IContextMenuService contextMenuService: IContextMenuService, @@ -938,10 +941,10 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, ) { - super([{ test, resultItem }], visible, model, codeEditorService, testService, contextMenuService, commandService, configurationService, testProfiles, contextKeyService, menuService); + super([{ test, resultItem }], visible, model, testService, contextMenuService, commandService, configurationService, testProfiles, contextKeyService, menuService); } - protected override getContextMenuActions() { + override getContextMenuActions() { return this.getTestContextMenuActions(this.tests[0].test, this.tests[0].resultItem); } } @@ -1027,4 +1030,8 @@ class TestMessageDecoration implements ITestDecoration { return false; } + + getContextMenuActions() { + return { object: [], dispose: () => { } }; + } } diff --git a/src/vs/workbench/contrib/testing/common/testingDecorations.ts b/src/vs/workbench/contrib/testing/common/testingDecorations.ts index f35708b3a1b..7f2ff34cbed 100644 --- a/src/vs/workbench/contrib/testing/common/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/common/testingDecorations.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IAction } from 'vs/base/common/actions'; import { binarySearch } from 'vs/base/common/arrays'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; @@ -58,6 +59,8 @@ export interface ITestDecoration { * Editor decoration instance. */ readonly editorDecoration: IModelDeltaDecoration; + + getContextMenuActions(): { object: IAction[]; dispose(): void }; } export class TestDecorations {