Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts
5267 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
/* eslint-disable local/code-no-dangerous-type-assertions */
6
7
import './media/keybindingsEditor.css';
8
import { localize } from '../../../../nls.js';
9
import { Delayer } from '../../../../base/common/async.js';
10
import * as DOM from '../../../../base/browser/dom.js';
11
import { isIOS, OS } from '../../../../base/common/platform.js';
12
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
13
import { ToggleActionViewItem } from '../../../../base/browser/ui/toggle/toggle.js';
14
import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
15
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
16
import { IAction, Action, Separator } from '../../../../base/common/actions.js';
17
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
18
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
19
import { IEditorOpenContext } from '../../../common/editor.js';
20
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
21
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
22
import { KeybindingsEditorModel, KEYBINDING_ENTRY_TEMPLATE_ID } from '../../../services/preferences/browser/keybindingsEditorModel.js';
23
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
24
import { IKeybindingService, IUserFriendlyKeybinding } from '../../../../platform/keybinding/common/keybinding.js';
25
import { DefineKeybindingWidget, KeybindingsSearchWidget } from './keybindingWidgets.js';
26
import { CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, CONTEXT_WHEN_FOCUS } from '../common/preferences.js';
27
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
28
import { IKeybindingEditingService } from '../../../services/keybinding/common/keybindingEditing.js';
29
import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';
30
import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js';
31
import { ThemeIcon } from '../../../../base/common/themables.js';
32
import { IContextKeyService, IContextKey, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
33
import { KeyCode } from '../../../../base/common/keyCodes.js';
34
import { badgeBackground, contrastBorder, badgeForeground, listActiveSelectionForeground, listInactiveSelectionForeground, listHoverForeground, listFocusForeground, editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground, listFocusBackground, listHoverBackground, registerColor, tableOddRowsBackgroundColor, asCssVariable } from '../../../../platform/theme/common/colorRegistry.js';
35
import { IEditorService } from '../../../services/editor/common/editorService.js';
36
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
37
import { WorkbenchTable } from '../../../../platform/list/browser/listService.js';
38
import { INotificationService } from '../../../../platform/notification/common/notification.js';
39
import { CancellationToken } from '../../../../base/common/cancellation.js';
40
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
41
import { Emitter, Event } from '../../../../base/common/event.js';
42
import { MenuRegistry, MenuId, isIMenuItem } from '../../../../platform/actions/common/actions.js';
43
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
44
import { WORKBENCH_BACKGROUND } from '../../../common/theme.js';
45
import { IKeybindingItemEntry, IKeybindingsEditorPane } from '../../../services/preferences/common/preferences.js';
46
import { keybindingsRecordKeysIcon, keybindingsSortIcon, keybindingsAddIcon, preferencesClearInputIcon, keybindingsEditIcon } from './preferencesIcons.js';
47
import { ITableRenderer, ITableVirtualDelegate } from '../../../../base/browser/ui/table/table.js';
48
import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js';
49
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
50
import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';
51
import { defaultKeybindingLabelStyles, defaultToggleStyles, getInputBoxStyle } from '../../../../platform/theme/browser/defaultStyles.js';
52
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
53
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
54
import { isString } from '../../../../base/common/types.js';
55
import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';
56
import { CompletionItemKind } from '../../../../editor/common/languages.js';
57
import { settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js';
58
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
59
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
60
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
61
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
62
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
63
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
64
import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';
65
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
66
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
67
68
const $ = DOM.$;
69
70
interface IKeybindingsEditorMemento {
71
searchHistory?: string[];
72
}
73
74
export class KeybindingsEditor extends EditorPane<IKeybindingsEditorMemento> implements IKeybindingsEditorPane {
75
76
static readonly ID: string = 'workbench.editor.keybindings';
77
78
private _onDefineWhenExpression: Emitter<IKeybindingItemEntry> = this._register(new Emitter<IKeybindingItemEntry>());
79
readonly onDefineWhenExpression: Event<IKeybindingItemEntry> = this._onDefineWhenExpression.event;
80
81
private _onRejectWhenExpression = this._register(new Emitter<IKeybindingItemEntry>());
82
readonly onRejectWhenExpression = this._onRejectWhenExpression.event;
83
84
private _onAcceptWhenExpression = this._register(new Emitter<IKeybindingItemEntry>());
85
readonly onAcceptWhenExpression = this._onAcceptWhenExpression.event;
86
87
private _onLayout: Emitter<void> = this._register(new Emitter<void>());
88
readonly onLayout: Event<void> = this._onLayout.event;
89
90
private keybindingsEditorModel: KeybindingsEditorModel | null = null;
91
92
private headerContainer!: HTMLElement;
93
private actionsContainer!: HTMLElement;
94
private searchWidget!: KeybindingsSearchWidget;
95
private searchHistoryDelayer: Delayer<void>;
96
97
private overlayContainer!: HTMLElement;
98
private defineKeybindingWidget!: DefineKeybindingWidget;
99
100
private unAssignedKeybindingItemToRevealAndFocus: IKeybindingItemEntry | null = null;
101
private tableEntries: IKeybindingItemEntry[] = [];
102
private keybindingsTableContainer!: HTMLElement;
103
private keybindingsTable!: WorkbenchTable<IKeybindingItemEntry>;
104
105
private dimension: DOM.Dimension | null = null;
106
private delayedFiltering: Delayer<void>;
107
private latestEmptyFilters: string[] = [];
108
private keybindingsEditorContextKey: IContextKey<boolean>;
109
private keybindingFocusContextKey: IContextKey<boolean>;
110
private searchFocusContextKey: IContextKey<boolean>;
111
private searchHasValueContextKey: IContextKey<boolean>;
112
113
private readonly sortByPrecedenceAction: Action;
114
private readonly recordKeysAction: Action;
115
116
private ariaLabelElement!: HTMLElement;
117
readonly overflowWidgetsDomNode: HTMLElement;
118
119
constructor(
120
group: IEditorGroup,
121
@ITelemetryService telemetryService: ITelemetryService,
122
@IThemeService themeService: IThemeService,
123
@IKeybindingService private readonly keybindingsService: IKeybindingService,
124
@IContextMenuService private readonly contextMenuService: IContextMenuService,
125
@IKeybindingEditingService private readonly keybindingEditingService: IKeybindingEditingService,
126
@IContextKeyService private readonly contextKeyService: IContextKeyService,
127
@INotificationService private readonly notificationService: INotificationService,
128
@IClipboardService private readonly clipboardService: IClipboardService,
129
@IInstantiationService private readonly instantiationService: IInstantiationService,
130
@IEditorService private readonly editorService: IEditorService,
131
@IStorageService storageService: IStorageService,
132
@IConfigurationService private readonly configurationService: IConfigurationService,
133
@IAccessibilityService private readonly accessibilityService: IAccessibilityService
134
) {
135
super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService);
136
this.delayedFiltering = this._register(new Delayer<void>(300));
137
this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get())));
138
139
this.keybindingsEditorContextKey = CONTEXT_KEYBINDINGS_EDITOR.bindTo(this.contextKeyService);
140
this.searchFocusContextKey = CONTEXT_KEYBINDINGS_SEARCH_FOCUS.bindTo(this.contextKeyService);
141
this.keybindingFocusContextKey = CONTEXT_KEYBINDING_FOCUS.bindTo(this.contextKeyService);
142
this.searchHasValueContextKey = CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE.bindTo(this.contextKeyService);
143
this.searchHistoryDelayer = this._register(new Delayer<void>(500));
144
145
this.recordKeysAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, localize('recordKeysLabel', "Record Keys"), ThemeIcon.asClassName(keybindingsRecordKeysIcon)));
146
this.recordKeysAction.checked = false;
147
148
this.sortByPrecedenceAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, localize('sortByPrecedeneLabel', "Sort by Precedence (Highest first)"), ThemeIcon.asClassName(keybindingsSortIcon)));
149
this.sortByPrecedenceAction.checked = false;
150
this.overflowWidgetsDomNode = $('.keybindings-overflow-widgets-container.monaco-editor');
151
}
152
153
override create(parent: HTMLElement): void {
154
super.create(parent);
155
this._register(registerNavigableContainer({
156
name: 'keybindingsEditor',
157
focusNotifiers: [this],
158
focusNextWidget: () => {
159
if (this.searchWidget.hasFocus()) {
160
this.focusKeybindings();
161
}
162
},
163
focusPreviousWidget: () => {
164
if (!this.searchWidget.hasFocus()) {
165
this.focusSearch();
166
}
167
}
168
}));
169
}
170
171
protected createEditor(parent: HTMLElement): void {
172
const keybindingsEditorElement = DOM.append(parent, $('div', { class: 'keybindings-editor' }));
173
174
this.createAriaLabelElement(keybindingsEditorElement);
175
this.createOverlayContainer(keybindingsEditorElement);
176
this.createHeader(keybindingsEditorElement);
177
this.createBody(keybindingsEditorElement);
178
}
179
180
override setInput(input: KeybindingsEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
181
this.keybindingsEditorContextKey.set(true);
182
return super.setInput(input, options, context, token)
183
.then(() => this.render(!!(options && options.preserveFocus)));
184
}
185
186
override clearInput(): void {
187
super.clearInput();
188
this.keybindingsEditorContextKey.reset();
189
this.keybindingFocusContextKey.reset();
190
}
191
192
layout(dimension: DOM.Dimension): void {
193
this.dimension = dimension;
194
this.layoutSearchWidget(dimension);
195
196
this.overlayContainer.style.width = dimension.width + 'px';
197
this.overlayContainer.style.height = dimension.height + 'px';
198
this.defineKeybindingWidget.layout(this.dimension);
199
200
this.layoutKeybindingsTable();
201
this._onLayout.fire();
202
}
203
204
override focus(): void {
205
super.focus();
206
207
const activeKeybindingEntry = this.activeKeybindingEntry;
208
if (activeKeybindingEntry) {
209
this.selectEntry(activeKeybindingEntry);
210
} else if (!isIOS) {
211
this.searchWidget.focus();
212
}
213
}
214
215
get activeKeybindingEntry(): IKeybindingItemEntry | null {
216
const focusedElement = this.keybindingsTable.getFocusedElements()[0];
217
return focusedElement && focusedElement.templateId === KEYBINDING_ENTRY_TEMPLATE_ID ? <IKeybindingItemEntry>focusedElement : null;
218
}
219
220
async defineKeybinding(keybindingEntry: IKeybindingItemEntry, add: boolean): Promise<void> {
221
this.selectEntry(keybindingEntry);
222
this.showOverlayContainer();
223
try {
224
const key = await this.defineKeybindingWidget.define();
225
if (key) {
226
await this.updateKeybinding(keybindingEntry, key, keybindingEntry.keybindingItem.when, add);
227
}
228
} catch (error) {
229
this.onKeybindingEditingError(error);
230
} finally {
231
this.hideOverlayContainer();
232
this.selectEntry(keybindingEntry);
233
}
234
}
235
236
defineWhenExpression(keybindingEntry: IKeybindingItemEntry): void {
237
if (keybindingEntry.keybindingItem.keybinding) {
238
this.selectEntry(keybindingEntry);
239
this._onDefineWhenExpression.fire(keybindingEntry);
240
}
241
}
242
243
rejectWhenExpression(keybindingEntry: IKeybindingItemEntry): void {
244
this._onRejectWhenExpression.fire(keybindingEntry);
245
}
246
247
acceptWhenExpression(keybindingEntry: IKeybindingItemEntry): void {
248
this._onAcceptWhenExpression.fire(keybindingEntry);
249
}
250
251
async updateKeybinding(keybindingEntry: IKeybindingItemEntry, key: string, when: string | undefined, add?: boolean): Promise<void> {
252
const currentKey = keybindingEntry.keybindingItem.keybinding ? keybindingEntry.keybindingItem.keybinding.getUserSettingsLabel() : '';
253
if (currentKey !== key || keybindingEntry.keybindingItem.when !== when) {
254
if (add) {
255
await this.keybindingEditingService.addKeybinding(keybindingEntry.keybindingItem.keybindingItem, key, when || undefined);
256
} else {
257
await this.keybindingEditingService.editKeybinding(keybindingEntry.keybindingItem.keybindingItem, key, when || undefined);
258
}
259
if (!keybindingEntry.keybindingItem.keybinding) { // reveal only if keybinding was added to unassinged. Because the entry will be placed in different position after rendering
260
this.unAssignedKeybindingItemToRevealAndFocus = keybindingEntry;
261
}
262
}
263
}
264
265
async removeKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<void> {
266
this.selectEntry(keybindingEntry);
267
if (keybindingEntry.keybindingItem.keybinding) { // This should be a pre-condition
268
try {
269
await this.keybindingEditingService.removeKeybinding(keybindingEntry.keybindingItem.keybindingItem);
270
this.focus();
271
} catch (error) {
272
this.onKeybindingEditingError(error);
273
this.selectEntry(keybindingEntry);
274
}
275
}
276
}
277
278
async resetKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<void> {
279
this.selectEntry(keybindingEntry);
280
try {
281
await this.keybindingEditingService.resetKeybinding(keybindingEntry.keybindingItem.keybindingItem);
282
if (!keybindingEntry.keybindingItem.keybinding) { // reveal only if keybinding was added to unassinged. Because the entry will be placed in different position after rendering
283
this.unAssignedKeybindingItemToRevealAndFocus = keybindingEntry;
284
}
285
this.selectEntry(keybindingEntry);
286
} catch (error) {
287
this.onKeybindingEditingError(error);
288
this.selectEntry(keybindingEntry);
289
}
290
}
291
292
async copyKeybinding(keybinding: IKeybindingItemEntry): Promise<void> {
293
this.selectEntry(keybinding);
294
const userFriendlyKeybinding: IUserFriendlyKeybinding = {
295
key: keybinding.keybindingItem.keybinding ? keybinding.keybindingItem.keybinding.getUserSettingsLabel() || '' : '',
296
command: keybinding.keybindingItem.command
297
};
298
if (keybinding.keybindingItem.when) {
299
userFriendlyKeybinding.when = keybinding.keybindingItem.when;
300
}
301
await this.clipboardService.writeText(JSON.stringify(userFriendlyKeybinding, null, ' '));
302
}
303
304
async copyKeybindingCommand(keybinding: IKeybindingItemEntry): Promise<void> {
305
this.selectEntry(keybinding);
306
await this.clipboardService.writeText(keybinding.keybindingItem.command);
307
}
308
309
async copyKeybindingCommandTitle(keybinding: IKeybindingItemEntry): Promise<void> {
310
this.selectEntry(keybinding);
311
await this.clipboardService.writeText(keybinding.keybindingItem.commandLabel);
312
}
313
314
focusSearch(): void {
315
this.searchWidget.focus();
316
}
317
318
search(filter: string): void {
319
this.focusSearch();
320
this.searchWidget.setValue(filter);
321
this.selectEntry(0);
322
}
323
324
clearSearchResults(): void {
325
this.searchWidget.clear();
326
this.searchHasValueContextKey.set(false);
327
}
328
329
showSimilarKeybindings(keybindingEntry: IKeybindingItemEntry): void {
330
const value = `"${keybindingEntry.keybindingItem.keybinding.getAriaLabel()}"`;
331
if (value !== this.searchWidget.getValue()) {
332
this.searchWidget.setValue(value);
333
}
334
}
335
336
private createAriaLabelElement(parent: HTMLElement): void {
337
this.ariaLabelElement = DOM.append(parent, DOM.$(''));
338
this.ariaLabelElement.setAttribute('id', 'keybindings-editor-aria-label-element');
339
this.ariaLabelElement.setAttribute('aria-live', 'assertive');
340
}
341
342
private createOverlayContainer(parent: HTMLElement): void {
343
this.overlayContainer = DOM.append(parent, $('.overlay-container'));
344
this.overlayContainer.style.position = 'absolute';
345
this.overlayContainer.style.zIndex = '40'; // has to greater than sash z-index which is 35
346
this.defineKeybindingWidget = this._register(this.instantiationService.createInstance(DefineKeybindingWidget, this.overlayContainer));
347
this._register(this.defineKeybindingWidget.onDidChange(keybindingStr => this.defineKeybindingWidget.printExisting(this.keybindingsEditorModel!.fetch(`"${keybindingStr}"`).length)));
348
this._register(this.defineKeybindingWidget.onShowExistingKeybidings(keybindingStr => this.searchWidget.setValue(`"${keybindingStr}"`)));
349
this.hideOverlayContainer();
350
}
351
352
private showOverlayContainer() {
353
this.overlayContainer.style.display = 'block';
354
}
355
356
private hideOverlayContainer() {
357
this.overlayContainer.style.display = 'none';
358
}
359
360
private createHeader(parent: HTMLElement): void {
361
this.headerContainer = DOM.append(parent, $('.keybindings-header'));
362
const fullTextSearchPlaceholder = localize('SearchKeybindings.FullTextSearchPlaceholder', "Type to search in keybindings");
363
const keybindingsSearchPlaceholder = localize('SearchKeybindings.KeybindingsSearchPlaceholder', "Recording Keys. Press Escape to exit");
364
365
const clearInputAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults()));
366
367
const searchContainer = DOM.append(this.headerContainer, $('.search-container'));
368
this.searchWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, searchContainer, {
369
ariaLabel: fullTextSearchPlaceholder,
370
placeholder: fullTextSearchPlaceholder,
371
focusKey: this.searchFocusContextKey,
372
ariaLabelledBy: 'keybindings-editor-aria-label-element',
373
recordEnter: true,
374
quoteRecordedKeys: true,
375
history: new Set<string>((this.getMemento(StorageScope.PROFILE, StorageTarget.USER)).searchHistory ?? []),
376
inputBoxStyles: getInputBoxStyle({
377
inputBorder: settingsTextInputBorder
378
})
379
}));
380
this._register(this.searchWidget.onDidChange(searchValue => {
381
const hasValue = !!searchValue;
382
clearInputAction.enabled = hasValue;
383
this.searchHasValueContextKey.set(hasValue);
384
this.delayedFiltering.trigger(() => this.filterKeybindings());
385
this.updateSearchOptions();
386
}));
387
this._register(this.searchWidget.onEscape(() => this.recordKeysAction.checked = false));
388
389
this.actionsContainer = DOM.append(searchContainer, DOM.$('.keybindings-search-actions-container'));
390
const recordingBadge = this.createRecordingBadge(this.actionsContainer);
391
392
this._register(this.sortByPrecedenceAction.onDidChange(e => {
393
if (e.checked !== undefined) {
394
this.renderKeybindingsEntries(false);
395
}
396
this.updateSearchOptions();
397
}));
398
399
this._register(this.recordKeysAction.onDidChange(e => {
400
if (e.checked !== undefined) {
401
recordingBadge.classList.toggle('disabled', !e.checked);
402
if (e.checked) {
403
this.searchWidget.inputBox.setPlaceHolder(keybindingsSearchPlaceholder);
404
this.searchWidget.inputBox.setAriaLabel(keybindingsSearchPlaceholder);
405
this.searchWidget.startRecordingKeys();
406
this.searchWidget.focus();
407
} else {
408
this.searchWidget.inputBox.setPlaceHolder(fullTextSearchPlaceholder);
409
this.searchWidget.inputBox.setAriaLabel(fullTextSearchPlaceholder);
410
this.searchWidget.stopRecordingKeys();
411
this.searchWidget.focus();
412
}
413
this.updateSearchOptions();
414
}
415
}));
416
417
const actions = [this.recordKeysAction, this.sortByPrecedenceAction, clearInputAction];
418
const toolBar = this._register(new ToolBar(this.actionsContainer, this.contextMenuService, {
419
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
420
if (action.id === this.sortByPrecedenceAction.id || action.id === this.recordKeysAction.id) {
421
return new ToggleActionViewItem(null, action, { ...options, keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel(), toggleStyles: defaultToggleStyles });
422
}
423
return undefined;
424
},
425
getKeyBinding: action => this.keybindingsService.lookupKeybinding(action.id)
426
}));
427
toolBar.setActions(actions);
428
this._register(this.keybindingsService.onDidUpdateKeybindings(() => toolBar.setActions(actions)));
429
}
430
431
private updateSearchOptions(): void {
432
const keybindingsEditorInput = this.input as KeybindingsEditorInput;
433
if (keybindingsEditorInput) {
434
keybindingsEditorInput.searchOptions = {
435
searchValue: this.searchWidget.getValue(),
436
recordKeybindings: !!this.recordKeysAction.checked,
437
sortByPrecedence: !!this.sortByPrecedenceAction.checked
438
};
439
}
440
}
441
442
private createRecordingBadge(container: HTMLElement): HTMLElement {
443
const recordingBadge = DOM.append(container, DOM.$('.recording-badge.monaco-count-badge.long.disabled'));
444
recordingBadge.textContent = localize('recording', "Recording Keys");
445
446
recordingBadge.style.backgroundColor = asCssVariable(badgeBackground);
447
recordingBadge.style.color = asCssVariable(badgeForeground);
448
recordingBadge.style.border = `1px solid ${asCssVariable(contrastBorder)}`;
449
450
return recordingBadge;
451
}
452
453
private layoutSearchWidget(dimension: DOM.Dimension): void {
454
this.searchWidget.layout(dimension);
455
this.headerContainer.classList.toggle('small', dimension.width < 400);
456
this.searchWidget.inputBox.inputElement.style.paddingRight = `${DOM.getTotalWidth(this.actionsContainer) + 12}px`;
457
}
458
459
private createBody(parent: HTMLElement): void {
460
const bodyContainer = DOM.append(parent, $('.keybindings-body'));
461
this.createTable(bodyContainer);
462
}
463
464
private createTable(parent: HTMLElement): void {
465
this.keybindingsTableContainer = DOM.append(parent, $('.keybindings-table-container'));
466
this.keybindingsTable = this._register(this.instantiationService.createInstance(WorkbenchTable,
467
'KeybindingsEditor',
468
this.keybindingsTableContainer,
469
new Delegate(),
470
[
471
{
472
label: '',
473
tooltip: '',
474
weight: 0,
475
minimumWidth: 40,
476
maximumWidth: 40,
477
templateId: ActionsColumnRenderer.TEMPLATE_ID,
478
project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; }
479
},
480
{
481
label: localize('command', "Command"),
482
tooltip: '',
483
weight: 0.3,
484
templateId: CommandColumnRenderer.TEMPLATE_ID,
485
project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; }
486
},
487
{
488
label: localize('keybinding', "Keybinding"),
489
tooltip: '',
490
weight: 0.2,
491
templateId: KeybindingColumnRenderer.TEMPLATE_ID,
492
project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; }
493
},
494
{
495
label: localize('when', "When"),
496
tooltip: '',
497
weight: 0.35,
498
templateId: WhenColumnRenderer.TEMPLATE_ID,
499
project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; }
500
},
501
{
502
label: localize('source', "Source"),
503
tooltip: '',
504
weight: 0.15,
505
templateId: SourceColumnRenderer.TEMPLATE_ID,
506
project(row: IKeybindingItemEntry): IKeybindingItemEntry { return row; }
507
},
508
],
509
[
510
this.instantiationService.createInstance(ActionsColumnRenderer, this),
511
this.instantiationService.createInstance(CommandColumnRenderer),
512
this.instantiationService.createInstance(KeybindingColumnRenderer),
513
this.instantiationService.createInstance(WhenColumnRenderer, this),
514
this.instantiationService.createInstance(SourceColumnRenderer),
515
],
516
{
517
identityProvider: { getId: (e: IKeybindingItemEntry) => e.id },
518
horizontalScrolling: false,
519
accessibilityProvider: new AccessibilityProvider(this.configurationService),
520
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IKeybindingItemEntry) => e.keybindingItem.commandLabel || e.keybindingItem.command },
521
overrideStyles: {
522
listBackground: editorBackground
523
},
524
multipleSelectionSupport: false,
525
setRowLineHeight: false,
526
openOnSingleClick: false,
527
transformOptimization: false // disable transform optimization as it causes the editor overflow widgets to be mispositioned
528
}
529
)) as WorkbenchTable<IKeybindingItemEntry>;
530
531
this._register(this.keybindingsTable.onContextMenu(e => this.onContextMenu(e)));
532
this._register(this.keybindingsTable.onDidChangeFocus(e => this.onFocusChange()));
533
this._register(this.keybindingsTable.onDidFocus(() => {
534
this.keybindingsTable.getHTMLElement().classList.add('focused');
535
this.onFocusChange();
536
}));
537
this._register(this.keybindingsTable.onDidBlur(() => {
538
this.keybindingsTable.getHTMLElement().classList.remove('focused');
539
this.keybindingFocusContextKey.reset();
540
}));
541
this._register(this.keybindingsTable.onDidOpen((e) => {
542
// stop double click action on the input #148493
543
if (e.browserEvent?.defaultPrevented) {
544
return;
545
}
546
const activeKeybindingEntry = this.activeKeybindingEntry;
547
if (activeKeybindingEntry) {
548
this.defineKeybinding(activeKeybindingEntry, false);
549
}
550
}));
551
552
DOM.append(this.keybindingsTableContainer, this.overflowWidgetsDomNode);
553
}
554
555
private async render(preserveFocus: boolean): Promise<void> {
556
if (this.input) {
557
const input: KeybindingsEditorInput = this.input as KeybindingsEditorInput;
558
this.keybindingsEditorModel = await input.resolve();
559
await this.keybindingsEditorModel.resolve(this.getActionsLabels());
560
this.renderKeybindingsEntries(false, preserveFocus);
561
if (input.searchOptions) {
562
this.recordKeysAction.checked = input.searchOptions.recordKeybindings;
563
this.sortByPrecedenceAction.checked = input.searchOptions.sortByPrecedence;
564
this.searchWidget.setValue(input.searchOptions.searchValue);
565
} else {
566
this.updateSearchOptions();
567
}
568
}
569
}
570
571
private getActionsLabels(): Map<string, string> {
572
const actionsLabels: Map<string, string> = new Map<string, string>();
573
for (const editorAction of EditorExtensionsRegistry.getEditorActions()) {
574
actionsLabels.set(editorAction.id, editorAction.label);
575
}
576
for (const menuItem of MenuRegistry.getMenuItems(MenuId.CommandPalette)) {
577
if (isIMenuItem(menuItem)) {
578
const title = typeof menuItem.command.title === 'string' ? menuItem.command.title : menuItem.command.title.value;
579
const category = menuItem.command.category ? typeof menuItem.command.category === 'string' ? menuItem.command.category : menuItem.command.category.value : undefined;
580
actionsLabels.set(menuItem.command.id, category ? `${category}: ${title}` : title);
581
}
582
}
583
return actionsLabels;
584
}
585
586
private filterKeybindings(): void {
587
this.renderKeybindingsEntries(this.searchWidget.hasFocus());
588
this.searchHistoryDelayer.trigger(() => {
589
this.searchWidget.inputBox.addToHistory();
590
(this.getMemento(StorageScope.PROFILE, StorageTarget.USER)).searchHistory = this.searchWidget.inputBox.getHistory();
591
this.saveState();
592
});
593
}
594
595
public clearKeyboardShortcutSearchHistory(): void {
596
this.searchWidget.inputBox.clearHistory();
597
(this.getMemento(StorageScope.PROFILE, StorageTarget.USER)).searchHistory = this.searchWidget.inputBox.getHistory();
598
this.saveState();
599
}
600
601
private renderKeybindingsEntries(reset: boolean, preserveFocus?: boolean): void {
602
if (this.keybindingsEditorModel) {
603
const filter = this.searchWidget.getValue();
604
const keybindingsEntries: IKeybindingItemEntry[] = this.keybindingsEditorModel.fetch(filter, this.sortByPrecedenceAction.checked);
605
this.accessibilityService.alert(localize('foundResults', "{0} results", keybindingsEntries.length));
606
this.ariaLabelElement.setAttribute('aria-label', this.getAriaLabel(keybindingsEntries));
607
608
if (keybindingsEntries.length === 0) {
609
this.latestEmptyFilters.push(filter);
610
}
611
const currentSelectedIndex = this.keybindingsTable.getSelection()[0];
612
this.tableEntries = keybindingsEntries;
613
this.keybindingsTable.splice(0, this.keybindingsTable.length, this.tableEntries);
614
this.layoutKeybindingsTable();
615
616
if (reset) {
617
this.keybindingsTable.setSelection([]);
618
this.keybindingsTable.setFocus([]);
619
} else {
620
if (this.unAssignedKeybindingItemToRevealAndFocus) {
621
const index = this.getNewIndexOfUnassignedKeybinding(this.unAssignedKeybindingItemToRevealAndFocus);
622
if (index !== -1) {
623
this.keybindingsTable.reveal(index, 0.2);
624
this.selectEntry(index);
625
}
626
this.unAssignedKeybindingItemToRevealAndFocus = null;
627
} else if (currentSelectedIndex !== -1 && currentSelectedIndex < this.tableEntries.length) {
628
this.selectEntry(currentSelectedIndex, preserveFocus);
629
} else if (this.editorService.activeEditorPane === this && !preserveFocus) {
630
this.focus();
631
}
632
}
633
}
634
}
635
636
private getAriaLabel(keybindingsEntries: IKeybindingItemEntry[]): string {
637
if (this.sortByPrecedenceAction.checked) {
638
return localize('show sorted keybindings', "Showing {0} Keybindings in precedence order", keybindingsEntries.length);
639
} else {
640
return localize('show keybindings', "Showing {0} Keybindings in alphabetical order", keybindingsEntries.length);
641
}
642
}
643
644
private layoutKeybindingsTable(): void {
645
if (!this.dimension) {
646
return;
647
}
648
649
const tableHeight = this.dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/);
650
this.keybindingsTableContainer.style.height = `${tableHeight}px`;
651
this.keybindingsTable.layout(tableHeight);
652
}
653
654
private getIndexOf(listEntry: IKeybindingItemEntry): number {
655
const index = this.tableEntries.indexOf(listEntry);
656
if (index === -1) {
657
for (let i = 0; i < this.tableEntries.length; i++) {
658
if (this.tableEntries[i].id === listEntry.id) {
659
return i;
660
}
661
}
662
}
663
return index;
664
}
665
666
private getNewIndexOfUnassignedKeybinding(unassignedKeybinding: IKeybindingItemEntry): number {
667
for (let index = 0; index < this.tableEntries.length; index++) {
668
const entry = this.tableEntries[index];
669
if (entry.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) {
670
const keybindingItemEntry = (<IKeybindingItemEntry>entry);
671
if (keybindingItemEntry.keybindingItem.command === unassignedKeybinding.keybindingItem.command) {
672
return index;
673
}
674
}
675
}
676
return -1;
677
}
678
679
private selectEntry(keybindingItemEntry: IKeybindingItemEntry | number, focus: boolean = true): void {
680
const index = typeof keybindingItemEntry === 'number' ? keybindingItemEntry : this.getIndexOf(keybindingItemEntry);
681
if (index !== -1 && index < this.keybindingsTable.length) {
682
if (focus) {
683
this.keybindingsTable.domFocus();
684
this.keybindingsTable.setFocus([index]);
685
}
686
this.keybindingsTable.setSelection([index]);
687
}
688
}
689
690
focusKeybindings(): void {
691
this.keybindingsTable.domFocus();
692
const currentFocusIndices = this.keybindingsTable.getFocus();
693
this.keybindingsTable.setFocus([currentFocusIndices.length ? currentFocusIndices[0] : 0]);
694
}
695
696
selectKeybinding(keybindingItemEntry: IKeybindingItemEntry): void {
697
this.selectEntry(keybindingItemEntry);
698
}
699
700
recordSearchKeys(): void {
701
this.recordKeysAction.checked = true;
702
}
703
704
toggleSortByPrecedence(): void {
705
this.sortByPrecedenceAction.checked = !this.sortByPrecedenceAction.checked;
706
}
707
708
private onContextMenu(e: IListContextMenuEvent<IKeybindingItemEntry>): void {
709
if (!e.element) {
710
return;
711
}
712
713
if (e.element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) {
714
const keybindingItemEntry = <IKeybindingItemEntry>e.element;
715
this.selectEntry(keybindingItemEntry);
716
this.contextMenuService.showContextMenu({
717
getAnchor: () => e.anchor,
718
getActions: () => [
719
this.createCopyAction(keybindingItemEntry),
720
this.createCopyCommandAction(keybindingItemEntry),
721
this.createCopyCommandTitleAction(keybindingItemEntry),
722
new Separator(),
723
...(keybindingItemEntry.keybindingItem.keybinding
724
? [this.createDefineKeybindingAction(keybindingItemEntry), this.createAddKeybindingAction(keybindingItemEntry)]
725
: [this.createDefineKeybindingAction(keybindingItemEntry)]),
726
new Separator(),
727
this.createRemoveAction(keybindingItemEntry),
728
this.createResetAction(keybindingItemEntry),
729
new Separator(),
730
this.createDefineWhenExpressionAction(keybindingItemEntry),
731
new Separator(),
732
this.createShowConflictsAction(keybindingItemEntry)]
733
});
734
}
735
}
736
737
private onFocusChange(): void {
738
this.keybindingFocusContextKey.reset();
739
const element = this.keybindingsTable.getFocusedElements()[0];
740
if (!element) {
741
return;
742
}
743
if (element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) {
744
this.keybindingFocusContextKey.set(true);
745
}
746
}
747
748
private createDefineKeybindingAction(keybindingItemEntry: IKeybindingItemEntry): IAction {
749
return <IAction>{
750
label: keybindingItemEntry.keybindingItem.keybinding ? localize('changeLabel', "Change Keybinding...") : localize('addLabel', "Add Keybinding..."),
751
enabled: true,
752
id: KEYBINDINGS_EDITOR_COMMAND_DEFINE,
753
run: () => this.defineKeybinding(keybindingItemEntry, false)
754
};
755
}
756
757
private createAddKeybindingAction(keybindingItemEntry: IKeybindingItemEntry): IAction {
758
return <IAction>{
759
label: localize('addLabel', "Add Keybinding..."),
760
enabled: true,
761
id: KEYBINDINGS_EDITOR_COMMAND_ADD,
762
run: () => this.defineKeybinding(keybindingItemEntry, true)
763
};
764
}
765
766
private createDefineWhenExpressionAction(keybindingItemEntry: IKeybindingItemEntry): IAction {
767
return <IAction>{
768
label: localize('editWhen', "Change When Expression"),
769
enabled: !!keybindingItemEntry.keybindingItem.keybinding,
770
id: KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN,
771
run: () => this.defineWhenExpression(keybindingItemEntry)
772
};
773
}
774
775
private createRemoveAction(keybindingItem: IKeybindingItemEntry): IAction {
776
return <IAction>{
777
label: localize('removeLabel', "Remove Keybinding"),
778
enabled: !!keybindingItem.keybindingItem.keybinding,
779
id: KEYBINDINGS_EDITOR_COMMAND_REMOVE,
780
run: () => this.removeKeybinding(keybindingItem)
781
};
782
}
783
784
private createResetAction(keybindingItem: IKeybindingItemEntry): IAction {
785
return <IAction>{
786
label: localize('resetLabel', "Reset Keybinding"),
787
enabled: !keybindingItem.keybindingItem.keybindingItem.isDefault,
788
id: KEYBINDINGS_EDITOR_COMMAND_RESET,
789
run: () => this.resetKeybinding(keybindingItem)
790
};
791
}
792
793
private createShowConflictsAction(keybindingItem: IKeybindingItemEntry): IAction {
794
return <IAction>{
795
label: localize('showSameKeybindings', "Show Same Keybindings"),
796
enabled: !!keybindingItem.keybindingItem.keybinding,
797
id: KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR,
798
run: () => this.showSimilarKeybindings(keybindingItem)
799
};
800
}
801
802
private createCopyAction(keybindingItem: IKeybindingItemEntry): IAction {
803
return <IAction>{
804
label: localize('copyLabel', "Copy"),
805
enabled: true,
806
id: KEYBINDINGS_EDITOR_COMMAND_COPY,
807
run: () => this.copyKeybinding(keybindingItem)
808
};
809
}
810
811
private createCopyCommandAction(keybinding: IKeybindingItemEntry): IAction {
812
return <IAction>{
813
label: localize('copyCommandLabel', "Copy Command ID"),
814
enabled: true,
815
id: KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND,
816
run: () => this.copyKeybindingCommand(keybinding)
817
};
818
}
819
820
private createCopyCommandTitleAction(keybinding: IKeybindingItemEntry): IAction {
821
return <IAction>{
822
label: localize('copyCommandTitleLabel', "Copy Command Title"),
823
enabled: !!keybinding.keybindingItem.commandLabel,
824
id: KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE,
825
run: () => this.copyKeybindingCommandTitle(keybinding)
826
};
827
}
828
829
private onKeybindingEditingError(error: unknown): void {
830
this.notificationService.error(typeof error === 'string' ? error : localize('error', "Error '{0}' while editing the keybinding. Please open 'keybindings.json' file and check for errors.", `${error}`));
831
}
832
}
833
834
class Delegate implements ITableVirtualDelegate<IKeybindingItemEntry> {
835
836
readonly headerRowHeight = 30;
837
838
getHeight(element: IKeybindingItemEntry) {
839
if (element.templateId === KEYBINDING_ENTRY_TEMPLATE_ID) {
840
const commandIdMatched = (<IKeybindingItemEntry>element).keybindingItem.commandLabel && (<IKeybindingItemEntry>element).commandIdMatches;
841
const commandDefaultLabelMatched = !!(<IKeybindingItemEntry>element).commandDefaultLabelMatches;
842
const extensionIdMatched = !!(<IKeybindingItemEntry>element).extensionIdMatches;
843
if (commandIdMatched && commandDefaultLabelMatched) {
844
return 60;
845
}
846
if (extensionIdMatched || commandIdMatched || commandDefaultLabelMatched) {
847
return 40;
848
}
849
}
850
return 24;
851
}
852
853
}
854
855
interface IActionsColumnTemplateData {
856
readonly actionBar: ActionBar;
857
}
858
859
class ActionsColumnRenderer implements ITableRenderer<IKeybindingItemEntry, IActionsColumnTemplateData> {
860
861
static readonly TEMPLATE_ID = 'actions';
862
863
readonly templateId: string = ActionsColumnRenderer.TEMPLATE_ID;
864
865
constructor(
866
private readonly keybindingsEditor: KeybindingsEditor,
867
@IKeybindingService private readonly keybindingsService: IKeybindingService
868
) {
869
}
870
871
renderTemplate(container: HTMLElement): IActionsColumnTemplateData {
872
const element = DOM.append(container, $('.actions'));
873
const actionBar = new ActionBar(element);
874
return { actionBar };
875
}
876
877
renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: IActionsColumnTemplateData): void {
878
templateData.actionBar.clear();
879
const actions: IAction[] = [];
880
if (keybindingItemEntry.keybindingItem.keybinding) {
881
actions.push(this.createEditAction(keybindingItemEntry));
882
} else {
883
actions.push(this.createAddAction(keybindingItemEntry));
884
}
885
templateData.actionBar.push(actions, { icon: true });
886
}
887
888
private createEditAction(keybindingItemEntry: IKeybindingItemEntry): IAction {
889
return <IAction>{
890
class: ThemeIcon.asClassName(keybindingsEditIcon),
891
enabled: true,
892
id: 'editKeybinding',
893
tooltip: this.keybindingsService.appendKeybinding(localize('editKeybindingLabel', "Change Keybinding"), KEYBINDINGS_EDITOR_COMMAND_DEFINE),
894
run: () => this.keybindingsEditor.defineKeybinding(keybindingItemEntry, false)
895
};
896
}
897
898
private createAddAction(keybindingItemEntry: IKeybindingItemEntry): IAction {
899
return <IAction>{
900
class: ThemeIcon.asClassName(keybindingsAddIcon),
901
enabled: true,
902
id: 'addKeybinding',
903
tooltip: this.keybindingsService.appendKeybinding(localize('addKeybindingLabel', "Add Keybinding"), KEYBINDINGS_EDITOR_COMMAND_DEFINE),
904
run: () => this.keybindingsEditor.defineKeybinding(keybindingItemEntry, false)
905
};
906
}
907
908
disposeTemplate(templateData: IActionsColumnTemplateData): void {
909
templateData.actionBar.dispose();
910
}
911
912
}
913
914
interface ICommandColumnTemplateData {
915
commandColumn: HTMLElement;
916
commandColumnHover: IManagedHover;
917
commandLabelContainer: HTMLElement;
918
commandLabel: HighlightedLabel;
919
commandDefaultLabelContainer: HTMLElement;
920
commandDefaultLabel: HighlightedLabel;
921
commandIdLabelContainer: HTMLElement;
922
commandIdLabel: HighlightedLabel;
923
}
924
925
class CommandColumnRenderer implements ITableRenderer<IKeybindingItemEntry, ICommandColumnTemplateData> {
926
927
static readonly TEMPLATE_ID = 'commands';
928
929
readonly templateId: string = CommandColumnRenderer.TEMPLATE_ID;
930
931
constructor(
932
@IHoverService private readonly _hoverService: IHoverService
933
) {
934
}
935
936
renderTemplate(container: HTMLElement): ICommandColumnTemplateData {
937
const commandColumn = DOM.append(container, $('.command'));
938
const commandColumnHover = this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), commandColumn, '');
939
const commandLabelContainer = DOM.append(commandColumn, $('.command-label'));
940
const commandLabel = new HighlightedLabel(commandLabelContainer);
941
const commandDefaultLabelContainer = DOM.append(commandColumn, $('.command-default-label'));
942
const commandDefaultLabel = new HighlightedLabel(commandDefaultLabelContainer);
943
const commandIdLabelContainer = DOM.append(commandColumn, $('.command-id.code'));
944
const commandIdLabel = new HighlightedLabel(commandIdLabelContainer);
945
return { commandColumn, commandColumnHover, commandLabelContainer, commandLabel, commandDefaultLabelContainer, commandDefaultLabel, commandIdLabelContainer, commandIdLabel };
946
}
947
948
renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: ICommandColumnTemplateData): void {
949
const keybindingItem = keybindingItemEntry.keybindingItem;
950
const commandIdMatched = !!(keybindingItem.commandLabel && keybindingItemEntry.commandIdMatches);
951
const commandDefaultLabelMatched = !!keybindingItemEntry.commandDefaultLabelMatches;
952
953
templateData.commandColumn.classList.toggle('vertical-align-column', commandIdMatched || commandDefaultLabelMatched);
954
const title = keybindingItem.commandLabel ? localize('title', "{0} ({1})", keybindingItem.commandLabel, keybindingItem.command) : keybindingItem.command;
955
templateData.commandColumn.setAttribute('aria-label', title);
956
templateData.commandColumnHover.update(title);
957
958
if (keybindingItem.commandLabel) {
959
templateData.commandLabelContainer.classList.remove('hide');
960
templateData.commandLabel.set(keybindingItem.commandLabel, keybindingItemEntry.commandLabelMatches);
961
} else {
962
templateData.commandLabelContainer.classList.add('hide');
963
templateData.commandLabel.set(undefined);
964
}
965
966
if (keybindingItemEntry.commandDefaultLabelMatches) {
967
templateData.commandDefaultLabelContainer.classList.remove('hide');
968
templateData.commandDefaultLabel.set(keybindingItem.commandDefaultLabel, keybindingItemEntry.commandDefaultLabelMatches);
969
} else {
970
templateData.commandDefaultLabelContainer.classList.add('hide');
971
templateData.commandDefaultLabel.set(undefined);
972
}
973
974
if (keybindingItemEntry.commandIdMatches || !keybindingItem.commandLabel) {
975
templateData.commandIdLabelContainer.classList.remove('hide');
976
templateData.commandIdLabel.set(keybindingItem.command, keybindingItemEntry.commandIdMatches);
977
} else {
978
templateData.commandIdLabelContainer.classList.add('hide');
979
templateData.commandIdLabel.set(undefined);
980
}
981
}
982
983
disposeTemplate(templateData: ICommandColumnTemplateData): void {
984
templateData.commandColumnHover.dispose();
985
templateData.commandDefaultLabel.dispose();
986
templateData.commandIdLabel.dispose();
987
templateData.commandLabel.dispose();
988
}
989
}
990
991
interface IKeybindingColumnTemplateData {
992
keybindingLabel: KeybindingLabel;
993
}
994
995
class KeybindingColumnRenderer implements ITableRenderer<IKeybindingItemEntry, IKeybindingColumnTemplateData> {
996
997
static readonly TEMPLATE_ID = 'keybindings';
998
999
readonly templateId: string = KeybindingColumnRenderer.TEMPLATE_ID;
1000
1001
constructor() { }
1002
1003
renderTemplate(container: HTMLElement): IKeybindingColumnTemplateData {
1004
const element = DOM.append(container, $('.keybinding'));
1005
const keybindingLabel = new KeybindingLabel(DOM.append(element, $('div.keybinding-label')), OS, defaultKeybindingLabelStyles);
1006
return { keybindingLabel };
1007
}
1008
1009
renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: IKeybindingColumnTemplateData): void {
1010
if (keybindingItemEntry.keybindingItem.keybinding) {
1011
templateData.keybindingLabel.set(keybindingItemEntry.keybindingItem.keybinding, keybindingItemEntry.keybindingMatches);
1012
} else {
1013
templateData.keybindingLabel.set(undefined, undefined);
1014
}
1015
}
1016
1017
disposeTemplate(templateData: IKeybindingColumnTemplateData): void {
1018
templateData.keybindingLabel.dispose();
1019
}
1020
}
1021
1022
interface ISourceColumnTemplateData {
1023
sourceColumn: HTMLElement;
1024
sourceColumnHover: IManagedHover;
1025
sourceLabel: HighlightedLabel;
1026
extensionContainer: HTMLElement;
1027
extensionLabel: HTMLAnchorElement;
1028
extensionId: HighlightedLabel;
1029
disposables: DisposableStore;
1030
}
1031
1032
function onClick(element: HTMLElement, callback: () => void): IDisposable {
1033
const disposables = new DisposableStore();
1034
disposables.add(DOM.addDisposableListener(element, DOM.EventType.CLICK, DOM.finalHandler(callback)));
1035
disposables.add(DOM.addDisposableListener(element, DOM.EventType.KEY_UP, e => {
1036
const keyboardEvent = new StandardKeyboardEvent(e);
1037
if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) {
1038
e.preventDefault();
1039
e.stopPropagation();
1040
callback();
1041
}
1042
}));
1043
return disposables;
1044
}
1045
1046
class SourceColumnRenderer implements ITableRenderer<IKeybindingItemEntry, ISourceColumnTemplateData> {
1047
1048
static readonly TEMPLATE_ID = 'source';
1049
1050
readonly templateId: string = SourceColumnRenderer.TEMPLATE_ID;
1051
1052
constructor(
1053
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
1054
@IHoverService private readonly hoverService: IHoverService,
1055
) { }
1056
1057
renderTemplate(container: HTMLElement): ISourceColumnTemplateData {
1058
const sourceColumn = DOM.append(container, $('.source'));
1059
const sourceColumnHover = this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), sourceColumn, '');
1060
const sourceLabel = new HighlightedLabel(DOM.append(sourceColumn, $('.source-label')));
1061
const extensionContainer = DOM.append(sourceColumn, $('.extension-container'));
1062
const extensionLabel = DOM.append<HTMLAnchorElement>(extensionContainer, $('a.extension-label', { tabindex: 0 }));
1063
const extensionId = new HighlightedLabel(DOM.append(extensionContainer, $('.extension-id-container.code')));
1064
return { sourceColumn, sourceColumnHover, sourceLabel, extensionLabel, extensionContainer, extensionId, disposables: new DisposableStore() };
1065
}
1066
1067
renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: ISourceColumnTemplateData): void {
1068
templateData.disposables.clear();
1069
if (isString(keybindingItemEntry.keybindingItem.source)) {
1070
templateData.extensionContainer.classList.add('hide');
1071
templateData.sourceLabel.element.classList.remove('hide');
1072
templateData.sourceColumnHover.update('');
1073
templateData.sourceLabel.set(keybindingItemEntry.keybindingItem.source || '-', keybindingItemEntry.sourceMatches);
1074
} else {
1075
templateData.extensionContainer.classList.remove('hide');
1076
templateData.sourceLabel.element.classList.add('hide');
1077
const extension = keybindingItemEntry.keybindingItem.source;
1078
const extensionLabel = extension.displayName ?? extension.identifier.value;
1079
templateData.sourceColumnHover.update(localize('extension label', "Extension ({0})", extensionLabel));
1080
templateData.extensionLabel.textContent = extensionLabel;
1081
templateData.disposables.add(onClick(templateData.extensionLabel, () => {
1082
this.extensionsWorkbenchService.open(extension.identifier.value);
1083
}));
1084
if (keybindingItemEntry.extensionIdMatches) {
1085
templateData.extensionId.element.classList.remove('hide');
1086
templateData.extensionId.set(extension.identifier.value, keybindingItemEntry.extensionIdMatches);
1087
} else {
1088
templateData.extensionId.element.classList.add('hide');
1089
templateData.extensionId.set(undefined);
1090
}
1091
}
1092
}
1093
1094
disposeTemplate(templateData: ISourceColumnTemplateData): void {
1095
templateData.sourceColumnHover.dispose();
1096
templateData.disposables.dispose();
1097
templateData.sourceLabel.dispose();
1098
templateData.extensionId.dispose();
1099
}
1100
}
1101
1102
class WhenInputWidget extends Disposable {
1103
1104
private readonly input: SuggestEnabledInput;
1105
1106
private readonly _onDidAccept = this._register(new Emitter<string>());
1107
readonly onDidAccept = this._onDidAccept.event;
1108
1109
private readonly _onDidReject = this._register(new Emitter<void>());
1110
readonly onDidReject = this._onDidReject.event;
1111
1112
constructor(
1113
parent: HTMLElement,
1114
keybindingsEditor: KeybindingsEditor,
1115
@IInstantiationService instantiationService: IInstantiationService,
1116
@IContextKeyService contextKeyService: IContextKeyService,
1117
) {
1118
super();
1119
const focusContextKey = CONTEXT_WHEN_FOCUS.bindTo(contextKeyService);
1120
this.input = this._register(instantiationService.createInstance(SuggestEnabledInput, 'keyboardshortcutseditor#wheninput', parent, {
1121
provideResults: () => {
1122
const result = [];
1123
for (const contextKey of RawContextKey.all()) {
1124
result.push({ label: contextKey.key, documentation: contextKey.description, detail: contextKey.type, kind: CompletionItemKind.Constant });
1125
}
1126
return result;
1127
},
1128
triggerCharacters: ['!', ' '],
1129
wordDefinition: /[a-zA-Z.]+/,
1130
alwaysShowSuggestions: true,
1131
}, '', `keyboardshortcutseditor#wheninput`, { focusContextKey, overflowWidgetsDomNode: keybindingsEditor.overflowWidgetsDomNode }));
1132
1133
this._register((DOM.addDisposableListener(this.input.element, DOM.EventType.DBLCLICK, e => DOM.EventHelper.stop(e))));
1134
this._register(toDisposable(() => focusContextKey.reset()));
1135
1136
this._register(keybindingsEditor.onAcceptWhenExpression(() => this._onDidAccept.fire(this.input.getValue())));
1137
this._register(Event.any(keybindingsEditor.onRejectWhenExpression, this.input.onDidBlur)(() => this._onDidReject.fire()));
1138
}
1139
1140
layout(dimension: DOM.Dimension): void {
1141
this.input.layout(dimension);
1142
}
1143
1144
show(value: string): void {
1145
this.input.setValue(value);
1146
this.input.focus(true);
1147
}
1148
1149
}
1150
1151
interface IWhenColumnTemplateData {
1152
readonly element: HTMLElement;
1153
readonly whenLabelContainer: HTMLElement;
1154
readonly whenInputContainer: HTMLElement;
1155
readonly whenLabel: HighlightedLabel;
1156
readonly disposables: DisposableStore;
1157
}
1158
1159
class WhenColumnRenderer implements ITableRenderer<IKeybindingItemEntry, IWhenColumnTemplateData> {
1160
1161
static readonly TEMPLATE_ID = 'when';
1162
1163
readonly templateId: string = WhenColumnRenderer.TEMPLATE_ID;
1164
1165
constructor(
1166
private readonly keybindingsEditor: KeybindingsEditor,
1167
@IHoverService private readonly hoverService: IHoverService,
1168
@IInstantiationService private readonly instantiationService: IInstantiationService,
1169
) { }
1170
1171
renderTemplate(container: HTMLElement): IWhenColumnTemplateData {
1172
const element = DOM.append(container, $('.when'));
1173
1174
const whenLabelContainer = DOM.append(element, $('div.when-label'));
1175
const whenLabel = new HighlightedLabel(whenLabelContainer);
1176
1177
const whenInputContainer = DOM.append(element, $('div.when-input-container'));
1178
1179
return {
1180
element,
1181
whenLabelContainer,
1182
whenLabel,
1183
whenInputContainer,
1184
disposables: new DisposableStore(),
1185
};
1186
}
1187
1188
renderElement(keybindingItemEntry: IKeybindingItemEntry, index: number, templateData: IWhenColumnTemplateData): void {
1189
templateData.disposables.clear();
1190
const whenInputDisposables = templateData.disposables.add(new DisposableStore());
1191
templateData.disposables.add(this.keybindingsEditor.onDefineWhenExpression(e => {
1192
if (keybindingItemEntry === e) {
1193
templateData.element.classList.add('input-mode');
1194
1195
const inputWidget = whenInputDisposables.add(this.instantiationService.createInstance(WhenInputWidget, templateData.whenInputContainer, this.keybindingsEditor));
1196
inputWidget.layout(new DOM.Dimension(templateData.element.parentElement!.clientWidth, 18));
1197
inputWidget.show(keybindingItemEntry.keybindingItem.when || '');
1198
1199
const hideInputWidget = () => {
1200
whenInputDisposables.clear();
1201
templateData.element.classList.remove('input-mode');
1202
templateData.element.parentElement!.style.paddingLeft = '10px';
1203
DOM.clearNode(templateData.whenInputContainer);
1204
};
1205
1206
whenInputDisposables.add(inputWidget.onDidAccept(value => {
1207
hideInputWidget();
1208
this.keybindingsEditor.updateKeybinding(keybindingItemEntry, keybindingItemEntry.keybindingItem.keybinding ? keybindingItemEntry.keybindingItem.keybinding.getUserSettingsLabel() || '' : '', value);
1209
this.keybindingsEditor.selectKeybinding(keybindingItemEntry);
1210
}));
1211
1212
whenInputDisposables.add(inputWidget.onDidReject(() => {
1213
hideInputWidget();
1214
this.keybindingsEditor.selectKeybinding(keybindingItemEntry);
1215
}));
1216
1217
templateData.element.parentElement!.style.paddingLeft = '0px';
1218
}
1219
}));
1220
1221
templateData.whenLabelContainer.classList.toggle('code', !!keybindingItemEntry.keybindingItem.when);
1222
templateData.whenLabelContainer.classList.toggle('empty', !keybindingItemEntry.keybindingItem.when);
1223
1224
if (keybindingItemEntry.keybindingItem.when) {
1225
templateData.whenLabel.set(keybindingItemEntry.keybindingItem.when, keybindingItemEntry.whenMatches, keybindingItemEntry.keybindingItem.when);
1226
templateData.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), templateData.element, keybindingItemEntry.keybindingItem.when));
1227
} else {
1228
templateData.whenLabel.set('-');
1229
}
1230
}
1231
1232
disposeTemplate(templateData: IWhenColumnTemplateData): void {
1233
templateData.disposables.dispose();
1234
templateData.whenLabel.dispose();
1235
}
1236
}
1237
1238
class AccessibilityProvider implements IListAccessibilityProvider<IKeybindingItemEntry> {
1239
1240
constructor(private readonly configurationService: IConfigurationService) { }
1241
1242
getWidgetAriaLabel(): string {
1243
return localize('keybindingsLabel', "Keybindings");
1244
}
1245
1246
getAriaLabel({ keybindingItem }: IKeybindingItemEntry): string {
1247
const ariaLabel = [
1248
keybindingItem.commandLabel ? keybindingItem.commandLabel : keybindingItem.command,
1249
keybindingItem.keybinding?.getAriaLabel() || localize('noKeybinding', "No keybinding assigned"),
1250
keybindingItem.when ? keybindingItem.when : localize('noWhen', "No when context"),
1251
isString(keybindingItem.source) ? keybindingItem.source : keybindingItem.source.description ?? keybindingItem.source.identifier.value,
1252
];
1253
if (this.configurationService.getValue(AccessibilityVerbositySettingId.KeybindingsEditor)) {
1254
const kbEditorAriaLabel = localize('keyboard shortcuts aria label', "use space or enter to change the keybinding.");
1255
ariaLabel.push(kbEditorAriaLabel);
1256
}
1257
return ariaLabel.join(', ');
1258
}
1259
}
1260
1261
registerColor('keybindingTable.headerBackground', tableOddRowsBackgroundColor, 'Background color for the keyboard shortcuts table header.');
1262
registerColor('keybindingTable.rowsBackground', tableOddRowsBackgroundColor, 'Background color for the keyboard shortcuts table alternating rows.');
1263
1264
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
1265
const foregroundColor = theme.getColor(foreground);
1266
if (foregroundColor) {
1267
const whenForegroundColor = foregroundColor.transparent(.8).makeOpaque(WORKBENCH_BACKGROUND(theme));
1268
collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColor}; }`);
1269
}
1270
1271
const listActiveSelectionForegroundColor = theme.getColor(listActiveSelectionForeground);
1272
const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground);
1273
if (listActiveSelectionForegroundColor && listActiveSelectionBackgroundColor) {
1274
const whenForegroundColor = listActiveSelectionForegroundColor.transparent(.8).makeOpaque(listActiveSelectionBackgroundColor);
1275
collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table.focused .monaco-list-row.selected .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColor}; }`);
1276
}
1277
1278
const listInactiveSelectionForegroundColor = theme.getColor(listInactiveSelectionForeground);
1279
const listInactiveSelectionBackgroundColor = theme.getColor(listInactiveSelectionBackground);
1280
if (listInactiveSelectionForegroundColor && listInactiveSelectionBackgroundColor) {
1281
const whenForegroundColor = listInactiveSelectionForegroundColor.transparent(.8).makeOpaque(listInactiveSelectionBackgroundColor);
1282
collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list-row.selected .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColor}; }`);
1283
}
1284
1285
const listFocusForegroundColor = theme.getColor(listFocusForeground);
1286
const listFocusBackgroundColor = theme.getColor(listFocusBackground);
1287
if (listFocusForegroundColor && listFocusBackgroundColor) {
1288
const whenForegroundColor = listFocusForegroundColor.transparent(.8).makeOpaque(listFocusBackgroundColor);
1289
collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table.focused .monaco-list-row.focused .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColor}; }`);
1290
}
1291
1292
const listHoverForegroundColor = theme.getColor(listHoverForeground);
1293
const listHoverBackgroundColor = theme.getColor(listHoverBackground);
1294
if (listHoverForegroundColor && listHoverBackgroundColor) {
1295
const whenForegroundColor = listHoverForegroundColor.transparent(.8).makeOpaque(listHoverBackgroundColor);
1296
collector.addRule(`.keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table.focused .monaco-list-row:hover:not(.focused):not(.selected) .monaco-table-tr .monaco-table-td .code { color: ${whenForegroundColor}; }`);
1297
}
1298
});
1299
1300