@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.
@@ -0,0 +1,428 @@
1
+ import cls from 'classnames';
2
+ import React, { useCallback, useEffect, useMemo } from 'react';
3
+
4
+ import { Input, ValidateInput, VALIDATE_TYPE, ValidateMessage } from '@opensumi/ide-components';
5
+ import { RecycleList } from '@opensumi/ide-components';
6
+ import {
7
+ localize,
8
+ useInjectable,
9
+ KeybindingScope,
10
+ NO_KEYBINDING_NAME,
11
+ KeyCode,
12
+ Key,
13
+ formatLocalize,
14
+ } from '@opensumi/ide-core-browser';
15
+ import { getIcon } from '@opensumi/ide-core-browser';
16
+ import { ReactEditorComponent } from '@opensumi/ide-editor/lib/browser';
17
+
18
+ import { IKeymapService, KeybindingItem } from '../common';
19
+
20
+ import styles from './keymaps.module.less';
21
+ import { KeymapService } from './keymaps.service';
22
+
23
+ export const KeymapsView: ReactEditorComponent<null> = () => {
24
+ const {
25
+ keybindings: defaultKeybindings,
26
+ searchKeybindings,
27
+ validateKeybinding,
28
+ detectKeybindings,
29
+ setKeybinding,
30
+ resetKeybinding,
31
+ getRaw,
32
+ getScope,
33
+ covert,
34
+ clearCovert,
35
+ fixed,
36
+ onDidKeymapChanges,
37
+ updateKeybindings,
38
+ }: KeymapService = useInjectable(IKeymapService);
39
+ const [activeKeyboardSearch, setActiveKeyboardSearch] = React.useState<boolean>(false);
40
+
41
+ const [search, setSearch] = React.useState<string>('');
42
+ const [keybindings, setKeybindings] = React.useState<KeybindingItem[]>(defaultKeybindings);
43
+
44
+ const template = ({ data, index }) => {
45
+ const { id, command, when, source, keybinding }: KeybindingItem = data;
46
+ const [isEditing, setIsEditing] = React.useState<boolean>(false);
47
+ const [value, setValue] = React.useState<string>(keybinding || '');
48
+ const [isDirty, setIsDirty] = React.useState<boolean>(false);
49
+ const [validateMessage, setValidateMessage] = React.useState<ValidateMessage>();
50
+ const [detectiveKeybindings, setDetectiveKeybindings] = React.useState<KeybindingItem[]>([]);
51
+ const clickHandler = () => {
52
+ // 修改时固定设置页面
53
+ if (!isDirty) {
54
+ fixed();
55
+ setIsDirty(true);
56
+ }
57
+ clearCovert();
58
+ // 每次keybinding编辑的时候还原快捷键文本
59
+ setValue(getRaw(keybinding));
60
+ setIsEditing(true);
61
+ };
62
+
63
+ const updateKeybinding = (value: string) => {
64
+ const validateMessage = validateKeybinding(data, value);
65
+ if (validateMessage) {
66
+ // ' ' 表示快捷键未修改
67
+ if (validateMessage !== ' ') {
68
+ setValidateMessage({
69
+ message: validateMessage,
70
+ type: VALIDATE_TYPE.ERROR,
71
+ });
72
+ } else {
73
+ setIsEditing(false);
74
+ }
75
+ } else {
76
+ setKeybinding(
77
+ {
78
+ command: getRaw(id),
79
+ when: getRaw(when) || '',
80
+ keybinding: getRaw(keybinding),
81
+ },
82
+ {
83
+ command: getRaw(id),
84
+ when: getRaw(when) || '',
85
+ keybinding: value,
86
+ },
87
+ );
88
+ setIsEditing(false);
89
+ clearCovert();
90
+ }
91
+ };
92
+
93
+ const blurHandler = () => {
94
+ setIsEditing(false);
95
+ };
96
+
97
+ const keydownHandler = (event: React.KeyboardEvent) => {
98
+ event.stopPropagation();
99
+ event.preventDefault();
100
+ const { key } = KeyCode.createKeyCode(event.nativeEvent);
101
+ const hasModifyKey =
102
+ event.nativeEvent.shiftKey ||
103
+ event.nativeEvent.metaKey ||
104
+ event.nativeEvent.altKey ||
105
+ event.nativeEvent.ctrlKey;
106
+ if (key && Key.ENTER.keyCode === key.keyCode && !hasModifyKey) {
107
+ if (value) {
108
+ updateKeybinding(value);
109
+ }
110
+ } else {
111
+ setValue(covert(event.nativeEvent));
112
+ }
113
+ };
114
+
115
+ const renderOptionalActions = () => {
116
+ const clear = () => {
117
+ setValidateMessage(undefined);
118
+ if (value) {
119
+ setValue('');
120
+ }
121
+ clearCovert();
122
+ };
123
+ const preventMouseDown = (event) => {
124
+ event.stopPropagation();
125
+ event.preventDefault();
126
+ };
127
+ return (
128
+ <div className={styles.keybinding_optional_actions} onMouseDown={preventMouseDown}>
129
+ <span
130
+ className={cls(getIcon('close-circle-fill'), styles.keybinding_optional_action)}
131
+ onClick={clear}
132
+ title={localize('keymaps.action.clear')}
133
+ ></span>
134
+ </div>
135
+ );
136
+ };
137
+
138
+ const renderPlaceholder = () => <div className={styles.keybinding_key_input_placeholder}>⏎</div>;
139
+
140
+ const renderReset = (source?: string) => {
141
+ const reset = (event) => {
142
+ event.preventDefault();
143
+ // 修改时固定设置页面
144
+ if (!isDirty) {
145
+ fixed();
146
+ setIsDirty(true);
147
+ }
148
+ resetKeybinding({
149
+ command: getRaw(id),
150
+ when: getRaw(when) || '',
151
+ keybinding: value,
152
+ });
153
+ };
154
+ // 重置快捷键作用域
155
+ if (source && getRaw(source) === getScope(KeybindingScope.USER)) {
156
+ return (
157
+ <span
158
+ className={cls(getIcon('rollback'), styles.keybinding_inline_action)}
159
+ onClick={reset}
160
+ title={localize('keymaps.action.reset')}
161
+ ></span>
162
+ );
163
+ }
164
+ };
165
+
166
+ const renderDetectiveKeybindings = () => {
167
+ if (!validateMessage && detectiveKeybindings.length > 0) {
168
+ return (
169
+ <div className={styles.keybinding_detective_messages}>
170
+ <div className={styles.keybinding_detective_messages_label}>
171
+ {formatLocalize('keymaps.keybinding.duplicate', detectiveKeybindings.length)}
172
+ </div>
173
+ <ul className={styles.keybinding_detective_messages_container}>
174
+ {detectiveKeybindings.map((keybinding: KeybindingItem, index: number) => (
175
+ <li
176
+ className={styles.keybinding_detective_messages_item}
177
+ key={`${keybinding.id}_${index}`}
178
+ title={`${keybinding.command}-${keybinding.when}`}
179
+ >
180
+ <div className={styles.title}>
181
+ {localize('keymaps.header.command.title')}: {getRaw(keybinding.command) || '-'}
182
+ </div>
183
+ <div className={styles.description}>
184
+ <div style={{ marginRight: 4 }}>
185
+ {localize('keymaps.header.source.title')}: {getRaw(keybinding.source) || '-'}
186
+ </div>
187
+ <div>
188
+ {localize('keymaps.header.when.title')}: {getRaw(keybinding.when) || '—'}
189
+ </div>
190
+ </div>
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ </div>
195
+ );
196
+ }
197
+ };
198
+
199
+ const renderKeybinding = () => {
200
+ if (isEditing) {
201
+ return (
202
+ <div className={styles.keybinding_key_input_container}>
203
+ {renderOptionalActions()}
204
+ <ValidateInput
205
+ placeholder={localize('keymaps.edit.placeholder')}
206
+ validateMessage={validateMessage}
207
+ className={styles.keybinding_key_input}
208
+ size='small'
209
+ autoFocus={true}
210
+ name={NO_KEYBINDING_NAME}
211
+ value={value}
212
+ onKeyDown={keydownHandler}
213
+ onBlur={blurHandler}
214
+ />
215
+ {renderPlaceholder()}
216
+ {renderDetectiveKeybindings()}
217
+ </div>
218
+ );
219
+ } else {
220
+ const keyBlocks = keybinding?.split(' ');
221
+ return (
222
+ <div className={styles.keybinding_key} title={getRaw(keybinding)} onDoubleClick={clickHandler}>
223
+ <div className={styles.keybinding_action} onClick={clickHandler}>
224
+ <span
225
+ className={cls(keybinding ? getIcon('edit') : getIcon('plus'), styles.keybinding_inline_action)}
226
+ title={keybinding ? localize('keymaps.action.edit') : localize('keymaps.action.add')}
227
+ ></span>
228
+ {renderReset(source)}
229
+ </div>
230
+ {keyBlocks && !!keyBlocks[0]
231
+ ? keyBlocks.map((block, index) => {
232
+ const keys = block.split('+');
233
+ return (
234
+ <div className={styles.keybinding_key_block} key={`${block}_${index}`}>
235
+ {keys.map((key, index) => (
236
+ <div
237
+ className={styles.keybinding_key_item}
238
+ key={`${key}_${index}`}
239
+ dangerouslySetInnerHTML={{ __html: key || '' }}
240
+ ></div>
241
+ ))}
242
+ </div>
243
+ );
244
+ })
245
+ : '—'}
246
+ </div>
247
+ );
248
+ }
249
+ };
250
+
251
+ useEffect(() => {
252
+ // 当值变化时清空错误信息
253
+ if (validateMessage) {
254
+ setValidateMessage(undefined);
255
+ }
256
+ // 根据快捷键查当前绑定的命令
257
+ if (value && isEditing) {
258
+ setDetectiveKeybindings(detectKeybindings(data, value));
259
+ } else {
260
+ setDetectiveKeybindings([]);
261
+ }
262
+ }, [value]);
263
+
264
+ return (
265
+ <div className={cls(styles.keybinding_list_item, index % 2 === 1 && styles.odd)}>
266
+ <div className={cls(styles.keybinding_list_item_box, styles.keybinding_command)}>
267
+ <div
268
+ className={styles.command_name}
269
+ title={getRaw(command)}
270
+ dangerouslySetInnerHTML={{ __html: command }}
271
+ ></div>
272
+ <div
273
+ className={cls(styles.limit_warp, styles.command_id)}
274
+ title={getRaw(id)}
275
+ dangerouslySetInnerHTML={{ __html: formatLocalize('keymaps.commandId.title', id) }}
276
+ ></div>
277
+ </div>
278
+ <div className={cls(styles.keybinding_list_item_box)}>{renderKeybinding()}</div>
279
+ <div className={styles.keybinding_list_item_box}>
280
+ <div
281
+ className={styles.limit_warp}
282
+ title={getRaw(when || '—')}
283
+ dangerouslySetInnerHTML={{ __html: when || '—' }}
284
+ ></div>
285
+ </div>
286
+ <div className={styles.keybinding_list_item_box}>
287
+ <div title={getRaw(source)} dangerouslySetInnerHTML={{ __html: source || '' }}></div>
288
+ </div>
289
+ </div>
290
+ );
291
+ };
292
+
293
+ const renderInputPlaceholder = () => {
294
+ const activeKeyboard = () => {
295
+ setActiveKeyboardSearch(!activeKeyboardSearch);
296
+ };
297
+ return (
298
+ <div className={styles.search_inline_action}>
299
+ <span
300
+ className={cls(getIcon('keyboard'), styles.search_inline_action_icon, activeKeyboardSearch && styles.active)}
301
+ onClick={activeKeyboard}
302
+ ></span>
303
+ </div>
304
+ );
305
+ };
306
+
307
+ const handleSearchChange = useCallback(
308
+ (value: string) => {
309
+ setSearch(value);
310
+ searchKeybindings(value);
311
+ },
312
+ [search],
313
+ );
314
+
315
+ const onChangeHandler = (event) => {
316
+ if (!activeKeyboardSearch) {
317
+ const value = event.target && event.target.value ? event.target.value.toLocaleLowerCase() : '';
318
+ handleSearchChange(value);
319
+ }
320
+ };
321
+
322
+ const onKeyDownHandler = (event) => {
323
+ if (activeKeyboardSearch) {
324
+ event.stopPropagation();
325
+ event.preventDefault();
326
+ const { key } = KeyCode.createKeyCode(event.nativeEvent);
327
+ if (key && Key.ENTER.keyCode === key.keyCode) {
328
+ // 屏蔽回车键作为快捷键搜索
329
+ return;
330
+ } else {
331
+ handleSearchChange(covert(event.nativeEvent));
332
+ }
333
+ }
334
+ };
335
+
336
+ const clearSearch = () => {
337
+ if (search) {
338
+ setSearch('');
339
+ searchKeybindings('');
340
+ }
341
+ clearCovert();
342
+ };
343
+
344
+ const renderOptionalActions = () => (
345
+ <div className={styles.keybinding_optional_actions}>
346
+ <span
347
+ className={cls(getIcon('close-circle-fill'), styles.keybinding_optional_action)}
348
+ onClick={clearSearch}
349
+ title={localize('keymaps.action.reset')}
350
+ ></span>
351
+ </div>
352
+ );
353
+
354
+ const renderSearchInput = () => (
355
+ <div className={styles.search_container}>
356
+ <Input
357
+ className={styles.search_input}
358
+ placeholder={localize(
359
+ activeKeyboardSearch ? 'keymaps.search.keyboard.placeholder' : 'keymaps.search.placeholder',
360
+ )}
361
+ type='text'
362
+ value={search}
363
+ name={NO_KEYBINDING_NAME}
364
+ onChange={onChangeHandler}
365
+ onKeyDown={onKeyDownHandler}
366
+ addonBefore={renderInputPlaceholder()}
367
+ />
368
+ {renderOptionalActions()}
369
+ </div>
370
+ );
371
+
372
+ const KeybindingHeader = useMemo(() => {
373
+ const headers = [
374
+ {
375
+ title: localize('keymaps.header.command.title'),
376
+ classname: styles.keybinding_header_item,
377
+ },
378
+ {
379
+ title: localize('keymaps.header.keybinding.title'),
380
+ classname: styles.keybinding_header_item,
381
+ },
382
+ {
383
+ title: localize('keymaps.header.when.title'),
384
+ classname: styles.keybinding_header_item,
385
+ },
386
+ {
387
+ title: localize('keymaps.header.source.title'),
388
+ classname: styles.keybinding_header_item,
389
+ },
390
+ ];
391
+ return (
392
+ <div className={styles.keybinding_header}>
393
+ {headers.map((h, index) => (
394
+ <div className={h.classname} key={`${h.title}_${index}`}>
395
+ {h.title}
396
+ </div>
397
+ ))}
398
+ </div>
399
+ );
400
+ }, []);
401
+
402
+ useEffect(() => {
403
+ const dispose = onDidKeymapChanges((kbs) => {
404
+ setKeybindings(kbs);
405
+ });
406
+ updateKeybindings();
407
+ return () => {
408
+ dispose.dispose();
409
+ };
410
+ }, []);
411
+
412
+ return (
413
+ <div className={styles.keybinding_container}>
414
+ <div className={styles.keybinding_searchbar}>{renderSearchInput()}</div>
415
+ <div className={styles.keybinding_body}>
416
+ {KeybindingHeader}
417
+ <div className={styles.keybinding_list}>
418
+ <RecycleList
419
+ itemHeight={40}
420
+ data={keybindings}
421
+ template={template}
422
+ className={styles.keybinding_list_container}
423
+ />
424
+ </div>
425
+ </div>
426
+ </div>
427
+ );
428
+ };
@@ -0,0 +1,3 @@
1
+ export const KEYMAPS_SCHEME = 'keymaps';
2
+
3
+ export const KEYMAPS_FILE_NAME = 'keymaps.json';
@@ -0,0 +1,2 @@
1
+ export * from './const';
2
+ export * from './keymaps';
@@ -0,0 +1,118 @@
1
+ // 快捷键相关功能为纯前端模块,这里直接从browser引入定义
2
+ import { Keybinding, IDisposable } from '@opensumi/ide-core-browser';
3
+
4
+ export const IKeymapService = Symbol('IKeymapService');
5
+
6
+ /**
7
+ * 从 keymap.json 读取的值
8
+ */
9
+ export interface KeymapItem {
10
+ /**
11
+ * 快捷键
12
+ */
13
+ key: string;
14
+ /**
15
+ * 快捷键
16
+ * @deprecated 为了兼容老格式,这个字段还保留
17
+ */
18
+ keybinding?: string;
19
+ /**
20
+ * 命令 id
21
+ */
22
+ command: string;
23
+ /**
24
+ * When条件语句
25
+ */
26
+ when?: string;
27
+ /**
28
+ * Context条件语句
29
+ */
30
+ context?: string;
31
+ /**
32
+ * 命令参数
33
+ */
34
+ args?: Record<string, string>;
35
+ }
36
+
37
+ export interface KeybindingItem extends Omit<KeymapItem, 'key'> {
38
+ id: string;
39
+ /**
40
+ * 快捷键
41
+ */
42
+ keybinding?: string;
43
+ /**
44
+ * 作用域
45
+ */
46
+ source?: string;
47
+ /**
48
+ * 判断快捷键是否带有command label
49
+ */
50
+ hasCommandLabel?: boolean;
51
+ }
52
+
53
+ export interface IKeymapService {
54
+ /**
55
+ * 快捷键是否初始化完成
56
+ */
57
+ whenReady: Promise<void>;
58
+ /**
59
+ * 初始化快捷键注册信息
60
+ */
61
+ init(): Promise<void>;
62
+ /**
63
+ * 设置快捷键
64
+ * @param {Keybinding} rawKeybinding
65
+ * @param {Keybinding} keybinding
66
+ * @returns {Promise<void>}
67
+ * @memberof KeymapsService
68
+ */
69
+ setKeybinding(rawKeybinding: Keybinding, keybinding: Keybinding): Promise<void>;
70
+
71
+ /**
72
+ * 移除给定ID的快捷键绑定
73
+ * @param {Keybinding} keybinding
74
+ * @returns {Promise<void>}
75
+ * @memberof KeymapsService
76
+ */
77
+ resetKeybinding(keybinding: Keybinding): Promise<void>;
78
+
79
+ /**
80
+ * 从keymaps.json获取快捷键列表
81
+ * @returns {Promise<KeybindingJson[]>}
82
+ * @memberof KeymapsService
83
+ */
84
+ getKeybindings(): Promise<Keybinding[]>;
85
+
86
+ /**
87
+ * 打开快捷键面板
88
+ * @returns {Promise<void>}
89
+ * @memberof IKeymapService
90
+ */
91
+ open(): Promise<void>;
92
+
93
+ /**
94
+ * 固定快捷键面板
95
+ * @returns {Promise<void>}
96
+ * @memberof IKeymapService
97
+ */
98
+ fixed(): Promise<void>;
99
+
100
+ /**
101
+ * 打开快捷键源文件 keymaps.json
102
+ * @returns {Promise<void>}
103
+ * @memberof IKeymapService
104
+ */
105
+ openResource(): Promise<void>;
106
+
107
+ /**
108
+ * 监听快捷键改变完成后事件
109
+ * @returns {void}
110
+ * @memberof IKeymapService
111
+ */
112
+ onDidKeymapChanges(listener: () => any): IDisposable;
113
+
114
+ /**
115
+ * 更新快捷键列表
116
+ */
117
+ updateKeybindings(): void;
118
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './common';