From fee5a8163d649c55f561f63582a64b99917aa8d2 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Wed, 1 Feb 2023 17:48:43 -0800 Subject: [PATCH 2/3] Add support for completing quoted variables in Eshell like $'FOO' This also adds the ability for Pcomplete handlers to set their own exit functions that will get called as appropriate. * lisp/pcomplete.el (pcomplete-completions-at-point): Check for 'pcomplete-exit-function' and call it if present. * lisp/eshell/em-cmpl.el (eshell-complete-parse-arguments): Handle quoted variables. We also build the 'posns' list from right-to-left now. * lisp/eshell/esh-var.el (eshell-envvar-names): Ensure that variable aliases are included in this list. (eshell-complete-variable-reference): Handle quoted variables. (eshell-variables-list): Handle quoted variables and set the exit function on the completions. (eshell-complete-variable-ref--exit): New function, extracted from 'eshell-variables-list'. * test/lisp/eshell/em-cmpl-tests.el (em-cmpl-test/quoted-variable-ref-completion) (em-cmpl-test/variable-ref-completion/directory): New tests. (em-cmpl-test/user-ref-completion): Fix typo. --- lisp/eshell/em-cmpl.el | 19 +++++----- lisp/eshell/esh-var.el | 60 +++++++++++++++++++------------ lisp/pcomplete.el | 29 +++++++++------ test/lisp/eshell/em-cmpl-tests.el | 25 +++++++++++++ 4 files changed, 92 insertions(+), 41 deletions(-) diff --git a/lisp/eshell/em-cmpl.el b/lisp/eshell/em-cmpl.el index 7f73922f370..380ecd0b91d 100644 --- a/lisp/eshell/em-cmpl.el +++ b/lisp/eshell/em-cmpl.el @@ -319,8 +319,7 @@ eshell-complete-parse-arguments (eshell--pcomplete-insert-tab)) (let ((end (point-marker)) (begin (save-excursion (beginning-of-line) (point))) - (posns (list t)) - args delim) + args posns delim) (when (and pcomplete-allow-modifications (memq this-command '(pcomplete-expand pcomplete-expand-and-complete))) @@ -335,18 +334,22 @@ eshell-complete-parse-arguments (cond ((member (car delim) '("{" "${" "$<")) (setq begin (1+ (cadr delim)) args (eshell-parse-arguments begin end))) + ((member (car delim) '("$'" "$\"")) + ;; Add the (incomplete) argument to our arguments, and + ;; note its position. + (setq args (append (nth 2 delim) (list (car delim)))) + (push (- (nth 1 delim) 2) posns)) ((member (car delim) '("(" "$(")) (throw 'pcompleted (elisp-completion-at-point))) (t (eshell--pcomplete-insert-tab)))) (when (get-text-property (1- end) 'comment) (eshell--pcomplete-insert-tab)) - (let ((pos begin)) - (while (< pos end) - (if (get-text-property pos 'arg-begin) - (nconc posns (list pos))) - (setq pos (1+ pos)))) - (setq posns (cdr posns)) + (let ((pos (1- end))) + (while (>= pos begin) + (when (get-text-property pos 'arg-begin) + (push pos posns)) + (setq pos (1- pos)))) (cl-assert (= (length args) (length posns))) (let ((a args) (i 0) new-start) (while a diff --git a/lisp/eshell/esh-var.el b/lisp/eshell/esh-var.el index a5bfbf4254d..952559e0d18 100644 --- a/lisp/eshell/esh-var.el +++ b/lisp/eshell/esh-var.el @@ -434,9 +434,15 @@ eshell-insert-envvar (defun eshell-envvar-names (&optional environment) "Return a list of currently visible environment variable names." - (mapcar (lambda (x) - (substring x 0 (string-search "=" x))) - (or environment process-environment))) + (delete-dups + (append + ;; Real environment variables + (mapcar (lambda (x) + (substring x 0 (string-search "=" x))) + (or environment process-environment)) + ;; Eshell variable aliases + (mapcar (lambda (x) (car x)) + eshell-variable-aliases-list)))) (defun eshell-environment-variables () "Return a `process-environment', fully updated. @@ -820,33 +826,41 @@ eshell-complete-variable-reference (let ((arg (pcomplete-actual-arg))) (when (string-match (rx "$" (? (or "#" "@")) - (? (group (regexp eshell-variable-name-regexp))) - string-end) + (? (or (group-n 1 (regexp eshell-variable-name-regexp) + string-end) + (seq (group-n 2 (or "'" "\"")) + (group-n 1 (+ anychar)))))) arg) (setq pcomplete-stub (substring arg (match-beginning 1))) + (setq pcomplete-exit-function + (apply-partially #'eshell-complete-variable-ref--exit + (match-string 2 arg))) (throw 'pcomplete-completions (eshell-variables-list))))) (defun eshell-variables-list () "Generate list of applicable variables." - (let ((argname pcomplete-stub) - completions) - (dolist (alias eshell-variable-aliases-list) - (if (string-match (concat "^" argname) (car alias)) - (setq completions (cons (car alias) completions)))) + (let ((argname pcomplete-stub)) (sort - (append - (mapcar - (lambda (varname) - (let ((value (eshell-get-variable varname))) - (if (and value - (stringp value) - (file-directory-p value)) - (concat varname "/") - varname))) - (eshell-envvar-names (eshell-environment-variables))) - (all-completions argname obarray 'boundp) - completions) - 'string-lessp))) + (append (eshell-envvar-names) + (all-completions argname obarray 'boundp)) + #'string-lessp))) + +(defun eshell-complete-variable-ref--exit (delimiter variable status) + "An exit function for completing Eshell variable references. +DELIMITER is a delimiter wrapping the variable (' or \") or nil. +VARIABLE is the name of the variable that was just completed. +STATUS is a symbol representing the state of the completion." + (when (eq status 'finished) + (when delimiter + (if (looking-at (regexp-quote delimiter)) + (goto-char (match-end 0)) + (insert delimiter))) + (let ((non-essential t) + (value (eshell-get-variable variable))) + (when (and (stringp value) (file-directory-p value)) + (insert "/") + ;; Tell Pcomplete not to insert its own termination string. + t)))) (defun eshell-complete-variable-assignment () "If there is a variable assignment, allow completion of entries." diff --git a/lisp/pcomplete.el b/lisp/pcomplete.el index 1ca7a213361..ad50493ac28 100644 --- a/lisp/pcomplete.el +++ b/lisp/pcomplete.el @@ -357,6 +357,7 @@ pcomplete-begins (defvar pcomplete-last nil) (defvar pcomplete-index nil) (defvar pcomplete-stub nil) +(defvar pcomplete-exit-function nil) (defvar pcomplete-seen nil) (defvar pcomplete-norm-func nil) @@ -406,6 +407,7 @@ pcomplete-completions-at-point (if pcomplete-allow-modifications buffer-read-only t)) pcomplete-seen pcomplete-norm-func pcomplete-args pcomplete-last pcomplete-index + pcomplete-exit-function (pcomplete-autolist pcomplete-autolist) (pcomplete-suffix-list pcomplete-suffix-list) ;; Apparently the vars above are global vars modified by @@ -494,16 +496,23 @@ pcomplete-completions-at-point (get-text-property 0 'pcomplete-help cand))) :predicate pred :exit-function - ;; If completion is finished, add a terminating space. - ;; We used to also do this if STATUS is `sole', but - ;; that does not work right when completion cycling. - (unless (zerop (length pcomplete-termination-string)) - (lambda (_s status) - (when (eq status 'finished) - (if (looking-at - (regexp-quote pcomplete-termination-string)) - (goto-char (match-end 0)) - (insert pcomplete-termination-string))))))))))) + (when (or pcomplete-exit-function + (length> pcomplete-termination-string 0)) + (let ((exit-function pcomplete-exit-function)) + (lambda (string status) + (let ((terminate-p t)) + (when exit-function + (setq terminate-p (not (funcall exit-function + string status)))) + ;; If completion is finished, add a terminating + ;; space. We used to also do this if STATUS is + ;; `sole', but that does not work right when + ;; completion cycling. + (when (and terminate-p (eq status 'finished)) + (if (looking-at + (regexp-quote pcomplete-termination-string)) + (goto-char (match-end 0)) + (insert pcomplete-termination-string))))))))))))) ;; I don't think such commands are usable before first setting up buffer-local ;; variables to parse args, so there's no point autoloading it. diff --git a/test/lisp/eshell/em-cmpl-tests.el b/test/lisp/eshell/em-cmpl-tests.el index 12a156fbb38..1f8c571c44c 100644 --- a/test/lisp/eshell/em-cmpl-tests.el +++ b/test/lisp/eshell/em-cmpl-tests.el @@ -183,6 +183,31 @@ em-cmpl-test/variable-ref-completion (should (equal (eshell-insert-and-complete "echo $system-nam") "echo $system-name ")))) +(ert-deftest em-cmpl-test/quoted-variable-ref-completion () + "Test completion of variable references like \"$'var'\". +See ." + (with-temp-eshell + (should (equal (eshell-insert-and-complete "echo $'system-nam") + "echo $'system-name' "))) + (with-temp-eshell + (should (equal (eshell-insert-and-complete "echo $\"system-nam") + "echo $\"system-name\" ")))) + +(ert-deftest em-cmpl-test/variable-ref-completion/directory () + "Test completion of variable references that expand to directories. +See ." + (with-temp-eshell + (should (equal (eshell-insert-and-complete "echo $PW") + "echo $PWD/"))) + (with-temp-eshell + (let ((minibuffer-message-timeout 0) + (inhibit-message t)) + (should (equal (eshell-insert-and-complete "echo $PWD") + "echo $PWD/")))) + (with-temp-eshell + (should (equal (eshell-insert-and-complete "echo $'PW") + "echo $'PWD'/")))) + (ert-deftest em-cmpl-test/variable-assign-completion () "Test completion of variable assignments like \"var=value\". See ." -- 2.25.1