22

In zsh, the cd command has a two-argument form: cd OLD NEW changes to ${PWD/OLD/NEW}. With the new-style completion system, zsh is capable of completing NEW: the second argument is completed based on what OLD can be substituted with to obtain an existing directory. But the first argument is only completed to existing directories.

How can I get zsh to offer completions that are possible values for OLD, in addition to completing existing directories?

For example, if the current directory is /path/to/foo and there are also directories /also/to/foo and /path/to/foo/prime, then cd pTab completes p to prime. If I intend to run cd path also then I'd like zsh to also offer path as a completion. How?

Using already-typed values of the second argument to limit the possibilities for the first argument would be a plus, but completing the first argument independently would be fine too.

5
  • Would your 2-argument completion example be: cd p also <Tab>, or cd p also <left arrow x 5> <Tab>?
    – Jeff Schaller
    Commented May 11, 2016 at 0:41
  • @JeffSchaller The second-argument-sensitive example would be cd p also<Left*5><Tab>: if I press <Tab> while the cursor is in also, I expect also to be completed, not p. Commented May 11, 2016 at 7:29
  • If you type cd t<tab> should it offer th/to/foo/prime and to/foo/prime? Or should it just limit itself to directory boundaries?
    – Barmar
    Commented May 11, 2016 at 18:30
  • @Barmar I think limiting to directory boundaries would be the most convenient. Commented May 11, 2016 at 18:51
  • INteresting question...
    – wogsland
    Commented Mar 19, 2017 at 15:27

1 Answer 1

1

I guess you could add the components of $PWD to the cd completion list, though this appears to require fiddling with _cd; that is, a customized version of _cd must appear first in $fpath.

% cd && mkdir zcomp
% cp $fpath[-1]/_cd zcomp
% fpath=(~/zcomp $fapth)

Then up at the top of ~/zcomp/_cd add a function

_our_pwd() {
  _values ourpwd ${(ps:/:)PWD}
}

and then just before the _alternative line add what that returns to the list of alternatives

  ...
  alt=("$service-options:$service option:_cd_options" "$alt[@]")
fi

alt=(ourpwd:pwd:_our_pwd "$alt[@]")

_alternative "$alt[@]" && ret=0

return ret
...

though this will always add the pwd components to cd completions:

% cd
Users    jdoe    Applications/  Desktop/  Documents/  Downloads/  Library/
...

with additional logic you could only add the $PWD components when there is already a second argument present instead of always.

However! This always messes up the cd completion and requires that we monkey patch the upstream _cd completion. Another option would be to create a new name for the function provided by the two-arg cd, perhaps called cdsub, and only have the completion of PWD components appear for that. Add this to ~/.zshrc

function cdsub { builtin cd "$@" }

And then a gutted _cd completion for _cdsub to be placed somewhere in $fpath:

#compdef cdsub
#
# Modified version of _cd from ZSH 5.3.1 with specific support for the
# `cd old new` form whereby PWD elements are provided for completion.

_cd_options() {
  _arguments -s \
  '-q[quiet, no output or use of hooks]' \
  '-s[refuse to use paths with symlinks]' \
  '(-P)-L[retain symbolic links ignoring CHASE_LINKS]' \
  '(-L)-P[resolve symbolic links as CHASE_LINKS]'
}

setopt localoptions nonomatch

local expl ret=1 curarg
integer argstart=2 noopts

if (( CURRENT > 1 )); then
  # if not in command position, may have options.
  # Careful: -<-> is not an option.
  while [[ $words[$argstart] = -* && argstart -lt CURRENT ]]; do
    curarg=$words[$argstart]
    [[ $curarg = -<-> ]] && break
    (( argstart++ ))
    [[ $curarg = -- ]] && noopts=1 && break
  done
fi

if [[ CURRENT -eq $((argstart+1)) ]]; then
  # cd old new: look for old in $PWD and see what can replace it
  local rep
  # Get possible completions using word in position 2
  rep=(${~PWD/$words[$argstart]/*}~$PWD(-/))
  # Now remove all the common parts of $PWD and the completions from this
  rep=(${${rep#${PWD%%$words[$argstart]*}}%${PWD#*$words[$argstart]}})
  (( $#rep )) && _wanted -C replacement strings expl replacement compadd -a rep
else
  _values ourpwd ${(ps:/:)PWD} && ret=0
  return ret
fi

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .