unofficial mirror of help-gnu-emacs@gnu.org
 help / color / mirror / Atom feed
* track-changes and undo
@ 2024-04-21  8:23 Joost Kremers
  2024-04-21 14:54 ` Stefan Monnier
  0 siblings, 1 reply; 8+ messages in thread
From: Joost Kremers @ 2024-04-21  8:23 UTC (permalink / raw)
  To: 'Help-Gnu-Emacs  (help-gnu-emacs@gnu.org)'; +Cc: Stefan Monnier

Hi Stefan,

I noticed your track-changes package on ELPA the other day. Since I once (a long
time ago) wrote a package[1] that uses `before-change-functions` and
`after-change-functions` and that commits all the sins that you mention in your
package description, I thought I might try and base my package on yours.

I quickly found, though, that using your package, I was not able to distinguish
between "normal" changes and change made by undo. Currently, my code uses
`undo-in-progress` for that, but that doesn't work in the SIGNAL function passed
to `track-changes-register`, because the undo has obviously already finished
when that function is run.

So I'm wondering: is there a way to know if a change is made during undo? If
not, would it make sense to add it as an option?

TIA

Joost


BTW, feel free to answer on the list.




Footnotes:
[1]  https://github.com/joostkremers/criticmarkup-emacs

-- 
Joost Kremers
Life has its moments



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-21  8:23 track-changes and undo Joost Kremers
@ 2024-04-21 14:54 ` Stefan Monnier
  2024-04-22  7:33   ` Joost Kremers
  0 siblings, 1 reply; 8+ messages in thread
From: Stefan Monnier @ 2024-04-21 14:54 UTC (permalink / raw)
  To: Joost Kremers; +Cc: 'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'

Hi Joost,

> So I'm wondering: is there a way to know if a change is made during
> undo?  If not, would it make sense to add it as an option?

Very interesting point.  Indeed, this is missing.
Hmm... not completely sure how to expose that info through this new API.
Maybe a concrete example would help figure out how to make that work.
Do you remember how you used (or why you needed) `undo-in-progress`?


        Stefan




^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-21 14:54 ` Stefan Monnier
@ 2024-04-22  7:33   ` Joost Kremers
  2024-04-22 12:38     ` Stefan Monnier
  0 siblings, 1 reply; 8+ messages in thread
From: Joost Kremers @ 2024-04-22  7:33 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'

Hi Stefan,

> Very interesting point.  Indeed, this is missing.
> Hmm... not completely sure how to expose that info through this new API.
> Maybe a concrete example would help figure out how to make that work.
> Do you remember how you used (or why you needed) `undo-in-progress`?

The short version: The package makes it easier to add CriticMarkup to a
document, i.e., markup to indicate additions, deletions, etc. There are
keybindings for the various markups (`{++...++}` for additions, `{--...--}` for
deletions, etc.).

The follow-changes mode tries to do that automatically: if you delete a word,
instead of actually deleting it, the word is enclosed in deletion markup.

What actually happens in the current implementation is that the text is deleted
(as per normal) and then reinserted in the buffer with deletion markup around it
(in a function in `after-change-functions`).

Similarly for an addition, instead of just inserting the new text, it is
inserted and enclosed in addition markup. That part happens in
`before-change-functions` in the current implementation.

Both hook functions start with `(unless undo-in-progress...)`, so when a change
is triggered by undo, they don't do anything special, and Emacs Does The
Right Thing. The Right Thing here being removing the mark-up, keeping the text
if the original action was a deletion and removing the text if the original
action was an addition. I assume the right thing happens because the changes
made to the buffer in the hook functions are recorded in the same change that
triggers the hooks.

It's probably easier to understand when you look at the code: 

https://github.com/joostkremers/criticmarkup-emacs/blob/master/cm-mode.el#L863

Hope this makes it clearer...

Joost



-- 
Joost Kremers
Life has its moments



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-22  7:33   ` Joost Kremers
@ 2024-04-22 12:38     ` Stefan Monnier
  2024-04-22 21:41       ` Joost Kremers
  0 siblings, 1 reply; 8+ messages in thread
From: Stefan Monnier @ 2024-04-22 12:38 UTC (permalink / raw)
  To: Joost Kremers; +Cc: 'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'

[-- Attachment #1: Type: text/plain, Size: 1466 bytes --]

> The short version: The package makes it easier to add CriticMarkup to a
> document, i.e., markup to indicate additions, deletions, etc. There are
> keybindings for the various markups (`{++...++}` for additions, `{--...--}` for
> deletions, etc.).
>
> The follow-changes mode tries to do that automatically: if you delete a word,
> instead of actually deleting it, the word is enclosed in deletion markup.

Thanks,

Yes, that makes a lot of sense and checking `undo-in-progress` is
indispensable to make undo work.  With the current API (if you want to
use `track-changes`, that is) the best you can do is to use the
`:immediate` option so that your code gets called directly from the
`after-change-functions` where you can (still) see the
`undo-in-progress`.

The gain from `track-changes` is just to provide you with the "before"
string for deletions so it takes care of reading it in
`before-change-functions` and then providing it to you in the
`after-change-functions` (with the advantage that it detects/handles the
various corner cases where that pairing fails).

One other thing that you might have trouble to reproduce with
`track-changes` is the following test:

    (and (= beg (point-min)) (= end (point-max)))

that you have in `cm-before-change`.  I'm not completely sure what this
is for, tho.  Is it for `revert-buffer`?

[ I tend to do "destructive reads", so the patch below is the result of
  reading that part of your code.  ]


        Stefan

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: criticmarkup.diff --]
[-- Type: text/x-diff, Size: 2963 bytes --]

diff --git a/cm-mode.el b/cm-mode.el
index 43c705a..882f77d 100644
--- a/cm-mode.el
+++ b/cm-mode.el
@@ -862,43 +862,31 @@ substitutions, `d' for comments and highlights."
 
 ;;; Follow Changes
 
-(defvar cm-follow-changes nil
-  "Flag indicating whether follow changes mode is active.")
-(make-variable-buffer-local 'cm-follow-changes)
-
 (defvar cm-current-deletion nil
   "The deleted text in follow changes mode.
 The value is actually a list consisting of the text and a flag
 indicating whether the deletion was done with the backspace
 key.")
 
-(defun cm-follow-changes (&optional arg)
-  "Activate follow changes mode.
-If ARG is positive, activate follow changes mode, if ARG is 0 or
-negative, deactivate it.  If ARG is `toggle', toggle follow
-changes mode."
-  (interactive (list (or current-prefix-arg 'toggle)))
-  (let ((enable (if (eq arg 'toggle)
-                    (not cm-follow-changes)
-                  (> (prefix-numeric-value arg) 0))))
-    (if enable
-        (progn
-          (add-to-list 'before-change-functions 'cm-before-change t)
-          (add-to-list 'after-change-functions 'cm-after-change)
-          (setq cm-follow-changes t)
-          (message "Follow changes mode activated."))
-      (setq before-change-functions (delq 'cm-before-change before-change-functions))
-      (setq after-change-functions (delq 'cm-after-change after-change-functions))
-      (setq cm-follow-changes nil)
-      (message "Follow changes mode deactivated."))))
+(define-minor-mode cm-follow-changes  ;FIXME: Shouldn't it end in `mode'?
+  "Minor mode to follow changes."
+  :global nil
+  (if cm-follow-changes
+      (progn
+        (add-hook 'before-change-functions #'cm-before-change t t)
+        (add-hook 'after-change-functions #'cm-after-change nil t))
+    (remove-hook 'before-change-functions #'cm-before-change t)
+    (remove-hook after-change-functions #'cm-after-change t)))
 
 (defun cm-before-change (beg end)
   "Function to execute before a buffer change.
 BEG and END are the beginning and the end of the region to be
 changed."
   (unless (or undo-in-progress
+              ;; FIXME: What do you mean by "buffer switches"?
               (and (= beg (point-min)) (= end (point-max))))  ; This happens on buffer switches.
     (if (= beg end)  ; Addition.
+        ;; FIXME: There can be corner cases where point is not at beg/end.
         (cm-make-addition (cm-markup-at-point))
       ;; When the deletion was done with backspace, point is at end. We record
       ;; this in `cm-current-deletion' so we can position point correctly.
@@ -906,7 +894,7 @@ changed."
 
 (defun cm-after-change (beg end length)
   "Function to execute after a buffer change.
-This function marks deletions.  See cm-before-change for details.
+This function marks deletions.  See `cm-before-change' for details.
 BEG and END mark the region to be changed, LENGTH is the length
 of the affected text."
   (unless (or undo-in-progress

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-22 12:38     ` Stefan Monnier
@ 2024-04-22 21:41       ` Joost Kremers
  2024-04-22 23:04         ` Stefan Monnier
  0 siblings, 1 reply; 8+ messages in thread
From: Joost Kremers @ 2024-04-22 21:41 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'

On Mon, Apr 22 2024, Stefan Monnier wrote:
> The gain from `track-changes` is just to provide you with the "before"
> string for deletions so it takes care of reading it in
> `before-change-functions` and then providing it to you in the
> `after-change-functions` (with the advantage that it detects/handles the
> various corner cases where that pairing fails).

I think I'll try and see if I can make it work with the `:immediate` option.
It would of course mean that the buffer is modified inside
`after-change-functions`, which you warn against, but it looks like that's the
only way.

> One other thing that you might have trouble to reproduce with
> `track-changes` is the following test:
>
>     (and (= beg (point-min)) (= end (point-max)))
>
> that you have in `cm-before-change`.  I'm not completely sure what this
> is for, tho.  Is it for `revert-buffer`?

I honestly don't remember... Based on the comment, it looks like
`switch-to-buffer` triggers `before-change-functions`, but a) that doesn't make
much sense; and b) the code seems to work just fine without that line. (I even
fired up a Vagrant box with an old Ubuntu release with Emacs 24, which would be
the most recent version when I wrote that code).

> [ I tend to do "destructive reads", so the patch below is the result of
>   reading that part of your code.  ]

Thanks.

> +(define-minor-mode cm-follow-changes  ;FIXME: Shouldn't it end in `mode'?
> +  "Minor mode to follow changes."
> +  :global nil
> +  (if cm-follow-changes
> +      (progn
> +        (add-hook 'before-change-functions #'cm-before-change t t)
> +        (add-hook 'after-change-functions #'cm-after-change nil t))
> +    (remove-hook 'before-change-functions #'cm-before-change t)
> +    (remove-hook after-change-functions #'cm-after-change t)))

Making it a minor mode makes sense, of course. And don't ask me why I used
`add-to-list` instead of `add-hook`. 🤔
  

-- 
Joost Kremers
Life has its moments



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-22 21:41       ` Joost Kremers
@ 2024-04-22 23:04         ` Stefan Monnier
  2024-04-23  5:58           ` Eli Zaretskii
  2024-04-23  7:06           ` Joost Kremers
  0 siblings, 2 replies; 8+ messages in thread
From: Stefan Monnier @ 2024-04-22 23:04 UTC (permalink / raw)
  To: Joost Kremers; +Cc: 'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'

>> The gain from `track-changes` is just to provide you with the "before"
>> string for deletions so it takes care of reading it in
>> `before-change-functions` and then providing it to you in the
>> `after-change-functions` (with the advantage that it detects/handles the
>> various corner cases where that pairing fails).
> I think I'll try and see if I can make it work with the `:immediate` option.
> It would of course mean that the buffer is modified inside
> `after-change-functions`, which you warn against,

It wouldn't be worse than what you have now since you also modify the
buffer from `before/after-change-functions`.

> but it looks like that's the only way.

In the general case it's tricky to postpone the buffer change to a safer
time, indeed.  In practice, tho, you should be able to distinguish "undo
commands" from all other commands (basically depending on whether they
do all their modifications with `undo-in-progress` or not) and then
ignore only the changes of undo commands: the result should be
good enough.

IOW something like:

    (track-changes-register #'cm-change-signal :immediate t)

    (defvar-local cm-change-pending nil)

    (defun cm-change-signal (id)
      (cond
       (cm-change-pending nil) ;; Nothing to do, we're already waiting.
       (undo-in-progress
        ;; Just ignore this undo change.
        (track-changes-fetch id #'ignore))
       (t
        (setq cm-change-pending
              (run-with-timer 0 nil #'cm-change-do id)))))
       
    (defun cm-change-do (id)
      (track-changes-fetch
       (lambda (beg end before)
         ..DO THE CriticalMarkup THING..
         ;; Ignore the changes we just made.
         (track-changes-fetch id #'ignore)
         (setq cm-change-pending nil))))

>> One other thing that you might have trouble to reproduce with
>> `track-changes` is the following test:
>>
>>     (and (= beg (point-min)) (= end (point-max)))
>>
>> that you have in `cm-before-change`.  I'm not completely sure what this
>> is for, tho.  Is it for `revert-buffer`?
>
> I honestly don't remember... Based on the comment, it looks like
> `switch-to-buffer` triggers `before-change-functions`, but a) that doesn't make
> much sense; and b) the code seems to work just fine without that line. (I even
> fired up a Vagrant box with an old Ubuntu release with Emacs 24, which would be
> the most recent version when I wrote that code).

Have you tried `M-x revert-buffer RET` (assuming the file and the
buffer aren't equal)?

> Making it a minor mode makes sense, of course.

It was already a minor mode, IMO, just one defined by hand instead of
using the dedicated macro.


        Stefan




^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-22 23:04         ` Stefan Monnier
@ 2024-04-23  5:58           ` Eli Zaretskii
  2024-04-23  7:06           ` Joost Kremers
  1 sibling, 0 replies; 8+ messages in thread
From: Eli Zaretskii @ 2024-04-23  5:58 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: help-gnu-emacs

> From: Stefan Monnier <monnier@iro.umontreal.ca>
> Cc: "'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'" <help-gnu-emacs@gnu.org>
> Date: Mon, 22 Apr 2024 19:04:10 -0400
> 
> >> The gain from `track-changes` is just to provide you with the "before"
> >> string for deletions so it takes care of reading it in
> >> `before-change-functions` and then providing it to you in the
> >> `after-change-functions` (with the advantage that it detects/handles the
> >> various corner cases where that pairing fails).
> > I think I'll try and see if I can make it work with the `:immediate` option.
> > It would of course mean that the buffer is modified inside
> > `after-change-functions`, which you warn against,
> 
> It wouldn't be worse than what you have now since you also modify the
> buffer from `before/after-change-functions`.
> 
> > but it looks like that's the only way.
> 
> In the general case it's tricky to postpone the buffer change to a safer
> time, indeed.  In practice, tho, you should be able to distinguish "undo
> commands" from all other commands (basically depending on whether they
> do all their modifications with `undo-in-progress` or not) and then
> ignore only the changes of undo commands: the result should be
> good enough.
> 
> IOW something like:
> 
>     (track-changes-register #'cm-change-signal :immediate t)
> 
>     (defvar-local cm-change-pending nil)
> 
>     (defun cm-change-signal (id)
>       (cond
>        (cm-change-pending nil) ;; Nothing to do, we're already waiting.
>        (undo-in-progress
>         ;; Just ignore this undo change.
>         (track-changes-fetch id #'ignore))
>        (t
>         (setq cm-change-pending
>               (run-with-timer 0 nil #'cm-change-do id)))))
>        
>     (defun cm-change-do (id)
>       (track-changes-fetch
>        (lambda (beg end before)
>          ..DO THE CriticalMarkup THING..
>          ;; Ignore the changes we just made.
>          (track-changes-fetch id #'ignore)
>          (setq cm-change-pending nil))))
> 
> >> One other thing that you might have trouble to reproduce with
> >> `track-changes` is the following test:
> >>
> >>     (and (= beg (point-min)) (= end (point-max)))
> >>
> >> that you have in `cm-before-change`.  I'm not completely sure what this
> >> is for, tho.  Is it for `revert-buffer`?
> >
> > I honestly don't remember... Based on the comment, it looks like
> > `switch-to-buffer` triggers `before-change-functions`, but a) that doesn't make
> > much sense; and b) the code seems to work just fine without that line. (I even
> > fired up a Vagrant box with an old Ubuntu release with Emacs 24, which would be
> > the most recent version when I wrote that code).
> 
> Have you tried `M-x revert-buffer RET` (assuming the file and the
> buffer aren't equal)?
> 
> > Making it a minor mode makes sense, of course.
> 
> It was already a minor mode, IMO, just one defined by hand instead of
> using the dedicated macro.

From the peanut gallery: if anything in this discussion should be
mentioned in the ELisp manual, where track-changes is documented,
please add those nits there.  For example, the whole issue of using
track-changes with undo should perhaps be mentioned there.

Thanks.



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: track-changes and undo
  2024-04-22 23:04         ` Stefan Monnier
  2024-04-23  5:58           ` Eli Zaretskii
@ 2024-04-23  7:06           ` Joost Kremers
  1 sibling, 0 replies; 8+ messages in thread
From: Joost Kremers @ 2024-04-23  7:06 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 'Help-Gnu-Emacs (help-gnu-emacs@gnu.org)'

On Mon, Apr 22 2024, Stefan Monnier wrote:
> In the general case it's tricky to postpone the buffer change to a safer
> time, indeed.  In practice, tho, you should be able to distinguish "undo
> commands" from all other commands (basically depending on whether they
> do all their modifications with `undo-in-progress` or not) and then
> ignore only the changes of undo commands: the result should be
> good enough.
>
> IOW something like:
[code snipped]

Thanks, I'll see if I can make it work.

>>> One other thing that you might have trouble to reproduce with
>>> `track-changes` is the following test:
>>>
>>>     (and (= beg (point-min)) (= end (point-max)))
>>>
>>> that you have in `cm-before-change`.  I'm not completely sure what this
>>> is for, tho.  Is it for `revert-buffer`?
>>
>> I honestly don't remember... Based on the comment, it looks like
>> `switch-to-buffer` triggers `before-change-functions`, but a) that doesn't make
>> much sense; and b) the code seems to work just fine without that line. (I even
>> fired up a Vagrant box with an old Ubuntu release with Emacs 24, which would be
>> the most recent version when I wrote that code).
>
> Have you tried `M-x revert-buffer RET` (assuming the file and the
> buffer aren't equal)?

I did, but that appears to resets `before-change-functions` (and presumably
`after-change-functions`). After `revert-buffer`, the function that
cm-follow-mode adds to `before-change-functions` is gone, and `cm-mode` itself
is disabled. So I doubt that was the reason for adding that line. (Also, I don't
think I would have written "buffer switches" in the comment if I actually meant
"reverting a buffer". But then again, one's past self can be quite
unpredictable...)

-- 
Joost Kremers
Life has its moments



^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2024-04-23  7:06 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-04-21  8:23 track-changes and undo Joost Kremers
2024-04-21 14:54 ` Stefan Monnier
2024-04-22  7:33   ` Joost Kremers
2024-04-22 12:38     ` Stefan Monnier
2024-04-22 21:41       ` Joost Kremers
2024-04-22 23:04         ` Stefan Monnier
2024-04-23  5:58           ` Eli Zaretskii
2024-04-23  7:06           ` Joost Kremers

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).