diff options
Diffstat (limited to 'lisp/eshell/em-hist.el')
-rw-r--r-- | lisp/eshell/em-hist.el | 299 |
1 files changed, 164 insertions, 135 deletions
diff --git a/lisp/eshell/em-hist.el b/lisp/eshell/em-hist.el index 1ab3c60b2c7..1db239b9f72 100644 --- a/lisp/eshell/em-hist.el +++ b/lisp/eshell/em-hist.el @@ -1,6 +1,6 @@ ;;; em-hist.el --- history list management -*- lexical-binding:t -*- -;; Copyright (C) 1999-2017 Free Software Foundation, Inc. +;; Copyright (C) 1999-2022 Free Software Foundation, Inc. ;; Author: John Wiegley <johnw@gnu.org> @@ -55,10 +55,10 @@ ;;; Code: (eval-when-compile (require 'cl-lib)) -(eval-when-compile (require 'subr-x)) ; `string-blank-p' (require 'ring) (require 'esh-opt) +(require 'esh-mode) (require 'em-pred) (require 'eshell) @@ -74,17 +74,14 @@ (defcustom eshell-hist-load-hook nil "A list of functions to call when loading `eshell-hist'." :version "24.1" ; removed eshell-hist-initialize - :type 'hook - :group 'eshell-hist) + :type 'hook) (defcustom eshell-hist-unload-hook (list - (function - (lambda () - (remove-hook 'kill-emacs-hook 'eshell-save-some-history)))) + (lambda () + (remove-hook 'kill-emacs-hook 'eshell-save-some-history))) "A hook that gets run when `eshell-hist' is unloaded." - :type 'hook - :group 'eshell-hist) + :type 'hook) (defcustom eshell-history-file-name (expand-file-name "history" eshell-directory-name) @@ -92,20 +89,21 @@ See also `eshell-read-history' and `eshell-write-history'. If it is nil, Eshell will use the value of HISTFILE." :type '(choice (const :tag "Use HISTFILE" nil) - file) - :group 'eshell-hist) + file)) (defcustom eshell-history-size 128 "Size of the input history ring. If nil, use envvar HISTSIZE." :type '(choice (const :tag "Use HISTSIZE" nil) - integer) - :group 'eshell-hist) + integer)) (defcustom eshell-hist-ignoredups nil "If non-nil, don't add input matching the last on the input ring. -This mirrors the optional behavior of bash." - :type 'boolean - :group 'eshell-hist) +The value `erase' mirrors the \"erasedups\" value of HISTCONTROL +in bash, and any other non-nil value mirrors the \"ignoredups\" +value." + :type '(choice (const :tag "Don't ignore anything" nil) + (const :tag "Ignore consecutive duplicates" t) + (const :tag "Only keep last duplicate" erase))) (defcustom eshell-save-history-on-exit t "Determine if history should be automatically saved. @@ -117,8 +115,7 @@ If set to `ask', ask if any Eshell buffers are open at exit time. If set to t, history will always be saved, silently." :type '(choice (const :tag "Never" nil) (const :tag "Ask" ask) - (const :tag "Always save" t)) - :group 'eshell-hist) + (const :tag "Always save" t))) (defcustom eshell-input-filter 'eshell-input-filter-default "Predicate for filtering additions to input history. @@ -128,40 +125,52 @@ whitespace." :type '(radio (function-item eshell-input-filter-default) (function-item eshell-input-filter-initial-space) (function :tag "Other function")) - :group 'eshell-hist) - -(put 'eshell-input-filter 'risky-local-variable t) + :risky t) + +(defun eshell-hist--update-keymap (symbol value) + "Update `eshell-hist-mode-map' for `eshell-hist-match-partial'." + ;; Don't try to set this before it is bound. See below. + (when (and (boundp 'eshell-hist-mode-map) + (eq symbol 'eshell-hist-match-partial)) + (dolist (keyb + (if value + `(("M-p" . ,#'eshell-previous-matching-input-from-input) + ("M-n" . ,#'eshell-next-matching-input-from-input) + ("C-c M-p" . ,#'eshell-previous-input) + ("C-c M-n" . ,#'eshell-next-input)) + `(("M-p" . ,#'eshell-previous-input) + ("M-n" . ,#'eshell-next-input) + ("C-c M-p" . ,#'eshell-previous-matching-input-from-input) + ("C-c M-n" . ,#'eshell-next-matching-input-from-input)))) + (keymap-set eshell-hist-mode-map (car keyb) (cdr keyb)))) + (set-default symbol value)) (defcustom eshell-hist-match-partial t "If non-nil, movement through history is constrained by current input. -Otherwise, typing <M-p> and <M-n> will always go to the next history +Otherwise, typing \\`M-p' and \\`M-n' will always go to the next history element, regardless of any text on the command line. In that case, -<C-c M-r> and <C-c M-s> still offer that functionality." +\\`C-c M-r' and \\`C-c M-s' still offer that functionality." :type 'boolean - :group 'eshell-hist) + :set 'eshell-hist--update-keymap) (defcustom eshell-hist-move-to-end t "If non-nil, move to the end of the buffer before cycling history." - :type 'boolean - :group 'eshell-hist) + :type 'boolean) (defcustom eshell-hist-event-designator "^!\\(!\\|-?[0-9]+\\|\\??[^:^$%*?]+\\??\\|#\\)" "The regexp used to identifier history event designators." - :type 'regexp - :group 'eshell-hist) + :type 'regexp) (defcustom eshell-hist-word-designator - "^:?\\([0-9]+\\|[$^%*]\\)?\\(\\*\\|-[0-9]*\\|[$^%*]\\)?" + "^:?\\([0-9]+\\|[$^%*]\\)?\\(-[0-9]*\\|[$^%*]\\)?" "The regexp used to identify history word designators." - :type 'regexp - :group 'eshell-hist) + :type 'regexp) (defcustom eshell-hist-modifier "^\\(:\\([hretpqx&g]\\|s/\\([^/]*\\)/\\([^/]*\\)/\\)\\)*" "The regexp used to identity history modifiers." - :type 'regexp - :group 'eshell-hist) + :type 'regexp) (defcustom eshell-hist-rebind-keys-alist '(([(control ?p)] . eshell-previous-input) @@ -179,8 +188,7 @@ element, regardless of any text on the command line. In that case, "History keys to bind differently if point is in input text." :type '(repeat (cons (vector :tag "Keys to bind" (repeat :inline t sexp)) - (function :tag "Command"))) - :group 'eshell-hist) + (function :tag "Command")))) ;;; Internal Variables: @@ -189,18 +197,31 @@ element, regardless of any text on the command line. In that case, (defvar eshell-matching-input-from-input-string "") (defvar eshell-save-history-index nil) -(defvar eshell-isearch-map - (let ((map (copy-keymap isearch-mode-map))) - (define-key map [(control ?m)] 'eshell-isearch-return) - (define-key map [return] 'eshell-isearch-return) - (define-key map [(control ?r)] 'eshell-isearch-repeat-backward) - (define-key map [(control ?s)] 'eshell-isearch-repeat-forward) - (define-key map [(control ?g)] 'eshell-isearch-abort) - (define-key map [backspace] 'eshell-isearch-delete-char) - (define-key map [delete] 'eshell-isearch-delete-char) - (define-key map "\C-c\C-c" 'eshell-isearch-cancel) - map) - "Keymap used in isearch in Eshell.") +(defvar-keymap eshell-isearch-map + :doc "Keymap used in isearch in Eshell." + :parent isearch-mode-map + "C-m" #'eshell-isearch-return + "C-r" #'eshell-isearch-repeat-backward + "C-s" #'eshell-isearch-repeat-forward + "C-g" #'eshell-isearch-abort + "<backspace>" #'eshell-isearch-delete-char + "<delete>" #'eshell-isearch-delete-char + "C-c C-c" #'eshell-isearch-cancel) + +(defvar-keymap eshell-hist-mode-map + "<up>" #'eshell-previous-matching-input-from-input + "<down>" #'eshell-next-matching-input-from-input + "C-<up>" #'eshell-previous-input + "C-<down>" #'eshell-next-input + "M-r" #'eshell-previous-matching-input + "M-s" #'eshell-next-matching-input + "C-c M-r" #'eshell-previous-matching-input-from-input + "C-c M-s" #'eshell-next-matching-input-from-input + "C-c C-l" #'eshell-list-history + "C-c C-x" #'eshell-get-next-from-history) +;; Update `eshell-hist-mode-map' for `eshell-hist-match-partial'. +(eshell-hist--update-keymap 'eshell-hist-match-partial + eshell-hist-match-partial) (defvar eshell-rebind-keys-alist) @@ -216,57 +237,36 @@ Returns non-nil if INPUT is blank." Returns nil if INPUT is prepended by blank space, otherwise non-nil." (not (string-match-p "\\`\\s-+" input))) -(defun eshell-hist-initialize () - "Initialize the history management code for one Eshell buffer." - (add-hook 'eshell-expand-input-functions - 'eshell-expand-history-references nil t) +(define-minor-mode eshell-hist-mode + "Minor mode for the eshell-hist module. +\\{eshell-hist-mode-map}" + :keymap eshell-hist-mode-map) + +(defun eshell-hist-initialize () ;Called from `eshell-mode' via intern-soft! + "Initialize the history management code for one Eshell buffer." (when (eshell-using-module 'eshell-cmpl) (add-hook 'pcomplete-try-first-hook - 'eshell-complete-history-reference nil t)) + #'eshell-complete-history-reference nil t)) (if (and (eshell-using-module 'eshell-rebind) (not eshell-non-interactive-p)) (let ((rebind-alist eshell-rebind-keys-alist)) - (make-local-variable 'eshell-rebind-keys-alist) - (setq eshell-rebind-keys-alist + (setq-local eshell-rebind-keys-alist (append rebind-alist eshell-hist-rebind-keys-alist)) - (set (make-local-variable 'search-invisible) t) - (set (make-local-variable 'search-exit-option) t) + (setq-local search-invisible t) + (setq-local search-exit-option t) (add-hook 'isearch-mode-hook - (function - (lambda () - (if (>= (point) eshell-last-output-end) - (setq overriding-terminal-local-map - eshell-isearch-map)))) nil t) + (lambda () + (if (>= (point) eshell-last-output-end) + (setq overriding-terminal-local-map + eshell-isearch-map))) + nil t) (add-hook 'isearch-mode-end-hook - (function - (lambda () - (setq overriding-terminal-local-map nil))) nil t)) - (define-key eshell-mode-map [up] 'eshell-previous-matching-input-from-input) - (define-key eshell-mode-map [down] 'eshell-next-matching-input-from-input) - (define-key eshell-mode-map [(control up)] 'eshell-previous-input) - (define-key eshell-mode-map [(control down)] 'eshell-next-input) - (define-key eshell-mode-map [(meta ?r)] 'eshell-previous-matching-input) - (define-key eshell-mode-map [(meta ?s)] 'eshell-next-matching-input) - (define-key eshell-command-map [(meta ?r)] - 'eshell-previous-matching-input-from-input) - (define-key eshell-command-map [(meta ?s)] - 'eshell-next-matching-input-from-input) - (if eshell-hist-match-partial - (progn - (define-key eshell-mode-map [(meta ?p)] - 'eshell-previous-matching-input-from-input) - (define-key eshell-mode-map [(meta ?n)] - 'eshell-next-matching-input-from-input) - (define-key eshell-command-map [(meta ?p)] 'eshell-previous-input) - (define-key eshell-command-map [(meta ?n)] 'eshell-next-input)) - (define-key eshell-mode-map [(meta ?p)] 'eshell-previous-input) - (define-key eshell-mode-map [(meta ?n)] 'eshell-next-input) - (define-key eshell-command-map [(meta ?p)] - 'eshell-previous-matching-input-from-input) - (define-key eshell-command-map [(meta ?n)] - 'eshell-next-matching-input-from-input))) + (lambda () + (setq overriding-terminal-local-map nil)) + nil t)) + (eshell-hist-mode)) (make-local-variable 'eshell-history-size) (or eshell-history-size @@ -286,25 +286,21 @@ Returns nil if INPUT is prepended by blank space, otherwise non-nil." (make-local-variable 'eshell-save-history-index) (if (minibuffer-window-active-p (selected-window)) - (set (make-local-variable 'eshell-save-history-on-exit) nil) - (set (make-local-variable 'eshell-history-ring) nil) + (setq-local eshell-save-history-on-exit nil) + (setq-local eshell-history-ring nil) (if eshell-history-file-name (eshell-read-history nil t)) - (add-hook 'eshell-exit-hook 'eshell-write-history nil t)) + (add-hook 'eshell-exit-hook #'eshell-write-history nil t)) (unless eshell-history-ring (setq eshell-history-ring (make-ring eshell-history-size))) - (add-hook 'eshell-exit-hook 'eshell-write-history nil t) - - (add-hook 'kill-emacs-hook 'eshell-save-some-history) + (add-hook 'eshell-exit-hook #'eshell-write-history nil t) - (make-local-variable 'eshell-input-filter-functions) - (add-hook 'eshell-input-filter-functions 'eshell-add-to-history nil t) + (add-hook 'kill-emacs-query-functions #'eshell-save-some-history) - (define-key eshell-command-map [(control ?l)] 'eshell-list-history) - (define-key eshell-command-map [(control ?x)] 'eshell-get-next-from-history)) + (add-hook 'eshell-input-filter-functions #'eshell-add-to-history nil t)) (defun eshell-save-some-history () "Save the history for any open Eshell buffers." @@ -319,7 +315,8 @@ Returns nil if INPUT is prepended by blank space, otherwise non-nil." (format-message "Save input history for Eshell buffer `%s'? " (buffer-name buf))))) - (eshell-write-history)))))) + (eshell-write-history))))) + t) (defun eshell/history (&rest args) "List in help buffer the buffer's input history." @@ -343,7 +340,7 @@ unless a different file is specified on the command line.") (error "No history")) (let (length file) (when (and args (string-match "^[0-9]+$" (car args))) - (setq length (min (eshell-convert (car args)) + (setq length (min (string-to-number (car args)) (ring-length eshell-history-ring)) args (cdr args))) (and length @@ -384,12 +381,22 @@ unless a different file is specified on the command line.") Input is entered into the input history ring, if the value of variable `eshell-input-filter' returns non-nil when called on the input." - (if (and (funcall eshell-input-filter input) - (or (null eshell-hist-ignoredups) - (not (ring-p eshell-history-ring)) - (ring-empty-p eshell-history-ring) - (not (string-equal (eshell-get-history 0) input)))) - (eshell-put-history input)) + (when (and (funcall eshell-input-filter input) + (if (eq eshell-hist-ignoredups 'erase) + ;; Remove any old occurrences of the input, and put + ;; the new one at the end. + (unless (ring-empty-p eshell-history-ring) + (ring-remove eshell-history-ring + (ring-member eshell-history-ring input)) + t) + ;; Always add... + (or (null eshell-hist-ignoredups) + ;; ... or add if it's not already present at the + ;; end. + (not (ring-p eshell-history-ring)) + (ring-empty-p eshell-history-ring) + (not (string-equal (eshell-get-history 0) input))))) + (eshell-put-history input)) (setq eshell-save-history-index eshell-history-index) (setq eshell-history-index nil)) @@ -400,7 +407,7 @@ variable `eshell-input-filter' returns non-nil when called on the command. This function is supposed to be called from the minibuffer, presumably -as a minibuffer-exit-hook." +as a `minibuffer-exit-hook'." (eshell-add-input-to-history (buffer-substring (minibuffer-prompt-end) (point-max)))) @@ -444,7 +451,6 @@ line, with the most recent command last. See also (ignore-dups eshell-hist-ignoredups)) (with-temp-buffer (insert-file-contents file) - ;; Save restriction in case file is already visited... ;; Watch for those date stamps in history files! (goto-char (point-max)) (while (and (< count size) @@ -470,15 +476,16 @@ lost if `eshell-history-ring' is not empty. If Useful within process sentinels. See also `eshell-read-history'." - (let ((file (or filename eshell-history-file-name))) + (let* ((file (or filename eshell-history-file-name)) + (resolved-file (if (stringp file) (file-truename file)))) (cond ((or (null file) (equal file "") (null eshell-history-ring) (ring-empty-p eshell-history-ring)) nil) - ((not (file-writable-p file)) - (message "Cannot write history file %s" file)) + ((not (file-writable-p resolved-file)) + (message "Cannot write history file %s" resolved-file)) (t (let* ((ring eshell-history-ring) (index (ring-length ring))) @@ -488,10 +495,12 @@ See also `eshell-read-history'." (while (> index 0) (setq index (1- index)) (let ((start (point))) - (insert (ring-ref ring index) ?\n) + ;; Remove properties before inserting, to avoid trouble + ;; with read-only strings (Bug#28700). + (insert (substring-no-properties (ring-ref ring index)) ?\n) (subst-char-in-region start (1- (point)) ?\n ?\177))) (eshell-with-private-file-modes - (write-region (point-min) (point-max) file append + (write-region (point-min) (point-max) resolved-file append 'no-message)))))))) (defun eshell-list-history () @@ -583,21 +592,30 @@ See also `eshell-read-history'." (defun eshell-expand-history-references (beg end) "Parse and expand any history references in current input." - (let ((result (eshell-hist-parse-arguments beg end))) + (let ((result (eshell-hist-parse-arguments beg end)) + (full-line (buffer-substring-no-properties beg end))) (when result (let ((textargs (nreverse (nth 0 result))) (posb (nreverse (nth 1 result))) - (pose (nreverse (nth 2 result)))) + (pose (nreverse (nth 2 result))) + (full-line-subst (eshell-history-substitution full-line))) (save-excursion - (while textargs - (let ((str (eshell-history-reference (car textargs)))) - (unless (eq str (car textargs)) - (goto-char (car posb)) - (insert-and-inherit str) - (delete-char (- (car pose) (car posb))))) - (setq textargs (cdr textargs) - posb (cdr posb) - pose (cdr pose)))))))) + (if full-line-subst + ;; Found a ^foo^bar substitution + (progn + (goto-char beg) + (insert-and-inherit full-line-subst) + (delete-char (- end beg))) + ;; Try to expand other substitutions + (while textargs + (let ((str (eshell-history-reference (car textargs)))) + (unless (eq str (car textargs)) + (goto-char (car posb)) + (insert-and-inherit str) + (delete-char (- (car pose) (car posb))))) + (setq textargs (cdr textargs) + posb (cdr posb) + pose (cdr pose))))))))) (defvar pcomplete-stub) (defvar pcomplete-last-completion-raw) @@ -632,20 +650,31 @@ See also `eshell-read-history'." (setq history (cdr history))) (cdr fhist))))))) +(defun eshell-history-substitution (line) + "Expand quick hist substitutions formatted as ^foo^bar^. +Returns nil if string does not match quick substitution format, +and acts like !!:s/foo/bar/ otherwise." + ;; `^string1^string2^' + ;; Quick Substitution. Repeat the last command, replacing + ;; STRING1 with STRING2. Equivalent to `!!:s/string1/string2/' + (when (and (eshell-using-module 'eshell-pred) + (string-match + "^\\^\\([^^]+\\)\\^\\([^^]+\\)\\(?:\\^\\(.*\\)\\)?$" + line)) + ;; Save trailing match as `eshell-history-reference' runs string-match. + (let ((matched-end (match-string 3 line))) + (concat + (eshell-history-reference + (format "!!:s/%s/%s/" + (match-string 1 line) + (match-string 2 line))) + matched-end)))) + (defun eshell-history-reference (reference) "Expand directory stack REFERENCE. The syntax used here was taken from the Bash info manual. Returns the resultant reference, or the same string REFERENCE if none matched." - ;; `^string1^string2^' - ;; Quick Substitution. Repeat the last command, replacing - ;; STRING1 with STRING2. Equivalent to `!!:s/string1/string2/' - (if (and (eshell-using-module 'eshell-pred) - (string-match "\\^\\([^^]+\\)\\^\\([^^]+\\)\\^?\\s-*$" - reference)) - (setq reference (format "!!:s/%s/%s/" - (match-string 1 reference) - (match-string 2 reference)))) ;; `!' ;; Start a history substitution, except when followed by a ;; space, tab, the end of the line, = or (. @@ -735,7 +764,7 @@ matched." (setq nth (eshell-hist-word-reference nth))) (unless (numberp mth) (setq mth (eshell-hist-word-reference mth))) - (cons (mapconcat 'identity (eshell-sublist textargs nth mth) " ") + (cons (mapconcat #'identity (seq-subseq textargs nth (1+ mth)) " ") end)))) (defun eshell-hist-parse-modifier (hist reference) @@ -835,7 +864,7 @@ Moves relative to START, or `eshell-history-index'." (setq prev n n (mod (+ n motion) len)) ;; If we haven't reached a match, step some more. - (while (and (< n len) (not tried-each-ring-item) + (while (and (not tried-each-ring-item) (not (string-match regexp (eshell-get-history n)))) (setq n (mod (+ n motion) len) ;; If we have gone all the way around in this search. |