Current Word Completion

2020-05-26

Tab completion is one of the main reasons I decided to make the switch to ZSH. Being able to see descriptions and cycle between options makes finding options a breeze:

ZSH Tab Completion

However, there are times that I would prefer to have completion options based on the current word I have typed, regardless of the command I am running. I suppose this is more like an extension of ZSH’s global aliases, but with the ability to choose from multiple options. An example of this is for word lists. There are a lot of commands I use that require word lists and it is a pain to write out the full path before I am able to tab-complete. My solution is what I have been calling Current Word Completion. My current iteration of it relies on FZF for the completions, although in the long term, I would like to make it work with ZSH’s built in completion system. Here is a demonstration using wl for wordlists and myip for my local machines ip addresses.

Current Word Completion

You can see that it doesn’t stop ZSH’s normal completion from working and it can work multiple times on any command.

As with tab-completion, I have it bound to my tab key.

If all you want to do is implement it yourself, you can find the source code here. Simply source the file in your .zshrc and you should be good to go. You can add new options to the word_replace function.

If you are interested in how it works, keep reading.

Overriding tab key

The tab key is interpreted by the terminal as ctrl+i for legacy reasons. That is why I am binding to ^I.

The code below will check what ctrl+i is currently bound to and store it. This means we can override the tab key in such a way that if we are not on a word that should be expanded, we can fall back to whatever tab used to do.

We then bind ctrl+i to a new function that will spawn a notification and then run whatever ctrl+i used to be bound to.

currentwordcomplete(){
    notify-send "Yay, it works"
    zle ${currentword_default_completion:-expand-or-complete}
}

# Record what ctrl+i is currently set to
# That way we can call it if currentword_default_completion doesn't result in anything
[ -z "$currentword_default_completion" ] && {
    binding=$(bindkey '^I')
    [[ $binding =~ 'undefined-key' ]] || currentword_default_completion=$binding[(s: :w)2]
    unset binding
}
zle     -N   currentwordcomplete
bindkey '^I' currentwordcomplete

You can check this works by sourcing the file and pushing tab. You should get a notification and then normal tab completion should work.

Splitting up the current line

The LBUFFER variable in a ZSH widget contains a string equal to everything before your cursor in the buffer. We can use expansion built into ZSH to turn that into an array of words.

tokens=(${(z)LBUFFER})

The z flag here will expand the string using shell parsing to split the string into arguments. This takes into account quotes and escaped spaces. Full details can be found in the documentation.

This means that we have an array of words. The word we are currently on will be the last:

lastWord="${tokens[-1]}"

I also get the first argument which is normally going to be the command currently being run. Although I don’t use this at the moment, I thought it might be useful to be able to exclude certain commands from completion.

cmd="${tokens[1]}"

Word Replace

I have a function called word_replace that takes the word that should be replaced and the command, and prints what it should be replaced with. It also returns 0 on success (there was a replacement found).

word_replace(){
    local ret=1
    local word="$1"
    local cmd="$2"
    case "$word" in
        wl) wordlistSelect; return 0 ;;
        myip) ip route | grep -oE '(dev|src) [^ ]+' | sed 'N;s/\n/,/;s/src //;s/dev //' | awk -F',' '{print $2 " " $1}' | sort -u | fzf -1 --no-preview | cut -d' ' -f1; return 0 ;;
    esac
    return "$ret"
}

In this case, it is a simple switch statement. An interesting side note is the use of -1 in the FZF command for myip. This will prevent FZF from running if there is only 1 option fed to it. So, if I am only connected on one interface, it will simply fill my ip address rather than prompting me to choose one.

Obviously, the logic used here could be as simple or complex as you wish.

Getting the completion

Inside the currentwordcomplete function, I get the output of the word_replace function which is passed the current word. If that doesn’t result in a completion, I will run the word_replace function again, using only the part of the word that comes after an = sign (if there is one).

In either case, a variable called swap will contain what the current word should be replaced with.

There will also be a variable called ret that will be equal to 0 if the replacement should be made.

currentwordcomplete(){
    ...
    # Check we haven't pushed space
    if [ "${LBUFFER[-1]}" != " " ]; then
        swap="$(word_replace "$lastWord" "$cmd")"
        ret="$?"

        # This part checks if the part after an = is completable
        if [ "$ret" -ne "0" ]; then
            local afterEqual="${lastWord##*=}"
            local beforeEqual="${lastWord%=*}"
            # If they are different, there is an equals in the word
            if [ "$afterEqual" != "$lastWord" ]; then
                swap="${beforeEqual}=$(word_replace "$afterEqual" "$cmd")"
                ret="$?"
            fi
        fi
    fi
    ...
}

Making the change

Finally, I check if the completion should be made. If it shouldn’t, I call whatever function the tab key used to be bound to. If it should, I change the last item of the tokens array that we created earlier. I then set the LBUFFER variable to the changed string.

    if [ "$ret" -eq "0" ]; then
        if [ -n "$swap" ]; then
            tokens[-1]="$swap"
            LBUFFER="${tokens[@]}"
        fi
        zle reset-prompt
        return 0
    else
        zle ${currentword_default_completion:-expand-or-complete}
        return
    fi

Conclusion

This is a relatively un-intrusive addition to ZSH that I use most days. I don’t use a huge number of these but the two I mentioned here, word lists and my ip, I use a lot.

You can find the full source here. If you are interested in my full ZSH config is here.