* lisp/mail/footnote.el (footnote-align-to-fn-text): New config var
[emacs.git] / lisp / winner.el
blob041e9253607a1433dd80867123295a670985247e
1 ;;; winner.el --- Restore old window configurations
3 ;; Copyright (C) 1997-1998, 2001-2017 Free Software Foundation, Inc.
5 ;; Author: Ivar Rummelhoff <ivarru@math.uio.no>
6 ;; Created: 27 Feb 1997
7 ;; Keywords: convenience frames
9 ;; This file is part of GNU Emacs.
11 ;; GNU Emacs is free software: you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
24 ;;; Commentary:
26 ;; Winner mode is a global minor mode that records the changes in the
27 ;; window configuration (i.e. how the frames are partitioned into
28 ;; windows) so that the changes can be "undone" using the command
29 ;; `winner-undo'. By default this one is bound to the key sequence
30 ;; ctrl-c left. If you change your mind (while undoing), you can
31 ;; press ctrl-c right (calling `winner-redo'). Even though it uses
32 ;; some features of Emacs20.3, winner.el should also work with
33 ;; Emacs19.34 and XEmacs20, provided that the installed version of
34 ;; custom is not obsolete.
36 ;; Winner mode was improved August 1998.
37 ;; Further improvements February 2002.
39 ;;; Code:
41 (eval-when-compile (require 'cl-lib))
43 (defun winner-active-region ()
44 (declare (gv-setter (lambda (store)
45 (if (featurep 'xemacs)
46 `(if ,store (zmacs-activate-region)
47 (zmacs-deactivate-region))
48 `(if ,store (activate-mark) (deactivate-mark))))))
49 (region-active-p))
51 (defalias 'winner-edges
52 (if (featurep 'xemacs) 'window-pixel-edges 'window-edges))
53 (defalias 'winner-window-list
54 (if (featurep 'xemacs)
55 (lambda () (delq (minibuffer-window) (window-list nil 0)))
56 (lambda () (window-list nil 0))))
58 (require 'ring)
60 (defgroup winner nil
61 "Restoring window configurations."
62 :group 'windows)
64 (defcustom winner-dont-bind-my-keys nil
65 "Non-nil means do not bind keys in Winner mode."
66 :type 'boolean
67 :group 'winner)
69 (defcustom winner-ring-size 200
70 "Maximum number of stored window configurations per frame."
71 :type 'integer
72 :group 'winner)
74 (defcustom winner-boring-buffers '("*Completions*")
75 "List of buffer names whose windows `winner-undo' will not restore.
76 You may want to include buffer names such as *Help*, *Apropos*,
77 *Buffer List*, *info* and *Compile-Log*."
78 :type '(repeat string)
79 :group 'winner)
83 ;;;; Saving old configurations (internal variables and subroutines)
86 ;;; Current configuration
88 ;; List the windows according to their edges.
89 (defun winner-sorted-window-list ()
90 (sort (winner-window-list)
91 (lambda (x y)
92 (cl-loop for a in (winner-edges x)
93 for b in (winner-edges y)
94 while (= a b)
95 finally return (< a b)))))
97 (defun winner-win-data ()
98 ;; Essential properties of the windows in the selected frame.
99 (cl-loop for win in (winner-sorted-window-list)
100 collect (cons (winner-edges win) (window-buffer win))))
102 ;; This variable is updated with the current window configuration
103 ;; every time it changes.
104 (defvar winner-currents nil)
106 ;; The current configuration (+ the buffers involved).
107 (defsubst winner-conf ()
108 (cons (current-window-configuration)
109 (winner-win-data)))
112 ;; Save current configuration.
113 ;; (Called below by `winner-save-old-configurations').
114 (defun winner-remember ()
115 (setf (alist-get (selected-frame) winner-currents) (winner-conf)))
117 ;; Consult `winner-currents'.
118 (defun winner-configuration (&optional frame)
119 (or (cdr (assq (or frame (selected-frame)) winner-currents))
120 (with-selected-frame frame
121 (winner-conf))))
125 ;;; Saved configurations
127 ;; This variable contains the window configuration rings.
128 ;; The key in this alist is the frame.
129 (defvar winner-ring-alist nil)
131 ;; Find the right ring. If it does not exist, create one.
132 (defsubst winner-ring (frame)
133 (or (cdr (assq frame winner-ring-alist))
134 (let ((ring (make-ring winner-ring-size)))
135 (ring-insert ring (winner-configuration frame))
136 (push (cons frame ring) winner-ring-alist)
137 ring)))
140 ;; If the same command is called several times in a row,
141 ;; we only save one window configuration.
142 (defvar winner-last-command nil)
144 ;; Frames affected by the previous command.
145 (defvar winner-last-frames nil)
148 (defsubst winner-equal (a b)
149 "Check whether two Winner configurations (as produced by
150 `winner-conf') are equal."
151 (equal (cdr a) (cdr b)))
154 ;; Save the current window configuration, if it has changed.
155 ;; If so return frame, otherwise return nil.
156 (defun winner-insert-if-new (frame)
157 (unless (or (memq frame winner-last-frames)
158 (eq this-command 'winner-redo))
159 (let ((conf (winner-configuration frame))
160 (ring (winner-ring frame)))
161 (when (and (not (ring-empty-p ring))
162 (winner-equal conf (ring-ref ring 0)))
163 ;; When the previous configuration was very similar,
164 ;; keep only the latest.
165 (ring-remove ring 0))
166 (ring-insert ring conf)
167 (push frame winner-last-frames)
168 frame)))
172 ;;; Hooks
174 ;; Frames affected by the current command.
175 (defvar winner-modified-list nil)
177 ;; Called whenever the window configuration changes
178 ;; (a `window-configuration-change-hook').
179 (defun winner-change-fun ()
181 ;; Cull dead frames.
182 (setq winner-modified-list
183 (cl-loop for frame in winner-modified-list
184 if (frame-live-p frame) collect frame))
186 (unless (or (memq (selected-frame) winner-modified-list)
187 (/= 0 (minibuffer-depth)))
188 (push (selected-frame) winner-modified-list)))
190 ;; A `post-command-hook' for emacsen with
191 ;; `window-configuration-change-hook'.
192 (defun winner-save-old-configurations ()
193 (when (zerop (minibuffer-depth))
194 (unless (eq this-command winner-last-command)
195 (setq winner-last-frames nil)
196 (setq winner-last-command this-command))
197 (dolist (frame winner-modified-list)
198 (winner-insert-if-new frame))
199 (setq winner-modified-list nil)
200 (winner-remember)))
202 ;; A `minibuffer-setup-hook'.
203 (defun winner-save-unconditionally ()
204 (unless (eq this-command winner-last-command)
205 (setq winner-last-frames nil)
206 (setq winner-last-command this-command))
207 (winner-insert-if-new (selected-frame))
208 (winner-remember))
210 ;; A `post-command-hook' for other emacsen.
211 ;; Also called by `winner-undo' before "undoing".
212 (defun winner-save-conditionally ()
213 (when (zerop (minibuffer-depth))
214 (winner-save-unconditionally)))
219 ;;;; Restoring configurations
221 ;; Works almost as `set-window-configuration',
222 ;; but does not change the contents or the size of the minibuffer,
223 ;; and tries to preserve the selected window.
224 (defun winner-set-conf (winconf)
225 (let* ((miniwin (minibuffer-window))
226 (chosen (selected-window))
227 (minisize (window-height miniwin)))
228 (cl-letf (((window-buffer miniwin))
229 ((window-point miniwin)))
230 (set-window-configuration winconf))
231 (cond
232 ((window-live-p chosen) (select-window chosen))
233 ((window-minibuffer-p) (other-window 1)))
234 (when (/= minisize (window-height miniwin))
235 (with-selected-window miniwin
236 (setf (window-height) minisize)))))
240 (defvar winner-point-alist nil)
241 ;; `set-window-configuration' restores old points and marks. This is
242 ;; not what we want, so we make a list of the "real" (i.e. new) points
243 ;; and marks before undoing window configurations.
245 ;; Format of entries: (buffer (mark . mark-active) (window . point) ..)
247 (defun winner-make-point-alist ()
248 (save-current-buffer
249 (cl-loop with alist
250 for win in (winner-window-list)
251 for entry =
252 (or (assq (window-buffer win) alist)
253 (car (push (list (set-buffer (window-buffer win))
254 (cons (mark t) (winner-active-region)))
255 alist)))
256 do (push (cons win (window-point win))
257 (cddr entry))
258 finally return alist)))
260 (defun winner-get-point (buf win)
261 ;; Consult (and possibly extend) `winner-point-alist'.
262 ;; Returns nil if buf no longer exists.
263 (when (buffer-name buf)
264 (let ((entry (assq buf winner-point-alist)))
265 (cond
266 (entry
267 (or (cdr (assq win (cddr entry)))
268 (cdr (assq nil (cddr entry)))
269 (with-current-buffer buf
270 (push (cons nil (point)) (cddr entry))
271 (point))))
272 (t (with-current-buffer buf
273 (push (list buf
274 (cons (mark t) (winner-active-region))
275 (cons nil (point)))
276 winner-point-alist)
277 (point)))))))
280 ;; Make sure point does not end up in the minibuffer and delete
281 ;; windows displaying dead or boring buffers
282 ;; (c.f. `winner-boring-buffers'). Return nil if all the windows
283 ;; should be deleted. Preserve correct points and marks.
284 (defun winner-set (conf)
285 ;; For the format of `conf', see `winner-conf'.
286 (let* ((buffers nil)
287 (alive
288 ;; Possibly update `winner-point-alist'
289 (cl-loop for buf in (mapcar 'cdr (cdr conf))
290 for pos = (winner-get-point buf nil)
291 if (and pos (not (memq buf buffers)))
292 do (push buf buffers)
293 collect pos)))
294 (winner-set-conf (car conf))
295 (let (xwins) ; to be deleted
297 ;; Restore points
298 (dolist (win (winner-sorted-window-list))
299 (unless (and (pop alive)
300 (setf (window-point win)
301 (winner-get-point (window-buffer win) win))
302 (not (member (buffer-name (window-buffer win))
303 winner-boring-buffers)))
304 (push win xwins))) ; delete this window
306 ;; Restore marks
307 ;; `winner-undo' shouldn't update the selection (Bug#28631) when
308 ;; select-enable-primary is non-nil.
309 (unless select-enable-primary
310 (save-current-buffer
311 (cl-loop for buf in buffers
312 for entry = (cadr (assq buf winner-point-alist))
313 do (progn (set-buffer buf)
314 (set-mark (car entry))
315 (setf (winner-active-region) (cdr entry))))))
316 ;; Delete windows, whose buffers are dead or boring.
317 ;; Return t if this is still a possible configuration.
318 (or (null xwins)
319 (progn
320 (mapc 'delete-window (cdr xwins)) ; delete all but one
321 (unless (one-window-p t)
322 (delete-window (car xwins))
323 t))))))
327 ;;;; Winner mode (a minor mode)
329 (defcustom winner-mode-hook nil
330 "Functions to run whenever Winner mode is turned on or off."
331 :type 'hook
332 :group 'winner)
334 (define-obsolete-variable-alias 'winner-mode-leave-hook
335 'winner-mode-off-hook "24.3")
337 (defcustom winner-mode-off-hook nil
338 "Functions to run whenever Winner mode is turned off."
339 :type 'hook
340 :group 'winner)
342 (defvar winner-mode-map
343 (let ((map (make-sparse-keymap)))
344 (unless winner-dont-bind-my-keys
345 (define-key map [(control c) left] 'winner-undo)
346 (define-key map [(control c) right] 'winner-redo))
347 map)
348 "Keymap for Winner mode.")
351 ;;;###autoload
352 (define-minor-mode winner-mode
353 "Toggle Winner mode on or off.
354 With a prefix argument ARG, enable Winner mode if ARG is
355 positive, and disable it otherwise. If called from Lisp, enable
356 the mode if ARG is omitted or nil, and toggle it if ARG is `toggle'.
358 Winner mode is a global minor mode that records the changes in
359 the window configuration (i.e. how the frames are partitioned
360 into windows) so that the changes can be \"undone\" using the
361 command `winner-undo'. By default this one is bound to the key
362 sequence `C-c <left>'. If you change your mind (while undoing),
363 you can press `C-c <right>' (calling `winner-redo')."
364 :global t
365 (if winner-mode
366 (progn
367 (add-hook 'window-configuration-change-hook 'winner-change-fun)
368 (add-hook 'post-command-hook 'winner-save-old-configurations)
369 (add-hook 'minibuffer-setup-hook 'winner-save-unconditionally)
370 (setq winner-modified-list (frame-list))
371 (winner-save-old-configurations))
372 (remove-hook 'window-configuration-change-hook 'winner-change-fun)
373 (remove-hook 'post-command-hook 'winner-save-old-configurations)
374 (remove-hook 'minibuffer-setup-hook 'winner-save-unconditionally)))
376 ;; Inspired by undo (simple.el)
378 (defvar winner-undo-frame nil)
380 (defvar winner-pending-undo-ring nil
381 "The ring currently used by `winner-undo'.")
382 (defvar winner-undo-counter nil)
383 (defvar winner-undone-data nil) ; There confs have been passed.
385 (defun winner-undo ()
386 "Switch back to an earlier window configuration saved by Winner mode.
387 In other words, \"undo\" changes in window configuration."
388 (interactive)
389 (cond
390 ((not winner-mode) (error "Winner mode is turned off"))
391 (t (unless (and (eq last-command 'winner-undo)
392 (eq winner-undo-frame (selected-frame)))
393 (winner-save-conditionally) ; current configuration->stack
394 (setq winner-undo-frame (selected-frame))
395 (setq winner-point-alist (winner-make-point-alist))
396 (setq winner-pending-undo-ring (winner-ring (selected-frame)))
397 (setq winner-undo-counter 0)
398 (setq winner-undone-data (list (winner-win-data))))
399 (cl-incf winner-undo-counter) ; starting at 1
400 (when (and (winner-undo-this)
401 (not (window-minibuffer-p)))
402 (message "Winner undo (%d / %d)"
403 winner-undo-counter
404 (1- (ring-length winner-pending-undo-ring)))))))
409 (defun winner-undo-this () ; The heart of winner undo.
410 (cl-loop
411 (cond
412 ((>= winner-undo-counter (ring-length winner-pending-undo-ring))
413 (message "No further window configuration undo information")
414 (cl-return nil))
416 ((and ; If possible configuration
417 (winner-set (ring-ref winner-pending-undo-ring
418 winner-undo-counter))
419 ; .. and new configuration
420 (let ((data (winner-win-data)))
421 (and (not (member data winner-undone-data))
422 (push data winner-undone-data))))
423 (cl-return t)) ; .. then everything is fine.
424 (t ;; Otherwise, discharge it (and try the next one).
425 (ring-remove winner-pending-undo-ring winner-undo-counter)))))
428 (defun winner-redo () ; If you change your mind.
429 "Restore a more recent window configuration saved by Winner mode."
430 (interactive)
431 (cond
432 ((eq last-command 'winner-undo)
433 (winner-set
434 (if (zerop (minibuffer-depth))
435 (ring-remove winner-pending-undo-ring 0)
436 (ring-ref winner-pending-undo-ring 0)))
437 (unless (eq (selected-window) (minibuffer-window))
438 (message "Winner undid undo")))
439 (t (user-error "Previous command was not a `winner-undo'"))))
441 (provide 'winner)
442 ;;; winner.el ends here