Skip to content

Commit

Permalink
Add keymaps package
Browse files Browse the repository at this point in the history
This allows the user to rebind commands with custom keybindings in a
file located in the user_storage location i.e
"user_storage:keymaps.json".

It also adds a menu contribution that the user can easily open with
File->Open Keyboard Shortcuts, which in turns calls the user storage
service for the resource.

It also adds easy naming to keybindings and
makes rebinding commands easier by only having to type "ctrl+a" instead
of "MODIFIER.M1+KeyA" or something like that.

Default keybindings are mapped in a structure called KeybindingIndex,
which allows keybindings to be "shadowed" (remapped) by another
KeybindingIndex. This way, eventually users could download an eclipse
keymaps file or an emacs keymaps file which would automatically shadow
the default index (or at least bindings it can shadow).

Signed-off-by: Patrick-Jeffrey Pollo Guilbert <patrick.pollo.guilbert@ericsson.com>
  • Loading branch information
epatpol committed Dec 13, 2017
1 parent 13f789b commit 692bd37
Show file tree
Hide file tree
Showing 17 changed files with 619 additions and 32 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"packages/languages/coverage/lcov.info",
"packages/monaco/coverage/lcov.info",
"packages/navigator/coverage/lcov.info",
"packages/keymaps/coverage/lcov.info",
"packages/preferences-api/coverage/lcov.info",
"packages/preferences/coverage/lcov.info",
"packages/process/coverage/lcov.info",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,15 @@ module.exports = {
{
test: /\\.js$/,
enforce: 'pre',
loader: 'source-map-loader'
loader: 'source-map-loader',
exclude: /jsonc-parser/
},
{
test: /\\.woff(2)?(\\?v=[0-9]\\.[0-9]\\.[0-9])?$/,
loader: "url-loader?limit=10000&mimetype=application/font-woff"
}
],
noParse: /vscode-languageserver-types|vscode-uri/
noParse: /vscode-languageserver-types|vscode-uri|jsonc-parser/
},
resolve: {
extensions: ['.js']${this.ifMonaco(() => `,
Expand Down
5 changes: 3 additions & 2 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"@theia/terminal": "^0.3.0",
"@theia/typescript": "^0.3.1",
"@theia/userstorage": "^0.3.0",
"@theia/workspace": "^0.3.0"
"@theia/workspace": "^0.3.0",
"@theia/keymaps": "^0.3.0"
},
"scripts": {
"prepare": "yarn run clean && yarn build",
Expand All @@ -44,4 +45,4 @@
"devDependencies": {
"@theia/cli": "^0.3.0"
}
}
}
5 changes: 3 additions & 2 deletions examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"@theia/terminal": "^0.3.0",
"@theia/typescript": "^0.3.1",
"@theia/userstorage": "^0.3.0",
"@theia/workspace": "^0.3.0"
"@theia/workspace": "^0.3.0",
"@theia/keymaps": "^0.3.0"
},
"scripts": {
"prepare": "yarn run clean && yarn build",
Expand All @@ -43,4 +44,4 @@
"devDependencies": {
"@theia/cli": "^0.3.0"
}
}
}
2 changes: 1 addition & 1 deletion packages/core/src/browser/widgets/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function addKeyListener<K extends keyof HTMLElementEventMap>(element: HTM
const toDispose = new DisposableCollection();
const keyCode = KeyCode.createKeyCode({ first: keybinding });
toDispose.push(addEventListener(element, 'keydown', e => {
if (KeyCode.createKeyCode(e).equals(keyCode)) {
if (KeyCode.createKeyCode(e) === keyCode) {
action();
e.stopPropagation();
e.preventDefault();
Expand Down
153 changes: 131 additions & 22 deletions packages/core/src/common/keybinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export interface Keybinding {
readonly accelerator?: Accelerator;
}

export enum KeybindingScope {
DEFAULT,
USER,
WORKSPACE,
END
}

export namespace Keybinding {

/**
Expand All @@ -42,7 +49,12 @@ export namespace Keybinding {
};
return JSON.stringify(copy);
}
}

export interface RawKeybinding {
command: string;
keybinding: string;
context?: string;
}

export const KeybindingContribution = Symbol("KeybindingContribution");
Expand Down Expand Up @@ -113,17 +125,18 @@ export class KeybindingContextRegistry {
@injectable()
export class KeybindingRegistry {

private keymaps: Keybinding[][] = [];
static readonly PASSTHROUGH_PSEUDO_COMMAND = "passthrough";
protected readonly keybindings: { [index: string]: Keybinding[] } = {};
protected readonly commands: { [commandId: string]: Keybinding[] } = {};

constructor(
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry,
@inject(KeybindingContextRegistry) protected readonly contextRegistry: KeybindingContextRegistry,
@inject(ContributionProvider) @named(KeybindingContribution)
protected readonly contributions: ContributionProvider<KeybindingContribution>,
@inject(ILogger) protected readonly logger: ILogger
) { }
) {
for (let i = KeybindingScope.DEFAULT; i < KeybindingScope.END; i++) { this.keymaps.push([]); }
}

onStart(): void {
for (const contribution of this.contributions.getContributions()) {
Expand All @@ -138,30 +151,20 @@ export class KeybindingRegistry {
}

/**
* Adds a keybinding to the registry.
* Register a default keybinding to the registry.
*
* @param binding
*/
registerKeybinding(binding: Keybinding) {
const existing = this.keybindings[binding.keyCode.keystroke];
if (existing) {
const collided = existing.filter(b => b.contextId === binding.contextId);
const existingBindings = this.getKeybindingsForKeyCode(binding.keyCode);
if (existingBindings.length > 0) {
const collided = existingBindings.filter(b => b.contextId === binding.contextId);
if (collided.length > 0) {
this.logger.warn('Collided keybinding is ignored; ', Keybinding.stringify(binding), ' collided with ', collided.map(b => Keybinding.stringify(b)).join(', '));
return;
}
}
const { keyCode, commandId } = binding;
const bindings = this.keybindings[keyCode.keystroke] || [];
bindings.push(binding);
this.keybindings[keyCode.keystroke] = bindings;

/* Keep a mapping of the Command -> Key mapping. */
if (!this.isPseudoCommand(commandId)) {
const commands = this.commands[commandId] || [];
commands.push(binding);
this.commands[commandId] = bindings;
}
this.keymaps[KeybindingScope.DEFAULT].push(binding);
}

/**
Expand All @@ -170,7 +173,20 @@ export class KeybindingRegistry {
* @param commandId The ID of the command for which we are looking for keybindings.
*/
getKeybindingsForCommand(commandId: string): Keybinding[] {
return this.commands[commandId] || [];
const result: Keybinding[] = [];

for (let scope = KeybindingScope.END - 1; scope >= KeybindingScope.DEFAULT; scope--) {
this.keymaps[scope].forEach(binding => {
if (binding.commandId === commandId) {
result.push(binding);
}
});

if (result.length > 0) {
return result;
}
}
return result;
}

/**
Expand All @@ -180,11 +196,58 @@ export class KeybindingRegistry {
* @param keyCode The key code for which we are looking for keybindings.
*/
getKeybindingsForKeyCode(keyCode: KeyCode): Keybinding[] {
const bindings = this.keybindings[keyCode.keystroke] || [];
const result: Keybinding[] = [];

for (let scope = KeybindingScope.DEFAULT; scope < KeybindingScope.END; scope++) {
this.keymaps[scope].forEach(binding => {
if (KeyCode.equals(binding.keyCode, keyCode)) {
if (!this.isKeybindingShadowed(scope, binding)) {
result.push(binding);
}
}
});
}
this.sortKeybindingsByPriority(result);
return result;
}

/**
* Returns a list of keybindings for a command in a specific scope
* @param scope specific scope to look for
* @param commandId unique id of the command
*/
getScopedKeybindingsForCommand(scope: KeybindingScope, commandId: string): Keybinding[] {
const result: Keybinding[] = [];

if (scope >= KeybindingScope.END) {
return [];
}

this.keymaps[scope].forEach(binding => {
if (binding.commandId === commandId) {
result.push(binding);
}
});
return result;
}

this.sortKeybindingsByPriority(bindings);
/**
* Returns true if a keybinding is shadowed in a more specific scope i.e bound in user scope but remapped in
* workspace scope means the user keybinding is shadowed.
* @param scope scope of the current keybinding
* @param binding keybinding that will be checked in more specific scopes
*/
isKeybindingShadowed(scope: KeybindingScope, binding: Keybinding): boolean {
if (scope >= KeybindingScope.END) {
return false;
}

const nextScope = ++scope;

return bindings;
if (this.getScopedKeybindingsForCommand(nextScope, binding.commandId).length > 0) {
return true;
}
return this.isKeybindingShadowed(nextScope, binding);
}

/**
Expand Down Expand Up @@ -267,4 +330,50 @@ export class KeybindingRegistry {
isPseudoCommand(commandId: string): boolean {
return commandId === KeybindingRegistry.PASSTHROUGH_PSEUDO_COMMAND;
}

setKeymap(scope: KeybindingScope, rawKeyBindings: RawKeybinding[]) {
const customBindings: Keybinding[] = [];
for (const rawKeyBinding of rawKeyBindings) {
if (this.commandRegistry.getCommand(rawKeyBinding.command)) {
const code = KeyCode.parse(rawKeyBinding.keybinding);
if (code) {
let context: KeybindingContext | undefined;
if (rawKeyBinding.context) {
context = this.contextRegistry.getContext(rawKeyBinding.context);
}

customBindings.push({
commandId: rawKeyBinding.command,
keyCode: code,
contextId: context ? context.id : undefined
});

} else {
this.resetKeybindingsForScope(scope);
return;
}
} else {
this.logger.warn(`Invalid command id: ${rawKeyBinding.command} does not exist, no command will be bound to keybinding: ${rawKeyBinding.keybinding}`);
return;
}
}
this.keymaps[scope] = customBindings;
}

/**
* Reset keybindings for a specific scope
* @param scope scope to reset the keybindings for
*/
resetKeybindingsForScope(scope: KeybindingScope) {
this.keymaps[scope] = [];
}

/**
* Reset keybindings for all scopes(only leaves the default keybindings mapped)
*/
resetKeybindings() {
for (let i = KeybindingScope.DEFAULT + 1; i < KeybindingScope.END; i++) {
this.keymaps[i] = [];
}
}
}
Loading

0 comments on commit 692bd37

Please sign in to comment.