Nicolas Martyanoff — Brain dump

Eshell key bindings and completion

I have been having completion issues in Eshell for some time, and I decided to fix it. Of course the yak shaving is always strong with Emacs, so it ended up being a bit more complicated than expected.

Defining keys with use-package

Eshell key bindings are stored in eshell-mode-map. For quite some time, Eshell has been initializing this value to nil, making it impossible to define keys before Eshell has been started. As initially reported, the workaround was to call define-key in a hook, eshell-mode-hook to be precise.

Apparently Emacs 28.1 fixed it. However to be able to use the key binding mechanism of use-package, eshell-mode-map must actually be defined. The solution is to load the right module before use-package configures key bindings, therefore using :init and not :config.

For example:

(use-package eshell
  :init
  (require 'esh-mode) ; eshell-mode-map

  :bind
  (("C-x s" . g-eshell)
   :map eshell-mode-map
   (("C-l" . 'g-eshell-clear)
    ("C-r" . helm-eshell-history))))

Configuring Company for completion

Eshell supports the standard completion-at-point completion system, the function being bound to <tab> by default. If you are using Helm, it will be automatically used to present the list of possible completions. This is a bit cumbersome: I want completion to be inline, not in a separate buffer in the bottom of the screen.

Since I use Company, I can instead bind <tab> to company-complete:

(define-key eshell-mode-map (kbd "<tab>") 'company-complete)

Company completion

Fixing completion of commands in subdirectories

As it turns out, completion works as intended for global commands, but fails when calling a command in a subdirectory. For example, when completing ./utils/create-blog-post, completion-at-point will replace the line by create-blog-post, which is of course incorrect. This bug is also present when completing the absolute path of an executable.

After spending way too much time on Google and in the em-cmpl module, which handles completion for Eshell, it turned out to be a known bug, introduced almost two years ago.

The workaround is to redefine eshell--complete-commands-list as it was before the commit:

(defun eshell--complete-commands-list ()
  "Generate list of applicable, visible commands."
  (let ((filename (pcomplete-arg)) glob-name)
    (if (file-name-directory filename)
        (if eshell-force-execution
            (pcomplete-dirs-or-entries nil #'file-readable-p)
          (pcomplete-executables))
      (if (and (> (length filename) 0)
               (eq (aref filename 0) eshell-explicit-command-char))
          (setq filename (substring filename 1)
                pcomplete-stub filename
                glob-name t))
      (let* ((paths (eshell-get-path))
             (cwd (file-name-as-directory
                   (expand-file-name default-directory)))
             (path "") (comps-in-path ())
             (file "") (filepath "") (completions ()))
        ;; Go thru each path in the search path, finding completions.
        (while paths
          (setq path (file-name-as-directory
                      (expand-file-name (or (car paths) ".")))
                comps-in-path
                (and (file-accessible-directory-p path)
                     (file-name-all-completions filename path)))
          ;; Go thru each completion found, to see whether it should
          ;; be used.
          (while comps-in-path
            (setq file (car comps-in-path)
                  filepath (concat path file))
            (if (and (not (member file completions)) ;
                     (or (string-equal path cwd)
                         (not (file-directory-p filepath)))
                     (if eshell-force-execution
                         (file-readable-p filepath)
                       (file-executable-p filepath)))
                (setq completions (cons file completions)))
            (setq comps-in-path (cdr comps-in-path)))
          (setq paths (cdr paths)))
        ;; Add aliases which are currently visible, and Lisp functions.
        (pcomplete-uniquify-list
         (if glob-name
             completions
           (setq completions
                 (append (if (fboundp 'eshell-alias-completions)
                             (eshell-alias-completions filename))
                         (eshell-winnow-list
                          (mapcar
                           (lambda (name)
                             (substring name 7))
                           (all-completions (concat "eshell/" filename)
                                            obarray #'functionp))
                          nil '(eshell-find-alias-function))
                         completions))
           (append (and (or eshell-show-lisp-completions
                            (and eshell-show-lisp-alternatives
                                 (null completions)))
                        (all-completions filename obarray #'functionp))
                   completions)))))))

I have no idea what is wrong in the new implementation of the function, but hopefully someone can find a fix to be merged before Emacs 29 is released.