2

There is a stock emacs function called append-to-file that is defined as follows (in files.el):

(defun append-to-file (start end filename)
  "Append the contents of the region to the end of file FILENAME.
When called from a function, expects three arguments,
START, END and FILENAME.  START and END are buffer positions
saying what text to write."
  (interactive "r\nFAppend to file: ")
  (write-region start end filename t))

I'm running spacemacs, which runs helm. When this stock function is called via M x append-to-file, an interactive prompt comes up that seems to use helm-find-files to help the user find a file to input into this stock function.

Question: How does this emacs function -- which was presumably written years before helm ever was -- know to call helm-find-files when prompting the user for an input file in the minibuffer?

Motivation: The reason why I'm curious about this is that I'm ultimately wanting to call a different interactive file-finding function (one other than helm-find-files) with this function (for example I'd like it to invoke helm-projectile-find-file which is slightly different).


EDIT: Using bmag's answer, I have a partial solution that doesn't use helm-projectile-find-file but instead uses the shell find command to get a list of files I'm interested in.

In particular, I'm interested in the list of files brought up by the shell command find -L /path/to/files -wholename */capture.md). How can I put this result into :sources below and have the appropriate buffers come up?

(defun helm-append-to-file (start end)
  (interactive "r")
  (let ((filename
         (helm :sources ... ;; <--what do I put here?
               :buffer "*helm append to file*"
               :prompt "File to append to: ")))
    (append-to-file start end filename)))
9
  • I think you could do this with customizing helm-completing-read-handlers-alist, but I am not completely sure how to set it correctly to your needs, probably something like (add-to-list 'helm-completing-read-handlers-alist '((ido-find-file . helm-projectile-find-file))) or (add-to-list 'helm-completing-read-handlers-alist '((append-to-file . helm-projectile-find-file))) will do.
    – theldoria
    Jul 11, 2017 at 4:52
  • @theldoria: That conceptually seems like it should do it, but neither of those options worked for some reason.
    – George
    Jul 11, 2017 at 12:10
  • @theldoria: I added a 50 point bounty to this question. If you could help me refine your solution to something that works I would gladly fork over the 50 points :)
    – George
    Jul 14, 2017 at 6:35
  • Hello George. What is your current value of the variable helm-completing-read-handlers-alist? Does it contain append-to-file? Jul 14, 2017 at 9:09
  • 2
    BTW, It is much easier to write your own version of append-to-file.
    – xuchunyang
    Jul 14, 2017 at 10:45

2 Answers 2

3
+50

As @db48x wrote, append-to-file uses Emacs' default completion facilities, which means calling the function completion-read. In turn, the variable completing-read-function determines what backend to use for completion. By controlling completion-read-function you decide what completion interface you want. Reading file names is a bit different, because it's done by read-file-name which is controlled by read-file-name-function. However, the default behavior is that read-file-name calls completing-read.

When helm-mode is enabled, it sets completing-read-function to helm--completing-read-default and read-file-name-function to helm--generic-read-file-name. In addition, in newer Emacs version (24.4+) Helm uses add-function to override these variables instead of setq. I assume that's because with setq it needs to know the original value when disabling helm-mode, but with add-function it doesn't.

The completion you get when invoking append-to-file is actually a generic helm file name completion, which isn't as heavy and feature-rich as the command helm-find-files. In fact, most Helm commands don't use interactive to read arguments, because they want more features than the generic Helm completion. Instead, they have a minimal interactive declaration and call helm function for reading arguments and performing actions, thus utilizing Helm's features. Notably, this includes helm-find-files and helm-projectile-find-file.

To change the default completion backend, you can just disable helm-mode and set completing-read-function and read-file-name-function to whatever you wish. Helm commands will still function through Helm as usual, and won't be affected by this change.

Alternatively, if you want append-to-file to use different completion, but still use Helm as the general completion backend, you will need to wrap append-to-file in another command. That command should read the arguments and then call append-to-file (non-interactively). With Helm, you could start with the following and modify according to your needs and how much of Helm's features you want to use:

(defun helm-append-to-file (start end)
  (interactive "r")
  (let ((filename
         (helm :sources some-helm-sources
               :buffer "*helm append to file*"
               :prompt "File to append to: ")))
    (append-to-file start end filename)))

Note that if (helm ...) changes the current buffer or region then append-to-file won't append the region you expect it to, so you'll want to wrap (helm ...) in a save-excursion or similar. You could use :candidates instead of :sources, but at that point you won't be much better off than the generic Helm completion. You might be lucky enough to find suitable pre-existing sources, so you don't need to write your own, but otherwise you will have to learn how to write a Helm source. You can look at helm-projectile.el and the macro helm-projectile-command to see how helm-projectile commands are defined.

For more info on Emacs completion you can visit section 19.6 of the elisp manual or read the relevant functions' documentation.

EDIT: Using find and Helm

The solution below uses the shell command find to get a list of filenames. I use split-string so the files show up in the Helm prompt without the common prefix (instead I include the common prefix in the source's name). Because of that, I need to use f-join to so append-to-file gets the correct filename. Note that Helm has a transformers feature to alter how candidates are displayed, but I didn't use them here since I'm less familiar with them.

Using f-join brings a dependency on f.el, which you will need to install (in Spacemacs it's already installed). Using seq-map brings a dependency on seq.el, which requires Emacs 25.1 or newer. Both of these can be replaced by functions available in Emacs 24.5 and older (cl-map instead of seq-map, several string and filename functions instead of f-join).

(require 'f)
(require 'helm)
(require 'seq)

(defun helm-append-to-file (start end)
  (interactive "r")
  (let* ((path "/path/to/files")
        (relative-filename
          (helm :sources (helm-build-sync-source "append"
                          :candidates (get-candidates path))
                :buffer "*helm append to file*"
                :prompt "File to append to: "))
        (filename (f-join path relative-filename)))
    (append-to-file start end filename)))

(defun get-candidates (path)
  "Get names of capture files.
Return a list of filenames. Each filename is relative to PATH."
  ;; use -print0 just in case a filename contains a newline
  (let ((find-command (format "find -L '%s' -wholename '*/capture.md' -print0" path)))
    ;; remove common path prefix so candidates are easier to read in helm prompt
    (seq-map (lambda (s) (string-remove-prefix path s))
            ;; "\0" matches -print0
            (split-string (shell-command-to-string find-command) "\0" t))))
3
  • This is a great answer, but it's still unclear to me how to use helm-projectile-find-file in, i.e., the sample function that you provided (helm-append-to-file).
    – George
    Jul 15, 2017 at 15:59
  • I attempted a solution based off your helm-append-to-file and edited my question. Can you take a look at it?
    – George
    Jul 15, 2017 at 16:19
  • Your edit doesn't state what you tried. I updated my answer with a working solution. Keep in mind that it isn't the most Helm-idiomatic code, due to the lack of transformers, but for private usage I think it's good enough.
    – bmag
    Jul 16, 2017 at 7:46
3

I may not be able to supply the full answer, but here's how you can find out.

append-to-file is an interactive function, so its arguments are described in the (interactive ...) declaration just before the function body. This is stringly-typed data, which is annoying (and there's not much we can do about it now); the argument types are given by single-letter codes embedded in the string. You can look up some pretty good help information just by running C-h f interactive <RET>, but better documentation is in chapter 20.2.2 of the the Elisp manual (C-h i to open the list of manuals, or find it online). We can see that the F code means to input a filename:

‘F’
     A file name.  The file need not exist.  Completion, Default,
     Prompt.

This is a bit terse, but if you look at the top of this chapter you'll find that Completion signifies that Emacs will first call completing-read to prompt the user for the filename:

Completion
     Provide completion.  <TAB>, <SPC>, and <RET> perform name
     completion because the argument is read using ‘completing-read’
     (*note Completion::).  ‘?’ displays a list of possible completions.

Helm thus needs only to modify how completing-read works; it could do this by redefining it, or by adding advice to it, but completion is quite a complicated feature so Helm is likely to be more subtle than that. completing-read calls into a number of other functions for various things, there are many variables that control the behavior of these functions, etc; see chapter 19.6 of the Elisp manual.

Have fun :)

Your Answer

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

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