@theia/keymaps 1.42.0 → 1.43.0
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.
- package/lib/browser/keybindings-widget.d.ts +40 -4
- package/lib/browser/keybindings-widget.d.ts.map +1 -1
- package/lib/browser/keybindings-widget.js +164 -16
- package/lib/browser/keybindings-widget.js.map +1 -1
- package/lib/browser/keymaps-frontend-contribution.d.ts +14 -1
- package/lib/browser/keymaps-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/keymaps-frontend-contribution.js +157 -2
- package/lib/browser/keymaps-frontend-contribution.js.map +1 -1
- package/lib/browser/keymaps-service.d.ts +14 -1
- package/lib/browser/keymaps-service.d.ts.map +1 -1
- package/lib/browser/keymaps-service.js +47 -45
- package/lib/browser/keymaps-service.js.map +1 -1
- package/package.json +7 -7
- package/src/browser/keybindings-widget.tsx +174 -21
- package/src/browser/keymaps-frontend-contribution.ts +161 -3
- package/src/browser/keymaps-service.ts +47 -47
- package/src/browser/style/index.css +10 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
matched = this.
|
|
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
|
-
|
|
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={
|
|
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
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
194
|
+
menus.registerMenuAction(CommonMenus.MANAGE_SETTINGS, {
|
|
98
195
|
commandId: KeymapsCommands.OPEN_KEYMAPS.id,
|
|
99
196
|
label: nls.localizeByDefault('Keyboard Shortcuts'),
|
|
100
|
-
order: '
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
*
|
|
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> {
|