Added speedbar support for RMAIL files.
[bpt/emacs.git] / lisp / msb.el
CommitLineData
b9a5a6af 1;;; msb.el --- Customizable buffer-selection with multiple menus.
b578f267 2
7612d61a 3;; Copyright (C) 1993, 1994, 1995, 1997 Free Software Foundation, Inc.
b578f267 4
0eb3b336 5;; Author: Lars Lindberg <Lars.G.Lindberg@capgemini.se>
b9a5a6af 6;; Created: 8 Oct 1993
0eb3b336
RS
7;; Lindberg's last update version: 3.33
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)
0eb3b336 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'.
0eb3b336 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
0eb3b336 120 mh-folder-mode))
b9a5a6af
RS
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
0eb3b336 158 "Invisible buffers (%d)")
b9a5a6af
RS
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
0eb3b336 175 mh-folder-mode))
b9a5a6af
RS
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.
0eb3b336
RS
215The separators will appear between all menus that have a sorting key
216that differs by this value or more.")
b9a5a6af
RS
217
218(defvar msb-files-by-directory-sort-key 0
0eb3b336 219 "*The sort key for files sorted by directory.")
b9a5a6af 220
4aa4849b 221(defvar msb-max-menu-items 15
b9a5a6af 222 "*The maximum number of items in a menu.
0eb3b336
RS
223If this variable is set to 15 for instance, then the submenu will be
224split up in minor parts, 15 items each. If nil, there is no limit.")
b9a5a6af
RS
225
226(defvar msb-max-file-menu-items 10
227 "*The maximum number of items from different directories.
228
2e6286be 229When the menu is of type `file by directory', this is the maximum
6331da4b 230number of buffers that are clumped together from different
b9a5a6af
RS
231directories.
232
4aa4849b
RS
233Set this to 1 if you want one menu per directory instead of clumping
234them together.
235
b9a5a6af
RS
236If the value is not a number, then the value 10 is used.")
237
238(defvar msb-most-recently-used-sort-key -1010
239 "*Where should the menu with the most recently used buffers be placed?")
240
4aa4849b 241(defvar msb-display-most-recently-used 15
b9a5a6af 242 "*How many buffers should be in the most-recently-used menu.
4aa4849b 243 No buffers at all if less than 1 or nil (or any non-number).")
b9a5a6af
RS
244
245(defvar msb-most-recently-used-title "Most recently used (%d)"
246 "*The title for the most-recently-used menu.")
0eb3b336 247
b9a5a6af 248(defvar msb-horizontal-shift-function '(lambda () 0)
0eb3b336 249 "*Function that specifies how many pixels to shift the top menu leftwards.")
b9a5a6af
RS
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
0eb3b336
RS
265When the function is called, BUFFER is the current buffer. This
266function is called for items in the variable `msb-menu-cond' that have
267nil 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.
0eb3b336 272
b9a5a6af 273The default function to call for handling the order of items in a menu
0eb3b336
RS
274item. This function is called like a sort function. The items look
275like (ITEM-NAME . BUFFER).
276
b9a5a6af
RS
277ITEM-NAME is the name of the item that will appear in the menu.
278BUFFER is the buffer, this is not necessarily the current buffer.
279
280Set this to nil or t if you don't want any sorting (faster).")
281
282(defvar msb-files-by-directory nil
283 "*Non-nil means that files should be sorted by directory instead of
284the groups in msb-menu-cond.")
285
286(defvar msb-menu-cond msb--very-many-menus
6331da4b 287 "*List of criteria for splitting the mouse buffer menu.
b9a5a6af
RS
288The elements in the list should be of this type:
289 (CONDITION MENU-SORT-KEY MENU-TITLE ITEM-HANDLING-FN ITEM-SORT-FN).
290
291When making the split, the buffers are tested one by one against the
292CONDITION, just like a lisp cond: When hitting a true condition, the
0eb3b336
RS
293other criteria are *not* tested and the buffer name will appear in the
294menu with the menu-title corresponding to the true condition.
b9a5a6af 295
2e6286be 296If the condition returns the symbol `multi', then the buffer will be
b9a5a6af 297added to this menu *and* tested for other menus too. If it returns
2e6286be 298`no-multi', then the buffer will only be added if it hasn't been added
b9a5a6af
RS
299to any other menu.
300
301During this test, the buffer in question is the current buffer, and
302the test is surrounded by calls to `save-excursion' and
2e6286be 303`save-match-data'.
b9a5a6af 304
0eb3b336
RS
305The categories are sorted by MENU-SORT-KEY. Smaller keys are on top.
306nil means don't display this menu.
b9a5a6af 307
0eb3b336
RS
308MENU-TITLE is really a format. If you add %d in it, the %d is
309replaced with the number of items in that menu.
b9a5a6af 310
0eb3b336
RS
311ITEM-HANDLING-FN, is optional. If it is supplied and is a function,
312than it is used for displaying the items in that particular buffer
313menu, otherwise the function pointed out by
2e6286be 314`msb-item-handling-function' is used.
b9a5a6af
RS
315
316ITEM-SORT-FN, is also optional.
317If it is not supplied, the function pointed out by
2e6286be 318`msb-item-sort-function' is used.
b9a5a6af
RS
319If it is nil, then no sort takes place and the buffers are presented
320in least-recently-used order.
321If it is t, then no sort takes place and the buffers are presented in
322most-recently-used order.
323If it is supplied and non-nil and not t than it is used for sorting
324the items in that particular buffer menu.
325
0eb3b336
RS
326Note1: There should always be a `catch-all' as last element, in this
327list. That is an element like (t TITLE ITEM-HANDLING-FUNCTION).
b9a5a6af
RS
328Note2: A buffer menu appears only if it has at least one buffer in it.
329Note3: If you have a CONDITION that can't be evaluated you will get an
330error every time you do \\[msb].")
331
332(defvar msb-after-load-hooks nil
333 "Hooks to be run after the msb package has been loaded.")
334
335;;;
336;;; Internal variables
337;;;
338
0eb3b336 339;; Home directory for the current user
7612d61a 340(defvar msb--home-dir
0eb3b336
RS
341 (condition-case nil
342 (substitute-in-file-name "$HOME")
343 ;; If $HOME isn't defined, use nil
344 (error nil)))
345
b9a5a6af
RS
346;; The last calculated menu.
347(defvar msb--last-buffer-menu nil)
348
349;; If this is non-nil, then it is a string that describes the error.
350(defvar msb--error nil)
351
352;;;
4aa4849b 353;;; Some example function to be used for `msb-item-handling-function'.
b9a5a6af
RS
354;;;
355(defun msb-item-handler (buffer &optional maxbuf)
356 "Create one string item, concerning BUFFER, for the buffer menu.
357The item looks like:
358*% <buffer-name>
2e6286be
RS
359The `*' appears only if the buffer is marked as modified.
360The `%' appears only if the buffer is read-only.
b9a5a6af
RS
361Optional second argument MAXBUF is completely ignored."
362 (let ((name (buffer-name))
363 (modified (if (buffer-modified-p) "*" " "))
364 (read-only (if buffer-read-only "%" " ")))
365 (format "%s%s %s" modified read-only name)))
366
367
368(eval-when-compile (require 'dired))
369
2e6286be
RS
370;; `dired' can be called with a list of the form (directory file1 file2 ...)
371;; which causes `dired-directory' to be in the same form.
b9a5a6af
RS
372(defun msb--dired-directory ()
373 (cond ((stringp dired-directory)
374 (abbreviate-file-name (expand-file-name dired-directory)))
375 ((consp dired-directory)
376 (abbreviate-file-name (expand-file-name (car dired-directory))))
377 (t
2e6286be 378 (error "Unknown type of `dired-directory' in buffer %s"
b9a5a6af
RS
379 (buffer-name)))))
380
381(defun msb-dired-item-handler (buffer &optional maxbuf)
382 "Create one string item, concerning a dired BUFFER, for the buffer menu.
383The item looks like:
384*% <buffer-name>
2e6286be
RS
385The `*' appears only if the buffer is marked as modified.
386The `%' appears only if the buffer is read-only.
b9a5a6af
RS
387Optional second argument MAXBUF is completely ignored."
388 (let ((name (msb--dired-directory))
389 (modified (if (buffer-modified-p) "*" " "))
390 (read-only (if buffer-read-only "%" " ")))
391 (format "%s%s %s" modified read-only name)))
392
393(defun msb-alon-item-handler (buffer maxbuf)
394 "Create one string item for the buffer menu.
395The item looks like:
396<buffer-name> *%# <file-name>
2e6286be
RS
397The `*' appears only if the buffer is marked as modified.
398The `%' appears only if the buffer is read-only.
399The `#' appears only version control file (SCCS/RCS)."
b9a5a6af
RS
400 (format (format "%%%ds %%s%%s%%s %%s" maxbuf)
401 (buffer-name buffer)
402 (if (buffer-modified-p) "*" " ")
403 (if buffer-read-only "%" " ")
404 (if (and (boundp 'vc-mode) vc-mode) "#" " ")
405 (or buffer-file-name "")))
406
407;;;
4aa4849b 408;;; Some example function to be used for `msb-item-sort-function'.
b9a5a6af
RS
409;;;
410(defun msb-sort-by-name (item1 item2)
411 "Sorts the items depending on their buffer-name
412An item look like (NAME . BUFFER)."
413 (string-lessp (buffer-name (cdr item1))
414 (buffer-name (cdr item2))))
415
416
417(defun msb-sort-by-directory (item1 item2)
2e6286be 418 "Sorts the items depending on their directory. Made for dired.
b9a5a6af 419An item look like (NAME . BUFFER)."
0eb3b336
RS
420 (string-lessp (save-excursion (set-buffer (cdr item1))
421 (msb--dired-directory))
422 (save-excursion (set-buffer (cdr item2))
423 (msb--dired-directory))))
b9a5a6af
RS
424
425;;;
426;;; msb
427;;;
428;;; This function can be used instead of (mouse-buffer-menu EVENT)
429;;; function in "mouse.el".
0eb3b336 430;;;
b9a5a6af
RS
431(defun msb (event)
432 "Pop up several menus of buffers for selection with the mouse.
433This command switches buffers in the window that you clicked on, and
434selects that window.
435
2e6286be
RS
436See the function `mouse-select-buffer' and the variable
437`msb-menu-cond' for more information about how the menus are split."
b9a5a6af 438 (interactive "e")
fd46fd17 439 (let ((old-window (selected-window))
b9a5a6af 440 (window (posn-window (event-start event))))
fd46fd17
RS
441 (unless (framep window) (select-window window))
442 (let ((buffer (mouse-select-buffer event)))
443 (if buffer
444 (switch-to-buffer buffer)
445 (select-window old-window))))
b9a5a6af
RS
446 nil)
447
448;;;
449;;; Some supportive functions
450;;;
451(defun msb-invisible-buffer-p (&optional buffer)
452 "Return t if optional BUFFER is an \"invisible\" buffer.
453If the argument is left out or nil, then the current buffer is considered."
454 (and (> (length (buffer-name buffer)) 0)
455 (eq ?\ (aref (buffer-name buffer) 0))))
456
7612d61a
RS
457;; Strip one hierarchy level from the end of DIR.
458(defun msb--strip-dir (dir)
b9a5a6af 459 (save-match-data
0eb3b336 460 (cond
7612d61a
RS
461 ((string-match "^\\([^/]*/.+/\\)[^/]+$" dir)
462 (substring dir (match-beginning 1) (match-end 1)))
463 ((string-match "^\\([^/]*/\\)" dir)
464 (substring dir (match-beginning 1) (match-end 1)))
0eb3b336 465 (t
7612d61a 466 (error "msb: Directory `%s' has an unrecognized format" dir)))))
b9a5a6af
RS
467
468;; Create an alist with all buffers from LIST that lies under the same
0eb3b336 469;; directory will be in the same item as the directory string.
9153492c 470;; ((PATH1 . (BUFFER-1 BUFFER-2 ...)) (PATH2 . (BUFFER-K BUFFER-K+1...)) ...)
b9a5a6af
RS
471(defun msb--init-file-alist (list)
472 (let ((buffer-alist
0eb3b336
RS
473 ;; Make alist that looks like
474 ;; ((PATH-1 BUFFER-1) (PATH-2 BUFFER-2) ...)
475 ;; sorted on PATH-x
b9a5a6af
RS
476 (sort (mapcan
477 (function
478 (lambda (buffer)
0eb3b336
RS
479 (let ((file-name (expand-file-name (buffer-file-name buffer)))) =
480;LGL 971218
b9a5a6af 481 (when file-name
7612d61a 482 (list (cons (msb--strip-dir file-name) buffer))))))
b9a5a6af
RS
483 list)
484 (function (lambda (item1 item2)
485 (string< (car item1) (car item2)))))))
0eb3b336 486 ;; Now clump buffers togehter that have the same path
b9a5a6af 487 ;; Make alist that looks like
2e6286be 488 ;; ((PATH1 . (BUFFER-1 BUFFER-2 ...)) (PATH2 . (BUFFER-K)) ...)
b9a5a6af 489 (let ((path nil)
0eb3b336
RS
490 (buffers nil))
491 (nconc
b9a5a6af 492 (mapcan (function
0eb3b336
RS
493 (lambda (item)
494 (cond
495 ((and path
9153492c 496 (string= path (car item)))
0eb3b336
RS
497 ;; The same path as earlier: Add to current list of
498 ;; buffers.
499 (push (cdr item) buffers)
500 ;; This item should not be added to list
501 nil)
502 (t
503 ;; New path
504 (let ((result (and path (cons path buffers))))
505 (setq path (car item))
506 (setq buffers (list (cdr item)))
507 ;; Add the last result the list.
508 (and result (list result)))))))
509 buffer-alist)
510 ;; Add the last result to the list
b9a5a6af
RS
511 (list (cons path buffers))))))
512
0eb3b336
RS
513;; Format a suitable title for the menu item.
514(defun msb--format-title (top-found-p path number-of-items)
515 (let ((new-path path))
7612d61a
RS
516 (when (and msb--home-dir
517 (string-match (concat "^" msb--home-dir) path))
0eb3b336
RS
518 (setq new-path (concat "~/"
519 (substring path (match-end 0)))))
520 (format (if top-found-p "%s... (%d)" "%s (%d)")
521 new-path number-of-items)))
522
523
b9a5a6af
RS
524;; Choose file-menu with respect to directory for every buffer in LIST.
525(defun msb--choose-file-menu (list)
526 (let ((buffer-alist (msb--init-file-alist list))
527 (final-list nil)
528 (max-clumped-together (if (numberp msb-max-file-menu-items)
529 msb-max-file-menu-items
530 10))
531 (top-found-p nil)
532 (last-path nil)
533 first rest path buffers)
0eb3b336
RS
534 ;; Prepare for looping over all items in buffer-alist
535 (setq first (car buffer-alist)
536 rest (cdr buffer-alist)
537 path (car first)
538 buffers (cdr first))
539 ;; This big loop tries to clump buffers together that have a
540 ;; similar name. Remember that buffer-alist is sorted based on the
541 ;; path for the buffers.
b9a5a6af
RS
542 (while rest
543 (let ((found-p nil)
544 (tmp-rest rest)
0eb3b336 545 result
b9a5a6af
RS
546 new-path item)
547 (setq item (car tmp-rest))
0eb3b336
RS
548 ;; Clump together the "rest"-buffers that have a path that is
549 ;; a subpath of the current one.
b9a5a6af
RS
550 (while (and tmp-rest
551 (<= (length buffers) max-clumped-together)
552 (>= (length (car item)) (length path))
553 (string= path (substring (car item) 0 (length path))))
554 (setq found-p t)
0eb3b336
RS
555 (setq buffers (append buffers (cdr item))) ;nconc is faster than append
556 (setq tmp-rest (cdr tmp-rest)
557 item (car tmp-rest)))
b9a5a6af
RS
558 (cond
559 ((> (length buffers) max-clumped-together)
0eb3b336
RS
560 ;; Oh, we failed. Too many buffers clumped together.
561 ;; Just use the original ones for the result.
b9a5a6af 562 (setq last-path (car first))
0eb3b336
RS
563 (push (cons (msb--format-title top-found-p
564 (car first)
565 (length (cdr first)))
566 (cdr first))
567 final-list)
4aa4849b 568 (setq top-found-p nil)
b9a5a6af 569 (setq first (car rest)
0eb3b336
RS
570 rest (cdr rest)
571 path (car first)
b9a5a6af
RS
572 buffers (cdr first)))
573 (t
0eb3b336
RS
574 ;; The first pass of clumping together worked out, go ahead
575 ;; with this result.
b9a5a6af
RS
576 (when found-p
577 (setq top-found-p t)
578 (setq first (cons path buffers)
579 rest tmp-rest))
0eb3b336
RS
580 ;; Now see if we can clump more buffers together if we go up
581 ;; one step in the file hierarchy.
7612d61a 582 (setq path (msb--strip-dir path)
b9a5a6af
RS
583 buffers (cdr first))
584 (when (and last-path
585 (or (and (>= (length path) (length last-path))
586 (string= last-path
587 (substring path 0 (length last-path))))
588 (and (< (length path) (length last-path))
589 (string= path
590 (substring last-path 0 (length path))))))
0eb3b336
RS
591 ;; We have reached the same place in the file hierarchy as
592 ;; the last result, so we should quit at this point and
593 ;; take what we have as result.
594 (push (cons (msb--format-title top-found-p
595 (car first)
596 (length (cdr first)))
597 (cdr first))
598 final-list)
4aa4849b 599 (setq top-found-p nil)
b9a5a6af 600 (setq first (car rest)
0eb3b336
RS
601 rest (cdr rest)
602 path (car first)
603 buffers (cdr first)))))))
604 ;; Now take care of the last item.
605 (push (cons (msb--format-title top-found-p
606 (car first)
607 (length (cdr first)))
608 (cdr first))
609 final-list)
4aa4849b 610 (setq top-found-p nil)
b9a5a6af
RS
611 (nreverse final-list)))
612
613;; Create a vector as:
614;; [BUFFER-LIST-VARIABLE CONDITION MENU-SORT-KEY MENU-TITLE ITEM-HANDLER SORTER)
2e6286be
RS
615;; from an element in `msb-menu-cond'. See that variable for a
616;; description of its elements.
b9a5a6af
RS
617(defun msb--create-function-info (menu-cond-elt)
618 (let* ((list-symbol (make-symbol "-msb-buffer-list"))
619 (tmp-ih (and (> (length menu-cond-elt) 3)
620 (nth 3 menu-cond-elt)))
621 (item-handler (if (and tmp-ih (fboundp tmp-ih))
622 tmp-ih
623 msb-item-handling-function))
624 (tmp-s (if (> (length menu-cond-elt) 4)
625 (nth 4 menu-cond-elt)
626 msb-item-sort-function))
627 (sorter (if (or (fboundp tmp-s)
628 (null tmp-s)
2e6286be 629 (eq tmp-s t))
b9a5a6af
RS
630 tmp-s
631 msb-item-sort-function)))
632 (when (< (length menu-cond-elt) 3)
633 (error "Wrong format of msb-menu-cond."))
634 (when (and (> (length menu-cond-elt) 3)
635 (not (fboundp tmp-ih)))
636 (signal 'invalid-function (list tmp-ih)))
637 (when (and (> (length menu-cond-elt) 4)
638 tmp-s
639 (not (fboundp tmp-s))
2e6286be 640 (not (eq tmp-s t)))
b9a5a6af 641 (signal 'invalid-function (list tmp-s)))
2e6286be 642 (set list-symbol ())
b9a5a6af
RS
643 (vector list-symbol ;BUFFER-LIST-VARIABLE
644 (nth 0 menu-cond-elt) ;CONDITION
645 (nth 1 menu-cond-elt) ;SORT-KEY
646 (nth 2 menu-cond-elt) ;MENU-TITLE
647 item-handler ;ITEM-HANDLER
648 sorter) ;SORTER
649 ))
650
651;; This defsubst is only used in `msb--choose-menu' below. It was
2e6286be 652;; pulled out merely to make the code somewhat clearer. The indention
b9a5a6af
RS
653;; level was too big.
654(defsubst msb--collect (function-info-vector)
655 (let ((result nil)
656 (multi-flag nil)
657 function-info-list)
658 (setq function-info-list
659 (loop for fi
660 across function-info-vector
661 if (and (setq result
662 (eval (aref fi 1))) ;Test CONDITION
663 (not (and (eq result 'no-multi)
664 multi-flag))
665 (progn (when (eq result 'multi)
666 (setq multi-flag t))
4aa4849b 667 t))
b9a5a6af
RS
668 collect fi
669 until (and result
670 (not (eq result 'multi)))))
671 (when (and (not function-info-list)
672 (not result))
673 (error "No catch-all in msb-menu-cond!"))
674 function-info-list))
675
676;; Adds BUFFER to the menu depicted by FUNCTION-INFO
2e6286be 677;; All side-effects. Adds an element of form (BUFFER-TITLE . BUFFER)
b9a5a6af
RS
678;; to the buffer-list variable in function-info.
679(defun msb--add-to-menu (buffer function-info max-buffer-name-length)
680 (let ((list-symbol (aref function-info 0))) ;BUFFER-LIST-VARIABLE
681 ;; Here comes the hairy side-effect!
682 (set list-symbol
683 (cons (cons (funcall (aref function-info 4) ;ITEM-HANDLER
684 buffer
685 max-buffer-name-length)
686 buffer)
687 (eval list-symbol)))))
0eb3b336 688
b9a5a6af
RS
689;; Selects the appropriate menu for BUFFER.
690;; This is all side-effects, folks!
691;; This should be optimized.
692(defsubst msb--choose-menu (buffer function-info-vector max-buffer-name-length)
693 (unless (and (not msb-display-invisible-buffers-p)
694 (msb-invisible-buffer-p buffer))
695 (condition-case nil
696 (save-excursion
697 (set-buffer buffer)
2e6286be 698 ;; Menu found. Add to this menu
b9a5a6af
RS
699 (mapc (function
700 (lambda (function-info)
701 (msb--add-to-menu buffer function-info max-buffer-name-length)))
702 (msb--collect function-info-vector)))
703 (error (unless msb--error
704 (setq msb--error
705 (format
2e6286be 706 "In msb-menu-cond, error for buffer `%s'."
b9a5a6af 707 (buffer-name buffer)))
76e4c0ba 708 (error "%s" msb--error))))))
b9a5a6af
RS
709
710;; Return (SORT-KEY TITLE . BUFFER-LIST) or nil if the
711;; buffer-list is empty.
712(defun msb--create-sort-item (function-info)
713 (let ((buffer-list (eval (aref function-info 0))))
714 (when buffer-list
715 (let ((sorter (aref function-info 5)) ;SORTER
716 (sort-key (aref function-info 2))) ;MENU-SORT-KEY
717 (when sort-key
0eb3b336 718 (cons sort-key
b9a5a6af
RS
719 (cons (format (aref function-info 3) ;MENU-TITLE
720 (length buffer-list))
721 (cond
722 ((null sorter)
723 buffer-list)
2e6286be 724 ((eq sorter t)
b9a5a6af
RS
725 (nreverse buffer-list))
726 (t
727 (sort buffer-list sorter))))))))))
728
729;; Returns a list on the form ((TITLE . BUFFER-LIST)) for
730;; the most recently used buffers.
731(defun msb--most-recently-used-menu (max-buffer-name-length)
4aa4849b
RS
732 (when (and (numberp msb-display-most-recently-used)
733 (> msb-display-most-recently-used 0))
fd46fd17
RS
734 (let* ((buffers (cdr (buffer-list)))
735 (most-recently-used
b9a5a6af 736 (loop with n = 0
fd46fd17 737 for buffer in buffers
b9a5a6af
RS
738 if (save-excursion
739 (set-buffer buffer)
740 (and (not (msb-invisible-buffer-p))
741 (not (eq major-mode 'dired-mode))))
742 collect (save-excursion
743 (set-buffer buffer)
744 (cons (funcall msb-item-handling-function
745 buffer
746 max-buffer-name-length)
747 buffer))
748 and do (incf n)
4aa4849b 749 until (>= n msb-display-most-recently-used))))
b9a5a6af
RS
750 (cons (if (stringp msb-most-recently-used-title)
751 (format msb-most-recently-used-title
752 (length most-recently-used))
753 (signal 'wrong-type-argument (list msb-most-recently-used-title)))
754 most-recently-used))))
755
756(defun msb--create-buffer-menu-2 ()
757 (let ((max-buffer-name-length 0)
758 file-buffers
759 function-info-vector)
760 ;; Calculate the longest buffer name.
761 (mapc
762 (function
763 (lambda (buffer)
764 (if (or msb-display-invisible-buffers-p
765 (not (msb-invisible-buffer-p)))
766 (setq max-buffer-name-length
767 (max max-buffer-name-length
768 (length (buffer-name buffer)))))))
769 (buffer-list))
770 ;; Make a list with elements of type
771 ;; (BUFFER-LIST-VARIABLE
772 ;; CONDITION
773 ;; MENU-SORT-KEY
774 ;; MENU-TITLE
775 ;; ITEM-HANDLER
776 ;; SORTER)
777 ;; Uses "function-global" variables:
778 ;; function-info-vector
779 (setq function-info-vector
780 (apply (function vector)
781 (mapcar (function msb--create-function-info)
782 msb-menu-cond)))
783 ;; Split the buffer-list into several lists; one list for each
2e6286be 784 ;; criteria. This is the most critical part with respect to time.
b9a5a6af
RS
785 (mapc (function (lambda (buffer)
786 (cond ((and msb-files-by-directory
787 (buffer-file-name buffer))
788 (push buffer file-buffers))
789 (t
790 (msb--choose-menu buffer
791 function-info-vector
792 max-buffer-name-length)))))
793 (buffer-list))
794 (when file-buffers
795 (setq file-buffers
796 (mapcar (function
797 (lambda (buffer-list)
798 (cons msb-files-by-directory-sort-key
799 (cons (car buffer-list)
800 (sort
801 (mapcar (function
802 (lambda (buffer)
4aa4849b
RS
803 (cons (save-excursion
804 (set-buffer buffer)
805 (funcall msb-item-handling-function
806 buffer
807 max-buffer-name-length))
b9a5a6af
RS
808 buffer)))
809 (cdr buffer-list))
810 (function
811 (lambda (item1 item2)
812 (string< (car item1) (car item2)))))))))
813 (msb--choose-file-menu file-buffers))))
814 ;; Now make the menu - a list of (TITLE . BUFFER-LIST)
4aa4849b 815 (let* (menu
b9a5a6af
RS
816 (most-recently-used
817 (msb--most-recently-used-menu max-buffer-name-length))
0eb3b336 818 (others (nconc file-buffers
b9a5a6af 819 (loop for elt
4aa4849b
RS
820 across function-info-vector
821 for value = (msb--create-sort-item elt)
822 if value collect value))))
b9a5a6af
RS
823 (setq menu
824 (mapcar 'cdr ;Remove the SORT-KEY
825 ;; Sort the menus - not the items.
826 (msb--add-separators
827 (sort
828 ;; Get a list of (SORT-KEY TITLE . BUFFER-LIST)
829 ;; Also sorts the items within the menus.
830 (if (cdr most-recently-used)
831 (cons
832 ;; Add most recent used buffers
833 (cons msb-most-recently-used-sort-key
834 most-recently-used)
835 others)
836 others)
837 (function (lambda (elt1 elt2)
838 (< (car elt1) (car elt2))))))))
839 ;; Now make it a keymap menu
840 (append
841 '(keymap "Select Buffer")
842 (msb--make-keymap-menu menu)
843 (when msb-separator-diff
0eb3b336
RS
844 (list (list 'separator "--")))
845 (list (cons 'toggle
b9a5a6af
RS
846 (cons
847 (if msb-files-by-directory
848 "*Files by type*"
849 "*Files by directory*")
850 'msb--toggle-menu-type)))))))
851
852(defun msb--create-buffer-menu ()
853 (save-match-data
854 (save-excursion
855 (msb--create-buffer-menu-2))))
856
857;;;
858;;; Multi purpose function for selecting a buffer with the mouse.
0eb3b336 859;;;
b9a5a6af
RS
860(defun msb--toggle-menu-type ()
861 (interactive)
862 (setq msb-files-by-directory (not msb-files-by-directory))
9153492c 863 (menu-bar-update-buffers))
b9a5a6af
RS
864
865(defun mouse-select-buffer (event)
866 "Pop up several menus of buffers, for selection with the mouse.
867Returns the selected buffer or nil if no buffer is selected.
868
4aa4849b 869The way the buffers are split is conveniently handled with the
2e6286be 870variable `msb-menu-cond'."
b9a5a6af
RS
871 ;; Popup the menu and return the selected buffer.
872 (when (or msb--error
873 (not msb--last-buffer-menu)
874 (not (fboundp 'frame-or-buffer-changed-p))
875 (frame-or-buffer-changed-p))
876 (setq msb--error nil)
877 (setq msb--last-buffer-menu (msb--create-buffer-menu)))
4aa4849b
RS
878 (let ((position event)
879 choice)
b9a5a6af
RS
880 (when (and (fboundp 'posn-x-y)
881 (fboundp 'posn-window))
882 (let ((posX (car (posn-x-y (event-start event))))
883 (posY (cdr (posn-x-y (event-start event))))
4aa4849b 884 (posWind (posn-window (event-start event))))
b9a5a6af
RS
885 ;; adjust position
886 (setq posX (- posX (funcall msb-horizontal-shift-function))
887 position (list (list posX posY) posWind))))
1cc9a99e 888 ;; This `sit-for' magically makes the menu stay up if the mouse
6331da4b 889 ;; button is released within 0.1 second.
1cc9a99e
RS
890 (sit-for 0 100)
891 ;; Popup the menu
4aa4849b 892 (setq choice (x-popup-menu position msb--last-buffer-menu))
b9a5a6af 893 (cond
4aa4849b
RS
894 ((eq (car choice) 'toggle)
895 ;; Bring up the menu again with type toggled.
896 (msb--toggle-menu-type)
897 (mouse-select-buffer event))
898 ((and (numberp (car choice))
899 (null (cdr choice)))
900 (let ((msb--last-buffer-menu (nthcdr 3 (assq (car choice) msb--last-buffer-menu))))
b9a5a6af 901 (mouse-select-buffer event)))
4aa4849b
RS
902 ((while (numberp (car choice))
903 (setq choice (cdr choice))))
904 ((and (stringp (car choice))
905 (null (cdr choice)))
906 (car choice))
dc3247b3
RS
907 ((null choice)
908 choice)
4aa4849b
RS
909 (t
910 (error "Unknown form for buffer: %s" choice)))))
0eb3b336 911
b9a5a6af
RS
912;; Add separators
913(defun msb--add-separators (sorted-list)
914 (cond
915 ((or (not msb-separator-diff)
916 (not (numberp msb-separator-diff)))
917 sorted-list)
918 (t
919 (let ((last-key nil))
920 (mapcan
921 (function
922 (lambda (item)
923 (cond
924 ((and msb-separator-diff
0eb3b336 925 last-key
b9a5a6af
RS
926 (> (- (car item) last-key)
927 msb-separator-diff))
928 (setq last-key (car item))
929 (list (cons last-key 'separator)
930 item))
931 (t
932 (setq last-key (car item))
933 (list item)))))
934 sorted-list)))))
935
4aa4849b
RS
936(defun msb--split-menus-2 (list mcount result)
937 (cond
938 ((> (length list) msb-max-menu-items)
939 (let ((count 0)
940 sub-name
941 (tmp-list nil))
942 (while (< count msb-max-menu-items)
943 (push (pop list) tmp-list)
944 (incf count))
945 (setq tmp-list (nreverse tmp-list))
946 (setq sub-name (concat (car (car tmp-list)) "..."))
0eb3b336
RS
947 (push (nconc (list mcount sub-name
948 'keymap sub-name)
4aa4849b
RS
949 tmp-list)
950 result))
951 (msb--split-menus-2 list (1+ mcount) result))
952 ((null result)
953 list)
954 (t
955 (let (sub-name)
956 (setq sub-name (concat (car (car list)) "..."))
0eb3b336
RS
957 (push (nconc (list mcount sub-name
958 'keymap sub-name)
4aa4849b
RS
959 list)
960 result))
961 (nreverse result))))
0eb3b336 962
4aa4849b
RS
963(defun msb--split-menus (list)
964 (msb--split-menus-2 list 0 nil))
965
966
b9a5a6af
RS
967(defun msb--make-keymap-menu (raw-menu)
968 (let ((end (cons '(nil) 'menu-bar-select-buffer))
969 (mcount 0))
970 (mapcar
971 (function
972 (lambda (sub-menu)
0eb3b336 973 (cond
b9a5a6af 974 ((eq 'separator sub-menu)
0eb3b336 975 (list 'separator "--"))
b9a5a6af 976 (t
4aa4849b
RS
977 (let ((buffers (mapcar (function
978 (lambda (item)
979 (let ((string (car item))
980 (buffer (cdr item)))
981 (cons (buffer-name buffer)
982 (cons string end)))))
983 (cdr sub-menu))))
0eb3b336 984 (nconc (list (incf mcount) (car sub-menu)
4aa4849b
RS
985 'keymap (car sub-menu))
986 (msb--split-menus buffers)))))))
b9a5a6af
RS
987 raw-menu)))
988
989(defun menu-bar-update-buffers (&optional arg)
990 ;; If user discards the Buffers item, play along.
991 (when (and (lookup-key (current-global-map) [menu-bar buffer])
992 (or (not (fboundp 'frame-or-buffer-changed-p))
993 (frame-or-buffer-changed-p)
994 arg))
fd46fd17 995 (let ((frames (frame-list))
b9a5a6af 996 buffers-menu frames-menu)
b9a5a6af
RS
997 ;; Make the menu of buffers proper.
998 (setq msb--last-buffer-menu (msb--create-buffer-menu))
999 (setq buffers-menu msb--last-buffer-menu)
1000 ;; Make a Frames menu if we have more than one frame.
fd46fd17
RS
1001 (when (cdr frames)
1002 (let* ((frame-length (length frames))
1003 (f-title (format "Frames (%d)" frame-length)))
1004 ;; List only the N most recently selected frames
1005 (when (and (integerp msb-max-menu-items)
1006 (> msb-max-menu-items 1)
1007 (> frame-length msb-max-menu-items))
1008 (setcdr (nthcdr msb-max-menu-items frames) nil))
b9a5a6af 1009 (setq frames-menu
fd46fd17
RS
1010 (nconc
1011 (list 'frame f-title '(nil) 'keymap f-title)
1012 (mapcar
1013 (function
1014 (lambda (frame)
1015 (nconc
1016 (list frame
1017 (cdr (assq 'name
1018 (frame-parameters frame)))
1019 (cons nil nil))
1020 'menu-bar-select-frame)))
1021 frames)))))
b9a5a6af
RS
1022 (define-key (current-global-map) [menu-bar buffer]
1023 (cons "Buffers"
1024 (if (and buffers-menu frames-menu)
fd46fd17
RS
1025 ;; Combine Frame and Buffers menus with separator between
1026 (nconc (list 'keymap "Buffers and Frames" frames-menu
0eb3b336 1027 (and msb-separator-diff '(separator "--")))
fd46fd17
RS
1028 (cddr buffers-menu))
1029 (or buffers-menu 'undefined)))))))
b9a5a6af 1030
0eb3b336
RS
1031(when (and (boundp 'menu-bar-update-hook)
1032 (not (fboundp 'frame-or-buffer-changed-p)))
1033 (defvar msb--buffer-count 0)
1034 (defun frame-or-buffer-changed-p ()
1035 (let ((count (length (buffer-list))))
1036 (when (/= count msb--buffer-count)
1037 (setq msb--buffer-count count)
1038 t))))
1039
b9a5a6af
RS
1040(unless (or (not (boundp 'menu-bar-update-hook))
1041 (memq 'menu-bar-update-buffers menu-bar-update-hook))
1042 (add-hook 'menu-bar-update-hook 'menu-bar-update-buffers))
1043
1044(and (fboundp 'mouse-buffer-menu)
1045 (substitute-key-definition 'mouse-buffer-menu 'msb (current-global-map)))
1046
1047(provide 'msb)
1048(eval-after-load 'msb (run-hooks 'msb-after-load-hooks))
be17d374 1049
b9a5a6af 1050;;; msb.el ends here