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