0

summary

Elisp newbie has been given interactive code (aka commands, called with M-x whatever) that "does stuff" on the currently-selected region in the current buffer. What is an/the "elispish way" to scale that code up to handle all of the following (and all interactive) usecases:

  1. (current) If called with (no argument && region selected in the current buffer):
    • do something on the text in the region
  2. If called with (no argument && no region selected in the current buffer):
    • do something on all the text in the buffer
  3. If called with (C-u filepath M-x whatever) (more on this below)
    • evaluate the argument as a filepath
    • do something on all the text in the file

?

example

I have code in this repo that currently "does stuff" (usecase detailed in thread beginning here) on a region in a buffer. The relevant code is part of this file (adapted from original code by Pascal Bourguignon):

(defun bulk-replace-region-with-tuples-from-file (start end)
  (interactive "r") ; need "r" to get region start and end
  (save-excursion
    (narrow-to-region start end)
    (goto-char (point-min))
    (replace-multiple-strings   ; the payload
      (with-file BULK-REPLACE-TUPLES-FILEPATH ; defined earlier in the test code
        (goto-char (point-min))
        (read (current-buffer))
      )
    )
    (widen)
  )
)

That works iff ∃region selected in the current buffer, i.e., usecase#=1 above. How to scale to handle the others?

answer-in-progress

Following wvxvw's guidance (below), I have

0. later discovered that a prefix argument cannot be a string! Dunno why I thought this: I've been using Emacs forever. Instead, if given a non-nil prefix argument, I'll hafta prompt for a filepath.

  1. Simply refactored bulk-replace-region-with-tuples-from-file above into a non-interactive helper bulk-replace-region-non-interactive, merely by removing the (interactive "r").
  2. Created a bulk-replace-buffer-non-interactive that simply wraps bulk-replace-region-non-interactive:
    (defun bulk-replace-buffer-non-interactive ()
      (bulk-replace-region-non-interactive (point-min) (point-max))
    )
  1. Created a bulk-replace-file-non-interactive that simply wraps bulk-replace-buffer-non-interactive:
    (defun bulk-replace-file-non-interactive (filepath)
      (set-buffer (find-file-noselect filepath))
      (bulk-replace-buffer-non-interactive)
      (save-buffer)
      (kill-buffer)
    )
  1. Created a hardcoded but interactive solution to usecase#=1 by simply wrapping its non-interactive helper. It's mostly for testing the helper, but it's a useful API as well as a "step along the path":
    (defun bulk-replace-region (begin end)
      (interactive "r") ; need "r" to get region begin and end
      (bulk-replace-region-non-interactive begin end) 
    )
  1. Created a hardcoded but interactive solution to usecase#=2 very similar to previous step:
    (defun bulk-replace-buffer ()
      (interactive) ; purely to enable `M-x`
      (bulk-replace-buffer-non-interactive)
    )
  1. Sorta/kinda handled usecase#=3 in a manner similar to the previous 2 steps: a hardcoded but interactive solution which
    ; Don't try this! see https://emacs.stackexchange.com/q/19082/5444
    ; (defun bulk-replace-file (filepath)
    ;   (interactive (concat "F" BULK-REPLACE-FILEPATH-PROMPT))

    (defun bulk-replace-file ()
      (interactive) ; purely to enable `M-x`
      (bulk-replace-file-non-interactive
        ; `(read-file-name)` does the prompting
        (expand-file-name (read-file-name BULK-REPLACE-FILEPATH-PROMPT))
      ) 
    )
  1. Handled all the usecases (hopefully--pending more complete testing) with an über-wrapper that hides {API, separate code paths} from the user:
    (defun bulk-replace (prefix-argument)
      (interactive "P")  ; to catch prefix argument (if given)
      (cond
        (prefix-argument ; got one, so prompt user for filepath
          (bulk-replace-file)
        )
        ((use-region-p)  ; we have a region, so bulk-replace-region
          (bulk-replace-region-non-interactive (region-beginning) (region-end))
        )
        (t               ; we always have a current-buffer, so bulk-replace-buffer
          (bulk-replace-buffer-non-interactive)
        )
      )
    )

It works! but needs more, better, automated} testing, docstrings, error-handling, etc: to be continued ...

4 Answers 4

1

Probably the confusing thing is the interactive part, so I'll work from it.

Interactive

Is the part of the function which is executed before the function is called. If it has some particular strings, Emacs will know to interpret those as asking for performing some interaction. But, more generally, interactive form can contain whatever code you like; the requirement is that it produce a list with the arguments matching function's formal arguments.

Your case

You can have two strategies:

  1. (bulk-replace-current-region-with-tuples-from-file file-or-begin end). In this case you would do something like:

    (defun bulk-replace-current-region-with-tuples-from-file (file-or-begin &optional end)
      (interactive
        (if (region-active-p)
           (list (region-beginning) (region-end))
           (read-file-name "Replace in file: ")))
      (if end ; this is an operation on region
         ; otherwise perform replacement in a file
      ...)
    

    This is an "ugly" function signature because it mixes types, and the meaning of its arguments is ambiguous, but it keeps the number of arguments to the minimum, and every combination of arguments has a meaning.

  2. (bulk-replace-current-region-with-tuples-from-file begin end file) In this case, your code might look like this:

     (defun bulk-replace-current-region-with-tuples-from-file (begin end file)
      (interactive
        (if (region-active-p)
           (list (region-beginning) (region-end) nil)
           (list nil nil (read-file-name "Replace in file: ")))
      (cond
         ((and begin end file) ; this is an operation on a region in a file
         )
         (file ; this is an operation on a file
         )
         ( ; this is an operation on a region
         ))
       ...)
    

    Unlike in the previous case, you would need to handle the situation when you are both given the file name and the region. Perhaps, you would also want to be able to specify both of these parameters interactively (in which case you would need to modify the interactive part).


Since the above seems complicated, my strategy would be to:

  1. Write a non-interactive function that does replacement on a region.
  2. Write two interactive functions calling the first function in a situation when I need it to work on a buffer, or to switch to the given file and to specify the begin and end by giving it (point-min) and (point-max). You'd have more functions, but each one of them would be simpler, so it's not so complicated after all.
3
  • I like the design (at end), and would prefer to have more/simpler functions. But shouldn't strategy item#=1 be 'Write a non-interactive function that does replacement on a region'? That seems to be the simplest case, no? Then (1) to handle region, pass begin and end (2) to handle buffer, pass (point-min) and (point-max) (3) to handle file, switch to it and pass (point-min) and (point-max). Or am I missing something?
    – TomRoche
    Dec 24, 2015 at 6:36
  • @TomRoche yes, I intended to be a region, not a buffer. That was a typo.
    – wvxvw
    Dec 24, 2015 at 8:04
  • and this is working! see latest code. Just need an über-wrapper and more thorough testing: provided that doesn't uncover dormant bombs, I'll mark this answer and doc it in my question.
    – TomRoche
    Dec 24, 2015 at 8:32
0

Your description does not say that the function needs to be a command. This is essentially what you want, in that case:

(defun foo (&optional file)
  "..."
  (if file
      (with-current-buffer (find-file-noselect file)
        (do-something-with-text-in-file))
    (if (use-region-p)                  ; or (region-active-p)
        (save-restriction
          (narrow-to-region (region-beginning) (region-end))
          (do-something-with-text-in-region))
      (do-something-with-text-in-current-buffer))))

use-region-p assumes you want the active region to be non-empty. If not, use active-region-p. Depending on what you want to do in the region, you might not need to use save-restriction -- you can perhaps just bind local variables to region-beginning and region-end, and work within those bounds.

And no, you do not need to mark the whole buffer to be able to act on it. Just perform whatever action you want, using point-min and point-max as the bounds of the action.

If it is the same action that you want to do when the region is active, then you can use the same code: if the region is active, just use save-restriction and narrow-to-region first. Within the narrowed buffer, point-min and point-max are the beginning and end of the region, not the buffer.

Note that if you do want a command then you will need to specify how you want the command to (optionally) get a file-name argument. Do you want to test a prefix argument and depending on its value prompt to read a file name? And so on. IOW, you need to decide what interactive behavior you want, if you want a command. For a command, your question is unclear so far.

9
  • good point, I have now edited question to mandate (interactive)-ity.
    – TomRoche
    Dec 24, 2015 at 6:21
  • It does no good to mandate a command if you do not specify how you expect/want to provide it a file name interactively. IOW, this means nothing: "If called with (argument)" - how is it to be called with an argument, interactively? How do you want to provide a file-name argument interactively - read it? So far, the question is unclear (underspecfied), and should be closed as such.
    – Drew
    Dec 24, 2015 at 6:23
  • Would not C-u work? and is not closing rather an overreaction in this case?
    – TomRoche
    Dec 24, 2015 at 6:27
  • How is your function to be called, so that it receives a file name? If called interactively, you need to say how it is to receive the file-name argument interactively.
    – Drew
    Dec 24, 2015 at 6:28
  • What does "would not C-u work" mean? Yes, you can define the behavior to interpret a prefix arg in such a way that something, somehow, provides a file-name argument. But you haven't specified that, so far. That's why I suggested that that is one way to provide it: test for a prefix arg and then read it. But I'm not specifying what you want - that's your job. Anyway, you should have enough info now to get what you want. You just need to decide what you want. If you have another question about that (e.g., how to test a prefix arg or read a file name), then post such questions separately.
    – Drew
    Dec 24, 2015 at 6:30
0

I'd use something like this:

  • write a general basic processing defun, with start-end interface
  • determine what you has been given
  • set start and end
  • apply basic processing defun

Here's an implementation. Note though that I didn't test it:

(defun extensible-omnivorous-defun (arg)
  "Applyies basic-processing to active region,
or whole buffer if there's no active region.
With C-u brings up a dialog to select the file to process."
  (interactive "P")

  ;; basic processing with start-end interface:
  (defun basic-processing (start end)
    ;; do something here
    )

  ;; determing start and end:
  (let (start end 
              (file-is-given? nil))

    (cond
     ((equal arg '(4))
      (ido-find-file "Select file to process: ")

      (setq start (point-min))
      (setq end   (point-max))

      (setq file-is-given? t)
      )
     ;; ;; easily extend the defun later:
     ;; ((equal arg '(16))
     ;;  (message "%s" arg)
     ;;  )
     ;; ((equal arg '-)
     ;;  (message "%s" arg)
     ;;  )
     (t
      ;; no file name is given
      (if (use-region-p)
          (progn (setq start (region-beginning))
                 (setq end (region-end)) )
        (progn (setq start (point-min))
               (setq end (point-max)) )  )
      )
     )

    ;; applying basic processing to what ever is given:
    (basic-processing start end)
    )

  ;; if it has been file -- save and close it:
  (when file-is-given?
    (save-buffer)
    (kill-buffer)
    )

  )
0

I find the following macro helpful for unifying the first two cases:

(defmacro on-region-or-buffer (&rest body)
  `(if (region-active-p)
       (save-restriction
         (narrow-to-region (region-beginning) (region-end))
         ,@body)
     ,@body))

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.