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 Answers1

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)
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • 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. – Gilles 'SO- stop being evil' Jul 31 '19 at 14:28
  • @Gilles Please try again. Now, essentially only some special kind of remote file names can cause problems. – Tobias Jul 31 '19 at 16:18
  • @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 Aug 02 '19 at 08:25
  • @clemera Oh. I think that was the bad quote. Could you try again with lexical binding but without the `defvar`? If you want to keep your Emacs session just call `M-: (unintern "ffap-always-exists" obarray)`. That clears the variable from `obarray`. – Tobias Aug 02 '19 at 08:37
  • @Tobias Yes, that works as well. Thanks! If I understand it correctly the quote prevented that the lexical defined ffap-always-exists changed its value. So when setting it via `(setq ffap-always-exists t)` it would set a global variable and not the lexical defined one, right? – clemera Aug 02 '19 at 08:47
  • 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 Aug 02 '19 at 09:49
  • @Tobias Thanks for breaking it down like this, I have a much clearer picture now. – clemera Aug 02 '19 at 09:54
  • @Gilles ping, so you don't miss the updated version. – clemera Aug 02 '19 at 10:15