5

[This question is related to, but not a duplicate of, the question copy contents of current buffer in a temp buffer and operate on that. Besides other incidental things (e.g., the question itself is different), none of the solutions offered both (1) execute the elisp in the same context as the original buffer, AND (2) maintain the state of the original buffer. Indeed, the purpose of the macro offered as a possible solution (with-indirect-buffer-in-foo-mode) in that question seems to be to actually edit the original text in a different mode. The macro also doesn't maintain the buffer-undo-list, of course (nor any other 'state' that may change when the text of a buffer changes).]

I'd like to be able to write

(save-complete-buffer-state
  (edit-the-buffer-arbitrarily)
  (condition-p))

and have everything (or as much as is practical) be preserved from the original emacs buffer state before the call, including, for example, point, kill ring, undo list, etc. Does such a macro already exist? If not, how difficult would it be to implement? (What needs to be saved?)

(I've found c-save-buffer-state at

Emacs: How to intelligently handle buffer-modified when setting text properties?

but I don't know enough about what needs to be done to understand how close this is to doing what I'm asking.)

EDIT: Using lawlist's and @phils' suggestions, I was able to come up with this solution. Please note that this macro works on a few simple test cases but has not been tested extensively.

(defmacro with-cloned-buffer (&rest body)
  "Executes BODY just like `progn' but maintains original buffer state."
  (declare (indent 0))
  (let ((return-value (make-symbol "return-value")))
    `(let (buffer-undo-list)   ;  maintain original buffer-undo-list
       (clone-indirect-buffer nil t)
       (let ((,return-value (progn ,@body)))
         (primitive-undo 1 buffer-undo-list)
         (kill-buffer-and-window)
         ,return-value))))

Here are two simple tests (first returns t, second returns nil):

(with-cloned-buffer
  (insert "A")
  (left-char 1)
  (looking-at-p "A"))

(with-cloned-buffer
  (insert "A")
  (left-char 1)
  (looking-at-p "B"))
Carl
  • 81
  • 5
  • How about cloning the buffer and doing what you want in the cloned buffer, and then go back to the original buffer when you are all done? https://www.gnu.org/software/emacs/manual/html_node/emacs/Indirect-Buffers.html Or make a temporary buffer and insert the entire buffer contents of the original buffer? **copy contents of current buffer in a temp buffer and operate on that**: http://emacs.stackexchange.com/questions/2191/copy-contents-of-current-buffer-in-a-temp-buffer-and-operate-on-that The link contains examples of both concepts as answers with several up-votes. – lawlist Mar 27 '17 at 00:45
  • Yes, I would suggest that a `(with-cloned-buffer ...)` macro makes slightly more sense than a `(save-complete-buffer-state ...)` macro -- if you are not touching the original buffer, there's also nothing to **fix** if things go horribly wrong. – phils Mar 27 '17 at 00:49
  • @lawlist, phils: Thanks for your advice to use a cloned buffer. It took me a little while to figure out why and how that would work, but after some trial and error, I seem to have come up with a macro that works, at least in the sense that it avoids all the problems that my initial attempts did not. :-) I am going to edit my question to include the macro. I'd greatly appreciate it if you'd point out any obvious problems with it! – Carl Mar 28 '17 at 00:48
  • @lawlist, phils: Looks like Carl accepts your solution as an answer. Please write your comment as an answer to mark this question as done. – Tobias Mar 28 '17 at 13:44
  • Possible duplicate of [copy contents of current buffer in a temp buffer and operate on that](http://emacs.stackexchange.com/questions/2191/copy-contents-of-current-buffer-in-a-temp-buffer-and-operate-on-that) – lawlist Mar 28 '17 at 15:50
  • @Tobias -- I chose to mark this question as a duplicate and voted to close. – lawlist Mar 28 '17 at 15:52
  • n.b. `clone-indirect-buffer` is very different to `clone-buffer` ! I do note that `clone-buffer` throws an error for file-visiting buffers, but you can let-bind `buffer-file-name` to nil around the function call. "A buffer whose major mode symbol has a non-nil `no-clone` property" is a different matter. That may or may not be something you care about in practice. – phils Mar 28 '17 at 23:06
  • Thanks, @phils. I read about `clone-buffer` last night but didn't realize that one could trick it by setting `buffer-file-name` to `nil`. – Carl Mar 29 '17 at 02:13
  • FYI it looks like `Info-mode` is the only standard major mode with any kind of `no-clone` property (in this case being `no-clone-indirect`). It wouldn't surprise me if the properties were created for that specific purpose and that nothing else uses them. – phils Mar 29 '17 at 03:06

2 Answers2

3

Your suggested solution confuses things more than anything: clone-indirect-buffer creates another buffer which shares the same text, so any changes to the text in the clone will also affect the original buffer, so you haven't gained anything, really. Maybe you'd be better served with clone-buffer (not indirect).

Using a temp buffer might be a good idea, but then you'd presumably want that other buffer to have its own copy of the contents, so you'd do something like:

(let ((orig-buf (current-buffer)))
  (with-temp-buffer
    (insert-buffer-substring orig-buffer)
    <do-your-thing>))

Of course, this can suffer from other problems, such as the fact that the temp buffer is not configured in the same major/minor modes as the original one.

Another option would be to stay in the same buffer and instead undo the changes at the end, using atomic-change-group, e.g.:

(catch 'done
 (atomic-change-group
  <do-your-thing>
  (throw 'done <result>)))
Stefan
  • 26,154
  • 3
  • 46
  • 84
  • Using an indirect buffer does help at least with maintaining buffer-local variables (buffer-local variables changed in the indirect buffer do not affect the same variables in the original buffer). It may also avoid having to worry about maintaining point after the `(undo)` in some situations? (I'm not sure.) – Carl Mar 28 '17 at 22:54
  • Using a temp buffer suffers from the problem you mention, as well as possibly being very inefficient. I haven't tried it yet, but I like the looks of your `atomic-change-group` suggestion. It might be the best solution. Does it even stop the effect of setting all variables? – Carl Mar 28 '17 at 22:59
  • No, `atomic-change-group` does not pay attention to variables, it only restores the buffer's textual content. – Stefan Mar 29 '17 at 00:04
  • Thanks, Stefan. I also discovered that in my experiments. :-) – Carl Mar 29 '17 at 02:11
  • Sidenote: `atomic-change-group`? Why not `buffer-text-transaction`? Would be clearer name :). – Tianxiang Xiong Mar 29 '17 at 05:10
3

I appreciate all the suggestions offered by everyone. Using them I have created three different macros with very similar (but not completely identical) functionality that do what I want. Note that the macro named with-cloned-buffer below is not the same as the macro with that name in my question. Also be aware that while these macros have all been tested on toy examples, they have not been tested extensively yet.

Except for the name, this is the same macro as in the question and uses clone-indirect-buffer:

(defmacro with-cloned-indirect-buffer (&rest body)
  "Executes BODY just like `progn' but maintains original buffer state."
  (declare (indent 0))
  (let ((return-value (make-symbol "return-value")))
    `(let (buffer-undo-list)   ;  maintain original buffer-undo-list
       (clone-indirect-buffer nil t)
       (let ((,return-value (progn ,@body)))
         (primitive-undo 1 buffer-undo-list)
         (kill-buffer-and-window)
         ,return-value))))

Using clone-buffer instead of clone-indirect-buffer:

(defmacro with-cloned-buffer (&rest body)
  "Executes BODY just like `progn' but maintains original buffer state."
  (declare (indent 0))
  (let ((return-value (make-symbol "return-value")))
    `(let ((buffer-file-name nil))
       (clone-buffer nil t)
       (let ((,return-value (progn ,@body)))
         (kill-buffer-and-window)
         ,return-value))))

Using atomic-change-group:

(defmacro save-buffer-state (&rest body)
  "Executes BODY just like `progn' but maintains original buffer state."
  (declare (indent 0))
  (let ((return-value (make-symbol "return-value")))
    `(catch 'done
       (atomic-change-group
         (let ((,return-value (progn ,@body)))
           (throw 'done ,return-value))))))

A couple notes. In spirit, the last version is my favorite, except that it doesn't maintain buffer-local variables. I'd actually prefer something that maintained all variables, but that's probably too much to ask.

Second, the atomic-change-group version can easily be tweaked to make a macro that runs editing code conditionally. One possibility is that the edits are kept if the last form returns a truthy value. (This is, of course, similar to atomic-change-group itself.)

Third, although it's very clean, using clone-buffer seems inefficient for the use-case I have in mind: testing small edits on possibly big buffers.

Finally, at least the first two macros would be better if they incorporated unwind-protect.

Practically speaking, assuming I haven't made any big programming errors, all three solve the problem I was hoping to solve. Thanks, everyone!

Carl
  • 81
  • 5