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