Escaping from insert mode with jk

A traditional “power user” configuration for vi is to map an unlikely insert-mode sequence like jj or jk to Escape, to make it easy to switch back to normal mode without leaving the home row. Kakoune doesn’t support this - to keep the mapping code simple, it only allows mapping a single key, not a sequence.

The official wiki has some suggestions about how to do this, but although they’re really simple, they have some downsides. In particular, if there’s a j already in the buffer, and you happen to insert a k after it for some reason, you’ll get booted out of insert mode.

@alexherbo2 recently challenged me to come up with a more robust system, and this is what I wound up with:

declare-option str escape_insert_first "j"
declare-option str escape_insert_second "k"

define-command escape_insert_setup %{
    # Remove any other hooks for the first character.
    remove-hooks window escape-insert-first

    # When we get the first character of the sequence,
    # start looking for the second.
    hook -group escape-insert-first window \
    InsertChar "\Q%opt{escape_insert_first}" %{
        escape_insert_setup_second
    }
}

define-command -hidden escape_insert_setup_second %{
    # If we get another character inserted, let's check it out.
    hook -group escape-insert-second window InsertChar .* %{
        # Remove any other left-over hooks for the second character.
        remove-hooks window escape-insert-second

        evaluate-commands %sh{
            case "$kak_hook_param" in
                "$kak_opt_escape_insert_second")
                    # We did indeed get the second character!
                    echo "execute-keys <backspace><backspace><esc>"
                    echo "escape_insert_setup"
                    ;;
                "$kak_opt_escape_insert_first")
                    # Got the first character,
                    # set up to check for the second again.
                    echo "escape_insert_setup_second"
                    ;;
                *)
                    # Got something else,
                    # go back to checking for the first character.
                    echo "escape_insert_setup"
            esac
        }
    }

    # If we delete a character, that's not the escape sequence.
    hook -group escape-insert-second window InsertDelete .* %{
        # Remove any other left-over hooks for the second character.
        remove-hooks window escape-insert-second

        # Set up to trigger on the first key of the sequence again.
        escape_insert_setup
    }

    # If we move the cursor, that's not the escape sequence.
    hook -group escape-insert-second window InsertMove .* %{
        # Remove any other left-over hooks for the second character.
        remove-hooks window escape-insert-second

        # Set up to trigger on the first key of the sequence again.
        escape_insert_setup
    }

    # If we leave insert mode, that's not the escape sequence.
    hook -group escape-insert-second window ModeChange .* %{
        # Remove any other left-over hooks for the second character.
        remove-hooks window escape-insert-second

        # Set up to trigger on the first key of the sequence again.
        escape_insert_setup
    }
}

To use it, drop it into your autoload directory, set the escape_insert_first and escape_insert_second options in your kakrc, then call escape_insert_setupwhenever you want a window to support the escape-insert sequence (such as in a WinDisplay hook).

I haven’t tested it extensively, and it’s a lot more involved than the examples on the wiki, but I think it’s still fairly easy to trace through how it works.

4 Likes