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