cursorless icon indicating copy to clipboard operation
cursorless copied to clipboard

Add keyboard interface

Open pokey opened this issue 3 years ago • 8 comments

Proposed approach

Everything is done in reverse order of Cursorless spoken forms, and you always move the cursor. So, for example, "chuck funk air" would be executed by running the following in sequence:

  1. A command that selects "air"
  2. A command that selects the current function
  3. A command that deletes the current selection

So we'd basically implement

  • Commands to select decorated marks
  • Commands to select containing scope type
  • Commands to execute action on selection

We'd want to make it so that the delete action knows it's deleting a function, by having Cursorless keep track of this somehow, probably by storing that information when they select containing scope type. Alternately we could try to infer it from the selection

In terms of keymaps, we could do something like the following. Here's an example where we have our own mode, so we can use normal keys, but would be easy to replace each of m, s, and a below with control sequences:

  • Define m<color><char> to select char with colored hat
  • Define s<scopeType> to select a scope type,
  • Define a<action> to execute an action

So for example, to do “chuck state blue air”, it would be:

  1. mba to select "blue air"
  2. ss to select containing statement
  3. ad to delete (alternately could remap <delete> when you're in this mode)

So all told it would be mbassad. Note that this is actually more verbose than the original proposal below, which would be just dsba 🤷. We could probably shorten a bit by removing the s and a prefix from common actions and scope types, respectively, so it would be mbasd. I guess we could even remove the m and put the colors at the top level as well, so it would be basd. There would be a lot of things competing for that top level, though, at that point, but could work. We could just do some juggling to see what makes sense

We could implement ranges by having an action that selects past a decorated mark. Then "chuck funk air past bat" would be

  1. Select "air"
  2. Select past "bat"
  3. Select function
  4. Delete

List targets (eg "air and "bat") could be implemented with a command that adds a new selection to the current list of selections

What about "swap"? I guess we could use bookmarking, so you basically mark a target as an argument for the next action

To get the benefits of properly "cursorless" operation, ie where you don't need to move your cursor, we could have a keyboard version of #190

Alternately, instead of moving cursor, the modifiers and marks could operate on a highlight that indicates the current target. Then for multiple target actions (eg "swap") there could be a command that switches to creating the next target

Also, given the large number of scope types and actions, we might want a command that pops up a quick input to search for action or scope type rather than memorising a ton of keyboard shortcuts

Proposed top-level keymap

  • a: extra actions
  • b: [color] blue
  • c: [scopeType] block (short for "chunk")
  • d: [color] default
  • e:
  • f: [scopeType] function
  • g: [color] green
  • h:
  • i:
  • j:
  • k:
  • l: [scopeType] line
  • m: [action] move
  • n:
  • o:
  • p: [color] pink
  • q:
  • r: [color] red
  • s: shape
  • t: extra scope types
  • u: [action] bring (short for use)
  • v:
  • w:
  • x: mark current selection as other argument for next multi-arg action, such as "bring"
  • y: [color] yellow
  • z:
  • <delete>: [action] delete

Note that for people that use more shapes than colors, they'd prob want shapes at top level rather than colors

Alternative approach

Capturing ideas from #417:

Without modifier keys

  • tda => "take air" (ie "take default air")
  • tga => "take green air"
  • ? tfda => "take fox air" (ie "take fox default air")
    • or tsfda => "take fox air" (ie "take shape fox default air")
  • ? tfga => "take fox green air"
  • tfda => "take funk air"
  • tft => "take funk" (ie "take funk this")
  • ? tf<pause> => "take funk"
  • tldadbl -> "take air and bat"
    • Note that the l is used both to start and end a list
  • trdadb -> "take air past bat"
    • Note that the range ends on its own because you've defined the start and end
  • tlrdadbdcl -> "take air past bat and cap"

To do / think about

  • [ ] Actions with multiple targets. I think for these you can just treat it kinda like a range target, where you just wait for another target after they've specified the first one, and then execute command after they've specified both
  • [ ] Figure out how "character" context works. I don't think we need an actual map literal for that one, because it woul basically be the identity mapping 😊. But it should prob be a proper context in the sense of the keyboard mapping / vscode thing
  • [ ] For toggleList, note that in case you're ending a list, you don't really necessarily transition to a target context, because you might have just ended the command (unless it had multiple targets). I don't think this one is a big deal; implementation will prob just ignore the nextContext in this case
  • [ ] What to do about multi-char sequences for eg scope types? We won't be able to have every scope type be one char. So we could either define them as two-char elements in the top-level target map, or have it so that the prefix char for them puts us into a new context where we're specifying a scope type

Advantage of having context to specify scope type is that then we can use it for "every", as well as next-gen scope modifiers, eg "first funk", "last funk", etc.

  • [ ] Maybe we should use l for "line" instead of "list". Then need to figure out different key for "list".

Action context

  • Every letter is an action => targetContext

Target context

  • a:
  • b:
  • c:
  • d: default color => characterContext
  • e:
  • f:
  • g: green => characterContext
  • h:
  • i:
  • j:
  • k:
  • l:
  • m:
  • n:
  • o:
  • p:
  • q:
  • r: red => characterContext
  • s:
  • t:
  • u:
  • v:
  • w:
  • x:
  • y:
  • z:

With modifier keys

  • ta => "take air"
  • tGa => "take green air"

pokey avatar May 31 '22 14:05 pokey

I have been thinking quite a lot about this. I have several colleagues with custom keyboards with programmable macro layers that would be optimal for Cursorless. In that case you don't need to think about advanced keyboard combinations since a single key can only mean a specific thing on that layer.

The approach I'm leaning towards is not using the Cursorless hats, but instead relying heavy on the scope types. I'm also thinking that we should only have one single action take. The reasoning for this is that I think the learning curve will be much simpler and it will be easier to implement as a first prototype. You also have to take into account how many keys you can press rapidly on a keyboard. I mean why have an action for remove when you could use take and press delete on your keyboard more or less at the same time? As soon as we have previous/next/grandparent on our scope type the navigation with keyboard is going to be really rapid.

I envisioned something like this. Layer/modifier key is assumed to be held: a => take token s => take line d => take block f => take pair q => take statement w => take item e => take function r => take class ...

Pressing that once will of course just perform that take action, but combining it with arrow keys for the previous/next/grandparent modifiers you could navigate the document extremely efficient. d+left:3 => take block 3 step up f+up => Take grandparent pair

This is kind of like vim on steroids and would be quite simple for us to implement.

AndreasArvidsson avatar Aug 21 '22 09:08 AndreasArvidsson

It'll probably be easy to build a POC for the take action using the Vim/Neovim extension.

auscompgeek avatar Aug 21 '22 10:08 auscompgeek

For some basic experimentation, can just add something like the following to your keybindings.json:

    {
        "key": "ctrl+a",
        "command": "cursorless.command",
        "args": {
            "version": 2,
            "action": {
                "name": "setSelection",
                "args": []
            },
            "targets": [
                {
                    "type": "primitive",
                    "modifiers": [
                        {
                            "type": "containingScope",
                            "scopeType": {
                                "type": "namedFunction"
                            }
                        }
                    ]
                }
            ],
            "usePrePhraseSnapshot": false
        },
        "when": "editorTextFocus",
    },

That will map <ctrl-a> to "take funk"

@auscompgeek can you elaborate on how to use Vim/Neovim extension? You mean like custom vim motions or something?

pokey avatar Aug 29 '22 21:08 pokey

I believe both extensions allow for mapping keybindings to complex VSCode commands.

I think with the Vim extension you'd have to modify your keybindings.json? You can match vim modes though.

With the VSCode Neovim extension you can call into VSCode from the Neovim side, so you can just map keys outside of insert mode in Neovim.

It'd be cool to have it all work as custom motions/text objects, but a simple POC to just select text (i.e. some "text objects" in visual mode) should be useful enough.

auscompgeek avatar Sep 04 '22 15:09 auscompgeek

On a related note, when poking around to see whether there's a decent Kakoune mode for VSCode, I found the ModalKeys extension which might be interesting. In particular:

The general phillosphy of ModalKeys is to leverage existing functionality and behavior already available from VSCode and its extensions, and make it easy to define modal key bindings for these behaviors.

Which sounds awfully similar to what we're trying to do here :smile:

auscompgeek avatar Sep 04 '22 15:09 auscompgeek

Here's an attempt to implement part of the keyboard interface using ModalKeys (thanks for the pointer @auscompgeek 🙌). Note that decorated marks won't work until https://github.com/haberdashPI/vscode-modal-keys/issues/63 is addressed

pokey avatar Sep 08 '22 16:09 pokey

Found this in a GitHub search for helix, looks like it wouldn't have the same issue: https://github.com/DCsunset/vscode-modal-editor

auscompgeek avatar Sep 23 '22 02:09 auscompgeek

We actually decided to fix this by making some top-level commands targeted at keyboards, see #958

pokey avatar Sep 23 '22 09:09 pokey

Are we keeping this open for any particular reason?

auscompgeek avatar Dec 23 '22 09:12 auscompgeek

Just gave it abother read-through. Yeah I don't think there's much here that's not captured in #989

pokey avatar Dec 23 '22 10:12 pokey

For posterity's sake, see the keyboard docs to see where this landed

pokey avatar Dec 23 '22 10:12 pokey