Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

API to configure all keybindings #102

Closed
sandyarmstrong opened this issue Aug 10, 2016 · 14 comments
Closed

API to configure all keybindings #102

sandyarmstrong opened this issue Aug 10, 2016 · 14 comments
Assignees
Labels
editor-api feature-request Request for new features or functionality verification-needed Verification of issue is requested verified Verification succeeded
Milestone

Comments

@sandyarmstrong
Copy link
Member

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?

@codebykat
Copy link

codebykat commented Sep 30, 2020

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!

@spahnke
Copy link
Contributor

spahnke commented Oct 1, 2020

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 #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);
        }
    }

@keegan-lillo
Copy link

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
    }
  }
}

@braebo
Copy link

braebo commented May 3, 2021

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 🙏

@keegan-lillo
Copy link

@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

@Chiragasourabh
Copy link

Chiragasourabh commented Jul 6, 2021

I have a similar issue like #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

@laureen-m
Copy link

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.

@spahnke
Copy link
Contributor

spahnke commented Oct 9, 2021

You can use the existing suggestWidgetVisible context key for that.

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

@laureen-m
Copy link

@spahnke Working perfectly, thanks!

alexdima added a commit that referenced this issue Nov 5, 2021
Fix invalid regex in getWorkerUrl
@Ademking
Copy link

Ademking commented Jul 23, 2022

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);

@alexdima
Copy link
Member

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

@r0b3r4
Copy link

r0b3r4 commented Nov 2, 2022

addKeybindingRule

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

@akphi
Copy link

akphi commented Nov 2, 2022

@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,
    },
  ]);

@r0b3r4
Copy link

r0b3r4 commented Nov 2, 2022

@akphi This is just awesome, thanks!

@github-actions github-actions bot locked and limited conversation to collaborators Dec 2, 2022
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
editor-api feature-request Request for new features or functionality verification-needed Verification of issue is requested verified Verification succeeded
Projects
None yet
Development

No branches or pull requests