Skip to content

Commit

Permalink
Contribute testing and debug actions to editor/lineNumber/context m…
Browse files Browse the repository at this point in the history
…enu (#176092)

* Contribute testing and debug actions to `editor/lineNumber/context` menu

* Address PR feedback
  • Loading branch information
joyceerhl authored Mar 4, 2023
1 parent 5b47122 commit e0b3039
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 71 deletions.
71 changes: 52 additions & 19 deletions src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IGutterActionsGenerator> = 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';
Expand All @@ -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();

Expand All @@ -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<IBreakpointEditorContribution>(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);
}

const menuActions = menu.getActions({ arg: { lineNumber, uri: model.uri }, shouldForwardArgs: true });
actions.push(...menuActions.map(a => a[1]));

this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => actions,
menuActionOptions: { shouldForwardArgs: true },
getActionsContext: () => ({ lineNumber, uri: model.uri }),
onHide: () => menu.dispose(),
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => Separator.join(...actions),
menuActionOptions: { shouldForwardArgs: true },
getActionsContext: () => ({ lineNumber, uri: model.uri }),
onHide: () => menu.dispose(),
});
});
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.$;

Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -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<IBreakpointEditorContribution>(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
Expand Down
81 changes: 44 additions & 37 deletions src/vs/workbench/contrib/testing/browser/testingDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -371,7 +395,9 @@ export class TestingDecorations extends Disposable implements IEditorContributio
return editor.getContribution<TestingDecorations>(Testing.DecorationsContributionId);
}

private currentUri?: URI;
public get currentUri() { return this._currentUri; }

private _currentUri?: URI;
private readonly expectedWidget = new MutableDisposable<ExpectedLensContentWidget>();
private readonly actualWidget = new MutableDisposable<ActualLensContentWidget>();

Expand All @@ -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)));
Expand All @@ -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;
}
Expand Down Expand Up @@ -464,7 +490,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio
uri = undefined;
}

this.currentUri = uri;
this._currentUri = uri;

if (!uri) {
return;
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -757,7 +777,7 @@ abstract class RunTestDecoration {
/**
* Called when the decoration is clicked on.
*/
protected abstract getContextMenuActions(): IReference<IAction[]>;
abstract getContextMenuActions(): IReference<IAction[]>;

protected defaultRun() {
return this.testService.runTests({
Expand All @@ -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<IBreakpointEditorContribution>(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,
});
}

Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -1027,4 +1030,8 @@ class TestMessageDecoration implements ITestDecoration {

return false;
}

getContextMenuActions() {
return { object: [], dispose: () => { } };
}
}
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/testing/common/testingDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -58,6 +59,8 @@ export interface ITestDecoration {
* Editor decoration instance.
*/
readonly editorDecoration: IModelDeltaDecoration;

getContextMenuActions(): { object: IAction[]; dispose(): void };
}

export class TestDecorations<T extends { id: string; line: number } = ITestDecoration> {
Expand Down

0 comments on commit e0b3039

Please sign in to comment.