vertico-repeat.el (9451B)
1 ;;; vertico-repeat.el --- Repeat Vertico sessions -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2021-2024 Free Software Foundation, Inc. 4 5 ;; Author: Daniel Mendler <mail@daniel-mendler.de> 6 ;; Maintainer: Daniel Mendler <mail@daniel-mendler.de> 7 ;; Created: 2021 8 ;; Package-Requires: ((emacs "28.1") (compat "30") (vertico "1.9")) 9 ;; URL: https://github.com/minad/vertico 10 11 ;; This file is part of GNU Emacs. 12 13 ;; This program is free software: you can redistribute it and/or modify 14 ;; it under the terms of the GNU General Public License as published by 15 ;; the Free Software Foundation, either version 3 of the License, or 16 ;; (at your option) any later version. 17 18 ;; This program is distributed in the hope that it will be useful, 19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 ;; GNU General Public License for more details. 22 23 ;; You should have received a copy of the GNU General Public License 24 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 25 26 ;;; Commentary: 27 28 ;; This package is a Vertico extension, which enables repetition of 29 ;; Vertico sessions via the `vertico-repeat', `vertico-repeat-previous' 30 ;; and `vertico-repeat-select' commands. If the repeat commands are 31 ;; called from an existing Vertico minibuffer session, only sessions 32 ;; corresponding to the current minibuffer command are offered via 33 ;; completion. 34 ;; 35 ;; It is necessary to register a minibuffer setup hook, which saves 36 ;; the Vertico state for repetition. In order to save the history 37 ;; across Emacs sessions, enable `savehist-mode' and add 38 ;; `vertico-repeat-history' to `savehist-additional-variables'. 39 ;; 40 ;; (keymap-global-set "M-R" #'vertico-repeat) 41 ;; (keymap-set vertico-map "M-P" #'vertico-repeat-previous) 42 ;; (keymap-set vertico-map "M-N" #'vertico-repeat-next) 43 ;; (keymap-set vertico-map "S-<prior>" #'vertico-repeat-previous) 44 ;; (keymap-set vertico-map "S-<next>" #'vertico-repeat-next) 45 ;; (add-hook 'minibuffer-setup-hook #'vertico-repeat-save) 46 ;; 47 ;; See also the related extension `vertico-suspend', which uses a 48 ;; different technique, relying on recursive minibuffers to suspend 49 ;; the current completion session temporarily while preserving the 50 ;; entire state. 51 52 ;;; Code: 53 54 (require 'vertico) 55 (eval-when-compile (require 'cl-lib)) 56 57 (defcustom vertico-repeat-filter 58 '(vertico-repeat 59 vertico-repeat-select 60 execute-extended-command 61 execute-extended-command-for-buffer) 62 "List of commands to filter out from the history." 63 :type '(repeat symbol) 64 :group 'vertico) 65 66 (defcustom vertico-repeat-transformers 67 (list #'vertico-repeat--filter-empty 68 #'vertico-repeat--filter-commands) 69 "List of functions to apply to history element before saving." 70 :type '(repeat function) 71 :group 'vertico) 72 73 (declare-function vertico-multiform-vertical "ext:vertico-multiform") 74 (defvar vertico-multiform--display-modes) 75 (defvar vertico-repeat-history nil) 76 (defvar-local vertico-repeat--command nil) 77 (defvar-local vertico-repeat--input nil) 78 (defvar-local vertico-repeat--step nil) 79 (defvar-local vertico-repeat--pos 0) 80 81 (defun vertico-repeat--filter-commands (session) 82 "Filter SESSION if command is listed in `vertico-repeat-filter'." 83 (and (not (memq (car session) vertico-repeat-filter)) session)) 84 85 (defun vertico-repeat--filter-empty (session) 86 "Filter SESSION if input is empty." 87 (and (cadr session) (not (equal (cadr session) "")) session)) 88 89 (defun vertico-repeat--save-input () 90 "Save current minibuffer input." 91 (setq vertico-repeat--input (minibuffer-contents-no-properties))) 92 93 (defun vertico-repeat--current () 94 "Return the current session datum." 95 `(,vertico-repeat--command 96 ,vertico-repeat--input 97 ,@(and vertico--lock-candidate 98 (>= vertico--index 0) 99 (list (substring-no-properties 100 (nth vertico--index vertico--candidates)))) 101 ,@(and (bound-and-true-p vertico-multiform-mode) 102 (ensure-list 103 (seq-find (lambda (x) (and (boundp x) (symbol-value x))) 104 vertico-multiform--display-modes))))) 105 106 (defun vertico-repeat--save-exit () 107 "Save command session in `vertico-repeat-history'." 108 (let ((session (vertico-repeat--current)) 109 (transform vertico-repeat-transformers)) 110 (while (and transform (setq session (funcall (pop transform) session)))) 111 (when session 112 (add-to-history 'vertico-repeat-history session)))) 113 114 (defun vertico-repeat--restore (session) 115 "Restore Vertico SESSION for `vertico-repeat'." 116 (delete-minibuffer-contents) 117 (insert (cadr session)) 118 (setq vertico--lock-candidate 119 (when-let ((cand (seq-find #'stringp (cddr session)))) 120 (vertico--update) 121 (when-let ((idx (seq-position vertico--candidates cand))) 122 (setq vertico--index idx) 123 t))) 124 ;; Restore display modes if not modifying the current session 125 (when-let (((not (and vertico-repeat--command 126 (eq vertico-repeat--command (car session))))) 127 (mode (seq-find #'symbolp (cddr session))) 128 ((bound-and-true-p vertico-multiform-mode)) 129 ((not (and (boundp mode) (symbol-value mode))))) 130 (vertico-multiform-vertical mode)) 131 (vertico--exhibit)) 132 133 (defun vertico-repeat--run (session) 134 "Run Vertico completion SESSION." 135 (unless session 136 (user-error "No repeatable session")) 137 (if (and vertico-repeat--command (eq vertico-repeat--command (car session))) 138 (vertico-repeat--restore session) 139 (minibuffer-with-setup-hook 140 (apply-partially #'vertico-repeat--restore session) 141 (command-execute (setq this-command (car session)))))) 142 143 ;;;###autoload 144 (defun vertico-repeat-save () 145 "Save Vertico session for `vertico-repeat'. 146 This function must be registered as `minibuffer-setup-hook'." 147 (when (and vertico--input (symbolp this-command)) 148 (setq vertico-repeat--command this-command) 149 (add-hook 'post-command-hook #'vertico-repeat--save-input nil 'local) 150 (add-hook 'minibuffer-exit-hook #'vertico-repeat--save-exit nil 'local))) 151 152 ;;;###autoload 153 (defun vertico-repeat-next (n) 154 "Repeat Nth next Vertico completion session. 155 This command must be called from an existing Vertico session 156 after `vertico-repeat-previous'." 157 (interactive "p") 158 (vertico-repeat-previous (- n))) 159 160 ;;;###autoload 161 (defun vertico-repeat-previous (n) 162 "Repeat Nth previous Vertico completion session. 163 If called from an existing Vertico session, restore the input and 164 selected candidate for the current command." 165 (interactive "p") 166 (vertico-repeat--run 167 (if (not vertico-repeat--command) 168 (and (> n 0) (nth (1- n) vertico-repeat-history)) 169 (cond 170 ((not vertico-repeat--step) 171 (setq vertico-repeat--step 172 (cons (vertico-repeat--current) 173 (cl-loop for h in vertico-repeat-history 174 if (eq (car h) vertico-repeat--command) collect h)))) 175 ((= vertico-repeat--pos 0) 176 (setcar vertico-repeat--step (vertico-repeat--current)))) 177 (cl-incf n vertico-repeat--pos) 178 (when-let (((>= n 0)) (session (nth n vertico-repeat--step))) 179 (setq vertico-repeat--pos n) 180 session)))) 181 182 ;;;###autoload 183 (defun vertico-repeat-select () 184 "Select a Vertico session from the session history and repeat it. 185 If called from an existing Vertico session, you can select among 186 previous sessions for the current command." 187 (interactive) 188 (vertico-repeat--run 189 (let* ((current-cmd vertico-repeat--command) 190 (trimmed 191 (delete-dups 192 (or 193 (cl-loop 194 for session in vertico-repeat-history 195 if (or (not current-cmd) (eq (car session) current-cmd)) 196 collect 197 (list 198 (symbol-name (car session)) 199 (replace-regexp-in-string 200 "\\s-+" " " 201 (string-trim (cadr session))) 202 session)) 203 (user-error "No repeatable session")))) 204 (max-cmd (cl-loop for (cmd . _) in trimmed 205 maximize (string-width cmd))) 206 (formatted (cl-loop 207 for (cmd input session) in trimmed collect 208 (cons 209 (concat 210 (and (not current-cmd) 211 (propertize cmd 'face 'font-lock-function-name-face)) 212 (and (not current-cmd) 213 (make-string (- max-cmd (string-width cmd) -4) ?\s)) 214 input) 215 session))) 216 (enable-recursive-minibuffers t)) 217 (cdr (assoc (completing-read 218 (if current-cmd 219 (format "History of %s: " current-cmd) 220 "Completion history: ") 221 (lambda (str pred action) 222 (if (eq action 'metadata) 223 '(metadata (display-sort-function . identity) 224 (cycle-sort-function . identity)) 225 (complete-with-action action formatted str pred))) 226 nil t nil t) 227 formatted))))) 228 229 ;;;###autoload 230 (defun vertico-repeat (&optional arg) 231 "Repeat last Vertico session. 232 If prefix ARG is non-nil, offer completion menu to select from session history." 233 (interactive "P") 234 (if arg (vertico-repeat-select) (vertico-repeat-previous 1))) 235 236 (provide 'vertico-repeat) 237 ;;; vertico-repeat.el ends here