@theia/keymaps 1.45.1 → 1.46.0-next.72
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/README.md +61 -61
- package/lib/browser/index.d.ts +3 -3
- package/lib/browser/index.js +30 -30
- package/lib/browser/keybinding-schema-updater.d.ts +85 -85
- package/lib/browser/keybinding-schema-updater.js +116 -116
- package/lib/browser/keybindings-widget.d.ts +275 -275
- package/lib/browser/keybindings-widget.js +824 -824
- package/lib/browser/keymaps-frontend-contribution.d.ts +37 -37
- package/lib/browser/keymaps-frontend-contribution.js +294 -294
- package/lib/browser/keymaps-frontend-module.d.ts +5 -5
- package/lib/browser/keymaps-frontend-module.js +44 -44
- package/lib/browser/keymaps-monaco-contribution.d.ts +1 -1
- package/lib/browser/keymaps-monaco-contribution.js +27 -27
- package/lib/browser/keymaps-service.d.ts +61 -61
- package/lib/browser/keymaps-service.js +231 -231
- package/lib/package.spec.js +25 -25
- package/package.json +8 -8
- package/src/browser/index.ts +19 -19
- package/src/browser/keybinding-schema-updater.ts +95 -95
- package/src/browser/keybindings-widget.tsx +953 -953
- package/src/browser/keymaps-frontend-contribution.ts +296 -296
- package/src/browser/keymaps-frontend-module.ts +44 -44
- package/src/browser/keymaps-monaco-contribution.ts +26 -26
- package/src/browser/keymaps-service.ts +214 -214
- package/src/browser/style/index.css +182 -182
- package/src/package.spec.ts +28 -28
|
@@ -1,953 +1,953 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2018 Ericsson and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import React = require('@theia/core/shared/react');
|
|
18
|
-
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
19
|
-
import * as fuzzy from '@theia/core/shared/fuzzy';
|
|
20
|
-
import { injectable, inject, postConstruct, unmanaged } from '@theia/core/shared/inversify';
|
|
21
|
-
import { Emitter, Event } from '@theia/core/lib/common/event';
|
|
22
|
-
import { CommandRegistry, Command } from '@theia/core/lib/common/command';
|
|
23
|
-
import { Keybinding } from '@theia/core/lib/common/keybinding';
|
|
24
|
-
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
|
25
|
-
import {
|
|
26
|
-
KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope,
|
|
27
|
-
SingleTextInputDialogProps, Key, ScopedKeybinding, codicon, StatefulWidget, Widget, ContextMenuRenderer, SELECTED_CLASS
|
|
28
|
-
} from '@theia/core/lib/browser';
|
|
29
|
-
import { KeymapsService } from './keymaps-service';
|
|
30
|
-
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
|
|
31
|
-
import { DisposableCollection, isOSX, isObject } from '@theia/core';
|
|
32
|
-
import { nls } from '@theia/core/lib/common/nls';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Representation of a keybinding item for the view.
|
|
36
|
-
*/
|
|
37
|
-
export interface KeybindingItem {
|
|
38
|
-
command: Command
|
|
39
|
-
keybinding?: ScopedKeybinding
|
|
40
|
-
/** human-readable labels can contain highlighting */
|
|
41
|
-
labels: {
|
|
42
|
-
id: RenderableLabel;
|
|
43
|
-
command: RenderableLabel;
|
|
44
|
-
keybinding: RenderableLabel;
|
|
45
|
-
context: RenderableLabel;
|
|
46
|
-
source: RenderableLabel;
|
|
47
|
-
}
|
|
48
|
-
visible?: boolean;
|
|
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
|
-
|
|
64
|
-
export interface RenderableLabel {
|
|
65
|
-
readonly value: string;
|
|
66
|
-
segments?: RenderableStringSegment[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface RenderableStringSegment {
|
|
70
|
-
value: string;
|
|
71
|
-
match: boolean;
|
|
72
|
-
key?: boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Representation of an individual table cell.
|
|
77
|
-
*/
|
|
78
|
-
export interface CellData {
|
|
79
|
-
/**
|
|
80
|
-
* The cell value.
|
|
81
|
-
*/
|
|
82
|
-
value: string,
|
|
83
|
-
/**
|
|
84
|
-
* Indicates if a cell's value is currently highlighted.
|
|
85
|
-
*/
|
|
86
|
-
highlighted: boolean,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
@injectable()
|
|
90
|
-
export class KeybindingWidget extends ReactWidget implements StatefulWidget {
|
|
91
|
-
|
|
92
|
-
@inject(CommandRegistry)
|
|
93
|
-
protected readonly commandRegistry: CommandRegistry;
|
|
94
|
-
|
|
95
|
-
@inject(KeybindingRegistry)
|
|
96
|
-
protected readonly keybindingRegistry: KeybindingRegistry;
|
|
97
|
-
|
|
98
|
-
@inject(KeymapsService)
|
|
99
|
-
protected readonly keymapsService: KeymapsService;
|
|
100
|
-
|
|
101
|
-
@inject(ContextMenuRenderer)
|
|
102
|
-
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
103
|
-
|
|
104
|
-
static readonly ID = 'keybindings.view.widget';
|
|
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'];
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* The list of all available keybindings.
|
|
115
|
-
*/
|
|
116
|
-
protected items: KeybindingItem[] = [];
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The current user search query.
|
|
120
|
-
*/
|
|
121
|
-
protected query: string = '';
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* The regular expression used to extract values between fuzzy results.
|
|
125
|
-
*/
|
|
126
|
-
protected readonly regexp = /<match>(.*?)<\/match>/g;
|
|
127
|
-
/**
|
|
128
|
-
* The regular expression used to extract values between the keybinding separator.
|
|
129
|
-
*/
|
|
130
|
-
protected readonly keybindingSeparator = /<match>\+<\/match>/g;
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* The fuzzy search options.
|
|
134
|
-
* The `pre` and `post` options are used to wrap fuzzy matches.
|
|
135
|
-
*/
|
|
136
|
-
protected readonly fuzzyOptions = {
|
|
137
|
-
pre: '<match>',
|
|
138
|
-
post: '</match>',
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
protected readonly onDidUpdateEmitter = new Emitter<void>();
|
|
142
|
-
readonly onDidUpdate: Event<void> = this.onDidUpdateEmitter.event;
|
|
143
|
-
protected readonly onRenderCallbacks = new DisposableCollection();
|
|
144
|
-
protected onRender = () => this.onRenderCallbacks.dispose();
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Search keybindings.
|
|
148
|
-
*/
|
|
149
|
-
protected readonly searchKeybindings: () => void = debounce(() => this.doSearchKeybindings(), 50);
|
|
150
|
-
|
|
151
|
-
constructor(@unmanaged() options?: Widget.IOptions) {
|
|
152
|
-
super(options);
|
|
153
|
-
this.onRender = this.onRender.bind(this);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Initialize the widget.
|
|
158
|
-
*/
|
|
159
|
-
@postConstruct()
|
|
160
|
-
protected init(): void {
|
|
161
|
-
this.id = KeybindingWidget.ID;
|
|
162
|
-
this.title.label = KeybindingWidget.LABEL;
|
|
163
|
-
this.title.caption = KeybindingWidget.LABEL;
|
|
164
|
-
this.title.iconClass = codicon('three-bars');
|
|
165
|
-
this.title.closable = true;
|
|
166
|
-
this.updateItemsAndRerender();
|
|
167
|
-
|
|
168
|
-
// Listen to changes made in the `keymaps.json` and update the view accordingly.
|
|
169
|
-
if (this.keymapsService.onDidChangeKeymaps) {
|
|
170
|
-
this.toDispose.push(this.keymapsService.onDidChangeKeymaps(() => {
|
|
171
|
-
this.items = this.getItems();
|
|
172
|
-
this.doSearchKeybindings();
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
this.toDispose.push(this.keybindingRegistry.onKeybindingsChanged(this.updateItemsAndRerender));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
protected updateItemsAndRerender = debounce(() => {
|
|
179
|
-
this.items = this.getItems();
|
|
180
|
-
this.update();
|
|
181
|
-
if (this.hasSearch()) {
|
|
182
|
-
this.doSearchKeybindings();
|
|
183
|
-
}
|
|
184
|
-
}, 100, { leading: false, trailing: true });
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Determine if there currently is a search term.
|
|
188
|
-
* @returns `true` if a search term is present.
|
|
189
|
-
*/
|
|
190
|
-
hasSearch(): boolean {
|
|
191
|
-
return !!this.query.length;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Clear the search and reset the view.
|
|
196
|
-
*/
|
|
197
|
-
clearSearch(): void {
|
|
198
|
-
const search = this.findSearchField();
|
|
199
|
-
if (search) {
|
|
200
|
-
search.value = '';
|
|
201
|
-
this.query = '';
|
|
202
|
-
this.doSearchKeybindings();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
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
|
-
|
|
223
|
-
protected override onActivateRequest(msg: Message): void {
|
|
224
|
-
super.onActivateRequest(msg);
|
|
225
|
-
this.focusInputField();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Perform a search based on the user's search query.
|
|
230
|
-
*/
|
|
231
|
-
protected doSearchKeybindings(): void {
|
|
232
|
-
this.onDidUpdateEmitter.fire(undefined);
|
|
233
|
-
const searchField = this.findSearchField();
|
|
234
|
-
this.query = searchField ? searchField.value.trim().toLocaleLowerCase() : '';
|
|
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]/);
|
|
246
|
-
this.items.forEach(item => {
|
|
247
|
-
let matched = !this.query;
|
|
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
|
-
}
|
|
256
|
-
item.visible = matched;
|
|
257
|
-
});
|
|
258
|
-
this.update();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
protected formatAndMatchCommand(item: KeybindingItem): boolean {
|
|
262
|
-
item.labels.command = this.toRenderableLabel(item.labels.command.value);
|
|
263
|
-
return Boolean(item.labels.command.segments);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
protected formatAndMatchKeybinding(item: KeybindingItem, queryItems: string[], exactMatch?: boolean): boolean {
|
|
267
|
-
if (item.keybinding) {
|
|
268
|
-
const unmatchedTerms = queryItems.filter(Boolean);
|
|
269
|
-
const segments = this.keybindingRegistry.resolveKeybinding(item.keybinding).reduce<RenderableStringSegment[]>((collection, code, codeIndex) => {
|
|
270
|
-
if (codeIndex !== 0) {
|
|
271
|
-
// Two non-breaking spaces.
|
|
272
|
-
collection.push({ value: '\u00a0\u00a0', match: false, key: false });
|
|
273
|
-
}
|
|
274
|
-
const displayChunks = this.keybindingRegistry.componentsForKeyCode(code);
|
|
275
|
-
const matchChunks = isOSX ? this.keybindingRegistry.componentsForKeyCode(code, true) : displayChunks;
|
|
276
|
-
|
|
277
|
-
displayChunks.forEach((chunk, chunkIndex) => {
|
|
278
|
-
if (chunkIndex !== 0) {
|
|
279
|
-
collection.push({ value: '+', match: false, key: false });
|
|
280
|
-
}
|
|
281
|
-
const indexOfTerm = unmatchedTerms.indexOf(matchChunks[chunkIndex].toLocaleLowerCase());
|
|
282
|
-
const chunkMatches = indexOfTerm > -1;
|
|
283
|
-
if (chunkMatches) { unmatchedTerms.splice(indexOfTerm, 1); }
|
|
284
|
-
collection.push({ value: chunk, match: chunkMatches, key: true });
|
|
285
|
-
});
|
|
286
|
-
return collection;
|
|
287
|
-
}, []);
|
|
288
|
-
item.labels.keybinding = { value: item.labels.keybinding.value, segments };
|
|
289
|
-
if (unmatchedTerms.length) {
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
if (exactMatch) {
|
|
293
|
-
return !segments.some(segment => segment.key && !segment.match);
|
|
294
|
-
}
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
item.labels.keybinding = { value: '' };
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
protected formatAndMatchContext(item: KeybindingItem): boolean {
|
|
302
|
-
item.labels.context = this.toRenderableLabel(item.labels.context.value);
|
|
303
|
-
return Boolean(item.labels.context.segments);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
protected formatAndMatchSource(item: KeybindingItem): boolean {
|
|
307
|
-
item.labels.source = this.toRenderableLabel(item.labels.source.value);
|
|
308
|
-
return Boolean(item.labels.source.segments);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
protected toRenderableLabel(label: string, query: string = this.query): RenderableLabel {
|
|
312
|
-
if (label && query) {
|
|
313
|
-
const fuzzyMatch = fuzzy.match(query, label, this.fuzzyOptions);
|
|
314
|
-
if (fuzzyMatch) {
|
|
315
|
-
return {
|
|
316
|
-
value: label,
|
|
317
|
-
segments: fuzzyMatch.rendered.split(this.fuzzyOptions.pre).reduce<RenderableStringSegment[]>((collection, segment) => {
|
|
318
|
-
const [maybeMatch, notMatch] = segment.split(this.fuzzyOptions.post);
|
|
319
|
-
if (notMatch === undefined) {
|
|
320
|
-
collection.push({ value: maybeMatch, match: false });
|
|
321
|
-
} else {
|
|
322
|
-
collection.push({ value: maybeMatch, match: true }, { value: notMatch, match: false });
|
|
323
|
-
}
|
|
324
|
-
return collection;
|
|
325
|
-
}, [])
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return { value: label };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Get the search input if available.
|
|
334
|
-
* @returns the search input if available.
|
|
335
|
-
*/
|
|
336
|
-
protected findSearchField(): HTMLInputElement | null {
|
|
337
|
-
return document.getElementById('search-kb') as HTMLInputElement;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Set the focus the search input field if available.
|
|
342
|
-
*/
|
|
343
|
-
protected focusInputField(): void {
|
|
344
|
-
const input = document.getElementById('search-kb');
|
|
345
|
-
if (input) {
|
|
346
|
-
(input as HTMLInputElement).focus();
|
|
347
|
-
(input as HTMLInputElement).select();
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Render the view.
|
|
353
|
-
*/
|
|
354
|
-
protected render(): React.ReactNode {
|
|
355
|
-
return <div id='kb-main-container'>
|
|
356
|
-
{this.renderSearch()}
|
|
357
|
-
{(this.items.length > 0) ? this.renderTable() : this.renderMessage()}
|
|
358
|
-
</div>;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Render the search container with the search input.
|
|
363
|
-
*/
|
|
364
|
-
protected renderSearch(): React.ReactNode {
|
|
365
|
-
return <div>
|
|
366
|
-
<div className='search-kb-container'>
|
|
367
|
-
<input
|
|
368
|
-
id='search-kb'
|
|
369
|
-
ref={this.onRender}
|
|
370
|
-
className={`theia-input${(this.items.length > 0) ? '' : ' no-kb'}`}
|
|
371
|
-
type='text'
|
|
372
|
-
spellCheck={false}
|
|
373
|
-
placeholder={nls.localizeByDefault('Type to search in keybindings')}
|
|
374
|
-
autoComplete='off'
|
|
375
|
-
onKeyUp={this.searchKeybindings}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
</div>;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Render the warning message when no search results are found.
|
|
383
|
-
*/
|
|
384
|
-
protected renderMessage(): React.ReactNode {
|
|
385
|
-
return <AlertMessage
|
|
386
|
-
type='WARNING'
|
|
387
|
-
header='No results found!'
|
|
388
|
-
/>;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Render the keybindings table.
|
|
393
|
-
*/
|
|
394
|
-
protected renderTable(): React.ReactNode {
|
|
395
|
-
return <div id='kb-table-container'>
|
|
396
|
-
<div className='kb'>
|
|
397
|
-
<table>
|
|
398
|
-
<thead>
|
|
399
|
-
<tr>
|
|
400
|
-
<th className='th-action'></th>
|
|
401
|
-
<th className='th-label'>{nls.localizeByDefault('Command')}</th>
|
|
402
|
-
<th className='th-keybinding'>{nls.localizeByDefault('Keybinding')}</th>
|
|
403
|
-
<th className='th-context'>{nls.localizeByDefault('When')}</th>
|
|
404
|
-
<th className='th-source'>{nls.localizeByDefault('Source')}</th>
|
|
405
|
-
</tr>
|
|
406
|
-
</thead>
|
|
407
|
-
<tbody>
|
|
408
|
-
{this.renderRows()}
|
|
409
|
-
</tbody>
|
|
410
|
-
</table>
|
|
411
|
-
</div>
|
|
412
|
-
</div>;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Render the table rows.
|
|
417
|
-
*/
|
|
418
|
-
protected renderRows(): React.ReactNode {
|
|
419
|
-
return <React.Fragment>
|
|
420
|
-
{this.items.map((item, index) => item.visible !== false && this.renderRow(item, index))}
|
|
421
|
-
</React.Fragment>;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
protected renderRow(item: KeybindingItem, index: number): React.ReactNode {
|
|
425
|
-
const { command, keybinding } = item;
|
|
426
|
-
// TODO get rid of array functions in event handlers
|
|
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)}>
|
|
430
|
-
<td className='kb-actions'>
|
|
431
|
-
{this.renderActions(item)}
|
|
432
|
-
</td>
|
|
433
|
-
<td className='kb-label' title={this.getCommandLabel(command)}>
|
|
434
|
-
{this.renderMatchedData(item.labels.command)}
|
|
435
|
-
</td>
|
|
436
|
-
<td title={this.getKeybindingLabel(keybinding)} className='kb-keybinding monaco-keybinding'>
|
|
437
|
-
{this.renderKeybinding(item)}
|
|
438
|
-
</td>
|
|
439
|
-
<td className='kb-context' title={this.getContextLabel(keybinding)}>
|
|
440
|
-
<code>{this.renderMatchedData(item.labels.context)}</code>
|
|
441
|
-
</td>
|
|
442
|
-
<td className='kb-source' title={this.getScopeLabel(keybinding)}>
|
|
443
|
-
<code className='td-source'>{this.renderMatchedData(item.labels.source)}</code>
|
|
444
|
-
</td>
|
|
445
|
-
</tr>;
|
|
446
|
-
}
|
|
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
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Render the actions container with action icons.
|
|
481
|
-
* @param item the keybinding item for the row.
|
|
482
|
-
*/
|
|
483
|
-
protected renderActions(item: KeybindingItem): React.ReactNode {
|
|
484
|
-
return <span className='kb-actions-icons'>{this.renderEdit(item)}{this.renderReset(item)}</span>;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Render the edit action used to update a keybinding.
|
|
489
|
-
* @param item the keybinding item for the row.
|
|
490
|
-
*/
|
|
491
|
-
protected renderEdit(item: KeybindingItem): React.ReactNode {
|
|
492
|
-
return <a title='Edit Keybinding' href='#' onClick={e => {
|
|
493
|
-
e.preventDefault();
|
|
494
|
-
this.editKeybinding(item);
|
|
495
|
-
}}><i className={`${codicon('edit', true)} kb-action-item`}></i></a>;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Render the reset action to reset the custom keybinding.
|
|
500
|
-
* Only visible if a keybinding has a `user` scope.
|
|
501
|
-
* @param item the keybinding item for the row.
|
|
502
|
-
*/
|
|
503
|
-
protected renderReset(item: KeybindingItem): React.ReactNode {
|
|
504
|
-
return this.canResetKeybinding(item)
|
|
505
|
-
? <a title='Reset Keybinding' href='#' onClick={e => {
|
|
506
|
-
e.preventDefault();
|
|
507
|
-
this.resetKeybinding(item);
|
|
508
|
-
}}><i className={`${codicon('discard', true)} kb-action-item`}></i></a> : '';
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Render the keybinding.
|
|
513
|
-
* @param keybinding the keybinding value.
|
|
514
|
-
*/
|
|
515
|
-
protected renderKeybinding(keybinding: KeybindingItem): React.ReactNode {
|
|
516
|
-
if (!keybinding.keybinding) {
|
|
517
|
-
return undefined;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (keybinding.labels.keybinding.segments) {
|
|
521
|
-
return keybinding.labels.keybinding.segments.map((segment, index) => {
|
|
522
|
-
if (segment.key) {
|
|
523
|
-
return <span key={index} className='monaco-keybinding-key'>
|
|
524
|
-
<span className={`${segment.match ? 'fuzzy-match' : ''}`}>{segment.value}</span>
|
|
525
|
-
</span>;
|
|
526
|
-
} else {
|
|
527
|
-
return <span key={index} className='monaco-keybinding-separator'>
|
|
528
|
-
{segment.value}
|
|
529
|
-
</span>;
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
console.warn('Unexpectedly encountered a keybinding without segment divisions');
|
|
535
|
-
return keybinding.labels.keybinding.value;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Get the list of keybinding items.
|
|
540
|
-
*
|
|
541
|
-
* @returns the list of keybinding items.
|
|
542
|
-
*/
|
|
543
|
-
protected getItems(): KeybindingItem[] {
|
|
544
|
-
// Sort the commands alphabetically.
|
|
545
|
-
const commands = this.commandRegistry.commands;
|
|
546
|
-
const items: KeybindingItem[] = [];
|
|
547
|
-
// Build the keybinding items.
|
|
548
|
-
for (let i = 0; i < commands.length; i++) {
|
|
549
|
-
const command = commands[i];
|
|
550
|
-
// Skip internal commands prefixed by `_`.
|
|
551
|
-
if (command.id.startsWith('_')) {
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
const keybindings = this.keybindingRegistry.getKeybindingsForCommand(command.id);
|
|
555
|
-
keybindings.forEach(keybinding => {
|
|
556
|
-
const item = this.createKeybindingItem(command, keybinding);
|
|
557
|
-
items.push(item);
|
|
558
|
-
});
|
|
559
|
-
// we might not have any keybindings for the command
|
|
560
|
-
if (keybindings.length < 1) {
|
|
561
|
-
const item = this.createKeybindingItem(command);
|
|
562
|
-
items.push(item);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return this.sortKeybindings(items);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
protected createKeybindingItem(command: Command, keybinding?: ScopedKeybinding): KeybindingItem {
|
|
570
|
-
const item = {
|
|
571
|
-
command,
|
|
572
|
-
keybinding,
|
|
573
|
-
labels: {
|
|
574
|
-
id: { value: command.id },
|
|
575
|
-
command: { value: this.getCommandLabel(command) },
|
|
576
|
-
keybinding: { value: this.getKeybindingLabel(keybinding) || '' },
|
|
577
|
-
context: { value: this.getContextLabel(keybinding) || '' },
|
|
578
|
-
source: { value: this.getScopeLabel(keybinding) || '' }
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
this.formatAndMatchCommand(item);
|
|
582
|
-
this.formatAndMatchKeybinding(item, []);
|
|
583
|
-
this.formatAndMatchContext(item);
|
|
584
|
-
this.formatAndMatchSource(item);
|
|
585
|
-
return item;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* @returns the input array, sorted.
|
|
590
|
-
* The sort priority is as follows: items with keybindings before those without, then alphabetical by command.
|
|
591
|
-
*/
|
|
592
|
-
protected sortKeybindings(bindings: KeybindingItem[]): KeybindingItem[] {
|
|
593
|
-
return bindings.sort((a, b) => {
|
|
594
|
-
if (a.keybinding && !b.keybinding) {
|
|
595
|
-
return -1;
|
|
596
|
-
}
|
|
597
|
-
if (b.keybinding && !a.keybinding) {
|
|
598
|
-
return 1;
|
|
599
|
-
}
|
|
600
|
-
return this.compareItem(a.command, b.command);
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Get the human-readable label for a given command.
|
|
606
|
-
* @param command the command.
|
|
607
|
-
*
|
|
608
|
-
* @returns a human-readable label for the given command.
|
|
609
|
-
*/
|
|
610
|
-
protected getCommandLabel(command: Command): string {
|
|
611
|
-
if (command.label) {
|
|
612
|
-
// Prefix the command label with the category if it exists, else return the simple label.
|
|
613
|
-
return command.category ? `${command.category}: ${command.label}` : command.label;
|
|
614
|
-
}
|
|
615
|
-
return command.id;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
protected getKeybindingLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
|
|
619
|
-
return keybinding && keybinding.keybinding;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
protected getContextLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
|
|
623
|
-
return keybinding ? keybinding.context || keybinding.when : undefined;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
protected getScopeLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
|
|
627
|
-
let scope = keybinding && keybinding.scope;
|
|
628
|
-
if (scope !== undefined) {
|
|
629
|
-
if (scope < KeybindingScope.USER) {
|
|
630
|
-
scope = KeybindingScope.DEFAULT;
|
|
631
|
-
}
|
|
632
|
-
return KeybindingScope[scope].toLocaleLowerCase();
|
|
633
|
-
}
|
|
634
|
-
return undefined;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Compare two commands.
|
|
639
|
-
* - Commands with a label should be prioritized and alphabetically sorted.
|
|
640
|
-
* - Commands without a label (id) should be placed at the bottom.
|
|
641
|
-
* @param a the first command.
|
|
642
|
-
* @param b the second command.
|
|
643
|
-
*
|
|
644
|
-
* @returns an integer indicating whether `a` comes before, after or is equivalent to `b`.
|
|
645
|
-
* - returns `-1` if `a` occurs before `b`.
|
|
646
|
-
* - returns `1` if `a` occurs after `b`.
|
|
647
|
-
* - returns `0` if they are equivalent.
|
|
648
|
-
*/
|
|
649
|
-
protected compareItem(a: Command, b: Command): number {
|
|
650
|
-
const labelA = this.getCommandLabel(a);
|
|
651
|
-
const labelB = this.getCommandLabel(b);
|
|
652
|
-
if (labelA === a.id && labelB === b.id) {
|
|
653
|
-
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
|
|
654
|
-
}
|
|
655
|
-
if (labelA === a.id) {
|
|
656
|
-
return 1;
|
|
657
|
-
}
|
|
658
|
-
if (labelB === b.id) {
|
|
659
|
-
return -1;
|
|
660
|
-
}
|
|
661
|
-
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Prompt users to update the keybinding for the given command.
|
|
666
|
-
* @param item the keybinding item.
|
|
667
|
-
*/
|
|
668
|
-
editKeybinding(item: KeybindingItem): void {
|
|
669
|
-
const command = item.command.id;
|
|
670
|
-
const oldKeybinding = item.keybinding;
|
|
671
|
-
const dialog = new EditKeybindingDialog({
|
|
672
|
-
title: nls.localize('theia/keymaps/editKeybindingTitle', 'Edit Keybinding for {0}', item.labels.command.value),
|
|
673
|
-
maxWidth: 400,
|
|
674
|
-
initialValue: oldKeybinding?.keybinding,
|
|
675
|
-
validate: newKeybinding => this.validateKeybinding(command, oldKeybinding?.keybinding, newKeybinding),
|
|
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
|
-
});
|
|
729
|
-
dialog.open().then(async keybinding => {
|
|
730
|
-
if (keybinding) {
|
|
731
|
-
await this.keymapsService.setKeybinding({
|
|
732
|
-
...item.keybinding,
|
|
733
|
-
command,
|
|
734
|
-
keybinding
|
|
735
|
-
}, undefined);
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Prompt users for confirmation before resetting.
|
|
742
|
-
* @param command the command label.
|
|
743
|
-
*
|
|
744
|
-
* @returns a Promise which resolves to `true` if a user accepts resetting.
|
|
745
|
-
*/
|
|
746
|
-
protected async confirmResetKeybinding(item: KeybindingItem): Promise<boolean> {
|
|
747
|
-
const message = document.createElement('div');
|
|
748
|
-
const question = document.createElement('p');
|
|
749
|
-
question.textContent = nls.localize('theia/keymaps/resetKeybindingConfirmation', 'Do you really want to reset this keybinding to its default value?');
|
|
750
|
-
message.append(question);
|
|
751
|
-
const info = document.createElement('p');
|
|
752
|
-
info.textContent = nls.localize('theia/keymaps/resetMultipleKeybindingsWarning', 'If multiple keybindings exist for this command, all of them will be reset.');
|
|
753
|
-
message.append(info);
|
|
754
|
-
|
|
755
|
-
const dialog = new ConfirmDialog({
|
|
756
|
-
title: nls.localize('theia/keymaps/resetKeybindingTitle', 'Reset keybinding for {0}', this.getCommandLabel(item.command)),
|
|
757
|
-
msg: message
|
|
758
|
-
});
|
|
759
|
-
return !!await dialog.open();
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Reset the keybinding to its default value.
|
|
764
|
-
* @param item the keybinding item.
|
|
765
|
-
*/
|
|
766
|
-
async resetKeybinding(item: KeybindingItem): Promise<void> {
|
|
767
|
-
const confirmed = await this.confirmResetKeybinding(item);
|
|
768
|
-
if (confirmed) {
|
|
769
|
-
this.keymapsService.removeKeybinding(item.command.id);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
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
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Validate the provided keybinding value against its previous value.
|
|
783
|
-
* @param command the command label.
|
|
784
|
-
* @param oldKeybinding the old keybinding value.
|
|
785
|
-
* @param keybinding the new keybinding value.
|
|
786
|
-
*
|
|
787
|
-
* @returns the end user message to display.
|
|
788
|
-
*/
|
|
789
|
-
protected validateKeybinding(command: string, oldKeybinding: string | undefined, keybinding: string): string {
|
|
790
|
-
if (!keybinding) {
|
|
791
|
-
return nls.localize('theia/keymaps/requiredKeybindingValidation', 'keybinding value is required');
|
|
792
|
-
}
|
|
793
|
-
try {
|
|
794
|
-
const binding = { command, keybinding };
|
|
795
|
-
KeySequence.parse(keybinding);
|
|
796
|
-
if (oldKeybinding === keybinding) {
|
|
797
|
-
return ''; // if old and new keybindings match, quietly reject update
|
|
798
|
-
}
|
|
799
|
-
if (this.keybindingRegistry.containsKeybindingInScope(binding)) {
|
|
800
|
-
return nls.localize('theia/keymaps/keybindingCollidesValidation', 'keybinding currently collides');
|
|
801
|
-
}
|
|
802
|
-
return '';
|
|
803
|
-
} catch (error) {
|
|
804
|
-
return error;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Build the cell data with highlights if applicable.
|
|
810
|
-
* @param raw the raw cell value.
|
|
811
|
-
*
|
|
812
|
-
* @returns the list of cell data.
|
|
813
|
-
*/
|
|
814
|
-
protected buildCellData(raw: string): CellData[] {
|
|
815
|
-
const data: CellData[] = [];
|
|
816
|
-
|
|
817
|
-
if (this.query === '') {
|
|
818
|
-
return data;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
let following = raw;
|
|
822
|
-
let leading;
|
|
823
|
-
let result;
|
|
824
|
-
|
|
825
|
-
const regexp = new RegExp(this.regexp);
|
|
826
|
-
|
|
827
|
-
while (result = regexp.exec(raw)) {
|
|
828
|
-
const splitLeftIndex = following.indexOf(result[0]);
|
|
829
|
-
const splitRightIndex = splitLeftIndex + result[0].length;
|
|
830
|
-
|
|
831
|
-
leading = following.slice(0, splitLeftIndex);
|
|
832
|
-
following = following.slice(splitRightIndex);
|
|
833
|
-
|
|
834
|
-
if (leading) {
|
|
835
|
-
data.push({ value: leading, highlighted: false });
|
|
836
|
-
}
|
|
837
|
-
data.push({ value: result[1], highlighted: true });
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
if (following) {
|
|
841
|
-
data.push({ value: following, highlighted: false });
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
return data;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Render the fuzzy representation of a matched result.
|
|
849
|
-
* @param property one of the `KeybindingItem` properties.
|
|
850
|
-
*/
|
|
851
|
-
protected renderMatchedData(property: RenderableLabel): React.ReactNode {
|
|
852
|
-
if (property.segments) {
|
|
853
|
-
return <>
|
|
854
|
-
{
|
|
855
|
-
property.segments.map((segment, index) => segment.match
|
|
856
|
-
? <span key={index} className='fuzzy-match'>{segment.value}</span>
|
|
857
|
-
: <span key={index}>{segment.value}</span>)
|
|
858
|
-
}
|
|
859
|
-
</>;
|
|
860
|
-
}
|
|
861
|
-
return property.value;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
storeState(): object | undefined {
|
|
865
|
-
return { query: this.query };
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
restoreState(oldState: { query: string }): void {
|
|
869
|
-
if (typeof oldState?.query === 'string') {
|
|
870
|
-
this.onRenderCallbacks.push({
|
|
871
|
-
dispose: () => {
|
|
872
|
-
const searchField = this.findSearchField();
|
|
873
|
-
if (searchField) {
|
|
874
|
-
searchField.value = oldState.query;
|
|
875
|
-
this.searchKeybindings();
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Dialog used to edit keybindings, and reset custom keybindings.
|
|
884
|
-
*/
|
|
885
|
-
class EditKeybindingDialog extends SingleTextInputDialog {
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* The keybinding item in question.
|
|
889
|
-
*/
|
|
890
|
-
protected item: KeybindingItem;
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* HTMLButtonElement used to reset custom keybindings.
|
|
894
|
-
* Custom keybindings have a `User` scope (exist in `keymaps.json`).
|
|
895
|
-
*/
|
|
896
|
-
protected resetButton: HTMLButtonElement | undefined;
|
|
897
|
-
|
|
898
|
-
constructor(
|
|
899
|
-
@inject(SingleTextInputDialogProps) props: SingleTextInputDialogProps,
|
|
900
|
-
@inject(KeymapsService) protected readonly keymapsService: KeymapsService,
|
|
901
|
-
item: KeybindingItem,
|
|
902
|
-
canReset: boolean
|
|
903
|
-
) {
|
|
904
|
-
super(props);
|
|
905
|
-
this.item = item;
|
|
906
|
-
// Add the `Reset` button if the command currently has a custom keybinding.
|
|
907
|
-
if (canReset) {
|
|
908
|
-
this.appendResetButton();
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
protected override onAfterAttach(msg: Message): void {
|
|
913
|
-
super.onAfterAttach(msg);
|
|
914
|
-
if (this.resetButton) {
|
|
915
|
-
this.addResetAction(this.resetButton, 'click');
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Add `Reset` action used to reset a custom keybinding, and close the dialog.
|
|
921
|
-
* @param element the HTML element in question.
|
|
922
|
-
* @param additionalEventTypes additional event types.
|
|
923
|
-
*/
|
|
924
|
-
protected addResetAction<K extends keyof HTMLElementEventMap>(element: HTMLElement, ...additionalEventTypes: K[]): void {
|
|
925
|
-
this.addKeyListener(element, Key.ENTER, () => {
|
|
926
|
-
this.reset();
|
|
927
|
-
this.close();
|
|
928
|
-
}, ...additionalEventTypes);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* Create the `Reset` button, and append it to the dialog.
|
|
933
|
-
*
|
|
934
|
-
* @returns the `Reset` button.
|
|
935
|
-
*/
|
|
936
|
-
protected appendResetButton(): HTMLButtonElement {
|
|
937
|
-
// Create the `Reset` button.
|
|
938
|
-
const resetButtonTitle = nls.localizeByDefault('Reset');
|
|
939
|
-
this.resetButton = this.createButton(resetButtonTitle);
|
|
940
|
-
// Add the `Reset` button to the dialog control panel, before the `Accept` button.
|
|
941
|
-
this.controlPanel.insertBefore(this.resetButton, this.acceptButton!);
|
|
942
|
-
this.resetButton.title = nls.localizeByDefault('Reset Keybinding');
|
|
943
|
-
this.resetButton.classList.add('secondary');
|
|
944
|
-
return this.resetButton;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* Perform keybinding reset.
|
|
949
|
-
*/
|
|
950
|
-
protected reset(): void {
|
|
951
|
-
this.keymapsService.removeKeybinding(this.item.command.id);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2018 Ericsson and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import React = require('@theia/core/shared/react');
|
|
18
|
+
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
19
|
+
import * as fuzzy from '@theia/core/shared/fuzzy';
|
|
20
|
+
import { injectable, inject, postConstruct, unmanaged } from '@theia/core/shared/inversify';
|
|
21
|
+
import { Emitter, Event } from '@theia/core/lib/common/event';
|
|
22
|
+
import { CommandRegistry, Command } from '@theia/core/lib/common/command';
|
|
23
|
+
import { Keybinding } from '@theia/core/lib/common/keybinding';
|
|
24
|
+
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
|
25
|
+
import {
|
|
26
|
+
KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope,
|
|
27
|
+
SingleTextInputDialogProps, Key, ScopedKeybinding, codicon, StatefulWidget, Widget, ContextMenuRenderer, SELECTED_CLASS
|
|
28
|
+
} from '@theia/core/lib/browser';
|
|
29
|
+
import { KeymapsService } from './keymaps-service';
|
|
30
|
+
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
|
|
31
|
+
import { DisposableCollection, isOSX, isObject } from '@theia/core';
|
|
32
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Representation of a keybinding item for the view.
|
|
36
|
+
*/
|
|
37
|
+
export interface KeybindingItem {
|
|
38
|
+
command: Command
|
|
39
|
+
keybinding?: ScopedKeybinding
|
|
40
|
+
/** human-readable labels can contain highlighting */
|
|
41
|
+
labels: {
|
|
42
|
+
id: RenderableLabel;
|
|
43
|
+
command: RenderableLabel;
|
|
44
|
+
keybinding: RenderableLabel;
|
|
45
|
+
context: RenderableLabel;
|
|
46
|
+
source: RenderableLabel;
|
|
47
|
+
}
|
|
48
|
+
visible?: boolean;
|
|
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
|
+
|
|
64
|
+
export interface RenderableLabel {
|
|
65
|
+
readonly value: string;
|
|
66
|
+
segments?: RenderableStringSegment[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RenderableStringSegment {
|
|
70
|
+
value: string;
|
|
71
|
+
match: boolean;
|
|
72
|
+
key?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Representation of an individual table cell.
|
|
77
|
+
*/
|
|
78
|
+
export interface CellData {
|
|
79
|
+
/**
|
|
80
|
+
* The cell value.
|
|
81
|
+
*/
|
|
82
|
+
value: string,
|
|
83
|
+
/**
|
|
84
|
+
* Indicates if a cell's value is currently highlighted.
|
|
85
|
+
*/
|
|
86
|
+
highlighted: boolean,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@injectable()
|
|
90
|
+
export class KeybindingWidget extends ReactWidget implements StatefulWidget {
|
|
91
|
+
|
|
92
|
+
@inject(CommandRegistry)
|
|
93
|
+
protected readonly commandRegistry: CommandRegistry;
|
|
94
|
+
|
|
95
|
+
@inject(KeybindingRegistry)
|
|
96
|
+
protected readonly keybindingRegistry: KeybindingRegistry;
|
|
97
|
+
|
|
98
|
+
@inject(KeymapsService)
|
|
99
|
+
protected readonly keymapsService: KeymapsService;
|
|
100
|
+
|
|
101
|
+
@inject(ContextMenuRenderer)
|
|
102
|
+
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
103
|
+
|
|
104
|
+
static readonly ID = 'keybindings.view.widget';
|
|
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'];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The list of all available keybindings.
|
|
115
|
+
*/
|
|
116
|
+
protected items: KeybindingItem[] = [];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The current user search query.
|
|
120
|
+
*/
|
|
121
|
+
protected query: string = '';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The regular expression used to extract values between fuzzy results.
|
|
125
|
+
*/
|
|
126
|
+
protected readonly regexp = /<match>(.*?)<\/match>/g;
|
|
127
|
+
/**
|
|
128
|
+
* The regular expression used to extract values between the keybinding separator.
|
|
129
|
+
*/
|
|
130
|
+
protected readonly keybindingSeparator = /<match>\+<\/match>/g;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The fuzzy search options.
|
|
134
|
+
* The `pre` and `post` options are used to wrap fuzzy matches.
|
|
135
|
+
*/
|
|
136
|
+
protected readonly fuzzyOptions = {
|
|
137
|
+
pre: '<match>',
|
|
138
|
+
post: '</match>',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
protected readonly onDidUpdateEmitter = new Emitter<void>();
|
|
142
|
+
readonly onDidUpdate: Event<void> = this.onDidUpdateEmitter.event;
|
|
143
|
+
protected readonly onRenderCallbacks = new DisposableCollection();
|
|
144
|
+
protected onRender = () => this.onRenderCallbacks.dispose();
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Search keybindings.
|
|
148
|
+
*/
|
|
149
|
+
protected readonly searchKeybindings: () => void = debounce(() => this.doSearchKeybindings(), 50);
|
|
150
|
+
|
|
151
|
+
constructor(@unmanaged() options?: Widget.IOptions) {
|
|
152
|
+
super(options);
|
|
153
|
+
this.onRender = this.onRender.bind(this);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Initialize the widget.
|
|
158
|
+
*/
|
|
159
|
+
@postConstruct()
|
|
160
|
+
protected init(): void {
|
|
161
|
+
this.id = KeybindingWidget.ID;
|
|
162
|
+
this.title.label = KeybindingWidget.LABEL;
|
|
163
|
+
this.title.caption = KeybindingWidget.LABEL;
|
|
164
|
+
this.title.iconClass = codicon('three-bars');
|
|
165
|
+
this.title.closable = true;
|
|
166
|
+
this.updateItemsAndRerender();
|
|
167
|
+
|
|
168
|
+
// Listen to changes made in the `keymaps.json` and update the view accordingly.
|
|
169
|
+
if (this.keymapsService.onDidChangeKeymaps) {
|
|
170
|
+
this.toDispose.push(this.keymapsService.onDidChangeKeymaps(() => {
|
|
171
|
+
this.items = this.getItems();
|
|
172
|
+
this.doSearchKeybindings();
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
this.toDispose.push(this.keybindingRegistry.onKeybindingsChanged(this.updateItemsAndRerender));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected updateItemsAndRerender = debounce(() => {
|
|
179
|
+
this.items = this.getItems();
|
|
180
|
+
this.update();
|
|
181
|
+
if (this.hasSearch()) {
|
|
182
|
+
this.doSearchKeybindings();
|
|
183
|
+
}
|
|
184
|
+
}, 100, { leading: false, trailing: true });
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Determine if there currently is a search term.
|
|
188
|
+
* @returns `true` if a search term is present.
|
|
189
|
+
*/
|
|
190
|
+
hasSearch(): boolean {
|
|
191
|
+
return !!this.query.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clear the search and reset the view.
|
|
196
|
+
*/
|
|
197
|
+
clearSearch(): void {
|
|
198
|
+
const search = this.findSearchField();
|
|
199
|
+
if (search) {
|
|
200
|
+
search.value = '';
|
|
201
|
+
this.query = '';
|
|
202
|
+
this.doSearchKeybindings();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
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
|
+
|
|
223
|
+
protected override onActivateRequest(msg: Message): void {
|
|
224
|
+
super.onActivateRequest(msg);
|
|
225
|
+
this.focusInputField();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Perform a search based on the user's search query.
|
|
230
|
+
*/
|
|
231
|
+
protected doSearchKeybindings(): void {
|
|
232
|
+
this.onDidUpdateEmitter.fire(undefined);
|
|
233
|
+
const searchField = this.findSearchField();
|
|
234
|
+
this.query = searchField ? searchField.value.trim().toLocaleLowerCase() : '';
|
|
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]/);
|
|
246
|
+
this.items.forEach(item => {
|
|
247
|
+
let matched = !this.query;
|
|
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
|
+
}
|
|
256
|
+
item.visible = matched;
|
|
257
|
+
});
|
|
258
|
+
this.update();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
protected formatAndMatchCommand(item: KeybindingItem): boolean {
|
|
262
|
+
item.labels.command = this.toRenderableLabel(item.labels.command.value);
|
|
263
|
+
return Boolean(item.labels.command.segments);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
protected formatAndMatchKeybinding(item: KeybindingItem, queryItems: string[], exactMatch?: boolean): boolean {
|
|
267
|
+
if (item.keybinding) {
|
|
268
|
+
const unmatchedTerms = queryItems.filter(Boolean);
|
|
269
|
+
const segments = this.keybindingRegistry.resolveKeybinding(item.keybinding).reduce<RenderableStringSegment[]>((collection, code, codeIndex) => {
|
|
270
|
+
if (codeIndex !== 0) {
|
|
271
|
+
// Two non-breaking spaces.
|
|
272
|
+
collection.push({ value: '\u00a0\u00a0', match: false, key: false });
|
|
273
|
+
}
|
|
274
|
+
const displayChunks = this.keybindingRegistry.componentsForKeyCode(code);
|
|
275
|
+
const matchChunks = isOSX ? this.keybindingRegistry.componentsForKeyCode(code, true) : displayChunks;
|
|
276
|
+
|
|
277
|
+
displayChunks.forEach((chunk, chunkIndex) => {
|
|
278
|
+
if (chunkIndex !== 0) {
|
|
279
|
+
collection.push({ value: '+', match: false, key: false });
|
|
280
|
+
}
|
|
281
|
+
const indexOfTerm = unmatchedTerms.indexOf(matchChunks[chunkIndex].toLocaleLowerCase());
|
|
282
|
+
const chunkMatches = indexOfTerm > -1;
|
|
283
|
+
if (chunkMatches) { unmatchedTerms.splice(indexOfTerm, 1); }
|
|
284
|
+
collection.push({ value: chunk, match: chunkMatches, key: true });
|
|
285
|
+
});
|
|
286
|
+
return collection;
|
|
287
|
+
}, []);
|
|
288
|
+
item.labels.keybinding = { value: item.labels.keybinding.value, segments };
|
|
289
|
+
if (unmatchedTerms.length) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
if (exactMatch) {
|
|
293
|
+
return !segments.some(segment => segment.key && !segment.match);
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
item.labels.keybinding = { value: '' };
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
protected formatAndMatchContext(item: KeybindingItem): boolean {
|
|
302
|
+
item.labels.context = this.toRenderableLabel(item.labels.context.value);
|
|
303
|
+
return Boolean(item.labels.context.segments);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected formatAndMatchSource(item: KeybindingItem): boolean {
|
|
307
|
+
item.labels.source = this.toRenderableLabel(item.labels.source.value);
|
|
308
|
+
return Boolean(item.labels.source.segments);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
protected toRenderableLabel(label: string, query: string = this.query): RenderableLabel {
|
|
312
|
+
if (label && query) {
|
|
313
|
+
const fuzzyMatch = fuzzy.match(query, label, this.fuzzyOptions);
|
|
314
|
+
if (fuzzyMatch) {
|
|
315
|
+
return {
|
|
316
|
+
value: label,
|
|
317
|
+
segments: fuzzyMatch.rendered.split(this.fuzzyOptions.pre).reduce<RenderableStringSegment[]>((collection, segment) => {
|
|
318
|
+
const [maybeMatch, notMatch] = segment.split(this.fuzzyOptions.post);
|
|
319
|
+
if (notMatch === undefined) {
|
|
320
|
+
collection.push({ value: maybeMatch, match: false });
|
|
321
|
+
} else {
|
|
322
|
+
collection.push({ value: maybeMatch, match: true }, { value: notMatch, match: false });
|
|
323
|
+
}
|
|
324
|
+
return collection;
|
|
325
|
+
}, [])
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { value: label };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the search input if available.
|
|
334
|
+
* @returns the search input if available.
|
|
335
|
+
*/
|
|
336
|
+
protected findSearchField(): HTMLInputElement | null {
|
|
337
|
+
return document.getElementById('search-kb') as HTMLInputElement;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Set the focus the search input field if available.
|
|
342
|
+
*/
|
|
343
|
+
protected focusInputField(): void {
|
|
344
|
+
const input = document.getElementById('search-kb');
|
|
345
|
+
if (input) {
|
|
346
|
+
(input as HTMLInputElement).focus();
|
|
347
|
+
(input as HTMLInputElement).select();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Render the view.
|
|
353
|
+
*/
|
|
354
|
+
protected render(): React.ReactNode {
|
|
355
|
+
return <div id='kb-main-container'>
|
|
356
|
+
{this.renderSearch()}
|
|
357
|
+
{(this.items.length > 0) ? this.renderTable() : this.renderMessage()}
|
|
358
|
+
</div>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Render the search container with the search input.
|
|
363
|
+
*/
|
|
364
|
+
protected renderSearch(): React.ReactNode {
|
|
365
|
+
return <div>
|
|
366
|
+
<div className='search-kb-container'>
|
|
367
|
+
<input
|
|
368
|
+
id='search-kb'
|
|
369
|
+
ref={this.onRender}
|
|
370
|
+
className={`theia-input${(this.items.length > 0) ? '' : ' no-kb'}`}
|
|
371
|
+
type='text'
|
|
372
|
+
spellCheck={false}
|
|
373
|
+
placeholder={nls.localizeByDefault('Type to search in keybindings')}
|
|
374
|
+
autoComplete='off'
|
|
375
|
+
onKeyUp={this.searchKeybindings}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
</div>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Render the warning message when no search results are found.
|
|
383
|
+
*/
|
|
384
|
+
protected renderMessage(): React.ReactNode {
|
|
385
|
+
return <AlertMessage
|
|
386
|
+
type='WARNING'
|
|
387
|
+
header='No results found!'
|
|
388
|
+
/>;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Render the keybindings table.
|
|
393
|
+
*/
|
|
394
|
+
protected renderTable(): React.ReactNode {
|
|
395
|
+
return <div id='kb-table-container'>
|
|
396
|
+
<div className='kb'>
|
|
397
|
+
<table>
|
|
398
|
+
<thead>
|
|
399
|
+
<tr>
|
|
400
|
+
<th className='th-action'></th>
|
|
401
|
+
<th className='th-label'>{nls.localizeByDefault('Command')}</th>
|
|
402
|
+
<th className='th-keybinding'>{nls.localizeByDefault('Keybinding')}</th>
|
|
403
|
+
<th className='th-context'>{nls.localizeByDefault('When')}</th>
|
|
404
|
+
<th className='th-source'>{nls.localizeByDefault('Source')}</th>
|
|
405
|
+
</tr>
|
|
406
|
+
</thead>
|
|
407
|
+
<tbody>
|
|
408
|
+
{this.renderRows()}
|
|
409
|
+
</tbody>
|
|
410
|
+
</table>
|
|
411
|
+
</div>
|
|
412
|
+
</div>;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Render the table rows.
|
|
417
|
+
*/
|
|
418
|
+
protected renderRows(): React.ReactNode {
|
|
419
|
+
return <React.Fragment>
|
|
420
|
+
{this.items.map((item, index) => item.visible !== false && this.renderRow(item, index))}
|
|
421
|
+
</React.Fragment>;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
protected renderRow(item: KeybindingItem, index: number): React.ReactNode {
|
|
425
|
+
const { command, keybinding } = item;
|
|
426
|
+
// TODO get rid of array functions in event handlers
|
|
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)}>
|
|
430
|
+
<td className='kb-actions'>
|
|
431
|
+
{this.renderActions(item)}
|
|
432
|
+
</td>
|
|
433
|
+
<td className='kb-label' title={this.getCommandLabel(command)}>
|
|
434
|
+
{this.renderMatchedData(item.labels.command)}
|
|
435
|
+
</td>
|
|
436
|
+
<td title={this.getKeybindingLabel(keybinding)} className='kb-keybinding monaco-keybinding'>
|
|
437
|
+
{this.renderKeybinding(item)}
|
|
438
|
+
</td>
|
|
439
|
+
<td className='kb-context' title={this.getContextLabel(keybinding)}>
|
|
440
|
+
<code>{this.renderMatchedData(item.labels.context)}</code>
|
|
441
|
+
</td>
|
|
442
|
+
<td className='kb-source' title={this.getScopeLabel(keybinding)}>
|
|
443
|
+
<code className='td-source'>{this.renderMatchedData(item.labels.source)}</code>
|
|
444
|
+
</td>
|
|
445
|
+
</tr>;
|
|
446
|
+
}
|
|
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
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Render the actions container with action icons.
|
|
481
|
+
* @param item the keybinding item for the row.
|
|
482
|
+
*/
|
|
483
|
+
protected renderActions(item: KeybindingItem): React.ReactNode {
|
|
484
|
+
return <span className='kb-actions-icons'>{this.renderEdit(item)}{this.renderReset(item)}</span>;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Render the edit action used to update a keybinding.
|
|
489
|
+
* @param item the keybinding item for the row.
|
|
490
|
+
*/
|
|
491
|
+
protected renderEdit(item: KeybindingItem): React.ReactNode {
|
|
492
|
+
return <a title='Edit Keybinding' href='#' onClick={e => {
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
this.editKeybinding(item);
|
|
495
|
+
}}><i className={`${codicon('edit', true)} kb-action-item`}></i></a>;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Render the reset action to reset the custom keybinding.
|
|
500
|
+
* Only visible if a keybinding has a `user` scope.
|
|
501
|
+
* @param item the keybinding item for the row.
|
|
502
|
+
*/
|
|
503
|
+
protected renderReset(item: KeybindingItem): React.ReactNode {
|
|
504
|
+
return this.canResetKeybinding(item)
|
|
505
|
+
? <a title='Reset Keybinding' href='#' onClick={e => {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
this.resetKeybinding(item);
|
|
508
|
+
}}><i className={`${codicon('discard', true)} kb-action-item`}></i></a> : '';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Render the keybinding.
|
|
513
|
+
* @param keybinding the keybinding value.
|
|
514
|
+
*/
|
|
515
|
+
protected renderKeybinding(keybinding: KeybindingItem): React.ReactNode {
|
|
516
|
+
if (!keybinding.keybinding) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (keybinding.labels.keybinding.segments) {
|
|
521
|
+
return keybinding.labels.keybinding.segments.map((segment, index) => {
|
|
522
|
+
if (segment.key) {
|
|
523
|
+
return <span key={index} className='monaco-keybinding-key'>
|
|
524
|
+
<span className={`${segment.match ? 'fuzzy-match' : ''}`}>{segment.value}</span>
|
|
525
|
+
</span>;
|
|
526
|
+
} else {
|
|
527
|
+
return <span key={index} className='monaco-keybinding-separator'>
|
|
528
|
+
{segment.value}
|
|
529
|
+
</span>;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
console.warn('Unexpectedly encountered a keybinding without segment divisions');
|
|
535
|
+
return keybinding.labels.keybinding.value;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get the list of keybinding items.
|
|
540
|
+
*
|
|
541
|
+
* @returns the list of keybinding items.
|
|
542
|
+
*/
|
|
543
|
+
protected getItems(): KeybindingItem[] {
|
|
544
|
+
// Sort the commands alphabetically.
|
|
545
|
+
const commands = this.commandRegistry.commands;
|
|
546
|
+
const items: KeybindingItem[] = [];
|
|
547
|
+
// Build the keybinding items.
|
|
548
|
+
for (let i = 0; i < commands.length; i++) {
|
|
549
|
+
const command = commands[i];
|
|
550
|
+
// Skip internal commands prefixed by `_`.
|
|
551
|
+
if (command.id.startsWith('_')) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const keybindings = this.keybindingRegistry.getKeybindingsForCommand(command.id);
|
|
555
|
+
keybindings.forEach(keybinding => {
|
|
556
|
+
const item = this.createKeybindingItem(command, keybinding);
|
|
557
|
+
items.push(item);
|
|
558
|
+
});
|
|
559
|
+
// we might not have any keybindings for the command
|
|
560
|
+
if (keybindings.length < 1) {
|
|
561
|
+
const item = this.createKeybindingItem(command);
|
|
562
|
+
items.push(item);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return this.sortKeybindings(items);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
protected createKeybindingItem(command: Command, keybinding?: ScopedKeybinding): KeybindingItem {
|
|
570
|
+
const item = {
|
|
571
|
+
command,
|
|
572
|
+
keybinding,
|
|
573
|
+
labels: {
|
|
574
|
+
id: { value: command.id },
|
|
575
|
+
command: { value: this.getCommandLabel(command) },
|
|
576
|
+
keybinding: { value: this.getKeybindingLabel(keybinding) || '' },
|
|
577
|
+
context: { value: this.getContextLabel(keybinding) || '' },
|
|
578
|
+
source: { value: this.getScopeLabel(keybinding) || '' }
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
this.formatAndMatchCommand(item);
|
|
582
|
+
this.formatAndMatchKeybinding(item, []);
|
|
583
|
+
this.formatAndMatchContext(item);
|
|
584
|
+
this.formatAndMatchSource(item);
|
|
585
|
+
return item;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @returns the input array, sorted.
|
|
590
|
+
* The sort priority is as follows: items with keybindings before those without, then alphabetical by command.
|
|
591
|
+
*/
|
|
592
|
+
protected sortKeybindings(bindings: KeybindingItem[]): KeybindingItem[] {
|
|
593
|
+
return bindings.sort((a, b) => {
|
|
594
|
+
if (a.keybinding && !b.keybinding) {
|
|
595
|
+
return -1;
|
|
596
|
+
}
|
|
597
|
+
if (b.keybinding && !a.keybinding) {
|
|
598
|
+
return 1;
|
|
599
|
+
}
|
|
600
|
+
return this.compareItem(a.command, b.command);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Get the human-readable label for a given command.
|
|
606
|
+
* @param command the command.
|
|
607
|
+
*
|
|
608
|
+
* @returns a human-readable label for the given command.
|
|
609
|
+
*/
|
|
610
|
+
protected getCommandLabel(command: Command): string {
|
|
611
|
+
if (command.label) {
|
|
612
|
+
// Prefix the command label with the category if it exists, else return the simple label.
|
|
613
|
+
return command.category ? `${command.category}: ${command.label}` : command.label;
|
|
614
|
+
}
|
|
615
|
+
return command.id;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
protected getKeybindingLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
|
|
619
|
+
return keybinding && keybinding.keybinding;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
protected getContextLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
|
|
623
|
+
return keybinding ? keybinding.context || keybinding.when : undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
protected getScopeLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
|
|
627
|
+
let scope = keybinding && keybinding.scope;
|
|
628
|
+
if (scope !== undefined) {
|
|
629
|
+
if (scope < KeybindingScope.USER) {
|
|
630
|
+
scope = KeybindingScope.DEFAULT;
|
|
631
|
+
}
|
|
632
|
+
return KeybindingScope[scope].toLocaleLowerCase();
|
|
633
|
+
}
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Compare two commands.
|
|
639
|
+
* - Commands with a label should be prioritized and alphabetically sorted.
|
|
640
|
+
* - Commands without a label (id) should be placed at the bottom.
|
|
641
|
+
* @param a the first command.
|
|
642
|
+
* @param b the second command.
|
|
643
|
+
*
|
|
644
|
+
* @returns an integer indicating whether `a` comes before, after or is equivalent to `b`.
|
|
645
|
+
* - returns `-1` if `a` occurs before `b`.
|
|
646
|
+
* - returns `1` if `a` occurs after `b`.
|
|
647
|
+
* - returns `0` if they are equivalent.
|
|
648
|
+
*/
|
|
649
|
+
protected compareItem(a: Command, b: Command): number {
|
|
650
|
+
const labelA = this.getCommandLabel(a);
|
|
651
|
+
const labelB = this.getCommandLabel(b);
|
|
652
|
+
if (labelA === a.id && labelB === b.id) {
|
|
653
|
+
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
|
|
654
|
+
}
|
|
655
|
+
if (labelA === a.id) {
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
if (labelB === b.id) {
|
|
659
|
+
return -1;
|
|
660
|
+
}
|
|
661
|
+
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Prompt users to update the keybinding for the given command.
|
|
666
|
+
* @param item the keybinding item.
|
|
667
|
+
*/
|
|
668
|
+
editKeybinding(item: KeybindingItem): void {
|
|
669
|
+
const command = item.command.id;
|
|
670
|
+
const oldKeybinding = item.keybinding;
|
|
671
|
+
const dialog = new EditKeybindingDialog({
|
|
672
|
+
title: nls.localize('theia/keymaps/editKeybindingTitle', 'Edit Keybinding for {0}', item.labels.command.value),
|
|
673
|
+
maxWidth: 400,
|
|
674
|
+
initialValue: oldKeybinding?.keybinding,
|
|
675
|
+
validate: newKeybinding => this.validateKeybinding(command, oldKeybinding?.keybinding, newKeybinding),
|
|
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
|
+
});
|
|
729
|
+
dialog.open().then(async keybinding => {
|
|
730
|
+
if (keybinding) {
|
|
731
|
+
await this.keymapsService.setKeybinding({
|
|
732
|
+
...item.keybinding,
|
|
733
|
+
command,
|
|
734
|
+
keybinding
|
|
735
|
+
}, undefined);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Prompt users for confirmation before resetting.
|
|
742
|
+
* @param command the command label.
|
|
743
|
+
*
|
|
744
|
+
* @returns a Promise which resolves to `true` if a user accepts resetting.
|
|
745
|
+
*/
|
|
746
|
+
protected async confirmResetKeybinding(item: KeybindingItem): Promise<boolean> {
|
|
747
|
+
const message = document.createElement('div');
|
|
748
|
+
const question = document.createElement('p');
|
|
749
|
+
question.textContent = nls.localize('theia/keymaps/resetKeybindingConfirmation', 'Do you really want to reset this keybinding to its default value?');
|
|
750
|
+
message.append(question);
|
|
751
|
+
const info = document.createElement('p');
|
|
752
|
+
info.textContent = nls.localize('theia/keymaps/resetMultipleKeybindingsWarning', 'If multiple keybindings exist for this command, all of them will be reset.');
|
|
753
|
+
message.append(info);
|
|
754
|
+
|
|
755
|
+
const dialog = new ConfirmDialog({
|
|
756
|
+
title: nls.localize('theia/keymaps/resetKeybindingTitle', 'Reset keybinding for {0}', this.getCommandLabel(item.command)),
|
|
757
|
+
msg: message
|
|
758
|
+
});
|
|
759
|
+
return !!await dialog.open();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Reset the keybinding to its default value.
|
|
764
|
+
* @param item the keybinding item.
|
|
765
|
+
*/
|
|
766
|
+
async resetKeybinding(item: KeybindingItem): Promise<void> {
|
|
767
|
+
const confirmed = await this.confirmResetKeybinding(item);
|
|
768
|
+
if (confirmed) {
|
|
769
|
+
this.keymapsService.removeKeybinding(item.command.id);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
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
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Validate the provided keybinding value against its previous value.
|
|
783
|
+
* @param command the command label.
|
|
784
|
+
* @param oldKeybinding the old keybinding value.
|
|
785
|
+
* @param keybinding the new keybinding value.
|
|
786
|
+
*
|
|
787
|
+
* @returns the end user message to display.
|
|
788
|
+
*/
|
|
789
|
+
protected validateKeybinding(command: string, oldKeybinding: string | undefined, keybinding: string): string {
|
|
790
|
+
if (!keybinding) {
|
|
791
|
+
return nls.localize('theia/keymaps/requiredKeybindingValidation', 'keybinding value is required');
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const binding = { command, keybinding };
|
|
795
|
+
KeySequence.parse(keybinding);
|
|
796
|
+
if (oldKeybinding === keybinding) {
|
|
797
|
+
return ''; // if old and new keybindings match, quietly reject update
|
|
798
|
+
}
|
|
799
|
+
if (this.keybindingRegistry.containsKeybindingInScope(binding)) {
|
|
800
|
+
return nls.localize('theia/keymaps/keybindingCollidesValidation', 'keybinding currently collides');
|
|
801
|
+
}
|
|
802
|
+
return '';
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return error;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Build the cell data with highlights if applicable.
|
|
810
|
+
* @param raw the raw cell value.
|
|
811
|
+
*
|
|
812
|
+
* @returns the list of cell data.
|
|
813
|
+
*/
|
|
814
|
+
protected buildCellData(raw: string): CellData[] {
|
|
815
|
+
const data: CellData[] = [];
|
|
816
|
+
|
|
817
|
+
if (this.query === '') {
|
|
818
|
+
return data;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let following = raw;
|
|
822
|
+
let leading;
|
|
823
|
+
let result;
|
|
824
|
+
|
|
825
|
+
const regexp = new RegExp(this.regexp);
|
|
826
|
+
|
|
827
|
+
while (result = regexp.exec(raw)) {
|
|
828
|
+
const splitLeftIndex = following.indexOf(result[0]);
|
|
829
|
+
const splitRightIndex = splitLeftIndex + result[0].length;
|
|
830
|
+
|
|
831
|
+
leading = following.slice(0, splitLeftIndex);
|
|
832
|
+
following = following.slice(splitRightIndex);
|
|
833
|
+
|
|
834
|
+
if (leading) {
|
|
835
|
+
data.push({ value: leading, highlighted: false });
|
|
836
|
+
}
|
|
837
|
+
data.push({ value: result[1], highlighted: true });
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (following) {
|
|
841
|
+
data.push({ value: following, highlighted: false });
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return data;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Render the fuzzy representation of a matched result.
|
|
849
|
+
* @param property one of the `KeybindingItem` properties.
|
|
850
|
+
*/
|
|
851
|
+
protected renderMatchedData(property: RenderableLabel): React.ReactNode {
|
|
852
|
+
if (property.segments) {
|
|
853
|
+
return <>
|
|
854
|
+
{
|
|
855
|
+
property.segments.map((segment, index) => segment.match
|
|
856
|
+
? <span key={index} className='fuzzy-match'>{segment.value}</span>
|
|
857
|
+
: <span key={index}>{segment.value}</span>)
|
|
858
|
+
}
|
|
859
|
+
</>;
|
|
860
|
+
}
|
|
861
|
+
return property.value;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
storeState(): object | undefined {
|
|
865
|
+
return { query: this.query };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
restoreState(oldState: { query: string }): void {
|
|
869
|
+
if (typeof oldState?.query === 'string') {
|
|
870
|
+
this.onRenderCallbacks.push({
|
|
871
|
+
dispose: () => {
|
|
872
|
+
const searchField = this.findSearchField();
|
|
873
|
+
if (searchField) {
|
|
874
|
+
searchField.value = oldState.query;
|
|
875
|
+
this.searchKeybindings();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Dialog used to edit keybindings, and reset custom keybindings.
|
|
884
|
+
*/
|
|
885
|
+
class EditKeybindingDialog extends SingleTextInputDialog {
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* The keybinding item in question.
|
|
889
|
+
*/
|
|
890
|
+
protected item: KeybindingItem;
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* HTMLButtonElement used to reset custom keybindings.
|
|
894
|
+
* Custom keybindings have a `User` scope (exist in `keymaps.json`).
|
|
895
|
+
*/
|
|
896
|
+
protected resetButton: HTMLButtonElement | undefined;
|
|
897
|
+
|
|
898
|
+
constructor(
|
|
899
|
+
@inject(SingleTextInputDialogProps) props: SingleTextInputDialogProps,
|
|
900
|
+
@inject(KeymapsService) protected readonly keymapsService: KeymapsService,
|
|
901
|
+
item: KeybindingItem,
|
|
902
|
+
canReset: boolean
|
|
903
|
+
) {
|
|
904
|
+
super(props);
|
|
905
|
+
this.item = item;
|
|
906
|
+
// Add the `Reset` button if the command currently has a custom keybinding.
|
|
907
|
+
if (canReset) {
|
|
908
|
+
this.appendResetButton();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
protected override onAfterAttach(msg: Message): void {
|
|
913
|
+
super.onAfterAttach(msg);
|
|
914
|
+
if (this.resetButton) {
|
|
915
|
+
this.addResetAction(this.resetButton, 'click');
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Add `Reset` action used to reset a custom keybinding, and close the dialog.
|
|
921
|
+
* @param element the HTML element in question.
|
|
922
|
+
* @param additionalEventTypes additional event types.
|
|
923
|
+
*/
|
|
924
|
+
protected addResetAction<K extends keyof HTMLElementEventMap>(element: HTMLElement, ...additionalEventTypes: K[]): void {
|
|
925
|
+
this.addKeyListener(element, Key.ENTER, () => {
|
|
926
|
+
this.reset();
|
|
927
|
+
this.close();
|
|
928
|
+
}, ...additionalEventTypes);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Create the `Reset` button, and append it to the dialog.
|
|
933
|
+
*
|
|
934
|
+
* @returns the `Reset` button.
|
|
935
|
+
*/
|
|
936
|
+
protected appendResetButton(): HTMLButtonElement {
|
|
937
|
+
// Create the `Reset` button.
|
|
938
|
+
const resetButtonTitle = nls.localizeByDefault('Reset');
|
|
939
|
+
this.resetButton = this.createButton(resetButtonTitle);
|
|
940
|
+
// Add the `Reset` button to the dialog control panel, before the `Accept` button.
|
|
941
|
+
this.controlPanel.insertBefore(this.resetButton, this.acceptButton!);
|
|
942
|
+
this.resetButton.title = nls.localizeByDefault('Reset Keybinding');
|
|
943
|
+
this.resetButton.classList.add('secondary');
|
|
944
|
+
return this.resetButton;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Perform keybinding reset.
|
|
949
|
+
*/
|
|
950
|
+
protected reset(): void {
|
|
951
|
+
this.keymapsService.removeKeybinding(this.item.command.id);
|
|
952
|
+
}
|
|
953
|
+
}
|