[SOLVED] How to move/expand to the beginning of line/non blank depending on where your cursor is

Lately, I’ve been trying to configure kakoune to behave more similar to non-modal editors in some ways (I’ll write a blog about my ideas at some point). I use this because I use Kakoune and vscode as my main editors and it can be a bit difficult to change between them.

Right now, I want to make the <home> button move to the first non-blank character (as gi does) if it is not there and to the beginning of the line if it’s there. This way, you can press once to go to the beginning of your code, as you’d usually do, or press twice to actually go to the beginning of the line.

This should work with multiple cursors and <s-home> should extend to the same places as above.

Any clever and simple ideas from more expert people?

There are two ways you can go, either store that last keystroke was <home> in some option an test that to select the command to run (can be done in shell, left as an exercise to reader).

Or check if the cursor is only preceeded by blanks, if so go to begin of line, else go first non blank which I think is better because its only dependent of selection state instead of depending on the last key that was hit.

On way to do that is to use the try … catch command to implement the check:

eval -itersel %{ # run each selection independently so that the test does not just remove failing selections
  try %{ 
    exec -draft %{ <a-h><a-k>\A\h+.\z<ret> } # check that the preceeding characters are horizontal whitespaces
    exec gh # if the previous line does not fail, go to begining of line
  } catch %{
    exec gi # if it failed, go to first non blank character
}}

Wrap that into a command and map your <home> key to :my-command<ret> and you should be good to go.

1 Like

Thank you very much @mawww, I wasn’t aware of the use try-catch and -draft mode for this purpose. The code I ended up adding to my kakrc is the following, in case anyone is interested:

# Home moves/expand to the begining of line/non blank depending on position
define-command -override -hidden home-movement %{
    # Run each selection independently so that the
    # test does not just remove failing selections.
    eval -itersel %{ 
        try %{
            exec -draft %{ <a-h><a-k>\A\h+.\z<ret> } # check that the preceeding characters are horizontal whitespaces
            exec gh # if the previous line does not fail, go to begining of line
        } catch %{
            exec gi # if it failed, go to first non blank character
        }
    }
}
define-command -override -hidden home-expansion %{
    # Run each selection independently so that the
    # test does not just remove failing selections.
    eval -itersel %{ 
        try %{
            exec -draft %{ <a-h><a-k>\A\h+.\z<ret> } # check that the preceeding characters are horizontal whitespaces
            exec Gh # if the previous line does not fail, go to begining of line
        } catch %{
            exec Gi # if it failed, go to first non blank character
        }
    }
}
map global insert <home>      "<a-;>: home-movement<ret>"
map global insert <s-home>    "<a-;>: home-expansion<ret>"
map global normal <home>      ": home-movement<ret>"
map global normal <s-home>    ": home-expansion<ret>"

You can derive SelectMode::Extend from SelectMode::Replace with the following command:

define-command evaluate-commands-extend -params .. %{
  evaluate-commands -save-regs '^' %{
    execute-keys ';'
    execute-keys -save-regs '' Z
    evaluate-commands -verbatim -- %arg{@}
    execute-keys '<a-z>u'
  }
}

map global normal <s-home> ': evaluate-commands-extend select-to-line-begin-smart<ret>'

That way, you don’t have to explicitly implement the extend commands.

1 Like

Seems interesting :thinking:

Check select_coord() in normal.cc.

Another alternative would be to take 1 parameter for the g/G key and pass it in the mapping:

define-command -override -hidden -params 1 home-movement %{
    ...
    exec %arg{1} h
    ...
    exec %arg{1} i
    ...
}

map global insert <home>      "<a-;>: home-movement g<ret>"
map global insert <s-home>    "<a-;>: home-movement G<ret>"

Is there a way to call internal functions or was it for me to understand how it’s implemented?

No there is no way to call internal functions; it was just to show you how it is implemented.

I have a slightly modified implementation of this to avoid a minor issue. In the case with multiple cursors where some are already at the first non-blank and others are not, the solution using -itersel will make each cursor alternate between home and the first non-blank, making it impossible to get every cursor to one or the other.

define-command -hidden -params 1 home-movement %{
  try %{
    exec -draft <a-h><a-K>\A\h*.\z<ret> # Remove all selections preceeded by whitespace.
    exec %arg{1} i # If there are any selections left, go to the first non-blank.
  } catch %{
    exec %arg{1} h # If every selection was preceeded by whitespace, go home.
  }
}
map global normal <home> ': home-movement g<ret>'
map global normal <s-home> ': home-movement G<ret>'

As an aside, I could not figure out how to get this to work in insert mode. Using a mapping like:

map global insert <home> "<a-;>: home-movement g<ret>"

Just caused “gi” characters to be inserted into the document. Is there some way to make the keys specified by exec run in normal mode when the editor is insert mode?

I would use <end><home> for that.

Not that I know of. You could probably evaluate the commands in a draft context and save the selections and then use <a-;> with z. Maybe?

With the new versions of kakoune it becomes necessary to quote the keymaps. This version does not have the behavior described by @greneholt but neither does it get stuck when one of the selections does not have indentation.

define-command home-expansion -hidden -docstring %{
    home-expansion: expand to the begining of
    line/non blank depending on position
} %{
    try %{
        # Keep selections preceeded by whitespace.
        exec -draft %{ '<a-h><a-k>\A\h+.\z<ret>' }
        # If there are any selections left, go to the beginning of the line.
        exec '<a-;>Gh'
    } catch %{
        # Othwerwise, go to the first non-blank.
        exec '<a-;>Gi'
    }
}

Also, I added <a-;> to make it work in insert mode as well.

My mappings:

bam global '<a-;>' <home>   ': home-expansion<ret>' ';'
bam global '<a-;>' <s-home> ': home-expansion<ret>'