You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
150 lines
6.8 KiB
150 lines
6.8 KiB
4 years ago
|
---
|
||
|
title: Current Word Completion
|
||
|
date: 2020-05-26
|
||
|
description: ZSH's tab completion is excellent, although limited to completing based on your command. With this, I can also make completions based on my current word.
|
||
|
tags:
|
||
|
- ZSH
|
||
|
- FZF
|
||
|
---
|
||
|
|
||
|
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](/assets/switch-to-zsh/zsh-tab-complete.gif)
|
||
|
|
||
|
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](https://github.com/junegunn/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](/assets/word-complete/currentWordComplete.gif)
|
||
|
|
||
|
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](https://git.jonathanh.co.uk/jab2870/Dotfiles/src/branch/master/shells/zsh/includes/currentwordcompletion.zsh). 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](https://bestasciitable.com/). 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.
|
||
|
|
||
|
```zsh
|
||
|
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.
|
||
|
|
||
|
```zsh
|
||
|
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](http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags).
|
||
|
|
||
|
This means that we have an array of words. The word we are currently on will be the last:
|
||
|
|
||
|
```zsh
|
||
|
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.
|
||
|
|
||
|
```zsh
|
||
|
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).
|
||
|
|
||
|
```zsh
|
||
|
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.
|
||
|
|
||
|
```zsh
|
||
|
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.
|
||
|
|
||
|
```zsh
|
||
|
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](https://git.jonathanh.co.uk/jab2870/Dotfiles/src/branch/master/shells/zsh/includes/currentwordcompletion.zsh). If you are interested in my full [ZSH config is here](https://git.jonathanh.co.uk/jab2870/Dotfiles/src/branch/master/shells/zsh/).
|