Merge branch 'master' into comment-cache
[emacs.git] / lisp / winner.el
blob7b0483338b975d443935cec60b9cb88b0f1eaf65
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 <http://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 (save-current-buffer
308 (cl-loop for buf in buffers
309 for entry = (cadr (assq buf winner-point-alist))
310 do (progn (set-buffer buf)
311 (set-mark (car entry))
312 (setf (winner-active-region) (cdr entry)))))
313 ;; Delete windows, whose buffers are dead or boring.
314 ;; Return t if this is still a possible configuration.
315 (or (null xwins)
316 (progn
317 (mapc 'delete-window (cdr xwins)) ; delete all but one
318 (unless (one-window-p t)
319 (delete-window (car xwins))
320 t))))))
324 ;;;; Winner mode (a minor mode)
326 (defcustom winner-mode-hook nil
327 "Functions to run whenever Winner mode is turned on or off."
328 :type 'hook
329 :group 'winner)
331 (define-obsolete-variable-alias 'winner-mode-leave-hook
332 'winner-mode-off-hook "24.3")
334 (defcustom winner-mode-off-hook nil
335 "Functions to run whenever Winner mode is turned off."
336 :type 'hook
337 :group 'winner)
339 (defvar winner-mode-map
340 (let ((map (make-sparse-keymap)))
341 (unless winner-dont-bind-my-keys
342 (define-key map [(control c) left] 'winner-undo)
343 (define-key map [(control c) right] 'winner-redo))
344 map)
345 "Keymap for Winner mode.")
348 ;;;###autoload
349 (define-minor-mode winner-mode
350 "Toggle Winner mode on or off.
351 With a prefix argument ARG, enable Winner mode if ARG is
352 positive, and disable it otherwise. If called from Lisp, enable
353 the mode if ARG is omitted or nil, and toggle it if ARG is ‘toggle’.
355 Winner mode is a global minor mode that records the changes in
356 the window configuration (i.e. how the frames are partitioned
357 into windows) so that the changes can be \"undone\" using the
358 command `winner-undo'. By default this one is bound to the key
359 sequence `C-c <left>'. If you change your mind (while undoing),
360 you can press `C-c <right>' (calling `winner-redo')."
361 :global t
362 (if winner-mode
363 (progn
364 (add-hook 'window-configuration-change-hook 'winner-change-fun)
365 (add-hook 'post-command-hook 'winner-save-old-configurations)
366 (add-hook 'minibuffer-setup-hook 'winner-save-unconditionally)
367 (setq winner-modified-list (frame-list))
368 (winner-save-old-configurations))
369 (remove-hook 'window-configuration-change-hook 'winner-change-fun)
370 (remove-hook 'post-command-hook 'winner-save-old-configurations)
371 (remove-hook 'minibuffer-setup-hook 'winner-save-unconditionally)))
373 ;; Inspired by undo (simple.el)
375 (defvar winner-undo-frame nil)
377 (defvar winner-pending-undo-ring nil
378 "The ring currently used by `winner-undo'.")
379 (defvar winner-undo-counter nil)
380 (defvar winner-undone-data nil) ; There confs have been passed.
382 (defun winner-undo ()
383 "Switch back to an earlier window configuration saved by Winner mode.
384 In other words, \"undo\" changes in window configuration."
385 (interactive)
386 (cond
387 ((not winner-mode) (error "Winner mode is turned off"))
388 (t (unless (and (eq last-command 'winner-undo)
389 (eq winner-undo-frame (selected-frame)))
390 (winner-save-conditionally) ; current configuration->stack
391 (setq winner-undo-frame (selected-frame))
392 (setq winner-point-alist (winner-make-point-alist))
393 (setq winner-pending-undo-ring (winner-ring (selected-frame)))
394 (setq winner-undo-counter 0)
395 (setq winner-undone-data (list (winner-win-data))))
396 (cl-incf winner-undo-counter) ; starting at 1
397 (when (and (winner-undo-this)
398 (not (window-minibuffer-p)))
399 (message "Winner undo (%d / %d)"
400 winner-undo-counter
401 (1- (ring-length winner-pending-undo-ring)))))))
406 (defun winner-undo-this () ; The heart of winner undo.
407 (cl-loop
408 (cond
409 ((>= winner-undo-counter (ring-length winner-pending-undo-ring))
410 (message "No further window configuration undo information")
411 (cl-return nil))
413 ((and ; If possible configuration
414 (winner-set (ring-ref winner-pending-undo-ring
415 winner-undo-counter))
416 ; .. and new configuration
417 (let ((data (winner-win-data)))
418 (and (not (member data winner-undone-data))
419 (push data winner-undone-data))))
420 (cl-return t)) ; .. then everything is fine.
421 (t ;; Otherwise, discharge it (and try the next one).
422 (ring-remove winner-pending-undo-ring winner-undo-counter)))))
425 (defun winner-redo () ; If you change your mind.
426 "Restore a more recent window configuration saved by Winner mode."
427 (interactive)
428 (cond
429 ((eq last-command 'winner-undo)
430 (winner-set
431 (if (zerop (minibuffer-depth))
432 (ring-remove winner-pending-undo-ring 0)
433 (ring-ref winner-pending-undo-ring 0)))
434 (unless (eq (selected-window) (minibuffer-window))
435 (message "Winner undid undo")))
436 (t (user-error "Previous command was not a `winner-undo'"))))
438 (provide 'winner)
439 ;;; winner.el ends here