Sync to HEAD
[bpt/emacs.git] / lisp / ffap.el
CommitLineData
6b61353c
KH
1;;; ffap.el --- find file (or url) at point
2
3;; Copyright (C) 1995, 96, 97, 2000, 2004 Free Software Foundation, Inc.
4
b578f267 5;; Author: Michelangelo Grigni <mic@mathcs.emory.edu>
6b61353c 6;; Maintainer: Rajesh Vaidheeswarran <rv@gnu.org>
87e2d039 7;; Created: 29 Mar 1993
f5f727f8 8;; Keywords: files, hypermedia, matching, mouse, convenience
3788c735 9;; X-URL: ftp://ftp.mathcs.emory.edu/pub/mic/emacs/
213d9a4f
RS
10
11;; This file is part of GNU Emacs.
12
13;; GNU Emacs is free software; you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by
15;; the Free Software Foundation; either version 2, or (at your option)
16;; any later version.
17
18;; GNU Emacs 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
21;; GNU General Public License for more details.
22
23;; You should have received a copy of the GNU General Public License
b578f267
EN
24;; along with GNU Emacs; see the file COPYING. If not, write to the
25;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
26;; Boston, MA 02111-1307, USA.
213d9a4f 27
213d9a4f 28\f
87e2d039 29;;; Commentary:
213d9a4f 30;;
87e2d039
RS
31;; Command find-file-at-point replaces find-file. With a prefix, it
32;; behaves exactly like find-file. Without a prefix, it first tries
41d34bed 33;; to guess a default file or URL from the text around the point
87e2d039
RS
34;; (`ffap-require-prefix' swaps these behaviors). This is useful for
35;; following references in situations such as mail or news buffers,
36;; README's, MANIFEST's, and so on. Submit bugs or suggestions with
37;; M-x ffap-bug.
213d9a4f 38;;
25050dab 39;; For the default installation, add this line to your .emacs file:
213d9a4f 40;;
87e2d039 41;; (ffap-bindings) ; do default key bindings
213d9a4f 42;;
87e2d039 43;; ffap-bindings makes the following global key bindings:
213d9a4f 44;;
87e2d039 45;; C-x C-f find-file-at-point (abbreviated as ffap)
28ccdfff 46;; C-x d dired-at-point
87e2d039
RS
47;; C-x 4 f ffap-other-window
48;; C-x 5 f ffap-other-frame
49;; S-mouse-3 ffap-at-mouse
0948761d 50;; C-S-mouse-3 ffap-menu
213d9a4f 51;;
87e2d039
RS
52;; ffap-bindings also adds hooks to make the following local bindings
53;; in vm, gnus, and rmail:
213d9a4f 54;;
0948761d
KH
55;; M-l ffap-next, or ffap-gnus-next in gnus (l == "link")
56;; M-m ffap-menu, or ffap-gnus-menu in gnus (m == "menu")
213d9a4f 57;;
87e2d039
RS
58;; If you do not like these bindings, modify the variable
59;; `ffap-bindings', or write your own.
213d9a4f 60;;
87e2d039
RS
61;; If you use ange-ftp, browse-url, complete, efs, or w3, it is best
62;; to load or autoload them before ffap. If you use ff-paths, load it
63;; afterwards. Try apropos {C-h a ffap RET} to get a list of the many
64;; option variables. In particular, if ffap is slow, try these:
213d9a4f 65;;
87e2d039
RS
66;; (setq ffap-alist nil) ; faster, dumber prompting
67;; (setq ffap-machine-p-known 'accept) ; no pinging
41d34bed 68;; (setq ffap-url-regexp nil) ; disable URL features in ffap
6b61353c 69;; (setq ffap-shell-prompt-regexp nil) ; disable shell prompt stripping
213d9a4f 70;;
3788c735
KH
71;; ffap uses `browse-url' (if found, else `w3-fetch') to fetch URL's.
72;; For a hairier `ffap-url-fetcher', try ffap-url.el (same ftp site).
87e2d039 73;; Also, you can add `ffap-menu-rescan' to various hooks to fontify
71296446 74;; the file and URL references within a buffer.
87e2d039 75
0948761d
KH
76\f
77;;; Change Log:
78;;
79;; The History and Contributors moved to ffap.LOG (same ftp site),
80;; which also has some old examples and commentary from ffap 1.5.
81
82\f
87e2d039 83;;; Todo list:
0948761d 84;; * use kpsewhich
95a85681 85;; * let "/dir/file#key" jump to key (tag or regexp) in /dir/file
87e2d039 86;; * find file of symbol if TAGS is loaded (like above)
0948761d
KH
87;; * break long menus into multiple panes (like imenu?)
88;; * notice node in "(dired)Virtual Dired" (quotes, parentheses, whitespace)
95a85681 89;; * notice "machine.dom blah blah blah dir/file" (how?)
0948761d 90;; * as w3 becomes standard, rewrite to rely more on its functions
87e2d039
RS
91;; * regexp options for ffap-string-at-point, like font-lock (MCOOK)
92;; * v19: could replace `ffap-locate-file' with a quieter `locate-library'
0948761d
KH
93;; * handle "$(VAR)" in Makefiles
94;; * use the font-lock machinery
213d9a4f
RS
95
96\f
97;;; Code:
98
99(provide 'ffap)
100
3788c735
KH
101;; Please do not delete this variable, it is checked in bug reports.
102(defconst ffap-version "1.9-fsf <97/06/25 13:21:41 mic>"
103 "The version of ffap: \"Major.Minor-Build <Timestamp>\"")
87e2d039 104
213d9a4f 105
41d34bed
RS
106(defgroup ffap nil
107 "Find file or URL at point."
3788c735 108 :link '(url-link :tag "URL" "ftp://ftp.mathcs.emory.edu/pub/mic/emacs/")
f5f727f8
DN
109 :group 'matching
110 :group 'convenience)
41d34bed 111
3788c735
KH
112;; The code is organized in pages, separated by formfeed characters.
113;; See the next two pages for standard customization ideas.
114
115\f
116;;; User Variables:
41d34bed 117
0948761d 118(defun ffap-soft-value (name &optional default)
87e2d039
RS
119 "Return value of symbol with NAME, if it is interned.
120Otherwise return nil (or the optional DEFAULT value)."
121 ;; Bug: (ffap-soft-value "nil" 5) --> 5
213d9a4f 122 (let ((sym (intern-soft name)))
0948761d 123 (if (and sym (boundp sym)) (symbol-value sym) default)))
213d9a4f 124
6b61353c
KH
125(defcustom ffap-shell-prompt-regexp
126 ;; This used to test for some shell prompts that don't have a space
127 ;; after them. The common root shell prompt (#) is not listed since it
128 ;; also doubles up as a valid URL character.
129 "[$%><]*"
130 "Paths matching this regexp are stripped off the shell prompt
131If nil, ffap doesn't do shell prompt stripping."
132 :type '(choice (const :tag "Disable" nil)
133 (const :tag "Standard" "[$%><]*")
134 regexp)
135 :group 'ffap)
136
41d34bed 137(defcustom ffap-ftp-regexp
0948761d
KH
138 ;; This used to test for ange-ftp or efs being present, but it should be
139 ;; harmless (and simpler) to give it this value unconditionally.
140 "\\`/[^/:]+:"
95a85681
RS
141 "*File names matching this regexp are treated as remote ffap.
142If nil, ffap neither recognizes nor generates such names."
41d34bed 143 :type '(choice (const :tag "Disable" nil)
0948761d 144 (const :tag "Standard" "\\`/[^/:]+:")
41d34bed
RS
145 regexp)
146 :group 'ffap)
147
148(defcustom ffap-url-unwrap-local t
95a85681 149 "*If non-nil, convert `file:' URL to local file name before prompting."
41d34bed
RS
150 :type 'boolean
151 :group 'ffap)
152
153(defcustom ffap-url-unwrap-remote t
95a85681 154 "*If non-nil, convert `ftp:' URL to remote file name before prompting.
41d34bed
RS
155This is ignored if `ffap-ftp-regexp' is nil."
156 :type 'boolean
157 :group 'ffap)
158
0948761d 159(defcustom ffap-ftp-default-user "anonymous"
95a85681 160 "*User name in ftp file names generated by `ffap-host-to-path'.
0948761d
KH
161Note this name may be omitted if it equals the default
162\(either `efs-default-user' or `ange-ftp-default-user'\)."
163 :type 'string
41d34bed 164 :group 'ffap)
213d9a4f 165
41d34bed 166(defcustom ffap-rfs-regexp
213d9a4f
RS
167 ;; Remote file access built into file system? HP rfa or Andrew afs:
168 "\\`/\\(afs\\|net\\)/."
169 ;; afs only: (and (file-exists-p "/afs") "\\`/afs/.")
95a85681 170 "*Matching file names are treated as remote. Use nil to disable."
41d34bed
RS
171 :type 'regexp
172 :group 'ffap)
213d9a4f
RS
173
174(defvar ffap-url-regexp
175 ;; Could just use `url-nonrelative-link' of w3, if loaded.
176 ;; This regexp is not exhaustive, it just matches common cases.
177 (concat
178 "\\`\\("
179 "news\\(post\\)?:\\|mailto:\\|file:" ; no host ok
180 "\\|"
1d34daae 181 "\\(ftp\\|https?\\|telnet\\|gopher\\|www\\|wais\\)://" ; needs host
213d9a4f
RS
182 "\\)." ; require one more character
183 )
41d34bed 184 "Regexp matching URL's. nil to disable URL features in ffap.")
213d9a4f 185
41d34bed
RS
186(defcustom ffap-foo-at-bar-prefix "mailto"
187 "*Presumed URL prefix type of strings like \"<foo.9z@bar>\".
188Sensible values are nil, \"news\", or \"mailto\"."
0948761d
KH
189 :type '(choice (const "mailto")
190 (const "news")
191 (const :tag "Disable" nil)
192 ;; string -- possible, but not really useful
193 )
41d34bed 194 :group 'ffap)
213d9a4f
RS
195
196\f
0948761d 197;;; Peanut Gallery (More User Variables):
87e2d039 198;;
213d9a4f
RS
199;; Users of ffap occasionally suggest new features. If I consider
200;; those features interesting but not clear winners (a matter of
201;; personal taste) I try to leave options to enable them. Read
87e2d039
RS
202;; through this section for features that you like, put an appropriate
203;; enabler in your .emacs file.
213d9a4f 204
0948761d 205(defcustom ffap-dired-wildcards nil
87e2d039
RS
206 ;; Suggestion from RHOGEE, 07 Jul 1994. Disabled, dired is still
207 ;; available by "C-x C-d <pattern>", and valid filenames may
208 ;; sometimes contain wildcard characters.
213d9a4f 209 "*A regexp matching filename wildcard characters, or nil.
87e2d039 210If `find-file-at-point' gets a filename matching this pattern,
41d34bed 211it passes it on to `dired' instead of `find-file'."
0948761d
KH
212 :type '(choice (const :tag "Disable" nil)
213 (const :tag "Enable" "[*?][^/]*\\'")
214 ;; regexp -- probably not useful
215 )
41d34bed 216 :group 'ffap)
213d9a4f 217
0948761d 218(defcustom ffap-newfile-prompt nil
87e2d039
RS
219 ;; Suggestion from RHOGEE, 11 Jul 1994. Disabled, I think this is
220 ;; better handled by `find-file-not-found-hooks'.
41d34bed
RS
221 "*Whether `find-file-at-point' prompts about a nonexistent file."
222 :type 'boolean
223 :group 'ffap)
213d9a4f 224
41d34bed 225(defcustom ffap-require-prefix nil
87e2d039
RS
226 ;; Suggestion from RHOGEE, 20 Oct 1994.
227 "*If set, reverses the prefix argument to `find-file-at-point'.
228This is nil so neophytes notice ffap. Experts may prefer to disable
41d34bed
RS
229ffap most of the time."
230 :type 'boolean
231 :group 'ffap)
232
233(defcustom ffap-file-finder 'find-file
234 "*The command called by `find-file-at-point' to find a file."
235 :type 'function
236 :group 'ffap)
87e2d039 237(put 'ffap-file-finder 'risky-local-variable t)
213d9a4f 238
41d34bed 239(defcustom ffap-url-fetcher
3788c735
KH
240 (if (fboundp 'browse-url)
241 'browse-url ; rely on browse-url-browser-function
242 'w3-fetch)
87e2d039 243 ;; Remote control references:
213d9a4f
RS
244 ;; http://www.ncsa.uiuc.edu/SDG/Software/XMosaic/remote-control.html
245 ;; http://home.netscape.com/newsref/std/x-remote.html
87e2d039 246 "*A function of one argument, called by ffap to fetch an URL.
3788c735 247Reasonable choices are `w3-fetch' or a `browse-url-*' function.
ee79ced8 248For a fancy alternative, get `ffap-url.el'."
0948761d 249 :type '(choice (const w3-fetch)
3788c735 250 (const browse-url) ; in recent versions of browse-url
0948761d
KH
251 (const browse-url-netscape)
252 (const browse-url-mosaic)
253 function)
41d34bed 254 :group 'ffap)
213d9a4f
RS
255(put 'ffap-url-fetcher 'risky-local-variable t)
256
257\f
3788c735
KH
258;;; Compatibility:
259;;
260;; This version of ffap supports Emacs 20 only, see the ftp site
261;; for a more general version. The following functions are necessary
262;; "leftovers" from the more general version.
263
264(defun ffap-mouse-event nil ; current mouse event, or nil
265 (and (listp last-nonmenu-event) last-nonmenu-event))
266(defun ffap-event-buffer (event)
267 (window-buffer (car (event-start event))))
0948761d
KH
268
269\f
270;;; Find Next Thing in buffer (`ffap-next'):
213d9a4f 271;;
87e2d039
RS
272;; Original ffap-next-url (URL's only) from RPECK 30 Mar 1995. Since
273;; then, broke it up into ffap-next-guess (noninteractive) and
274;; ffap-next (a command). It now work on files as well as url's.
213d9a4f 275
41d34bed 276(defcustom ffap-next-regexp
213d9a4f
RS
277 ;; If you want ffap-next to find URL's only, try this:
278 ;; (and ffap-url-regexp (string-match "\\\\`" ffap-url-regexp)
279 ;; (concat "\\<" (substring ffap-url-regexp 2))))
280 ;;
281 ;; It pays to put a big fancy regexp here, since ffap-guesser is
282 ;; much more time-consuming than regexp searching:
283 "[/:.~a-zA-Z]/\\|@[a-zA-Z][-a-zA-Z0-9]*\\."
41d34bed
RS
284 "*Regular expression governing movements of `ffap-next'."
285 :type 'regexp
286 :group 'ffap)
213d9a4f 287
c9ae9869
RS
288(defvar ffap-next-guess nil
289 "Last value returned by `ffap-next-guess'.")
290
291(defvar ffap-string-at-point-region '(1 1)
292 "List (BEG END), last region returned by `ffap-string-at-point'.")
293
213d9a4f 294(defun ffap-next-guess (&optional back lim)
41d34bed 295 "Move point to next file or URL, and return it as a string.
87e2d039 296If nothing is found, leave point at limit and return nil.
213d9a4f
RS
297Optional BACK argument makes search backwards.
298Optional LIM argument limits the search.
299Only considers strings that match `ffap-next-regexp'."
300 (or lim (setq lim (if back (point-min) (point-max))))
301 (let (guess)
302 (while (not (or guess (eq (point) lim)))
303 (funcall (if back 're-search-backward 're-search-forward)
304 ffap-next-regexp lim 'move)
305 (setq guess (ffap-guesser)))
306 ;; Go to end, so we do not get same guess twice:
307 (goto-char (nth (if back 0 1) ffap-string-at-point-region))
308 (setq ffap-next-guess guess)))
309
310;;;###autoload
311(defun ffap-next (&optional back wrap)
41d34bed 312 "Search buffer for next file or URL, and run ffap.
213d9a4f
RS
313Optional argument BACK says to search backwards.
314Optional argument WRAP says to try wrapping around if necessary.
315Interactively: use a single prefix to search backwards,
316double prefix to wrap forward, triple to wrap backwards.
87e2d039 317Actual search is done by `ffap-next-guess'."
213d9a4f
RS
318 (interactive
319 (cdr (assq (prefix-numeric-value current-prefix-arg)
320 '((1) (4 t) (16 nil t) (64 t t)))))
321 (let ((pt (point))
322 (guess (ffap-next-guess back)))
323 ;; Try wraparound if necessary:
324 (and (not guess) wrap
325 (goto-char (if back (point-max) (point-min)))
326 (setq guess (ffap-next-guess back pt)))
327 (if guess
328 (progn
329 (sit-for 0) ; display point movement
330 (find-file-at-point (ffap-prompter guess)))
331 (goto-char pt) ; restore point
41d34bed 332 (message "No %sfiles or URL's found"
213d9a4f
RS
333 (if wrap "" "more ")))))
334
335(defun ffap-next-url (&optional back wrap)
87e2d039 336 "Like `ffap-next', but search with `ffap-url-regexp'."
213d9a4f
RS
337 (interactive)
338 (let ((ffap-next-regexp ffap-url-regexp))
339 (if (interactive-p)
340 (call-interactively 'ffap-next)
341 (ffap-next back wrap))))
342
343\f
0948761d 344;;; Machines (`ffap-machine-p'):
213d9a4f
RS
345
346;; I cannot decide a "best" strategy here, so these are variables. In
347;; particular, if `Pinging...' is broken or takes too long on your
348;; machine, try setting these all to accept or reject.
41d34bed 349(defcustom ffap-machine-p-local 'reject ; this happens often
ee79ced8 350 "*What `ffap-machine-p' does with hostnames that have no domain.
88b5c6b3 351Value should be a symbol, one of `ping', `accept', and `reject'."
41d34bed
RS
352 :type '(choice (const ping)
353 (const accept)
354 (const reject))
355 :group 'ffap)
ee79ced8
KH
356(defcustom ffap-machine-p-known 'ping ; `accept' for higher speed
357 "*What `ffap-machine-p' does with hostnames that have a known domain.
358Value should be a symbol, one of `ping', `accept', and `reject'.
359See `mail-extr.el' for the known domains."
41d34bed
RS
360 :type '(choice (const ping)
361 (const accept)
362 (const reject))
363 :group 'ffap)
364(defcustom ffap-machine-p-unknown 'reject
ee79ced8
KH
365 "*What `ffap-machine-p' does with hostnames that have an unknown domain.
366Value should be a symbol, one of `ping', `accept', and `reject'.
367See `mail-extr.el' for the known domains."
41d34bed
RS
368 :type '(choice (const ping)
369 (const accept)
370 (const reject))
371 :group 'ffap)
87e2d039
RS
372
373(defun ffap-what-domain (domain)
374 ;; Like what-domain in mail-extr.el, returns string or nil.
375 (require 'mail-extr)
3788c735
KH
376 (let ((ob (or (ffap-soft-value "mail-extr-all-top-level-domains")
377 (ffap-soft-value "all-top-level-domains")))) ; XEmacs
378 (and ob (get (intern-soft (downcase domain) ob) 'domain-name))))
87e2d039
RS
379
380(defun ffap-machine-p (host &optional service quiet strategy)
381 "Decide whether HOST is the name of a real, reachable machine.
382Depending on the domain (none, known, or unknown), follow the strategy
383named by the variable `ffap-machine-p-local', `ffap-machine-p-known',
384or `ffap-machine-p-unknown'. Pinging uses `open-network-stream'.
385Optional SERVICE specifies the port used \(default \"discard\"\).
213d9a4f 386Optional QUIET flag suppresses the \"Pinging...\" message.
87e2d039 387Optional STRATEGY overrides the three variables above.
213d9a4f 388Returned values:
87e2d039
RS
389 t means that HOST answered.
390'accept means the relevant variable told us to accept.
391\"mesg\" means HOST exists, but does not respond for some reason."
392 ;; Try some (Emory local):
393 ;; (ffap-machine-p "ftp" nil nil 'ping)
394 ;; (ffap-machine-p "nonesuch" nil nil 'ping)
395 ;; (ffap-machine-p "ftp.mathcs.emory.edu" nil nil 'ping)
396 ;; (ffap-machine-p "mathcs" 5678 nil 'ping)
397 ;; (ffap-machine-p "foo.bonk" nil nil 'ping)
398 ;; (ffap-machine-p "foo.bonk.com" nil nil 'ping)
213d9a4f 399 (if (or (string-match "[^-a-zA-Z0-9.]" host) ; Illegal chars (?)
87e2d039 400 (not (string-match "[^0-9]" host))) ; 1: a number? 2: quick reject
213d9a4f
RS
401 nil
402 (let* ((domain
403 (and (string-match "\\.[^.]*$" host)
404 (downcase (substring host (1+ (match-beginning 0))))))
87e2d039
RS
405 (what-domain (if domain (ffap-what-domain domain) "Local")))
406 (or strategy
407 (setq strategy
408 (cond ((not domain) ffap-machine-p-local)
409 ((not what-domain) ffap-machine-p-unknown)
410 (t ffap-machine-p-known))))
213d9a4f
RS
411 (cond
412 ((eq strategy 'accept) 'accept)
413 ((eq strategy 'reject) nil)
e75e894b 414 ((not (fboundp 'open-network-stream)) nil)
213d9a4f
RS
415 ;; assume (eq strategy 'ping)
416 (t
417 (or quiet
87e2d039
RS
418 (if (stringp what-domain)
419 (message "Pinging %s (%s)..." host what-domain)
213d9a4f
RS
420 (message "Pinging %s ..." host)))
421 (condition-case error
422 (progn
423 (delete-process
424 (open-network-stream
425 "ffap-machine-p" nil host (or service "discard")))
426 t)
427 (error
428 (let ((mesg (car (cdr error))))
429 (cond
430 ;; v18:
431 ((string-match "^Unknown host" mesg) nil)
432 ((string-match "not responding$" mesg) mesg)
433 ;; v19:
434 ;; (file-error "connection failed" "permission denied"
435 ;; "nonesuch" "ffap-machine-p")
436 ;; (file-error "connection failed" "host is unreachable"
437 ;; "gopher.house.gov" "ffap-machine-p")
438 ;; (file-error "connection failed" "address already in use"
439 ;; "ftp.uu.net" "ffap-machine-p")
440 ((equal mesg "connection failed")
441 (if (equal (nth 2 error) "permission denied")
442 nil ; host does not exist
87e2d039 443 ;; Other errors mean the host exists:
213d9a4f
RS
444 (nth 2 error)))
445 ;; Could be "Unknown service":
446 (t (signal (car error) (cdr error))))))))))))
447
0948761d
KH
448\f
449;;; Possibly Remote Resources:
450
95a85681 451(defun ffap-replace-file-component (fullname name)
0948761d
KH
452 "In remote FULLNAME, replace path with NAME. May return nil."
453 ;; Use ange-ftp or efs if loaded, but do not load them otherwise.
454 (let (found)
455 (mapcar
456 (function (lambda (sym) (and (fboundp sym) (setq found sym))))
457 '(
458 efs-replace-path-component
459 ange-ftp-replace-path-component
460 ange-ftp-replace-name-component
461 ))
462 (and found
95a85681 463 (fset 'ffap-replace-file-component found)
0948761d 464 (funcall found fullname name))))
95a85681 465;; (ffap-replace-file-component "/who@foo.com:/whatever" "/new")
0948761d 466
3788c735 467(defun ffap-file-suffix (file)
ee79ced8 468 "Return trailing `.foo' suffix of FILE, or nil if none."
3788c735
KH
469 (let ((pos (string-match "\\.[^./]*\\'" file)))
470 (and pos (substring file pos nil))))
471
472(defvar ffap-compression-suffixes '(".gz" ".Z") ; .z is mostly dead
473 "List of suffixes tried by `ffap-file-exists-string'.")
474
475(defun ffap-file-exists-string (file &optional nomodify)
476 ;; Early jka-compr versions modified file-exists-p to return the
477 ;; filename, maybe modified by adding a suffix like ".gz". That
478 ;; broke the interface of file-exists-p, so it was later dropped.
479 ;; Here we document and simulate the old behavior.
ee79ced8 480 "Return FILE (maybe modified) if the file exists, else nil.
3788c735
KH
481When using jka-compr (a.k.a. `auto-compression-mode'), the returned
482name may have a suffix added from `ffap-compression-suffixes'.
483The optional NOMODIFY argument suppresses the extra search."
484 (cond
485 ((not file) nil) ; quietly reject nil
486 ((file-exists-p file) file) ; try unmodified first
487 ;; three reasons to suppress search:
488 (nomodify nil)
489 ((not (rassq 'jka-compr-handler file-name-handler-alist)) nil)
490 ((member (ffap-file-suffix file) ffap-compression-suffixes) nil)
491 (t ; ok, do the search
492 (let ((list ffap-compression-suffixes) try ret)
493 (while list
494 (if (file-exists-p (setq try (concat file (car list))))
495 (setq ret try list nil)
496 (setq list (cdr list))))
497 ret))))
0948761d 498
213d9a4f 499(defun ffap-file-remote-p (filename)
ee79ced8 500 "If FILENAME looks remote, return it (maybe slightly improved)."
213d9a4f 501 ;; (ffap-file-remote-p "/user@foo.bar.com:/pub")
95a85681 502 ;; (ffap-file-remote-p "/cssun.mathcs.emory.edu://dir")
87e2d039 503 ;; (ffap-file-remote-p "/ffap.el:80")
213d9a4f
RS
504 (or (and ffap-ftp-regexp
505 (string-match ffap-ftp-regexp filename)
95a85681
RS
506 ;; Convert "/host.com://dir" to "/host:/dir", to handle a dieing
507 ;; practice of advertising ftp files as "host.dom://filename".
213d9a4f 508 (if (string-match "//" filename)
87e2d039
RS
509 ;; (replace-match "/" nil nil filename)
510 (concat (substring filename 0 (1+ (match-beginning 0)))
511 (substring filename (match-end 0)))
213d9a4f
RS
512 filename))
513 (and ffap-rfs-regexp
514 (string-match ffap-rfs-regexp filename)
515 filename)))
516
517(defun ffap-machine-at-point nil
87e2d039
RS
518 "Return machine name at point if it exists, or nil."
519 (let ((mach (ffap-string-at-point 'machine)))
213d9a4f
RS
520 (and (ffap-machine-p mach) mach)))
521
95a85681 522(defsubst ffap-host-to-filename (host)
0948761d 523 "Convert HOST to something like \"/USER@HOST:\" or \"/HOST:\".
87e2d039 524Looks at `ffap-ftp-default-user', returns \"\" for \"localhost\"."
0948761d
KH
525 (if (equal host "localhost")
526 ""
527 (let ((user ffap-ftp-default-user))
528 ;; Avoid including the user if it is same as default:
529 (if (or (equal user (ffap-soft-value "ange-ftp-default-user"))
530 (equal user (ffap-soft-value "efs-default-user")))
531 (setq user nil))
532 (concat "/" user (and user "@") host ":"))))
87e2d039 533
213d9a4f 534(defun ffap-fixup-machine (mach)
95a85681 535 ;; Convert a hostname into an url, an ftp file name, or nil.
213d9a4f
RS
536 (cond
537 ((not (and ffap-url-regexp (stringp mach))) nil)
87e2d039 538 ;; gopher.well.com
213d9a4f
RS
539 ((string-match "\\`gopher[-.]" mach) ; or "info"?
540 (concat "gopher://" mach "/"))
87e2d039 541 ;; www.ncsa.uiuc.edu
213d9a4f
RS
542 ((and (string-match "\\`w\\(ww\\|eb\\)[-.]" mach))
543 (concat "http://" mach "/"))
544 ;; More cases? Maybe "telnet:" for archie?
95a85681 545 (ffap-ftp-regexp (ffap-host-to-filename mach))
213d9a4f
RS
546 ))
547
c9ae9869
RS
548(defvar ffap-newsgroup-regexp "^[a-z]+\\.[-+a-z_0-9.]+$"
549 "Strings not matching this fail `ffap-newsgroup-p'.")
550(defvar ffap-newsgroup-heads ; entirely inadequate
551 '("alt" "comp" "gnu" "misc" "news" "sci" "soc" "talk")
552 "Used by `ffap-newsgroup-p' if gnus is not running.")
553
213d9a4f
RS
554(defun ffap-newsgroup-p (string)
555 "Return STRING if it looks like a newsgroup name, else nil."
556 (and
557 (string-match ffap-newsgroup-regexp string)
558 (let ((htbs '(gnus-active-hashtb gnus-newsrc-hashtb gnus-killed-hashtb))
559 (heads ffap-newsgroup-heads)
560 htb ret)
561 (while htbs
562 (setq htb (car htbs) htbs (cdr htbs))
563 (condition-case nil
564 (progn
565 ;; errs: htb symbol may be unbound, or not a hash-table.
566 ;; gnus-gethash is just a macro for intern-soft.
001f5583
RS
567 (and (symbol-value htb)
568 (intern-soft string (symbol-value htb))
213d9a4f 569 (setq ret string htbs nil))
87e2d039 570 ;; If we made it this far, gnus is running, so ignore "heads":
213d9a4f
RS
571 (setq heads nil))
572 (error nil)))
573 (or ret (not heads)
574 (let ((head (string-match "\\`\\([a-z]+\\)\\." string)))
575 (and head (setq head (substring string 0 (match-end 1)))
576 (member head heads)
577 (setq ret string))))
87e2d039 578 ;; Is there ever a need to modify string as a newsgroup name?
213d9a4f 579 ret)))
213d9a4f 580
87e2d039
RS
581(defsubst ffap-url-p (string)
582 "If STRING looks like an url, return it (maybe improved), else nil."
213d9a4f
RS
583 (let ((case-fold-search t))
584 (and ffap-url-regexp (string-match ffap-url-regexp string)
585 ;; I lied, no improvement:
586 string)))
587
87e2d039
RS
588;; Broke these out of ffap-fixup-url, for use of ffap-url package.
589(defsubst ffap-url-unwrap-local (url)
590 "Return URL as a local file, or nil. Ignores `ffap-url-regexp'."
213d9a4f
RS
591 (and (string-match "\\`\\(file\\|ftp\\):/?\\([^/]\\|\\'\\)" url)
592 (substring url (1+ (match-end 1)))))
87e2d039
RS
593(defsubst ffap-url-unwrap-remote (url)
594 "Return URL as a remote file, or nil. Ignores `ffap-url-regexp'."
213d9a4f
RS
595 (and (string-match "\\`\\(ftp\\|file\\)://\\([^:/]+\\):?\\(/.*\\)" url)
596 (concat
95a85681 597 (ffap-host-to-filename (substring url (match-beginning 2) (match-end 2)))
213d9a4f 598 (substring url (match-beginning 3) (match-end 3)))))
87e2d039 599;; Test: (ffap-url-unwrap-remote "ftp://foo.com/bar.boz")
213d9a4f
RS
600
601(defun ffap-fixup-url (url)
87e2d039 602 "Clean up URL and return it, maybe as a file name."
213d9a4f
RS
603 (cond
604 ((not (stringp url)) nil)
605 ((and ffap-url-unwrap-local (ffap-url-unwrap-local url)))
606 ((and ffap-url-unwrap-remote ffap-ftp-regexp
607 (ffap-url-unwrap-remote url)))
3788c735
KH
608 ((fboundp 'url-normalize-url) ; may autoload url (part of w3)
609 (url-normalize-url url))
610 (url)))
213d9a4f
RS
611
612\f
95a85681 613;;; File Name Handling:
213d9a4f 614;;
0948761d 615;; The upcoming ffap-alist actions need various utilities to prepare
95a85681 616;; and search directories. Too many features here.
0948761d
KH
617
618;; (defun ffap-last (l) (while (cdr l) (setq l (cdr l))) l)
619;; (defun ffap-splice (func inlist)
620;; "Equivalent to (apply 'nconc (mapcar FUNC INLIST)), but less consing."
621;; (let* ((head (cons 17 nil)) (last head))
622;; (while inlist
623;; (setcdr last (funcall func (car inlist)))
624;; (setq last (ffap-last last) inlist (cdr inlist)))
625;; (cdr head)))
213d9a4f
RS
626
627(defun ffap-list-env (env &optional empty)
0948761d
KH
628 "Return a list of strings parsed from environment variable ENV.
629Optional EMPTY is the default list if \(getenv ENV\) is undefined, and
630also is substituted for the first empty-string component, if there is one.
631Uses `path-separator' to separate the path into substrings."
632 ;; We cannot use parse-colon-path (files.el), since it kills
633 ;; "//" entries using file-name-as-directory.
634 ;; Similar: dired-split, TeX-split-string, and RHOGEE's psg-list-env
635 ;; in ff-paths and bib-cite. The EMPTY arg may help mimic kpathsea.
213d9a4f
RS
636 (if (or empty (getenv env)) ; should return something
637 (let ((start 0) match dir ret)
87e2d039 638 (setq env (concat (getenv env) path-separator))
8c1001f6 639 (while (setq match (string-match path-separator env start))
213d9a4f
RS
640 (setq dir (substring env start match) start (1+ match))
641 ;;(and (file-directory-p dir) (not (member dir ret)) ...)
642 (setq ret (cons dir ret)))
643 (setq ret (nreverse ret))
644 (and empty (setq match (member "" ret))
0948761d 645 (progn ; allow string or list here
213d9a4f
RS
646 (setcdr match (append (cdr-safe empty) (cdr match)))
647 (setcar match (or (car-safe empty) empty))))
648 ret)))
649
650(defun ffap-reduce-path (path)
87e2d039 651 "Remove duplicates and non-directories from PATH list."
213d9a4f
RS
652 (let (ret tem)
653 (while path
654 (setq tem path path (cdr path))
87e2d039 655 (if (equal (car tem) ".") (setcar tem ""))
213d9a4f
RS
656 (or (member (car tem) ret)
657 (not (file-directory-p (car tem)))
658 (progn (setcdr tem ret) (setq ret tem))))
659 (nreverse ret)))
660
0948761d
KH
661(defun ffap-all-subdirs (dir &optional depth)
662 "Return list all subdirectories under DIR, starting with itself.
663Directories beginning with \".\" are ignored, and directory symlinks
664are listed but never searched (to avoid loops).
665Optional DEPTH limits search depth."
666 (and (file-exists-p dir)
667 (ffap-all-subdirs-loop (expand-file-name dir) (or depth -1))))
668
669(defun ffap-all-subdirs-loop (dir depth) ; internal
670 (setq depth (1- depth))
671 (cons dir
672 (and (not (eq depth -1))
673 (apply 'nconc
674 (mapcar
675 (function
676 (lambda (d)
677 (cond
678 ((not (file-directory-p d)) nil)
679 ((file-symlink-p d) (list d))
680 (t (ffap-all-subdirs-loop d depth)))))
681 (directory-files dir t "\\`[^.]")
682 )))))
683
684(defvar ffap-kpathsea-depth 1
685 "Bound on depth of subdirectory search in `ffap-kpathsea-expand-path'.
686Set to 0 to avoid all searching, or nil for no limit.")
687
688(defun ffap-kpathsea-expand-path (path)
689 "Replace each \"//\"-suffixed dir in PATH by a list of its subdirs.
690The subdirs begin with the original directory, and the depth of the
691search is bounded by `ffap-kpathsea-depth'. This is intended to mimic
692kpathsea, a library used by some versions of TeX."
693 (apply 'nconc
694 (mapcar
695 (function
696 (lambda (dir)
697 (if (string-match "[^/]//\\'" dir)
698 (ffap-all-subdirs (substring dir 0 -2) ffap-kpathsea-depth)
699 (list dir))))
700 path)))
701
3788c735
KH
702(defun ffap-locate-file (file &optional nosuffix path dir-ok)
703 ;; The Emacs 20 version of locate-library could almost replace this,
6b61353c 704 ;; except it does not let us override the suffix list. The
3788c735 705 ;; compression-suffixes search moved to ffap-file-exists-string.
0948761d
KH
706 "A generic path-searching function, mimics `load' by default.
707Returns path to file that \(load FILE\) would load, or nil.
708Optional NOSUFFIX, if nil or t, is like the fourth argument
709for load: whether to try the suffixes (\".elc\" \".el\" \"\").
710If a nonempty list, it is a list of suffixes to try instead.
3788c735
KH
711Optional PATH is a list of directories instead of `load-path'.
712Optional DIR-OK means that returning a directory is allowed,
713DIR-OK is already implicit if FILE looks like a directory.
714
715This uses ffap-file-exists-string, which may try adding suffixes from
716`ffap-compression-suffixes'."
0948761d 717 (or path (setq path load-path))
3788c735 718 (or dir-ok (setq dir-ok (equal "" (file-name-nondirectory file))))
0948761d
KH
719 (if (file-name-absolute-p file)
720 (setq path (list (file-name-directory file))
721 file (file-name-nondirectory file)))
722 (let ((suffixes-to-try
723 (cond
724 ((consp nosuffix) nosuffix)
725 (nosuffix '(""))
3788c735
KH
726 (t '(".elc" ".el" ""))))
727 suffixes try found)
728 (while path
729 (setq suffixes suffixes-to-try)
730 (while suffixes
731 (setq try (ffap-file-exists-string
732 (expand-file-name
733 (concat file (car suffixes)) (car path))))
734 (if (and try (or dir-ok (not (file-directory-p try))))
735 (setq found try suffixes nil path nil)
736 (setq suffixes (cdr suffixes))))
737 (setq path (cdr path)))
738 found))
0948761d
KH
739
740\f
741;;; Action List (`ffap-alist'):
742;;
743;; These search actions depend on the major-mode or regexps matching
744;; the current name. The little functions and their variables are
745;; deferred to the next section, at some loss of "code locality". A
746;; good example of featuritis. Trim this list for speed.
213d9a4f 747
213d9a4f 748(defvar ffap-alist
c9ae9869 749 '(
0948761d
KH
750 ("" . ffap-completable) ; completion, slow on some systems
751 ("\\.info\\'" . ffap-info) ; gzip.info
752 ("\\`info/" . ffap-info-2) ; info/emacs
753 ("\\`[-a-z]+\\'" . ffap-info-3) ; (emacs)Top [only in the parentheses]
754 ("\\.elc?\\'" . ffap-el) ; simple.el, simple.elc
755 (emacs-lisp-mode . ffap-el-mode) ; rmail, gnus, simple, custom
3788c735 756 ;; (lisp-interaction-mode . ffap-el-mode) ; maybe
0948761d
KH
757 (finder-mode . ffap-el-mode) ; type {C-h p} and try it
758 (help-mode . ffap-el-mode) ; maybe useful
759 (c++-mode . ffap-c-mode) ; search ffap-c-path
760 (cc-mode . ffap-c-mode) ; same
761 ("\\.\\([chCH]\\|cc\\|hh\\)\\'" . ffap-c-mode) ; stdio.h
762 (fortran-mode . ffap-fortran-mode) ; FORTRAN requested by MDB
763 ("\\.[fF]\\'" . ffap-fortran-mode)
764 (tex-mode . ffap-tex-mode) ; search ffap-tex-path
765 (latex-mode . ffap-latex-mode) ; similar
c9ae9869 766 ("\\.\\(tex\\|sty\\|doc\\|cls\\)\\'" . ffap-tex)
0948761d
KH
767 ("\\.bib\\'" . ffap-bib) ; search ffap-bib-path
768 ("\\`\\." . ffap-home) ; .emacs, .bashrc, .profile
769 ("\\`~/" . ffap-lcd) ; |~/misc/ffap.el.Z|
c9ae9869 770 ("^[Rr][Ff][Cc][- #]?\\([0-9]+\\)" ; no $
0948761d
KH
771 . ffap-rfc) ; "100% RFC2100 compliant"
772 (dired-mode . ffap-dired) ; maybe in a subdirectory
773 )
87e2d039
RS
774 "Alist of \(KEY . FUNCTION\) pairs parsed by `ffap-file-at-point'.
775If string NAME at point (maybe \"\") is not a file or url, these pairs
776specify actions to try creating such a string. A pair matches if either
777 KEY is a symbol, and it equals `major-mode', or
778 KEY is a string, it should matches NAME as a regexp.
779On a match, \(FUNCTION NAME\) is called and should return a file, an
780url, or nil. If nil, search the alist for further matches.")
781
213d9a4f 782(put 'ffap-alist 'risky-local-variable t)
0948761d 783
3788c735
KH
784;; Example `ffap-alist' modifications:
785;;
786;; (setq ffap-alist ; remove a feature in `ffap-alist'
787;; (delete (assoc 'c-mode ffap-alist) ffap-alist))
788;;
789;; (setq ffap-alist ; add something to `ffap-alist'
790;; (cons
791;; (cons "^YSN[0-9]+$"
792;; (defun ffap-ysn (name)
793;; (concat
794;; "http://www.physics.uiuc.edu/"
795;; "ysn/httpd/htdocs/ysnarchive/issuefiles/"
796;; (substring name 3) ".html")))
797;; ffap-alist))
798
c9ae9869 799\f
0948761d
KH
800;;; Action Definitions:
801;;
802;; Define various default members of `ffap-alist'.
803
804(defun ffap-completable (name)
805 (let* ((dir (or (file-name-directory name) default-directory))
806 (cmp (file-name-completion (file-name-nondirectory name) dir)))
807 (and cmp (concat dir cmp))))
808
809(defun ffap-home (name) (ffap-locate-file name t '("~")))
c9ae9869
RS
810
811(defun ffap-info (name)
0948761d 812 (ffap-locate-file
c9ae9869
RS
813 name '("" ".info")
814 (or (ffap-soft-value "Info-directory-list")
815 (ffap-soft-value "Info-default-directory-list")
0948761d 816 )))
c9ae9869
RS
817
818(defun ffap-info-2 (name) (ffap-info (substring name 5)))
819
c9ae9869 820(defun ffap-info-3 (name)
0948761d 821 ;; This ignores the node! "(emacs)Top" same as "(emacs)Intro"
c9ae9869
RS
822 (and (equal (ffap-string-around) "()") (ffap-info name)))
823
0948761d 824(defun ffap-el (name) (ffap-locate-file name t))
c9ae9869
RS
825
826(defun ffap-el-mode (name)
0948761d
KH
827 ;; If name == "foo.el" we will skip it, since ffap-el already
828 ;; searched for it once. (This assumes the default ffap-alist.)
c9ae9869 829 (and (not (string-match "\\.el\\'" name))
0948761d
KH
830 (ffap-locate-file name '(".el"))))
831
832(defvar ffap-c-path
833 ;; Need smarter defaults here! Suggestions welcome.
834 '("/usr/include" "/usr/local/include"))
835(defun ffap-c-mode (name)
836 (ffap-locate-file name t ffap-c-path))
837
838(defvar ffap-fortran-path '("../include" "/usr/include"))
839
840(defun ffap-fortran-mode (name)
841 (ffap-locate-file name t ffap-fortran-path))
c9ae9869 842
c9ae9869
RS
843(defvar ffap-tex-path
844 t ; delayed initialization
845 "Path where `ffap-tex-mode' looks for tex files.
846If t, `ffap-tex-init' will initialize this when needed.")
213d9a4f 847
c9ae9869
RS
848(defun ffap-tex-init nil
849 ;; Compute ffap-tex-path if it is now t.
850 (and (eq t ffap-tex-path)
0948761d 851 ;; this may be slow, so say something
c9ae9869
RS
852 (message "Initializing ffap-tex-path ...")
853 (setq ffap-tex-path
854 (ffap-reduce-path
0948761d
KH
855 (cons
856 "."
857 (ffap-kpathsea-expand-path
858 (append
859 (ffap-list-env "TEXINPUTS")
860 ;; (ffap-list-env "BIBINPUTS")
861 (ffap-soft-value
862 "TeX-macro-global" ; AUCTeX
863 '("/usr/local/lib/tex/macros"
864 "/usr/local/lib/tex/inputs")))))))))
c9ae9869
RS
865
866(defun ffap-tex-mode (name)
867 (ffap-tex-init)
0948761d 868 (ffap-locate-file name '(".tex" "") ffap-tex-path))
c9ae9869
RS
869
870(defun ffap-latex-mode (name)
871 (ffap-tex-init)
0948761d
KH
872 ;; only rare need for ""
873 (ffap-locate-file name '(".cls" ".sty" ".tex" "") ffap-tex-path))
c9ae9869
RS
874
875(defun ffap-tex (name)
876 (ffap-tex-init)
0948761d
KH
877 (ffap-locate-file name t ffap-tex-path))
878
879(defvar ffap-bib-path
880 (ffap-list-env "BIBINPUTS"
881 (ffap-reduce-path
882 '(
883 ;; a few wild guesses, need better
884 "/usr/local/lib/tex/macros/bib" ; Solaris?
885 "/usr/lib/texmf/bibtex/bib" ; Linux?
886 ))))
c9ae9869
RS
887
888(defun ffap-bib (name)
0948761d 889 (ffap-locate-file name t ffap-bib-path))
c9ae9869
RS
890
891(defun ffap-dired (name)
892 (let ((pt (point)) dir try)
893 (save-excursion
894 (and (progn
895 (beginning-of-line)
896 (looking-at " *[-d]r[-w][-x][-r][-w][-x][-r][-w][-x] "))
897 (re-search-backward "^ *$" nil t)
898 (re-search-forward "^ *\\([^ \t\n:]*\\):\n *total " pt t)
899 (file-exists-p
900 (setq try
901 (expand-file-name
902 name
903 (buffer-substring
904 (match-beginning 1) (match-end 1)))))
905 try))))
906
907;; Maybe a "Lisp Code Directory" reference:
908(defun ffap-lcd (name)
909 (and
910 (or
911 ;; lisp-dir-apropos output buffer:
912 (string-match "Lisp Code Dir" (buffer-name))
913 ;; Inside an LCD entry like |~/misc/ffap.el.Z|,
914 ;; or maybe the holy LCD-Datafile itself:
915 (member (ffap-string-around) '("||" "|\n")))
916 (concat
917 ;; lispdir.el may not be loaded yet:
95a85681 918 (ffap-host-to-filename
c9ae9869
RS
919 (ffap-soft-value "elisp-archive-host"
920 "archive.cis.ohio-state.edu"))
921 (file-name-as-directory
922 (ffap-soft-value "elisp-archive-directory"
923 "/pub/gnu/emacs/elisp-archive/"))
924 (substring name 2))))
925
926(defvar ffap-rfc-path
95a85681 927 (concat (ffap-host-to-filename "ds.internic.net") "/rfc/rfc%s.txt"))
c9ae9869
RS
928
929(defun ffap-rfc (name)
930 (format ffap-rfc-path
931 (substring name (match-beginning 1) (match-end 1))))
0948761d 932
213d9a4f
RS
933\f
934;;; At-Point Functions:
935
936(defvar ffap-string-at-point-mode-alist
937 '(
87e2d039 938 ;; The default, used when the `major-mode' is not found.
213d9a4f
RS
939 ;; Slightly controversial decisions:
940 ;; * strip trailing "@" and ":"
941 ;; * no commas (good for latex)
87e2d039
RS
942 (file "--:$+<>@-Z_a-z~" "<@" "@>;.,!?:")
943 ;; An url, or maybe a email/news message-id:
0948761d 944 (url "--:=&?$+@-Z_a-z~#,%" "^A-Za-z0-9" ":;.,!?")
87e2d039
RS
945 ;; Find a string that does *not* contain a colon:
946 (nocolon "--9$+<>@-Z_a-z~" "<@" "@>;.,!?")
947 ;; A machine:
948 (machine "-a-zA-Z0-9." "" ".")
949 ;; Mathematica paths: allow backquotes
950 (math-mode ",-:$+<>@-Z_a-z~`" "<" "@>;.,!?`:")
213d9a4f 951 )
87e2d039 952 "Alist of \(MODE CHARS BEG END\), where MODE is a symbol,
ee79ced8
KH
953possibly a major-mode name, or one of the symbol
954`file', `url', `machine', and `nocolon'.
87e2d039
RS
955`ffap-string-at-point' uses the data fields as follows:
9561. find a maximal string of CHARS around point,
9572. strip BEG chars before point from the beginning,
9583. Strip END chars after point from the end.")
213d9a4f 959
213d9a4f
RS
960(defvar ffap-string-at-point nil
961 ;; Added at suggestion of RHOGEE (for ff-paths), 7/24/95.
87e2d039
RS
962 "Last string returned by `ffap-string-at-point'.")
963
964(defun ffap-string-at-point (&optional mode)
965 "Return a string of characters from around point.
ee79ced8 966MODE (defaults to value of `major-mode') is a symbol used to look up string
87e2d039 967syntax parameters in `ffap-string-at-point-mode-alist'.
ee79ced8 968If MODE is not found, we use `file' instead of MODE.
6b61353c 969If the region is active, return a string from the region.
87e2d039
RS
970Sets `ffap-string-at-point' and `ffap-string-at-point-region'."
971 (let* ((args
972 (cdr
973 (or (assq (or mode major-mode) ffap-string-at-point-mode-alist)
974 (assq 'file ffap-string-at-point-mode-alist))))
975 (pt (point))
976 (str
6b61353c
KH
977 (if (and transient-mark-mode mark-active)
978 (buffer-substring
979 (setcar ffap-string-at-point-region (region-beginning))
980 (setcar (cdr ffap-string-at-point-region) (region-end)))
981 (buffer-substring
982 (save-excursion
983 (skip-chars-backward (car args))
984 (skip-chars-forward (nth 1 args) pt)
985 (setcar ffap-string-at-point-region (point)))
986 (save-excursion
987 (skip-chars-forward (car args))
988 (skip-chars-backward (nth 2 args) pt)
989 (setcar (cdr ffap-string-at-point-region) (point)))))))
0948761d 990 (set-text-properties 0 (length str) nil str)
87e2d039 991 (setq ffap-string-at-point str)))
213d9a4f
RS
992
993(defun ffap-string-around nil
994 ;; Sometimes useful to decide how to treat a string.
87e2d039
RS
995 "Return string of two chars around last `ffap-string-at-point'.
996Assumes the buffer has not changed."
213d9a4f
RS
997 (save-excursion
998 (format "%c%c"
999 (progn
1000 (goto-char (car ffap-string-at-point-region))
1001 (preceding-char)) ; maybe 0
1002 (progn
1003 (goto-char (nth 1 ffap-string-at-point-region))
1004 (following-char)) ; maybe 0
1005 )))
1006
87e2d039
RS
1007(defun ffap-copy-string-as-kill (&optional mode)
1008 ;; Requested by MCOOK. Useful?
1009 "Call `ffap-string-at-point', and copy result to `kill-ring'."
1010 (interactive)
1011 (let ((str (ffap-string-at-point mode)))
1012 (if (equal "" str)
1013 (message "No string found around point.")
1014 (kill-new str)
1015 ;; Older: (apply 'copy-region-as-kill ffap-string-at-point-region)
1016 (message "Copied to kill ring: %s" str))))
1017
213d9a4f 1018(defun ffap-url-at-point nil
87e2d039
RS
1019 "Return url from around point if it exists, or nil."
1020 ;; Could use w3's url-get-url-at-point instead. Both handle "URL:",
1021 ;; ignore non-relative links, trim punctuation. The other will
1022 ;; actually look back if point is in whitespace, but I would rather
0948761d 1023 ;; ffap be less aggressive in such situations.
213d9a4f
RS
1024 (and
1025 ffap-url-regexp
1026 (or
0948761d
KH
1027 ;; In a w3 buffer button?
1028 (and (eq major-mode 'w3-mode)
1029 ;; interface recommended by wmperry:
1030 (w3-view-this-url t))
213d9a4f 1031 ;; Is there a reason not to strip trailing colon?
87e2d039 1032 (let ((name (ffap-string-at-point 'url)))
213d9a4f
RS
1033 (cond
1034 ((string-match "^url:" name) (setq name (substring name 4)))
0948761d 1035 ((and (string-match "\\`[^:</>@]+@[^:</>@]+[a-zA-Z0-9]\\'" name)
213d9a4f 1036 ;; "foo@bar": could be "mailto" or "news" (a Message-ID).
0948761d
KH
1037 ;; Without "<>" it must be "mailto". Otherwise could be
1038 ;; either, so consult `ffap-foo-at-bar-prefix'.
213d9a4f 1039 (let ((prefix (if (and (equal (ffap-string-around) "<>")
0948761d 1040 ;; Expect some odd characters:
213d9a4f
RS
1041 (string-match "[$.0-9].*[$.0-9].*@" name))
1042 ;; Could be news:
87e2d039 1043 ffap-foo-at-bar-prefix
213d9a4f
RS
1044 "mailto")))
1045 (and prefix (setq name (concat prefix ":" name))))))
1046 ((ffap-newsgroup-p name) (setq name (concat "news:" name)))
1047 ((and (string-match "\\`[a-z0-9]+\\'" name) ; <mic> <root> <nobody>
1048 (equal (ffap-string-around) "<>")
1049 ;; (ffap-user-p name):
1050 (not (string-match "~" (expand-file-name (concat "~" name))))
1051 )
1052 (setq name (concat "mailto:" name)))
1053 )
1054 (and (ffap-url-p name) name)
1055 ))))
1056
1057(defvar ffap-gopher-regexp
1058 "^.*\\<\\(Type\\|Name\\|Path\\|Host\\|Port\\) *= *\\(.*\\) *$"
1059 "Regexp Matching a line in a gopher bookmark (maybe indented).
87e2d039 1060The two subexpressions are the KEY and VALUE.")
213d9a4f
RS
1061
1062(defun ffap-gopher-at-point nil
1063 "If point is inside a gopher bookmark block, return its url."
87e2d039 1064 ;; `gopher-parse-bookmark' from gopher.el is not so robust
213d9a4f
RS
1065 (save-excursion
1066 (beginning-of-line)
1067 (if (looking-at ffap-gopher-regexp)
1068 (progn
1069 (while (and (looking-at ffap-gopher-regexp) (not (bobp)))
1070 (forward-line -1))
1071 (or (looking-at ffap-gopher-regexp) (forward-line 1))
1072 (let ((type "1") name path host (port "70"))
1073 (while (looking-at ffap-gopher-regexp)
1074 (let ((var (intern
1075 (downcase
1076 (buffer-substring (match-beginning 1)
1077 (match-end 1)))))
1078 (val (buffer-substring (match-beginning 2)
1079 (match-end 2))))
1080 (set var val)
1081 (forward-line 1)))
1082 (if (and path (string-match "^ftp:.*@" path))
1083 (concat "ftp://"
1084 (substring path 4 (1- (match-end 0)))
1085 (substring path (match-end 0)))
1086 (and (= (length type) 1)
1087 host;; (ffap-machine-p host)
1088 (concat "gopher://" host
1089 (if (equal port "70") "" (concat ":" port))
1090 "/" type path))))))))
1091
1092(defvar ffap-ftp-sans-slash-regexp
1093 (and
1094 ffap-ftp-regexp
87e2d039 1095 ;; Note: by now, we know it is not an url.
213d9a4f
RS
1096 ;; Icky regexp avoids: default: 123: foo::bar cs:pub
1097 ;; It does match on: mic@cs: cs:/pub mathcs.emory.edu: (point at end)
213d9a4f 1098 "\\`\\([^:@]+@[^:@]+:\\|[^@.:]+\\.[^@:]+:\\|[^:]+:[~/]\\)\\([^:]\\|\\'\\)")
95a85681 1099 "Strings matching this are coerced to ftp file names by ffap.
213d9a4f
RS
1100That is, ffap just prepends \"/\". Set to nil to disable.")
1101
1102(defun ffap-file-at-point nil
1103 "Return filename from around point if it exists, or nil.
1104Existence test is skipped for names that look remote.
1105If the filename is not obvious, it also tries `ffap-alist',
87e2d039
RS
1106which may actually result in an url rather than a filename."
1107 ;; Note: this function does not need to look for url's, just
213d9a4f 1108 ;; filenames. On the other hand, it is responsible for converting
95a85681 1109 ;; a pseudo-url "site.com://dir" to an ftp file name
213d9a4f
RS
1110 (let* ((case-fold-search t) ; url prefixes are case-insensitive
1111 (data (match-data))
87e2d039 1112 (string (ffap-string-at-point)) ; uses mode alist
213d9a4f 1113 (name
87e2d039
RS
1114 (or (condition-case nil
1115 (and (not (string-match "//" string)) ; foo.com://bar
1116 (substitute-in-file-name string))
1117 (error nil))
1118 string))
213d9a4f
RS
1119 (abs (file-name-absolute-p name))
1120 (default-directory default-directory))
1121 (unwind-protect
1122 (cond
1123 ;; Immediate rejects (/ and // are too common in C++):
8005ea3f
RS
1124 ((member name '("" "/" "//" ".")) nil)
1125 ;; Immediately test local filenames. If default-directory is
1126 ;; remote, you probably already have a connection.
1127 ((and (not abs) (ffap-file-exists-string name)))
1128 ;; Try stripping off line numbers; good for compilation/grep output.
1129 ((and (not abs) (string-match ":[0-9]" name)
1130 (ffap-file-exists-string (substring name 0 (match-beginning 0)))))
6b61353c
KH
1131 ;; Try stripping off prominent (non-root - #) shell prompts
1132 ;; if the ffap-shell-prompt-regexp is non-nil.
1133 ((and ffap-shell-prompt-regexp
1134 (not abs) (string-match ffap-shell-prompt-regexp name)
1135 (ffap-file-exists-string (substring name (match-end 0)))))
213d9a4f
RS
1136 ;; Accept remote names without actual checking (too slow):
1137 ((if abs
1138 (ffap-file-remote-p name)
95a85681 1139 ;; Try adding a leading "/" (common omission in ftp file names):
213d9a4f
RS
1140 (and
1141 ffap-ftp-sans-slash-regexp
1142 (string-match ffap-ftp-sans-slash-regexp name)
1143 (ffap-file-remote-p (concat "/" name)))))
1144 ;; Ok, not remote, try the existence test even if it is absolute:
1145 ((and abs (ffap-file-exists-string name)))
87e2d039
RS
1146 ;; If it contains a colon, get rid of it (and return if exists)
1147 ((and (string-match path-separator name)
1148 (setq name (ffap-string-at-point 'nocolon))
1149 (ffap-file-exists-string name)))
213d9a4f
RS
1150 ;; File does not exist, try the alist:
1151 ((let ((alist ffap-alist) tem try case-fold-search)
1152 (while (and alist (not try))
1153 (setq tem (car alist) alist (cdr alist))
1154 (if (or (eq major-mode (car tem))
1155 (and (stringp (car tem))
1156 (string-match (car tem) name)))
0948761d
KH
1157 (and (setq try
1158 (condition-case nil
1159 (funcall (cdr tem) name)
1160 (error nil)))
213d9a4f
RS
1161 (setq try (or
1162 (ffap-url-p try) ; not a file!
1163 (ffap-file-remote-p try)
1164 (ffap-file-exists-string try))))))
1165 try))
1166 ;; Alist failed? Try to guess an active remote connection
1167 ;; from buffer variables, and try once more, both as an
95a85681 1168 ;; absolute and relative file name on that remote host.
213d9a4f
RS
1169 ((let* (ffap-rfs-regexp ; suppress
1170 (remote-dir
1171 (cond
1172 ((ffap-file-remote-p default-directory))
1173 ((and (eq major-mode 'internal-ange-ftp-mode)
1174 (string-match "^\\*ftp \\(.*\\)@\\(.*\\)\\*$"
1175 (buffer-name)))
1176 (concat "/" (substring (buffer-name) 5 -1) ":"))
1177 ;; This is too often a bad idea:
1178 ;;((and (eq major-mode 'w3-mode)
1179 ;; (stringp url-current-server))
1180 ;; (host-to-ange-path url-current-server))
1181 )))
1182 (and remote-dir
1183 (or
1184 (and (string-match "\\`\\(/?~?ftp\\)/" name)
1185 (ffap-file-exists-string
95a85681 1186 (ffap-replace-file-component
213d9a4f
RS
1187 remote-dir (substring name (match-end 1)))))
1188 (ffap-file-exists-string
95a85681 1189 (ffap-replace-file-component remote-dir name))))))
6b61353c
KH
1190 ;; Try all parent directories by deleting the trailing directory
1191 ;; name until existing directory is found or name stops changing
1192 ((let ((dir name))
1193 (while (and dir
1194 (not (ffap-file-exists-string dir))
1195 (not (equal dir (setq dir (file-name-directory
1196 (directory-file-name dir)))))))
1197 (ffap-file-exists-string dir)))
213d9a4f 1198 )
3c2c6be2 1199 (set-match-data data))))
213d9a4f 1200\f
0948761d 1201;;; Prompting (`ffap-read-file-or-url'):
213d9a4f 1202;;
87e2d039
RS
1203;; We want to complete filenames as in read-file-name, but also url's
1204;; which read-file-name-internal would truncate at the "//" string.
1205;; The solution here is to replace read-file-name-internal with
1206;; `ffap-read-file-or-url-internal', which checks the minibuffer
1207;; contents before attempting to complete filenames.
213d9a4f
RS
1208
1209(defun ffap-read-file-or-url (prompt guess)
87e2d039 1210 "Read file or url from minibuffer, with PROMPT and initial GUESS."
213d9a4f 1211 (or guess (setq guess default-directory))
87e2d039 1212 (let (dir)
213d9a4f
RS
1213 ;; Tricky: guess may have or be a local directory, like "w3/w3.elc"
1214 ;; or "w3/" or "../el/ffap.el" or "../../../"
87e2d039 1215 (or (ffap-url-p guess)
213d9a4f
RS
1216 (progn
1217 (or (ffap-file-remote-p guess)
3788c735
KH
1218 (setq guess
1219 (abbreviate-file-name (expand-file-name guess))
1220 ))
213d9a4f 1221 (setq dir (file-name-directory guess))))
a22f0735
RS
1222 (let ((minibuffer-completing-file-name t))
1223 (setq guess
1224 (completing-read
1225 prompt
1226 'ffap-read-file-or-url-internal
1227 dir
1228 nil
1229 (if dir (cons guess (length dir)) guess)
1230 (list 'file-name-history))))
87e2d039
RS
1231 ;; Do file substitution like (interactive "F"), suggested by MCOOK.
1232 (or (ffap-url-p guess) (setq guess (substitute-in-file-name guess)))
1233 ;; Should not do it on url's, where $ is a common (VMS?) character.
1234 ;; Note: upcoming url.el package ought to handle this automatically.
1235 guess))
213d9a4f
RS
1236
1237(defun ffap-read-url-internal (string dir action)
87e2d039
RS
1238 "Complete url's from history, treating given string as valid."
1239 (let ((hist (ffap-soft-value "url-global-history-hash-table")))
213d9a4f
RS
1240 (cond
1241 ((not action)
1242 (or (try-completion string hist) string))
1243 ((eq action t)
1244 (or (all-completions string hist) (list string)))
87e2d039
RS
1245 ;; action == lambda, documented where? Tests whether string is a
1246 ;; valid "match". Let us always say yes.
1247 (t t))))
213d9a4f
RS
1248
1249(defun ffap-read-file-or-url-internal (string dir action)
d4021fd9
GM
1250 (unless dir
1251 (setq dir default-directory))
1252 (unless string
1253 (setq string default-directory))
213d9a4f
RS
1254 (if (ffap-url-p string)
1255 (ffap-read-url-internal string dir action)
1256 (read-file-name-internal string dir action)))
1257
87e2d039
RS
1258;; The rest of this page is just to work with package complete.el.
1259;; This code assumes that you load ffap.el after complete.el.
1260;;
1261;; We must inform complete about whether our completion function
6b61353c 1262;; will do filename style completion.
87e2d039
RS
1263
1264(defun ffap-complete-as-file-p nil
1265 ;; Will `minibuffer-completion-table' complete the minibuffer
1266 ;; contents as a filename? Assumes the minibuffer is current.
1267 ;; Note: t and non-nil mean somewhat different reasons.
1268 (if (eq minibuffer-completion-table 'ffap-read-file-or-url-internal)
1269 (not (ffap-url-p (buffer-string))) ; t
9925c419 1270 (and minibuffer-completing-file-name '(t)))) ;list
87e2d039 1271
213d9a4f
RS
1272(and
1273 (featurep 'complete)
87e2d039
RS
1274 (if (boundp 'PC-completion-as-file-name-predicate)
1275 ;; modern version of complete.el, just set the variable:
6b61353c 1276 (setq PC-completion-as-file-name-predicate 'ffap-complete-as-file-p)))
213d9a4f
RS
1277
1278\f
0948761d 1279;;; Highlighting (`ffap-highlight'):
213d9a4f
RS
1280;;
1281;; Based on overlay highlighting in Emacs 19.28 isearch.el.
1282
33514810 1283(defvar ffap-highlight t
213d9a4f
RS
1284 "If non-nil, ffap highlights the current buffer substring.")
1285
0948761d
KH
1286(defvar ffap-highlight-overlay nil
1287 "Overlay used by `ffap-highlight'.")
213d9a4f
RS
1288
1289(defun ffap-highlight (&optional remove)
87e2d039
RS
1290 "If `ffap-highlight' is set, highlight the guess in this buffer.
1291That is, the last buffer substring found by `ffap-string-at-point'.
213d9a4f 1292Optional argument REMOVE means to remove any such highlighting.
87e2d039 1293Uses the face `ffap' if it is defined, or else `highlight'."
213d9a4f 1294 (cond
0948761d
KH
1295 (remove
1296 (and ffap-highlight-overlay
3788c735
KH
1297 (delete-overlay ffap-highlight-overlay))
1298 )
213d9a4f 1299 ((not ffap-highlight) nil)
87e2d039 1300 (ffap-highlight-overlay
3788c735
KH
1301 (move-overlay
1302 ffap-highlight-overlay
1303 (car ffap-string-at-point-region)
1304 (nth 1 ffap-string-at-point-region)
1305 (current-buffer)))
213d9a4f 1306 (t
0948761d 1307 (setq ffap-highlight-overlay
3788c735
KH
1308 (apply 'make-overlay ffap-string-at-point-region))
1309 (overlay-put ffap-highlight-overlay 'face
e5eee690 1310 (if (facep 'ffap) 'ffap 'highlight)))))
87e2d039 1311
213d9a4f 1312\f
3788c735 1313;;; Main Entrance (`find-file-at-point' == `ffap'):
213d9a4f
RS
1314
1315(defun ffap-guesser nil
0948761d 1316 "Return file or URL or nil, guessed from text around point."
213d9a4f
RS
1317 (or (and ffap-url-regexp
1318 (ffap-fixup-url (or (ffap-url-at-point)
1319 (ffap-gopher-at-point))))
1320 (ffap-file-at-point) ; may yield url!
1321 (ffap-fixup-machine (ffap-machine-at-point))))
1322
1323(defun ffap-prompter (&optional guess)
1324 ;; Does guess and prompt step for find-file-at-point.
87e2d039 1325 ;; Extra complication for the temporary highlighting.
213d9a4f 1326 (unwind-protect
3788c735
KH
1327 ;; This catch will let ffap-alist entries do their own prompting
1328 ;; and then maybe skip over this prompt (ff-paths, for example).
1329 (catch 'ffap-prompter
1330 (ffap-read-file-or-url
1331 (if ffap-url-regexp "Find file or URL: " "Find file: ")
1332 (prog1
1333 (setq guess (or guess (ffap-guesser))) ; using ffap-alist here
1334 (and guess (ffap-highlight))
1335 )))
213d9a4f
RS
1336 (ffap-highlight t)))
1337
1338;;;###autoload
1339(defun find-file-at-point (&optional filename)
0948761d
KH
1340 "Find FILENAME, guessing a default from text around point.
1341If `ffap-url-regexp' is not nil, the FILENAME may also be an URL.
1342With a prefix, this command behaves exactly like `ffap-file-finder'.
213d9a4f 1343If `ffap-require-prefix' is set, the prefix meaning is reversed.
0948761d
KH
1344See also the variables `ffap-dired-wildcards', `ffap-newfile-prompt',
1345and the functions `ffap-file-at-point' and `ffap-url-at-point'.
213d9a4f 1346
87e2d039 1347See <ftp://ftp.mathcs.emory.edu/pub/mic/emacs/> for latest version."
213d9a4f
RS
1348 (interactive)
1349 (if (and (interactive-p)
1350 (if ffap-require-prefix (not current-prefix-arg)
1351 current-prefix-arg))
1352 ;; Do exactly the ffap-file-finder command, even the prompting:
87e2d039
RS
1353 (let (current-prefix-arg) ; we already interpreted it
1354 (call-interactively ffap-file-finder))
213d9a4f
RS
1355 (or filename (setq filename (ffap-prompter)))
1356 (cond
1357 ((ffap-url-p filename)
87e2d039
RS
1358 (let (current-prefix-arg) ; w3 2.3.25 bug, reported by KPC
1359 (funcall ffap-url-fetcher filename)))
213d9a4f 1360 ;; This junk more properly belongs in a modified ffap-file-finder:
87e2d039
RS
1361 ((and ffap-dired-wildcards
1362 (string-match ffap-dired-wildcards filename))
213d9a4f
RS
1363 (dired filename))
1364 ((or (not ffap-newfile-prompt)
1365 (file-exists-p filename)
1366 (y-or-n-p "File does not exist, create buffer? "))
1367 (funcall ffap-file-finder
1368 ;; expand-file-name fixes "~/~/.emacs" bug sent by CHUCKR.
1369 (expand-file-name filename)))
1370 ;; User does not want to find a non-existent file:
1371 ((signal 'file-error (list "Opening file buffer"
1372 "no such file or directory"
1373 filename))))))
1374
0948761d 1375;; Shortcut: allow {M-x ffap} rather than {M-x find-file-at-point}.
22ac7ca0
MR
1376;;;###autoload
1377(defalias 'ffap 'find-file-at-point)
1378
213d9a4f 1379\f
0948761d 1380;;; Menu support (`ffap-menu'):
213d9a4f
RS
1381
1382(defvar ffap-menu-regexp nil
87e2d039 1383 "*If non-nil, overrides `ffap-next-regexp' during `ffap-menu'.
213d9a4f 1384Make this more restrictive for faster menu building.
0948761d 1385For example, try \":/\" for URL (and some ftp) references.")
213d9a4f
RS
1386
1387(defvar ffap-menu-alist nil
87e2d039 1388 "Buffer local cache of menu presented by `ffap-menu'.")
213d9a4f
RS
1389(make-variable-buffer-local 'ffap-menu-alist)
1390
87e2d039 1391(defvar ffap-menu-text-plist
3788c735 1392 (cond
33514810
EZ
1393 ((display-mouse-p) '(face bold mouse-face highlight)) ; keymap <mousy-map>
1394 (t nil))
87e2d039
RS
1395 "Text properties applied to strings found by `ffap-menu-rescan'.
1396These properties may be used to fontify the menu references.")
1397
213d9a4f
RS
1398;;;###autoload
1399(defun ffap-menu (&optional rescan)
87e2d039
RS
1400 "Put up a menu of files and urls mentioned in this buffer.
1401Then set mark, jump to choice, and try to fetch it. The menu is
1402cached in `ffap-menu-alist', and rebuilt by `ffap-menu-rescan'.
1403The optional RESCAN argument \(a prefix, interactively\) forces
1404a rebuild. Searches with `ffap-menu-regexp'."
213d9a4f
RS
1405 (interactive "P")
1406 ;; (require 'imenu) -- no longer used, but roughly emulated
1407 (if (or (not ffap-menu-alist) rescan
1408 ;; or if the first entry is wrong:
1409 (and ffap-menu-alist
1410 (let ((first (car ffap-menu-alist)))
1411 (save-excursion
1412 (goto-char (cdr first))
1413 (not (equal (car first) (ffap-guesser)))))))
1414 (ffap-menu-rescan))
1415 ;; Tail recursive:
1416 (ffap-menu-ask
1417 (if ffap-url-regexp "Find file or URL" "Find file")
1418 (cons (cons "*Rescan Buffer*" -1) ffap-menu-alist)
1419 'ffap-menu-cont))
1420
1421(defun ffap-menu-cont (choice) ; continuation of ffap-menu
1422 (if (< (cdr choice) 0)
1423 (ffap-menu t) ; *Rescan*
1424 (push-mark)
1425 (goto-char (cdr choice))
1426 ;; Momentary highlight:
1427 (unwind-protect
1428 (progn
1429 (and ffap-highlight (ffap-guesser) (ffap-highlight))
1430 (sit-for 0) ; display
1431 (find-file-at-point (car choice)))
1432 (ffap-highlight t))))
1433
1434(defun ffap-menu-ask (title alist cont)
1435 "Prompt from a menu of choices, and then apply some action.
0948761d 1436Arguments are TITLE, ALIST, and CONT \(a continuation function\).
213d9a4f
RS
1437This uses either a menu or the minibuffer depending on invocation.
1438The TITLE string is used as either the prompt or menu title.
ee79ced8 1439Each ALIST entry looks like (STRING . DATA) and defines one choice.
0948761d
KH
1440Function CONT is applied to the entry chosen by the user."
1441 ;; Note: this function is used with a different continuation
1442 ;; by the ffap-url add-on package.
1443 ;; Could try rewriting to use easymenu.el or lmenu.el.
1444 (let (choice)
1445 (cond
1446 ;; Emacs mouse:
1447 ((and (fboundp 'x-popup-menu) (ffap-mouse-event))
1448 (setq choice
1449 (x-popup-menu
1450 t
1451 (list "" (cons title
1452 (mapcar (function (lambda (i) (cons (car i) i)))
1453 alist))))))
1454 ;; minibuffer with completion buffer:
1455 (t
1456 (let ((minibuffer-setup-hook 'minibuffer-completion-help))
1457 ;; Bug: prompting may assume unique strings, no "".
1458 (setq choice
1459 (completing-read
1460 (format "%s (default %s): " title (car (car alist)))
1461 alist nil t
1462 ;; (cons (car (car alist)) 0)
1463 nil)))
1464 (sit-for 0) ; redraw original screen
1465 ;; Convert string to its entry, or else the default:
1466 (setq choice (or (assoc choice alist) (car alist))))
1467 )
1468 (if choice
1469 (funcall cont choice)
1470 (message "No choice made!") ; possible with menus
1471 nil)))
213d9a4f
RS
1472
1473(defun ffap-menu-rescan nil
87e2d039
RS
1474 "Search buffer for `ffap-menu-regexp' to build `ffap-menu-alist'.
1475Applies `ffap-menu-text-plist' text properties at all matches."
213d9a4f
RS
1476 (interactive)
1477 (let ((ffap-next-regexp (or ffap-menu-regexp ffap-next-regexp))
0948761d
KH
1478 (range (- (point-max) (point-min)))
1479 (mod (buffer-modified-p)) ; was buffer modified?
87e2d039 1480 buffer-read-only ; to set text-properties
0948761d 1481 item
87e2d039
RS
1482 ;; Avoid repeated searches of the *mode-alist:
1483 (major-mode (if (assq major-mode ffap-string-at-point-mode-alist)
1484 major-mode
0948761d 1485 'file)))
213d9a4f 1486 (setq ffap-menu-alist nil)
0948761d
KH
1487 (unwind-protect
1488 (save-excursion
1489 (goto-char (point-min))
1490 (while (setq item (ffap-next-guess))
1491 (setq ffap-menu-alist (cons (cons item (point)) ffap-menu-alist))
1492 (add-text-properties (car ffap-string-at-point-region) (point)
1493 ffap-menu-text-plist)
1494 (message "Scanning...%2d%% <%s>"
1495 (/ (* 100 (- (point) (point-min))) range) item)))
1496 (or mod (set-buffer-modified-p nil))))
213d9a4f
RS
1497 (message "Scanning...done")
1498 ;; Remove duplicates.
1499 (setq ffap-menu-alist ; sort by item
1500 (sort ffap-menu-alist
1501 (function
1502 (lambda (a b) (string-lessp (car a) (car b))))))
0948761d 1503 (let ((ptr ffap-menu-alist)) ; remove duplicates
213d9a4f
RS
1504 (while (cdr ptr)
1505 (if (equal (car (car ptr)) (car (car (cdr ptr))))
1506 (setcdr ptr (cdr (cdr ptr)))
1507 (setq ptr (cdr ptr)))))
1508 (setq ffap-menu-alist ; sort by position
1509 (sort ffap-menu-alist
1510 (function
1511 (lambda (a b) (< (cdr a) (cdr b)))))))
1512
1513\f
0948761d 1514;;; Mouse Support (`ffap-at-mouse'):
213d9a4f 1515;;
87e2d039 1516;; See the suggested binding in ffap-bindings (near eof).
213d9a4f 1517
0948761d
KH
1518(defvar ffap-at-mouse-fallback nil ; ffap-menu? too time-consuming
1519 "Command invoked by `ffap-at-mouse' if nothing found at click, or nil.
1520Ignored when `ffap-at-mouse' is called programmatically.")
213d9a4f
RS
1521(put 'ffap-at-mouse-fallback 'risky-local-variable t)
1522
0948761d 1523;;;###autoload
213d9a4f 1524(defun ffap-at-mouse (e)
0948761d 1525 "Find file or url guessed from text around mouse click.
3788c735
KH
1526Interactively, calls `ffap-at-mouse-fallback' if no guess is found.
1527Return value:
1528 * if a guess string is found, return it (after finding it)
1529 * if the fallback is called, return whatever it returns
1530 * otherwise, nil"
213d9a4f
RS
1531 (interactive "e")
1532 (let ((guess
1533 ;; Maybe less surprising without the save-excursion?
1534 (save-excursion
1535 (mouse-set-point e)
0948761d
KH
1536 ;; Would prefer to do nothing unless click was *on* text. How
1537 ;; to tell that the click was beyond the end of current line?
213d9a4f
RS
1538 (ffap-guesser))))
1539 (cond
1540 (guess
0948761d 1541 (set-buffer (ffap-event-buffer e))
213d9a4f
RS
1542 (ffap-highlight)
1543 (unwind-protect
1544 (progn
1545 (sit-for 0) ; display
0948761d
KH
1546 (message "Finding `%s'" guess)
1547 (find-file-at-point guess)
3788c735 1548 guess) ; success: return non-nil
213d9a4f 1549 (ffap-highlight t)))
0948761d
KH
1550 ((interactive-p)
1551 (if ffap-at-mouse-fallback
1552 (call-interactively ffap-at-mouse-fallback)
3788c735
KH
1553 (message "No file or url found at mouse click.")
1554 nil)) ; no fallback, return nil
0948761d
KH
1555 ;; failure: return nil
1556 )))
213d9a4f
RS
1557
1558\f
0948761d
KH
1559;;; ffap-other-* commands:
1560;;
1561;; Requested by KPC.
1562
1563;; There could be a real `ffap-noselect' function, but we would need
1564;; at least two new user variables, and there is no w3-fetch-noselect.
1565;; So instead, we just fake it with a slow save-window-excursion.
213d9a4f
RS
1566
1567(defun ffap-other-window nil
0948761d
KH
1568 "Like `ffap', but put buffer in another window.
1569Only intended for interactive use."
213d9a4f
RS
1570 (interactive)
1571 (switch-to-buffer-other-window
1572 (save-window-excursion (call-interactively 'ffap) (current-buffer))))
1573
1574(defun ffap-other-frame nil
0948761d
KH
1575 "Like `ffap', but put buffer in another frame.
1576Only intended for interactive use."
213d9a4f 1577 (interactive)
0948761d
KH
1578 ;; Extra code works around dedicated windows (noted by JENS, 7/96):
1579 (let* ((win (selected-window)) (wdp (window-dedicated-p win)))
1580 (unwind-protect
1581 (progn
1582 (set-window-dedicated-p win nil)
1583 (switch-to-buffer-other-frame
1584 (save-window-excursion
1585 (call-interactively 'ffap)
1586 (current-buffer))))
1587 (set-window-dedicated-p win wdp))))
213d9a4f
RS
1588
1589\f
87e2d039
RS
1590;;; Bug Reporter:
1591
213d9a4f 1592(defun ffap-bug nil
87e2d039
RS
1593 "Submit a bug report for the ffap package."
1594 ;; Important: keep the version string here in synch with that at top
1595 ;; of file! Could use lisp-mnt from Emacs 19, but that would depend
1596 ;; on being able to find the ffap.el source file.
213d9a4f
RS
1597 (interactive)
1598 (require 'reporter)
1599 (let ((reporter-prompt-for-summary-p t))
1600 (reporter-submit-bug-report
87e2d039 1601 "Michelangelo Grigni <mic@mathcs.emory.edu>"
3788c735 1602 "ffap"
87e2d039
RS
1603 (mapcar 'intern (all-completions "ffap-" obarray 'boundp)))))
1604
213d9a4f
RS
1605(fset 'ffap-submit-bug 'ffap-bug) ; another likely name
1606
1607\f
87e2d039 1608;;; Hooks for Gnus, VM, Rmail:
213d9a4f 1609;;
87e2d039
RS
1610;; If you do not like these bindings, write versions with whatever
1611;; bindings you would prefer.
213d9a4f 1612
87e2d039
RS
1613(defun ffap-ro-mode-hook nil
1614 "Bind `ffap-next' and `ffap-menu' to M-l and M-m, resp."
1615 (local-set-key "\M-l" 'ffap-next)
1616 (local-set-key "\M-m" 'ffap-menu)
1617 )
213d9a4f 1618
87e2d039
RS
1619(defun ffap-gnus-hook nil
1620 "Bind `ffap-gnus-next' and `ffap-gnus-menu' to M-l and M-m, resp."
1621 (set (make-local-variable 'ffap-foo-at-bar-prefix) "news") ; message-id's
1622 ;; Note "l", "L", "m", "M" are taken:
1623 (local-set-key "\M-l" 'ffap-gnus-next)
1624 (local-set-key "\M-m" 'ffap-gnus-menu))
213d9a4f 1625
87e2d039
RS
1626(defun ffap-gnus-wrapper (form) ; used by both commands below
1627 (and (eq (current-buffer) (get-buffer gnus-summary-buffer))
1628 (gnus-summary-select-article)) ; get article of current line
1629 ;; Preserve selected buffer, but do not do save-window-excursion,
1630 ;; since we want to see any window created by the form. Temporarily
1631 ;; select the article buffer, so we can see any point movement.
1632 (let ((sb (window-buffer (selected-window))))
1633 (gnus-configure-windows 'article)
1634 (pop-to-buffer gnus-article-buffer)
1635 (widen)
1636 ;; Skip headers for ffap-gnus-next (which will wrap around)
1637 (if (eq (point) (point-min)) (search-forward "\n\n" nil t))
1638 (unwind-protect
1639 (eval form)
1640 (pop-to-buffer sb))))
1641
1642(defun ffap-gnus-next nil
1643 "Run `ffap-next' in the gnus article buffer."
1644 (interactive) (ffap-gnus-wrapper '(ffap-next nil t)))
1645
1646(defun ffap-gnus-menu nil
1647 "Run `ffap-menu' in the gnus article buffer."
1648 (interactive) (ffap-gnus-wrapper '(ffap-menu)))
1649
1650\f
61154252
RS
1651(defcustom dired-at-point-require-prefix nil
1652 "*If set, reverses the prefix argument to `dired-at-point'.
1653This is nil so neophytes notice ffap. Experts may prefer to disable
1654ffap most of the time."
1655 :type 'boolean
1656 :group 'ffap
1657 :version "20.3")
1658
1659;;;###autoload
1660(defun dired-at-point (&optional filename)
1661 "Start Dired, defaulting to file at point. See `ffap'."
1662 (interactive)
1663 (if (and (interactive-p)
1664 (if dired-at-point-require-prefix
1665 (not current-prefix-arg)
1666 current-prefix-arg))
1667 (let (current-prefix-arg) ; already interpreted
b8e2ae05 1668 (call-interactively 'dired))
61154252
RS
1669 (or filename (setq filename (dired-at-point-prompter)))
1670 (cond
1671 ((ffap-url-p filename)
1672 (funcall ffap-url-fetcher filename))
1673 ((and ffap-dired-wildcards
1674 (string-match ffap-dired-wildcards filename))
1675 (dired filename))
1676 ((file-exists-p filename)
1677 (if (file-directory-p filename)
1678 (dired (expand-file-name filename))
1679 (dired (concat (expand-file-name filename) "*"))))
6b61353c
KH
1680 ((and (file-writable-p
1681 (or (file-name-directory (directory-file-name filename))
1682 filename))
3fa86f26 1683 (y-or-n-p "Directory does not exist, create it? "))
61154252
RS
1684 (make-directory filename)
1685 (dired filename))
1686 ((error "No such file or directory `%s'" filename)))))
1687
1688(defun dired-at-point-prompter (&optional guess)
1689 ;; Does guess and prompt step for find-file-at-point.
1690 ;; Extra complication for the temporary highlighting.
1691 (unwind-protect
1692 (ffap-read-file-or-url
1693 (if ffap-url-regexp "Dired file or URL: " "Dired file: ")
1694 (prog1
6b61353c
KH
1695 (setq guess (or guess
1696 (let ((guess (ffap-guesser)))
1697 (if (or (not guess)
1698 (ffap-url-p guess)
1699 (ffap-file-remote-p guess))
1700 guess
1701 (setq guess (abbreviate-file-name
1702 (expand-file-name guess)))
1703 (cond
1704 ;; Interpret local directory as a directory.
1705 ((file-directory-p guess)
1706 (file-name-as-directory guess))
1707 ;; Get directory component from local files.
1708 ((file-regular-p guess)
1709 (file-name-directory guess))
1710 (guess))))
1711 ))
1712 (and guess (ffap-highlight))))
61154252
RS
1713 (ffap-highlight t)))
1714\f
0948761d 1715;;; Offer default global bindings (`ffap-bindings'):
87e2d039
RS
1716
1717(defvar ffap-bindings
3788c735
KH
1718 '(
1719 (global-set-key [S-mouse-3] 'ffap-at-mouse)
1720 (global-set-key [C-S-mouse-3] 'ffap-menu)
1721 (global-set-key "\C-x\C-f" 'find-file-at-point)
1722 (global-set-key "\C-x4f" 'ffap-other-window)
1723 (global-set-key "\C-x5f" 'ffap-other-frame)
3d729a9a 1724 (global-set-key "\C-xd" 'dired-at-point)
3788c735
KH
1725 (add-hook 'gnus-summary-mode-hook 'ffap-gnus-hook)
1726 (add-hook 'gnus-article-mode-hook 'ffap-gnus-hook)
1727 (add-hook 'vm-mode-hook 'ffap-ro-mode-hook)
1728 (add-hook 'rmail-mode-hook 'ffap-ro-mode-hook)
1729 ;; (setq dired-x-hands-off-my-keys t) ; the default
1730 )
1731 "List of binding forms evaluated by function `ffap-bindings'.
148b5960 1732A reasonable ffap installation needs just this one line:
87e2d039 1733 (ffap-bindings)
0948761d 1734Of course if you do not like these bindings, just roll your own!")
87e2d039 1735
25050dab 1736;;;###autoload
87e2d039
RS
1737(defun ffap-bindings nil
1738 "Evaluate the forms in variable `ffap-bindings'."
25050dab 1739 (interactive)
87e2d039
RS
1740 (eval (cons 'progn ffap-bindings)))
1741
87e2d039 1742\f
6b61353c
KH
1743
1744;;; arch-tag: 9dd3e88a-5dec-4607-bd57-60ae9ede8ebc
213d9a4f 1745;;; ffap.el ends here