VSCodeVim / Vim

:star: Vim for Visual Studio Code
http://aka.ms/vscodevim
MIT License
13.56k stars 1.3k forks source link

Support context for keybindings #4765

Open bioe007 opened 4 years ago

bioe007 commented 4 years ago

In regular vim I bind the same key to different tools depending on the language currently open.

For example, binding 't' to run pytest in python, or go tests when editing golang.

In vim I use a different au command based on the file or buffer language type to map the keys.

In vscode's keybindings.json they have the concept of 'context' , so maybe something like this would work:

"vim.normalModeKeyBindings": [
        {
            "before": ["<leader>", "r"],
            "commands": ["python.datascience.runFileInteractive"],
            "when": "editorLangId == python && editorTextFocus"
        },
       {
            "before": ["T"],
            "commands": ["test-explorer.run-all"],
            "when": "editorTextFocus"
        },
        {
            "before": ["T"],
            "commands": ["go.test.all"],
            "when": "editorTextFocus && editorLangId == go"
        }
]

in this example, the only when editing a python file would the IDE try and run file in jupyter.

Similarly, the Go language's test plugin doesn't work with Test Explorer, but Java and Python do - so when editing go files call the appropriate plugin method.

Idea 2

Another way to get this behavior might be to leverage VSCode's own 'per language' settings, like this, in settings.json:

    "[go]": {

        "editor.snippetSuggestions": "none",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
            "source.organizeImports": true,
        },
        "vim.normalModeKeyBindings": [
            {
                "before": ["t"],
                "commands": ["go.test.cursor"],
                "when": "editorLangId == go && editorTextFocus"
            },
            {
                "before": ["T"],
                "commands": ["go.test.all"],
                "when": "editorTextFocus && editorLangId == go"
            }
        ]
    },

In mocking up this scenario I noticed that VSCode hints this 'setting does not support per-language configuration.' I wonder if it's possible that VSCodeVim can somehow set these as available to be per-language configurable?

Idea 3

I have also wondered if it's possible to lean on VSCode's native keybinding system might help with things like this? Because then its context information would be available, and perhaps even used, before VSCodeVim even has to worry about the keybinding.

I'm not sure how feasible it is to get VSCode to allow non-modifier keys as keybindings though.

Also in this scenario a helper plugin like macros might be needed to get the same kind of functionality we're used to in VSCodeVim because vanilla VSCode doesn't support multiple calls.

disberd commented 4 years ago

I actually stumbled upon a similar need when creating my own extension as I wanted to bind some key combinations just to specific vim modes and only when in the context introduced by my extension. For the moment since I was binding to simple single keys I just used the standard vscode keybindings but having access to context inside vim remapping (if feasible) would allow deeper customization

olliegilbey commented 4 years ago

I would love to see @bioe007 's Idea 2 implemented, came here looking for this language specific support. It seems to be supported for extensions: https://github.com/microsoft/vscode-extension-samples/tree/master/configuration-sample

sql-koala commented 3 years ago

I have thought about this feature too, in the last days. Basically, because I also would like to have it, ;-) the use case is obvious.

However, the request in this form is more broad than it needs too be and it is not easily supported by API (afaik).

But we can simplify it and still get the functionality. We do not need vscodes "per language" setting and no when clause; this should be enough. It should not be hard to have an (optional) string array "attached" to a mapping:

 "vim.normalModeKeyBindings": [
            {
                "before": ["t"],
                "commands": ["go.test.cursor"],
                "languageIds": ["go"]
            },
]

These would be loaded into the Remapper class and checked at runtime against "vimState.editor.document.languageId". A mapping with empty field is valid for all, a mapping with languageIds only for those.

@berknam could you take a look at this? I peeked at the code, but I am better at other parts of the codebase.

berknam commented 3 years ago

Currently we don't have a per buffer implementation of remaps, they're always global. We only set the remaps on startup or when configuration changes. In order to do what you're saying we would need to have the remapper check for that languageIds field and if it existed compare it to document.languageId. I don't think we should do this, because this would then have to be checked on every key press you make and if we want to try and improve this extensions performance that is not the way to go.

It might be possible to implement Idea 2 above, I would have to look into it properly. I don't know if vscode calls onDidChangeConfiguration when moving through files with different languageIds.

Now there is currently some workarounds for this. One of them would be to create those bindings directly in keybindings.json with the check for the editorLangId context and the check for vim.active && vim.mode == 'Normal'. But you should not do this because then Vim will have no way of knowing you just pressed a key and that will cause problems.

There is however a good workaround that should work properly:

Important Note: What I'm about to explain is not the proper way of doing remaps in VSCodeVim, this is just a hacky workaround that helps solve some issues like this one. Please use with caution, try not to abuse this and know that future changes to this extension might make this not work anymore!

First thing to know, the remapper is actually quite versatile, when you make a binding the "before" and "after" take an array of strings that are considered to be keys, like "j" refers to the key j, "esc" or "\<Esc>" refer to the escape key. So you can create your own made-up key like "\<GoTestAll>". Now as far as I know there is no keyboard with a "\<GoTestAll>" key but we can have vscode send Vim this key as if it actually was a thing. With this in mind here is what you can do:

First in your 'settings.json' file create all the command remaps you want for different situations each with its own made-up key:

"vim.normalModeKeyBindingsNonRecursive": [
  {
    "before": ["<GoTestAll>"],
    "commands": ["go.test.all"]
  },
  {
    "before": ["<TestExplorerAll>"],
    "commands": ["test-explorer.run-all"]
  },
  // !!!! You can even have multiple commands like this: !!!!
  {
    "before": ["<PythonRunInteractive>"],
    "commands": [
        "workbench.action.files.save",
        "python.datascience.runFileInteractive",
    ]
  },
]

Second in your 'keybindings.json' file create the remaps using the context you want and using the vim.remap command:

    {
        "key": "t",
        "command": "vim.remap",
        "when": "editorTextFocus && !inDebugRepl && vim.active && vim.mode == 'Normal'",
        "args": {
            "after": ["<TestExplorerAll>"],
        }
    },
    {
        "key": "t",
        "command": "vim.remap",
        "when": "editorTextFocus && !inDebugRepl && vim.active && vim.mode == 'Normal' && editorLangId == go",
        "args": {
            "after": ["<GoTestAll>"],
        }
    },
    {
        "key": "t",
        "command": "vim.remap",
        "when": "editorTextFocus && !inDebugRepl && vim.active && vim.mode == 'Normal' && editorLangId == python",
        "args": {
            "after": ["<PythonRunInteractive>"],
        }
    },

Now I personally wouldn't use t for this because you would lose the t<character> action. I don't think this is possible to be done with a key combination like space+t because vscode's keybindings don't work well with this sort of multiple key combinations. However you could use something like "key": "ctrl+t" or "key": "alt+t" and these will work.

Hope this workaround can help you for now, until we get a better way of doing this.

sql-koala commented 3 years ago

Hi @berknam Thanks for the quick reply and detailed explanation. I tried to implement my idea, if just as a test, but there is something in the code that makes it not work. Remappings are stored like so, where the key (of the map) is the "before" part from config. remappings: Map<string, IKeyRemapping>

So mappings with the same "before" but different "languageIds" overwrite each other and code would require many more changes than thought. Gnaarff. :(

berknam commented 3 years ago

@sql-koala Yeah this would require implementing a global remappings and a per document remappings. Right now I think its better to go with the workaround above at least while the new remapper is in this initial test phase.