3

How can I get ffap to offer a non-existent file?

I'm in a buffer with point inside something that looks like some/directory/structure/a_long_file_name.ext, where a directory some/directory/structure exists but there is no file called a_long_file_name.ext. I want to create that file. Obviously I can select the path, copy it, and yank it at the find-file prompt, but the point of ffap is to not have to do that. If I run M-x ffap, it highlights the whole path but only offers the existing prefix in the minibuffer.

Steps to reproduce: emacs -Q, type /etc/foo then M-x ffap RET. Observed behavior: /etc/foo is highlighted, but the minibuffer offers /etc/. Desired behavior: the minibuffer offers /etc/foo.

I'm using Emacs 24.5 or 25.2 with no higher-level package, just plain ffap.el.

1 Answer 1

2

Simple Solution

One can fake the file's existence with an around advice for ffap-file-exists-string:

(defun ffap-accept-all (fun file &optional nomodify)
  "Around advice for `ffap-file-exists-string'."
  (or (funcall fun file nomodify) file))

(advice-add 'ffap-file-exists-string :around #'ffap-accept-all)

Refined Solution

The simple solution leads to the problem that appended line numbers like :9 are included in the default file name.

ffap-file-at-point accepts such a file name without doing any other tests because it assumes that a file with that name exists (see the corresponding comment by Gilles).

We can ensure that many of the tests in ffap-file-at-point are taken care of if we let ffap-file-exists-string only accept any file when ffap-alist is already tested.

That is what the following Elisp magick does:

(require 'ffap)

(defun ffap-file-at-point-ad (fun)
  "Around advice for `ffap-file-at-point' as FUN.
Accept any file name after testing ffap-alist."
  (cl-letf*
      ((ffap-always-exists nil)
       (old-ffap-file-exists-string (symbol-function 'ffap-file-exists-string))
       (ffap-alist (append ffap-alist
                           (list
                            (cons ""
                                  (lambda (name)
                                    (setq ffap-always-exists t)
                                    nil)))))
       ((symbol-function 'ffap-file-exists-string)
        (lambda (file &optional nomodify)
          (or
           (funcall old-ffap-file-exists-string file nomodify)
           (and ffap-always-exists
                file)))))
    (funcall fun)))

(advice-add 'ffap-file-at-point :around #'ffap-file-at-point-ad)
9
  • That works, but it isn't ideal, since ffap tries a bunch of heuristics to strip off suffixes that aren't supposed to be part of the file name, and this bypasses those heuristics. For example, ffap on /etc/profile:3 now offers /etc/profile:3. Looking at the code of ffap-file-at-point, I think what I'd like is to only suppress the parent directory lookup loop near the end, which is unfortunately not sanely hot-patchable. Commented Jul 31, 2019 at 14:28
  • @Gilles Please try again. Now, essentially only some special kind of remote file names can cause problems.
    – Tobias
    Commented Jul 31, 2019 at 16:18
  • Oh wow, nice trick! Commented Jul 31, 2019 at 16:23
  • @Tobias The code depends on dynamic-binding of ffap-always-exists, I had to add (defvar ffap-always-exists nil) to make it work in the file where I defined it (which uses lexical binding).
    – clemera
    Commented Aug 2, 2019 at 8:25
  • 1
    @clemera The lambda is self-quoting. But it uses function instead of quote for quoting. (function (lambda...)) in a lexical environment evaluates to closure instead of lambda. The variables of the local lexical scope are available in the closure but not in the lambda. The quoted list is read as it is -- lambda remains lambda. For that reason your statement is right. In the closure the value of the uninterned symbol ffap-always-exists of the lexical environment is set. In the lambda the value of the symbol ffap-always-exists in the global obarray is set.
    – Tobias
    Commented Aug 2, 2019 at 9:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.