monaco-editor
monaco-editor copied to clipboard
API to configure all keybindings
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?
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!
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);
}
}
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
}
}
}
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 🙏
@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
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
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.
You can use the existing suggestWidgetVisible
context key for that.
editor.addCommand(monaco.KeyCode.Space, () => editor.trigger('', 'hideSuggestWidget', null), 'suggestWidgetVisible');
@spahnke Working perfectly, thanks!
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);
Since 0.34.1, monaco.editor.addKeybindingRule(s)
can be used to tweak default keybindings.
addKeybindingRule
Can you please provide a small example of how it works?
@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 This is just awesome, thanks!