Fixing My Zsh Setup: Autosuggestions & History Search
I use macOS Terminal with ohmyzsh, since I’m not a power user and just want the simplest and most convenient setup. But for a while my shell config had two problems I kept ignoring:
- I couldn’t accept a single word from the grey hint, tab would accept the whole suggestion.
- After typing a prefix like
npm runand pressing↓through history matches, hitting↓at the bottom just got stuck. But I want it to go back to a clean line.
1 The original setup
I had both zsh-autocomplete and zsh-autosuggestions installed at the same time, and didn’t notice they conflict. zsh-autocomplete is quite heavy to me and takes over ZLE widget registration at load time, quietly breaking things that rely on the standard completion machinery. So I decided to remove it and rely solely on zsh-autosuggestions. This fixed several ghost issues I hadn’t noticed before.
But the more interesting problem was the key bindings.
2 The ^I trap
I had this line in my config:
bindkey '^I' autosuggest-accept # ^I is TabBinding it to autosuggest-accept meant Tab would accept the entire suggestion instead of triggering completion. It seems reasonable on its face, but also caused a side effect: in macOS Terminal, → shares some input handling with Tab in certain contexts, so when I set bindkey '^[[C' forward-word, pressing → was silently triggering autosuggest-accept (whole line) rather than just moving the cursor one word to the right.
The solution is to remove the ^I binding. I finally decided to use → to accept the entire suggestion instead, and use option + → to accept one word at a time, just as how I usually move cursor in other IDEs (sometimes I do use vim though, but not that often).
Btw, to confirm what any key actually sends, use cat -v and press the key, then Ctrl+C to abort.
3 Accepting one word at a time
zsh-autosuggestions doesn’t have a built-in “accept one word” widget. Instead, it has a widget called ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS. Any widget listed there will, when executed, absorb the corresponding portion of the suggestion into the real buffer.
Meanwhile, forward-word moves the cursor one word to the right. So by adding it to the list, bind → to it, and pressing → will pull one word out of the suggestion each time. And that’s exactly what I always wanted.
Note that two things matter here:
- The variable must be set before sourcing the plugin. It’s read once at initialisation. Setting it afterwards has no effect.
- The binding must be set after
oh-my-zsh.shis sourced. Oh My Zsh will overwrite bindings set before it loads. Anything you want to stick should be placed at the bottom of.zshrc
4 History substring search
zsh-history-substring-search filters history by whatever you have already typed. For example, type npm run, press ↑, and you cycle only through commands that start with npm run. With nothing typed, it behaves like normal history navigation.
source $(brew --prefix)/share/zsh-history-substring-search/zsh-history-substring-search.zsh
HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE=1
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
bindkey '^P' history-substring-search-up # fallback for tmux / SSH
bindkey '^N' history-substring-search-downThe remaining problem was the stuck-at-bottom behaviour. When you reach the most recent match and press ↓ again, the plugin sets HISTORY_SUBSTRING_SEARCH_RESULT to an empty string. That’s the signal to clear the line:
_history_substring_search_down_then_clear() {
if [[ $HISTORY_SUBSTRING_SEARCH_RESULT == '' ]]; then
zle .kill-whole-line
else
zle history-substring-search-down
fi
}
zle -N _history_substring_search_down_then_clear
bindkey '^[[B' _history_substring_search_down_then_clear5 Plugin source order
All three plugins must be sourced in this order:
zsh-syntax-highlighting
zsh-autosuggestions
zsh-history-substring-search
zsh-syntax-highlighting wraps every registered ZLE widget at load time to apply colouring. If zsh-autosuggestions loads first, the widgets it creates won’t be wrapped, and highlighting breaks for those interactions. Source highlighting first, and it captures everything that comes after.
6 fzf
One addition worth mentioning: I currently use fzf to replace the default Ctrl+R history search with a full-screen fuzzy finder. Install it with brew install fzf, then add this to .zshrc:
eval "$(fzf --zsh)"This also enables Ctrl+T (fuzzy file picker, inserts path into command line) and Alt+C (fuzzy cd).
7 Final config
Feel free to check my final zshrc!