Path: blob/main/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { IMouseEvent, IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js';8import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';9import { IAnchor } from '../../../../base/browser/ui/contextview/contextview.js';10import { IAction, Separator, SubmenuAction } from '../../../../base/common/actions.js';11import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';12import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';13import { DisposableStore } from '../../../../base/common/lifecycle.js';14import { isIOS } from '../../../../base/common/platform.js';15import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../browser/editorBrowser.js';16import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';17import { EditorOption } from '../../../common/config/editorOptions.js';18import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';19import { EditorContextKeys } from '../../../common/editorContextKeys.js';20import { ITextModel } from '../../../common/model.js';21import * as nls from '../../../../nls.js';22import { IMenuService, MenuId, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';23import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';24import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';25import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';26import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';27import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';28import { IWorkspaceContextService, isStandaloneEditorWorkspace } from '../../../../platform/workspace/common/workspace.js';2930export class ContextMenuController implements IEditorContribution {3132public static readonly ID = 'editor.contrib.contextmenu';3334public static get(editor: ICodeEditor): ContextMenuController | null {35return editor.getContribution<ContextMenuController>(ContextMenuController.ID);36}3738private readonly _toDispose = new DisposableStore();39private _contextMenuIsBeingShownCount: number = 0;40private readonly _editor: ICodeEditor;4142constructor(43editor: ICodeEditor,44@IContextMenuService private readonly _contextMenuService: IContextMenuService,45@IContextViewService private readonly _contextViewService: IContextViewService,46@IContextKeyService private readonly _contextKeyService: IContextKeyService,47@IKeybindingService private readonly _keybindingService: IKeybindingService,48@IMenuService private readonly _menuService: IMenuService,49@IConfigurationService private readonly _configurationService: IConfigurationService,50@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,51) {52this._editor = editor;5354this._toDispose.add(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e)));55this._toDispose.add(this._editor.onMouseWheel((e: IMouseWheelEvent) => {56if (this._contextMenuIsBeingShownCount > 0) {57const view = this._contextViewService.getContextViewElement();58const target = e.srcElement as HTMLElement;5960// Event triggers on shadow root host first61// Check if the context view is under this host before hiding it #10316962if (!(target.shadowRoot && dom.getShadowRoot(view) === target.shadowRoot)) {63this._contextViewService.hideContextView();64}65}66}));67this._toDispose.add(this._editor.onKeyDown((e: IKeyboardEvent) => {68if (!this._editor.getOption(EditorOption.contextmenu)) {69return; // Context menu is turned off through configuration70}71if (e.keyCode === KeyCode.ContextMenu) {72// Chrome is funny like that73e.preventDefault();74e.stopPropagation();75this.showContextMenu();76}77}));78}7980private _onContextMenu(e: IEditorMouseEvent): void {81if (!this._editor.hasModel()) {82return;83}8485if (!this._editor.getOption(EditorOption.contextmenu)) {86this._editor.focus();87// Ensure the cursor is at the position of the mouse click88if (e.target.position && !this._editor.getSelection().containsPosition(e.target.position)) {89this._editor.setPosition(e.target.position);90}91return; // Context menu is turned off through configuration92}9394if (e.target.type === MouseTargetType.OVERLAY_WIDGET) {95return; // allow native menu on widgets to support right click on input field for example in find96}97if (e.target.type === MouseTargetType.CONTENT_TEXT && e.target.detail.injectedText) {98return; // allow native menu on injected text99}100101e.event.preventDefault();102e.event.stopPropagation();103104if (e.target.type === MouseTargetType.SCROLLBAR) {105return this._showScrollbarContextMenu(e.event);106}107108if (e.target.type !== MouseTargetType.CONTENT_TEXT && e.target.type !== MouseTargetType.CONTENT_EMPTY && e.target.type !== MouseTargetType.TEXTAREA) {109return; // only support mouse click into text or native context menu key for now110}111112// Ensure the editor gets focus if it hasn't, so the right events are being sent to other contributions113this._editor.focus();114115// Ensure the cursor is at the position of the mouse click116if (e.target.position) {117let hasSelectionAtPosition = false;118for (const selection of this._editor.getSelections()) {119if (selection.containsPosition(e.target.position)) {120hasSelectionAtPosition = true;121break;122}123}124125if (!hasSelectionAtPosition) {126this._editor.setPosition(e.target.position);127}128}129130// Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position131let anchor: IMouseEvent | null = null;132if (e.target.type !== MouseTargetType.TEXTAREA) {133anchor = e.event;134}135136// Show the context menu137this.showContextMenu(anchor);138}139140public showContextMenu(anchor?: IMouseEvent | null): void {141if (!this._editor.getOption(EditorOption.contextmenu)) {142return; // Context menu is turned off through configuration143}144if (!this._editor.hasModel()) {145return;146}147148// Find actions available for menu149const menuActions = this._getMenuActions(this._editor.getModel(),150this._editor.contextMenuId);151152// Show menu if we have actions to show153if (menuActions.length > 0) {154this._doShowContextMenu(menuActions, anchor);155}156}157158private _getMenuActions(model: ITextModel, menuId: MenuId): IAction[] {159const result: IAction[] = [];160161// get menu groups162const groups = this._menuService.getMenuActions(menuId, this._contextKeyService, { arg: model.uri });163164// translate them into other actions165for (const group of groups) {166const [, actions] = group;167let addedItems = 0;168for (const action of actions) {169if (action instanceof SubmenuItemAction) {170const subActions = this._getMenuActions(model, action.item.submenu);171if (subActions.length > 0) {172result.push(new SubmenuAction(action.id, action.label, subActions));173addedItems++;174}175} else {176result.push(action);177addedItems++;178}179}180181if (addedItems) {182result.push(new Separator());183}184}185186if (result.length) {187result.pop(); // remove last separator188}189190return result;191}192193private _doShowContextMenu(actions: IAction[], event: IMouseEvent | null = null): void {194if (!this._editor.hasModel()) {195return;196}197198let anchor: IMouseEvent | IAnchor | null = event;199if (!anchor) {200// Ensure selection is visible201this._editor.revealPosition(this._editor.getPosition(), ScrollType.Immediate);202203this._editor.render();204const cursorCoords = this._editor.getScrolledVisiblePosition(this._editor.getPosition());205206// Translate to absolute editor position207const editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode());208const posx = editorCoords.left + cursorCoords.left;209const posy = editorCoords.top + cursorCoords.top + cursorCoords.height;210211anchor = { x: posx, y: posy };212}213214const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035215216// Show menu217this._contextMenuIsBeingShownCount++;218this._contextMenuService.showContextMenu({219domForShadowRoot: useShadowDOM ? this._editor.getOverflowWidgetsDomNode() ?? this._editor.getDomNode() : undefined,220221getAnchor: () => anchor,222223getActions: () => actions,224225getActionViewItem: (action) => {226const keybinding = this._keybindingFor(action);227if (keybinding) {228return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel(), isMenu: true });229}230231const customActionViewItem = <any>action;232if (typeof customActionViewItem.getActionViewItem === 'function') {233return customActionViewItem.getActionViewItem();234}235236return new ActionViewItem(action, action, { icon: true, label: true, isMenu: true });237},238239getKeyBinding: (action): ResolvedKeybinding | undefined => {240return this._keybindingFor(action);241},242243onHide: (wasCancelled: boolean) => {244this._contextMenuIsBeingShownCount--;245}246});247}248249private _showScrollbarContextMenu(anchor: IMouseEvent): void {250if (!this._editor.hasModel()) {251return;252}253254if (isStandaloneEditorWorkspace(this._workspaceContextService.getWorkspace())) {255// can't update the configuration properly in the standalone editor256return;257}258259const minimapOptions = this._editor.getOption(EditorOption.minimap);260261let lastId = 0;262const createAction = (opts: { label: string; enabled?: boolean; checked?: boolean; run: () => void }): IAction => {263return {264id: `menu-action-${++lastId}`,265label: opts.label,266tooltip: '',267class: undefined,268enabled: (typeof opts.enabled === 'undefined' ? true : opts.enabled),269checked: opts.checked,270run: opts.run271};272};273const createSubmenuAction = (label: string, actions: IAction[]): SubmenuAction => {274return new SubmenuAction(275`menu-action-${++lastId}`,276label,277actions,278undefined279);280};281const createEnumAction = <T>(label: string, enabled: boolean, configName: string, configuredValue: T, options: { label: string; value: T }[]): IAction => {282if (!enabled) {283return createAction({ label, enabled, run: () => { } });284}285const createRunner = (value: T) => {286return () => {287this._configurationService.updateValue(configName, value);288};289};290const actions: IAction[] = [];291for (const option of options) {292actions.push(createAction({293label: option.label,294checked: configuredValue === option.value,295run: createRunner(option.value)296}));297}298return createSubmenuAction(299label,300actions301);302};303304const actions: IAction[] = [];305actions.push(createAction({306label: nls.localize('context.minimap.minimap', "Minimap"),307checked: minimapOptions.enabled,308run: () => {309this._configurationService.updateValue(`editor.minimap.enabled`, !minimapOptions.enabled);310}311}));312actions.push(new Separator());313actions.push(createAction({314label: nls.localize('context.minimap.renderCharacters', "Render Characters"),315enabled: minimapOptions.enabled,316checked: minimapOptions.renderCharacters,317run: () => {318this._configurationService.updateValue(`editor.minimap.renderCharacters`, !minimapOptions.renderCharacters);319}320}));321actions.push(createEnumAction<'proportional' | 'fill' | 'fit'>(322nls.localize('context.minimap.size', "Vertical size"),323minimapOptions.enabled,324'editor.minimap.size',325minimapOptions.size,326[{327label: nls.localize('context.minimap.size.proportional', "Proportional"),328value: 'proportional'329}, {330label: nls.localize('context.minimap.size.fill', "Fill"),331value: 'fill'332}, {333label: nls.localize('context.minimap.size.fit', "Fit"),334value: 'fit'335}]336));337actions.push(createEnumAction<'always' | 'mouseover'>(338nls.localize('context.minimap.slider', "Slider"),339minimapOptions.enabled,340'editor.minimap.showSlider',341minimapOptions.showSlider,342[{343label: nls.localize('context.minimap.slider.mouseover', "Mouse Over"),344value: 'mouseover'345}, {346label: nls.localize('context.minimap.slider.always', "Always"),347value: 'always'348}]349));350351const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035352this._contextMenuIsBeingShownCount++;353this._contextMenuService.showContextMenu({354domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined,355getAnchor: () => anchor,356getActions: () => actions,357onHide: (wasCancelled: boolean) => {358this._contextMenuIsBeingShownCount--;359this._editor.focus();360}361});362}363364private _keybindingFor(action: IAction): ResolvedKeybinding | undefined {365return this._keybindingService.lookupKeybinding(action.id);366}367368public dispose(): void {369if (this._contextMenuIsBeingShownCount > 0) {370this._contextViewService.hideContextView();371}372373this._toDispose.dispose();374}375}376377class ShowContextMenu extends EditorAction {378379constructor() {380super({381id: 'editor.action.showContextMenu',382label: nls.localize2('action.showContextMenu.label', "Show Editor Context Menu"),383precondition: undefined,384kbOpts: {385kbExpr: EditorContextKeys.textInputFocus,386primary: KeyMod.Shift | KeyCode.F10,387weight: KeybindingWeight.EditorContrib388}389});390}391392public run(accessor: ServicesAccessor, editor: ICodeEditor): void {393ContextMenuController.get(editor)?.showContextMenu();394}395}396397registerEditorContribution(ContextMenuController.ID, ContextMenuController, EditorContributionInstantiation.BeforeFirstInteraction);398registerEditorAction(ShowContextMenu);399400401