Add support for completing special references (e.g. buffers) in Eshell

* lisp/eshell/em-cmpl.el (eshell-complete-parse-arguments): Handle
special references.

* lisp/eshell/em-arg.el (eshell-parse-special-reference): Ensure point
is just after the "#<" when incomplete, and handle backslash escapes
more thoroughly.
(eshell-complete-special-reference): New function.

* test/lisp/eshell/esh-arg-tests.el
(esh-arg-test/special-reference/default)
(esh-arg-test/special-reference/buffer)
(esh-arg-test/special-reference/special):
* test/lisp/eshell/em-cmpl-tests.el
(em-cmpl-test/special-ref-completion/type)
(em-cmpl-test/special-ref-completion/implicit-buffer)
(em-cmpl-test/special-ref-completion/buffer): New tests.
This commit is contained in:
Jim Porter 2023-01-24 21:22:06 -08:00
parent b8e7061232
commit 9199fa00ca
4 changed files with 135 additions and 13 deletions

View file

@ -317,7 +317,7 @@ to writing a completion function."
(eshell--pcomplete-insert-tab))
(let ((end (point-marker))
(begin (save-excursion (beginning-of-line) (point)))
args posns delim)
args posns delim incomplete-arg)
(when (and pcomplete-allow-modifications
(memq this-command '(pcomplete-expand
pcomplete-expand-and-complete)))
@ -332,10 +332,11 @@ to writing a completion function."
(cond ((member (car delim) '("{" "${" "$<"))
(setq begin (1+ (cadr delim))
args (eshell-parse-arguments begin end)))
((member (car delim) '("$'" "$\""))
((member (car delim) '("$'" "$\"" "#<"))
;; Add the (incomplete) argument to our arguments, and
;; note its position.
(setq args (append (nth 2 delim) (list (car delim))))
(setq args (append (nth 2 delim) (list (car delim)))
incomplete-arg t)
(push (- (nth 1 delim) 2) posns))
((member (car delim) '("(" "$("))
(throw 'pcompleted (elisp-completion-at-point)))
@ -362,7 +363,8 @@ to writing a completion function."
(setq args (nthcdr (1+ new-start) args)
posns (nthcdr (1+ new-start) posns))))
(cl-assert (= (length args) (length posns)))
(when (and args (eq (char-syntax (char-before end)) ? )
(when (and args (not incomplete-arg)
(eq (char-syntax (char-before end)) ? )
(not (eq (char-before (1- end)) ?\\)))
(nconc args (list ""))
(nconc posns (list (point))))

View file

@ -28,6 +28,9 @@
;;; Code:
(require 'esh-util)
(require 'esh-module)
(require 'pcomplete)
(eval-when-compile
(require 'cl-lib))
@ -175,7 +178,11 @@ treated as a literal character."
"Initialize the argument parsing code."
(eshell-arg-mode)
(setq-local eshell-inside-quote-regexp nil)
(setq-local eshell-outside-quote-regexp nil))
(setq-local eshell-outside-quote-regexp nil)
(when (eshell-using-module 'eshell-cmpl)
(add-hook 'pcomplete-try-first-hook
#'eshell-complete-special-reference nil t)))
(defun eshell-insert-buffer-name (buffer-name)
"Insert BUFFER-NAME into the current buffer at point."
@ -506,21 +513,28 @@ If the form has no `type', the syntax is parsed as if `type' were
\"buffer\"."
(when (and (not eshell-current-argument)
(not eshell-current-quoted)
(looking-at "#<\\(\\(buffer\\|process\\)\\s-\\)?"))
(looking-at (rx "#<" (? (group (or "buffer" "process"))
space))))
(let ((here (point)))
(goto-char (match-end 0)) ;; Go to the end of the match.
(let ((buffer-p (if (match-string 1)
(string= (match-string 2) "buffer")
t)) ;; buffer-p is non-nil by default.
(let ((buffer-p (if (match-beginning 1)
(equal (match-string 1) "buffer")
t)) ; With no type keyword, assume we want a buffer.
(end (eshell-find-delimiter ?\< ?\>)))
(when (not end)
(when (match-beginning 1)
(goto-char (match-beginning 1)))
(throw 'eshell-incomplete "#<"))
(if (eshell-arg-delimiter (1+ end))
(prog1
(list (if buffer-p 'get-buffer-create 'get-process)
(replace-regexp-in-string
(rx "\\" (group (or "\\" "<" ">"))) "\\1"
(buffer-substring-no-properties (point) end)))
(list (if buffer-p #'get-buffer-create #'get-process)
;; FIXME: We should probably parse this as a
;; real Eshell argument so that we get the
;; benefits of quoting, variable-expansion, etc.
(string-trim-right
(replace-regexp-in-string
(rx "\\" (group anychar)) "\\1"
(buffer-substring-no-properties (point) end))))
(goto-char (1+ end)))
(ignore (goto-char here)))))))
@ -574,5 +588,41 @@ If no argument requested a splice, return nil."
(when splicep
grouped-args)))
;;;_* Special ref completion
(defun eshell-complete-special-reference ()
"If there is a special reference, complete it."
(let ((arg (pcomplete-actual-arg)))
(when (string-match
(rx string-start
"#<" (? (group (or "buffer" "process")) space)
(group (* anychar))
string-end)
arg)
(let ((all-results (if (equal (match-string 1 arg) "process")
(mapcar #'process-name (process-list))
(mapcar #'buffer-name (buffer-list))))
(saw-type (match-beginning 1)))
(unless saw-type
;; Include the special reference types as completion options.
(setq all-results (append '("buffer" "process") all-results)))
(setq pcomplete-stub (replace-regexp-in-string
(rx "\\" (group anychar)) "\\1"
(substring arg (match-beginning 2))))
;; When finished with completion, add a trailing ">" (unless
;; we just completed the initial "buffer" or "process"
;; keyword).
(add-function
:before (var pcomplete-exit-function)
(lambda (value status)
(when (and (eq status 'finished)
(or saw-type
(not (member value '("buffer" "process")))))
(if (looking-at ">")
(goto-char (match-end 0))
(insert ">")))))
(throw 'pcomplete-completions
(all-completions pcomplete-stub all-results))))))
(provide 'esh-arg)
;;; esh-arg.el ends here

View file

@ -176,6 +176,46 @@ See <lisp/eshell/esh-cmd.el>."
(should (equal (eshell-insert-and-complete "echo (eshell/ech")
"echo (eshell/echo"))))
(ert-deftest em-cmpl-test/special-ref-completion/type ()
"Test completion of the start of special references like \"#<buffer\".
See <lisp/eshell/esh-arg.el>."
(with-temp-eshell
(should (equal (eshell-insert-and-complete "echo hi > #<buf")
"echo hi > #<buffer ")))
(with-temp-eshell
(should (equal (eshell-insert-and-complete "echo hi > #<proc")
"echo hi > #<process "))))
(ert-deftest em-cmpl-test/special-ref-completion/implicit-buffer ()
"Test completion of special references like \"#<buf>\".
See <lisp/eshell/esh-arg.el>."
(let (bufname)
(with-temp-buffer
(setq bufname (rename-buffer "my-buffer" t))
(with-temp-eshell
(should (equal (eshell-insert-and-complete "echo hi > #<my-buf")
(format "echo hi > #<%s> " bufname))))
(setq bufname (rename-buffer "another buffer" t))
(with-temp-eshell
(should (equal (eshell-insert-and-complete "echo hi > #<anoth")
(format "echo hi > #<%s> "
(string-replace " " "\\ " bufname))))))))
(ert-deftest em-cmpl-test/special-ref-completion/buffer ()
"Test completion of special references like \"#<buffer buf>\".
See <lisp/eshell/esh-arg.el>."
(let (bufname)
(with-temp-buffer
(setq bufname (rename-buffer "my-buffer" t))
(with-temp-eshell
(should (equal (eshell-insert-and-complete "echo hi > #<buffer my-buf")
(format "echo hi > #<buffer %s> " bufname))))
(setq bufname (rename-buffer "another buffer" t))
(with-temp-eshell
(should (equal (eshell-insert-and-complete "echo hi > #<buffer anoth")
(format "echo hi > #<buffer %s> "
(string-replace " " "\\ " bufname))))))))
(ert-deftest em-cmpl-test/variable-ref-completion ()
"Test completion of variable references like \"$var\".
See <lisp/eshell/esh-var.el>."

View file

@ -102,4 +102,34 @@ treated literally, as a backslash and a newline."
(eshell-match-command-output "echo \"hi\\\nthere\""
"hithere\n")))
(ert-deftest esh-arg-test/special-reference/default ()
"Test that \"#<buf>\" refers to the buffer \"buf\"."
(with-temp-buffer
(rename-buffer "my-buffer" t)
(eshell-command-result-equal
(format "echo #<%s>" (buffer-name))
(current-buffer))))
(ert-deftest esh-arg-test/special-reference/buffer ()
"Test that \"#<buffer buf>\" refers to the buffer \"buf\"."
(with-temp-buffer
(rename-buffer "my-buffer" t)
(eshell-command-result-equal
(format "echo #<buffer %s>" (buffer-name))
(current-buffer))))
(ert-deftest esh-arg-test/special-reference/special ()
"Test that \"#<...>\" works correctly when escaping special characters."
(with-temp-buffer
(rename-buffer "<my buffer>" t)
(let ((escaped-bufname (replace-regexp-in-string
(rx (group (or "\\" "<" ">" space))) "\\\\\\1"
(buffer-name))))
(eshell-command-result-equal
(format "echo #<%s>" escaped-bufname)
(current-buffer))
(eshell-command-result-equal
(format "echo #<buffer %s>" escaped-bufname)
(current-buffer)))))
;; esh-arg-tests.el ends here