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)
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.