forked from jupyterlab/jupyterlab
/
index.ts
224 lines (195 loc) · 6.82 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { ISettingRegistry, SettingRegistry } from '@jupyterlab/coreutils';
import { CommandRegistry } from '@lumino/commands';
import {
JSONExt,
ReadonlyJSONObject,
ReadonlyJSONValue
} from '@lumino/coreutils';
import { DisposableSet, IDisposable } from '@lumino/disposable';
/**
* The default shortcuts extension.
*
* #### Notes
* Shortcut values are stored in the setting system. The default values for each
* shortcut are preset in the settings schema file of this extension.
* Additionally, each shortcut can be individually set by the end user by
* modifying its setting (either in the text editor or by modifying its
* underlying JSON schema file).
*
* When setting shortcut selectors, there are two concepts to consider:
* specificity and matchability. These two interact in sometimes
* counterintuitive ways. Keyboard events are triggered from an element and
* they propagate up the DOM until they reach the `documentElement` (`<body>`).
*
* When a registered shortcut sequence is fired, the shortcut manager checks
* the node that fired the event and each of its ancestors until a node matches
* one or more registered selectors. The *first* matching selector in the
* chain of ancestors will invoke the shortcut handler and the traversal will
* end at that point. If a node matches more than one selector, the handler for
* whichever selector is more *specific* fires.
* @see https://www.w3.org/TR/css3-selectors/#specificity
*
* The practical consequence of this is that a very broadly matching selector,
* e.g. `'*'` or `'div'` may match and therefore invoke a handler *before* a
* more specific selector. The most common pitfall is to use the universal
* (`'*'`) selector. For almost any use case where a global keyboard shortcut is
* required, using the `'body'` selector is more appropriate.
*/
const shortcuts: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab/shortcuts-extension:shortcuts',
requires: [ISettingRegistry],
activate: async (app: JupyterFrontEnd, registry: ISettingRegistry) => {
const { commands } = app;
let canonical: ISettingRegistry.ISchema;
let loaded: { [name: string]: ISettingRegistry.IShortcut[] } = {};
/**
* Populate the plugin's schema defaults.
*/
function populate(schema: ISettingRegistry.ISchema) {
const commands = app.commands.listCommands().join('\n');
loaded = {};
schema.properties.shortcuts.default = Object.keys(registry.plugins)
.map(plugin => {
let shortcuts =
registry.plugins[plugin].schema['jupyter.lab.shortcuts'] || [];
loaded[plugin] = shortcuts;
return shortcuts;
})
.reduce((acc, val) => acc.concat(val), [])
.sort((a, b) => a.command.localeCompare(b.command));
schema.properties.shortcuts.description = `Note: To disable a system default shortcut,
copy it to User Preferences and add the
"disabled" key, for example:
{
"command": "application:activate-next-tab",
"keys": [
"Ctrl Shift ]"
],
"selector": "body",
"disabled": true
}
List of commands followed by keyboard shortcuts:
${commands}
List of keyboard shortcuts:`;
}
registry.pluginChanged.connect(async (sender, plugin) => {
if (plugin !== shortcuts.id) {
// If the plugin changed its shortcuts, reload everything.
let oldShortcuts = loaded[plugin];
let newShortcuts =
registry.plugins[plugin].schema['jupyter.lab.shortcuts'] || [];
if (
oldShortcuts === undefined ||
!JSONExt.deepEqual(oldShortcuts, newShortcuts)
) {
canonical = null;
await registry.reload(shortcuts.id);
}
}
});
// Transform the plugin object to return different schema than the default.
registry.transform(shortcuts.id, {
compose: plugin => {
// Only override the canonical schema the first time.
if (!canonical) {
canonical = JSONExt.deepCopy(plugin.schema);
populate(canonical);
}
const defaults = canonical.properties.shortcuts.default;
const user = {
shortcuts: ((plugin.data && plugin.data.user) || {}).shortcuts || []
};
const composite = {
shortcuts: SettingRegistry.reconcileShortcuts(
defaults,
user.shortcuts as ISettingRegistry.IShortcut[]
)
};
plugin.data = { composite, user };
return plugin;
},
fetch: plugin => {
// Only override the canonical schema the first time.
if (!canonical) {
canonical = JSONExt.deepCopy(plugin.schema);
populate(canonical);
}
return {
data: plugin.data,
id: plugin.id,
raw: plugin.raw,
schema: canonical,
version: plugin.version
};
}
});
try {
// Repopulate the canonical variable after the setting registry has
// preloaded all initial plugins.
canonical = null;
const settings = await registry.load(shortcuts.id);
Private.loadShortcuts(commands, settings.composite);
settings.changed.connect(() => {
Private.loadShortcuts(commands, settings.composite);
});
} catch (error) {
console.error(`Loading ${shortcuts.id} failed.`, error);
}
},
autoStart: true
};
/**
* Export the shortcut plugin as default.
*/
export default shortcuts;
/**
* A namespace for private module data.
*/
namespace Private {
/**
* The internal collection of currently loaded shortcuts.
*/
let disposables: IDisposable;
/**
* Load the keyboard shortcuts from settings.
*/
export function loadShortcuts(
commands: CommandRegistry,
composite: ReadonlyJSONObject
): void {
const shortcuts = composite.shortcuts as ISettingRegistry.IShortcut[];
if (disposables) {
disposables.dispose();
}
disposables = shortcuts.reduce((acc, val): DisposableSet => {
const options = normalizeOptions(val);
if (options) {
acc.add(commands.addKeyBinding(options));
}
return acc;
}, new DisposableSet());
}
/**
* Normalize potential keyboard shortcut options.
*/
function normalizeOptions(
value: ReadonlyJSONValue | Partial<CommandRegistry.IKeyBindingOptions>
): CommandRegistry.IKeyBindingOptions | undefined {
if (!value || typeof value !== 'object') {
return undefined;
}
const { isArray } = Array;
const valid =
'command' in value &&
'keys' in value &&
'selector' in value &&
isArray((value as Partial<CommandRegistry.IKeyBindingOptions>).keys);
return valid ? (value as CommandRegistry.IKeyBindingOptions) : undefined;
}
}