Fish-like autosuggestions in eshell

Update: company-eshell-autosuggest is now a package on melpa!

I’ve been moving over to Emacs eshell from zsh, inspired in part by a reddit post by Pierre Neidhardt and re-motivated by an excellent video by Howard Abrams. One thing I missed from zsh was fish-like history autosuggestions, which “suggest” a previous shell command based on your current input.

I thought company-mode could be repurposed to provide such autosuggestions, since its front end is pretty similar to what I wanted and you get the completion framework for free. And with the help of pcomplete, ivy, completion-at-point and emacs-fish-completion, I was already covered on zsh-like completion, so company-mode was really more in the way than anything.

All that’s needed is to define some functions for the autosuggest backend1:

(defun company-eshell-autosuggest-candidates (prefix)
  (let* ((history
          (delete-dups
           (mapcar (lambda (str)
                     (string-trim (substring-no-properties str)))
                   (ring-elements eshell-history-ring))))
         (most-similar (cl-find-if
                        (lambda (str)
                          (string-prefix-p prefix str))
                        history)))
    (when most-similar
      `(,most-similar))))

(defun company-eshell-autosuggest--prefix ()
  (let ((prefix
         (string-trim-left
          (buffer-substring-no-properties
           (save-excursion
             (eshell-bol))
           (save-excursion (end-of-line) (point))))))
    (if (not (string-empty-p prefix))
        prefix
      'stop)))

(defun company-eshell-autosuggest (command &optional arg &rest ignored)
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-eshell))
    (prefix (and (eq major-mode 'eshell-mode)
                 (company-eshell-autosuggest--prefix)))
    (candidates (company-eshell-autosuggest-candidates arg))))

All we’re doing here is defining a company backend that grabs the first history entry that has the current eshell input as a prefix, and providing it as a completion candidate via company. To enable this, we just need to enable company-mode in eshell and set company-backends locally in eshell-mode 2:

(add-hook 'eshell-mode-hook #'company-mode) ; Not needed if using global-company-mode

(defun setup-eshell-autosuggest ()
  (with-eval-after-load 'company
    (setq-local company-backends '(company-eshell-autosuggest))
    (setq-local company-frontends '(company-preview-frontend))))

Setting company-frontends to company-preview-frontend just gets rid of the company popup, and uses the dark overlay by default. This is fine since at any given point there is only one completion candidate. Of course it would be simple to tweak this and provide all history entries that match the current input as well as use the popup, but I wanted to be consistent with the fish-like behavior for now.

Here’s a screencap of it in action:

You can just hit RET to accept the autosuggest, or you can bind a key of your choosing in company-active-map.


  1. Updated 2017-11-01: delete-dups more concisely achieves the same thing as cl-remove-duplicates in this case. [return]
  2. Updated 2017-11-01: with-eval-after-load is probably more sane than require. [return]

»