Skip to content

Commit

Permalink
[keybinding] fix #6878: allow a user to change default keybindings
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Jan 14, 2020
1 parent f11045a commit 1a4ba8c
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 65 deletions.
24 changes: 6 additions & 18 deletions packages/keymaps/src/browser/keybindings-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { CommandRegistry, Emitter, Event } from '@theia/core/lib/common';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope, SingleTextInputDialogProps, Key } from '@theia/core/lib/browser';
import { KeymapsParser } from './keymaps-parser';
import { KeymapsService, KeybindingJson } from './keymaps-service';
import { KeymapsService } from './keymaps-service';
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';

/**
Expand Down Expand Up @@ -504,37 +504,25 @@ export class KeybindingWidget extends ReactWidget {
return 0;
}

/**
* Determine if the keybinding currently exists in a user's `keymaps.json`.
*
* @returns `true` if the keybinding exists.
*/
protected keybindingExistsInJson(keybindings: KeybindingJson[], command: string): boolean {
for (let i = 0; i < keybindings.length; i++) {
if (keybindings[i].command === command) {
return true;
}
}
return false;
}

/**
* Prompt users to update the keybinding for the given command.
* @param item the keybinding item.
*/
protected editKeybinding(item: KeybindingItem): void {
const command = this.getRawValue(item.command);
const id = this.getRawValue(item.id);
const keybinding = (item.keybinding) ? this.getRawValue(item.keybinding) : '';
const context = (item.context) ? this.getRawValue(item.context) : '';
const dialog = new EditKeybindingDialog({
title: `Edit Keybinding For ${command}`,
initialValue: keybinding,
validate: newKeybinding => this.validateKeybinding(command, keybinding, newKeybinding),
}, this.keymapsService, item);
dialog.open().then(async newKeybinding => {
if (newKeybinding) {
await this.keymapsService.setKeybinding({ 'command': id, 'keybinding': newKeybinding, 'context': context });
await this.keymapsService.setKeybinding({
command: item.command,
keybinding: newKeybinding,
when: item.context
}, keybinding);
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/keymaps/src/browser/keymaps-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import * as Ajv from 'ajv';
import * as parser from 'jsonc-parser';
import { injectable } from 'inversify';
import { Keybinding } from '@theia/core/lib/browser';
import { Keybinding } from '@theia/core/lib/common/keybinding';

export const keymapsSchema = {
type: 'array',
Expand Down
87 changes: 41 additions & 46 deletions packages/keymaps/src/browser/keymaps-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,13 @@
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { ResourceProvider, Resource } from '@theia/core/lib/common';
import { Keybinding, KeybindingRegistry, KeybindingScope, OpenerService, open, WidgetOpenerOptions, Widget } from '@theia/core/lib/browser';
import { OpenerService, open, WidgetOpenerOptions, Widget } from '@theia/core/lib/browser';
import { KeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding';
import { Keybinding } from '@theia/core/lib/common/keybinding';
import { UserStorageUri } from '@theia/userstorage/lib/browser';
import { KeymapsParser } from './keymaps-parser';
import * as jsoncparser from 'jsonc-parser';
import { Emitter } from '@theia/core/lib/common/';

/**
* Representation of a JSON keybinding.
*/
export interface KeybindingJson {
/**
* The keybinding command.
*/
command: string,
/**
* The actual keybinding.
*/
keybinding: string,
/**
* The keybinding context.
*/
context: string,
}
import { Emitter } from '@theia/core/lib/common/event';

@injectable()
export class KeymapsService {
Expand Down Expand Up @@ -109,25 +93,45 @@ export class KeymapsService {

/**
* Set the keybinding in the JSON.
* @param keybindingJson the JSON keybindings.
* @param newKeybinding the JSON keybindings.
*/
async setKeybinding(keybindingJson: KeybindingJson): Promise<void> {
async setKeybinding(newKeybinding: Keybinding, oldKeybinding: string): Promise<void> {
if (!this.resource.saveContents) {
return;
}
const content = await this.resource.readContents();
const keybindings: KeybindingJson[] = content ? jsoncparser.parse(content) : [];
let updated = false;
for (let i = 0; i < keybindings.length; i++) {
if (keybindings[i].command === keybindingJson.command) {
updated = true;
keybindings[i].keybinding = keybindingJson.keybinding;
const keybindings: Keybinding[] = content ? jsoncparser.parse(content) : [];
let newAdded = false;
let oldRemoved = false;
for (const keybinding of keybindings) {
if (keybinding.command === newKeybinding.command &&
(keybinding.context || '') === (newKeybinding.context || '') &&
(keybinding.when || '') === (newKeybinding.when || '')) {
newAdded = true;
keybinding.keybinding = newKeybinding.keybinding;
}
if (keybinding.command === '-' + newKeybinding.command &&
keybinding.keybinding === oldKeybinding &&
(keybinding.context || '') === (newKeybinding.context || '') &&
(keybinding.when || '') === (newKeybinding.when || '')) {
oldRemoved = false;
}
}
if (!updated) {
const item: KeybindingJson = { ...keybindingJson };
keybindings.push(item);
if (!newAdded) {
keybindings.push(newKeybinding);
}
if (!oldRemoved) {
keybindings.push({
command: '-' + newKeybinding.command,
// TODO key: oldKeybinding, see https://github.com/eclipse-theia/theia/issues/6879
keybinding: oldKeybinding,
context: newKeybinding.context,
when: newKeybinding.when
});
}
// TODO use preference values to get proper json settings
// TODO handle dirty models properly
// TODO handle race conditions properly
await this.resource.saveContents(JSON.stringify(keybindings, undefined, 4));
}

Expand All @@ -140,22 +144,13 @@ export class KeymapsService {
return;
}
const content = await this.resource.readContents();
const keybindings: KeybindingJson[] = content ? jsoncparser.parse(content) : [];
const filtered = keybindings.filter(a => a.command !== commandId);
const keybindings: Keybinding[] = content ? jsoncparser.parse(content) : [];
const removedCommand = '-' + commandId;
const filtered = keybindings.filter(a => a.command !== commandId && a.command !== removedCommand);
// TODO use preference values to get proper json settings
// TODO handle dirty models properly
// TODO handle race conditions properly
await this.resource.saveContents(JSON.stringify(filtered, undefined, 4));
}

/**
* Get the list of keybindings from the JSON.
*
* @returns the list of keybindings in JSON.
*/
async getKeybindings(): Promise<KeybindingJson[]> {
if (!this.resource.saveContents) {
return [];
}
const content = await this.resource.readContents();
return content ? jsoncparser.parse(content) : [];
}

}
20 changes: 20 additions & 0 deletions packages/monaco/src/browser/monaco-editor-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export class MonacoEditorProvider {
}, toDispose);
editor.onDispose(() => toDispose.dispose());

this.suppressMonaconKeybindingListener(editor);

const standaloneCommandService = new monaco.services.StandaloneCommandService(editor.instantiationService);
commandService.setDelegate(standaloneCommandService);
this.installQuickOpenService(editor);
Expand All @@ -144,6 +146,24 @@ export class MonacoEditorProvider {
return editor;
}

/**
* Suppresses Monaco keydown listener to avoid triggering default Monaco keybindings
* if they are overriden by a user. Monaco keybindings should be registered as Theia keybindings
* to allow a user to customize them.
*/
protected suppressMonaconKeybindingListener(editor: MonacoEditor): void {
let keydownListener: monaco.IDisposable | undefined;
for (const listener of editor.getControl()._standaloneKeybindingService._store._toDispose) {
if ('_type' in listener && listener['_type'] === 'keydown') {
keydownListener = listener;
break;
}
}
if (keydownListener) {
keydownListener.dispose();
}
}

protected createEditor(uri: URI, override: IEditorOverrideServices, toDispose: DisposableCollection): Promise<MonacoEditor> {
if (DiffUris.isDiffUri(uri)) {
return this.createMonacoDiffEditor(uri, override, toDispose);
Expand Down
5 changes: 5 additions & 0 deletions packages/monaco/src/typings/monaco/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ declare module monaco.editor {
setDecorations(decorationTypeKey: string, ranges: IDecorationOptions[]): void;
setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void;
trigger(source: string, handlerId: string, payload: any): void
_standaloneKeybindingService: {
_store: {
_toDispose: monaco.IDisposable[]
}
}
}

// https://github.com/TypeFox/vscode/blob/monaco/0.18.0/src/vs/editor/browser/widget/codeEditorWidget.ts#L107
Expand Down

0 comments on commit 1a4ba8c

Please sign in to comment.