Add 'eshell-special-ref-alist' to allow extending Eshell special refs

* lisp/eshell/esh-cmd.el (eshell--region-p, eshell-with-temp-command):
Move to...
* lisp/eshell/esh-util.el (eshell--region-p)
(eshell-with-temp-command): ... here.

* lisp/eshell/em-cmpl.el (eshell-complete-parse-arguments): Fix edge
case when 'end' is at beginning of (possibly-narrowed) buffer.

* lisp/eshell/esh-arg.el (eshell-special-ref-alist) New variable...
(eshell-special-ref-default): ... New option...
(eshell--special-ref-function): ... New function...
(eshell-parse-special-reference): ... use them.
(eshell-insert-special-reference): New function.
(eshell-complete-special-reference): Reimplement to use a nested call
to Pcomplete.
(eshell-complete-buffer-ref): New function.

* lisp/eshell/esh-proc.el (eshell-proc-initialize): Add "process"
special ref type here.
(eshell-complete-process-ref): New function.

* doc/misc/eshell.texi (Bugs and ideas): Remove now-implemented idea.
This commit is contained in:
Jim Porter 2023-08-22 18:43:51 -07:00
parent 1c2cb9cd61
commit 69e8333210
6 changed files with 190 additions and 94 deletions

View file

@ -2590,11 +2590,6 @@ If it's a Lisp function, input redirection implies @command{xargs} (in a
way@dots{}). If input redirection is added, also update the
@code{file-name-quote-list}, and the delimiter list.
@item Allow @samp{#<@var{word} @var{arg}>} as a generic syntax
With the handling of @emph{word} specified by an
@code{eshell-special-alist}.
@item In @code{eshell-eval-using-options}, allow a @code{:complete} tag
It would be used to provide completion rules for that command. Then the

View file

@ -377,7 +377,8 @@ to writing a completion function."
(throw 'pcompleted (elisp-completion-at-point)))
(t
(eshell--pcomplete-insert-tab)))))
(when (get-text-property (1- end) 'comment)
(when (and (< begin end)
(get-text-property (1- end) 'comment))
(eshell--pcomplete-insert-tab))
(let ((pos (1- end)))
(while (>= pos begin)

View file

@ -165,6 +165,39 @@ treated as a literal character."
:type 'hook
:group 'eshell-arg)
(defvar eshell-special-ref-alist
'(("buffer"
(creation-function eshell-get-buffer)
(insertion-function eshell-insert-buffer-name)
(completion-function eshell-complete-buffer-ref)))
"Alist of special reference types for Eshell.
Each entry is a list of the form (TYPE (KEY VALUE)...). TYPE is
the name of the special reference type, and each KEY/VALUE pair
represents a parameter for the type. Eshell defines the
following KEYs:
* `creation-function'
A function taking any number of arguments that returns the Lisp
object for this special ref type.
* `insertion-function'
An interactive function that returns the special reference in
string form. This string should look like \"#<TYPE ARG...>\";
Eshell will pass the ARGs to `creation-function'.
* `completion-function'
A function using Pcomplete to perform completion on any
arguments necessary for creating this special reference type.")
(defcustom eshell-special-ref-default "buffer"
"The default type for special references when the type keyword is omitted.
This should be a key in `eshell-special-ref-alist' (which see).
Eshell will expand special refs like \"#<ARG...>\" into
\"#<`eshell-special-ref-default' ARG...>\"."
:version "30.1"
:type 'string
:group 'eshell-arg)
(defvar-keymap eshell-arg-mode-map
"C-c M-b" #'eshell-insert-buffer-name)
@ -554,70 +587,120 @@ If no argument requested a splice, return nil."
;;; Special references
(defsubst eshell--special-ref-function (type function)
"Get the specified FUNCTION for a particular special ref TYPE.
If TYPE is nil, get the FUNCTION for the `eshell-special-ref-default'."
(cadr (assq function (assoc (or type eshell-special-ref-default)
eshell-special-ref-alist))))
(defun eshell-parse-special-reference ()
"Parse a special syntax reference, of the form `#<args>'.
args := `type' `whitespace' `arbitrary-args' | `arbitrary-args'
type := \"buffer\" or \"process\"
type := one of the keys in `eshell-special-ref-alist'
arbitrary-args := any number of Eshell arguments
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 (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-beginning 1)
(equal (match-string 1) "buffer")
t)) ; With no type keyword, assume we want a buffer.
(end (eshell-find-delimiter ?\< ?\>)))
(when (not end)
`eshell-special-ref-default'."
(let ((here (point))
(special-ref-types (mapcar #'car eshell-special-ref-alist)))
(when (and (not eshell-current-argument)
(not eshell-current-quoted)
(looking-at (rx-to-string
`(seq "#<" (? (group (or ,@special-ref-types))
(+ space)))
t)))
(goto-char (match-end 0)) ; Go to the end of the match.
(let ((end (eshell-find-delimiter ?\< ?\>))
(creation-fun (eshell--special-ref-function
(match-string 1) 'creation-function)))
(unless end
(when (match-beginning 1)
(goto-char (match-beginning 1)))
(throw 'eshell-incomplete "#<"))
(if (eshell-arg-delimiter (1+ end))
(prog1
(cons (if buffer-p #'eshell-get-buffer #'get-process)
(cons creation-fun
(let ((eshell-current-argument-plain t))
(eshell-parse-arguments (point) end)))
(goto-char (1+ end)))
(ignore (goto-char here)))))))
(defun eshell-insert-special-reference (type &rest args)
"Insert a special reference of the specified TYPE.
ARGS is a list of arguments to pass to the insertion function for
TYPE (see `eshell-special-ref-alist')."
(interactive
(let* ((type (completing-read
(format-prompt "Type" eshell-special-ref-default)
(mapcar #'car eshell-special-ref-alist)
nil 'require-match nil nil eshell-special-ref-default))
(insertion-fun (eshell--special-ref-function
type 'insertion-function)))
(list :interactive (call-interactively insertion-fun))))
(if (eq type :interactive)
(car args)
(apply (eshell--special-ref-function type 'insertion-function) args)))
(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))))))
(when (string-prefix-p "#<" (pcomplete-actual-arg))
(let ((special-ref-types (mapcar #'car eshell-special-ref-alist))
num-args explicit-type)
;; When finished with completion, add a trailing ">" when
;; appropriate.
(add-function
:around (var pcomplete-exit-function)
(lambda (oldfun value status)
(when (eq status 'finished)
;; Don't count the special reference type (e.g. "buffer").
(when (or explicit-type
(and (= num-args 1)
(member value special-ref-types)))
(setq num-args (1- num-args)))
(let ((creation-fun (eshell--special-ref-function
explicit-type 'creation-function)))
;; Check if we already have the maximum number of
;; arguments for this special ref type. If so, finish
;; the ref with ">". Otherwise, insert a space and set
;; the completion status to `sole'.
(if (eq (cdr (func-arity creation-fun)) num-args)
(if (looking-at ">")
(goto-char (match-end 0))
(insert ">"))
(pcomplete-default-exit-function value status)
(setq status 'sole))
(funcall oldfun value status)))))
;; Parse the arguments to this special reference and call the
;; appropriate completion function.
(save-excursion
(eshell-with-temp-command (cons (+ 2 (pcomplete-begin)) (point))
(goto-char (point-max))
(let (pcomplete-args pcomplete-last pcomplete-index pcomplete-begins)
(when (let ((eshell-current-argument-plain t))
(pcomplete-parse-arguments
pcomplete-expand-before-complete))
(setq num-args (length pcomplete-args))
(if (= pcomplete-index pcomplete-last)
;; Call the default special ref completion function,
;; and also add the known special ref types as
;; possible completions.
(throw 'pcomplete-completions
(nconc
(mapcar #'car eshell-special-ref-alist)
(catch 'pcomplete-completions
(funcall (eshell--special-ref-function
nil 'completion-function)))))
;; Get the special ref type and call its completion
;; function.
(let ((first (pcomplete-arg 'first)))
(when (member first special-ref-types)
;; "Complete" the ref type (which we already
;; completed above).
(pcomplete-here)
(setq explicit-type first)))
(funcall (eshell--special-ref-function
explicit-type 'completion-function))))))))))
(defun eshell-get-buffer (buffer-or-name)
"Return the buffer specified by BUFFER-OR-NAME, creating a new one if needed.
@ -630,5 +713,9 @@ single argument."
(interactive "BName of buffer: ")
(insert-and-inherit "#<buffer " (eshell-quote-argument buffer-name) ">"))
(defun eshell-complete-buffer-ref ()
"Perform completion for buffer references."
(pcomplete-here (mapcar #'buffer-name (buffer-list))))
(provide 'esh-arg)
;;; esh-arg.el ends here

View file

@ -393,49 +393,6 @@ for a given process."
;; Command parsing
(defsubst eshell--region-p (object)
"Return non-nil if OBJECT is a pair of numbers or markers."
(and (consp object)
(number-or-marker-p (car object))
(number-or-marker-p (cdr object))))
(defmacro eshell-with-temp-command (command &rest body)
"Temporarily insert COMMAND into the buffer and execute the forms in BODY.
COMMAND can be a string to insert, a cons cell (START . END)
specifying a region in the current buffer, or (:file . FILENAME)
to temporarily insert the contents of FILENAME.
Before executing BODY, narrow the buffer to the text for COMMAND
and and set point to the beginning of the narrowed region.
The value returned is the last form in BODY."
(declare (indent 1))
(let ((command-sym (make-symbol "command"))
(begin-sym (make-symbol "begin"))
(end-sym (make-symbol "end")))
`(let ((,command-sym ,command))
(if (eshell--region-p ,command-sym)
(save-restriction
(narrow-to-region (car ,command-sym) (cdr ,command-sym))
(goto-char (car ,command-sym))
,@body)
;; Since parsing relies partly on buffer-local state
;; (e.g. that of `eshell-parse-argument-hook'), we need to
;; perform the parsing in the Eshell buffer.
(let ((,begin-sym (point)) ,end-sym)
(with-silent-modifications
(if (stringp ,command-sym)
(insert ,command-sym)
(forward-char (cadr (insert-file-contents (cdr ,command-sym)))))
(setq ,end-sym (point))
(unwind-protect
(save-restriction
(narrow-to-region ,begin-sym ,end-sym)
(goto-char ,begin-sym)
,@body)
(delete-region ,begin-sym ,end-sym))))))))
(defun eshell-parse-command (command &optional args toplevel)
"Parse the COMMAND, adding ARGS if given.
COMMAND can be a string, a cons cell (START . END) demarcating a

View file

@ -23,6 +23,7 @@
;;; Code:
(require 'esh-arg)
(require 'esh-io)
(require 'esh-util)
@ -158,6 +159,14 @@ PROC and STATUS to functions on the latter."
(defun eshell-proc-initialize () ;Called from `eshell-mode' via intern-soft!
"Initialize the process handling code."
(make-local-variable 'eshell-process-list)
(setq-local eshell-special-ref-alist
(cons
`("process"
(creation-function get-process)
(insertion-function eshell-insert-process)
(completion-function eshell-complete-process-ref))
eshell-special-ref-alist))
(eshell-proc-mode))
(define-obsolete-function-alias 'eshell-reset-after-proc
@ -699,5 +708,9 @@ The prompt will be set to PROMPT."
(eshell-quote-argument (process-name process))
">"))
(defun eshell-complete-process-ref ()
"Perform completion for process references."
(pcomplete-here (mapcar #'process-name (process-list))))
(provide 'esh-proc)
;;; esh-proc.el ends here

View file

@ -242,6 +242,49 @@ current buffer."
string)
string)
(defsubst eshell--region-p (object)
"Return non-nil if OBJECT is a pair of numbers or markers."
(and (consp object)
(number-or-marker-p (car object))
(number-or-marker-p (cdr object))))
(defmacro eshell-with-temp-command (command &rest body)
"Temporarily insert COMMAND into the buffer and execute the forms in BODY.
COMMAND can be a string to insert, a cons cell (START . END)
specifying a region in the current buffer, or (:file . FILENAME)
to temporarily insert the contents of FILENAME.
Before executing BODY, narrow the buffer to the text for COMMAND
and and set point to the beginning of the narrowed region.
The value returned is the last form in BODY."
(declare (indent 1))
(let ((command-sym (make-symbol "command"))
(begin-sym (make-symbol "begin"))
(end-sym (make-symbol "end")))
`(let ((,command-sym ,command))
(if (eshell--region-p ,command-sym)
(save-restriction
(narrow-to-region (car ,command-sym) (cdr ,command-sym))
(goto-char (car ,command-sym))
,@body)
;; Since parsing relies partly on buffer-local state
;; (e.g. that of `eshell-parse-argument-hook'), we need to
;; perform the parsing in the Eshell buffer.
(let ((,begin-sym (point)) ,end-sym)
(with-silent-modifications
(if (stringp ,command-sym)
(insert ,command-sym)
(forward-char (cadr (insert-file-contents (cdr ,command-sym)))))
(setq ,end-sym (point))
(unwind-protect
(save-restriction
(narrow-to-region ,begin-sym ,end-sym)
(goto-char ,begin-sym)
,@body)
(delete-region ,begin-sym ,end-sym))))))))
(defun eshell-find-delimiter
(open close &optional bound reverse-p backslash-p)
"From point, find the CLOSE delimiter corresponding to OPEN.