]> code.delx.au - gnu-emacs-elpa/blob - ace-window.el
fdd542215a64f39e6577a185a169e459c85b5514
[gnu-emacs-elpa] / ace-window.el
1 ;;; ace-window.el --- Quickly switch windows. -*- lexical-binding: t -*-
2
3 ;; Copyright (C) 2015 Free Software Foundation, Inc.
4
5 ;; Author: Oleh Krehel <ohwoeowho@gmail.com>
6 ;; Maintainer: Oleh Krehel <ohwoeowho@gmail.com>
7 ;; URL: https://github.com/abo-abo/ace-window
8 ;; Version: 0.8.0
9 ;; Package-Requires: ((avy "0.1.0"))
10 ;; Keywords: window, location
11
12 ;; This file is part of GNU Emacs.
13
14 ;; This file 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, or (at your option)
17 ;; 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 ;; For a full copy of the GNU General Public License
25 ;; see <http://www.gnu.org/licenses/>.
26
27 ;;; Commentary:
28 ;;
29 ;; The main function, `ace-window' is meant to replace `other-window'.
30 ;; In fact, when there are only two windows present, `other-window' is
31 ;; called. If there are more, each window will have its first
32 ;; character highlighted. Pressing that character will switch to that
33 ;; window.
34 ;;
35 ;; To setup this package, just add to your .emacs:
36 ;;
37 ;; (global-set-key (kbd "M-p") 'ace-window)
38 ;;
39 ;; replacing "M-p" with an appropriate shortcut.
40 ;;
41 ;; Depending on your window usage patterns, you might want to set
42 ;;
43 ;; (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
44 ;;
45 ;; This way they are all on the home row, although the intuitive
46 ;; ordering is lost.
47 ;;
48 ;; If you don't want the gray background that makes the red selection
49 ;; characters stand out more, set this:
50 ;;
51 ;; (setq aw-background nil)
52 ;;
53 ;; If you want to know the selection characters ahead of time, you can
54 ;; turn on `ace-window-display-mode'.
55 ;;
56 ;; When prefixed with one `universal-argument', instead of switching
57 ;; to selected window, the selected window is swapped with current one.
58 ;;
59 ;; When prefixed with two `universal-argument', the selected window is
60 ;; deleted instead.
61
62 ;;; Code:
63 (require 'avy-jump)
64 (require 'ring)
65
66 ;;* Customization
67 (defgroup ace-window nil
68 "Quickly switch current window."
69 :group 'convenience
70 :prefix "aw-")
71
72 (defcustom aw-keys '(?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)
73 "Keys for selecting window.")
74
75 (defcustom aw-scope 'global
76 "The scope used by `ace-window'."
77 :type '(choice
78 (const :tag "global" global)
79 (const :tag "frame" frame)))
80
81 (defcustom aw-ignored-buffers '("*Calc Trail*" "*LV*")
82 "List of buffers to ignore when selecting window."
83 :type '(repeat string))
84
85 (defcustom aw-ignore-on t
86 "When t, `ace-window' will ignore `aw-ignored-buffers'.
87 Use M-0 `ace-window' to toggle this value."
88 :type 'boolean)
89
90 (defcustom aw-background t
91 "When t, `ace-window' will dim out all buffers temporarily when used.'."
92 :type 'boolean)
93
94 (defcustom aw-leading-char-style 'char
95 "Style of the leading char overlay."
96 :type '(choice
97 (const :tag "single char" 'char)
98 (const :tag "full path" 'path)))
99
100 (defface aw-leading-char-face
101 '((((class color)) (:foreground "red"))
102 (((background dark)) (:foreground "gray100"))
103 (((background light)) (:foreground "gray0"))
104 (t (:foreground "gray100" :underline nil)))
105 "Face for each window's leading char.")
106
107 (defface aw-background-face
108 '((t (:foreground "gray40")))
109 "Face for whole window background during selection.")
110
111 (defface aw-mode-line-face
112 '((t (:inherit mode-line-buffer-id)))
113 "Face used for displaying the ace window key in the mode-line.")
114
115 ;;* Implementation
116 (defun aw-ignored-p (window)
117 "Return t if WINDOW should be ignored."
118 (and aw-ignore-on
119 (member (buffer-name (window-buffer window))
120 aw-ignored-buffers)))
121
122 (defun aw-window-list ()
123 "Return the list of interesting windows."
124 (sort
125 (cl-remove-if
126 (lambda (w)
127 (let ((f (window-frame w))
128 (b (window-buffer w)))
129 (or (not (and (frame-live-p f)
130 (frame-visible-p f)))
131 (string= "initial_terminal" (terminal-name f))
132 (aw-ignored-p w)
133 (with-current-buffer b
134 (and buffer-read-only
135 (= 0 (buffer-size b)))))))
136 (cl-case aw-scope
137 (global
138 (cl-mapcan #'window-list (frame-list)))
139 (frame
140 (window-list))
141 (t
142 (error "Invalid `aw-scope': %S" aw-scope))))
143 'aw-window<))
144
145 (defvar aw-overlays-back nil
146 "Hold overlays for when `aw-background' is t.")
147
148 (defvar ace-window-mode nil
149 "Minor mode during the selection process.")
150
151 ;; register minor mode
152 (or (assq 'ace-window-mode minor-mode-alist)
153 (nconc minor-mode-alist
154 (list '(ace-window-mode ace-window-mode))))
155
156 (defun aw--done ()
157 "Clean up mode line and overlays."
158 ;; mode line
159 (setq ace-window-mode nil)
160 (force-mode-line-update)
161 ;; background
162 (mapc #'delete-overlay aw-overlays-back)
163 (setq aw-overlays-back nil)
164 (avy--remove-leading-chars))
165
166 (defun aw--lead-overlay (path leaf)
167 "Create an overlay using PATH at LEAF.
168 LEAF is (PT . WND)."
169 (let* ((pt (car leaf))
170 (wnd (cdr leaf))
171 (ol (make-overlay pt (1+ pt) (window-buffer wnd)))
172 (old-str (or
173 (ignore-errors
174 (with-selected-window wnd
175 (buffer-substring pt (1+ pt))))
176 ""))
177 (new-str
178 (concat
179 (cl-case aw-leading-char-style
180 (char
181 (apply #'string (last path)))
182 (path
183 (apply #'string (reverse path)))
184 (t
185 (error "Bad `aw-leading-char-style': %S"
186 aw-leading-char-style)))
187 (cond ((string-equal old-str "\t")
188 (make-string (1- tab-width) ?\ ))
189 ((string-equal old-str "\n")
190 "\n")
191 (t
192 (make-string
193 (max 0 (1- (string-width old-str)))
194 ?\ ))))))
195 (overlay-put ol 'face 'aw-leading-char-face)
196 (overlay-put ol 'window wnd)
197 (overlay-put ol 'display new-str)
198 (push ol avy--overlays-lead)))
199
200 (defun aw--make-backgrounds (wnd-list)
201 "Create a dim background overlay for each window on WND-LIST."
202 (when aw-background
203 (setq aw-overlays-back
204 (mapcar (lambda (w)
205 (let ((ol (make-overlay
206 (window-start w)
207 (window-end w)
208 (window-buffer w))))
209 (overlay-put ol 'face 'aw-background-face)
210 ol))
211 wnd-list))))
212
213 (defvar aw--flip-keys nil
214 "Pre-processed `aw-flip-keys'.")
215
216 (defcustom aw-flip-keys '("n")
217 "Keys which should select the last window."
218 :set (lambda (sym val)
219 (set sym val)
220 (setq aw--flip-keys
221 (mapcar (lambda (x) (aref (kbd x) 0)) val))))
222
223 (defun aw-select (mode-line)
224 "Return a selected other window.
225 Amend MODE-LINE to the mode line for the duration of the selection."
226 (let ((start-window (selected-window))
227 (next-window-scope (cl-case aw-scope
228 ('global 'visible)
229 ('frame 'frame)))
230 (wnd-list (aw-window-list))
231 final-window)
232 (cl-case (length wnd-list)
233 (0
234 start-window)
235 (1
236 (car wnd-list))
237 (2
238 (setq final-window (next-window nil nil next-window-scope))
239 (while (and (aw-ignored-p final-window)
240 (not (equal final-window start-window)))
241 (setq final-window (next-window final-window nil next-window-scope)))
242 final-window)
243 (t
244 (let ((candidate-list
245 (mapcar (lambda (wnd)
246 ;; can't jump if the buffer is empty
247 (with-current-buffer (window-buffer wnd)
248 (when (= 0 (buffer-size))
249 (insert " ")))
250 (cons (aw-offset wnd) wnd))
251 wnd-list)))
252 (aw--make-backgrounds wnd-list)
253 (setq ace-window-mode mode-line)
254 (force-mode-line-update)
255 ;; turn off helm transient map
256 (remove-hook 'post-command-hook 'helm--maybe-update-keymap)
257 (unwind-protect
258 (condition-case err
259 (or (cdr (avy-read (avy-tree candidate-list aw-keys)
260 #'aw--lead-overlay
261 #'avy--remove-leading-chars))
262 start-window)
263 (error
264 (if (memq (nth 2 err) aw--flip-keys)
265 (aw--pop-window)
266 (signal (car err) (cdr err)))))
267 (aw--done)))))))
268
269 ;;* Interactive
270 ;;;###autoload
271 (defun ace-select-window ()
272 "Ace select window."
273 (interactive)
274 (aw-switch-to-window
275 (aw-select " Ace - Window")))
276
277 ;;;###autoload
278 (defun ace-delete-window ()
279 "Ace delete window."
280 (interactive)
281 (aw-delete-window
282 (aw-select " Ace - Delete Window")))
283
284 ;;;###autoload
285 (defun ace-swap-window ()
286 "Ace swap window."
287 (interactive)
288 (aw-swap-window
289 (aw-select " Ace - Swap Window")))
290
291 ;;;###autoload
292 (defun ace-maximize-window ()
293 "Ace maximize window."
294 (interactive)
295 (select-window
296 (aw-select " Ace - Maximize Window"))
297 (delete-other-windows))
298
299 ;;;###autoload
300 (defun ace-window (arg)
301 "Select a window.
302 Perform an action based on ARG described below.
303
304 By default, behaves like extended `other-window'.
305
306 Prefixed with one \\[universal-argument], does a swap between the
307 selected window and the current window, so that the selected
308 buffer moves to current window (and current buffer moves to
309 selected window).
310
311 Prefixed with two \\[universal-argument]'s, deletes the selected
312 window."
313 (interactive "p")
314 (cl-case arg
315 (0
316 (setq aw-ignore-on
317 (not aw-ignore-on))
318 (ace-select-window))
319 (4 (ace-swap-window))
320 (16 (ace-delete-window))
321 (t (ace-select-window))))
322
323 ;;* Utility
324 (defun aw-window< (wnd1 wnd2)
325 "Return true if WND1 is less than WND2.
326 This is determined by their respective window coordinates.
327 Windows are numbered top down, left to right."
328 (let ((f1 (window-frame wnd1))
329 (f2 (window-frame wnd2))
330 (e1 (window-edges wnd1))
331 (e2 (window-edges wnd2)))
332 (cond ((string< (frame-parameter f1 'window-id)
333 (frame-parameter f2 'window-id))
334 t)
335 ((< (car e1) (car e2))
336 t)
337 ((> (car e1) (car e2))
338 nil)
339 ((< (cadr e1) (cadr e2))
340 t))))
341
342 (defvar aw--window-ring (make-ring 10)
343 "Hold the window switching history.")
344
345 (defun aw--push-window (window)
346 "Store WINDOW to `aw--window-ring'."
347 (when (or (zerop (ring-length aw--window-ring))
348 (not (equal
349 (ring-ref aw--window-ring 0)
350 window)))
351 (ring-insert aw--window-ring (selected-window))))
352
353 (defun aw--pop-window ()
354 "Return the removed top of `aw--window-ring'."
355 (let (res)
356 (condition-case nil
357 (while (not (window-live-p
358 (setq res (ring-remove aw--window-ring 0)))))
359 (error
360 (error "No previous windows stored")))
361 res))
362
363 (defun aw-switch-to-window (window)
364 "Switch to the window WINDOW."
365 (let ((frame (window-frame window)))
366 (when (and (frame-live-p frame)
367 (not (eq frame (selected-frame))))
368 (select-frame-set-input-focus frame))
369 (if (window-live-p window)
370 (progn
371 (aw--push-window (selected-window))
372 (select-window window))
373 (error "Got a dead window %S" window))))
374
375 (defun aw-flip-window ()
376 "Switch to the window you were previously in."
377 (interactive)
378 (aw-switch-to-window (aw--pop-window)))
379
380 (defun aw-delete-window (window)
381 "Delete window WINDOW."
382 (let ((frame (window-frame window)))
383 (when (and (frame-live-p frame)
384 (not (eq frame (selected-frame))))
385 (select-frame-set-input-focus (window-frame window)))
386 (if (= 1 (length (window-list)))
387 (delete-frame frame)
388 (if (window-live-p window)
389 (delete-window window)
390 (error "Got a dead window %S" window)))))
391
392 (defun aw-swap-window (window)
393 "Swap buffers of current window and WINDOW."
394 (cl-labels ((swap-windows (window1 window2)
395 "Swap the buffers of WINDOW1 and WINDOW2."
396 (let ((buffer1 (window-buffer window1))
397 (buffer2 (window-buffer window2)))
398 (set-window-buffer window1 buffer2)
399 (set-window-buffer window2 buffer1)
400 (select-window window2))))
401 (let ((frame (window-frame window))
402 (this-window (selected-window)))
403 (when (and (frame-live-p frame)
404 (not (eq frame (selected-frame))))
405 (select-frame-set-input-focus (window-frame window)))
406 (when (and (window-live-p window)
407 (not (eq window this-window)))
408 (aw--push-window this-window)
409 (swap-windows this-window window)))))
410
411 (defun aw-offset (window)
412 "Return point in WINDOW that's closest to top left corner.
413 The point is writable, i.e. it's not part of space after newline."
414 (let ((h (window-hscroll window))
415 (beg (window-start window))
416 (end (window-end window))
417 (inhibit-field-text-motion t))
418 (with-current-buffer
419 (window-buffer window)
420 (save-excursion
421 (goto-char beg)
422 (while (and (< (point) end)
423 (< (- (line-end-position)
424 (line-beginning-position))
425 h))
426 (forward-line))
427 (+ (point) h)))))
428
429 ;;* Mode line
430 ;;;###autoload
431 (define-minor-mode ace-window-display-mode
432 "Minor mode for showing the ace window key in the mode line."
433 :global t
434 (if ace-window-display-mode
435 (progn
436 (aw-update)
437 (set-default
438 'mode-line-format
439 `((ace-window-display-mode
440 (:eval (window-parameter (selected-window) 'ace-window-path)))
441 ,@(assq-delete-all
442 'ace-window-display-mode
443 (default-value 'mode-line-format))))
444 (force-mode-line-update t)
445 (add-hook 'window-configuration-change-hook 'aw-update))
446 (set-default
447 'mode-line-format
448 (assq-delete-all
449 'ace-window-display-mode
450 (default-value 'mode-line-format)))
451 (remove-hook 'window-configuration-change-hook 'aw-update)))
452
453 (defun aw-update ()
454 "Update ace-window-path window parameter for all windows."
455 (avy-traverse
456 (avy-tree (aw-window-list) aw-keys)
457 (lambda (path leaf)
458 (set-window-parameter
459 leaf 'ace-window-path
460 (propertize
461 (apply #'string (reverse path))
462 'face 'aw-mode-line-face)))))
463
464 (provide 'ace-window)
465
466 ;;; ace-window.el ends here