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