Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { IMouseEvent, IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js';
9
import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
10
import { IAnchor } from '../../../../base/browser/ui/contextview/contextview.js';
11
import { IAction, Separator, SubmenuAction } from '../../../../base/common/actions.js';
12
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
13
import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';
14
import { DisposableStore } from '../../../../base/common/lifecycle.js';
15
import { isIOS } from '../../../../base/common/platform.js';
16
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../browser/editorBrowser.js';
17
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';
18
import { EditorOption } from '../../../common/config/editorOptions.js';
19
import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';
20
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
21
import { ITextModel } from '../../../common/model.js';
22
import * as nls from '../../../../nls.js';
23
import { IMenuService, MenuId, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';
24
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
25
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
26
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
27
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
28
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
29
import { IWorkspaceContextService, isStandaloneEditorWorkspace } from '../../../../platform/workspace/common/workspace.js';
30
31
export class ContextMenuController implements IEditorContribution {
32
33
public static readonly ID = 'editor.contrib.contextmenu';
34
35
public static get(editor: ICodeEditor): ContextMenuController | null {
36
return editor.getContribution<ContextMenuController>(ContextMenuController.ID);
37
}
38
39
private readonly _toDispose = new DisposableStore();
40
private _contextMenuIsBeingShownCount: number = 0;
41
private readonly _editor: ICodeEditor;
42
43
constructor(
44
editor: ICodeEditor,
45
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
46
@IContextViewService private readonly _contextViewService: IContextViewService,
47
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
48
@IKeybindingService private readonly _keybindingService: IKeybindingService,
49
@IMenuService private readonly _menuService: IMenuService,
50
@IConfigurationService private readonly _configurationService: IConfigurationService,
51
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
52
) {
53
this._editor = editor;
54
55
this._toDispose.add(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e)));
56
this._toDispose.add(this._editor.onMouseWheel((e: IMouseWheelEvent) => {
57
if (this._contextMenuIsBeingShownCount > 0) {
58
const view = this._contextViewService.getContextViewElement();
59
const target = e.srcElement as HTMLElement;
60
61
// Event triggers on shadow root host first
62
// Check if the context view is under this host before hiding it #103169
63
if (!(target.shadowRoot && dom.getShadowRoot(view) === target.shadowRoot)) {
64
this._contextViewService.hideContextView();
65
}
66
}
67
}));
68
this._toDispose.add(this._editor.onKeyDown((e: IKeyboardEvent) => {
69
if (!this._editor.getOption(EditorOption.contextmenu)) {
70
return; // Context menu is turned off through configuration
71
}
72
if (e.keyCode === KeyCode.ContextMenu) {
73
// Chrome is funny like that
74
e.preventDefault();
75
e.stopPropagation();
76
this.showContextMenu();
77
}
78
}));
79
}
80
81
private _onContextMenu(e: IEditorMouseEvent): void {
82
if (!this._editor.hasModel()) {
83
return;
84
}
85
86
if (!this._editor.getOption(EditorOption.contextmenu)) {
87
this._editor.focus();
88
// Ensure the cursor is at the position of the mouse click
89
if (e.target.position && !this._editor.getSelection().containsPosition(e.target.position)) {
90
this._editor.setPosition(e.target.position);
91
}
92
return; // Context menu is turned off through configuration
93
}
94
95
if (e.target.type === MouseTargetType.OVERLAY_WIDGET) {
96
return; // allow native menu on widgets to support right click on input field for example in find
97
}
98
if (e.target.type === MouseTargetType.CONTENT_TEXT && e.target.detail.injectedText) {
99
return; // allow native menu on injected text
100
}
101
102
e.event.preventDefault();
103
e.event.stopPropagation();
104
105
if (e.target.type === MouseTargetType.SCROLLBAR) {
106
return this._showScrollbarContextMenu(e.event);
107
}
108
109
if (e.target.type !== MouseTargetType.CONTENT_TEXT && e.target.type !== MouseTargetType.CONTENT_EMPTY && e.target.type !== MouseTargetType.TEXTAREA) {
110
return; // only support mouse click into text or native context menu key for now
111
}
112
113
// Ensure the editor gets focus if it hasn't, so the right events are being sent to other contributions
114
this._editor.focus();
115
116
// Ensure the cursor is at the position of the mouse click
117
if (e.target.position) {
118
let hasSelectionAtPosition = false;
119
for (const selection of this._editor.getSelections()) {
120
if (selection.containsPosition(e.target.position)) {
121
hasSelectionAtPosition = true;
122
break;
123
}
124
}
125
126
if (!hasSelectionAtPosition) {
127
this._editor.setPosition(e.target.position);
128
}
129
}
130
131
// Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position
132
let anchor: IMouseEvent | null = null;
133
if (e.target.type !== MouseTargetType.TEXTAREA) {
134
anchor = e.event;
135
}
136
137
// Show the context menu
138
this.showContextMenu(anchor);
139
}
140
141
public showContextMenu(anchor?: IMouseEvent | null): void {
142
if (!this._editor.getOption(EditorOption.contextmenu)) {
143
return; // Context menu is turned off through configuration
144
}
145
if (!this._editor.hasModel()) {
146
return;
147
}
148
149
// Find actions available for menu
150
const menuActions = this._getMenuActions(this._editor.getModel(),
151
this._editor.contextMenuId);
152
153
// Show menu if we have actions to show
154
if (menuActions.length > 0) {
155
this._doShowContextMenu(menuActions, anchor);
156
}
157
}
158
159
private _getMenuActions(model: ITextModel, menuId: MenuId): IAction[] {
160
const result: IAction[] = [];
161
162
// get menu groups
163
const groups = this._menuService.getMenuActions(menuId, this._contextKeyService, { arg: model.uri });
164
165
// translate them into other actions
166
for (const group of groups) {
167
const [, actions] = group;
168
let addedItems = 0;
169
for (const action of actions) {
170
if (action instanceof SubmenuItemAction) {
171
const subActions = this._getMenuActions(model, action.item.submenu);
172
if (subActions.length > 0) {
173
result.push(new SubmenuAction(action.id, action.label, subActions));
174
addedItems++;
175
}
176
} else {
177
result.push(action);
178
addedItems++;
179
}
180
}
181
182
if (addedItems) {
183
result.push(new Separator());
184
}
185
}
186
187
if (result.length) {
188
result.pop(); // remove last separator
189
}
190
191
return result;
192
}
193
194
private _doShowContextMenu(actions: IAction[], event: IMouseEvent | null = null): void {
195
if (!this._editor.hasModel()) {
196
return;
197
}
198
199
let anchor: IMouseEvent | IAnchor | null = event;
200
if (!anchor) {
201
// Ensure selection is visible
202
this._editor.revealPosition(this._editor.getPosition(), ScrollType.Immediate);
203
204
this._editor.render();
205
const cursorCoords = this._editor.getScrolledVisiblePosition(this._editor.getPosition());
206
207
// Translate to absolute editor position
208
const editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode());
209
const posx = editorCoords.left + cursorCoords.left;
210
const posy = editorCoords.top + cursorCoords.top + cursorCoords.height;
211
212
anchor = { x: posx, y: posy };
213
}
214
215
const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035
216
217
// Show menu
218
this._contextMenuIsBeingShownCount++;
219
this._contextMenuService.showContextMenu({
220
domForShadowRoot: useShadowDOM ? this._editor.getOverflowWidgetsDomNode() ?? this._editor.getDomNode() : undefined,
221
222
getAnchor: () => anchor,
223
224
getActions: () => actions,
225
226
getActionViewItem: (action) => {
227
const keybinding = this._keybindingFor(action);
228
if (keybinding) {
229
return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel(), isMenu: true });
230
}
231
232
const customActionViewItem = <any>action;
233
if (typeof customActionViewItem.getActionViewItem === 'function') {
234
return customActionViewItem.getActionViewItem();
235
}
236
237
return new ActionViewItem(action, action, { icon: true, label: true, isMenu: true });
238
},
239
240
getKeyBinding: (action): ResolvedKeybinding | undefined => {
241
return this._keybindingFor(action);
242
},
243
244
onHide: (wasCancelled: boolean) => {
245
this._contextMenuIsBeingShownCount--;
246
}
247
});
248
}
249
250
private _showScrollbarContextMenu(anchor: IMouseEvent): void {
251
if (!this._editor.hasModel()) {
252
return;
253
}
254
255
if (isStandaloneEditorWorkspace(this._workspaceContextService.getWorkspace())) {
256
// can't update the configuration properly in the standalone editor
257
return;
258
}
259
260
const minimapOptions = this._editor.getOption(EditorOption.minimap);
261
262
let lastId = 0;
263
const createAction = (opts: { label: string; enabled?: boolean; checked?: boolean; run: () => void }): IAction => {
264
return {
265
id: `menu-action-${++lastId}`,
266
label: opts.label,
267
tooltip: '',
268
class: undefined,
269
enabled: (typeof opts.enabled === 'undefined' ? true : opts.enabled),
270
checked: opts.checked,
271
run: opts.run
272
};
273
};
274
const createSubmenuAction = (label: string, actions: IAction[]): SubmenuAction => {
275
return new SubmenuAction(
276
`menu-action-${++lastId}`,
277
label,
278
actions,
279
undefined
280
);
281
};
282
const createEnumAction = <T>(label: string, enabled: boolean, configName: string, configuredValue: T, options: { label: string; value: T }[]): IAction => {
283
if (!enabled) {
284
return createAction({ label, enabled, run: () => { } });
285
}
286
const createRunner = (value: T) => {
287
return () => {
288
this._configurationService.updateValue(configName, value);
289
};
290
};
291
const actions: IAction[] = [];
292
for (const option of options) {
293
actions.push(createAction({
294
label: option.label,
295
checked: configuredValue === option.value,
296
run: createRunner(option.value)
297
}));
298
}
299
return createSubmenuAction(
300
label,
301
actions
302
);
303
};
304
305
const actions: IAction[] = [];
306
actions.push(createAction({
307
label: nls.localize('context.minimap.minimap', "Minimap"),
308
checked: minimapOptions.enabled,
309
run: () => {
310
this._configurationService.updateValue(`editor.minimap.enabled`, !minimapOptions.enabled);
311
}
312
}));
313
actions.push(new Separator());
314
actions.push(createAction({
315
label: nls.localize('context.minimap.renderCharacters', "Render Characters"),
316
enabled: minimapOptions.enabled,
317
checked: minimapOptions.renderCharacters,
318
run: () => {
319
this._configurationService.updateValue(`editor.minimap.renderCharacters`, !minimapOptions.renderCharacters);
320
}
321
}));
322
actions.push(createEnumAction<'proportional' | 'fill' | 'fit'>(
323
nls.localize('context.minimap.size', "Vertical size"),
324
minimapOptions.enabled,
325
'editor.minimap.size',
326
minimapOptions.size,
327
[{
328
label: nls.localize('context.minimap.size.proportional', "Proportional"),
329
value: 'proportional'
330
}, {
331
label: nls.localize('context.minimap.size.fill', "Fill"),
332
value: 'fill'
333
}, {
334
label: nls.localize('context.minimap.size.fit', "Fit"),
335
value: 'fit'
336
}]
337
));
338
actions.push(createEnumAction<'always' | 'mouseover'>(
339
nls.localize('context.minimap.slider', "Slider"),
340
minimapOptions.enabled,
341
'editor.minimap.showSlider',
342
minimapOptions.showSlider,
343
[{
344
label: nls.localize('context.minimap.slider.mouseover', "Mouse Over"),
345
value: 'mouseover'
346
}, {
347
label: nls.localize('context.minimap.slider.always', "Always"),
348
value: 'always'
349
}]
350
));
351
352
const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035
353
this._contextMenuIsBeingShownCount++;
354
this._contextMenuService.showContextMenu({
355
domForShadowRoot: useShadowDOM ? this._editor.getDomNode() : undefined,
356
getAnchor: () => anchor,
357
getActions: () => actions,
358
onHide: (wasCancelled: boolean) => {
359
this._contextMenuIsBeingShownCount--;
360
this._editor.focus();
361
}
362
});
363
}
364
365
private _keybindingFor(action: IAction): ResolvedKeybinding | undefined {
366
return this._keybindingService.lookupKeybinding(action.id);
367
}
368
369
public dispose(): void {
370
if (this._contextMenuIsBeingShownCount > 0) {
371
this._contextViewService.hideContextView();
372
}
373
374
this._toDispose.dispose();
375
}
376
}
377
378
class ShowContextMenu extends EditorAction {
379
380
constructor() {
381
super({
382
id: 'editor.action.showContextMenu',
383
label: nls.localize2('action.showContextMenu.label', "Show Editor Context Menu"),
384
precondition: undefined,
385
kbOpts: {
386
kbExpr: EditorContextKeys.textInputFocus,
387
primary: KeyMod.Shift | KeyCode.F10,
388
weight: KeybindingWeight.EditorContrib
389
}
390
});
391
}
392
393
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
394
ContextMenuController.get(editor)?.showContextMenu();
395
}
396
}
397
398
registerEditorContribution(ContextMenuController.ID, ContextMenuController, EditorContributionInstantiation.BeforeFirstInteraction);
399
registerEditorAction(ShowContextMenu);
400
401