mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
Contribute testing and debug actions to editor/lineNumber/context menu (#176092)
* Contribute testing and debug actions to `editor/lineNumber/context` menu * Address PR feedback
This commit is contained in:
@@ -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';
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
|
||||
@@ -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<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>();
|
||||
|
||||
@@ -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<IAction[]>;
|
||||
abstract getContextMenuActions(): IReference<IAction[]>;
|
||||
|
||||
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<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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: () => { } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T extends { id: string; line: number } = ITestDecoration> {
|
||||
|
||||
Reference in New Issue
Block a user