monaco-editor icon indicating copy to clipboard operation
monaco-editor copied to clipboard

API to configure all keybindings

Open sandyarmstrong opened this issue 7 years ago • 10 comments

Many VS Code language extensions include keybindings like:

{
  "key": ".",
  "command": "^acceptSelectedSuggestion",
  "when": "editorTextFocus && suggestWidgetVisible && editorLangId == 'csharp' && suggestionSupportsAcceptOnKey"
}

In monaco-editor, there doesn't appear to be any way to do the same. You can intercept the . key, but the trigger and command APIs on the standalone editor don't recognize ^acceptSelectedSuggestion.

Is there a way to configure keybindings similar to VS Code extensions?

Would it be reasonable to expose the settings API in monaco-editor, and add API for loading settings from JSON strings, perhaps?

sandyarmstrong avatar Aug 10 '16 16:08 sandyarmstrong

Just mentioning that the workaround to unbind keyboard shortcuts as mentioned in e.g. #1350 throws an error now. This changed sometime in the last version or two. I had to pass an empty handler:

editor._standaloneKeybindingService.addDynamicKeybinding(
 '-actions.find' // command ID prefixed by '-'
 null, // keybinding
 () => {} // need to pass an empty handler
);

Here's the error I was getting:

commands.js?ac27:24 Uncaught Error: invalid command
    at _class.registerCommand (commands.js?ac27:24)
    at StandaloneKeybindingService.addDynamicKeybinding (simpleServices.js?14b2:224)
    [...]
    at MonacoEditor.editorDidMount (editor.tsx?04db:143)
    at MonacoEditor.initMonaco (editor.tsx?04db:132)
    at MonacoEditor.componentDidMount (editor.tsx?04db:50)

Hope this helps someone else!

codebykat avatar Sep 30 '20 23:09 codebykat

I had the same problem after the update, but also got an error because I passed undefined as the second parameter. And after debugging a bit passing the existing keybinding worked for me. I haven't checked using null as you suggested, though. Maybe I can simplify that again on my end, so thank you for your example.

Edit: @codebykat Reading your comment again while not having a fading migraine I now see that you posted a solution and not a question. So I'm terribly sorry if I hopped in here unsolicited and edited the first part of my comment accordingly 🙇‍♂️

Edit (number 5362 😅): Turns out my only problem was indeed only the missing command handler (the fix for https://github.com/microsoft/monaco-editor/issues/1857 made that mandatory) and passing undefined or null as second parameter is still fine 👍

Addition: My whole process to remove/change existing keybindings currently looks like this and uses a lot of unofficial APIs, so a public API to accomplish both those things would be appreciated 🙇‍♂️

class CodeEditor {
    //...

    /**
     * CAUTION: Uses an internal API to get an object of the non-exported class ContextKeyExpr.
     */
    static get ContextKeyExpr(): Promise<monaco.platform.IContextKeyExprFactory> { // I defined these types myself elsewhere
        return new Promise(resolve => {
            window.require(["vs/platform/contextkey/common/contextkey"], (x: { ContextKeyExpr: monaco.platform.IContextKeyExprFactory }) => {
                resolve(x.ContextKeyExpr);
            });
        });
    }

    private patchExistingKeyBindings() {
        this.patchKeyBinding("editor.action.quickFix", monaco.KeyMod.Alt | monaco.KeyCode.Enter); // Default is Ctrl+.
        this.patchKeyBinding("editor.action.quickOutline", monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_O); // Default is Ctrl+Shift+O
        this.patchKeyBinding("editor.action.rename", monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_R, monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_R)); // Default is F2
    }
    
    private patchKeyBinding(id: string, newKeyBinding?: number, context?: string): void {
        // remove existing one; no official API yet
        // the '-' before the commandId removes the binding
        // as of >=0.21.0 we need to supply a dummy command handler to not get errors (because of the fix for https://github.com/microsoft/monaco-editor/issues/1857)
        this.editor._standaloneKeybindingService.addDynamicKeybinding(`-${id}`, undefined, () => { });
        if (newKeyBinding) {
            const action = this.editor.getAction(id);
            const when = ContextKeyExpr.deserialize(context);
            this.editor._standaloneKeybindingService.addDynamicKeybinding(id, newKeyBinding, () => action.run(), when);
        }
    }

spahnke avatar Oct 01 '20 07:10 spahnke

Thanks @spahnke! This was super helpful in getting my own implementation to work. For other weary travellers, here is my solution. I only needed to change the keyboard shortcuts, not update the context so I bypassed the ContextKeyExpr stuff by pulling the existing context straight from the CommandsRegistry.

import { editor } from 'monaco-editor'
import { CommandsRegistry } from 'monaco-editor/esm/vs/platform/commands/common/commands'

export const updateKeyBinding = (
  editor: editor.ICodeEditor,
  id: string,
  newKeyBinding?: number,
) => {
  editor._standaloneKeybindingService.addDynamicKeybinding(`-${id}`, undefined, () => {})

  if (newKeyBinding) {
    const { handler, when } = CommandsRegistry.getCommand(id) ?? {}
    if (handler) {
      editor._standaloneKeybindingService.addDynamicKeybinding(id, newKeyBinding, handler, when)
    }
  }
}

and here are the types I created.

import { IDisposable, editor as editorBase, IEditorAction } from 'monaco-editor'

declare module 'monaco-editor' {
  export namespace editor {
    export interface StandaloneKeybindingService {
      // from: https://github.com/microsoft/vscode/blob/df6d78a/src/vs/editor/standalone/browser/simpleServices.ts#L337
      // Passing undefined with `-` prefixing the commandId, will unset the existing keybinding.
      // eg `addDynamicKeybinding('-fooCommand', undefined, () => {})`
      // this is technically not defined in the source types, but still works. We can't pass `0`
      // because then the underlying method exits early.
      // See: https://github.com/microsoft/vscode/blob/df6d78a/src/vs/base/common/keyCodes.ts#L414
      addDynamicKeybinding(
        commandId: string,
        keybinding: number | undefined,
        handler: editorBase.ICommandHandler,
        when?: ContextKeyExpression,
      ): IDisposable
    }

    export interface ICodeEditor {
      _standaloneKeybindingService: StandaloneKeybindingService
    }
  }
}

keegan-lillo avatar Apr 20 '21 05:04 keegan-lillo

Hey guys! I'm a bit unsure about how to change the Command Pallet to CTRL + P like in VSCode.

Using @keegan-lillo's approach, I have updateKeyBinding(editor, 'CommandPalette', 46)- but I'm not sure where to specify the modifier key.

Sorry for the confusion 🙏

braebo avatar May 03 '21 21:05 braebo

@FractalHQ You're almost there! Monaco has some very clever (yet not super obvious) ways of doing keybindings where it uses the binary representation of the number to describe the keybinding. To add modifier keys, you use a bitwise OR.

updateKeyBinding(editor, 'CommandPalette',  KeyMod.CtrlCmd | KeyCode.KEY_P)

Internally it looks like this:

/**
 * Binary encoding strategy:
 * ```
 *    1111 11
 *    5432 1098 7654 3210
 *    ---- CSAW KKKK KKKK
 *  C = bit 11 = ctrlCmd flag
 *  S = bit 10 = shift flag
 *  A = bit 9 = alt flag
 *  W = bit 8 = winCtrl flag
 *  K = bits 0-7 = key code
 * ```
 */
export const enum KeyMod {
  CtrlCmd = (1 << 11) >>> 0,
  Shift = (1 << 10) >>> 0,
  Alt = (1 << 9) >>> 0,
  WinCtrl = (1 << 8) >>> 0,
}

KeyMod.CtrlCmd in binary is: 100000000000 or 2048 as a number KeyCode.KEY_P in binary is: 101110 or 46 as a number

so when we OR them together you get:

    100000000000
OR  000000101110
-----------------
 =  100000101110

keegan-lillo avatar May 04 '21 11:05 keegan-lillo

I have a similar issue like https://github.com/microsoft/monaco-editor/issues/685

I want to change the show suggestion command to Tab. and I'm able to do so. but I do not want to show suggestions when the line is empty. is there any "when" parameter that will check this case and trigger suggest only when there is some content in the line and perform normal tab operation when the line is empty?

and if there is no direct way to achieve this, what is the workaround?

A working code snippet will be much helpful

Chiragasourabh avatar Jul 06 '21 08:07 Chiragasourabh

Hi! I've added a shortcut to escape the Monaco suggestions widget with the space bar, which caused the space bar not to work anymore as a space bar. I'm thinking that ideally, I would just have to add a conditional that would enable the shortcut only when the Monaco suggestions widget is visible, is it something that is feasible?

Here is my code, so far:

const hideSuggestions = editor.createContextKey('hideSuggestions', true)

editor.addCommand(
  monaco.KeyCode.Space, function () {editor.trigger('', 'hideSuggestWidget', null) }, 'hideSuggestions' )

I'm only missing some way of changing hideSuggestions from true to false whether the Monaco suggestions widget is triggered or not.

laureen-m avatar Oct 08 '21 23:10 laureen-m

You can use the existing suggestWidgetVisible context key for that.

editor.addCommand(monaco.KeyCode.Space, () => editor.trigger('', 'hideSuggestWidget', null), 'suggestWidgetVisible');

spahnke avatar Oct 09 '21 06:10 spahnke

@spahnke Working perfectly, thanks!

laureen-m avatar Oct 09 '21 18:10 laureen-m

I found this solution (not working with Ctrl+P 😢)

  const removeKeybinding = (editor, id) => {
    editor._standaloneKeybindingService.addDynamicKeybinding(`-${id}`, undefined, () => {})
  }

  const addKeybinding = (editor, id, newKeyBinding) => {
    if (newKeyBinding) {
      const action = editor.getAction(id)
      const ContextKeyExpr = new Promise(resolve => {
        window.require(['vs/platform/contextkey/common/contextkey'], x => {
          resolve(x.ContextKeyExpr)
        })
      })
      ContextKeyExpr.then(ContextKeyExprClass => {
        editor._standaloneKeybindingService.addDynamicKeybinding(id, newKeyBinding, () => action.run(), undefined)
      });
    }
  }
  
  removeKeybinding(editor, 'editor.action.quickCommand')
  addKeybinding(editor, 'editor.action.quickCommand', monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter)

Edit:

To use Ctrl + Shift + P with chrome, use this:

  window.addEventListener('keydown', function(event) {
    if (event.keyCode === 80 && (event.ctrlKey || event.metaKey) && !event.altKey && (!event.shiftKey || window.chrome || window.opera)) {
        event.preventDefault();
        if (event.stopImmediatePropagation) {
            event.stopImmediatePropagation();
        } else {
            event.stopPropagation();
        }
        editor.trigger('ctrl-shift-p', 'editor.action.quickCommand', null)
        return;
        }
}, true);

Ademking avatar Jul 23 '22 02:07 Ademking

Since 0.34.1, monaco.editor.addKeybindingRule(s) can be used to tweak default keybindings.

alexdima avatar Oct 18 '22 19:10 alexdima

addKeybindingRule

Can you please provide a small example of how it works?

r0b3r4 avatar Nov 02 '22 15:11 r0b3r4

@r0b3r4 for example, we have this snippet running when we initialize our app, here we configure monaco-editor

...
// editor.defineTheme(...);
// do some other things
...
editor.addKeybindingRules([
    {
      // disable show command center
      keybinding: KeyCode.F1,
      command: null,
    },
    {
      // disable show error command
      keybinding: KeyCode.F8,
      command: null,
    },
    {
      // disable toggle debugger breakpoint
      keybinding: KeyCode.F9,
      command: null,
    },
  ]);

akphi avatar Nov 02 '22 15:11 akphi

@akphi This is just awesome, thanks!

r0b3r4 avatar Nov 02 '22 15:11 r0b3r4