20

zsh forward-word acts a bit different from bash/emacs, and I'd like to change that.

Instead of description of all differences, let me just show you step by step behaviour of bash. I marked cursor as "^" symbol.

foo bar --non-needed-param --needed-param^

M-b

foo bar --non-needed-param --needed-^param

M-b

foo bar --non-needed-param --^needed-param

M-b

foo bar --non-needed-^param --needed-param

M-b

foo bar --non-^needed-param --needed-param

M-b

foo bar --^non-needed-param --needed-param

M-b

foo ^bar --non-needed-param --needed-param

M-f

foo bar^ --non-needed-param --needed-param

M-d

foo bar^-needed-param --needed-param

M-d

foo bar^-param --needed-param

M-d

foo bar^ --needed-param

This algorithm is both flexible for moving through words, removing parts of them for me. Also it's in emacs, so I'm used to it. I'd like to see it in zsh too. Thanks.

Konstantine Rybnikov
  • 2,457
  • 1
  • 22
  • 29

2 Answers2

29

I have this in my .zshrc for exactly that purpose:

# Bash-like navigation
autoload -U select-word-style
select-word-style bash

Edit: Ah, I remember what was missing in order to get everything working as I wanted to. I also overwrote forward-word-match by putting the following content into $ZDOTDIR/functions/forward-word-match (assuming your $ZDOTDIR/functions directory is in $fpath; otherwise put it into one or modify the $fpath array as well):

emulate -L zsh
setopt extendedglob

autoload match-words-by-style

local curcontext=":zle:$WIDGET" word
local -a matched_words
integer count=${NUMERIC:-1}

if (( count < 0 )); then
    (( NUMERIC = -count ))
    zle ${WIDGET/forward/backward}
    return
fi

while (( count-- )); do

    match-words-by-style

    # For some reason forward-word doesn't work like the other word
    # commands; it skips whitespace only after any matched word
    # characters.

    if [[ -n $matched_words[4] ]]; then
        # just skip the whitespace and the following word
  word=$matched_words[4]$matched_words[5]
    else
        # skip the word but not the trailing whitespace
  word=$matched_words[5]
    fi

    if [[ -n $word ]]; then
  (( CURSOR += ${#word} ))
    else
  return 1
    fi
done

return 0
Moritz Bunkus
  • 11,592
  • 3
  • 37
  • 49
  • Thanks! But it actually acts different from bash (did I do something wrong?). On this situation: "fo^ooo --bar", if you press "M-f", it should go immediately to the end of foooo, like this: "foooo^ --bar", but I enabled as you showed, it will go to this: "foooo --^bar". It actually looks like this plugin just does export WORDCHARS=''. – Konstantine Rybnikov Jun 03 '12 at 16:47
  • My full zsh configuration works as you want it to -- so I guess I've done something more than I've stated in my answer. My WORDCHARS contains quite some more chars though I cannot fathom where it is actually set: ($WORDCHARS is `*?_-.[]~=/&;!#$%^(){}<>`) – Moritz Bunkus Jun 03 '12 at 20:40
  • Alright, remembered what it was that made it work and updated my answer accordingly. – Moritz Bunkus Jun 03 '12 at 20:45
  • Hmm. If I do select-word-style bash and then set WORDCHARS -- WORDCHARS doesn't matter. Looks like select-word-style bash has some other configuration parameters. – Konstantine Rybnikov Jun 03 '12 at 20:49
  • This is insane! Can't believe you don't have 25 upvotes. **Works!** – Emanuel Berg Oct 31 '12 at 01:10
  • 2
    If I don't set `ZDOTDIR` (then it defaults to $HOME) and I use oh-my-zsh, then how can I apply this solution? – Forethinker Oct 14 '13 at 16:45
  • @Forethinker did you get this working for oh-my-zsh? – J Spen May 15 '16 at 05:58
  • @MoritzBunkus `forward-word-match` overwriting seems doesn't need any more. – bam Jan 05 '20 at 02:49
5

To get your desired behavior with Emacs keybindings, all I had to do is:

bindkey '\ef' emacs-forward-word

This part of Moriz Bunkus' answer seems to work by itself now, too:

autoload -U select-word-style
select-word-style bash

It seems to perform even better in some edge cases (e.g. it treats ? as a word boundary while the above does not.)

danlei
  • 14,121
  • 5
  • 58
  • 82