Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/actionWidget/browser/actionWidget.ts
5221 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
import * as dom from '../../../base/browser/dom.js';
6
import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js';
7
import { IAnchor } from '../../../base/browser/ui/contextview/contextview.js';
8
import { IAction } from '../../../base/common/actions.js';
9
import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
10
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
11
import './actionWidget.css';
12
import { localize, localize2 } from '../../../nls.js';
13
import { acceptSelectedActionCommand, ActionList, IActionListDelegate, IActionListItem, previewSelectedActionCommand } from './actionList.js';
14
import { Action2, registerAction2 } from '../../actions/common/actions.js';
15
import { IContextKeyService, RawContextKey } from '../../contextkey/common/contextkey.js';
16
import { IContextViewService } from '../../contextview/browser/contextView.js';
17
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
18
import { createDecorator, IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js';
19
import { KeybindingWeight } from '../../keybinding/common/keybindingsRegistry.js';
20
import { inputActiveOptionBackground, registerColor } from '../../theme/common/colorRegistry.js';
21
import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
22
import { IListAccessibilityProvider } from '../../../base/browser/ui/list/listWidget.js';
23
24
registerColor(
25
'actionBar.toggledBackground',
26
inputActiveOptionBackground,
27
localize('actionBar.toggledBackground', 'Background color for toggled action items in action bar.')
28
);
29
30
const ActionWidgetContextKeys = {
31
Visible: new RawContextKey<boolean>('codeActionMenuVisible', false, localize('codeActionMenuVisible', "Whether the action widget list is visible"))
32
};
33
34
export const IActionWidgetService = createDecorator<IActionWidgetService>('actionWidgetService');
35
36
export interface IActionWidgetService {
37
readonly _serviceBrand: undefined;
38
39
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void;
40
41
hide(didCancel?: boolean): void;
42
43
readonly isVisible: boolean;
44
}
45
46
class ActionWidgetService extends Disposable implements IActionWidgetService {
47
declare readonly _serviceBrand: undefined;
48
49
get isVisible() {
50
return ActionWidgetContextKeys.Visible.getValue(this._contextKeyService) || false;
51
}
52
53
private readonly _list = this._register(new MutableDisposable<ActionList<unknown>>());
54
55
constructor(
56
@IContextViewService private readonly _contextViewService: IContextViewService,
57
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
58
@IInstantiationService private readonly _instantiationService: IInstantiationService
59
) {
60
super();
61
}
62
63
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void {
64
const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService);
65
66
const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate, accessibilityProvider);
67
this._contextViewService.showContextView({
68
getAnchor: () => anchor,
69
render: (container: HTMLElement) => {
70
visibleContext.set(true);
71
return this._renderWidget(container, list, actionBarActions ?? []);
72
},
73
onHide: (didCancel) => {
74
visibleContext.reset();
75
this._onWidgetClosed(didCancel);
76
},
77
}, container, false);
78
}
79
80
acceptSelected(preview?: boolean) {
81
this._list.value?.acceptSelected(preview);
82
}
83
84
focusPrevious() {
85
this._list?.value?.focusPrevious();
86
}
87
88
focusNext() {
89
this._list?.value?.focusNext();
90
}
91
92
hide(didCancel?: boolean) {
93
this._list.value?.hide(didCancel);
94
this._list.clear();
95
}
96
97
clear() {
98
this._list.clear();
99
}
100
101
private _renderWidget(element: HTMLElement, list: ActionList<unknown>, actionBarActions: readonly IAction[]): IDisposable {
102
const widget = document.createElement('div');
103
widget.classList.add('action-widget');
104
element.appendChild(widget);
105
106
this._list.value = list;
107
if (this._list.value) {
108
widget.appendChild(this._list.value.domNode);
109
} else {
110
throw new Error('List has no value');
111
}
112
const renderDisposables = new DisposableStore();
113
114
// Invisible div to block mouse interaction in the rest of the UI
115
const menuBlock = document.createElement('div');
116
const block = element.appendChild(menuBlock);
117
block.classList.add('context-view-block');
118
renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));
119
120
// Invisible div to block mouse interaction with the menu
121
const pointerBlockDiv = document.createElement('div');
122
const pointerBlock = element.appendChild(pointerBlockDiv);
123
pointerBlock.classList.add('context-view-pointerBlock');
124
125
// Removes block on click INSIDE widget or ANY mouse movement
126
renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.POINTER_MOVE, () => pointerBlock.remove()));
127
renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.MOUSE_DOWN, () => pointerBlock.remove()));
128
129
// Action bar
130
let actionBarWidth = 0;
131
if (actionBarActions.length) {
132
const actionBar = this._createActionBar('.action-widget-action-bar', actionBarActions);
133
if (actionBar) {
134
widget.appendChild(actionBar.getContainer().parentElement!);
135
renderDisposables.add(actionBar);
136
actionBarWidth = actionBar.getContainer().offsetWidth;
137
}
138
}
139
140
const width = this._list.value?.layout(actionBarWidth);
141
widget.style.width = `${width}px`;
142
143
const focusTracker = renderDisposables.add(dom.trackFocus(element));
144
renderDisposables.add(focusTracker.onDidBlur(() => {
145
// Don't hide if focus moved to a hover that belongs to this action widget
146
const activeElement = dom.getActiveElement();
147
if (activeElement?.closest('.action-widget-hover')) {
148
return;
149
}
150
this.hide(true);
151
}));
152
153
return renderDisposables;
154
}
155
156
private _createActionBar(className: string, actions: readonly IAction[]): ActionBar | undefined {
157
if (!actions.length) {
158
return undefined;
159
}
160
161
const container = dom.$(className);
162
const actionBar = new ActionBar(container);
163
actionBar.push(actions, { icon: false, label: true });
164
return actionBar;
165
}
166
167
private _onWidgetClosed(didCancel?: boolean): void {
168
this._list.value?.hide(didCancel);
169
}
170
}
171
172
registerSingleton(IActionWidgetService, ActionWidgetService, InstantiationType.Delayed);
173
174
const weight = KeybindingWeight.EditorContrib + 1000;
175
176
registerAction2(class extends Action2 {
177
constructor() {
178
super({
179
id: 'hideCodeActionWidget',
180
title: localize2('hideCodeActionWidget.title', "Hide action widget"),
181
precondition: ActionWidgetContextKeys.Visible,
182
keybinding: {
183
weight,
184
primary: KeyCode.Escape,
185
secondary: [KeyMod.Shift | KeyCode.Escape]
186
},
187
});
188
}
189
190
run(accessor: ServicesAccessor): void {
191
accessor.get(IActionWidgetService).hide(true);
192
}
193
});
194
195
registerAction2(class extends Action2 {
196
constructor() {
197
super({
198
id: 'selectPrevCodeAction',
199
title: localize2('selectPrevCodeAction.title', "Select previous action"),
200
precondition: ActionWidgetContextKeys.Visible,
201
keybinding: {
202
weight,
203
primary: KeyCode.UpArrow,
204
secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow],
205
mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] },
206
}
207
});
208
}
209
210
run(accessor: ServicesAccessor): void {
211
const widgetService = accessor.get(IActionWidgetService);
212
if (widgetService instanceof ActionWidgetService) {
213
widgetService.focusPrevious();
214
}
215
}
216
});
217
218
registerAction2(class extends Action2 {
219
constructor() {
220
super({
221
id: 'selectNextCodeAction',
222
title: localize2('selectNextCodeAction.title', "Select next action"),
223
precondition: ActionWidgetContextKeys.Visible,
224
keybinding: {
225
weight,
226
primary: KeyCode.DownArrow,
227
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow],
228
mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] }
229
}
230
});
231
}
232
233
run(accessor: ServicesAccessor): void {
234
const widgetService = accessor.get(IActionWidgetService);
235
if (widgetService instanceof ActionWidgetService) {
236
widgetService.focusNext();
237
}
238
}
239
});
240
241
registerAction2(class extends Action2 {
242
constructor() {
243
super({
244
id: acceptSelectedActionCommand,
245
title: localize2('acceptSelected.title', "Accept selected action"),
246
precondition: ActionWidgetContextKeys.Visible,
247
keybinding: {
248
weight,
249
primary: KeyCode.Enter,
250
secondary: [KeyMod.CtrlCmd | KeyCode.Period],
251
}
252
});
253
}
254
255
run(accessor: ServicesAccessor): void {
256
const widgetService = accessor.get(IActionWidgetService);
257
if (widgetService instanceof ActionWidgetService) {
258
widgetService.acceptSelected();
259
}
260
}
261
});
262
263
registerAction2(class extends Action2 {
264
constructor() {
265
super({
266
id: previewSelectedActionCommand,
267
title: localize2('previewSelected.title', "Preview selected action"),
268
precondition: ActionWidgetContextKeys.Visible,
269
keybinding: {
270
weight,
271
primary: KeyMod.CtrlCmd | KeyCode.Enter,
272
}
273
});
274
}
275
276
run(accessor: ServicesAccessor): void {
277
const widgetService = accessor.get(IActionWidgetService);
278
if (widgetService instanceof ActionWidgetService) {
279
widgetService.acceptSelected(true);
280
}
281
}
282
});
283
284