(dired-local-variables-file): Use convert-standard-filename.
[bpt/emacs.git] / lisp / ffap.el
CommitLineData
213d9a4f
RS
1;;; ffap.el -- find-file-at-point,
2;; Copyright (C) 1994, 1995 Free Software Foundation, Inc.
3
4;;; Author: Michelangelo Grigni <mic@mathcs.emory.edu>
5
6;; This file is part of GNU Emacs.
7
8;; GNU Emacs is free software; you can redistribute it and/or modify
9;; it under the terms of the GNU General Public License as published by
10;; the Free Software Foundation; either version 2, or (at your option)
11;; any later version.
12
13;; GNU Emacs is distributed in the hope that it will be useful,
14;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16;; GNU General Public License for more details.
17
18;; You should have received a copy of the GNU General Public License
19;; along with GNU Emacs; see the file COPYING. If not, write to
20;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
21
22;;; Commentary:
23
24;; A replacement for find-file {C-x C-f}: finds file or URL,
25;; guessing default from text at point. Many features!
26;; Send bugs or suggestions with M-x ffap-bug.
27
28;; See ftp://ftp.mathcs.emory.edu:/pub/mic/emacs/ for most recent version:
29;; ffap.el.gz -- this file, compressed with gzip
30;; ffap-xe.el -- support code for XEmacs 19.*
31;; COPYING.gz -- GNU General Public License, version 2
32;; README -- description of these and other files
33;;
34;; For the last version sent to elisp-archive@cis.ohio-state.edu, see:
35;; ftp://ftp.cis.ohio-state.edu/pub/gnu/emacs/elisp-archive/misc/ffap.el.Z
36;; (mirrored in gatekeeper.dec.com:/pub/GNU/elisp-archive/misc/)
37\f
38;;; Description:
39;;
40;; Command find-file-at-point (== ffap) replaces find-file. With a
41;; prefix, it behaves exactly like find-file. Without a prefix, it
42;; first tries to guess a default file or URL based on the text around
43;; the point (set `ffap-require-prefix' to swap these behaviors).
44;; This is a quick way to fetch URL and file references in many
45;; situations, such as in mail or news messages, README's, and
46;; MANIFEST's.
47;;
48;; Some related commands are ffap-at-mouse, ffap-next, ffap-menu,
49;; ffap-other-window, ffap-other-frame.
50;;
51;; This package is about user convenience. It adds nothing to the
52;; elisp programmer's repertoire.
53
54\f
55;;; Installation:
56
57;; Quick Setup:
58;;
59;; For a basic installation, just install ffap.el somewhere in your
60;; `load-path', byte-compile it, and add the following two lines near
61;; the end of your ~/.emacs (or equivalent) file:
62;;
63;; (require 'ffap) ; load this file
64;; (global-set-key "\C-x\C-f" 'find-file-at-point)
65;;
66;; Other Packages: ffap notices the presence of several other packages
67;; when it is loaded. In particular, if you use ange-ftp, efs, w3,
68;; complete, or ff-paths (version < 3.00), it is best to load or
69;; autoload them before loading ffap (ffap does not need any of them).
70;; If you use ff-paths version >= 3.00, load it after ffap.
71
72;; Fancier Setup:
73;;
74;; ffap has many options. The next comment block contains some
75;; fancier code that you might want to adapt for your .emacs. For
76;; even more features, look at the documentation (M-x apropos ffap),
77;; and perhaps check the comments in the "User Variables" and "Peanut
78;; Gallery" sections of this file.
79
80;; ;; Before loading ffap:
81;;
82;; (setq ffap-url-regexp nil) ; to disable all URL features
83;;
84;; ;; Loading ffap:
85;; (require 'ffap) ; as in "Quick Setup" above
86;;
87;; After loading ffap:
88;;
89;; (global-set-key "\C-x\C-f" 'find-file-at-point) ; as in "Quick Setup"
90;; (global-set-key "\C-x4f" 'ffap-other-window) ; or \C-f
91;; (global-set-key "\C-x5f" 'ffap-other-frame) ; or \C-f
92;;
93;; (setq ffap-alist ; remove something in `ffap-alist'
94;; (delete (assoc 'c-mode ffap-alist) ffap-alist))
95;;
96;; (setq ffap-alist ; add something to `ffap-alist'
97;; (cons
98;; (cons "^[Yy][Ss][Nn][0-9]+$"
99;; (defun ffap-ysn (name)
100;; (concat
101;; "http://snorri.chem.washington.edu/ysnarchive/issuefiles/"
102;; (substring name 3) ".html")))
103;; ffap-alist))
104;;
105;;
106;; Before or after loading ffap:
107;;
108;; (setq ffap-alist nil) ; disable all `ffap-alist' actions
109;;
110;; (setq ffap-require-prefix t) ; without prefix, ffap == find-file
111;;
112;; (setq ffap-machine-p-known 'accept) ; to avoid pinging
113;;
114;; ;; Choose a mouse binding appropriate for your emacs version:
115;; (global-set-key [S-mouse-1] 'ffap-at-mouse) ; Emacs 19
116;; (global-set-key [(meta button1)] 'ffap-at-mouse) ; XEmacs
117;; (and window-system ; Emacs 18 (from .emacs)
118;; (setq window-setup-hook
119;; '(lambda nil (define-key mouse-map x-button-s-left
120;; 'ffap-at-mouse))))
121;;
122;; ;; Use Netscape instead of w3 to fetch URL's. Mosaic is very similar.
123;; (if (eq window-system 'x)
124;; (progn
125;; ;; Get browse-url at http://wombat.doc.ic.ac.uk/emacs/browse-url.el,
126;; ;; or get a (probably out of date) copy from the ftp site above.
127;; (autoload 'browse-url-netscape "browse-url" nil t)
128;; (setq ffap-url-fetcher 'browse-url-netscape)))
129;; ;; Or for a hairier ffap-url-fetcher, get ffap-url.el (same ftp site).
130;;
131;; ;; Support for gnus, vm, rmail (see hook definitions for bindings):
132;; (add-hook 'gnus-summary-mode-hook 'ffap-gnus-hook)
133;; (add-hook 'gnus-article-mode-hook 'ffap-gnus-hook)
134;; (add-hook 'vm-mode-hook 'ffap-ro-mode-hook)
135;; (add-hook 'rmail-mode-hook 'ffap-ro-mode-hook)
136
137\f
138;;; Related packages:
139;;
140;; If you have hyperbole, you may not need this package, although ffap
141;; is smaller and smarter at this particular task. Also note that w3
142;; (ftp.cs.indiana.edu:/pub/elisp/w3/README) offers a similar command
143;; w3-follow-url-at-point.
144;;
145;; The browse-url package (above) notices URL's and hands them off to
146;; w3 or an external WWW browser. Package |~/misc/goto-address.el.gz|
147;; by Eric J. Ding <ericding@mit.edu> notices URL's and mail
148;; addresses, and can pre-fontify a buffer to highlight them. Gnus5
149;; (ding) and vm also provide similar support in their messages.
150
151\f
152;;; Examples:
153;;
154;; Try M-x find-file-at-point (maybe {C-x C-f}) on these examples.
155;; These local file examples use ordinary find-file:
156;;
157;; ffap.el, /etc/motd, $MAIL -- find local or absolute files
158;; .emacs book.sty info/cl pwd.h -- search paths depending on filename
159;; (require 'rmail) -- search paths depending on major-mode
160;; file:/etc/motd -- depends on `ffap-url-unwrap-local'
161;;
162;; These remote file examples work if you have ange-ftp or efs:
163;;
164;; ftp:/pub -- no ping (always works)
165;; ftp.x.org:README -- no ping, a nice recursive example
166;; anonymous@ftp.x.org:/README -- synonym
167;; ftp.x.org://README -- synonym
168;; ftp://ftp.x.org/README -- depends on `ffap-url-unwrap-remote'
169;; ftp.mathcs.emory.edu -- depends on `ffap-machine-p-known'
170;; mic@ftp:/ -- depends on `ffap-machine-p-local'
171;; ftp.mathcs.emory.edu:/ -- depends on `ffap-ftp-sans-slash-regexp'
172;;
173;; These URL examples use `ffap-url-fetcher' (default w3-fetch):
174;;
175;; http://www.cc.emory.edu
176;; http://www.cs.indiana.edu/elisp/w3/docs.html
177;; http://info.cern.ch/default.html
178;; news:news.newusers.questions
179;; mailto:mic@mathcs.emory.edu
180;; mic@mathcs.emory.edu -- same as previous
181;; <mic@mathcs.emory.edu> -- same as previous
182;; <root> -- mailto:root
183;; <mic.9@mathcs.emory.edu> -- see `ffap-foo@bar-prefix'
184;; file:/etc/motd -- see `ffap-url-unwrap-local'
185;; ftp://ftp.x.org/README -- see `ffap-url-unwrap-remote'
186;;
187;; Multiline gopher blocks (as in .gopherrc and usenet of yesteryear):
188;;
189;; Type=1
190;; Name=Electronic Texts (ffap ignores this)
191;; Path=
192;; Host=etext.archive.umich.edu
193;; Port=70
194
195\f
196;;; Code:
197
198(provide 'ffap)
199
200;;; User Variables:
201
202;; This function is used inside defvars:
203(defun ffap-soft-value (name &optional default)
204 ;; Avoid interning. Bug: (ffap-soft-value "nil" 5) --> 5
205 (let ((sym (intern-soft name)))
206 (if (and sym (boundp sym)) (symbol-value sym) default)))
207
208
209(defvar ffap-ftp-regexp
210 (and
211 (or (featurep 'ange-ftp)
212 (featurep 'efs)
213 (and (boundp 'file-name-handler-alist) ; v19
214 (or (rassq 'ange-ftp-hook-function file-name-handler-alist)
215 (rassq 'efs-file-handler-function file-name-handler-alist))))
216 ;; Apparently this is good enough for both ange-ftp and efs:
217 "\\`/[^/:]+:")
218 "*Treat paths matching this as remote ftp paths. Nil to disable.
219Nil also disables the generation of such paths by ffap.")
220
221(defvar ffap-url-unwrap-local t
222 "*If set, convert local \"file:\" URL to path before prompting.")
223
224(defvar ffap-url-unwrap-remote t
225 "*Convert remote \"file:\" or \"ftp:\" URL to path before prompting.
226This is ignored if `ffap-ftp-regexp' is nil.")
227
228(defvar ffap-ftp-default-user
229 (if (or (equal (ffap-soft-value "ange-ftp-default-user") "anonymous")
230 (equal (ffap-soft-value "efs-default-user") "anonymous"))
231 nil
232 "anonymous")
233 "*User name in ftp paths generated by ffap (see host-to-ftp-path).
234Nil to fall back on `efs-default-user' or `ange-ftp-default-user'.")
235
236(defvar ffap-rfs-regexp
237 ;; Remote file access built into file system? HP rfa or Andrew afs:
238 "\\`/\\(afs\\|net\\)/."
239 ;; afs only: (and (file-exists-p "/afs") "\\`/afs/.")
240 "*Paths matching this are remote file-system paths. Nil to disable.")
241
242(defvar ffap-url-regexp
243 ;; Could just use `url-nonrelative-link' of w3, if loaded.
244 ;; This regexp is not exhaustive, it just matches common cases.
245 (concat
246 "\\`\\("
247 "news\\(post\\)?:\\|mailto:\\|file:" ; no host ok
248 "\\|"
249 "\\(ftp\\|http\\|telnet\\|gopher\\|www\\|wais\\)://" ; needs host
250 "\\)." ; require one more character
251 )
252 "Regexp matching URL's, or nil to disable.")
253
254(defvar ffap-foo@bar-prefix "mailto"
255 "*Presumed url prefix type of strings like \"<foo.9z@bar>\".
256Sensible values are nil, \"news\", or \"mailto\".")
257
258\f
259;;; Peanut Gallery:
260
261;; Users of ffap occasionally suggest new features. If I consider
262;; those features interesting but not clear winners (a matter of
263;; personal taste) I try to leave options to enable them. Read
264;; through this section, and for any features you like, put an
265;; appropriate form in your ~/.emacs file.
266
267(defvar ffap-dired-wildcards nil ; "[*?][^/]*$"
268 ;; From RHOGEE, 07 Jul 1994.
269 ;; Disabled: dired is still available by "C-x C-d <pattern>", and
270 ;; valid filenames may contain wildcard characters.
271 "*A regexp matching filename wildcard characters, or nil.
272If find-file-at-point gets a filename matching this pattern,
273it passes it on to dired instead of find-file.")
274
275(defvar ffap-newfile-prompt nil ; t
276 ;; From RHOGEE, 11 Jul 1994.
277 ;; Disabled: this is better handled by `find-file-not-found-hooks'.
278 "*Whether find-file-at-point prompts about a nonexistent file.")
279
280(defvar ffap-require-prefix nil
281 ;; From RHOGEE, 20 Oct 1994.
282 ;; This is nil so that neophytes notice ffap. Experts instead may
283 ;; prefer to disable ffap most of the time.
284 "*If set, reverses the prefix argument to find-file-at-point.")
285
286(defvar ffap-file-finder
287 ;; From RHOGEE, 20 Oct 1994.
288 ;; This allows compatibility with ff-paths version < 3.00.
289 ;; For ff-paths version >= 3.00, just load it after ffap.
290 (if (commandp 'find-file-using-paths)
291 'find-file-using-paths
292 ;; Try to overcome load-order dependency:
293 (eval-after-load
294 "ff-paths"
295 '(and (commandp 'find-file-using-paths)
296 (setq ffap-file-finder find-file-using-paths)))
297 'find-file)
298 "*The command symbol called by find-file-at-point to find a file.
299Probably find-file, or find-file-using-paths if you use ff-paths
300with version < 3.00.")
301(put 'ffap-file-finder 'risky-local-variable t)
302
303(defvar ffap-url-fetcher 'w3-fetch
304 "*A function of one argument, called by ffap to fetch URL's.
305The default is w3-fetch from the w3 package. If you prefer Mosaic or
306Netscape, install http://wombat.doc.ic.ac.uk/emacs/browse-url.el, and
307add one of the following lines to your setup:
308
309\(setq ffap-url-fetcher 'browse-url-netscape\)
310\(setq ffap-url-fetcher 'browse-url-mosaic\)
311
312Or for something hairier \(choose fetch method based on url type and
313prompting\) get ffap-url.el wherever you ffap.el."
314 ;; Big old `lambda' examples deleted. Some remote-control references:
315 ;; http://www.ncsa.uiuc.edu/SDG/Software/XMosaic/remote-control.html
316 ;; http://home.netscape.com/newsref/std/x-remote.html
317 )
318(put 'ffap-url-fetcher 'risky-local-variable t)
319
320\f
321;;; Command ffap-next:
322;;
323;; Original ffap-next-url (URL's only) from RPECK 30 Mar 1995.
324;; Since then, broke up into ffap-next-guess (noninteractive) and
325;; ffap-next (a command), now work on files as well as url's.
326
327(defvar ffap-next-regexp
328 ;; If you want ffap-next to find URL's only, try this:
329 ;; (and ffap-url-regexp (string-match "\\\\`" ffap-url-regexp)
330 ;; (concat "\\<" (substring ffap-url-regexp 2))))
331 ;;
332 ;; It pays to put a big fancy regexp here, since ffap-guesser is
333 ;; much more time-consuming than regexp searching:
334 "[/:.~a-zA-Z]/\\|@[a-zA-Z][-a-zA-Z0-9]*\\."
335 "*Regular expression governing search of ffap-next.")
336
337(defvar ffap-next-guess nil "Last value returned by `ffap-next-guess'.")
338(defun ffap-next-guess (&optional back lim)
339 "Move point to next file or url, and return it as a string.
340If nothing found, leaves point at limit and returns nil.
341Optional BACK argument makes search backwards.
342Optional LIM argument limits the search.
343Only considers strings that match `ffap-next-regexp'."
344 (or lim (setq lim (if back (point-min) (point-max))))
345 (let (guess)
346 (while (not (or guess (eq (point) lim)))
347 (funcall (if back 're-search-backward 're-search-forward)
348 ffap-next-regexp lim 'move)
349 (setq guess (ffap-guesser)))
350 ;; Go to end, so we do not get same guess twice:
351 (goto-char (nth (if back 0 1) ffap-string-at-point-region))
352 (setq ffap-next-guess guess)))
353
354;;;###autoload
355(defun ffap-next (&optional back wrap)
356 "Search buffer for next file or url, and run ffap.
357Optional argument BACK says to search backwards.
358Optional argument WRAP says to try wrapping around if necessary.
359Interactively: use a single prefix to search backwards,
360double prefix to wrap forward, triple to wrap backwards.
361Actual search is done by ffap-next-guess."
362 (interactive
363 (cdr (assq (prefix-numeric-value current-prefix-arg)
364 '((1) (4 t) (16 nil t) (64 t t)))))
365 (let ((pt (point))
366 (guess (ffap-next-guess back)))
367 ;; Try wraparound if necessary:
368 (and (not guess) wrap
369 (goto-char (if back (point-max) (point-min)))
370 (setq guess (ffap-next-guess back pt)))
371 (if guess
372 (progn
373 (sit-for 0) ; display point movement
374 (find-file-at-point (ffap-prompter guess)))
375 (goto-char pt) ; restore point
376 (message "No %sfiles or URL's found."
377 (if wrap "" "more ")))))
378
379(defun ffap-next-url (&optional back wrap)
380 "Just like ffap-next, but searches with `ffap-url-regexp'."
381 (interactive)
382 (let ((ffap-next-regexp ffap-url-regexp))
383 (if (interactive-p)
384 (call-interactively 'ffap-next)
385 (ffap-next back wrap))))
386
387\f
388;;; Hooks for GNUS, VM, Rmail:
389;;
390;; See "Installation" above for suggested use of these hooks.
391;; If you do not like these bindings, just write hooks with
392;; whatever bindings you would prefer.
393;;
394;; Any suggestions of more "memorable" bindings? -- Mic
395
396(defun ffap-ro-mode-hook nil
397 "Binds ffap-gnus-next and ffap-gnus-menu to M-l and M-m, resp."
398 (local-set-key "\M-l" 'ffap-next)
399 (local-set-key "\M-m" 'ffap-menu)
400 )
401
402(defun ffap-gnus-hook nil
403 "Binds ffap-gnus-next and ffap-gnus-menu to L and M, resp."
404 (set (make-local-variable 'ffap-foo@bar-prefix) "news") ; message-id's
405 ;; Note lowercase l and m are taken:
406 (local-set-key "L" 'ffap-gnus-next)
407 (local-set-key "M" 'ffap-gnus-menu))
408
409(defun ffap-gnus-wrapper (form) ; used by both commands below
410 (and (eq (current-buffer) (get-buffer gnus-summary-buffer))
411 (gnus-summary-select-article)) ; get article of current line
412 ;; Preserve selected buffer, but do not do save-window-excursion,
413 ;; since we want to see any window created by form. Temporarily
414 ;; select the article buffer, so we see any point movement.
415 (let ((sb (window-buffer (selected-window))))
416 (gnus-configure-windows 'article)
417 (pop-to-buffer gnus-article-buffer)
418 (widen)
419 ;; Skip headers at first, for ffap-gnus-next (which wraps around)
420 (if (eq (point) (point-min)) (search-forward "\n\n" nil t))
421 (unwind-protect
422 (eval form)
423 (pop-to-buffer sb))))
424
425(defun ffap-gnus-next nil
426 "Run ffap-next in the GNUS article buffer."
427 (interactive) (ffap-gnus-wrapper '(ffap-next nil t)))
428
429(defun ffap-gnus-menu nil
430 "Run ffap-menu in the GNUS article buffer."
431 (interactive) (ffap-gnus-wrapper '(ffap-menu)))
432
433\f
434;;; Remote machines and paths:
435
436(fset 'ffap-replace-path-component
437 (if (or (featurep 'efs)
438 (and
439 (boundp 'file-name-handler-alist) ; v19
440 (rassq 'efs-file-handler-function file-name-handler-alist)))
441 'efs-replace-path-component
442 'ange-ftp-replace-name-component))
443
444(defun ffap-file-exists-string (file)
445 ;; With certain packages (ange-ftp, jka-compr?) file-exists-p
446 ;; sometimes returns a nicer string than it is given. Otherwise, it
447 ;; just returns nil or t.
448 "Return FILE \(maybe modified\) if it exists, else nil."
449 (let ((exists (file-exists-p file)))
450 (and exists (if (stringp exists) exists file))))
451
452;; I cannot decide a "best" strategy here, so these are variables. In
453;; particular, if `Pinging...' is broken or takes too long on your
454;; machine, try setting these all to accept or reject.
455(defvar ffap-machine-p-local 'reject ; this happens often
456 "A symbol, one of: ping, accept, reject.
457This is what ffap-machine-p does with hostnames that have no domain.")
458(defvar ffap-machine-p-known 'ping ; 'accept for speed
459 "A symbol, one of: ping, accept, reject.
460This is what ffap-machine-p does with hostnames that have a known domain
461\(see lisp/mail-extr.el for the list of known domains\).")
462(defvar ffap-machine-p-unknown 'reject
463 "A symbol, one of: ping, accept, reject.
464This is what ffap-machine-p does with hostnames that have an unknown domain
465\(see lisp/mail-extr.el for the list of known domains\).")
466
467(defvar ffap-machine-p-known-domains
468 '("com" "edu" "net" "org" "mil" "gov" "us" "arpa") ; USA USA...
469 ;; This variable is mainly for emacs18.
470 "Top-level domains known to ffap. Ignored if mail-extr is loadable.")
471
472(defun ffap-machine-p (host &optional service quiet)
473 "Indicate whether HOST is the name of a real machine.
474The variables ffap-machine-p-local, ffap-machine-p-known, and ffap-machine-p-unknown
475control ffap-machine-p depending on HOST's domain \(none/known/unknown\).
476Pinging is done using open-network-stream to decide HOST existence.
477Optional SERVICE specifies the service used \(default \"discard\"\).
478Optional QUIET flag suppresses the \"Pinging...\" message.
479Returned values:
480A t value means that HOST answered.
481A symbol \(accept\) means the relevant variable told us to accept.
482A string means the machine exists, but does not respond for some reason."
483 ;; Try some:
484 ;; (ffap-machine-p "ftp")
485 ;; (ffap-machine-p "nonesuch")
486 ;; (ffap-machine-p "ftp.mathcs.emory.edu")
487 ;; (ffap-machine-p "foo.bonk")
488 ;; (ffap-machine-p "foo.bonk.com")
489 ;; (ffap-machine-p "cs" 5678)
490 ;; (ffap-machine-p "gopher.house.gov")
491 ;; Not known to 19.28
492 ;; (ffap-
493 (if (or (string-match "[^-a-zA-Z0-9.]" host) ; Illegal chars (?)
494 (not (string-match "[^0-9]" host))) ; all numeric! reject it
495 nil
496 (let* ((domain
497 (and (string-match "\\.[^.]*$" host)
498 (downcase (substring host (1+ (match-beginning 0))))))
499 (domain-name ; t, "Country", "Local", or nil
500 (cond
501 ((not domain) "Local")
502 ;; common non-country domains (some imply US though):
503 ;; t)
504 (t
505 ;; Use domain-name properties from v19 lisp/mail-extr.el;
506 ;; bbdb/mail-extr also puts this in `all-top-level-domains'.
507 (if (or (featurep 'mail-extr)
508 (and (load "mail-extr" t t)
509 ;; It became a feature between 19.22 and 19.28
510 (provide 'mail-extr)))
511 (get (intern-soft
512 domain
513 (condition-case nil
514 mail-extr-all-top-level-domains
515 ;; Before 19.28, the symbols were in `obarray':
516 (error obarray)))
517 'domain-name)
518 ;; Emacs18 does not have mail-extr:
519 (and (member domain ffap-machine-p-known-domains) t))
520 )))
521 (strategy
522 (cond ((not domain) ffap-machine-p-local)
523 ((not domain-name) ffap-machine-p-unknown)
524 (ffap-machine-p-known))))
525 (cond
526 ((eq strategy 'accept) 'accept)
527 ((eq strategy 'reject) nil)
528 ;; assume (eq strategy 'ping)
529 (t
530 (or quiet
531 (if (stringp domain-name)
532 (message "Pinging %s (%s)..." host domain-name)
533 (message "Pinging %s ..." host)))
534 (condition-case error
535 (progn
536 (delete-process
537 (open-network-stream
538 "ffap-machine-p" nil host (or service "discard")))
539 t)
540 (error
541 (let ((mesg (car (cdr error))))
542 (cond
543 ;; v18:
544 ((string-match "^Unknown host" mesg) nil)
545 ((string-match "not responding$" mesg) mesg)
546 ;; v19:
547 ;; (file-error "connection failed" "permission denied"
548 ;; "nonesuch" "ffap-machine-p")
549 ;; (file-error "connection failed" "host is unreachable"
550 ;; "gopher.house.gov" "ffap-machine-p")
551 ;; (file-error "connection failed" "address already in use"
552 ;; "ftp.uu.net" "ffap-machine-p")
553 ((equal mesg "connection failed")
554 (if (equal (nth 2 error) "permission denied")
555 nil ; host does not exist
556 ;; Other errors mean host exists:
557 (nth 2 error)))
558 ;; Could be "Unknown service":
559 (t (signal (car error) (cdr error))))))))))))
560
561(defun ffap-file-remote-p (filename)
562 "If FILENAME looks remote, return it \(maybe slightly improved\)."
563 ;; (ffap-file-remote-p "/user@foo.bar.com:/pub")
564 ;; (ffap-file-remote-p "/foo.dom://path")
565 (or (and ffap-ftp-regexp
566 (string-match ffap-ftp-regexp filename)
567 ;; Convert "/host://path" to "/host:/path", to handle a dieing
568 ;; practice of advertising ftp paths as "host.dom://path".
569 (if (string-match "//" filename)
570 (concat (substring filename 0 (match-beginning 0))
571 (substring filename (1- (match-end 0))))
572 filename))
573 (and ffap-rfs-regexp
574 (string-match ffap-rfs-regexp filename)
575 filename)))
576
577(defun ffap-machine-at-point nil
578 "Return machine name from around point if it exists, or nil."
579 (let ((mach (ffap-string-at-point "-a-zA-Z0-9." nil ".")))
580 (and (ffap-machine-p mach) mach)))
581
582(defun ffap-fixup-machine (mach)
583 ;; Convert a machine into an URL, an ftp path, or nil.
584 (cond
585 ((not (and ffap-url-regexp (stringp mach))) nil)
586 ((string-match "\\`gopher[-.]" mach) ; or "info"?
587 (concat "gopher://" mach "/"))
588 ((and (string-match "\\`w\\(ww\\|eb\\)[-.]" mach))
589 (concat "http://" mach "/"))
590 ;; More cases? Maybe "telnet:" for archie?
591 (ffap-ftp-regexp (ffap-host-to-path mach))
592 ))
593
594(defun ffap-host-to-path (host)
595 "Convert \"HOST\" to \"/anonymous@HOST:\" (or \"\" for \"localhost\").
596Variable `ffap-ftp-default-user' overrides or suppresses \"anonymous\"."
597 (if (equal host "localhost")
598 ""
599 (if ffap-ftp-default-user
600 (concat "/" ffap-ftp-default-user "@" host ":")
601 (concat "/" host ":"))))
602
603(defun ffap-newsgroup-p (string)
604 "Return STRING if it looks like a newsgroup name, else nil."
605 (and
606 (string-match ffap-newsgroup-regexp string)
607 (let ((htbs '(gnus-active-hashtb gnus-newsrc-hashtb gnus-killed-hashtb))
608 (heads ffap-newsgroup-heads)
609 htb ret)
610 (while htbs
611 (setq htb (car htbs) htbs (cdr htbs))
612 (condition-case nil
613 (progn
614 ;; errs: htb symbol may be unbound, or not a hash-table.
615 ;; gnus-gethash is just a macro for intern-soft.
616 (and (intern-soft string (symbol-value htb))
617 (setq ret string htbs nil))
618 ;; If we made it this far, GNUS is running, so ignore "heads":
619 (setq heads nil))
620 (error nil)))
621 (or ret (not heads)
622 (let ((head (string-match "\\`\\([a-z]+\\)\\." string)))
623 (and head (setq head (substring string 0 (match-end 1)))
624 (member head heads)
625 (setq ret string))))
626 ;; Ever any need to modify string as a newsgroup name?
627 ret)))
628(defvar ffap-newsgroup-regexp "^[a-z]+\\.[-+a-z_0-9.]+$"
629 "ffap-newsgroup-p quickly rejects strings that do not match this.")
630(defvar ffap-newsgroup-heads ; entirely inadequate
631 '("alt" "comp" "gnu" "misc" "news" "sci" "soc" "talk")
632 "Used by ffap-newsgroup-p if GNUS is not running.")
633
634(defun ffap-url-p (string)
635 "If STRING looks like an URL, return it (maybe improved), else nil."
636 ;; Does it look like an URL? Ignore case.
637 (let ((case-fold-search t))
638 (and ffap-url-regexp (string-match ffap-url-regexp string)
639 ;; I lied, no improvement:
640 string)))
641
642;; Broke these two out of ffap-fixup-url, for sake of ffap-url package.
643(defun ffap-url-unwrap-local (url)
644 "Return unwrapped local file URL, or nil. Ignores ffap-* variables."
645 (and (string-match "\\`\\(file\\|ftp\\):/?\\([^/]\\|\\'\\)" url)
646 (substring url (1+ (match-end 1)))))
647(defun ffap-url-unwrap-remote (url)
648 "Return unwrapped remote file URL, or nil. Ignores ffap-* variables."
649 (and (string-match "\\`\\(ftp\\|file\\)://\\([^:/]+\\):?\\(/.*\\)" url)
650 (concat
651 (ffap-host-to-path (substring url (match-beginning 2) (match-end 2)))
652 (substring url (match-beginning 3) (match-end 3)))))
653
654(defun ffap-fixup-url (url)
655 "Given URL, clean it up and return it. May become a file name."
656 (cond
657 ((not (stringp url)) nil)
658 ((and ffap-url-unwrap-local (ffap-url-unwrap-local url)))
659 ((and ffap-url-unwrap-remote ffap-ftp-regexp
660 (ffap-url-unwrap-remote url)))
661 ;; Do not load w3 just for this:
662 (t (let ((normal (and (fboundp 'url-normalize-url)
663 (url-normalize-url url))))
664 ;; In case url-normalize-url is confused:
665 (or (and normal (not (zerop (length normal))) normal)
666 url)))))
667
668\f
669;;; `ffap-alist':
670;;
671;; Search actions depending on the major-mode or extensions of the
672;; current name. Note all the little defun's could be broken out, at
673;; some loss of locality. I have had a vote for eliminating this
674;; from ffap (featuritis)
675
676;; First, some helpers for functions in `ffap-alist':
677
678(defun ffap-list-env (env &optional empty)
679 ;; Replace this with parse-colon-path (lisp/files.el)?
680 "Directory list parsed from \":\"-separated ENVinronment variable.
681Optional EMPTY is default if (getenv ENV) is undefined, and is also
682substituted for the first empty-string component, if there is one."
683 ;; Derived from psg-list-env in RHOGEE's ff-paths and
684 ;; bib-cite packages. The `empty' argument is intended to mimic
685 ;; the semantics of TeX/BibTeX variables, it is substituted for
686 ;; any empty string entry.
687 (if (or empty (getenv env)) ; should return something
688 (let ((start 0) match dir ret)
689 (setq env (concat (getenv env) ":")) ; note undefined --> ":"
690 (while (setq match (string-match ":" env start))
691 (setq dir (substring env start match) start (1+ match))
692 ;;(and (file-directory-p dir) (not (member dir ret)) ...)
693 (setq ret (cons dir ret)))
694 (setq ret (nreverse ret))
695 (and empty (setq match (member "" ret))
696 (progn
697 (setcdr match (append (cdr-safe empty) (cdr match)))
698 (setcar match (or (car-safe empty) empty))))
699 ret)))
700
701(defun ffap-reduce-path (path)
702 "Remove duplicates or non-dirs from PATH."
703 (let (ret tem)
704 (while path
705 (setq tem path path (cdr path))
706 (or (member (car tem) ret)
707 (not (file-directory-p (car tem)))
708 (progn (setcdr tem ret) (setq ret tem))))
709 (nreverse ret)))
710
711(defun ffap-add-subdirs (path)
712 "Return PATH augmented with its immediate subdirectories."
713 ;; (ffap-add-subdirs '("/notexist" "~"))
714 (let (ret subs)
715 (while path
716 (mapcar
717 (function
718 (lambda (f) (and (file-directory-p f) (setq ret (cons f ret)))))
719 (condition-case nil
720 (directory-files (car path) t "[^.]")
721 (error nil)))
722 (setq ret (cons (car path) ret)
723 path (cdr path)))
724 (nreverse ret)))
725
726(defvar ffap-locate-jka-suffixes t
727 "List of compression suffixes that ffap-locate-file tries.
728If not a list, it will be initialized by ffap-locate-file,
729and it will become nil unless you are using jka-compr.
730You might set this to nil or a list like '(\".gz\" \".z\" \".Z\").")
731
732(defun ffap-locate-file (file &optional nosuffix path)
733 ;; If this package is only working in v19 now, maybe should
734 ;; replace this with a quiet version of locate-library.
735 "A generic path-searching function, defaults mimic `load' behavior.
736Returns path of an existing FILE that (load FILE) would load, or nil.
737Optional second argument NOSUFFIX, if t, is like the fourth argument
738for load, i.e. don't try adding suffixes \".elc\" and \".el\".
739If a list, it is taken as a list of suffixes to try instead.
740Optional third argument PATH specifies a different search path, it
741defaults to `load-path'."
742 (or path (setq path load-path))
743 (if (file-name-absolute-p file)
744 (setq path (list (file-name-directory file))
745 file (file-name-nondirectory file)))
746 (let ((suffixes-to-try
747 (cond
748 ((consp nosuffix) nosuffix)
749 (nosuffix '(""))
750 (t '(".elc" ".el" "")))))
751 ;; Compensate for modern (19.28) jka-compr, that no longer searches
752 ;; for foo.gz when you asked for foo:
753 (or (listp ffap-locate-jka-suffixes)
754 (setq ffap-locate-jka-suffixes
755 (and (featurep 'jka-compr) ; an early version was jka-compr19
756 (not (featurep 'jka-aux))
757 jka-compr-file-name-handler-entry
758 (not (string-match
759 (car jka-compr-file-name-handler-entry)
760 "foo"))
761 ;; Hard to do cleverly across various jka-compr versions:
762 '(".gz" ".Z"))))
763 (if ffap-locate-jka-suffixes
764 (setq suffixes-to-try
765 (apply
766 'nconc
767 (mapcar
768 (function
769 (lambda (suf)
770 (cons suf
771 (mapcar
772 (function (lambda (x) (concat suf x)))
773 ffap-locate-jka-suffixes))))
774 suffixes-to-try))))
775 (let (found suffixes)
776 (while (and path (not found))
777 (setq suffixes suffixes-to-try)
778 (while (and suffixes (not found))
779 (let ((try (expand-file-name
780 (concat file (car suffixes))
781 (car path))))
782 (if (and (file-exists-p try) (not (file-directory-p try)))
783 (setq found try)))
784 (setq suffixes (cdr suffixes)))
785 (setq path (cdr path)))
786 found)))
787
788(defvar ffap-alist
789 ;; A big mess! Parts are probably useless.
790 (list
791 (cons "\\.info\\'"
792 (defun ffap-info (name)
793 (ffap-locate-file
794 name '("" ".info")
795 (or (ffap-soft-value "Info-directory-list")
796 (ffap-soft-value "Info-default-directory-list")
797 ;; v18:
798 (list (ffap-soft-value "Info-directory" "~/info/"))))))
799 ;; Since so many info files do not have .info extension, also do this:
800 (cons "\\`info/"
801 (defun ffap-info-2 (name) (ffap-info (substring name 5))))
802 (cons "\\`[-a-z]+\\'"
803 ;; This ignores the node! "(emacs)Top" same as "(emacs)Intro"
804 (defun ffap-info-3 (name)
805 (and (equal (ffap-string-around) "()") (ffap-info name))))
806 (cons "\\.elc?\\'"
807 (defun ffap-el (name) (ffap-locate-file name t)))
808 (cons 'emacs-lisp-mode
809 (defun ffap-el-mode (name)
810 ;; We do not bother with "" here, since it was considered above.
811 ;; Also ignore "elc", for speed (who else reads elc files?)
812 (and (not (string-match "\\.el\\'" name))
813 (ffap-locate-file name '(".el")))))
814 '(finder-mode . ffap-el-mode) ; v19: {C-h p}
815 '(help-mode . ffap-el-mode) ; v19.29
816 (cons 'c-mode
817 (progn
818 ;; Need better default here:
819 (defvar ffap-c-path '("/usr/include" "/usr/local/include"))
820 (defun ffap-c-mode (name)
821 (ffap-locate-file name t ffap-c-path))))
822 '(c++-mode . ffap-c-mode)
823 '(cc-mode . ffap-c-mode)
824 '("\\.\\([chCH]\\|cc\\|hh\\)\\'" . ffap-c-mode)
825 (cons 'tex-mode
826 ;; Complicated because auctex may not be loaded yet.
827 (progn
828 (defvar ffap-tex-path
829 (ffap-reduce-path
830 (append
831 (list ".")
832 (ffap-list-env "TEXINPUTS")
833 ;; (ffap-list-env "BIBINPUTS")
834 (ffap-add-subdirs
835 (ffap-list-env "TEXINPUTS_SUBDIR"
836 (ffap-soft-value
837 "TeX-macro-global"
838 '("/usr/local/lib/tex/macros"
839 "/usr/local/lib/tex/inputs")
840 )))))
841 "*Where ffap-tex-mode looks for tex files.")
842 (defun ffap-tex-mode (name)
843 (ffap-locate-file name '(".tex" "") ffap-tex-path))))
844 (cons 'latex-mode
845 (defun ffap-latex-mode (name)
846 ;; Any real need for "" here?
847 (ffap-locate-file name '(".sty" ".tex" "") ffap-tex-path)))
848 (cons "\\.\\(tex\\|sty\\|doc\\)\\'"
849 (defun ffap-tex (name)
850 (ffap-locate-file name t ffap-tex-path)))
851 (cons "\\.bib\\'"
852 (defun ffap-bib (name)
853 (ffap-locate-file
854 name t
855 (ffap-list-env "BIBINPUTS" '("/usr/local/lib/tex/macros/bib")))))
856 (cons 'math-mode
857 (defun ffap-math-mode (name)
858 (while (string-match "`" name)
859 (setq name (concat (substring name 0 (match-beginning 0))
860 "/"
861 (substring name (match-end 0)))))
862 (ffap-locate-file
863 name '(".m" "") (ffap-soft-value "Mathematica-search-path"))))
864 (cons "\\`\\." (defun ffap-home (name) (ffap-locate-file name t '("~"))))
865 (cons "\\`~/"
866 ;; Maybe a "Lisp Code Directory" reference:
867 (defun ffap-lcd (name)
868 (and
869 (or
870 ;; lisp-dir-apropos output buffer:
871 (string-match "Lisp Code Dir" (buffer-name))
872 ;; Inside an LCD entry like |~/misc/ffap.el.Z|,
873 ;; or maybe the holy LCD-Datafile itself:
874 (member (ffap-string-around) '("||" "|\n")))
875 (concat
876 ;; lispdir.el may not be loaded yet:
877 (ffap-host-to-path
878 (ffap-soft-value "elisp-archive-host"
879 "archive.cis.ohio-state.edu"))
880 (file-name-as-directory
881 (ffap-soft-value "elisp-archive-directory"
882 "/pub/gnu/emacs/elisp-archive/"))
883 (substring name 2)))))
884 (cons "^[Rr][Ff][Cc][- #]?\\([0-9]+\\)" ; no $
885 (progn
886 (defvar ffap-rfc-path
887 (concat (ffap-host-to-path "ds.internic.net") "/rfc/rfc%s.txt"))
888 (defun ffap-rfc (name)
889 (format ffap-rfc-path
890 (substring name (match-beginning 1) (match-end 1))))))
891 )
892 "Alist of \(KEY . FUNCTION\), applied to text around point.
893
894If ffap-file-at-point has a string NAME (maybe \"\") which is not an
895existing filename, it looks for pairs with a matching KEY:
896 * if KEY is a symbol, it should equal `major-mode'.
897 * if KEY is a string, it should match NAME as a regular expression.
898If KEY matches, ffap-file-at-point calls \(FUNCTION NAME\).
899FUNCTION should return a file, url, or nil \(nil means keep looking
900for more KEY matches\). Note URL's are ok despite the function name.")
901(put 'ffap-alist 'risky-local-variable t)
902
903\f
904;;; At-Point Functions:
905
906(defvar ffap-string-at-point-mode-alist
907 '(
908 ;; Slightly controversial decisions:
909 ;; * strip trailing "@" and ":"
910 ;; * no commas (good for latex)
911 (t "--:$+<>@-Z_a-z~" "<@" "@>;.,!?:")
912 (math-mode ",-:$+<>@-Z_a-z~`" "<" "@>;.,!?`:") ; allow backquote
913 ;; Note: you are better off using "C-c C-c" in compilation buffers:
914 ;; Maybe handle "$HOME", or "$(HOME)/bin/foo" in makefile-mode?
915 )
916 "Alist of \(MODE CHARS BEG END\), where MODE is a major-mode or t.
917The data are arguments to ffap-string-at-point, used to guess the
918filename at point. The `t' entry is the default.")
919
920(defvar ffap-string-at-point-region '(1 1)
921 "List (BEG END), last region returned by ffap-string-at-point.")
922
923(defvar ffap-string-at-point nil
924 ;; Added at suggestion of RHOGEE (for ff-paths), 7/24/95.
925 "Last string returned by ffap-string-at-point.")
926(defun ffap-string-at-point (&optional chars begpunct endpunct)
927 "Return maximal string of CHARS (a string) around point.
928Optional BEGPUNCT chars before point are stripped from the beginning;
929Optional ENDPUNCT chars after point are stripped from the end.
930Without arguments, uses `ffap-string-at-point-mode-alist'.
931Also sets `ffap-string-at-point' and `ffap-string-at-point-region'."
932 (if chars
933 (let* ((pt (point))
934 (str
935 (buffer-substring
936 (save-excursion
937 (skip-chars-backward chars)
938 (and begpunct (skip-chars-forward begpunct pt))
939 (setcar ffap-string-at-point-region (point)))
940 (save-excursion
941 (skip-chars-forward chars)
942 (and endpunct (skip-chars-backward endpunct pt))
943 (setcar (cdr ffap-string-at-point-region) (point))))))
944 (set-text-properties 0 (length str) nil str)
945 (setq ffap-string-at-point str))
946 ;; Get default args from `ffap-string-at-point-mode-alist'
947 (apply 'ffap-string-at-point
948 (cdr (or (assq major-mode ffap-string-at-point-mode-alist)
949 (assq t ffap-string-at-point-mode-alist)
950 ;; avoid infinite loop!
951 (error "ffap-string-at-point: bad alist")
952 )))))
953
954(defun ffap-string-around nil
955 ;; Sometimes useful to decide how to treat a string.
956 "Return string of two characters around last ffap-string-at-point."
957 (save-excursion
958 (format "%c%c"
959 (progn
960 (goto-char (car ffap-string-at-point-region))
961 (preceding-char)) ; maybe 0
962 (progn
963 (goto-char (nth 1 ffap-string-at-point-region))
964 (following-char)) ; maybe 0
965 )))
966
967(defun ffap-url-at-point nil
968 "Return URL from around point if it exists, or nil."
969 ;; Could use url-get-url-at-point instead ... how do they compare?
970 ;; Both handle "URL:", ignore non-relative links, trim punctuation.
971 ;; The other will actually look back if point is in whitespace, but
972 ;; I would rather ffap be non-rabid in such situations.
973 (and
974 ffap-url-regexp
975 (or
976 ;; In a w3 buffer button zone?
977 (let (tem)
978 (and (eq major-mode 'w3-mode)
979 ;; assume: (boundp 'w3-zone-at) (boundp 'w3-zone-data)
980 (setq tem (w3-zone-at (point)))
981 (consp (setq tem (w3-zone-data tem)))
982 (nth 2 tem)))
983 ;; Is there a reason not to strip trailing colon?
984 (let ((name (ffap-string-at-point
985 ;; Allow leading digits for email/news id's:
986 "--:?$+@-Z_a-z~#,%" "^A-Za-z0-9" ":;.,!?")))
987 ;; (case-fold-search t), why?
988 (cond
989 ((string-match "^url:" name) (setq name (substring name 4)))
990 ((and (string-match "\\`[^:</>@]+@[^:</>@]+[a-zA-Z]\\'" name)
991 ;; "foo@bar": could be "mailto" or "news" (a Message-ID).
992 ;; If not adorned with "<>", it must be "mailto".
993 ;; Otherwise could be either, so consult `ffap-foo@bar-prefix'.
994 (let ((prefix (if (and (equal (ffap-string-around) "<>")
995 ;; At least a couple of odd characters:
996 (string-match "[$.0-9].*[$.0-9].*@" name))
997 ;; Could be news:
998 ffap-foo@bar-prefix
999 "mailto")))
1000 (and prefix (setq name (concat prefix ":" name))))))
1001 ((ffap-newsgroup-p name) (setq name (concat "news:" name)))
1002 ((and (string-match "\\`[a-z0-9]+\\'" name) ; <mic> <root> <nobody>
1003 (equal (ffap-string-around) "<>")
1004 ;; (ffap-user-p name):
1005 (not (string-match "~" (expand-file-name (concat "~" name))))
1006 )
1007 (setq name (concat "mailto:" name)))
1008 )
1009 (and (ffap-url-p name) name)
1010 ))))
1011
1012(defvar ffap-gopher-regexp
1013 "^.*\\<\\(Type\\|Name\\|Path\\|Host\\|Port\\) *= *\\(.*\\) *$"
1014 "Regexp Matching a line in a gopher bookmark (maybe indented).
1015Two subexpressions are the KEY and VALUE.")
1016
1017(defun ffap-gopher-at-point nil
1018 "If point is inside a gopher bookmark block, return its url."
1019 ;; We could use gopher-parse-bookmark from gopher.el, but it is not
1020 ;; so robust, and w3 users are better off without gopher.el anyway.
1021 (save-excursion
1022 (beginning-of-line)
1023 (if (looking-at ffap-gopher-regexp)
1024 (progn
1025 (while (and (looking-at ffap-gopher-regexp) (not (bobp)))
1026 (forward-line -1))
1027 (or (looking-at ffap-gopher-regexp) (forward-line 1))
1028 (let ((type "1") name path host (port "70"))
1029 (while (looking-at ffap-gopher-regexp)
1030 (let ((var (intern
1031 (downcase
1032 (buffer-substring (match-beginning 1)
1033 (match-end 1)))))
1034 (val (buffer-substring (match-beginning 2)
1035 (match-end 2))))
1036 (set var val)
1037 (forward-line 1)))
1038 (if (and path (string-match "^ftp:.*@" path))
1039 (concat "ftp://"
1040 (substring path 4 (1- (match-end 0)))
1041 (substring path (match-end 0)))
1042 (and (= (length type) 1)
1043 host;; (ffap-machine-p host)
1044 (concat "gopher://" host
1045 (if (equal port "70") "" (concat ":" port))
1046 "/" type path))))))))
1047
1048(defvar ffap-ftp-sans-slash-regexp
1049 (and
1050 ffap-ftp-regexp
1051 ;; Note: by now, we know it is not an URL.
1052 ;; Icky regexp avoids: default: 123: foo::bar cs:pub
1053 ;; It does match on: mic@cs: cs:/pub mathcs.emory.edu: (point at end)
1054 ;; Todo: handle foo.com://path
1055 "\\`\\([^:@]+@[^:@]+:\\|[^@.:]+\\.[^@:]+:\\|[^:]+:[~/]\\)\\([^:]\\|\\'\\)")
1056 "Strings matching this are coerced to ftp paths by ffap.
1057That is, ffap just prepends \"/\". Set to nil to disable.")
1058
1059(defun ffap-file-at-point nil
1060 "Return filename from around point if it exists, or nil.
1061Existence test is skipped for names that look remote.
1062If the filename is not obvious, it also tries `ffap-alist',
1063which may actually result in an URL rather than a filename."
1064 ;; Note: this function does not need to look for URL's, just
1065 ;; filenames. On the other hand, it is responsible for converting
1066 ;; a pseudo-URL "site.dom://path" to an ftp path "/site.dom:/path"
1067 (let* ((case-fold-search t) ; url prefixes are case-insensitive
1068 (data (match-data))
1069 (string (ffap-string-at-point)) ; use its mode-alist
1070 (name
1071 (condition-case nil
1072 (substitute-in-file-name string)
1073 (error string)))
1074 (abs (file-name-absolute-p name))
1075 (default-directory default-directory))
1076 (unwind-protect
1077 (cond
1078 ;; Immediate rejects (/ and // are too common in C++):
1079 ((member name '("" "/" "//")) nil)
1080 ;; Immediately test local filenames. If default-directory is
1081 ;; remote, you probably already have a connection.
1082 ((and (not abs) (ffap-file-exists-string name)))
1083 ;; Accept remote names without actual checking (too slow):
1084 ((if abs
1085 (ffap-file-remote-p name)
1086 ;; Try adding a leading "/" (common omission in ftp paths):
1087 (and
1088 ffap-ftp-sans-slash-regexp
1089 (string-match ffap-ftp-sans-slash-regexp name)
1090 (ffap-file-remote-p (concat "/" name)))))
1091 ;; Ok, not remote, try the existence test even if it is absolute:
1092 ((and abs (ffap-file-exists-string name)))
1093 ;; File does not exist, try the alist:
1094 ((let ((alist ffap-alist) tem try case-fold-search)
1095 (while (and alist (not try))
1096 (setq tem (car alist) alist (cdr alist))
1097 (if (or (eq major-mode (car tem))
1098 (and (stringp (car tem))
1099 (string-match (car tem) name)))
1100 (and (setq try (funcall (cdr tem) name))
1101 (setq try (or
1102 (ffap-url-p try) ; not a file!
1103 (ffap-file-remote-p try)
1104 (ffap-file-exists-string try))))))
1105 try))
1106 ;; Alist failed? Try to guess an active remote connection
1107 ;; from buffer variables, and try once more, both as an
1108 ;; absolute and relative path on that remote host.
1109 ((let* (ffap-rfs-regexp ; suppress
1110 (remote-dir
1111 (cond
1112 ((ffap-file-remote-p default-directory))
1113 ((and (eq major-mode 'internal-ange-ftp-mode)
1114 (string-match "^\\*ftp \\(.*\\)@\\(.*\\)\\*$"
1115 (buffer-name)))
1116 (concat "/" (substring (buffer-name) 5 -1) ":"))
1117 ;; This is too often a bad idea:
1118 ;;((and (eq major-mode 'w3-mode)
1119 ;; (stringp url-current-server))
1120 ;; (host-to-ange-path url-current-server))
1121 )))
1122 (and remote-dir
1123 (or
1124 (and (string-match "\\`\\(/?~?ftp\\)/" name)
1125 (ffap-file-exists-string
1126 (ffap-replace-path-component
1127 remote-dir (substring name (match-end 1)))))
1128 (ffap-file-exists-string
1129 (ffap-replace-path-component remote-dir name))))))
1130 )
1131 (store-match-data data))))
1132
1133\f
1134;;; ffap-read-file-or-url:
1135;;
1136;; Want to read filenames with completion as in read-file-name, but
1137;; also allow URL's which read-file-name-internal would truncate at
1138;; the "//" string. Solution here is to replace read-file-name-internal
1139;; with another function that does not attempt to complete url's.
1140
1141;; We implement a pretty clean completion semantics to work with
1142;; packages like complete.el and exit-minibuffer.el. Even for
1143;; complete.el (v19.22), we still need to make a small patch (it has a
1144;; hardwired list of `minibuffer-completion-table' values which it
1145;; considers to deal with filenames, this ought to be a variable).
1146
1147(defun ffap-read-file-or-url (prompt guess)
1148 "Read a file or url from minibuffer, with PROMPT and initial GUESS."
1149 (or guess (setq guess default-directory))
1150 (let ((filep (not (ffap-url-p guess))) dir)
1151 ;; Tricky: guess may have or be a local directory, like "w3/w3.elc"
1152 ;; or "w3/" or "../el/ffap.el" or "../../../"
1153 (if filep
1154 (progn
1155 (or (ffap-file-remote-p guess)
1156 (setq guess (abbreviate-file-name (expand-file-name guess))))
1157 (setq dir (file-name-directory guess))))
1158 (apply
1159 'completing-read
1160 prompt
1161 'ffap-read-file-or-url-internal
1162 dir
1163 nil
1164 (if (and dir) (cons guess (length dir)) guess)
1165 (list 'file-name-history)
1166 )))
1167
1168(defvar url-global-history-completion-list nil) ; variable in w3/url.el
1169
1170(defun ffap-read-url-internal (string dir action)
1171 ;; Complete URL's from history, always treat given url as acceptable.
1172 (let ((hist url-global-history-completion-list))
1173 (cond
1174 ((not action)
1175 (or (try-completion string hist) string))
1176 ((eq action t)
1177 (or (all-completions string hist) (list string)))
1178 ;; lambda?
1179 (t string))))
1180
1181(defun ffap-read-file-or-url-internal (string dir action)
1182 (if (ffap-url-p string)
1183 (ffap-read-url-internal string dir action)
1184 (read-file-name-internal string dir action)))
1185
1186;; Unfortunately, for complete.el to work correctly, we need to vary
1187;; the value it sees of minibuffer-completion-table, depending on the
1188;; current minibuffer contents! It would be nice if it were written a
1189;; little more easily. I consider this a bug in complete.el, since
1190;; the builtin emacs functions do not have this problem.
1191(and
1192 (featurep 'complete)
1193 (require 'advice)
1194 (defadvice PC-do-completion (around ffap-fix act)
1195 "Work with ffap.el."
1196 (let ((minibuffer-completion-table minibuffer-completion-table)
1197 ;; (minibuffer-completion-predicate minibuffer-completion-predicate)
1198 )
1199 (and (eq minibuffer-completion-table 'ffap-read-file-or-url-internal)
1200 (setq minibuffer-completion-table
1201 (if (ffap-url-p (buffer-string))
1202 ;; List would work better with icomplete ...
1203 'ffap-read-url-internal
1204 'read-file-name-internal)))
1205 ad-do-it)))
1206
1207\f
1208;;; Highlighting:
1209;;
1210;; Based on overlay highlighting in Emacs 19.28 isearch.el.
1211
1212(defvar ffap-highlight (and window-system t)
1213 "If non-nil, ffap highlights the current buffer substring.")
1214
1215(defvar ffap-overlay nil "Overlay used by ffap-highlight.")
1216
1217(defun ffap-highlight (&optional remove)
1218 "If `ffap-highlight' is set, highlight the guess in the buffer.
1219That is, the last buffer substring found by ffap-string-at-point.
1220Optional argument REMOVE means to remove any such highlighting.
1221Uses the face `ffap' if it is defined, else `highlight'."
1222 (cond
1223 (remove (and ffap-overlay (delete-overlay ffap-overlay)))
1224 ((not ffap-highlight) nil)
1225 (ffap-overlay
1226 (move-overlay ffap-overlay
1227 (car ffap-string-at-point-region)
1228 (nth 1 ffap-string-at-point-region)
1229 (current-buffer)))
1230 (t
1231 (setq ffap-overlay (apply 'make-overlay ffap-string-at-point-region))
1232 (overlay-put ffap-overlay 'face
1233 (if (internal-find-face 'ffap nil)
1234 'ffap 'highlight)))))
1235\f
1236;;; The big enchilada:
1237
1238(defun ffap-guesser nil
1239 "Return file or URL or nil, guessed from text around point."
1240 (or (and ffap-url-regexp
1241 (ffap-fixup-url (or (ffap-url-at-point)
1242 (ffap-gopher-at-point))))
1243 (ffap-file-at-point) ; may yield url!
1244 (ffap-fixup-machine (ffap-machine-at-point))))
1245
1246(defun ffap-prompter (&optional guess)
1247 ;; Does guess and prompt step for find-file-at-point.
1248 ;; Extra complication just to do the temporary highlighting.
1249 (unwind-protect
1250 (ffap-read-file-or-url
1251 (if ffap-url-regexp "Find file or URL: " "Find file: ")
1252 (prog1
1253 (setq guess (or guess (ffap-guesser)))
1254 (and guess (ffap-highlight))))
1255 (ffap-highlight t)))
1256
1257;;;###autoload
1258(defun find-file-at-point (&optional filename)
1259 "Find FILENAME (or url), guessing default from text around point.
1260If `ffap-dired-wildcards' is set, wildcard patterns are passed to dired.
1261See also the functions ffap-file-at-point, ffap-url-at-point.
1262With a prefix, this command behaves *exactly* like `ffap-file-finder'.
1263If `ffap-require-prefix' is set, the prefix meaning is reversed.
1264
1265See ftp://ftp.mathcs.emory.edu/pub/mic/emacs/ for most recent version."
1266 (interactive)
1267 (if (and (interactive-p)
1268 (if ffap-require-prefix (not current-prefix-arg)
1269 current-prefix-arg))
1270 ;; Do exactly the ffap-file-finder command, even the prompting:
1271 (call-interactively ffap-file-finder)
1272 (or filename (setq filename (ffap-prompter)))
1273 (cond
1274 ((ffap-url-p filename)
1275 (funcall ffap-url-fetcher filename))
1276 ;; This junk more properly belongs in a modified ffap-file-finder:
1277 ((and ffap-dired-wildcards (string-match ffap-dired-wildcards filename))
1278 (dired filename))
1279 ((or (not ffap-newfile-prompt)
1280 (file-exists-p filename)
1281 (y-or-n-p "File does not exist, create buffer? "))
1282 (funcall ffap-file-finder
1283 ;; expand-file-name fixes "~/~/.emacs" bug sent by CHUCKR.
1284 (expand-file-name filename)))
1285 ;; User does not want to find a non-existent file:
1286 ((signal 'file-error (list "Opening file buffer"
1287 "no such file or directory"
1288 filename))))))
1289
1290;; M-x shortcut:
1291(fset 'ffap 'find-file-at-point)
1292
1293\f
1294;;; Menu support:
1295;;
1296;; Bind ffap-menu to a key if you want, since it also works in tty mode.
1297;; Or just use it through the ffap-at-mouse binding (next section).
1298
1299(defvar ffap-menu-regexp nil
1300 "*If non-nil, overrides `ffap-next-regexp' during ffap-menu.
1301Make this more restrictive for faster menu building.
1302For example, try \":/\" for url (and some ftp) references.")
1303
1304(defvar ffap-menu-alist nil
1305 "Buffer local menu of files and urls cached by ffap-menu.")
1306(make-variable-buffer-local 'ffap-menu-alist)
1307
1308;;;###autoload
1309(defun ffap-menu (&optional rescan)
1310 "Puts up a menu of files and urls mentioned in the buffer.
1311Sets mark, jumps to choice, and tries to fetch it.
1312Menu is cached in `ffap-menu-alist', but will always be rebuilt
1313with the optional RESCAN argument (a prefix interactively).
1314Searches buffer with `ffap-menu-regexp' (see `ffap-next-regexp')."
1315 (interactive "P")
1316 ;; (require 'imenu) -- no longer used, but roughly emulated
1317 (if (or (not ffap-menu-alist) rescan
1318 ;; or if the first entry is wrong:
1319 (and ffap-menu-alist
1320 (let ((first (car ffap-menu-alist)))
1321 (save-excursion
1322 (goto-char (cdr first))
1323 (not (equal (car first) (ffap-guesser)))))))
1324 (ffap-menu-rescan))
1325 ;; Tail recursive:
1326 (ffap-menu-ask
1327 (if ffap-url-regexp "Find file or URL" "Find file")
1328 (cons (cons "*Rescan Buffer*" -1) ffap-menu-alist)
1329 'ffap-menu-cont))
1330
1331(defun ffap-menu-cont (choice) ; continuation of ffap-menu
1332 (if (< (cdr choice) 0)
1333 (ffap-menu t) ; *Rescan*
1334 (push-mark)
1335 (goto-char (cdr choice))
1336 ;; Momentary highlight:
1337 (unwind-protect
1338 (progn
1339 (and ffap-highlight (ffap-guesser) (ffap-highlight))
1340 (sit-for 0) ; display
1341 (find-file-at-point (car choice)))
1342 (ffap-highlight t))))
1343
1344(defun ffap-menu-ask (title alist cont)
1345 "Prompt from a menu of choices, and then apply some action.
1346Arguments are TITLE, ALIST, and CONT (a continuation).
1347This uses either a menu or the minibuffer depending on invocation.
1348The TITLE string is used as either the prompt or menu title.
1349Each (string . data) entry in ALIST defines a choice (data is ignored).
1350Once the user makes a choice, function CONT is applied to the entry.
1351Always returns nil."
1352 ;; Bug: minibuffer prompting assumes the strings are unique.
1353 ;; Todo: break up long menus into multiple panes (like imenu).
1354 (let ((choice
1355 (if (and (fboundp 'x-popup-menu) ; 19 or XEmacs 19.13
1356 (boundp 'last-nonmenu-event) ; not in XEmacs 19.13
1357 (listp last-nonmenu-event))
1358 (x-popup-menu
1359 t
1360 (list ""
1361 (cons title
1362 (mapcar
1363 (function (lambda (i) (cons (car i) i)))
1364 alist))))
1365 ;; Automatically popup completion help, one way or another:
1366 (let ((minibuffer-setup-hook 'minibuffer-completion-help)
1367 (unread-command-char -1))
1368 ;; BUG: this code assumes that "" is not a valid choice
1369 (completing-read
1370 (format "%s (default %s): " title (car (car alist)))
1371 alist nil t
1372 ;; Let first be default:
1373 ;; (if ffap-v18 (car (car alist))
1374 ;; (cons (car (car alist)) 0))
1375 ;; No, then you do not get all completions!
1376 nil
1377 )))))
1378 ;; Defaulting: convert "" to (car (car alist))
1379 (and (equal choice "") (setq choice (car (car alist))))
1380 (and (stringp choice) (setq choice (assoc choice alist)))
1381 (if choice (funcall cont choice) (message "No choice made!")))
1382 nil) ; return nothing
1383
1384(defun ffap-menu-rescan nil
1385 (interactive)
1386 (let ((ffap-next-regexp (or ffap-menu-regexp ffap-next-regexp))
1387 (range (- (point-max) (point-min))) item)
1388 (setq ffap-menu-alist nil)
1389 (save-excursion
1390 (goto-char (point-min))
1391 (while (setq item (ffap-next-guess))
1392 (setq ffap-menu-alist (cons (cons item (point)) ffap-menu-alist))
1393 (message "Scanning...%2d%% <%s>"
1394 (/ (* 100 (- (point) (point-min))) range) item))))
1395 (message "Scanning...done")
1396 ;; Remove duplicates.
1397 (setq ffap-menu-alist ; sort by item
1398 (sort ffap-menu-alist
1399 (function
1400 (lambda (a b) (string-lessp (car a) (car b))))))
1401 (let ((ptr ffap-menu-alist))
1402 (while (cdr ptr)
1403 (if (equal (car (car ptr)) (car (car (cdr ptr))))
1404 (setcdr ptr (cdr (cdr ptr)))
1405 (setq ptr (cdr ptr)))))
1406 (setq ffap-menu-alist ; sort by position
1407 (sort ffap-menu-alist
1408 (function
1409 (lambda (a b) (< (cdr a) (cdr b)))))))
1410
1411\f
1412;;; Mouse Support:
1413;;
1414;; I suggest a mouse binding, something like:
1415;; (global-set-key [S-mouse-1] 'ffap-at-mouse)
1416
1417(defvar ffap-at-mouse-fallback 'ffap-menu
1418 "Invoked by ffap-at-mouse if no file or url found at point.
1419A command symbol, or nil for nothing.")
1420(put 'ffap-at-mouse-fallback 'risky-local-variable t)
1421
1422(defun ffap-at-mouse (e)
1423 "Find file or URL guessed from text around mouse point.
1424If none is found, call `ffap-at-mouse-fallback'."
1425 (interactive "e")
1426 (let ((guess
1427 ;; Maybe less surprising without the save-excursion?
1428 (save-excursion
1429 (mouse-set-point e)
1430 ;; Would like to do nothing unless click was *on* text. How?
1431 ;; (cdr (posn-col-row (event-start e))) is always same as
1432 ;; current column. For posn-x-y, need pixel-width!
1433 (ffap-guesser))))
1434 (cond
1435 (guess
1436 (ffap-highlight)
1437 (unwind-protect
1438 (progn
1439 (sit-for 0) ; display
1440 (message "Guessing `%s'" guess)
1441 (find-file-at-point guess))
1442 (ffap-highlight t)))
1443 ((and (interactive-p)
1444 ffap-at-mouse-fallback)
1445 (call-interactively ffap-at-mouse-fallback))
1446 ((message "No file or URL found at mouse click.")))))
1447
1448\f
1449;;; ffap-other-* commands
1450;; Suggested by KPC. Possible bindings for C-x 4 C-f, C-x 5 C-f.
1451
1452(defun ffap-other-window nil
1453 "Like ffap, but put buffer in another window."
1454 (interactive)
1455 (switch-to-buffer-other-window
1456 (save-window-excursion (call-interactively 'ffap) (current-buffer))))
1457
1458(defun ffap-other-frame nil
1459 "Like ffap, but put buffer in another frame."
1460 (interactive)
1461 (switch-to-buffer-other-frame
1462 (save-window-excursion (call-interactively 'ffap) (current-buffer))))
1463
1464\f
1465;;; ffap-bug:
1466(defun ffap-bug nil
1467 ;; Tested with Emacs 19.28 reporter.el
1468 "Submit a bug report for ffap."
1469 (interactive)
1470 (require 'reporter)
1471 (let ((reporter-prompt-for-summary-p t))
1472 (reporter-submit-bug-report
1473 "mic@mathcs.emory.edu" "ffap "
1474 (mapcar 'intern (all-completions "ffap-" obarray 'boundp))
1475 )))
1476(fset 'ffap-submit-bug 'ffap-bug) ; another likely name
1477
1478\f
1479;;; Todo, End.
1480;;
1481;; * w3 may eventually make URL's part of the filesystem!
1482;; this package (prompt & completion) could become much simpler
1483;; * improve minibuffer-completion-help display of long completions
1484;; * notice "machine.dom blah blah blah path/file" (how?)
1485;; * check X selections (x-get-selection PRIMARY/SECONDARY LENGTH/TEXT)
1486;; * let "/path/file#key" jump to key (anchor or regexp) in /path/file
1487;; * notice node in "(dired)Virtual Dired" (how to handle space?)
1488;; * try find-tag on symbol if TAGS is loaded (need above)
1489;;
1490;; For information on URL/URI syntax, try:
1491;; <http://ds.internic.net/rfc/rfc1630.txt>
1492;; <http://www.w3.org/hypertext/WWW/Protocols/Overview.html>
1493;; <http://info.cern.ch/hypertext/WWW/Addressing/Addressing.html>
1494
1495;; Local Variables?
1496;; foo: bar
1497;; End:
1498
1499
1500;;; ffap.el ends here