1 ;;; wrolo.el --- Hierarchical, multi-file, easy to use rolodex system
3 ;; Copyright (C) 1989, 1990, 1991, 1992, 1995, 2006 Free Software
5 ;; Developed with support from Motorola Inc.
7 ;; Author: Bob Weiner, Brown U.
8 ;; Maintainer: Mats Lidell <matsl@contactor.se>
9 ;; Keywords: hypermedia, matching
11 ;; This file is part of GNU Hyperbole.
13 ;; GNU Hyperbole is free software; you can redistribute it and/or
14 ;; modify it under the terms of the GNU General Public License as
15 ;; published by the Free Software Foundation; either version 3, or (at
16 ;; your option) any later version.
18 ;; GNU Hyperbole is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 ;; General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs; see the file COPYING. If not, write to the
25 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
26 ;; Boston, MA 02110-1301, USA.
30 ;; The `put whatever you feel like into it' rolodex.
34 ;; 1. Multiple rolodex files with free text lookup. No structured
37 ;; 2. Hierarchical rolodex entries as in:
42 ;; Searching for Manager turns up all Underlings. Searching for
43 ;; Company retrieves all listed employees.
45 ;; This hierarchical system has proved very effective for retrieving
46 ;; computer system administration problem reports by vendor name,
47 ;; problem number or by subject area, without having to resort to a
48 ;; database system, and also for extraction of relevant text
49 ;; sections from reports.
51 ;; 3. String and regular expression searching capabilities. Matches are
52 ;; found anywhere within entries, so entries may be of any format you
53 ;; like without the bother of fixed field restrictions.
54 ;; Ability to restrict number of matches or to report number of matches
55 ;; without displaying entries.
57 ;; 4. Smart addition, editing and sorting of entries by hierarchy level.
59 ;; 5. Support for Hyperbole buttons within rolodex entries.
61 ;; See "wrolo-logic.el" for logical search functions (and, or, not, xor).
62 ;; See "wrolo-menu.el" for menu handling functions. (If you received
63 ;; wrolo as part of Hyperbole, this file in unneeded and so not included.)
68 ;; The variable 'rolo-file-list' is a list of files to search for
69 ;; matching rolodex entries. To add personal files to rolo-file-list,
70 ;; when you find these functions are useful for any sort of list lookup,
71 ;; add the following to your ~/.emacs file (substituting where you see
74 ;; (setq rolo-file-list (append rolo-file-list '("<file1>" "<file2>")))
76 ;; We recommend that entries in 'rolo-file-list' have ".otl" suffixes
77 ;; so that they do not conflict with file names that other rolodex
78 ;; programs might use and so that they are edited in 'outline-mode' by
79 ;; default. If you want the latter behavior, uncomment and add something
80 ;; like the following to one of your GNU Emacs initialization files:
82 ;; ;; Add to the list of suffixes that causes automatic mode invocation
83 ;; (setq auto-mode-alist
84 ;; (append '(("\\.otl$" . outline-mode)) auto-mode-alist))
86 ;; The buffers containing the rolodex files are not killed after a search
87 ;; on the assumption that another search is likely to follow within this
88 ;; Emacs session. You may wish to change this behavior with the following
91 ;; (setq rolo-kill-buffers-after-use t)
93 ;; After an entry is killed, the modified rolodex file is automatically
94 ;; saved. If you would rather always save files yourself, use this
97 ;; (setq rolo-save-buffers-after-use nil)
99 ;; When adding an entry from within a buffer containing a mail
100 ;; message, the rolodex add function will extract the sender's name
101 ;; and e-mail address and prompt you with the name as a default. If
102 ;; you accept it, it will enter the name and the email address using
103 ;; the format given by the 'rolo-email-format' variable. See its
104 ;; documentation if you want to change its value.
107 ;; If you use Hyperbole V2.3 or greater, then no other rolodex setup
108 ;; is necessary, simply select the "Rolo/" menu item from the top
109 ;; level Hyperbole menu. Otherwise, add the following to your
112 ;; (autoload 'rolo-menu "rolo-menu" "Load wrolo system." t)
113 ;; (global-set-key "\C-x4r" 'rolo-menu)
115 ;; And then simply invoke the rolodex menu with {C-x 4 r} after Emacs
116 ;; has read those lines in your init file.
121 ;; The rolo menu provides access to the following commands:
123 ;; Menu Item Function Description
124 ;; ====================================================================
125 ;; Add rolo-add Adds a rolodex entry
126 ;; Display rolo-display-matches Displays last matches again
127 ;; Edit rolo-edit Edits an existing rolodex entry
128 ;; Info Displays Rolodex manual entry
129 ;; Kill rolo-kill Removes an entry from the rolodex
130 ;; Order rolo-sort Sorts all levels in rolodex
131 ;; RegexFind rolo-grep Finds all entries containing
132 ;; a regular expression
133 ;; StringFind rolo-fgrep Finds all entries containing
135 ;; WordFind rolo-word Finds all entries containing
136 ;; a string of whole words
137 ;; Yank rolo-yank Inserts first matching rolodex
140 ;; For any of these commands that prompt you for a name, you may use the form
141 ;; parent/child to locate a child entry below a parent entry, e.g.
142 ;; from the example near the top, we could give Company/Manager/Underlings.
144 ;; Here is a snippet from our group rolodex file. The ';'s should be
145 ;; removed of course and the '*'s should begin at the start of the
146 ;; line. If a rolodex file begins with two separator lines whose
147 ;; first three characters are "===", then these lines and any text
148 ;; between them are prepended to the output buffer whenever any
149 ;; entries are retrieved from that file.
151 ;;=============================================================================
153 ;; <Last Name>, <First Name> <Co/Categ> W<Work #> H<Home #> P<Pager #>
154 ;; F<Fax #> M<Modem #> C<Cellular #>
155 ;; <Address> <Miscellaneous Info, Key Words>
156 ;;=============================================================================
157 ;;* EX594, Digital-Systems-Research
158 ;;** Weiner, Bob Motorola W2087 P7-7489
164 ;; Entries in rolodex files are separated by patterns matching
165 ;; 'rolo-entry-regexp'. Each entry may have any number of sub-entries
166 ;; which represent the next level down in the entry hierarchy.
167 ;; Sub-entries' separator patterns are always longer than their parents'.
168 ;; For example, if an entry began with '*' then its sub-entries would begin
169 ;; with '**' and so on. Blank lines in rolodex files will not end up where
170 ;; you want them if you use the rolo-sort commands; therefore, blank lines
171 ;; are not recommended. If you change the value of
172 ;; 'rolo-entry-regexp', you will have to modify 'rolo-sort'.
174 ;; The following additional functions are provided:
176 ;; 'rolo-sort-level' sorts a specific level of entries in a rolodex file;
177 ;; 'rolo-map-level' runs a user specified function on a specific level of
178 ;; entries in a rolodex file;
179 ;; 'rolo-fgrep-file', same as 'rolo-fgrep' but operates on a single file;
180 ;; 'rolo-grep-file', same as 'rolo-grep' but operates on a single file;
181 ;; 'rolo-display-matches', display last set of rolodex matches, if any;
182 ;; 'rolo-toggle-narrow-to-entry' toggles between display of current entry
183 ;; and display of all matching entries.
189 ;; Added internal 'rolo-shrink-window' function for use in
190 ;; compressing/uncompressing the rolo view window to/from a size just
191 ;; large enough for the selected entry. This is useful when a search
192 ;; turns up more entries than desired.
195 ;; Modified 'rolo-grep-file' and 'rolo-map-level' so they only set buffers
196 ;; read-only the first time they are read in. This way, if someone edits a
197 ;; rolodex file and then does a rolo-fgrep or other function, the buffer
198 ;; will not be back in read-only mode.
201 ;; Modified 'rolo-grep-file' to expand any hidden entries in rolo file
202 ;; before doing a search.
205 ;; Added Hyperbole button support.
208 ;; Added convenient support for entry add, edit, kill and yank.
211 ;; Fixed bug in rolo-to that ended search too early.
217 ;;; Other required Elisp libraries
227 (defvar rolo-email-format "%s\t\t<%s>"
228 "Format string to use when adding an entry with e-mail addr from a mail msg.
229 It must contain a %s indicating where to put the entry name and a second
230 %s indicating where to put the e-mail address.")
232 (defvar rolo-file-list
233 (if (memq system-type '(ms-windows windows-nt ms-dos))
234 '("c:/_rolodex.otl") '("~/.rolodex.otl"))
235 "*List of files containing rolodex entries.
236 The first file should be a user-specific rolodex file, typically in the home
237 directory. The second file is often a shared, group-specific rolodex file.
239 A rolo-file consists of:
240 (1) an optional header beginning with and ending with a line which matches
242 (2) one or more rolodex entries which each begin with
243 rolo-entry-regexp and may be nested.")
245 (defvar rolo-highlight-face nil
246 "*Face used to highlight rolodex search matches.")
247 (if rolo-highlight-face
249 (setq rolo-highlight-face
250 (cond (hyperb:emacs19-p
251 (if (fboundp 'make-face)
252 (progn (make-face 'rolo-highlight-face)
253 'rolo-highlight-face)))
254 (hyperb:epoch-p (make-style))
255 (t (if (fboundp 'make-face)
256 (face-name (make-face 'rolo-highlight-face))))))
257 (if (fboundp 'hproperty:set-item-highlight)
258 (hproperty:set-item-highlight)))
260 (defvar rolo-kill-buffers-after-use nil
261 "*Non-nil means kill rolodex file buffers after searching them for entries.
262 Only unmodified buffers are killed.")
264 (defvar rolo-save-buffers-after-use t
265 "*Non-nil means save rolodex file after an entry is killed.")
267 (defvar wrolo-yank-reformat-function nil
268 "*Value is a function of two arguments, START and END, invoked after a rolo-yank.
269 It should reformat the region given by the arguments to some preferred style.
270 Default value is nil, meaning no reformmating is done.")
277 (defun rolo-add (name &optional file)
278 "Adds a new entry in personal rolodex for NAME.
279 Last name first is best, e.g. \"Smith, John\".
280 With prefix argument, prompts for optional FILE to add entry within.
281 NAME may be of the form: parent/child to insert child below a parent
282 entry which begins with the parent string."
285 (or (fboundp 'mail-fetch-field) (require 'mail-utils))
286 (let* ((lst (rolo-name-and-email))
288 (email (car (cdr lst)))
289 (entry (read-string "Name to add to rolo: "
291 (list (if (and email name
292 (string-match (concat "\\`" (regexp-quote entry)) name))
293 (format rolo-email-format entry email) entry)
294 current-prefix-arg))))
295 (if (or (not (stringp name)) (string= name ""))
296 (error "(rolo-add): Invalid name: '%s'" name))
297 (if (and (interactive-p) file)
298 (setq file (completing-read "File to add to: "
299 (mapcar 'list rolo-file-list))))
300 (if (null file) (setq file (car rolo-file-list)))
301 (cond ((and file (or (not (stringp file)) (string= file "")))
302 (error "(rolo-add): Invalid file: '%s'" file))
303 ((and (file-exists-p file) (not (file-readable-p file)))
304 (error "(rolo-add): File not readable: '%s'" file))
305 ((not (file-writable-p file))
306 (error "(rolo-add): File not writable: '%s'" file)))
307 (set-buffer (or (get-file-buffer file) (find-file-noselect file)))
308 (if (interactive-p) (message "Locating insertion point for '%s'..." name))
309 (let ((parent "") (level "") end)
310 (widen) (goto-char 1)
311 (while (setq end (string-match "/" name))
312 (setq parent (substring name 0 end)
313 name (substring name (min (1+ end) (length name))))
314 (if (re-search-forward
315 (concat "\\(" rolo-entry-regexp "\\)[ \t]*"
316 (regexp-quote parent)) nil t)
317 (setq level (buffer-substring (match-beginning 1)
319 (error "(rolo-add): '%s' category not found in \"%s\"."
321 (narrow-to-region (point)
322 (progn (rolo-to-entry-end t level) (point)))
323 (goto-char (point-min))
324 (let* ((len (length name))
325 (name-level (concat level "*"))
326 (level-len (length name-level))
334 (concat "\\(" rolo-entry-regexp "\\)\\([ \t]*\\)")
336 (setq entry-level (buffer-substring (match-beginning 1)
338 (if (/= (length entry-level) level-len)
339 (rolo-to-entry-end t entry-level)
340 (setq entry (buffer-substring (point) (+ (point) len))
341 entry-spc (buffer-substring (match-beginning 2)
343 (cond ((string< entry name)
344 (rolo-to-entry-end t entry-level))
345 ((string< name entry)
346 (setq again nil) (beginning-of-line))
347 (t ;; found existing entry matching name
348 (setq again nil match t)))))
349 (setq buffer-read-only nil)
352 (insert (or entry-level (concat level "*"))
353 (if (string= entry-spc "") " " entry-spc)
357 (rolo-to-buffer (current-buffer))
358 ;; Fixes non-display update bug when buf is on screen before
359 ;; interactive command invocation.
362 (message "Edit entry at point.")))))
365 (defun rolo-display-matches (&optional display-buf return-to-buffer)
366 "Display optional DISPLAY-BUF buffer of previously found rolodex matches.
367 If DISPLAY-BUF is nil, use the value in 'rolo-display-buffer'.
368 Second arg RETURN-TO-BUFFER is the buffer to leave point within after the display."
370 (or display-buf (setq display-buf (get-buffer rolo-display-buffer)))
372 (error "(rolo-display-matches): Search the rolodex first."))
373 ;; Save current window configuration if rolodex match buffer is not
374 ;; displayed in one of the windows already.
376 ;; Handle both Emacs V18 and V19 versions of get-buffer-window.
378 (get-buffer-window display-buf (selected-frame))
379 (error (get-buffer-window display-buf)))
380 (setq *rolo-wconfig* (current-window-configuration)))
381 (rolo-to-buffer display-buf t)
382 (if (eq major-mode 'wrolo-mode) nil (wrolo-mode))
383 (setq buffer-read-only nil)
384 (if (fboundp 'hproperty:but-create) (hproperty:but-create))
386 (goto-char (point-min))
387 (set-buffer-modified-p nil)
388 (setq buffer-read-only t)
389 (run-hooks 'wrolo-display-hook)
390 ;; Leave point in match buffer unless a specific RETURN-TO-BUFFER has
391 ;; been specified. Use {q} to quit and restore display.
392 (if return-to-buffer (rolo-to-buffer return-to-buffer t)))
395 (defun rolo-edit (&optional name file)
396 "Edits a rolodex entry given by optional NAME within 'rolo-file-list'.
397 With prefix argument, prompts for optional FILE to locate entry within.
398 With no NAME arg, simply displays FILE or first entry in 'rolo-file-list' in an
399 editable mode. NAME may be of the form: parent/child to edit child below a
400 parent entry which begins with the parent string."
401 (interactive "sName to edit in rolo: \nP")
402 (if (string-equal name "") (setq name nil))
403 (and name (not (stringp name))
404 (error "(rolo-edit): Invalid name: '%s'" name))
405 (if (and (interactive-p) current-prefix-arg)
406 (if (= (length rolo-file-list) 1)
407 (setq file (car rolo-file-list))
408 (setq file (completing-read "Entry's File: "
409 (mapcar 'list rolo-file-list)))))
410 (let ((found-point) (file-list (if file (list file) rolo-file-list)))
411 (or file (setq file (car file-list)))
413 (progn (if (not (file-writable-p file))
414 (error "(rolo-edit): File not writable: '%s'" file))
415 (find-file-other-window file) (setq buffer-read-only nil))
416 (if (setq found-point (rolo-to name file-list))
418 (setq file buffer-file-name)
419 (if (file-writable-p file)
420 (setq buffer-read-only nil)
422 "(rolo-edit): Entry found but file not writable: '%s'" file)
424 (rolo-to-buffer (current-buffer)))
425 (message "(rolo-edit): '%s' not found." name)
427 (rolo-to-buffer (or (get-file-buffer (car file-list))
428 (find-file-noselect (car file-list))))
429 (setq buffer-read-only nil))
431 ;; Fixes display update bug in some Emacs versions. When buf is
432 ;; on screen before interactive command invocation, point is not
433 ;; moved to proper location.
434 (if found-point (goto-char found-point)))))
436 (defun rolo-edit-entry ()
437 "Edit the source entry of the rolodex match buffer entry at point.
438 Returns entry name if found, else nil."
440 (let ((name (rolo-name-at)))
441 (if name (progn (rolo-edit name (hbut:key-src))
445 (defun rolo-fgrep (string
446 &optional max-matches rolo-file count-only no-display)
447 "Display rolodex entries matching STRING.
448 To a maximum of optional prefix arg MAX-MATCHES, in file(s) from optional
449 ROLO-FILE or rolo-file-list. Default is to find all matching entries. Each
450 entry is displayed with all of its sub-entries. Optional COUNT-ONLY non-nil
451 means don't retrieve and don't display matching entries. Optional NO-DISPLAY
452 non-nil means retrieve entries but don't display.
454 Nil value of MAX-MATCHES means find all matches, t value means find all matches
455 but omit file headers, negative values mean find up to the inverse of that
456 number of entries and omit file headers.
458 Returns number of entries matched. See also documentation for the variable
460 (interactive "sRolodex string to match: \nP")
461 (let ((total-matches (rolo-grep (regexp-quote string) max-matches
462 rolo-file count-only no-display)))
464 (message "%s matching entr%s found in rolodex."
465 (if (= total-matches 0) "No" total-matches)
466 (if (= total-matches 1) "y" "ies")))
470 (defun rolo-grep (regexp &optional max-matches rolo-bufs count-only no-display)
471 "Display rolodex entries matching REGEXP.
472 To a maximum of prefix arg MAX-MATCHES, in buffer(s) from optional ROLO-BUFS or
473 rolo-file-list. Default is to find all matching entries. Each entry is
474 displayed with all of its sub-entries. Optional COUNT-ONLY non-nil means don't
475 retrieve and don't display matching entries. Optional NO-DISPLAY non-nil
476 means retrieve entries but don't display.
478 Nil value of MAX-MATCHES means find all matches, t value means find all matches
479 but omit file headers, negative values mean find up to the inverse of that
480 number of entries and omit file headers.
482 Returns number of entries matched. See also documentation for the variable
484 (interactive "sRolodex regular expression to match: \nP")
485 (let ((rolo-file-list
486 (cond ((null rolo-bufs) rolo-file-list)
487 ((listp rolo-bufs) rolo-bufs)
489 (display-buf (if count-only
491 (set-buffer (get-buffer-create rolo-display-buffer))))
494 (inserting (or (eq max-matches t)
495 (and (integerp max-matches) (< max-matches 0))))
498 (setq buffer-read-only nil)
499 (or inserting (erase-buffer)))
500 (while (and (setq file (car rolo-file-list))
501 (or (not (integerp max-matches))
502 (< total-matches (max max-matches (- max-matches)))))
503 (setq rolo-file-list (cdr rolo-file-list)
504 num-matched (rolo-grep-file file regexp max-matches count-only)
505 total-matches (+ total-matches num-matched))
506 (if (integerp max-matches)
508 (if (>= max-matches 0)
509 (- max-matches num-matched)
510 (+ max-matches num-matched)))))
511 (if (or count-only no-display inserting (= total-matches 0))
513 (rolo-display-matches display-buf))
515 (message "%s matching entr%s found in rolodex."
516 (if (= total-matches 0) "No" total-matches)
517 (if (= total-matches 1) "y" "ies")
521 (defun rolo-isearch ()
522 "Interactively search forward for next occurrence of current match regexp.
523 Use this to add characters to further narrow the search."
525 (if (equal (buffer-name) rolo-display-buffer)
526 (execute-kbd-macro (concat "\e\C-s" rolo-match-regexp))
527 (error "(rolo-isearch): Use this command in the %s match buffer"
528 rolo-display-buffer)))
531 (defun rolo-kill (name &optional file)
532 "Kills a rolodex entry given by NAME within 'rolo-file-list'.
533 With prefix argument, prompts for optional FILE to locate entry within.
534 NAME may be of the form: parent/child to kill child below a parent entry
535 which begins with the parent string.
536 Returns t if entry is killed, nil otherwise."
537 (interactive "sName to kill in rolo: \nP")
538 (if (or (not (stringp name)) (string= name ""))
539 (error "(rolo-kill): Invalid name: '%s'" name))
540 (if (and (interactive-p) current-prefix-arg)
541 (setq file (completing-read "Entry's File: "
542 (mapcar 'list rolo-file-list))))
543 (let ((file-list (if file (list file) rolo-file-list))
545 (or file (setq file (car file-list)))
546 (if (rolo-to name file-list)
548 (setq file buffer-file-name)
549 (if (file-writable-p file)
551 (function (lambda (start level)
553 start (rolo-to-entry-end t level))
556 (rolo-kill-buffer))))
558 (setq buffer-read-only nil)
559 (re-search-backward rolo-entry-regexp nil t)
560 (setq end (match-end 0))
563 level (buffer-substring start end))
565 (skip-chars-forward " \t")
567 (let ((entry-line (buffer-substring
570 (progn (end-of-line) (point))))))
571 (if (y-or-n-p (format "Kill `%s...' " entry-line))
573 (funcall kill-op start level)
575 (message "Aborted")))
576 (funcall kill-op start level)))
578 "(rolo-kill): Entry found but file not writable: '%s'" file)
580 (message "(rolo-kill): '%s' not found." name)
584 (defun rolo-mail-to ()
585 "Start composing mail addressed to the first e-mail address at or after point."
587 (let ((opoint (point)) button)
588 (skip-chars-backward "^ \t\n\r<>")
589 (if (and (re-search-forward mail-address-regexp nil t)
590 (goto-char (match-beginning 1))
591 (setq button (ibut:at-p)))
592 (hui:hbut-act button)
595 (message "(rolo-mail-to): Invalid buffer or no e-mail address found"))))
597 (defun rolo-next-match ()
598 "Move point forward to the start of the next rolodex search match."
600 (if (not (stringp rolo-match-regexp))
601 (error "(rolo-next-match): Invoke a rolodex search expression first"))
602 (let ((start (point))
603 (case-fold-search t))
604 (if (looking-at rolo-match-regexp)
605 (goto-char (match-end 0)))
606 (if (re-search-forward rolo-match-regexp nil t)
607 (goto-char (match-beginning 0))
610 "(rolo-next-match): No following matches for \"%s\"" rolo-match-regexp))))
612 (defun rolo-previous-match ()
613 "Move point back to the start of the previous rolodex search match."
615 (if (not (stringp rolo-match-regexp))
616 (error "(rolo-previous-match): Invoke a rolodex search expression first"))
617 (let ((case-fold-search t))
618 (if (re-search-backward rolo-match-regexp nil t)
621 "(rolo-previous-match): No prior matches for \"%s\"" rolo-match-regexp))))
624 "Quit from the rolodex match buffer and restore the prior frame display."
627 (if (and *rolo-wconfig*
628 (if (fboundp 'window-configuration-p)
629 (window-configuration-p *rolo-wconfig*)
631 (set-window-configuration *rolo-wconfig*)))
634 (defun rolo-sort (&optional rolo-file)
635 "Sorts up to 14 levels of entries in ROLO-FILE (default is personal rolo).
636 Assumes entries are delimited by one or more '*'characters.
637 Returns list of number of groupings at each entry level."
639 (list (let ((default "")
643 (format "Sort rolo file (default %s): "
644 (file-name-nondirectory
646 (if (and buffer-file-name
651 (equal buffer-file-name
652 (expand-file-name file))))
655 (car rolo-file-list)))))
656 (mapcar 'list rolo-file-list)))
657 (if (string= file "") default file))))
658 (if (or (not rolo-file) (equal rolo-file ""))
659 (setq rolo-file (car rolo-file-list)))
660 (if (not (and (stringp rolo-file) (file-readable-p rolo-file)))
661 (error "(rolo-sort): Invalid or unreadable file: %s" rolo-file))
662 (let ((level-regexp (regexp-quote "**************"))
663 (entries-per-level-list)
665 (while (not (equal level-regexp ""))
666 (setq n (rolo-sort-level rolo-file level-regexp))
667 (if (or (/= n 0) entries-per-level-list)
668 (setq entries-per-level-list
669 (append (list n) entries-per-level-list)))
670 (setq level-regexp (substring level-regexp 0 (- (length level-regexp) 2))))
671 entries-per-level-list))
673 (defun rolo-sort-level (rolo-file level-regexp &optional max-groupings)
674 "Sorts groupings of entries in ROLO-FILE at hierarchy level LEVEL-REGEXP.
675 To a maximum of optional MAX-GROUPINGS. Nil value of MAX-GROUPINGS means all
676 groupings at the given level. LEVEL-REGEXP should simply match the text of
677 any rolodex entry of the given level, not the beginning of a line (^); an
678 example, might be (regexp-quote \"**\") to match level two. Returns number
679 of groupings sorted."
680 (interactive "sRolodex file to sort: \nRegexp for level's entries: \nP")
682 (function (lambda (start end) (sort-lines nil start end)))
687 (defun rolo-toggle-narrow-to-entry ()
688 "Toggle between display of current entry and display of all matched entries.
689 Useful when bound to a mouse key."
691 (if (rolo-narrowed-p)
693 (if (or (looking-at rolo-entry-regexp)
694 (re-search-backward rolo-entry-regexp nil t))
695 (progn (forward-char)
696 (narrow-to-region (1- (point)) (rolo-display-to-entry-end)))))
698 (goto-char (point-min)))
700 (defun rolo-word (string
701 &optional max-matches rolo-file count-only no-display)
702 "Display rolodex entries with whole word matches for STRING.
703 To a maximum of optional prefix arg MAX-MATCHES, in file(s) from optional
704 ROLO-FILE or rolo-file-list. Default is to find all matching entries. Each
705 entry is displayed with all of its sub-entries. Optional COUNT-ONLY non-nil
706 means don't retrieve and don't display matching entries. Optional NO-DISPLAY
707 non-nil means retrieve entries but don't display.
709 Nil value of MAX-MATCHES means find all matches, t value means find all matches
710 but omit file headers, negative values mean find up to the inverse of that
711 number of entries and omit file headers.
713 Returns number of entries matched. See also documentation for the variable
715 (interactive "sRolodex whole words to match: \nP")
716 (let ((total-matches (rolo-grep (format "\\b%s\\b" (regexp-quote string))
718 rolo-file count-only no-display)))
720 (message "%s matching entr%s found in rolodex."
721 (if (= total-matches 0) "No" total-matches)
722 (if (= total-matches 1) "y" "ies")))
726 (defun rolo-yank (name &optional regexp-p)
727 "Inserts at point the first rolodex entry matching NAME.
728 With optional prefix arg, REGEXP-P, treats NAME as a regular expression instead
730 (interactive "sName to insert record for: \nP")
731 (let ((rolo-display-buffer (current-buffer))
735 (setq found (if regexp-p
737 (rolo-grep (regexp-quote name) -1))))
738 ;; Let user reformat the region just yanked.
739 (if (and (= found 1) (fboundp wrolo-yank-reformat-function))
740 (funcall wrolo-yank-reformat-function start (point)))
747 (defun rolo-fgrep-file (rolo-buf string &optional max-matches count-only)
748 "Retrieve entries in ROLO-BUF matching STRING to a maximum of optional MAX-MATCHES.
749 Nil value of MAX-MATCHES means find all matches, t value means find all matches
750 but omit file headers, negative values mean find up to the inverse of that
751 number of entries and omit file headers. Optional COUNT-ONLY non-nil
752 means don't retrieve matching entries.
753 Returns number of matching entries found."
754 (rolo-grep-file rolo-buf (regexp-quote string) max-matches count-only))
756 (defun rolo-grep-file (rolo-buf regexp &optional max-matches count-only)
757 "Retrieve entries in ROLO-BUF matching REGEXP to a maximum of optional MAX-MATCHES.
758 Nil value of MAX-MATCHES means find all matches, t value means find all matches
759 but omit file headers, negative values mean find up to the inverse of that
760 number of entries and omit file headers. Optional COUNT-ONLY non-nil
761 means don't retrieve matching entries.
762 Returns number of matching entries found."
764 ;; Save regexp as last rolodex search expression.
765 (setq rolo-match-regexp regexp)
767 (let ((new-buf-p) (actual-buf))
768 (if (and (or (null max-matches) (eq max-matches t) (integerp max-matches))
769 (or (setq actual-buf (rolo-buffer-exists-p rolo-buf))
770 (if (file-exists-p rolo-buf)
771 (setq actual-buf (find-file-noselect rolo-buf t)
773 (let ((hdr-pos) (num-found 0) (curr-entry-level)
776 (cond ((eq max-matches t)
777 (setq incl-hdr nil max-matches nil))
780 max-matches (- max-matches)))))
781 (set-buffer actual-buf)
782 (if new-buf-p (setq buffer-read-only t))
787 ;; Ensure no entries in outline mode are hidden.
788 ;; Uses 'show-all' function from outline.el.
789 (and (search-forward "\C-m" nil t)
791 (if (re-search-forward rolo-hdr-regexp nil t 2)
792 (progn (forward-line)
793 (setq hdr-pos (cons (point-min) (point)))))
794 (re-search-forward rolo-entry-regexp nil t)
795 (while (and (or (null max-matches) (< num-found max-matches))
796 (re-search-forward regexp nil t))
797 (re-search-backward rolo-entry-regexp nil t)
798 (let ((start (point))
800 (re-search-forward rolo-entry-regexp nil t)
801 (setq curr-entry-level (buffer-substring start (point)))
802 (rolo-to-entry-end t curr-entry-level)
804 (if (and (= num-found 0) incl-hdr)
805 (let* ((src (or (buffer-file-name actual-buf)
809 (concat (if (boundp 'hbut:source-prefix)
813 (prin1-to-string src))))
814 (set-buffer rolo-display-buffer)
815 (goto-char (point-max))
818 (insert-buffer-substring
819 actual-buf (car hdr-pos) (cdr hdr-pos))
820 (insert src-line "\n\n"))
821 (insert (format rolo-hdr-format src-line)))
822 (set-buffer actual-buf))))
823 (setq num-found (1+ num-found))
825 (rolo-add-match rolo-display-buffer regexp start (point)))))))
826 (rolo-kill-buffer actual-buf)
830 (defun rolo-map-level (func rolo-buf level-regexp &optional max-groupings)
831 "Perform FUNC on groupings of ROLO-BUF entries at level LEVEL-REGEXP,
832 to a maximum of optional argument MAX-GROUPINGS. Nil value of MAX-GROUPINGS
833 means all groupings at the given level. FUNC should take two arguments, the
834 start and the end of the region that it should manipulate. LEVEL-REGEXP
835 should simply match the text of any rolodex entry of the given level, not the
836 beginning of a line (^); an example, might be (regexp-quote \"**\") to match
837 level two. Returns number of groupings matched."
839 (if (and (or (null max-groupings) (< 0 max-groupings))
840 (or (setq actual-buf (rolo-buffer-exists-p rolo-buf))
841 (if (file-exists-p rolo-buf)
842 (progn (setq actual-buf (find-file-noselect rolo-buf t))
845 (set-buffer actual-buf)
847 (exact-level-regexp (concat "^\\(" level-regexp "\\)[ \t\n]"))
848 (outline-regexp rolo-entry-regexp)
851 ;; Load 'outline' library since its functions are used here.
852 (if (not (boundp 'outline-mode-map))
853 (load-library "outline"))
854 (goto-char (point-min))
855 ;; Pass buffer header if it exists
856 (if (re-search-forward rolo-hdr-regexp nil t 2)
858 (while (and (or (null max-groupings) (< num-found max-groupings))
859 (re-search-forward exact-level-regexp nil t))
860 (setq num-found (1+ num-found))
861 (let* ((opoint (prog1 (point) (beginning-of-line)))
862 (grouping-start (point))
863 (start grouping-start)
864 (level-len (or level-len (- (1- opoint) start)))
870 (if (setq next-entry-exists
872 rolo-entry-regexp nil t 2))
875 (progn (beginning-of-line)
878 (< next-level-len level-len)
880 (<= next-level-len level-len))
881 (setq grouping-end t no-subtree t)
882 (goto-char (point-max)))
885 (hide-subtree) ; And hide multiple entry lines
886 ;; Move to start of next entry at equal
891 (if (re-search-forward
892 rolo-entry-regexp nil t)
893 (progn (beginning-of-line) (point))
895 ;; Remember last expression in 'progn'
896 ;; must always return non-nil.
900 (goto-char grouping-start)
901 (funcall func grouping-start end)
904 (rolo-kill-buffer actual-buf)
909 ;;; Private functions
912 (defun rolo-add-match (rolo-matches-buffer regexp start end)
913 "Insert before point in ROLO-MATCHES-BUFFER an entry matching REGEXP from the current region between START to END."
914 (let ((rolo-buf (current-buffer))
916 (set-buffer (get-buffer-create rolo-matches-buffer))
917 (setq opoint (point))
918 (insert-buffer-substring rolo-buf start end)
919 (rolo-highlight-matches regexp opoint (point))
920 (set-buffer rolo-buf)))
922 (defun rolo-buffer-exists-p (rolo-buf)
923 "Returns buffer given by ROLO-BUF or nil.
924 ROLO-BUF may be a file-name, buffer-name, or buffer."
925 (car (memq (get-buffer (or (and (stringp rolo-buf)
926 (get-file-buffer rolo-buf))
930 (defun rolo-display-to-entry-end ()
931 "Go to end of current entry, ignoring sub-entries."
932 (if (re-search-forward (concat rolo-hdr-regexp "\\|"
933 rolo-entry-regexp) nil t)
934 (progn (beginning-of-line) (point))
935 (goto-char (point-max))))
938 (defun rolo-format-name (name-str first last)
939 "Reverse order of NAME-STR field given my regexp match field FIRST and LAST."
940 (if (match-beginning last)
941 (concat (substring name-str (match-beginning last) (match-end last))
943 (substring name-str (match-beginning first) (match-end first)))))
945 (defun rolo-highlight-matches (regexp start end)
946 "Highlight matches for REGEXP in region from START to END."
947 (if (fboundp 'hproperty:but-add)
948 (let ((hproperty:but-emphasize-p))
951 (while (re-search-forward regexp nil t)
952 (hproperty:but-add (match-beginning 0) (match-end 0)
953 (or rolo-highlight-face
954 hproperty:highlight-face)))))))
956 (defun rolo-kill-buffer (&optional rolo-buf)
957 "Kills optional ROLO-BUF if unchanged and 'rolo-kill-buffers-after-use' is t.
958 Default is current buffer."
959 (or rolo-buf (setq rolo-buf (current-buffer)))
960 (and rolo-kill-buffers-after-use (not (buffer-modified-p rolo-buf))
961 (kill-buffer rolo-buf)))
963 (defun rolo-name-and-email ()
964 "If point is in a mail message, returns list of (name email-addr) of sender.
965 Name is returned as 'last, first-and-middle'."
966 (let ((email) (name) (from))
967 (save-window-excursion
968 (if (or (hmail:lister-p) (hnews:lister-p))
972 (goto-char (point-min))
973 (if (search-forward "\n\n" nil t)
974 (narrow-to-region (point-min) (point)))
975 (setq email (mail-fetch-field "reply-to")
976 from (mail-fetch-field "from")))))
979 ;; Match: email, email (name), email "name"
981 (concat "^\\([^\"<>() \t\n]+\\)"
982 "\\([ \t]*[(\"][ \t]*\\([^\"()]+\\)[ \t]+"
983 "\\([^\" \t()]+\\)[ \t]*[)\"]\\)?[ \t]*$")
985 (setq name (rolo-format-name from 3 4))
986 (or email (setq email (substring from (match-beginning 1)
988 ;; Match: <email>, name <email>, "name" <email>
990 (concat "^\\(\"?\\([^\"<>()\n]+\\)[ \t]+"
991 "\\([^\" \t()<>]+\\)\"?[ \t]+\\)?"
992 "<\\([^\"<>() \t\n]+\\)>[ \t]*$")
994 (setq name (rolo-format-name from 2 3))
995 (or email (setq email (substring from (match-beginning 4)
1000 (defun rolo-name-at ()
1001 "If point is within an entry in 'rolo-display-buffer', returns entry, else nil."
1002 (if (string-equal (buffer-name) rolo-display-buffer)
1004 (if (or (looking-at rolo-entry-regexp)
1005 (progn (end-of-line)
1006 (re-search-backward rolo-entry-regexp nil t)))
1007 (progn (goto-char (match-end 0))
1008 (skip-chars-forward " \t")
1009 (if (or (looking-at "[^ \t\n\^M]+ ?, ?[^ \t\n\^M]+")
1010 (looking-at "\\( ?[^ \t\n\^M]+\\)+"))
1011 (buffer-substring (match-beginning 0)
1012 (match-end 0))))))))
1014 (defun rolo-narrowed-p ()
1015 (or (/= (point-min) 1) (/= (1+ (buffer-size)) (point-max))))
1017 (defun rolo-save-buffer (&optional rolo-buf)
1018 "Saves optional ROLO-BUF if changed and 'rolo-save-buffers-after-use' is t.
1019 Default is current buffer. Used, for example, after a rolo entry is killed."
1020 (or rolo-buf (setq rolo-buf (current-buffer)))
1021 (and rolo-save-buffers-after-use (buffer-modified-p rolo-buf)
1022 (set-buffer rolo-buf) (save-buffer)))
1024 (defun rolo-shrink-window ()
1025 (let* ((lines (count-lines (point-min) (point-max)))
1026 (height (window-height))
1027 (window-min-height 2)
1028 (desired-shrinkage (1- (min (- height lines)))))
1030 (/= desired-shrinkage 0)
1031 (> (frame-height) (1+ height))
1033 (if (< desired-shrinkage 0)
1034 (max desired-shrinkage (- height (/ (frame-height) 2)))
1035 (min desired-shrinkage (- height window-min-height)))))))
1037 (defun rolo-to (name &optional file-list)
1038 "Moves point to entry for NAME within optional FILE-LIST.
1039 'rolo-file-list' is used as default when FILE-LIST is nil.
1040 Leaves point immediately after match for NAME within entry.
1041 Switches internal current buffer but does not alter the frame.
1042 Returns point where matching entry begins or nil if not found."
1043 (or file-list (setq file-list rolo-file-list))
1045 (while (and (not found) file-list)
1046 (setq file (car file-list)
1047 file-list (cdr file-list))
1048 (cond ((and file (or (not (stringp file)) (string= file "")))
1049 (error "(rolo-to): Invalid file: '%s'" file))
1050 ((and (file-exists-p file) (not (file-readable-p file)))
1051 (error "(rolo-to): File not readable: '%s'" file)))
1052 (set-buffer (or (get-file-buffer file) (find-file-noselect file)))
1053 (let ((case-fold-search t) (real-name name) (parent "") (level) end)
1054 (widen) (goto-char 1)
1055 (while (setq end (string-match "/" name))
1057 parent (substring name 0 end)
1058 name (substring name (min (1+ end) (length name))))
1060 (while (and (not level) (search-forward parent nil t))
1064 (concat "\\(" rolo-entry-regexp "\\)[ \t]*"
1065 (regexp-quote parent)))
1066 (setq level (buffer-substring (match-beginning 1)
1069 ((equal name real-name));; Try next file.
1070 (t;; Found parent but not child
1071 (setq buffer-read-only nil)
1072 (rolo-to-buffer (current-buffer))
1073 (error "(rolo-to): '%s' part of name not found in \"%s\"."
1076 (narrow-to-region (point)
1078 (rolo-to-entry-end t level) (point)))))
1079 (goto-char (point-min))
1080 (while (and (search-forward name nil t)
1081 (not (save-excursion
1085 (concat "\\(" rolo-entry-regexp
1087 (regexp-quote name)))
1089 (or found (rolo-kill-buffer))) ;; conditionally kill
1093 (defun rolo-to-buffer (buffer &optional other-window-flag frame)
1095 (cond (hyperb:xemacs-p
1096 (pop-to-buffer buffer other-window-flag
1097 ;; default is to use selected frame
1098 (or frame (selected-frame))))
1099 (t (pop-to-buffer buffer other-window-flag))))
1101 (defun rolo-to-entry-end (&optional include-sub-entries curr-entry-level)
1102 "Goes to end of whole entry if optional INCLUDE-SUB-ENTRIES is non-nil.
1103 CURR-ENTRY-LEVEL is a string whose length is the same as the last found entry
1104 header. If INCLUDE-SUB-ENTRIES is nil, CURR-ENTRY-LEVEL is not needed.
1105 Returns current point."
1106 (while (and (setq next-entry-exists
1107 (re-search-forward rolo-entry-regexp nil t))
1109 (> (- (point) (save-excursion
1112 (length curr-entry-level))))
1113 (if next-entry-exists
1114 (progn (beginning-of-line) (point))
1115 (goto-char (point-max))))
1117 (defun wrolo-mode ()
1118 "Major mode for the rolodex match buffer.
1119 Calls the functions given by `wrolo-mode-hook'.
1122 (setq major-mode 'wrolo-mode
1123 mode-name "Rolodex")
1124 (use-local-map wrolo-mode-map)
1126 ;; Loads menus under non-tty InfoDock, XEmacs or Emacs19; does nothing
1128 (and (not (featurep 'wrolo-menu)) hyperb:window-system
1129 (or hyperb:xemacs-p hyperb:emacs19-p) (require 'wrolo-menu))
1131 (if (not (fboundp 'outline-minor-mode))
1133 (outline-minor-mode 1))
1134 (run-hooks 'wrolo-mode-hook))
1137 ;;; Private variables
1140 (defvar rolo-display-buffer "*Rolodex*"
1141 "Buffer used to display set of last matching rolodex entries.")
1143 (defvar rolo-entry-regexp "^\\*+"
1144 "Regular expression to match the beginning of a rolodex entry.
1145 This pattern must match the beginning of the line. Entries may be nested
1146 through the use of increasingly longer beginning patterns.")
1148 (defconst rolo-hdr-format
1150 "======================================================================\n"
1152 "======================================================================\n")
1153 "Header to insert preceding a file's first rolodex entry match when
1154 file has none of its own. Used with one argument, the file name."
1157 (defconst rolo-hdr-regexp "^==="
1158 "Regular expression to match the first and last lines of rolodex file headers.
1159 This header is inserted into rolo-display-buffer before any entries from the
1162 (defconst rolo-match-regexp nil
1163 "Last regular expression used to search the rolodex.
1164 Nil before a search is done.
1165 String search expressions are converted to regular expressions.")
1167 (defvar *rolo-wconfig* nil
1168 "Saves frame's window configuration prior to a rolodex search.")
1170 (defvar wrolo-mode-map nil
1171 "Keymap for the rolodex match buffer.")
1175 (setq wrolo-mode-map (make-keymap))
1176 (if (fboundp 'set-keymap-name)
1177 (set-keymap-name wrolo-mode-map 'wrolo-mode-map))
1178 (suppress-keymap wrolo-mode-map)
1179 (define-key wrolo-mode-map "<" 'beginning-of-buffer)
1180 (define-key wrolo-mode-map ">" 'end-of-buffer)
1181 (define-key wrolo-mode-map "." 'beginning-of-buffer)
1182 (define-key wrolo-mode-map "," 'end-of-buffer)
1183 (define-key wrolo-mode-map "?" 'describe-mode)
1184 (define-key wrolo-mode-map "\177" 'scroll-down)
1185 (define-key wrolo-mode-map " " 'scroll-up)
1186 (define-key wrolo-mode-map "a" 'show-all)
1187 (define-key wrolo-mode-map "b" 'outline-backward-same-level)
1188 (define-key wrolo-mode-map "e" 'rolo-edit-entry)
1189 (define-key wrolo-mode-map "f" 'outline-forward-same-level)
1190 (define-key wrolo-mode-map "h" 'hide-subtree)
1191 (define-key wrolo-mode-map "m" 'rolo-mail-to)
1192 (define-key wrolo-mode-map "n" 'outline-next-visible-heading)
1193 (define-key wrolo-mode-map "p" 'outline-previous-visible-heading)
1194 (define-key wrolo-mode-map "q" 'rolo-quit)
1195 (define-key wrolo-mode-map "r" 'rolo-previous-match)
1196 (define-key wrolo-mode-map "s" 'show-subtree)
1197 (define-key wrolo-mode-map "\M-s" 'rolo-isearch)
1198 (define-key wrolo-mode-map "t" 'hide-body)
1199 (define-key wrolo-mode-map "\C-i" 'rolo-next-match) ;; {TAB}
1200 (define-key wrolo-mode-map "\M-\C-i" 'rolo-previous-match) ;; {M-TAB}
1201 (define-key wrolo-mode-map "u" 'outline-up-heading)
1206 ;;; wrolo.el ends here