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.
 
 
 
 

6.8 KiB

title date description tags
Current Word Completion 2020-05-26 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. [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

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.