1 ;; -*- mode: emacs-lisp -*-
3 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4 ;; This program is free software; you can redistribute it and/or ;;
5 ;; modify it under the terms of the GNU General Public License as ;;
6 ;; published by the Free Software Foundation; either version 3, or (at ;;
7 ;; your option) any later version. ;;
9 ;; This program is distributed in the hope that it will be useful, but ;;
10 ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;;
11 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;;
12 ;; General Public License for more details. ;;
14 ;; You should have received a copy of the GNU General Public License ;;
15 ;; along with this program. If not, see <http://www.gnu.org/licenses/>. ;;
17 ;; Written by and Copyright (C) Francois Fleuret ;;
18 ;; Contact <francois@fleuret.org> for comments & bug reports ;;
19 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
21 ;; The selector/select function provides a simple interface for
22 ;; selecting an object with on-the-fly pattern matching in a standard
23 ;; buffer (i.e. not in the minibuffer). You can either use it in your
24 ;; own functions or directly use selector/quick-pick-recent or
25 ;; selector/quick-move-in-buffer.
27 ;; For instance, you can add in your .emacs.el
32 ;; (when (load "selector" t t)
33 ;; (define-key global-map [(control x) (control r)] 'selector/quick-pick-recent)
34 ;; (define-key global-map [(control c) (control s)] 'selector/quick-move-in-buffer)
35 ;; (define-key global-map [(control x) (control b)] 'selector/switch-buffer)
39 "Major mode for selection of entries with dynamic pattern matching"
42 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
43 ;; User-configurable variables
44 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
46 (defcustom selector/memorize-entry-only-on-motions t
47 "If non-nil, only the cursor motions memorize the current selection.
48 Restriction of the selection does not. This means that if you
49 change the pattern and then edit it to cancel the change, the
50 cursor will come back to its original location, unless you have
51 explicitely moved it with the arrow keys at some point."
55 (defcustom selector/info-in-mode-line nil
56 "If nil, the pattern is shown in the menu header.
57 Otherwise use the mode-line."
61 (defcustom selector/always-create-buffer nil
62 "If nil, re-use existing similar buffer when possible."
66 (defcustom selector/add-to-file-name-history t
67 "If non-nil, file selected with selector/quick-pick-recent will be added to the mini-buffer filename history."
71 (defcustom selector/mode-hook nil
72 "Hook called at the end of the selector mode initialization."
76 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
78 (defface selector/selection
80 '((t (:background "chartreuse")))
81 "The face for the current selection.")
84 '((t (:foreground "gray70")))
85 "The face for dimmed entries.")
87 (defface selector/date
88 '((t (:foreground "dark violet")))
89 "The face for the dates in selector/quick-pick-recent.")
91 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
93 (defvar selector/pattern
95 "The pattern to match to appear in the selector buffer.")
97 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
99 (defun selector/string-match-all (regexps string)
100 "Return if STRING matches all regular expressions in REGEXPS."
102 (and (string-match (car regexps) string)
103 (selector/string-match-all (cdr regexps) string))
106 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
108 (defun selector/move-highlight-overlay ()
109 "Move the highlight overlay to highlight the current line."
110 (if (get-text-property (point) 'entry)
111 (move-overlay selector/highlight-overlay
112 (or (previous-single-property-change (1+ (point)) 'entry)
114 (or (next-single-property-change (point) 'entry)
116 ;; (move-overlay selector/highlight-overlay 0 0)
117 (delete-overlay selector/highlight-overlay)
120 (unless (and selector/memorize-entry-only-on-motions
122 '(selector/delete-backward-char
123 selector/self-insert-command)))
124 (setq selector/current-entry (get-text-property (point) 'entry)))
127 (defun selector/refresh ()
128 "Erase and reconstruct the content of the current buffer
129 according to `selector/entries' and `selector/pattern'."
131 (let ((inhibit-read-only t)
133 (line-beginning (line-beginning-position))
134 (regexps (mapcar 'regexp-quote (split-string selector/pattern ";")))
136 (nb-shown-entries 0))
141 (when (selector/string-match-all regexps (car s))
142 (setq nb-shown-entries (1+ nb-shown-entries))
143 (if (eq (cdr s) selector/current-entry)
144 (setq newpos (+ (- pos line-beginning) (point))))
146 (propertize (concat (car s) "\n")
148 ;; 'face 'compilation-error
152 (setq newpos (min newpos (point-max)))
153 (setq selector/nb-shown-entries (number-to-string nb-shown-entries))
155 (goto-char (or (and (get-text-property newpos 'entry) newpos)
156 (previous-single-property-change newpos 'entry)
160 (force-mode-line-update)
163 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
165 (defun selector/self-insert-command ()
166 "Insert the last pressed key at the end of `selector/pattern'."
168 (setq selector/pattern (concat selector/pattern
169 (this-command-keys)))
173 (defun selector/delete-backward-char ()
174 "Remove the last character of `selector/pattern'."
176 (when (> (length selector/pattern) 0)
177 (setq selector/pattern (substring selector/pattern 0 -1)))
181 (defun selector/kill-line ()
182 "Move the content of `selector/pattern' to the kill ring."
184 (kill-new selector/pattern t)
185 (setq selector/pattern "")
188 (defun selector/yank (&optional arg)
189 "Append the content of the kill ring to `selector/pattern'."
191 (setq selector/pattern (concat selector/pattern
198 (defun selector/return ()
199 "Call the function specified by `selector/callback' with the
200 entry at point as parameter."
202 (let ((result (get-text-property (point) 'entry))
203 (callback selector/callback))
205 (if result (funcall callback result)
206 (error "No selection"))))
208 (defun selector/goto-next-entry ()
209 "Move point to the next entry."
211 (let ((n (or (next-single-property-change (point) 'entry)
213 (if n (goto-char n))))
215 (defun selector/goto-previous-entry ()
216 "Move point to the previous entry."
218 (let ((n (or (previous-single-property-change (point) 'entry)
219 (previous-single-property-change (point-max) 'entry))))
220 (if n (goto-char n))))
222 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
224 (defun selector/mode ()
225 "Mode for selection of strings. See `selector/select' for a
226 detailed explanation."
228 (unless (boundp 'selector/map)
229 (setq selector/map (make-sparse-keymap))
232 (substitute-key-definition (car p)
234 selector/map global-map)
237 ;; What are the functions to substitute by what
238 '((self-insert-command . selector/self-insert-command)
239 (delete-backward-char . selector/delete-backward-char)
240 (kill-line . selector/kill-line)
241 (yank . selector/yank)
242 (newline . selector/return)
243 ;; (keyboard-quit . kill-this-buffer)
246 (define-key selector/map "\C-g"
249 (define-key selector/map (kbd "TAB")
250 'selector/goto-next-entry)
252 (define-key selector/map [(shift iso-lefttab)]
253 'selector/goto-previous-entry)
257 (setq major-mode 'selector/mode
263 (if selector/info-in-mode-line 'mode-line-format 'header-line-format)
264 '(" " selector/nb-shown-entries "/"
265 selector/nb-total-entries " pattern: " selector/pattern)
268 (buffer-disable-undo)
269 (use-local-map selector/map)
270 (run-hooks 'selector/mode-hook)
273 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
275 (defun selector/select (entries callback &optional name)
276 "Open a new buffer showing dynamically a subset of entries
277 matching a pattern that can be changed by pressing the usual
278 \"insertable\" symbols or backspace. Pressing the enter key
279 validates the selection.
281 Note that the pattern is not a regexp but a series of substrings
282 separated by `;'s that have all to be present.
284 The key mapping is hacked so that the keys associated to
285 `self-insert-command', `delete-backward-char', `kill-line',
286 `yank' and `newline' are associated to functions which do somehow
287 what they are supposed to do. The latter validating the
290 ENTRIES is a list of cons cells, each composed of a string to
291 display and an object to pass as the unique parameter to CALLBACK
292 when the user actually does a selection. The optional NAME
293 parameter specifies the name to give to the buffer.
295 Setting `selector/memorize-entry-only-on-motions' to non-nil
296 means that the entry to keep the cursor on when changing the
297 selection is set only on cursor motions. To show the pattern in
298 the modeline set `selector/info-in-mode-line'. The header line is
299 used by default. To always open a new buffer and not re-use an
300 existing buffer with the same name, set
301 `selector/always-create-buffer' to non-nil.
303 There seems to be header-line refreshing problems with emacs21."
308 (if selector/always-create-buffer 'generate-new-buffer-name 'identity)
309 (or name "*selector*"))))
311 (set (make-local-variable 'selector/entries) entries)
312 (set (make-local-variable 'selector/callback) callback)
313 (set (make-local-variable 'selector/pattern) "")
314 (set (make-local-variable 'selector/highlight-overlay) (make-overlay 0 0))
315 (set (make-local-variable 'selector/current-entry) nil)
316 (set (make-local-variable 'selector/nb-total-entries)
317 (number-to-string (length entries)))
318 (set (make-local-variable 'selector/nb-shown-entries) "?")
320 (overlay-put selector/highlight-overlay 'face 'selector/selection)
322 (add-hook 'post-command-hook 'selector/move-highlight-overlay nil t)
327 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
328 ;; To open recent files
329 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
331 (defun selector/filename-to-string (filename)
332 "Generate the line associated to a filename for `selector/quick-pick-recent'"
335 (if (file-remote-p s)
338 (format-time-string "%b %a %e" (elt (file-attributes s) 5))
339 'face 'selector/date))
345 (if (string-match abbreviated-home-dir s)
347 (substring s 0 (match-end 0)) 'face 'selector/dim)
348 (substring s (match-end 0)))
351 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
352 ;; (if (and (boundp 'selector/previous-filename) selector/previous-filename)
353 ;; (let ((l (abs (compare-strings
354 ;; selector/previous-filename nil nil
355 ;; filename nil nil))))
356 ;; ;; (if (> l 0) (setq l
357 ;; (setq selector/previous-filename filename)
358 ;; (concat (propertize
359 ;; (substring s 0 l) 'face 'selector/dim)
364 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
369 (defun selector/find-file (filename)
370 (if selector/add-to-file-name-history
371 (add-to-history 'file-name-history
372 (replace-regexp-in-string
373 abbreviated-home-dir "~/" filename)
377 (find-file filename))
379 (defun selector/pick-file (filename)
380 "Callback function for `selector/quick-pick-recent'. When
381 called with a universal argument, allows the user to edit the
384 (if current-prefix-arg
385 (selector/find-file (read-file-name
387 (file-name-directory filename)
390 (file-name-nondirectory filename)))
391 (selector/find-file filename)))
393 (defun selector/quick-pick-recent ()
394 "Open a file picked in `recentf-list' with the dynamic
395 pattern-matching search implemented in `selector/select'. With a
396 prefix argument, allows to edit the filename after selection."
399 (unless (and (boundp recentf-mode) recentf-mode)
400 (error "recentf mode must be turned on"))
402 ;; (setq selector/previous-filename "")
408 (cons (selector/filename-to-string s) s))
412 "*selector find-file*"
415 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
416 ;; To search in the current buffer
417 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
419 (defun selector/quick-move-in-buffer ()
420 "Move the cursor in the current buffer to a line selected
421 dynamically with `selector/select'."
427 (goto-char (point-min))
428 (while (< (point) (point-max))
429 (setq l (cons (cons (buffer-substring (point-at-bol) (point-at-eol))
435 "*selector buffer move*"
438 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
439 ;; To switch between buffers
440 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
442 (defun selector/switch-buffer () (interactive)
443 "Select the current buffer dynamically with `selector/select'."
449 (with-current-buffer buffer
450 (let ((name (buffer-name))
452 (file (buffer-file-name))
453 (modified (buffer-modified-p)))
454 (when (not (string-match "^ +" name))
457 (replace-regexp-in-string
463 (if modified "*" "-")
466 (replace-regexp-in-string abbreviated-home-dir
473 (reverse (buffer-list)))
476 "*selector switch-buffer*"
479 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
480 ;; To search among sentences (i.e. between periods, not between \n)
481 ;; This is work in progress, it currently looks kind of ugly but is
482 ;; already useful to navigate in a long article
483 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
485 (defun selector/search-sentence ()
486 "Move the cursor to a sentence chosen dynamically with
490 (let ((sentences nil))
492 (goto-char (point-min))
493 (while (re-search-forward "[^.]+\\." nil t)
494 (let ((s (replace-regexp-in-string "^[ \n]+" ""
495 (match-string-no-properties 0)))
496 (p (match-beginning 0)))
497 (setq s (replace-regexp-in-string "[ \n]+$" "" s))
498 (when (> (length s) 1)
502 (fill-region (point-min) (point-max))
508 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
510 (defface selector/dir
511 '((t (:foreground "red")))
512 "The face for directories.")
514 (defface selector/symlink
515 '((t (:foreground "blue")))
516 "The face for symlinks.")
518 (defun selector/rec-find-file (&optional filename) (interactive)
519 (setq filename (or filename
520 (and (buffer-file-name) (file-name-directory (buffer-file-name)))
523 (if (file-regular-p filename) (find-file filename)
527 (let ((f (car file)))
529 (if (file-regular-p f)
531 (if (file-symlink-p f)
532 (propertize f 'face 'selector/symlink)
533 (propertize f 'face 'selector/dir)))
534 (concat filename "/" f))))
535 (directory-files-and-attributes filename))
536 'selector/rec-find-file
537 (concat "selector " filename)
540 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;