@theia/keymaps 1.42.1 → 1.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,14 +20,15 @@ import * as fuzzy from '@theia/core/shared/fuzzy';
20
20
  import { injectable, inject, postConstruct, unmanaged } from '@theia/core/shared/inversify';
21
21
  import { Emitter, Event } from '@theia/core/lib/common/event';
22
22
  import { CommandRegistry, Command } from '@theia/core/lib/common/command';
23
+ import { Keybinding } from '@theia/core/lib/common/keybinding';
23
24
  import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
24
25
  import {
25
26
  KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope,
26
- SingleTextInputDialogProps, Key, ScopedKeybinding, codicon, StatefulWidget, Widget
27
+ SingleTextInputDialogProps, Key, ScopedKeybinding, codicon, StatefulWidget, Widget, ContextMenuRenderer, SELECTED_CLASS
27
28
  } from '@theia/core/lib/browser';
28
29
  import { KeymapsService } from './keymaps-service';
29
30
  import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
30
- import { DisposableCollection, isOSX } from '@theia/core';
31
+ import { DisposableCollection, isOSX, isObject } from '@theia/core';
31
32
  import { nls } from '@theia/core/lib/common/nls';
32
33
 
33
34
  /**
@@ -47,6 +48,19 @@ export interface KeybindingItem {
47
48
  visible?: boolean;
48
49
  }
49
50
 
51
+ export namespace KeybindingItem {
52
+ export function is(arg: unknown): arg is KeybindingItem {
53
+ return isObject(arg) && 'command' in arg && 'labels' in arg;
54
+ }
55
+
56
+ export function keybinding(item: KeybindingItem): Keybinding {
57
+ return item.keybinding ?? {
58
+ command: item.command.id,
59
+ keybinding: ''
60
+ };
61
+ }
62
+ }
63
+
50
64
  export interface RenderableLabel {
51
65
  readonly value: string;
52
66
  segments?: RenderableStringSegment[];
@@ -84,8 +98,17 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
84
98
  @inject(KeymapsService)
85
99
  protected readonly keymapsService: KeymapsService;
86
100
 
101
+ @inject(ContextMenuRenderer)
102
+ protected readonly contextMenuRenderer: ContextMenuRenderer;
103
+
87
104
  static readonly ID = 'keybindings.view.widget';
88
105
  static readonly LABEL = nls.localizeByDefault('Keyboard Shortcuts');
106
+ static readonly CONTEXT_MENU = ['keybinding-context-menu'];
107
+ static readonly COPY_MENU = [...KeybindingWidget.CONTEXT_MENU, 'a_copy'];
108
+ static readonly EDIT_MENU = [...KeybindingWidget.CONTEXT_MENU, 'b_edit'];
109
+ static readonly ADD_MENU = [...KeybindingWidget.CONTEXT_MENU, 'c_add'];
110
+ static readonly REMOVE_MENU = [...KeybindingWidget.CONTEXT_MENU, 'd_remove'];
111
+ static readonly SHOW_MENU = [...KeybindingWidget.CONTEXT_MENU, 'e_show'];
89
112
 
90
113
  /**
91
114
  * The list of all available keybindings.
@@ -164,14 +187,14 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
164
187
  * Determine if there currently is a search term.
165
188
  * @returns `true` if a search term is present.
166
189
  */
167
- public hasSearch(): boolean {
190
+ hasSearch(): boolean {
168
191
  return !!this.query.length;
169
192
  }
170
193
 
171
194
  /**
172
195
  * Clear the search and reset the view.
173
196
  */
174
- public clearSearch(): void {
197
+ clearSearch(): void {
175
198
  const search = this.findSearchField();
176
199
  if (search) {
177
200
  search.value = '';
@@ -180,6 +203,23 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
180
203
  }
181
204
  }
182
205
 
206
+ /**
207
+ * Show keybinding items with the same key sequence as the given item.
208
+ * @param item the keybinding item
209
+ */
210
+ showSameKeybindings(item: KeybindingItem): void {
211
+ const keybinding = item.keybinding;
212
+ if (keybinding) {
213
+ const search = this.findSearchField();
214
+ if (search) {
215
+ const query = `"${this.keybindingRegistry.acceleratorFor(keybinding, '+', true).join(' ')}"`;
216
+ search.value = query;
217
+ this.query = query;
218
+ this.doSearchKeybindings();
219
+ }
220
+ }
221
+ }
222
+
183
223
  protected override onActivateRequest(msg: Message): void {
184
224
  super.onActivateRequest(msg);
185
225
  this.focusInputField();
@@ -192,13 +232,27 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
192
232
  this.onDidUpdateEmitter.fire(undefined);
193
233
  const searchField = this.findSearchField();
194
234
  this.query = searchField ? searchField.value.trim().toLocaleLowerCase() : '';
195
- const queryItems = this.query.split(/[+\s]/);
235
+ let query = this.query;
236
+ const startsWithQuote = query.startsWith('"');
237
+ const endsWithQuote = query.endsWith('"');
238
+ const matchKeybindingOnly = startsWithQuote && endsWithQuote;
239
+ if (startsWithQuote) {
240
+ query = query.slice(1);
241
+ }
242
+ if (endsWithQuote) {
243
+ query = query.slice(0, -1);
244
+ }
245
+ const queryItems = query.split(/[+\s]/);
196
246
  this.items.forEach(item => {
197
247
  let matched = !this.query;
198
- matched = this.formatAndMatchCommand(item) || matched;
199
- matched = this.formatAndMatchKeybinding(item, queryItems) || matched;
200
- matched = this.formatAndMatchContext(item) || matched;
201
- matched = this.formatAndMatchSource(item) || matched;
248
+ if (!matchKeybindingOnly) {
249
+ matched = this.formatAndMatchCommand(item) || matched;
250
+ }
251
+ matched = this.formatAndMatchKeybinding(item, queryItems, matchKeybindingOnly) || matched;
252
+ if (!matchKeybindingOnly) {
253
+ matched = this.formatAndMatchContext(item) || matched;
254
+ matched = this.formatAndMatchSource(item) || matched;
255
+ }
202
256
  item.visible = matched;
203
257
  });
204
258
  this.update();
@@ -209,7 +263,7 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
209
263
  return Boolean(item.labels.command.segments);
210
264
  }
211
265
 
212
- protected formatAndMatchKeybinding(item: KeybindingItem, queryItems: string[]): boolean {
266
+ protected formatAndMatchKeybinding(item: KeybindingItem, queryItems: string[], exactMatch?: boolean): boolean {
213
267
  if (item.keybinding) {
214
268
  const unmatchedTerms = queryItems.filter(Boolean);
215
269
  const segments = this.keybindingRegistry.resolveKeybinding(item.keybinding).reduce<RenderableStringSegment[]>((collection, code, codeIndex) => {
@@ -232,7 +286,13 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
232
286
  return collection;
233
287
  }, []);
234
288
  item.labels.keybinding = { value: item.labels.keybinding.value, segments };
235
- return !unmatchedTerms.length;
289
+ if (unmatchedTerms.length) {
290
+ return false;
291
+ }
292
+ if (exactMatch) {
293
+ return !segments.some(segment => segment.key && !segment.match);
294
+ }
295
+ return true;
236
296
  }
237
297
  item.labels.keybinding = { value: '' };
238
298
  return false;
@@ -364,7 +424,9 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
364
424
  protected renderRow(item: KeybindingItem, index: number): React.ReactNode {
365
425
  const { command, keybinding } = item;
366
426
  // TODO get rid of array functions in event handlers
367
- return <tr className='kb-item-row' key={index} onDoubleClick={() => this.editKeybinding(item)}>
427
+ return <tr className='kb-item-row' key={index} onDoubleClick={event => this.handleItemDoubleClick(item, index, event)}
428
+ onClick={event => this.handleItemClick(item, index, event)}
429
+ onContextMenu={event => this.handleItemContextMenu(item, index, event)}>
368
430
  <td className='kb-actions'>
369
431
  {this.renderActions(item)}
370
432
  </td>
@@ -383,6 +445,37 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
383
445
  </tr>;
384
446
  }
385
447
 
448
+ protected handleItemClick(item: KeybindingItem, index: number, event: React.MouseEvent<HTMLElement>): void {
449
+ event.preventDefault();
450
+ this.selectItem(item, index, event.currentTarget);
451
+ }
452
+
453
+ protected handleItemDoubleClick(item: KeybindingItem, index: number, event: React.MouseEvent<HTMLElement>): void {
454
+ event.preventDefault();
455
+ this.selectItem(item, index, event.currentTarget);
456
+ this.editKeybinding(item);
457
+ }
458
+
459
+ protected handleItemContextMenu(item: KeybindingItem, index: number, event: React.MouseEvent<HTMLElement>): void {
460
+ event.preventDefault();
461
+ this.selectItem(item, index, event.currentTarget);
462
+ this.contextMenuRenderer.render({
463
+ menuPath: KeybindingWidget.CONTEXT_MENU,
464
+ anchor: event.nativeEvent,
465
+ args: [item, this]
466
+ });
467
+ }
468
+
469
+ protected selectItem(item: KeybindingItem, index: number, element: HTMLElement): void {
470
+ if (!element.classList.contains(SELECTED_CLASS)) {
471
+ const selected = element.parentElement?.getElementsByClassName(SELECTED_CLASS)[0];
472
+ if (selected) {
473
+ selected.classList.remove(SELECTED_CLASS);
474
+ }
475
+ element.classList.add(SELECTED_CLASS);
476
+ }
477
+ }
478
+
386
479
  /**
387
480
  * Render the actions container with action icons.
388
481
  * @param item the keybinding item for the row.
@@ -408,7 +501,7 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
408
501
  * @param item the keybinding item for the row.
409
502
  */
410
503
  protected renderReset(item: KeybindingItem): React.ReactNode {
411
- return (item.keybinding && item.keybinding.scope === KeybindingScope.USER)
504
+ return this.canResetKeybinding(item)
412
505
  ? <a title='Reset Keybinding' href='#' onClick={e => {
413
506
  e.preventDefault();
414
507
  this.resetKeybinding(item);
@@ -572,22 +665,74 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
572
665
  * Prompt users to update the keybinding for the given command.
573
666
  * @param item the keybinding item.
574
667
  */
575
- protected editKeybinding(item: KeybindingItem): void {
668
+ editKeybinding(item: KeybindingItem): void {
576
669
  const command = item.command.id;
577
670
  const oldKeybinding = item.keybinding;
578
671
  const dialog = new EditKeybindingDialog({
579
- title: nls.localize('theia/keymaps/editKeybindingTitle', 'Edit Keybinding for {0}', command),
672
+ title: nls.localize('theia/keymaps/editKeybindingTitle', 'Edit Keybinding for {0}', item.labels.command.value),
580
673
  maxWidth: 400,
581
674
  initialValue: oldKeybinding?.keybinding,
582
675
  validate: newKeybinding => this.validateKeybinding(command, oldKeybinding?.keybinding, newKeybinding),
583
- }, this.keymapsService, item);
676
+ }, this.keymapsService, item, this.canResetKeybinding(item));
677
+ dialog.open().then(async keybinding => {
678
+ if (keybinding && keybinding !== oldKeybinding?.keybinding) {
679
+ await this.keymapsService.setKeybinding({
680
+ ...oldKeybinding,
681
+ command,
682
+ keybinding
683
+ }, oldKeybinding);
684
+ }
685
+ });
686
+ }
687
+
688
+ /**
689
+ * Prompt users to update when expression for the given keybinding.
690
+ * @param item the keybinding item
691
+ */
692
+ editWhenExpression(item: KeybindingItem): void {
693
+ const keybinding = item.keybinding;
694
+ if (!keybinding) {
695
+ return;
696
+ }
697
+ const dialog = new SingleTextInputDialog({
698
+ title: nls.localize('theia/keymaps/editWhenExpressionTitle', 'Edit When Expression for {0}', item.labels.command.value),
699
+ maxWidth: 400,
700
+ initialValue: keybinding.when
701
+ });
702
+ dialog.open().then(async when => {
703
+ if (when === undefined) {
704
+ return; // cancelled by the user
705
+ }
706
+ if (when !== (keybinding.when ?? '')) {
707
+ if (when === '') {
708
+ when = undefined;
709
+ }
710
+ await this.keymapsService.setKeybinding({
711
+ ...keybinding,
712
+ when
713
+ }, keybinding);
714
+ }
715
+ });
716
+ }
717
+
718
+ /**
719
+ * Prompt users to add a keybinding for the given command.
720
+ * @param item the keybinding item
721
+ */
722
+ addKeybinding(item: KeybindingItem): void {
723
+ const command = item.command.id;
724
+ const dialog = new SingleTextInputDialog({
725
+ title: nls.localize('theia/keymaps/addKeybindingTitle', 'Add Keybinding for {0}', item.labels.command.value),
726
+ maxWidth: 400,
727
+ validate: newKeybinding => this.validateKeybinding(command, undefined, newKeybinding),
728
+ });
584
729
  dialog.open().then(async keybinding => {
585
730
  if (keybinding) {
586
731
  await this.keymapsService.setKeybinding({
587
732
  ...item.keybinding,
588
733
  command,
589
734
  keybinding
590
- }, oldKeybinding);
735
+ }, undefined);
591
736
  }
592
737
  });
593
738
  }
@@ -618,13 +763,21 @@ export class KeybindingWidget extends ReactWidget implements StatefulWidget {
618
763
  * Reset the keybinding to its default value.
619
764
  * @param item the keybinding item.
620
765
  */
621
- protected async resetKeybinding(item: KeybindingItem): Promise<void> {
766
+ async resetKeybinding(item: KeybindingItem): Promise<void> {
622
767
  const confirmed = await this.confirmResetKeybinding(item);
623
768
  if (confirmed) {
624
769
  this.keymapsService.removeKeybinding(item.command.id);
625
770
  }
626
771
  }
627
772
 
773
+ /**
774
+ * Whether the keybinding can be reset to its default value.
775
+ * @param item the keybinding item
776
+ */
777
+ canResetKeybinding(item: KeybindingItem): boolean {
778
+ return item.keybinding?.scope === KeybindingScope.USER || this.keymapsService.hasKeybinding('-' + item.command.id);
779
+ }
780
+
628
781
  /**
629
782
  * Validate the provided keybinding value against its previous value.
630
783
  * @param command the command label.
@@ -745,12 +898,13 @@ class EditKeybindingDialog extends SingleTextInputDialog {
745
898
  constructor(
746
899
  @inject(SingleTextInputDialogProps) props: SingleTextInputDialogProps,
747
900
  @inject(KeymapsService) protected readonly keymapsService: KeymapsService,
748
- item: KeybindingItem
901
+ item: KeybindingItem,
902
+ canReset: boolean
749
903
  ) {
750
904
  super(props);
751
905
  this.item = item;
752
906
  // Add the `Reset` button if the command currently has a custom keybinding.
753
- if (this.item.keybinding && this.item.keybinding.scope === KeybindingScope.USER) {
907
+ if (canReset) {
754
908
  this.appendResetButton();
755
909
  }
756
910
  }
@@ -796,5 +950,4 @@ class EditKeybindingDialog extends SingleTextInputDialog {
796
950
  protected reset(): void {
797
951
  this.keymapsService.removeKeybinding(this.item.command.id);
798
952
  }
799
-
800
953
  }
@@ -23,10 +23,12 @@ import {
23
23
  MenuModelRegistry
24
24
  } from '@theia/core/lib/common';
25
25
  import { AbstractViewContribution, codicon, Widget } from '@theia/core/lib/browser';
26
+ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
26
27
  import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
27
28
  import { KeymapsService } from './keymaps-service';
29
+ import { Keybinding } from '@theia/core/lib/common/keybinding';
28
30
  import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
29
- import { KeybindingWidget } from './keybindings-widget';
31
+ import { KeybindingItem, KeybindingWidget } from './keybindings-widget';
30
32
  import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
31
33
  import { nls } from '@theia/core/lib/common/nls';
32
34
 
@@ -49,6 +51,51 @@ export namespace KeymapsCommands {
49
51
  id: 'keymaps.clearSearch',
50
52
  iconClass: codicon('clear-all')
51
53
  };
54
+ export const COPY_KEYBINDING = Command.toLocalizedCommand({
55
+ id: 'keymaps:keybinding.copy',
56
+ category: CommonCommands.PREFERENCES_CATEGORY,
57
+ label: 'Copy Keybinding'
58
+ }, 'theia/keymaps/keybinding/copy', CommonCommands.PREFERENCES_CATEGORY_KEY);
59
+ export const COPY_COMMAND_ID = Command.toLocalizedCommand({
60
+ id: 'keymaps:keybinding.copyCommandId',
61
+ category: CommonCommands.PREFERENCES_CATEGORY,
62
+ label: 'Copy Keybinding Command ID'
63
+ }, 'theia/keymaps/keybinding/copyCommandId', CommonCommands.PREFERENCES_CATEGORY_KEY);
64
+ export const COPY_COMMAND_TITLE = Command.toLocalizedCommand({
65
+ id: 'keymaps:keybinding.copyCommandTitle',
66
+ category: CommonCommands.PREFERENCES_CATEGORY,
67
+ label: 'Copy Keybinding Command Title'
68
+ }, 'theia/keymaps/keybinding/copyCommandTitle', CommonCommands.PREFERENCES_CATEGORY_KEY);
69
+ export const EDIT_KEYBINDING = Command.toLocalizedCommand({
70
+ id: 'keymaps:keybinding.edit',
71
+ category: CommonCommands.PREFERENCES_CATEGORY,
72
+ label: 'Edit Keybinding...'
73
+ }, 'theia/keymaps/keybinding/edit', CommonCommands.PREFERENCES_CATEGORY_KEY);
74
+ export const EDIT_WHEN_EXPRESSION = Command.toLocalizedCommand({
75
+ id: 'keymaps:keybinding.editWhenExpression',
76
+ category: CommonCommands.PREFERENCES_CATEGORY,
77
+ label: 'Edit Keybinding When Expression...'
78
+ }, 'theia/keymaps/keybinding/editWhenExpression', CommonCommands.PREFERENCES_CATEGORY_KEY);
79
+ export const ADD_KEYBINDING = Command.toDefaultLocalizedCommand({
80
+ id: 'keymaps:keybinding.add',
81
+ category: CommonCommands.PREFERENCES_CATEGORY,
82
+ label: 'Add Keybinding...'
83
+ });
84
+ export const REMOVE_KEYBINDING = Command.toDefaultLocalizedCommand({
85
+ id: 'keymaps:keybinding.remove',
86
+ category: CommonCommands.PREFERENCES_CATEGORY,
87
+ label: 'Remove Keybinding'
88
+ });
89
+ export const RESET_KEYBINDING = Command.toDefaultLocalizedCommand({
90
+ id: 'keymaps:keybinding.reset',
91
+ category: CommonCommands.PREFERENCES_CATEGORY,
92
+ label: 'Reset Keybinding'
93
+ });
94
+ export const SHOW_SAME = Command.toDefaultLocalizedCommand({
95
+ id: 'keymaps:keybinding.showSame',
96
+ category: CommonCommands.PREFERENCES_CATEGORY,
97
+ label: 'Show Same Keybindings'
98
+ });
52
99
  }
53
100
 
54
101
  @injectable()
@@ -57,6 +104,9 @@ export class KeymapsFrontendContribution extends AbstractViewContribution<Keybin
57
104
  @inject(KeymapsService)
58
105
  protected readonly keymaps: KeymapsService;
59
106
 
107
+ @inject(ClipboardService)
108
+ protected readonly clipboard: ClipboardService;
109
+
60
110
  constructor() {
61
111
  super({
62
112
  widgetId: KeybindingWidget.ID,
@@ -86,6 +136,53 @@ export class KeymapsFrontendContribution extends AbstractViewContribution<Keybin
86
136
  isVisible: w => this.withWidget(w, () => true),
87
137
  execute: w => this.withWidget(w, widget => widget.clearSearch()),
88
138
  });
139
+ commands.registerCommand(KeymapsCommands.COPY_KEYBINDING, {
140
+ isEnabled: (...args) => this.withItem(() => true, ...args),
141
+ isVisible: (...args) => this.withItem(() => true, ...args),
142
+ execute: (...args) => this.withItem(item => this.clipboard.writeText(
143
+ JSON.stringify(Keybinding.apiObjectify(KeybindingItem.keybinding(item)), undefined, ' ')
144
+ ), ...args)
145
+ });
146
+ commands.registerCommand(KeymapsCommands.COPY_COMMAND_ID, {
147
+ isEnabled: (...args) => this.withItem(() => true, ...args),
148
+ isVisible: (...args) => this.withItem(() => true, ...args),
149
+ execute: (...args) => this.withItem(item => this.clipboard.writeText(item.command.id), ...args)
150
+ });
151
+ commands.registerCommand(KeymapsCommands.COPY_COMMAND_TITLE, {
152
+ isEnabled: (...args) => this.withItem(item => !!item.command.label, ...args),
153
+ isVisible: (...args) => this.withItem(() => true, ...args),
154
+ execute: (...args) => this.withItem(item => this.clipboard.writeText(item.command.label!), ...args)
155
+ });
156
+ commands.registerCommand(KeymapsCommands.EDIT_KEYBINDING, {
157
+ isEnabled: (...args) => this.withWidgetItem(() => true, ...args),
158
+ isVisible: (...args) => this.withWidgetItem(() => true, ...args),
159
+ execute: (...args) => this.withWidgetItem((item, widget) => widget.editKeybinding(item), ...args)
160
+ });
161
+ commands.registerCommand(KeymapsCommands.EDIT_WHEN_EXPRESSION, {
162
+ isEnabled: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
163
+ isVisible: (...args) => this.withWidgetItem(() => true, ...args),
164
+ execute: (...args) => this.withWidgetItem((item, widget) => widget.editWhenExpression(item), ...args)
165
+ });
166
+ commands.registerCommand(KeymapsCommands.ADD_KEYBINDING, {
167
+ isEnabled: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
168
+ isVisible: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
169
+ execute: (...args) => this.withWidgetItem((item, widget) => widget.addKeybinding(item), ...args)
170
+ });
171
+ commands.registerCommand(KeymapsCommands.REMOVE_KEYBINDING, {
172
+ isEnabled: (...args) => this.withItem(item => !!item.keybinding, ...args),
173
+ isVisible: (...args) => this.withItem(() => true, ...args),
174
+ execute: (...args) => this.withItem(item => this.keymaps.unsetKeybinding(item.keybinding!), ...args)
175
+ });
176
+ commands.registerCommand(KeymapsCommands.RESET_KEYBINDING, {
177
+ isEnabled: (...args) => this.withWidgetItem((item, widget) => widget.canResetKeybinding(item), ...args),
178
+ isVisible: (...args) => this.withWidgetItem(() => true, ...args),
179
+ execute: (...args) => this.withWidgetItem((item, widget) => widget.resetKeybinding(item), ...args)
180
+ });
181
+ commands.registerCommand(KeymapsCommands.SHOW_SAME, {
182
+ isEnabled: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
183
+ isVisible: (...args) => this.withWidgetItem(() => true, ...args),
184
+ execute: (...args) => this.withWidgetItem((item, widget) => widget.showSameKeybindings(item), ...args)
185
+ });
89
186
  }
90
187
 
91
188
  override registerMenus(menus: MenuModelRegistry): void {
@@ -94,10 +191,55 @@ export class KeymapsFrontendContribution extends AbstractViewContribution<Keybin
94
191
  label: nls.localizeByDefault('Keyboard Shortcuts'),
95
192
  order: 'a20'
96
193
  });
97
- menus.registerMenuAction(CommonMenus.SETTINGS_OPEN, {
194
+ menus.registerMenuAction(CommonMenus.MANAGE_SETTINGS, {
98
195
  commandId: KeymapsCommands.OPEN_KEYMAPS.id,
99
196
  label: nls.localizeByDefault('Keyboard Shortcuts'),
100
- order: 'a20'
197
+ order: 'a30'
198
+ });
199
+ menus.registerMenuAction(KeybindingWidget.COPY_MENU, {
200
+ commandId: KeymapsCommands.COPY_KEYBINDING.id,
201
+ label: nls.localizeByDefault('Copy'),
202
+ order: 'a'
203
+ });
204
+ menus.registerMenuAction(KeybindingWidget.COPY_MENU, {
205
+ commandId: KeymapsCommands.COPY_COMMAND_ID.id,
206
+ label: nls.localizeByDefault('Copy Command ID'),
207
+ order: 'b'
208
+ });
209
+ menus.registerMenuAction(KeybindingWidget.COPY_MENU, {
210
+ commandId: KeymapsCommands.COPY_COMMAND_TITLE.id,
211
+ label: nls.localizeByDefault('Copy Command Title'),
212
+ order: 'c'
213
+ });
214
+ menus.registerMenuAction(KeybindingWidget.EDIT_MENU, {
215
+ commandId: KeymapsCommands.EDIT_KEYBINDING.id,
216
+ label: nls.localize('theia/keymaps/editKeybinding', 'Edit Keybinding...'),
217
+ order: 'a'
218
+ });
219
+ menus.registerMenuAction(KeybindingWidget.EDIT_MENU, {
220
+ commandId: KeymapsCommands.EDIT_WHEN_EXPRESSION.id,
221
+ label: nls.localize('theia/keymaps/editWhenExpression', 'Edit When Expression...'),
222
+ order: 'b'
223
+ });
224
+ menus.registerMenuAction(KeybindingWidget.ADD_MENU, {
225
+ commandId: KeymapsCommands.ADD_KEYBINDING.id,
226
+ label: nls.localizeByDefault('Add Keybinding...'),
227
+ order: 'a'
228
+ });
229
+ menus.registerMenuAction(KeybindingWidget.REMOVE_MENU, {
230
+ commandId: KeymapsCommands.REMOVE_KEYBINDING.id,
231
+ label: nls.localizeByDefault('Remove Keybinding'),
232
+ order: 'a'
233
+ });
234
+ menus.registerMenuAction(KeybindingWidget.REMOVE_MENU, {
235
+ commandId: KeymapsCommands.RESET_KEYBINDING.id,
236
+ label: nls.localizeByDefault('Reset Keybinding'),
237
+ order: 'b'
238
+ });
239
+ menus.registerMenuAction(KeybindingWidget.SHOW_MENU, {
240
+ commandId: KeymapsCommands.SHOW_SAME.id,
241
+ label: nls.localizeByDefault('Show Same Keybindings'),
242
+ order: 'a'
101
243
  });
102
244
  }
103
245
 
@@ -135,4 +277,20 @@ export class KeymapsFrontendContribution extends AbstractViewContribution<Keybin
135
277
  }
136
278
  return false;
137
279
  }
280
+
281
+ protected withItem<T>(fn: (item: KeybindingItem, ...rest: unknown[]) => T, ...args: unknown[]): T | false {
282
+ const [item] = args;
283
+ if (KeybindingItem.is(item)) {
284
+ return fn(item, args.slice(1));
285
+ }
286
+ return false;
287
+ }
288
+
289
+ protected withWidgetItem<T>(fn: (item: KeybindingItem, widget: KeybindingWidget, ...rest: unknown[]) => T, ...args: unknown[]): T | false {
290
+ const [item, widget] = args;
291
+ if (widget instanceof KeybindingWidget && widget.id === KeybindingWidget.ID && KeybindingItem.is(item)) {
292
+ return fn(item, widget, args.slice(2));
293
+ }
294
+ return false;
295
+ }
138
296
  }
@@ -128,62 +128,62 @@ export class KeymapsService {
128
128
  */
129
129
  async setKeybinding(newKeybinding: Keybinding, oldKeybinding: ScopedKeybinding | undefined): Promise<void> {
130
130
  return this.updateKeymap(() => {
131
- let newAdded = false;
132
- let isOldKeybindingDisabled = false;
133
- let addedDisabledEntry = false;
134
- const keybindings = [];
135
- for (let keybinding of this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER)) {
136
- // search for the old keybinding and modify it
137
- if (oldKeybinding && Keybinding.equals(keybinding, oldKeybinding, false, true)) {
138
- newAdded = true;
139
- keybinding = {
140
- ...keybinding,
141
- keybinding: newKeybinding.keybinding
142
- };
143
- }
144
-
145
- // we have an disabled entry for the same command and the oldKeybinding
146
- if (oldKeybinding?.keybinding &&
147
- Keybinding.equals(keybinding, { ...newKeybinding, keybinding: oldKeybinding.keybinding, command: '-' + newKeybinding.command }, false, true)) {
148
- isOldKeybindingDisabled = true;
149
- }
150
- keybindings.push(keybinding);
151
- }
152
- if (!newAdded) {
153
- keybindings.push({
154
- command: newKeybinding.command,
155
- keybinding: newKeybinding.keybinding,
156
- context: newKeybinding.context,
157
- when: newKeybinding.when,
158
- args: newKeybinding.args
159
- });
160
- newAdded = true;
131
+ const keybindings: Keybinding[] = [...this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER)];
132
+ if (!oldKeybinding) {
133
+ Keybinding.addKeybinding(keybindings, newKeybinding);
134
+ return keybindings;
135
+ } else if (oldKeybinding.scope === KeybindingScope.DEFAULT) {
136
+ Keybinding.addKeybinding(keybindings, newKeybinding);
137
+ const disabledBinding = {
138
+ ...oldKeybinding,
139
+ command: '-' + oldKeybinding.command
140
+ };
141
+ Keybinding.addKeybinding(keybindings, disabledBinding);
142
+ return keybindings;
143
+ } else if (Keybinding.replaceKeybinding(keybindings, oldKeybinding, newKeybinding)) {
144
+ return keybindings;
161
145
  }
162
- // we want to add a disabled entry for the old keybinding only when we are modifying the default value
163
- if (!isOldKeybindingDisabled && oldKeybinding?.scope === KeybindingScope.DEFAULT) {
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Unset the given keybinding in the JSON.
151
+ * If the given keybinding has a default scope, it will be disabled in the JSON.
152
+ * Otherwise, it will be removed from the JSON.
153
+ * @param keybinding the keybinding to unset
154
+ */
155
+ unsetKeybinding(keybinding: ScopedKeybinding): Promise<void> {
156
+ return this.updateKeymap(() => {
157
+ const keybindings = this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER);
158
+ if (keybinding.scope === KeybindingScope.DEFAULT) {
159
+ const result: Keybinding[] = [...keybindings];
164
160
  const disabledBinding = {
165
- command: '-' + newKeybinding.command,
166
- // TODO key: oldKeybinding, see https://github.com/eclipse-theia/theia/issues/6879
167
- keybinding: oldKeybinding.keybinding,
168
- context: newKeybinding.context,
169
- when: newKeybinding.when,
170
- args: newKeybinding.args
161
+ ...keybinding,
162
+ command: '-' + keybinding.command
171
163
  };
172
- // Add disablement of the old keybinding if it isn't already disabled in the list to avoid duplicate disabled entries
173
- if (!keybindings.some(binding => Keybinding.equals(binding, disabledBinding, false, true))) {
174
- keybindings.push(disabledBinding);
164
+ Keybinding.addKeybinding(result, disabledBinding);
165
+ return result;
166
+ } else {
167
+ const filtered = keybindings.filter(a => !Keybinding.equals(a, keybinding, false, true));
168
+ if (filtered.length !== keybindings.length) {
169
+ return filtered;
175
170
  }
176
- isOldKeybindingDisabled = true;
177
- addedDisabledEntry = true;
178
- }
179
- if (newAdded || addedDisabledEntry) {
180
- return keybindings;
181
171
  }
182
172
  });
183
173
  }
184
174
 
185
175
  /**
186
- * Remove the given keybinding with the given command id from the JSON.
176
+ * Whether there is a keybinding with the given command id in the JSON.
177
+ * @param commandId the keybinding command id
178
+ */
179
+ hasKeybinding(commandId: string): boolean {
180
+ const keybindings = this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER);
181
+ return keybindings.some(a => a.command === commandId);
182
+ }
183
+
184
+ /**
185
+ * Remove the keybindings with the given command id from the JSON.
186
+ * This includes disabled keybindings.
187
187
  * @param commandId the keybinding command id.
188
188
  */
189
189
  removeKeybinding(commandId: string): Promise<void> {