6

This question refers exclusively to interactive functions that modify the contents of the current buffer.

What does one need to do to ensure that a single execution of (undo), right after running the function, will fully undo all the changes it made to the buffer's contents?

(For the rest of this post I'll use the word undoable to mean "can be undone with a single execution of (undo)".)


Apparently, interactive functions are not undoable by default, as the example below shows.

(NB: I stress that the function below is just an example of the problem; I'm looking for a general solution, not one that just works for this particular function/use-case. Also, please do not use this function! In addition to not being undoable, it has other shortcomings.)

The function quote-region inserts single quotes at the beginning and end of the current region, and replaces all occurrences of single quotes in-between with a backslash-escaped single quote:

(defun quote-region ()
  (interactive)

  (let ((beginning (region-beginning))
        (end       (region-end)))

    (goto-char end)
    (insert "'")

    (replace-string "'" "\\'" nil beginning end)

    (goto-char beginning)
    (insert "'")

))

For example, if the current region's content is

foo'bar

...then running M-x quote-region will change it to

'foo\'bar'

...as desired.

But if I now hit C-/ (undo), the buffer changes to

foo'bar'

I must then hit C-/ a second time to return to

foo'bar

Hence quote-region is not undoable, as defined above.

Drew
  • 75,699
  • 9
  • 109
  • 225
kjo
  • 3,145
  • 14
  • 42

1 Answers1

10

I recommend you do C-h f replace-string RET and read it:

[...]
This function is for interactive use only;
in Lisp code use `search-forward' and `replace-match' instead.
[...]

So, as suggested replace that call with something like

(while (search-backward "'" beginning t)
  (replace-match "\\'" t t)
  (goto-char (match-beginning 0)))

Some functions do "unusual" additional things which are helpful in interactive use but get in the way when you need their main functionality within your own function. Among those "unusual" operations, some interactive functions (such as replace-string) insert an undo-boundary, others set the mark (e.g. beginning-of-buffer), etc... We try to help the user discover those quirks by documenting them in the C-h f output, and we also try to make the byte-compiler emit a warning when it sees you call such a function, but you need to C-h f or to byte-compile your file in order to discover the problem.

Stefan
  • 26,154
  • 3
  • 46
  • 84
  • 6
    Hi Stefan, I think the OP downvoted because he does not see the general rule in this answer. I know that you implicitly gave this rule here. Therefore I compensated the downvote. I propose that you explicitly state the general rule: "You can avoid messing up the `buffer-undo-list` if you don't use commands that are marked as *for interactive use only* in your emacs lisp code." – Tobias Nov 02 '16 at 14:07
  • After reading what this answer looked like before the edit, I would have worded that comment in a stronger fashion than @Tobias did: before the edit it not only didn't explain what the general rule was, it even presented that code without so much as *hinting* at the fact that it fixed the OP's issue in this particular case! – Omar Nov 02 '16 at 16:52
  • 2
    Omar: I think the "hint" was that it was posted as *an answer to the OP's question*. The additional context improves the answer, for sure, but it's not necessary to state "this answer addresses the question" when you write an answer -- that part is very much assumed. – phils Nov 02 '16 at 19:59
  • It's probably because I'm in the middle of grading exams but I don't currently have a lot of faith in the idea that what people write in response to a question automatically addresses it, @phils, specially in answers without explanation. I hope I'll eventually recover and share your attitude! (In this case, of course, the answer *was* completely relevant, and with the added explanation is a fine answer.) – Omar Nov 03 '16 at 03:55
  • @Tobias: I did not downvote Stefan's answer. On the contrary, I upvoted it, and accepted it. – kjo Nov 03 '16 at 10:06
  • @kjo Sorry, then sombody else did so. Thanks for the clarification! – Tobias Nov 03 '16 at 10:08