From 69e8333210ae678407d6a5ba647657cc301932b7 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Tue, 22 Aug 2023 18:43:51 -0700 Subject: [PATCH] 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. --- doc/misc/eshell.texi | 5 -- lisp/eshell/em-cmpl.el | 3 +- lisp/eshell/esh-arg.el | 177 ++++++++++++++++++++++++++++++---------- lisp/eshell/esh-cmd.el | 43 ---------- lisp/eshell/esh-proc.el | 13 +++ lisp/eshell/esh-util.el | 43 ++++++++++ 6 files changed, 190 insertions(+), 94 deletions(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index cc94f610615..b5cc9faeec2 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -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 diff --git a/lisp/eshell/em-cmpl.el b/lisp/eshell/em-cmpl.el index 61f1237b907..0255da88dbd 100644 --- a/lisp/eshell/em-cmpl.el +++ b/lisp/eshell/em-cmpl.el @@ -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) diff --git a/lisp/eshell/esh-arg.el b/lisp/eshell/esh-arg.el index c3d3347e888..d5fcabccb14 100644 --- a/lisp/eshell/esh-arg.el +++ b/lisp/eshell/esh-arg.el @@ -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 \"#\"; + 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 \"#\" 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 := `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 "#")) +(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 diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el index 990d2ca1122..ecd947774ee 100644 --- a/lisp/eshell/esh-cmd.el +++ b/lisp/eshell/esh-cmd.el @@ -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 diff --git a/lisp/eshell/esh-proc.el b/lisp/eshell/esh-proc.el index ea5896461b4..6561561440e 100644 --- a/lisp/eshell/esh-proc.el +++ b/lisp/eshell/esh-proc.el @@ -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 diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el index ca2f775318a..b22c286c635 100644 --- a/lisp/eshell/esh-util.el +++ b/lisp/eshell/esh-util.el @@ -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. -- 2.39.2