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.