(*:GNU:*:*):
[bpt/emacs.git] / lisp / msb.el
CommitLineData
b9a5a6af 1;;; msb.el --- Customizable buffer-selection with multiple menus.
b578f267 2
f8c25f1b 3;; Copyright (C) 1993, 1994, 1995 Lars Lindberg <Lars.Lindberg@sypro.cap.se>
b578f267 4
b9a5a6af
RS
5;; Author: Lars Lindberg <Lars.Lindberg@sypro.cap.se>
6;; Created: 8 Oct 1993
fd46fd17 7;; Lindberg's last update version: 3.31
b9a5a6af 8;; Keywords: mouse buffer menu
b578f267
EN
9
10;; This file is part of GNU Emacs.
11
12;; GNU Emacs is free software; you can redistribute it and/or modify
b9a5a6af 13;; it under the terms of the GNU General Public License as published by
b578f267
EN
14;; the Free Software Foundation; either version 2, or (at your option)
15;; any later version.
16
17;; GNU Emacs is distributed in the hope that it will be useful,
b9a5a6af
RS
18;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;; GNU General Public License for more details.
b578f267 21
b9a5a6af 22;; You should have received a copy of the GNU General Public License
b578f267
EN
23;; along with GNU Emacs; see the file COPYING. If not, write to the
24;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
25;; Boston, MA 02111-1307, USA.
b9a5a6af 26
b9a5a6af 27;;; Commentary:
b578f267 28
b9a5a6af
RS
29;; Purpose of this package:
30;; 1. Offer a function for letting the user choose buffer,
31;; not necessarily for switching to it.
32;; 2. Make a better mouse-buffer-menu.
33;;
34;; Installation:
1cc9a99e
RS
35
36;; 1. Byte compile msb first. It uses things in the cl package that
37;; are slow if not compiled, but blazingly fast when compiled. I
38;; have also had one report that said that msb malfunctioned when
39;; not compiled.
40;; 2. (require 'msb)
41;; Note! You now use msb instead of mouse-buffer-menu.
42;; 3. Now try the menu bar Buffers menu.
b9a5a6af
RS
43;;
44;; Customization:
2e6286be
RS
45;; Look at the variable `msb-menu-cond' for deciding what menus you
46;; want. It's not that hard to customize, despite my not-so-good
47;; doc-string. Feel free to send me a better doc-string.
b9a5a6af
RS
48;; There are some constants for you to try here:
49;; msb--few-menus
50;; msb--very-many-menus (default)
51;;
2e6286be
RS
52;; Look at the variable `msb-item-handling-function' for customization
53;; of the appearance of every menu item. Try for instance setting
54;; it to `msb-alon-item-handler'.
b9a5a6af 55;;
2e6286be
RS
56;; Look at the variable `msb-item-sort-function' for customization
57;; of sorting the menus. Set it to t for instance, which means no
b9a5a6af
RS
58;; sorting - you will get latest used buffer first.
59;;
2e6286be 60;; Also check out the variable `msb-display-invisible-buffers-p'.
b9a5a6af
RS
61
62;; Known bugs:
4aa4849b 63;; - Files-by-directory
fd46fd17
RS
64;; + No possibility to show client/changed buffers separately.
65;; + All file buffers only appear in in a file sub-menu, they will
66;; for instance not appear in the Mail sub-menu.
67
b9a5a6af 68;; Future enhancements:
b9a5a6af 69
b9a5a6af 70;;; Thanks goes to
fd46fd17
RS
71;; Mark Brader <msb@sq.com>
72;; Jim Berry <m1jhb00@FRB.GOV>
73;; Hans Chalupsky <hans@cs.Buffalo.EDU>
74;; Larry Rosenberg <ljr@ictv.com>
75;; Will Henney <will@astroscu.unam.mx>
76;; Jari Aalto <jaalto@tre.tele.nokia.fi>
77;; Michael Kifer <kifer@sbkifer.cs.sunysb.edu>
78;; Gael Marziou <gael@gnlab030.grenoble.hp.com>
79;; Dave Gillespie <daveg@thymus.synaptics.com>
80;; Alon Albert <alon@milcse.rtsg.mot.com>
81;; Kevin Broadey, <KevinB@bartley.demon.co.uk>
82;; Ake Stenhof <ake@cadpoint.se>
83;; Richard Stallman <rms@gnu.ai.mit.edu>
84;; Steve Fisk <fisk@medved.bowdoin.edu>
b9a5a6af
RS
85
86;;; Code:
87
88(require 'cl)
89
90;;;
2e6286be
RS
91;;; Some example constants to be used for `msb-menu-cond'. See that
92;;; variable for more information. Please note that if the condition
93;;; returns `multi', then the buffer can appear in several menus.
b9a5a6af
RS
94;;;
95(defconst msb--few-menus
96 '(((and (boundp 'server-buffer-clients)
97 server-buffer-clients
98 'multi)
99 3030
100 "Clients (%d)")
101 ((and msb-display-invisible-buffers-p
102 (msb-invisible-buffer-p)
103 'multi)
104 3090
105 "Invisible buffers (%d)")
106 ((eq major-mode 'dired-mode)
107 2010
108 "Dired (%d)"
109 msb-dired-item-handler
110 msb-sort-by-directory)
111 ((eq major-mode 'Man-mode)
112 4090
113 "Manuals (%d)")
114 ((eq major-mode 'w3-mode)
115 4020
116 "WWW (%d)")
fd46fd17 117 ((or (memq major-mode '(rmail-mode rmail-edit-mode vm-summary-mode vm-mode mail-mode))
b9a5a6af
RS
118 (memq major-mode '(mh-letter-mode
119 mh-show-mode
120 mh-folder-mode))
121 (memq major-mode '(gnus-summary-mode
122 news-reply-mode
123 gnus-group-mode
124 gnus-article-mode
125 gnus-kill-file-mode
126 gnus-browse-killed-mode)))
127 4010
128 "Mail (%d)")
129 ((not buffer-file-name)
130 4099
131 "Buffers (%d)")
132 ('no-multi
133 1099
134 "Files (%d)")))
135
136(defconst msb--very-many-menus
137 '(((and (boundp 'server-buffer-clients)
138 server-buffer-clients
139 'multi)
140 1010
141 "Clients (%d)")
142 ((and (boundp 'vc-mode) vc-mode 'multi)
143 1020
144 "Version Control (%d)")
145 ((and buffer-file-name
146 (buffer-modified-p)
147 'multi)
148 1030
149 "Changed files (%d)")
150 ((and (get-buffer-process (current-buffer))
151 'multi)
152 1040
153 "Processes (%d)")
154 ((and msb-display-invisible-buffers-p
155 (msb-invisible-buffer-p)
156 'multi)
157 1090
158 "Invisible buffers (%d)")
159 ((eq major-mode 'dired-mode)
160 2010
161 "Dired (%d)"
162 ;; Note this different menu-handler
163 msb-dired-item-handler
164 ;; Also note this item-sorter
165 msb-sort-by-directory)
166 ((eq major-mode 'Man-mode)
167 4030
168 "Manuals (%d)")
169 ((eq major-mode 'w3-mode)
170 4020
171 "WWW (%d)")
fd46fd17 172 ((or (memq major-mode '(rmail-mode rmail-edit-mode vm-summary-mode vm-mode mail-mode))
b9a5a6af
RS
173 (memq major-mode '(mh-letter-mode
174 mh-show-mode
175 mh-folder-mode))
176 (memq major-mode '(gnus-summary-mode
177 news-reply-mode
178 gnus-group-mode
179 gnus-article-mode
180 gnus-kill-file-mode
181 gnus-browse-killed-mode)))
182 4010
183 "Mail (%d)")
184 ;; Catchup for all non-file buffers
185 ((and (not buffer-file-name)
186 'no-multi)
187 4099
188 "Other non-file buffers (%d)")
189 ((and (string-match "/\\.[^/]*$" buffer-file-name)
190 'multi)
191 3090
192 "Hidden Files (%d)")
193 ((memq major-mode '(c-mode c++-mode))
194 3010
195 "C/C++ Files (%d)")
196 ((eq major-mode 'emacs-lisp-mode)
197 3020
198 "Elisp Files (%d)")
199 ((eq major-mode 'latex-mode)
200 3030
201 "LaTex Files (%d)")
202 ('no-multi
203 3099
204 "Other files (%d)")))
205
206;; msb--many-menus is obsolete
207(defvar msb--many-menus msb--very-many-menus)
208
209;;;
210;;; Customizable variables
211;;;
212
213(defvar msb-separator-diff 100
214 "*Non-nil means use separators.
215The separators will appear between all menus that have a sorting key that differs by this value or more.")
216
217(defvar msb-files-by-directory-sort-key 0
218 "*The sort key for files sorted by directory")
219
4aa4849b 220(defvar msb-max-menu-items 15
b9a5a6af 221 "*The maximum number of items in a menu.
4aa4849b 222If this variable is set to 15 for instance, then the submenu will be split up in minor parts, 15 items each.
b9a5a6af
RS
223Nil means no limit.")
224
225(defvar msb-max-file-menu-items 10
226 "*The maximum number of items from different directories.
227
2e6286be 228When the menu is of type `file by directory', this is the maximum
6331da4b 229number of buffers that are clumped together from different
b9a5a6af
RS
230directories.
231
4aa4849b
RS
232Set this to 1 if you want one menu per directory instead of clumping
233them together.
234
b9a5a6af
RS
235If the value is not a number, then the value 10 is used.")
236
237(defvar msb-most-recently-used-sort-key -1010
238 "*Where should the menu with the most recently used buffers be placed?")
239
4aa4849b 240(defvar msb-display-most-recently-used 15
b9a5a6af 241 "*How many buffers should be in the most-recently-used menu.
4aa4849b 242 No buffers at all if less than 1 or nil (or any non-number).")
b9a5a6af
RS
243
244(defvar msb-most-recently-used-title "Most recently used (%d)"
245 "*The title for the most-recently-used menu.")
246
247(defvar msb-horizontal-shift-function '(lambda () 0)
248 "*Function that specifies a number of pixels by which the top menu should
249be shifted leftwards.")
250
251(defvar msb-display-invisible-buffers-p nil
252 "*Show invisible buffers or not.
253Non-nil means that the buffer menu should include buffers that have
254names that starts with a space character.")
255
256(defvar msb-item-handling-function 'msb-item-handler
257 "*The appearance of a buffer menu.
258
259The default function to call for handling the appearance of a menu
2e6286be 260item. It should take to arguments, BUFFER and MAX-BUFFER-NAME-LENGTH,
b9a5a6af 261where the latter is the max length of all buffer names.
4aa4849b
RS
262
263The function should return the string to use in the menu.
264
b9a5a6af 265When the function is called, BUFFER is the current buffer.
2e6286be
RS
266This function is called for items in the variable `msb-menu-cond' that
267have nil as ITEM-HANDLING-FUNCTION. See `msb-menu-cond' for more
b9a5a6af
RS
268information.")
269
270(defvar msb-item-sort-function 'msb-sort-by-name
271 "*The order of items in a buffer menu.
272The default function to call for handling the order of items in a menu
2e6286be 273item. This function is called like a sort function. The items
b9a5a6af
RS
274look like (ITEM-NAME . BUFFER).
275ITEM-NAME is the name of the item that will appear in the menu.
276BUFFER is the buffer, this is not necessarily the current buffer.
277
278Set this to nil or t if you don't want any sorting (faster).")
279
280(defvar msb-files-by-directory nil
281 "*Non-nil means that files should be sorted by directory instead of
282the groups in msb-menu-cond.")
283
284(defvar msb-menu-cond msb--very-many-menus
6331da4b 285 "*List of criteria for splitting the mouse buffer menu.
b9a5a6af
RS
286The elements in the list should be of this type:
287 (CONDITION MENU-SORT-KEY MENU-TITLE ITEM-HANDLING-FN ITEM-SORT-FN).
288
289When making the split, the buffers are tested one by one against the
290CONDITION, just like a lisp cond: When hitting a true condition, the
6331da4b 291other criteria are *not* tested and the buffer name will appear in
b9a5a6af
RS
292the menu with the menu-title corresponding to the true condition.
293
2e6286be 294If the condition returns the symbol `multi', then the buffer will be
b9a5a6af 295added to this menu *and* tested for other menus too. If it returns
2e6286be 296`no-multi', then the buffer will only be added if it hasn't been added
b9a5a6af
RS
297to any other menu.
298
299During this test, the buffer in question is the current buffer, and
300the test is surrounded by calls to `save-excursion' and
2e6286be 301`save-match-data'.
b9a5a6af 302
2e6286be
RS
303The categories are sorted by MENU-SORT-KEY. Smaller keys are on
304top. nil means don't display this menu.
b9a5a6af 305
2e6286be 306MENU-TITLE is really a format. If you add %d in it, the %d is replaced
b9a5a6af
RS
307with the number of items in that menu.
308
2e6286be 309ITEM-HANDLING-FN, is optional. If it is supplied and is a
b9a5a6af
RS
310function, than it is used for displaying the items in that particular
311buffer menu, otherwise the function pointed out by
2e6286be 312`msb-item-handling-function' is used.
b9a5a6af
RS
313
314ITEM-SORT-FN, is also optional.
315If it is not supplied, the function pointed out by
2e6286be 316`msb-item-sort-function' is used.
b9a5a6af
RS
317If it is nil, then no sort takes place and the buffers are presented
318in least-recently-used order.
319If it is t, then no sort takes place and the buffers are presented in
320most-recently-used order.
321If it is supplied and non-nil and not t than it is used for sorting
322the items in that particular buffer menu.
323
2e6286be
RS
324Note1: There should always be a `catch-all' as last element,
325in this list. That is an element like (t TITLE ITEM-HANDLING-FUNCTION).
b9a5a6af
RS
326Note2: A buffer menu appears only if it has at least one buffer in it.
327Note3: If you have a CONDITION that can't be evaluated you will get an
328error every time you do \\[msb].")
329
330(defvar msb-after-load-hooks nil
331 "Hooks to be run after the msb package has been loaded.")
332
333;;;
334;;; Internal variables
335;;;
336
337;; The last calculated menu.
338(defvar msb--last-buffer-menu nil)
339
340;; If this is non-nil, then it is a string that describes the error.
341(defvar msb--error nil)
342
343;;;
4aa4849b 344;;; Some example function to be used for `msb-item-handling-function'.
b9a5a6af
RS
345;;;
346(defun msb-item-handler (buffer &optional maxbuf)
347 "Create one string item, concerning BUFFER, for the buffer menu.
348The item looks like:
349*% <buffer-name>
2e6286be
RS
350The `*' appears only if the buffer is marked as modified.
351The `%' appears only if the buffer is read-only.
b9a5a6af
RS
352Optional second argument MAXBUF is completely ignored."
353 (let ((name (buffer-name))
354 (modified (if (buffer-modified-p) "*" " "))
355 (read-only (if buffer-read-only "%" " ")))
356 (format "%s%s %s" modified read-only name)))
357
358
359(eval-when-compile (require 'dired))
360
2e6286be
RS
361;; `dired' can be called with a list of the form (directory file1 file2 ...)
362;; which causes `dired-directory' to be in the same form.
b9a5a6af
RS
363(defun msb--dired-directory ()
364 (cond ((stringp dired-directory)
365 (abbreviate-file-name (expand-file-name dired-directory)))
366 ((consp dired-directory)
367 (abbreviate-file-name (expand-file-name (car dired-directory))))
368 (t
2e6286be 369 (error "Unknown type of `dired-directory' in buffer %s"
b9a5a6af
RS
370 (buffer-name)))))
371
372(defun msb-dired-item-handler (buffer &optional maxbuf)
373 "Create one string item, concerning a dired BUFFER, for the buffer menu.
374The item looks like:
375*% <buffer-name>
2e6286be
RS
376The `*' appears only if the buffer is marked as modified.
377The `%' appears only if the buffer is read-only.
b9a5a6af
RS
378Optional second argument MAXBUF is completely ignored."
379 (let ((name (msb--dired-directory))
380 (modified (if (buffer-modified-p) "*" " "))
381 (read-only (if buffer-read-only "%" " ")))
382 (format "%s%s %s" modified read-only name)))
383
384(defun msb-alon-item-handler (buffer maxbuf)
385 "Create one string item for the buffer menu.
386The item looks like:
387<buffer-name> *%# <file-name>
2e6286be
RS
388The `*' appears only if the buffer is marked as modified.
389The `%' appears only if the buffer is read-only.
390The `#' appears only version control file (SCCS/RCS)."
b9a5a6af
RS
391 (format (format "%%%ds %%s%%s%%s %%s" maxbuf)
392 (buffer-name buffer)
393 (if (buffer-modified-p) "*" " ")
394 (if buffer-read-only "%" " ")
395 (if (and (boundp 'vc-mode) vc-mode) "#" " ")
396 (or buffer-file-name "")))
397
398;;;
4aa4849b 399;;; Some example function to be used for `msb-item-sort-function'.
b9a5a6af
RS
400;;;
401(defun msb-sort-by-name (item1 item2)
402 "Sorts the items depending on their buffer-name
403An item look like (NAME . BUFFER)."
404 (string-lessp (buffer-name (cdr item1))
405 (buffer-name (cdr item2))))
406
407
408(defun msb-sort-by-directory (item1 item2)
2e6286be 409 "Sorts the items depending on their directory. Made for dired.
b9a5a6af
RS
410An item look like (NAME . BUFFER)."
411 (string-lessp (save-excursion (set-buffer (cdr item1)) (msb--dired-directory))
412 (save-excursion (set-buffer (cdr item2)) (msb--dired-directory))))
413
414;;;
415;;; msb
416;;;
417;;; This function can be used instead of (mouse-buffer-menu EVENT)
418;;; function in "mouse.el".
419;;;
420(defun msb (event)
421 "Pop up several menus of buffers for selection with the mouse.
422This command switches buffers in the window that you clicked on, and
423selects that window.
424
2e6286be
RS
425See the function `mouse-select-buffer' and the variable
426`msb-menu-cond' for more information about how the menus are split."
b9a5a6af 427 (interactive "e")
fd46fd17 428 (let ((old-window (selected-window))
b9a5a6af 429 (window (posn-window (event-start event))))
fd46fd17
RS
430 (unless (framep window) (select-window window))
431 (let ((buffer (mouse-select-buffer event)))
432 (if buffer
433 (switch-to-buffer buffer)
434 (select-window old-window))))
b9a5a6af
RS
435 nil)
436
437;;;
438;;; Some supportive functions
439;;;
440(defun msb-invisible-buffer-p (&optional buffer)
441 "Return t if optional BUFFER is an \"invisible\" buffer.
442If the argument is left out or nil, then the current buffer is considered."
443 (and (> (length (buffer-name buffer)) 0)
444 (eq ?\ (aref (buffer-name buffer) 0))))
445
6331da4b 446;; Strip one hierarchy level from the end of PATH.
b9a5a6af
RS
447(defun msb--strip-path (path)
448 (save-match-data
449 (if (string-match "\\(.+\\)/[^/]+$" path)
450 (substring path (match-beginning 1) (match-end 1))
451 "/")))
452
453;; Create an alist with all buffers from LIST that lies under the same
454;; directory will be in the same item as the directory string as
2e6286be 455;; ((PATH1 . (BUFFER-1 BUFFER-2 ...)) (PATH2 . (BUFFER-K BUFFER-K+1...)) ...)
b9a5a6af
RS
456(defun msb--init-file-alist (list)
457 (let ((buffer-alist
458 (sort (mapcan
459 (function
460 (lambda (buffer)
461 (let ((file-name (buffer-file-name buffer)))
462 (when file-name
463 (list (cons (msb--strip-path file-name) buffer))))))
464 list)
465 (function (lambda (item1 item2)
466 (string< (car item1) (car item2)))))))
467 ;; Make alist that looks like
2e6286be 468 ;; ((PATH1 . (BUFFER-1 BUFFER-2 ...)) (PATH2 . (BUFFER-K)) ...)
b9a5a6af
RS
469 (let ((path nil)
470 (buffers nil)
471 (result nil))
472 (append
473 (mapcan (function
474 (lambda (item)
475 (cond
476 ((and path
b9a5a6af
RS
477 (string= path (car item)))
478 (push (cdr item) buffers)
479 nil)
480 (t
481 (when path
482 (setq result (cons path buffers)))
483 (setq path (car item))
484 (setq buffers (list (cdr item)))
485 (and result (list result))))))
486 buffer-alist)
487 (list (cons path buffers))))))
488
489;; Choose file-menu with respect to directory for every buffer in LIST.
490(defun msb--choose-file-menu (list)
491 (let ((buffer-alist (msb--init-file-alist list))
492 (final-list nil)
493 (max-clumped-together (if (numberp msb-max-file-menu-items)
494 msb-max-file-menu-items
495 10))
496 (top-found-p nil)
497 (last-path nil)
498 first rest path buffers)
499 (setq first (car buffer-alist))
500 (setq rest (cdr buffer-alist))
501 (setq path (car first))
502 (setq buffers (cdr first))
503 (while rest
504 (let ((found-p nil)
505 (tmp-rest rest)
506 new-path item)
507 (setq item (car tmp-rest))
508 (while (and tmp-rest
509 (<= (length buffers) max-clumped-together)
510 (>= (length (car item)) (length path))
511 (string= path (substring (car item) 0 (length path))))
512 (setq found-p t)
513 (setq buffers (append buffers (cdr item)))
514 (setq tmp-rest (cdr tmp-rest))
515 (setq item (car tmp-rest)))
516 (cond
517 ((> (length buffers) max-clumped-together)
518 (setq last-path (car first))
4aa4849b
RS
519 (setq first
520 (cons (format (if top-found-p
521 "%s/... (%d)"
522 "%s (%d)")
523 (car first)
524 (length (cdr first)))
525 (cdr first)))
526 (setq top-found-p nil)
b9a5a6af
RS
527 (push first final-list)
528 (setq first (car rest)
529 rest (cdr rest))
530 (setq path (car first)
531 buffers (cdr first)))
532 (t
533 (when found-p
534 (setq top-found-p t)
535 (setq first (cons path buffers)
536 rest tmp-rest))
537 (setq path (msb--strip-path path)
538 buffers (cdr first))
539 (when (and last-path
540 (or (and (>= (length path) (length last-path))
541 (string= last-path
542 (substring path 0 (length last-path))))
543 (and (< (length path) (length last-path))
544 (string= path
545 (substring last-path 0 (length path))))))
546
4aa4849b
RS
547 (setq first
548 (cons (format (if top-found-p
549 "%s/... (%d)"
550 "%s (%d)")
551 (car first)
552 (length (cdr first)))
553 (cdr first)))
554 (setq top-found-p nil)
b9a5a6af
RS
555 (push first final-list)
556 (setq first (car rest)
557 rest (cdr rest))
558 (setq path (car first)
559 buffers (cdr first)))))))
4aa4849b
RS
560 (setq first
561 (cons (format (if top-found-p
562 "%s/... (%d)"
563 "%s (%d)")
564 (car first)
565 (length (cdr first)))
566 (cdr first)))
567 (setq top-found-p nil)
b9a5a6af
RS
568 (push first final-list)
569 (nreverse final-list)))
570
571;; Create a vector as:
572;; [BUFFER-LIST-VARIABLE CONDITION MENU-SORT-KEY MENU-TITLE ITEM-HANDLER SORTER)
2e6286be
RS
573;; from an element in `msb-menu-cond'. See that variable for a
574;; description of its elements.
b9a5a6af
RS
575(defun msb--create-function-info (menu-cond-elt)
576 (let* ((list-symbol (make-symbol "-msb-buffer-list"))
577 (tmp-ih (and (> (length menu-cond-elt) 3)
578 (nth 3 menu-cond-elt)))
579 (item-handler (if (and tmp-ih (fboundp tmp-ih))
580 tmp-ih
581 msb-item-handling-function))
582 (tmp-s (if (> (length menu-cond-elt) 4)
583 (nth 4 menu-cond-elt)
584 msb-item-sort-function))
585 (sorter (if (or (fboundp tmp-s)
586 (null tmp-s)
2e6286be 587 (eq tmp-s t))
b9a5a6af
RS
588 tmp-s
589 msb-item-sort-function)))
590 (when (< (length menu-cond-elt) 3)
591 (error "Wrong format of msb-menu-cond."))
592 (when (and (> (length menu-cond-elt) 3)
593 (not (fboundp tmp-ih)))
594 (signal 'invalid-function (list tmp-ih)))
595 (when (and (> (length menu-cond-elt) 4)
596 tmp-s
597 (not (fboundp tmp-s))
2e6286be 598 (not (eq tmp-s t)))
b9a5a6af 599 (signal 'invalid-function (list tmp-s)))
2e6286be 600 (set list-symbol ())
b9a5a6af
RS
601 (vector list-symbol ;BUFFER-LIST-VARIABLE
602 (nth 0 menu-cond-elt) ;CONDITION
603 (nth 1 menu-cond-elt) ;SORT-KEY
604 (nth 2 menu-cond-elt) ;MENU-TITLE
605 item-handler ;ITEM-HANDLER
606 sorter) ;SORTER
607 ))
608
609;; This defsubst is only used in `msb--choose-menu' below. It was
2e6286be 610;; pulled out merely to make the code somewhat clearer. The indention
b9a5a6af
RS
611;; level was too big.
612(defsubst msb--collect (function-info-vector)
613 (let ((result nil)
614 (multi-flag nil)
615 function-info-list)
616 (setq function-info-list
617 (loop for fi
618 across function-info-vector
619 if (and (setq result
620 (eval (aref fi 1))) ;Test CONDITION
621 (not (and (eq result 'no-multi)
622 multi-flag))
623 (progn (when (eq result 'multi)
624 (setq multi-flag t))
4aa4849b 625 t))
b9a5a6af
RS
626 collect fi
627 until (and result
628 (not (eq result 'multi)))))
629 (when (and (not function-info-list)
630 (not result))
631 (error "No catch-all in msb-menu-cond!"))
632 function-info-list))
633
634;; Adds BUFFER to the menu depicted by FUNCTION-INFO
2e6286be 635;; All side-effects. Adds an element of form (BUFFER-TITLE . BUFFER)
b9a5a6af
RS
636;; to the buffer-list variable in function-info.
637(defun msb--add-to-menu (buffer function-info max-buffer-name-length)
638 (let ((list-symbol (aref function-info 0))) ;BUFFER-LIST-VARIABLE
639 ;; Here comes the hairy side-effect!
640 (set list-symbol
641 (cons (cons (funcall (aref function-info 4) ;ITEM-HANDLER
642 buffer
643 max-buffer-name-length)
644 buffer)
645 (eval list-symbol)))))
646
647;; Selects the appropriate menu for BUFFER.
648;; This is all side-effects, folks!
649;; This should be optimized.
650(defsubst msb--choose-menu (buffer function-info-vector max-buffer-name-length)
651 (unless (and (not msb-display-invisible-buffers-p)
652 (msb-invisible-buffer-p buffer))
653 (condition-case nil
654 (save-excursion
655 (set-buffer buffer)
2e6286be 656 ;; Menu found. Add to this menu
b9a5a6af
RS
657 (mapc (function
658 (lambda (function-info)
659 (msb--add-to-menu buffer function-info max-buffer-name-length)))
660 (msb--collect function-info-vector)))
661 (error (unless msb--error
662 (setq msb--error
663 (format
2e6286be 664 "In msb-menu-cond, error for buffer `%s'."
b9a5a6af 665 (buffer-name buffer)))
76e4c0ba 666 (error "%s" msb--error))))))
b9a5a6af
RS
667
668;; Return (SORT-KEY TITLE . BUFFER-LIST) or nil if the
669;; buffer-list is empty.
670(defun msb--create-sort-item (function-info)
671 (let ((buffer-list (eval (aref function-info 0))))
672 (when buffer-list
673 (let ((sorter (aref function-info 5)) ;SORTER
674 (sort-key (aref function-info 2))) ;MENU-SORT-KEY
675 (when sort-key
676 (cons sort-key
677 (cons (format (aref function-info 3) ;MENU-TITLE
678 (length buffer-list))
679 (cond
680 ((null sorter)
681 buffer-list)
2e6286be 682 ((eq sorter t)
b9a5a6af
RS
683 (nreverse buffer-list))
684 (t
685 (sort buffer-list sorter))))))))))
686
687;; Returns a list on the form ((TITLE . BUFFER-LIST)) for
688;; the most recently used buffers.
689(defun msb--most-recently-used-menu (max-buffer-name-length)
4aa4849b
RS
690 (when (and (numberp msb-display-most-recently-used)
691 (> msb-display-most-recently-used 0))
fd46fd17
RS
692 (let* ((buffers (cdr (buffer-list)))
693 (most-recently-used
b9a5a6af 694 (loop with n = 0
fd46fd17 695 for buffer in buffers
b9a5a6af
RS
696 if (save-excursion
697 (set-buffer buffer)
698 (and (not (msb-invisible-buffer-p))
699 (not (eq major-mode 'dired-mode))))
700 collect (save-excursion
701 (set-buffer buffer)
702 (cons (funcall msb-item-handling-function
703 buffer
704 max-buffer-name-length)
705 buffer))
706 and do (incf n)
4aa4849b 707 until (>= n msb-display-most-recently-used))))
b9a5a6af
RS
708 (cons (if (stringp msb-most-recently-used-title)
709 (format msb-most-recently-used-title
710 (length most-recently-used))
711 (signal 'wrong-type-argument (list msb-most-recently-used-title)))
712 most-recently-used))))
713
714(defun msb--create-buffer-menu-2 ()
715 (let ((max-buffer-name-length 0)
716 file-buffers
717 function-info-vector)
718 ;; Calculate the longest buffer name.
719 (mapc
720 (function
721 (lambda (buffer)
722 (if (or msb-display-invisible-buffers-p
723 (not (msb-invisible-buffer-p)))
724 (setq max-buffer-name-length
725 (max max-buffer-name-length
726 (length (buffer-name buffer)))))))
727 (buffer-list))
728 ;; Make a list with elements of type
729 ;; (BUFFER-LIST-VARIABLE
730 ;; CONDITION
731 ;; MENU-SORT-KEY
732 ;; MENU-TITLE
733 ;; ITEM-HANDLER
734 ;; SORTER)
735 ;; Uses "function-global" variables:
736 ;; function-info-vector
737 (setq function-info-vector
738 (apply (function vector)
739 (mapcar (function msb--create-function-info)
740 msb-menu-cond)))
741 ;; Split the buffer-list into several lists; one list for each
2e6286be 742 ;; criteria. This is the most critical part with respect to time.
b9a5a6af
RS
743 (mapc (function (lambda (buffer)
744 (cond ((and msb-files-by-directory
745 (buffer-file-name buffer))
746 (push buffer file-buffers))
747 (t
748 (msb--choose-menu buffer
749 function-info-vector
750 max-buffer-name-length)))))
751 (buffer-list))
752 (when file-buffers
753 (setq file-buffers
754 (mapcar (function
755 (lambda (buffer-list)
756 (cons msb-files-by-directory-sort-key
757 (cons (car buffer-list)
758 (sort
759 (mapcar (function
760 (lambda (buffer)
4aa4849b
RS
761 (cons (save-excursion
762 (set-buffer buffer)
763 (funcall msb-item-handling-function
764 buffer
765 max-buffer-name-length))
b9a5a6af
RS
766 buffer)))
767 (cdr buffer-list))
768 (function
769 (lambda (item1 item2)
770 (string< (car item1) (car item2)))))))))
771 (msb--choose-file-menu file-buffers))))
772 ;; Now make the menu - a list of (TITLE . BUFFER-LIST)
4aa4849b 773 (let* (menu
b9a5a6af
RS
774 (most-recently-used
775 (msb--most-recently-used-menu max-buffer-name-length))
776 (others (append file-buffers
777 (loop for elt
4aa4849b
RS
778 across function-info-vector
779 for value = (msb--create-sort-item elt)
780 if value collect value))))
b9a5a6af
RS
781 (setq menu
782 (mapcar 'cdr ;Remove the SORT-KEY
783 ;; Sort the menus - not the items.
784 (msb--add-separators
785 (sort
786 ;; Get a list of (SORT-KEY TITLE . BUFFER-LIST)
787 ;; Also sorts the items within the menus.
788 (if (cdr most-recently-used)
789 (cons
790 ;; Add most recent used buffers
791 (cons msb-most-recently-used-sort-key
792 most-recently-used)
793 others)
794 others)
795 (function (lambda (elt1 elt2)
796 (< (car elt1) (car elt2))))))))
797 ;; Now make it a keymap menu
798 (append
799 '(keymap "Select Buffer")
800 (msb--make-keymap-menu menu)
801 (when msb-separator-diff
802 (list (list 'separator "---")))
803 (list (cons 'toggle
804 (cons
805 (if msb-files-by-directory
806 "*Files by type*"
807 "*Files by directory*")
808 'msb--toggle-menu-type)))))))
809
810(defun msb--create-buffer-menu ()
811 (save-match-data
812 (save-excursion
813 (msb--create-buffer-menu-2))))
814
815;;;
816;;; Multi purpose function for selecting a buffer with the mouse.
817;;;
818(defun msb--toggle-menu-type ()
819 (interactive)
820 (setq msb-files-by-directory (not msb-files-by-directory))
505ce248 821 (menu-bar-update-buffers t))
b9a5a6af
RS
822
823(defun mouse-select-buffer (event)
824 "Pop up several menus of buffers, for selection with the mouse.
825Returns the selected buffer or nil if no buffer is selected.
826
4aa4849b 827The way the buffers are split is conveniently handled with the
2e6286be 828variable `msb-menu-cond'."
b9a5a6af
RS
829 ;; Popup the menu and return the selected buffer.
830 (when (or msb--error
831 (not msb--last-buffer-menu)
832 (not (fboundp 'frame-or-buffer-changed-p))
833 (frame-or-buffer-changed-p))
834 (setq msb--error nil)
835 (setq msb--last-buffer-menu (msb--create-buffer-menu)))
4aa4849b
RS
836 (let ((position event)
837 choice)
b9a5a6af
RS
838 (when (and (fboundp 'posn-x-y)
839 (fboundp 'posn-window))
840 (let ((posX (car (posn-x-y (event-start event))))
841 (posY (cdr (posn-x-y (event-start event))))
4aa4849b 842 (posWind (posn-window (event-start event))))
b9a5a6af
RS
843 ;; adjust position
844 (setq posX (- posX (funcall msb-horizontal-shift-function))
845 position (list (list posX posY) posWind))))
1cc9a99e 846 ;; This `sit-for' magically makes the menu stay up if the mouse
6331da4b 847 ;; button is released within 0.1 second.
1cc9a99e
RS
848 (sit-for 0 100)
849 ;; Popup the menu
4aa4849b 850 (setq choice (x-popup-menu position msb--last-buffer-menu))
b9a5a6af 851 (cond
4aa4849b
RS
852 ((eq (car choice) 'toggle)
853 ;; Bring up the menu again with type toggled.
854 (msb--toggle-menu-type)
855 (mouse-select-buffer event))
856 ((and (numberp (car choice))
857 (null (cdr choice)))
858 (let ((msb--last-buffer-menu (nthcdr 3 (assq (car choice) msb--last-buffer-menu))))
b9a5a6af 859 (mouse-select-buffer event)))
4aa4849b
RS
860 ((while (numberp (car choice))
861 (setq choice (cdr choice))))
862 ((and (stringp (car choice))
863 (null (cdr choice)))
864 (car choice))
dc3247b3
RS
865 ((null choice)
866 choice)
4aa4849b
RS
867 (t
868 (error "Unknown form for buffer: %s" choice)))))
b9a5a6af
RS
869
870;; Add separators
871(defun msb--add-separators (sorted-list)
872 (cond
873 ((or (not msb-separator-diff)
874 (not (numberp msb-separator-diff)))
875 sorted-list)
876 (t
877 (let ((last-key nil))
878 (mapcan
879 (function
880 (lambda (item)
881 (cond
882 ((and msb-separator-diff
883 last-key
884 (> (- (car item) last-key)
885 msb-separator-diff))
886 (setq last-key (car item))
887 (list (cons last-key 'separator)
888 item))
889 (t
890 (setq last-key (car item))
891 (list item)))))
892 sorted-list)))))
893
4aa4849b
RS
894(defun msb--split-menus-2 (list mcount result)
895 (cond
896 ((> (length list) msb-max-menu-items)
897 (let ((count 0)
898 sub-name
899 (tmp-list nil))
900 (while (< count msb-max-menu-items)
901 (push (pop list) tmp-list)
902 (incf count))
903 (setq tmp-list (nreverse tmp-list))
904 (setq sub-name (concat (car (car tmp-list)) "..."))
905 (push (append (list mcount sub-name
906 'keymap sub-name)
907 tmp-list)
908 result))
909 (msb--split-menus-2 list (1+ mcount) result))
910 ((null result)
911 list)
912 (t
913 (let (sub-name)
914 (setq sub-name (concat (car (car list)) "..."))
915 (push (append (list mcount sub-name
916 'keymap sub-name)
917 list)
918 result))
919 (nreverse result))))
920
921(defun msb--split-menus (list)
922 (msb--split-menus-2 list 0 nil))
923
924
b9a5a6af
RS
925(defun msb--make-keymap-menu (raw-menu)
926 (let ((end (cons '(nil) 'menu-bar-select-buffer))
927 (mcount 0))
928 (mapcar
929 (function
930 (lambda (sub-menu)
931 (cond
932 ((eq 'separator sub-menu)
933 (list 'separator "---"))
934 (t
4aa4849b
RS
935 (let ((buffers (mapcar (function
936 (lambda (item)
937 (let ((string (car item))
938 (buffer (cdr item)))
939 (cons (buffer-name buffer)
940 (cons string end)))))
941 (cdr sub-menu))))
942 (append (list (incf mcount) (car sub-menu)
943 'keymap (car sub-menu))
944 (msb--split-menus buffers)))))))
b9a5a6af
RS
945 raw-menu)))
946
947(defun menu-bar-update-buffers (&optional arg)
948 ;; If user discards the Buffers item, play along.
949 (when (and (lookup-key (current-global-map) [menu-bar buffer])
950 (or (not (fboundp 'frame-or-buffer-changed-p))
951 (frame-or-buffer-changed-p)
952 arg))
fd46fd17 953 (let ((frames (frame-list))
b9a5a6af 954 buffers-menu frames-menu)
b9a5a6af
RS
955 ;; Make the menu of buffers proper.
956 (setq msb--last-buffer-menu (msb--create-buffer-menu))
957 (setq buffers-menu msb--last-buffer-menu)
958 ;; Make a Frames menu if we have more than one frame.
fd46fd17
RS
959 (when (cdr frames)
960 (let* ((frame-length (length frames))
961 (f-title (format "Frames (%d)" frame-length)))
962 ;; List only the N most recently selected frames
963 (when (and (integerp msb-max-menu-items)
964 (> msb-max-menu-items 1)
965 (> frame-length msb-max-menu-items))
966 (setcdr (nthcdr msb-max-menu-items frames) nil))
b9a5a6af 967 (setq frames-menu
fd46fd17
RS
968 (nconc
969 (list 'frame f-title '(nil) 'keymap f-title)
970 (mapcar
971 (function
972 (lambda (frame)
973 (nconc
974 (list frame
975 (cdr (assq 'name
976 (frame-parameters frame)))
977 (cons nil nil))
978 'menu-bar-select-frame)))
979 frames)))))
b9a5a6af
RS
980 (define-key (current-global-map) [menu-bar buffer]
981 (cons "Buffers"
982 (if (and buffers-menu frames-menu)
fd46fd17
RS
983 ;; Combine Frame and Buffers menus with separator between
984 (nconc (list 'keymap "Buffers and Frames" frames-menu
985 (and msb-separator-diff '(separator "---")))
986 (cddr buffers-menu))
987 (or buffers-menu 'undefined)))))))
b9a5a6af
RS
988
989(when (and (boundp 'menu-bar-update-hook)
990 (not (fboundp 'frame-or-buffer-changed-p)))
991 (defvar msb--buffer-count 0)
992 (defun frame-or-buffer-changed-p ()
993 (let ((count (length (buffer-list))))
994 (when (/= count msb--buffer-count)
995 (setq msb--buffer-count count)
996 t))))
997
998(unless (or (not (boundp 'menu-bar-update-hook))
999 (memq 'menu-bar-update-buffers menu-bar-update-hook))
1000 (add-hook 'menu-bar-update-hook 'menu-bar-update-buffers))
1001
1002(and (fboundp 'mouse-buffer-menu)
1003 (substitute-key-definition 'mouse-buffer-menu 'msb (current-global-map)))
1004
1005(provide 'msb)
1006(eval-after-load 'msb (run-hooks 'msb-after-load-hooks))
1007;;; msb.el ends here