@opensumi/ide-keymaps 2.21.13 → 2.22.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/index.js.map +1 -1
- package/lib/browser/keymaps-parser.js +16 -16
- package/lib/browser/keymaps-parser.js.map +1 -1
- package/lib/browser/keymaps.contribution.d.ts.map +1 -1
- package/lib/browser/keymaps.contribution.js +1 -1
- package/lib/browser/keymaps.contribution.js.map +1 -1
- package/lib/browser/keymaps.service.d.ts +1 -1
- package/lib/browser/keymaps.service.d.ts.map +1 -1
- package/lib/browser/keymaps.service.js +14 -15
- package/lib/browser/keymaps.service.js.map +1 -1
- package/lib/browser/keymaps.view.d.ts.map +1 -1
- package/lib/browser/keymaps.view.js +2 -1
- package/lib/browser/keymaps.view.js.map +1 -1
- package/lib/common/keymaps.d.ts +4 -0
- package/lib/common/keymaps.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/browser/index.ts +18 -0
- package/src/browser/keymaps-parser.ts +116 -0
- package/src/browser/keymaps.contribution.ts +202 -0
- package/src/browser/keymaps.module.less +409 -0
- package/src/browser/keymaps.service.ts +762 -0
- package/src/browser/keymaps.view.tsx +428 -0
- package/src/common/const.ts +3 -0
- package/src/common/index.ts +2 -0
- package/src/common/keymaps.ts +118 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import * as fuzzy from 'fuzzy';
|
|
2
|
+
|
|
3
|
+
import { Injectable, Autowired } from '@opensumi/di';
|
|
4
|
+
import {
|
|
5
|
+
Disposable,
|
|
6
|
+
IDisposable,
|
|
7
|
+
ScopedKeybinding,
|
|
8
|
+
KeybindingRegistry,
|
|
9
|
+
URI,
|
|
10
|
+
Emitter,
|
|
11
|
+
Keybinding,
|
|
12
|
+
KeybindingScope,
|
|
13
|
+
CommandService,
|
|
14
|
+
EDITOR_COMMANDS,
|
|
15
|
+
CommandRegistry,
|
|
16
|
+
localize,
|
|
17
|
+
KeySequence,
|
|
18
|
+
KeybindingService,
|
|
19
|
+
ILogger,
|
|
20
|
+
Event,
|
|
21
|
+
KeybindingWeight,
|
|
22
|
+
ThrottledDelayer,
|
|
23
|
+
FileStat,
|
|
24
|
+
DisposableCollection,
|
|
25
|
+
ProgressLocation,
|
|
26
|
+
IProgress,
|
|
27
|
+
IProgressStep,
|
|
28
|
+
Deferred,
|
|
29
|
+
Throttler,
|
|
30
|
+
Schemes,
|
|
31
|
+
runWhenIdle,
|
|
32
|
+
} from '@opensumi/ide-core-browser';
|
|
33
|
+
import { IProgressService } from '@opensumi/ide-core-browser/lib/progress';
|
|
34
|
+
import { IFileServiceClient } from '@opensumi/ide-file-service';
|
|
35
|
+
|
|
36
|
+
import { KEYMAPS_FILE_NAME, IKeymapService, KEYMAPS_SCHEME, KeybindingItem, KeymapItem } from '../common';
|
|
37
|
+
|
|
38
|
+
import { KeymapsParser } from './keymaps-parser';
|
|
39
|
+
|
|
40
|
+
@Injectable()
|
|
41
|
+
export class KeymapService implements IKeymapService {
|
|
42
|
+
static DEFAULT_SEARCH_DELAY = 100;
|
|
43
|
+
static KEYMAP_FILE_URI: URI = new URI().withScheme(Schemes.userStorage).withPath(KEYMAPS_FILE_NAME);
|
|
44
|
+
|
|
45
|
+
@Autowired(KeybindingRegistry)
|
|
46
|
+
protected readonly keyBindingRegistry: KeybindingRegistry;
|
|
47
|
+
|
|
48
|
+
@Autowired(KeymapsParser)
|
|
49
|
+
protected readonly parser: KeymapsParser;
|
|
50
|
+
|
|
51
|
+
@Autowired(CommandService)
|
|
52
|
+
protected readonly commandService: CommandService;
|
|
53
|
+
|
|
54
|
+
@Autowired(CommandRegistry)
|
|
55
|
+
protected readonly commandRegistry: CommandRegistry;
|
|
56
|
+
|
|
57
|
+
@Autowired(KeybindingRegistry)
|
|
58
|
+
protected readonly keybindingRegistry: KeybindingRegistry;
|
|
59
|
+
|
|
60
|
+
@Autowired(KeybindingService)
|
|
61
|
+
protected readonly keybindingService: KeybindingService;
|
|
62
|
+
|
|
63
|
+
@Autowired(IFileServiceClient)
|
|
64
|
+
protected readonly filesystem: IFileServiceClient;
|
|
65
|
+
|
|
66
|
+
@Autowired(ILogger)
|
|
67
|
+
private readonly logger: ILogger;
|
|
68
|
+
|
|
69
|
+
@Autowired(IProgressService)
|
|
70
|
+
private readonly progressService: IProgressService;
|
|
71
|
+
|
|
72
|
+
private currentSearchValue: string;
|
|
73
|
+
|
|
74
|
+
protected resource: FileStat | undefined;
|
|
75
|
+
|
|
76
|
+
protected readonly keymapChangeEmitter = new Emitter<KeybindingItem[]>();
|
|
77
|
+
|
|
78
|
+
get onDidKeymapChanges(): Event<KeybindingItem[]> {
|
|
79
|
+
return this.keymapChangeEmitter.event;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected convertKeySequence: KeySequence = [];
|
|
83
|
+
|
|
84
|
+
protected readonly toUnregisterUserKeybindingMap: Map<string, IDisposable> = new Map();
|
|
85
|
+
protected readonly toRestoreDefaultKeybindingMap: Map<string, IDisposable> = new Map();
|
|
86
|
+
|
|
87
|
+
private searchDelayer = new ThrottledDelayer(KeymapService.DEFAULT_SEARCH_DELAY);
|
|
88
|
+
private disposableCollection: DisposableCollection = new DisposableCollection();
|
|
89
|
+
|
|
90
|
+
private _whenReadyDeferred: Deferred<void> = new Deferred();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* fuzzy搜索参数,pre及post用于包裹搜索结果
|
|
94
|
+
* @protected
|
|
95
|
+
* @memberof KeymapService
|
|
96
|
+
*/
|
|
97
|
+
protected readonly fuzzyOptions = {
|
|
98
|
+
pre: '<match>',
|
|
99
|
+
post: '</match>',
|
|
100
|
+
};
|
|
101
|
+
private _storeKeybindings: KeymapItem[];
|
|
102
|
+
|
|
103
|
+
private reconcileQueue: Throttler = new Throttler();
|
|
104
|
+
|
|
105
|
+
get storeKeybindings() {
|
|
106
|
+
return (
|
|
107
|
+
this._storeKeybindings &&
|
|
108
|
+
this._storeKeybindings.map((keybinding) => ({
|
|
109
|
+
...keybinding,
|
|
110
|
+
command: this.getValidateCommand(keybinding.command),
|
|
111
|
+
// 保持对旧版 keybinding 格式兼容
|
|
112
|
+
key: keybinding.key || keybinding.keybinding!,
|
|
113
|
+
}))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
set storeKeybindings(value: KeymapItem[]) {
|
|
118
|
+
this._storeKeybindings = value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
keybindings: KeybindingItem[] = [];
|
|
122
|
+
|
|
123
|
+
get whenReady() {
|
|
124
|
+
return this._whenReadyDeferred.promise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async init() {
|
|
128
|
+
await this.reconcile();
|
|
129
|
+
const watcher = await this.filesystem.watchFileChanges(KeymapService.KEYMAP_FILE_URI);
|
|
130
|
+
this.disposableCollection.push(watcher);
|
|
131
|
+
watcher.onFilesChanged(() => {
|
|
132
|
+
// 快捷键绑定文件内容变化,重新更新快捷键信息
|
|
133
|
+
this.reconcileQueue.queue(this.reconcile.bind(this));
|
|
134
|
+
});
|
|
135
|
+
this._whenReadyDeferred.resolve();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async openResource() {
|
|
139
|
+
if (!this.resource || !this.resource!.uri) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const fsPath = await this.resource.uri;
|
|
143
|
+
if (!fsPath) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const exist = await this.filesystem.access(fsPath);
|
|
147
|
+
if (!exist) {
|
|
148
|
+
const fileStat = await this.filesystem.createFile(fsPath);
|
|
149
|
+
const stat = await this.filesystem.setContent(fileStat, '{\n}');
|
|
150
|
+
this.resource = stat || undefined;
|
|
151
|
+
}
|
|
152
|
+
if (fsPath) {
|
|
153
|
+
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, new URI(fsPath), { preview: false });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private disposeRegistedKeybinding() {
|
|
158
|
+
for (const [, value] of this.toUnregisterUserKeybindingMap) {
|
|
159
|
+
value.dispose();
|
|
160
|
+
}
|
|
161
|
+
for (const [, value] of this.toRestoreDefaultKeybindingMap) {
|
|
162
|
+
value.dispose();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
dispose() {
|
|
167
|
+
this.disposeRegistedKeybinding();
|
|
168
|
+
this.disposableCollection.dispose();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 因为 monaco.edtior.* 替换为 editor.* 为了兼容在 storage 里存量的数据,需要兼容一下
|
|
173
|
+
* @param command
|
|
174
|
+
*/
|
|
175
|
+
private getValidateCommand(command: string) {
|
|
176
|
+
return command.replace(/^monaco.editor/, 'editor');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 重新加载并设置Keymap定义的快捷键
|
|
181
|
+
* @param keybindings
|
|
182
|
+
*/
|
|
183
|
+
async reconcile(keybindings?: KeymapItem[]) {
|
|
184
|
+
const keymapUrl = KeymapService.KEYMAP_FILE_URI.toString();
|
|
185
|
+
this.resource = await this.filesystem.getFileStat(keymapUrl);
|
|
186
|
+
// 如果不存在,则默认创建一个空文件
|
|
187
|
+
// 集成测有可能后置才会同步这个配置,如果不创建好不方便 watch
|
|
188
|
+
if (!this.resource) {
|
|
189
|
+
this.resource = await this.filesystem.createFile(keymapUrl, {
|
|
190
|
+
content: JSON.stringify([]),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const keymap = keybindings ? keybindings.slice(0) : await this.parseKeybindings();
|
|
194
|
+
const bindings: Keybinding[] = keymap.map((kb) =>
|
|
195
|
+
// 清洗存入keymap数据
|
|
196
|
+
({
|
|
197
|
+
when: kb.when,
|
|
198
|
+
command: this.getValidateCommand(kb.command),
|
|
199
|
+
keybinding: kb.key || kb.keybinding!,
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
const added: Keybinding[] = [];
|
|
203
|
+
const removed: Keybinding[] = [];
|
|
204
|
+
// 重新注册快捷键前取消注册先前的快捷键
|
|
205
|
+
this.disposeRegistedKeybinding();
|
|
206
|
+
bindings.forEach((kb: Keybinding) => {
|
|
207
|
+
if (kb.command.startsWith('-')) {
|
|
208
|
+
removed.push(kb);
|
|
209
|
+
} else {
|
|
210
|
+
added.push(kb);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// 卸载快捷键的语法仅对默认快捷键生效,故这里不需要进行额外操作
|
|
214
|
+
added.map((kb) => {
|
|
215
|
+
this.unregisterDefaultKeybinding(kb, true);
|
|
216
|
+
this.registerUserKeybinding(kb);
|
|
217
|
+
});
|
|
218
|
+
// 卸载默认快捷键
|
|
219
|
+
removed.map((kb) => {
|
|
220
|
+
// 去除开头的 '-' 便于查找默认快捷键
|
|
221
|
+
kb.command = kb.command.slice(1);
|
|
222
|
+
this.unregisterDefaultKeybinding(kb, true);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private unregisterUserKeybinding(kb: Keybinding) {
|
|
227
|
+
const key = this.toUniqueKey(kb);
|
|
228
|
+
if (this.toUnregisterUserKeybindingMap.has(key)) {
|
|
229
|
+
const disposeable = this.toUnregisterUserKeybindingMap.get(key);
|
|
230
|
+
disposeable?.dispose();
|
|
231
|
+
this.toUnregisterUserKeybindingMap.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private registerUserKeybinding(kb: Keybinding) {
|
|
236
|
+
const key = this.toUniqueKey(kb);
|
|
237
|
+
this.toUnregisterUserKeybindingMap.set(key, this.keybindingRegistry.registerKeybinding(kb, KeybindingScope.USER));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private unregisterDefaultKeybinding(kd: Keybinding, fromUserScope = false) {
|
|
241
|
+
if (fromUserScope) {
|
|
242
|
+
// 当卸载默认快捷键的操作是从用户快捷键初始化时操作的时候
|
|
243
|
+
// 此时需要找到command下对应keybindings中when及快捷键匹配的快捷键进行卸载
|
|
244
|
+
const keybindings = this.keybindingRegistry.getKeybindingsForCommand(kd.command);
|
|
245
|
+
keybindings.map((rawKd: ScopedKeybinding) => {
|
|
246
|
+
const targetKd = {
|
|
247
|
+
...kd,
|
|
248
|
+
keybinding: rawKd.keybinding,
|
|
249
|
+
};
|
|
250
|
+
this.keybindingRegistry.unregisterKeybinding(targetKd);
|
|
251
|
+
const key = this.toUniqueKey(kd);
|
|
252
|
+
// 存储可恢复默认快捷键注册的函数
|
|
253
|
+
this.toRestoreDefaultKeybindingMap.set(
|
|
254
|
+
key,
|
|
255
|
+
Disposable.create(() => {
|
|
256
|
+
this.keybindingRegistry.registerKeybinding(targetKd);
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
// 当直接从快捷键编辑面板直接修改默认快捷键的时候
|
|
262
|
+
// 由于卸载是定向的快捷键,有明确的快捷键,不需要进行command对应到快捷键的查找,可直接卸载
|
|
263
|
+
this.keybindingRegistry.unregisterKeybinding(kd);
|
|
264
|
+
const key = this.toUniqueKey(kd);
|
|
265
|
+
// 存储可恢复默认快捷键注册的函数
|
|
266
|
+
this.toRestoreDefaultKeybindingMap.set(
|
|
267
|
+
key,
|
|
268
|
+
Disposable.create(() => {
|
|
269
|
+
this.keybindingRegistry.registerKeybinding(kd);
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private restoreDefaultKeybinding(kb: Keybinding) {
|
|
276
|
+
const key = this.toUniqueKey(kb);
|
|
277
|
+
const restore = this.toRestoreDefaultKeybindingMap.get(key);
|
|
278
|
+
if (restore) {
|
|
279
|
+
restore?.dispose();
|
|
280
|
+
this.toRestoreDefaultKeybindingMap.delete(key);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private toUniqueKey(kb: Keybinding) {
|
|
285
|
+
return `${kb.command}${kb.when ? `-${kb.when}` : '-'}${kb.keybinding ? `-${kb.keybinding}` : '-'}`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 更新keybindings列表
|
|
289
|
+
*/
|
|
290
|
+
public updateKeybindings = () => {
|
|
291
|
+
if (this.currentSearchValue) {
|
|
292
|
+
this.doSearchKeybindings(this.currentSearchValue);
|
|
293
|
+
} else {
|
|
294
|
+
runWhenIdle(() => {
|
|
295
|
+
this.keybindings = this.getKeybindingItems();
|
|
296
|
+
this.keymapChangeEmitter.fire(this.keybindings);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 解析快捷键数据
|
|
303
|
+
* @protected
|
|
304
|
+
* @returns {Promise<Keybinding[]>}
|
|
305
|
+
* @memberof KeymapsService
|
|
306
|
+
*/
|
|
307
|
+
protected async parseKeybindings(): Promise<KeymapItem[]> {
|
|
308
|
+
try {
|
|
309
|
+
const resource = await this.resource;
|
|
310
|
+
if (!resource) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
const { content } = await this.filesystem.readFile(resource.uri);
|
|
314
|
+
this.storeKeybindings = this.parser.parse(content.toString());
|
|
315
|
+
} catch (error) {
|
|
316
|
+
this.logger.warn(`ParseKeybindings fail: ${error.stack}`);
|
|
317
|
+
this.storeKeybindings = [];
|
|
318
|
+
}
|
|
319
|
+
return this.storeKeybindings;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 设置快捷键
|
|
324
|
+
* @param {Keybinding} raw
|
|
325
|
+
* @param {Keybinding} keybindings
|
|
326
|
+
* @returns {Promise<void>}
|
|
327
|
+
* @memberof KeymapsService
|
|
328
|
+
*/
|
|
329
|
+
setKeybinding = (raw: Keybinding, keybinding: Keybinding) =>
|
|
330
|
+
this.progressService.withProgress(
|
|
331
|
+
{
|
|
332
|
+
location: ProgressLocation.Notification,
|
|
333
|
+
},
|
|
334
|
+
(progress: IProgress<IProgressStep>) =>
|
|
335
|
+
new Promise<any>((resolve, reject) => {
|
|
336
|
+
progress.report({ message: localize('keymaps.keybinding.loading'), increment: 0, total: 100 });
|
|
337
|
+
const keybindings: KeymapItem[] = this.storeKeybindings || [];
|
|
338
|
+
let updated = false;
|
|
339
|
+
for (const kb of keybindings) {
|
|
340
|
+
const item: Keybinding = {
|
|
341
|
+
when: kb.when,
|
|
342
|
+
command: kb.command,
|
|
343
|
+
keybinding: kb.key,
|
|
344
|
+
};
|
|
345
|
+
if (kb.command === keybinding.command) {
|
|
346
|
+
updated = true;
|
|
347
|
+
this.unregisterUserKeybinding(item);
|
|
348
|
+
const rawKey = this.toUniqueKey(raw);
|
|
349
|
+
const restore = this.toRestoreDefaultKeybindingMap.get(rawKey);
|
|
350
|
+
if (restore) {
|
|
351
|
+
const newKey = this.toUniqueKey(keybinding as Keybinding);
|
|
352
|
+
this.toRestoreDefaultKeybindingMap.delete(rawKey);
|
|
353
|
+
this.toRestoreDefaultKeybindingMap.set(newKey, restore);
|
|
354
|
+
}
|
|
355
|
+
kb.key = keybinding.keybinding;
|
|
356
|
+
this.registerUserKeybinding({
|
|
357
|
+
...item,
|
|
358
|
+
priority: KeybindingWeight.WorkbenchContrib * 100,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!updated) {
|
|
363
|
+
const defaultBinding = this.keybindings.find(
|
|
364
|
+
(kb: KeybindingItem) => kb.id === keybinding.command && this.getRaw(kb.when) === keybinding.when,
|
|
365
|
+
);
|
|
366
|
+
if (defaultBinding && defaultBinding.keybinding) {
|
|
367
|
+
this.unregisterDefaultKeybinding({
|
|
368
|
+
when: this.getRaw(defaultBinding.when),
|
|
369
|
+
command: defaultBinding.id,
|
|
370
|
+
keybinding: this.getRaw(defaultBinding.keybinding),
|
|
371
|
+
});
|
|
372
|
+
const rawKey = this.toUniqueKey(raw as Keybinding);
|
|
373
|
+
const restore = this.toRestoreDefaultKeybindingMap.get(rawKey);
|
|
374
|
+
if (restore) {
|
|
375
|
+
const newKey = this.toUniqueKey(keybinding);
|
|
376
|
+
this.toRestoreDefaultKeybindingMap.delete(rawKey);
|
|
377
|
+
this.toRestoreDefaultKeybindingMap.set(newKey, restore);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// 不能额外传入keybinding的resolved值
|
|
381
|
+
const item: KeymapItem = {
|
|
382
|
+
when: this.getWhen(keybinding),
|
|
383
|
+
command: keybinding.command,
|
|
384
|
+
key: keybinding.keybinding,
|
|
385
|
+
};
|
|
386
|
+
keybindings.push(item);
|
|
387
|
+
this.registerUserKeybinding(keybinding);
|
|
388
|
+
}
|
|
389
|
+
// 后置存储流程
|
|
390
|
+
return this.saveKeybinding(keybindings)
|
|
391
|
+
.then(() => {
|
|
392
|
+
resolve(undefined);
|
|
393
|
+
progress.report({ message: localize('keymaps.keybinding.success'), increment: 99 });
|
|
394
|
+
})
|
|
395
|
+
.catch((e: any) => {
|
|
396
|
+
reject(e);
|
|
397
|
+
progress.report({ message: localize('keymaps.keybinding.fail'), increment: 99 });
|
|
398
|
+
})
|
|
399
|
+
.finally(() => {
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
// 3s 后再隐藏进度条
|
|
402
|
+
progress.report({ increment: 100 });
|
|
403
|
+
}, 3000);
|
|
404
|
+
});
|
|
405
|
+
}),
|
|
406
|
+
() => {},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
private async saveKeybinding(keymaps: KeymapItem[]) {
|
|
410
|
+
this.storeKeybindings = keymaps;
|
|
411
|
+
this.updateKeybindings();
|
|
412
|
+
if (!this.resource) {
|
|
413
|
+
this.resource = await this.filesystem.createFile(KeymapService.KEYMAP_FILE_URI.toString());
|
|
414
|
+
}
|
|
415
|
+
// 更新当前文件资源
|
|
416
|
+
const stat = await this.filesystem.setContent(this.resource, JSON.stringify(keymaps, undefined, 2));
|
|
417
|
+
this.resource = stat || undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
covert = (event: KeyboardEvent) => this.keybindingService.convert(event, ' ');
|
|
421
|
+
|
|
422
|
+
clearCovert = () => this.keybindingService.clearConvert();
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 移除给定ID的快捷键绑定
|
|
426
|
+
* @param {string} commandId
|
|
427
|
+
* @returns {Promise<void>}
|
|
428
|
+
* @memberof KeymapsService
|
|
429
|
+
*/
|
|
430
|
+
resetKeybinding = async (item: Keybinding) => {
|
|
431
|
+
if (!this.resource) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const keymaps: KeymapItem[] = this.storeKeybindings;
|
|
435
|
+
const filtered = keymaps.filter((a) => a.command !== item.command);
|
|
436
|
+
this.unregisterUserKeybinding(item);
|
|
437
|
+
this.restoreDefaultKeybinding(item);
|
|
438
|
+
return this.saveKeybinding(filtered);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 从keymaps.json获取快捷键列表
|
|
443
|
+
* @returns {Promise<Keybinding[]>}
|
|
444
|
+
* @memberof KeymapsService
|
|
445
|
+
*/
|
|
446
|
+
async getKeybindings(): Promise<Keybinding[]> {
|
|
447
|
+
return this.storeKeybindings.map((keymap) => ({
|
|
448
|
+
command: keymap.command,
|
|
449
|
+
keybinding: keymap.key,
|
|
450
|
+
when: keymap.when,
|
|
451
|
+
args: keymap.args,
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 打开快捷键面板
|
|
457
|
+
* @protected
|
|
458
|
+
* @returns {Promise<void>}
|
|
459
|
+
* @memberof KeymapService
|
|
460
|
+
*/
|
|
461
|
+
async open(): Promise<void> {
|
|
462
|
+
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, new URI().withScheme(KEYMAPS_SCHEME), {
|
|
463
|
+
preview: true,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
fixed = async () => {
|
|
468
|
+
this.commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, new URI().withScheme(KEYMAPS_SCHEME), {
|
|
469
|
+
preview: false,
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
getWhen(keybinding?: Keybinding | KeymapItem) {
|
|
474
|
+
if (!keybinding) {
|
|
475
|
+
return '';
|
|
476
|
+
}
|
|
477
|
+
return this.keybindingService.convertMonacoWhen(keybinding.when);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 获取可读的作用域
|
|
482
|
+
* @param {KeybindingScope} scope
|
|
483
|
+
* @returns
|
|
484
|
+
* @memberof KeymapService
|
|
485
|
+
*/
|
|
486
|
+
getScope(scope: KeybindingScope) {
|
|
487
|
+
if (scope === KeybindingScope.DEFAULT) {
|
|
488
|
+
return localize('keymaps.source.default');
|
|
489
|
+
} else if (scope === KeybindingScope.USER) {
|
|
490
|
+
return localize('keymaps.source.user');
|
|
491
|
+
} else if (scope === KeybindingScope.WORKSPACE) {
|
|
492
|
+
return localize('keymaps.source.workspace');
|
|
493
|
+
} else {
|
|
494
|
+
return '';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* 获取所有快捷键项
|
|
500
|
+
* @returns {KeybindingItem[]}
|
|
501
|
+
* @memberof KeymapService
|
|
502
|
+
*/
|
|
503
|
+
getKeybindingItems(): KeybindingItem[] {
|
|
504
|
+
const commands = this.commandRegistry.getCommands();
|
|
505
|
+
const items: KeybindingItem[] = [];
|
|
506
|
+
for (const command of commands) {
|
|
507
|
+
const keybindings = this.keybindingRegistry.getKeybindingsForCommand(command.id);
|
|
508
|
+
if (!keybindings || !keybindings.length) {
|
|
509
|
+
// 针对带有label的Command,在快捷键面板上添加可配置按钮
|
|
510
|
+
if (command.label) {
|
|
511
|
+
items.push({
|
|
512
|
+
id: command.id,
|
|
513
|
+
command: command.label,
|
|
514
|
+
hasCommandLabel: true,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
keybindings.forEach((kd: ScopedKeybinding) => {
|
|
519
|
+
let item: KeybindingItem;
|
|
520
|
+
if (this.storeKeybindings) {
|
|
521
|
+
const isUserKeybinding = this.storeKeybindings.find((kb) => command && kb.command === command.id);
|
|
522
|
+
item = {
|
|
523
|
+
id: command.id,
|
|
524
|
+
command: command.label || command.id,
|
|
525
|
+
keybinding: isUserKeybinding
|
|
526
|
+
? isUserKeybinding.key
|
|
527
|
+
: kd
|
|
528
|
+
? this.keybindingRegistry.acceleratorFor(kd, '+').join(' ')
|
|
529
|
+
: '',
|
|
530
|
+
when: isUserKeybinding ? this.getWhen(isUserKeybinding) : this.getWhen(keybindings && kd),
|
|
531
|
+
source: isUserKeybinding ? this.getScope(KeybindingScope.USER) : this.getScope(KeybindingScope.DEFAULT),
|
|
532
|
+
hasCommandLabel: !!command.label,
|
|
533
|
+
};
|
|
534
|
+
} else {
|
|
535
|
+
item = {
|
|
536
|
+
id: command.id,
|
|
537
|
+
command: command.label || command.id,
|
|
538
|
+
keybinding: keybindings && kd ? this.keybindingRegistry.acceleratorFor(kd, '+').join(' ') : '',
|
|
539
|
+
when: this.getWhen(keybindings && kd),
|
|
540
|
+
source: keybindings && kd && typeof kd.scope !== 'undefined' ? this.getScope(kd.scope!) : '',
|
|
541
|
+
hasCommandLabel: !!command.label,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
items.push(item);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 获取排序后的列表
|
|
550
|
+
const sorted: KeybindingItem[] = items.sort((a: KeybindingItem, b: KeybindingItem) => this.compareItem(a, b));
|
|
551
|
+
// 获取定义了快捷键的列表
|
|
552
|
+
const keyItems: KeybindingItem[] = sorted.filter((a: KeybindingItem) => !!a.keybinding);
|
|
553
|
+
|
|
554
|
+
const otherItems: KeybindingItem[] = sorted.filter((a: KeybindingItem) => !a.keybinding && a.hasCommandLabel);
|
|
555
|
+
|
|
556
|
+
// 让带有 Label 的快捷键命令排序靠前
|
|
557
|
+
const withLabelItems = keyItems.filter((keybinding) => keybinding.hasCommandLabel);
|
|
558
|
+
const withoutLabelItems = keyItems.filter((keybinding) => !keybinding.hasCommandLabel);
|
|
559
|
+
|
|
560
|
+
return [...withLabelItems, ...withoutLabelItems, ...otherItems];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 字典排序
|
|
564
|
+
protected compareItem(a: KeybindingItem, b: KeybindingItem): number {
|
|
565
|
+
if (a && b) {
|
|
566
|
+
if (a.source === b.source) {
|
|
567
|
+
return a.command.toLowerCase().localeCompare(b.command.toLowerCase());
|
|
568
|
+
} else if (a.source === this.getScope(KeybindingScope.USER)) {
|
|
569
|
+
return -1;
|
|
570
|
+
} else {
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* 搜索快捷键
|
|
579
|
+
*/
|
|
580
|
+
searchKeybindings = (search: string) => {
|
|
581
|
+
this.currentSearchValue = search;
|
|
582
|
+
// throttle
|
|
583
|
+
if (!this.searchDelayer.isTriggered) {
|
|
584
|
+
this.searchDelayer.cancel();
|
|
585
|
+
}
|
|
586
|
+
this.searchDelayer.trigger(async () => {
|
|
587
|
+
this.doSearchKeybindings(this.currentSearchValue);
|
|
588
|
+
});
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
private isSearching = false;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 模糊搜索匹配的快捷键
|
|
595
|
+
* @protected
|
|
596
|
+
*/
|
|
597
|
+
protected readonly doSearchKeybindings = (search) => {
|
|
598
|
+
if (search) {
|
|
599
|
+
this.isSearching = true;
|
|
600
|
+
} else {
|
|
601
|
+
this.isSearching = false;
|
|
602
|
+
}
|
|
603
|
+
const items = this.getKeybindingItems();
|
|
604
|
+
const result: KeybindingItem[] = [];
|
|
605
|
+
items.forEach((item) => {
|
|
606
|
+
const keys: string[] = ['id', 'command', 'keybinding', 'when', 'context', 'source'];
|
|
607
|
+
let matched = false;
|
|
608
|
+
for (const key of keys) {
|
|
609
|
+
const str = item[key];
|
|
610
|
+
if (str) {
|
|
611
|
+
const fuzzyMatch = fuzzy.match(search, str as string, this.fuzzyOptions);
|
|
612
|
+
if (fuzzyMatch) {
|
|
613
|
+
item[key] = fuzzyMatch.rendered;
|
|
614
|
+
matched = true;
|
|
615
|
+
} else {
|
|
616
|
+
// 匹配到的快捷键会有不同的显示优先级
|
|
617
|
+
// 排序
|
|
618
|
+
if (key === 'keybinding') {
|
|
619
|
+
const queryItems = search.split('+');
|
|
620
|
+
// 处理组合键
|
|
621
|
+
const tempItems = str.split(' ');
|
|
622
|
+
// 存储空格字符串
|
|
623
|
+
const spaceIndexArr = [0];
|
|
624
|
+
let bindingItems: string[] = [];
|
|
625
|
+
if (tempItems.length > 1) {
|
|
626
|
+
tempItems.forEach((tItem) => {
|
|
627
|
+
const tKeys = tItem.split('+');
|
|
628
|
+
spaceIndexArr.push(tKeys.length + spaceIndexArr[-1]);
|
|
629
|
+
bindingItems.push(...tKeys);
|
|
630
|
+
});
|
|
631
|
+
} else {
|
|
632
|
+
bindingItems = str.split('+');
|
|
633
|
+
}
|
|
634
|
+
spaceIndexArr.shift();
|
|
635
|
+
|
|
636
|
+
const renderedResult = [...bindingItems];
|
|
637
|
+
let matchCounter = 0;
|
|
638
|
+
|
|
639
|
+
queryItems.forEach((queryItem) => {
|
|
640
|
+
let keyFuzzyMatch: fuzzy.MatchResult = { rendered: '', score: 0 };
|
|
641
|
+
let keyIndex = -1;
|
|
642
|
+
if (str) {
|
|
643
|
+
bindingItems.forEach((bindingItem: string) => {
|
|
644
|
+
// 通过用户输入匹配所有快捷键字段
|
|
645
|
+
const tempFuzzyMatch = fuzzy.match(queryItem, bindingItem, this.fuzzyOptions);
|
|
646
|
+
// 选择匹配度的匹配项
|
|
647
|
+
if (tempFuzzyMatch && tempFuzzyMatch.score > keyFuzzyMatch.score) {
|
|
648
|
+
keyFuzzyMatch = tempFuzzyMatch;
|
|
649
|
+
// 获取在快捷键数组中对应的位置
|
|
650
|
+
keyIndex = renderedResult.indexOf(bindingItem);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const keyRendered = keyFuzzyMatch.rendered;
|
|
655
|
+
if (keyRendered) {
|
|
656
|
+
if (keyIndex > -1) {
|
|
657
|
+
renderedResult[keyIndex] = keyRendered;
|
|
658
|
+
}
|
|
659
|
+
// 在快捷键数组中移除匹配过的快捷键
|
|
660
|
+
bindingItems.splice(keyIndex, 1, '');
|
|
661
|
+
matchCounter += 1;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
if (matchCounter === queryItems.length) {
|
|
666
|
+
// 处理组合键的渲染
|
|
667
|
+
if (spaceIndexArr.length > 0) {
|
|
668
|
+
const chordRenderedResult = '';
|
|
669
|
+
renderedResult.forEach((resultKey, index) => {
|
|
670
|
+
if (index === 0) {
|
|
671
|
+
chordRenderedResult.concat(resultKey);
|
|
672
|
+
} else if (spaceIndexArr.indexOf(index) !== -1) {
|
|
673
|
+
chordRenderedResult.concat(' ' + resultKey);
|
|
674
|
+
} else {
|
|
675
|
+
chordRenderedResult.concat('+' + resultKey);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
item[key] = chordRenderedResult;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
item[key] = renderedResult.join('+');
|
|
682
|
+
matched = true;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (matched) {
|
|
690
|
+
result.push(item);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
this.keybindings = result;
|
|
694
|
+
this.keymapChangeEmitter.fire(this.keybindings);
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 验证快捷键是否可用
|
|
699
|
+
* @memberof KeymapService
|
|
700
|
+
*/
|
|
701
|
+
validateKeybinding = (keybindingItem: KeybindingItem, keybinding: string): string => {
|
|
702
|
+
if (!keybinding) {
|
|
703
|
+
return localize('keymaps.keybinding.require');
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
const binding = {
|
|
707
|
+
command: this.isSearching ? this.getRaw(keybindingItem.command) : keybindingItem.command,
|
|
708
|
+
when: this.isSearching ? this.getRaw(keybindingItem.when) : keybindingItem.when,
|
|
709
|
+
keybinding: this.isSearching ? this.getRaw(keybinding) : keybinding,
|
|
710
|
+
};
|
|
711
|
+
if (keybindingItem.keybinding === keybinding) {
|
|
712
|
+
return ' ';
|
|
713
|
+
}
|
|
714
|
+
return this.keybindingRegistry.validateKeybindingInScope(binding);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
return error;
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
detectKeybindings = (keybindingItem: KeybindingItem, keybinding: string): KeybindingItem[] => {
|
|
721
|
+
if (!keybinding) {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
if (keybindingItem.keybinding === keybinding) {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
// 可能匹配了高亮结果,需要还原部分数据
|
|
729
|
+
const keybindings = this.keybindings.filter((kb) => this.getRaw(kb.keybinding) === keybinding);
|
|
730
|
+
// 只返回全匹配快捷键,不返回包含关系快捷键(包含关系会在保存前提示冲突)
|
|
731
|
+
if (keybindings.length > 0) {
|
|
732
|
+
return keybindings.map((binding) => {
|
|
733
|
+
const command = this.commandRegistry.getCommand(this.getRaw(binding.command));
|
|
734
|
+
const isUserKeybinding = this.storeKeybindings.find((kb) => command && kb.command === command.id);
|
|
735
|
+
binding.when = this.getRaw(binding.when);
|
|
736
|
+
binding.keybinding = this.getRaw(binding.keybinding);
|
|
737
|
+
return {
|
|
738
|
+
id: command ? command.id : this.getRaw(binding.command),
|
|
739
|
+
command: (command ? command.label || command.id : binding.command) || '',
|
|
740
|
+
when: this.keybindingService.convertMonacoWhen(binding.when),
|
|
741
|
+
keybinding: this.getRaw(binding.keybinding),
|
|
742
|
+
source: isUserKeybinding ? this.getScope(KeybindingScope.USER) : this.getScope(KeybindingScope.DEFAULT),
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
} catch (error) {
|
|
747
|
+
this.logger.error(error);
|
|
748
|
+
}
|
|
749
|
+
return [];
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 获取被fuzzy替换的原始值
|
|
754
|
+
* @param {string} keybinding
|
|
755
|
+
*/
|
|
756
|
+
getRaw(keybinding?: string) {
|
|
757
|
+
if (!keybinding) {
|
|
758
|
+
return '';
|
|
759
|
+
}
|
|
760
|
+
return keybinding.replace(new RegExp(/<(\/)?match>/gi), '');
|
|
761
|
+
}
|
|
762
|
+
}
|