GNU bug report logs - #70077
An easier way to track buffer changes

Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.

Package: emacs; Reported by: Stefan Monnier <monnier@HIDDEN>; Keywords: patch; dated Fri, 29 Mar 2024 16:17:01 UTC; Maintainer for emacs is bug-gnu-emacs@HIDDEN.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 9 Apr 2024 03:56:30 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 23:56:30 2024
Received: from localhost ([127.0.0.1]:48001 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1ru2b0-0005Uq-3e
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 23:56:30 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:47476)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1ru2av-0005Tp-SO
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 23:56:28 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1ru2af-0002i1-DA; Mon, 08 Apr 2024 23:56:09 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=iyy0Zl8A4/1Ds6HZjGUNuNy3AGqvxlAR1M1laPwo0Wo=; b=eqnOaBH2KCs4
 b0gbALxp5wgyZKam3EXlD8KpqoMx6s/48a0CYGG2IrlPORgpv+9MTp9kkRS2Jc+SCAXg/KPXdOTlp
 bl18R0pAcyZDuF3iUrQrivkKLZjrS87h6UPOrRyCJ+9PoamQvH3Ft5OK+c8+7gGmo5CCeJdal7hKJ
 zsMbSD2MwGzy/WGNqC56pykt+VyiZG3c1HXQB1eQtdMtdYLP0Wp8OjlZg5M19SX3q3mYG1TRON7gt
 GQqivq4XS2MVbasvHotFtNfQSsDaubQ/Ze85uqGT6+4KuEqtuZv1WUr6jBGmJ45ggFsoKKv0yvfIi
 mQiCBHnEHf0elpjBzge2dg==;
Date: Tue, 09 Apr 2024 06:56:07 +0300
Message-Id: <865xwrxk88.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwv1q7fd1p5.fsf-monnier+emacs@HIDDEN> (message from Stefan
 Monnier on Mon, 08 Apr 2024 16:45:39 -0400)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN>
 <jwv1q7fd1p5.fsf-monnier+emacs@HIDDEN>
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

> From: Stefan Monnier <monnier@HIDDEN>
> Cc: 70077 <at> debbugs.gnu.org,  acm@HIDDEN,  yantar92@HIDDEN
> Date: Mon, 08 Apr 2024 16:45:39 -0400
> 
> >> Last, but not least: this needs suitable changes in NEWS and ELisp
> >> manual.
> > Working on it.
> 
> Here it is (and aso on `scratch/track-changes`).

Thanks.

> +Using @code{before-change-functions} and @code{after-change-functions}
> +can be difficult in practice because of a number of pitfalls, such as
> +the fact that the two calls are not always properly paired, or some
> +calls may be missing, either because of bugs in the C code or because of
> +inappropriate use of @code{inhibit-modification-hooks}.

I don't think we should talk about bugs in C code in the manual, at
least not so explicitly.  I would rephrase

  the fact that the two calls are not always properly paired, or some
  calls may be missing, either because some Emacs primitives cannot
  properly pair them or because of incorrect use of
  @code{inhibit-modification-hooks}.

> +The start tracking changes, you have to call
   ^^^^^^^^^
"To start"

> +@code{track-changes-register}, passing it a @var{signal} function as
> +argument.  This will return a tracker @var{id} which is used to identify
> +your tracker to the other functions of the library.  The other main
> +function of the library is @code{track-changes-fetch} which lets you
> +fetch the changes you have not yet processed.

The last sentence is redundant, since you are about to describe
track-changes-fetch shortly.

> +When the buffer is modified, the library will call the @var{signal}
> +function to inform you of that change and will immediately start
> +accumulating subsequent changes into a single combined change.
> +The @var{signal} function serves only to warn that a modification
> +occurred but does not receive a description of the change.  Also the
> +library will not call it again until after you processed
> +the change.

The last sentence should IMO say "...until after you retrieved the
change by calling @code{track-changes-fetch}."  The important part
here is to say what "process" means in practice, instead of leaving it
unsaid.

> +To process changes, you need to call @code{track-changes-fetch}, which

That's not really "processing", that's "retrieval", right?  Processing
is what the program does after it retrieves the changes.

> +@defun track-changes-register signal &key nobefore disjoint immediate
> +This function creates a new @emph{tracker}.  Trackers are kept abstract,

I suggest to use "change tracker" instead of just "tracker".  On my
daytime job, "tracker" has a very different meaning, so I stumble each
time I see this used like that.

Also, I suggest to use @dfn for its markup (and add a @cindex for it
for good measure).

> +By default, the call to the @var{signal} function does not happen
> +immediately, but is instead postponed with a 0 seconds timer.
                                                ^^^^^^^^^^^^^^^
A cross-reference to where timers are described is in order there.

> +usually desired to make sure the @var{signal} function is not called too
> +frequently and runs in a permissive context, freeing the client from
> +performance concerns or worries about which operations might be
> +problematic.  If a client wants to have more control, they can provide
> +a non-nil value as the @var{immediate} argument in which case the
         ^^^
@code{nil}

> +If you're not interested in the actual previous content of the buffer,
> +but are using this library only for its ability to combine many small
> +changes into a larger one and to delay the processing to a more
> +convenient time, you can specify a non-nil value for the @var{before}
                                          ^^^
Likewise.

> +While you may like to accumulate many small changes into larger ones,
> +you may not want to do that if the changes are too far apart.  If you
> +specify a non-nil value for the @var{disjoint} argument, the library
                 ^^^
And likewise.

> +modified region, but if you specified a non-nil @var{nobefore} argument
                                               ^^^
And likewise.

> +In case no changes occurred since the last call,
> +@code{track-changes-fetch} simply does not call @var{func} and returns
> +nil.  If changes did occur, it calls @var{func} and returns the value
   ^^^
And likewise.

> +Once @var{func} finishes, @code{track-changes-fetch} re-enables the
> +@var{signal} function so that it will be called the next time a change
> +occurs.  This is the reason why it calls @var{func} instead of returning
> +a description: it makes sure that the @var{signal} function will not be
> +called while you're still processing past changes.

I think there's a subtler issue here that needs to be described
explicitly: if the entire processing of the change is not done inside
FUNC, there's no guarantee that by the time some other function
processes it, the change is still valid and in particular SIGNAL will
not have been called again.  This is not a trivial aspect, since a
program can use FUNC to do just some partial processing, like squirrel
the changes to some buffer for later processing.

>  * New Modes and Packages in Emacs 30.1
>  
> +** New package Track-Changes.

This should be marked with "+++".




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 20:57:46 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 16:57:46 2024
Received: from localhost ([127.0.0.1]:47792 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtw3j-0002rE-QK
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 16:57:46 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:63779)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtw3T-0002pl-Ku
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 16:57:42 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 5341C441C54;
 Mon,  8 Apr 2024 16:57:14 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712609833;
 bh=SEMtbhWii2mjvSKh0w5Kj+9mZeud33f6DM8yMXHbscE=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=Ha6W90gS/bChxi+sEeVO8RFJEnLCwNAItSHzTQ/dScW5HPy90vn8KnWUhrEOFfBtz
 dsYk74HDUHiKUl2PXa1p+/GfEbGJ+HM2Uvs36SU8CyUXIIIR53+v1Y/Zl15MgwrQ/Q
 wVd2amhPz10ymY5CLTP113ZDwjT9S8YFOJz9i6LyjLxA092fFZOuau9N5Bp3SUq7D7
 WV6NjQeAfiG65AD3Ji+bBmtmxGO/mWn4Cw0OjbJWb+U41Yx/iuPR1oeVudzOf5SwWq
 LOWwlRiyEUn9X5Q+dQG5cG5W5ekeI0MwM9v1fiGJsp5l+87G2nQQ9H5HcbID+df9GG
 i3a951jdBq9Tg==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id ECF5E441C2A;
 Mon,  8 Apr 2024 16:57:12 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id BC7B2120185;
 Mon,  8 Apr 2024 16:57:12 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86il0rya4m.fsf@HIDDEN> (Eli Zaretskii's message of "Mon, 08 Apr
 2024 21:36:41 +0300")
Message-ID: <jwvv84rbmze.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN> <86msq3yhot.fsf@HIDDEN>
 <jwvcyqzdcmx.fsf-monnier+emacs@HIDDEN> <86il0rya4m.fsf@HIDDEN>
Date: Mon, 08 Apr 2024 16:57:11 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.025 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

> If this indicates that the slots are of built-in-class type, why do we
> show the cryptic t there?

No, what it's saying is that these slots can contain values of any type
(since any type is a subtype of t).
This `Type` information is a way to document what kind of values can be
found in those slots.  Very often we don't bother specifying it, in
which case `t` is used as a default.

>> >> >> +By default SIGNAL is called as soon as convenient after a change, which is
>> >> >                                ^^^^^^^^^^^^^^^^^^^^^
>> >> > "as soon as it's convenient", I presume?
>> >> Other than the extra " it's", what is the difference?
>> > Nothing.  I indeed thing "it's" is missing there.
>> My local native-English representative says that "both work fine"
>> (somewhat dismissively, I must add).
> In that case I'll yield, but do note that it got me stumbled.

Wow.  To me this is just as natural as "as soon as possible", tho used
admittedly less frequently.

>> > In general, when I want to create a clean slate, I don't care too much
>> > about the dirt I remove.  Why is it important to signal errors because
>> > a state I am dumping had some errors?
>> I don't understand why you think it will signal an error?
> Doesn't cl-assert signal an error if the condition is false?

Yes.  What makes you think it will be false?

>> More to the point, it should signal an error only if I made a mistake in
>> `track-changes.el` or if you messed with the internals.
> I have the latter possibility in mind, yes.  Why catch me doing that
> when I'm cleaning up my mess, _after_ all the damage, such as it is,
> was already done?

But `track-changes--clean-state` is not a function to "clean up
the mess" any more than, say, `track-changes-fetch` or
`track-changes--before`.

>> >> >> +;;;; Extra candidates for the API.
>> >> >> +;; This could be a good alternative to using a temp-buffer like I used in
>> >> >                                                                    ^^^^^^
>> >> > "I"?
>> >> Yes, that refers to the code I wrote.
>> > We don't usually leave such style in long-term comments and
>> > documentation.
>> `grep " I " lisp/**/*.el` suggests otherwise.
> "A journey of a thousand miles begins with one first step."

I disagree with the goal, tho.
If you want, I can add something like "--Stef" at the end to clarify who
is this "I", tho nowadays I tend to rely on `C-x v h` to find that kind
of information.

The text there is just recording my thoughts about this part of the
design of the API.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 20:46:19 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 16:46:18 2024
Received: from localhost ([127.0.0.1]:47785 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtvsb-0001h4-FE
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 16:46:18 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:15124)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtvsR-0001f6-LN
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 16:46:10 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 5D45C441C2A;
 Mon,  8 Apr 2024 16:45:49 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712609141;
 bh=DefC+tGrm+5mDXXBCXt5f+tPfvKyMAXy9fX0ww7LxEA=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=AbSH/5S1kChQKq7DaBZYFTXLVNpOBbewigUrcJMGkdOz9roYHRpvV6ZyXuINhqMAp
 5synTrW/P5wjJZavLNFqBpiEbpJa9J89LromNW9bgFJJ24RKsftjkM3OG9xvKN32He
 AF1l2cE6pgpKYtQv1RRm2jfZCGNSu67EmxtWmlZYrrBdN0qtCY3n+bPef++qVlnc+X
 YJNbmcudvDTaaBjcCaj/sv9gkkEgc5HhADIxEs2lEyuH1Ajxprnj9VFVJklqzjXKXq
 onErKB+0BsLqW93cq++mB6bCCnQuxmm2fHJV6qV29X+bXNrz8SNoqm9lRvNzI+BsyP
 /CfzAiNvjycnA==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 6F5464413F4;
 Mon,  8 Apr 2024 16:45:41 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 1FA35120370;
 Mon,  8 Apr 2024 16:45:41 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvplv0c662.fsf-monnier+emacs@HIDDEN> (Stefan Monnier's message
 of "Mon, 08 Apr 2024 11:24:38 -0400")
Message-ID: <jwv1q7fd1p5.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN>
Date: Mon, 08 Apr 2024 16:45:39 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.026 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

--=-=-=
Content-Type: text/plain

>> Last, but not least: this needs suitable changes in NEWS and ELisp
>> manual.
> Working on it.

Here it is (and aso on `scratch/track-changes`).


        Stefan

--=-=-=
Content-Type: text/x-diff; charset=iso-8859-1
Content-Disposition: inline;
 filename=0001-lisp-emacs-lisp-track-changes.el-New-file.patch
Content-Transfer-Encoding: quoted-printable

From b676b0ff3f046a1456a433a4b7741599c7ae4714 Mon Sep 17 00:00:00 2001
From: Stefan Monnier <monnier@HIDDEN>
Date: Fri, 5 Apr 2024 17:37:32 -0400
Subject: [PATCH] lisp/emacs-lisp/track-changes.el: New file

This new package provides an API that is easier to use right than
our `*-change-functions` hooks.

The patch includes changes to `diff-mode.el` and `eglot.el` to
make use of this new package.

* lisp/emacs-lisp/track-changes.el: New file.
* test/lisp/emacs-lisp/track-changes-tests.el: New file.
* doc/lispref/text.texi (Tracking changes): New subsection.

* lisp/progmodes/eglot.el: Require `track-changes`.
(eglot--virtual-pos-to-lsp-position): New function.
(eglot--track-changes): New var.
(eglot--managed-mode): Use `track-changes-register` i.s.o
`after/before-change-functions` when available.
(eglot--track-changes-signal): New function, partly extracted from
`eglot--after-change`.
(eglot--after-change): Use it.
(eglot--track-changes-fetch): New function.
(eglot--signal-textDocument/didChange): Use it.

* lisp/vc/diff-mode.el: Require `track-changes`.
Also require `easy-mmode` before the `eval-when-compile`s.
(diff-unhandled-changes): Delete variable.
(diff-after-change-function): Delete function.
(diff--track-changes-function): Rename from `diff-post-command-hook`
and adjust to new calling convention.
(diff--track-changes): New variable.
(diff--track-changes-signal): New function.
(diff-mode, diff-minor-mode): Use it with `track-changes-register`.
---
 doc/lispref/text.texi                       | 141 +++++
 etc/NEWS                                    |  11 +
 lisp/emacs-lisp/track-changes.el            | 599 ++++++++++++++++++++
 lisp/progmodes/eglot.el                     |  64 ++-
 lisp/vc/diff-mode.el                        |  85 ++-
 test/lisp/emacs-lisp/track-changes-tests.el | 156 +++++
 6 files changed, 1003 insertions(+), 53 deletions(-)
 create mode 100644 lisp/emacs-lisp/track-changes.el
 create mode 100644 test/lisp/emacs-lisp/track-changes-tests.el

diff --git a/doc/lispref/text.texi b/doc/lispref/text.texi
index 18f0ee88fe5..2875f6f6ba8 100644
--- a/doc/lispref/text.texi
+++ b/doc/lispref/text.texi
@@ -6375,3 +6375,144 @@ Change Hooks
 use @code{combine-change-calls} or @code{combine-after-change-calls}
 instead.
 @end defvar
+
+@node Tracking changes
+@subsection Tracking changes
+@cindex track-changes
+
+Using @code{before-change-functions} and @code{after-change-functions}
+can be difficult in practice because of a number of pitfalls, such as
+the fact that the two calls are not always properly paired, or some
+calls may be missing, either because of bugs in the C code or because of
+inappropriate use of @code{inhibit-modification-hooks}.  Furthermore,
+many restrictions apply to those hook functions, such as the fact that
+they basically should never modify the current buffer, nor use an
+operation that may block, and they proceed quickly because
+some commands may call these hooks a large number of times.
+
+The Track-Changes library fundamentally provides an alternative API,
+built on top of those hooks.  Compared to @code{after-change-functions},
+the first important difference is that, instead of providing the bounds
+of the change and the previous length, it provides the bounds of the
+change and the actual previous content of that region.  The need to
+extract information from the original contents of the buffer is one of
+the main reasons why some packages need to use both
+@code{before-change-functions} and @code{after-change-functions} and
+then try to match them up.
+
+The second difference is that it decouples the notification of a change
+from the act of processing it, and it automatically combines into
+a single change operation all the changes that occur between the first
+change and the actual processing.  This makes it natural and easy to
+process the changes at a larger granularity, such as once per command,
+and eliminates most of the restrictions that apply to the usual change
+hook functions, making it possible to use blocking operations or to
+modify the buffer
+
+The start tracking changes, you have to call
+@code{track-changes-register}, passing it a @var{signal} function as
+argument.  This will return a tracker @var{id} which is used to identify
+your tracker to the other functions of the library.  The other main
+function of the library is @code{track-changes-fetch} which lets you
+fetch the changes you have not yet processed.
+
+When the buffer is modified, the library will call the @var{signal}
+function to inform you of that change and will immediately start
+accumulating subsequent changes into a single combined change.
+The @var{signal} function serves only to warn that a modification
+occurred but does not receive a description of the change.  Also the
+library will not call it again until after you processed
+the change.
+
+To process changes, you need to call @code{track-changes-fetch}, which
+will provide you with the bounds of the changes accumulated since the
+last call, as well as the previous content of that region.  It will also
+``re-arm'' the @var{signal} function so that the library will call it
+again after the next buffer modification.
+
+@defun track-changes-register signal &key nobefore disjoint immediate
+This function creates a new @emph{tracker}.  Trackers are kept abstract,
+so we refer to them as mere identities, and the function thus returns
+the tracker's @var{id}.
+
+@var{signal} is a function that the library will call to notify of
+a change.  It will sometimes call it with a single argument and
+sometimes with two.  Upon the first change to the buffer since this
+tracker last called @code{track-changes-fetch}, the library calls this
+@var{signal} function with a single argument holding the @var{id} of
+the tracker.
+
+By default, the call to the @var{signal} function does not happen
+immediately, but is instead postponed with a 0 seconds timer.  This is
+usually desired to make sure the @var{signal} function is not called too
+frequently and runs in a permissive context, freeing the client from
+performance concerns or worries about which operations might be
+problematic.  If a client wants to have more control, they can provide
+a non-nil value as the @var{immediate} argument in which case the
+library will call the @var{signal} function directly from
+@code{after-change-functions}.  Beware that it means that the
+@var{signal} function has to be careful not to modify the buffer or use
+operations that may block.
+
+If you're not interested in the actual previous content of the buffer,
+but are using this library only for its ability to combine many small
+changes into a larger one and to delay the processing to a more
+convenient time, you can specify a non-nil value for the @var{before}
+argument.  This will make it so the library provides you only with the
+length of the previous content, just like
+@code{after-change-functions}.  It will also allow the library to save
+some work.
+
+While you may like to accumulate many small changes into larger ones,
+you may not want to do that if the changes are too far apart.  If you
+specify a non-nil value for the @var{disjoint} argument, the library
+will let you know when a change is about to occur ``far'' from the
+currently pending ones by calling the @var{signal} function right away,
+passing it two arguments this time: the @var{id} of the tracker, and the
+number of characters that separates the upcoming change from the
+already pending changes.  This in itself does not prevent combining this
+new change with the previous ones, so if you think the upcoming change
+is indeed too far, you need to call @code{track-change-fetch}
+right away.
+Beware that when the @var{signal} function is called because of
+a disjoint change, this happens directly from
+@code{before-change-functions}, so the usual restrictions apply about
+modifying the buffer or using operations that may block.
+@end defun
+
+@defun track-changes-fetch id func
+This is the function that lets you find out what has changed in the
+buffer.  By providing the tracker @var{id} you let the library figure
+out which changes have already been seen by your tracker.  Instead of
+returning a description of the changes, @code{track-changes-fetch} calls
+the @var{func} function with that description in the form of
+3 arguments: @var{beg}, @var{end}, and @var{before}, where
+@code{@var{beg}..@var{end}} delimit the region that was modified and
+@var{before} describes the previous content of that region.
+Usually @var{before} is a string containing the previous text of the
+modified region, but if you specified a non-nil @var{nobefore} argument
+to @code{track-changes-register}, then it is replaced by the number of
+characters of that previous text.
+
+In case no changes occurred since the last call,
+@code{track-changes-fetch} simply does not call @var{func} and returns
+nil.  If changes did occur, it calls @var{func} and returns the value
+returned by @var{func}.  But note that @var{func} is called just once
+regardless of how many changes occurred: those are summarized into
+a single @var{beg}/@var{end}/@var{before} triplet.
+
+Once @var{func} finishes, @code{track-changes-fetch} re-enables the
+@var{signal} function so that it will be called the next time a change
+occurs.  This is the reason why it calls @var{func} instead of returning
+a description: it makes sure that the @var{signal} function will not be
+called while you're still processing past changes.
+@end defun
+
+@defun track-changes-unregister id
+This function tells the library that the tracker @var{id} does not need
+to know about buffer changes any more.  Most clients will never want to
+stop tracking changes, but for clients such as minor modes, it is
+important to call this function when the minor mode is disabled,
+otherwise the tracker will keep accumulating changes and consume more
+and more resources.
+@end defun
diff --git a/etc/NEWS b/etc/NEWS
index b2543ae77d9..d85b65abd0b 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1569,6 +1569,17 @@ This allows disabling JavaScript in xwidget Webkit s=
essions.
 
 * New Modes and Packages in Emacs 30.1
=20
+** New package Track-Changes.
+This library is a layer of abstraction above 'before-change-functions'
+and 'after-change-functions' which provides a superset of
+the functionality of 'after-change-functions':
+- It provides the actual previous text rather than only its length.
+- It takes care of accumulating and bundling changes until a time when
+  its client finds it convenient to react to them.
+- It detects most cases where some changes were not properly
+  reported (calls to 'before/after-change-functions' that are
+  incorrectly paired, missing, etc...) and reports them adequately.
+
 ** New major modes based on the tree-sitter library
=20
 +++
diff --git a/lisp/emacs-lisp/track-changes.el b/lisp/emacs-lisp/track-chang=
es.el
new file mode 100644
index 00000000000..fef74074582
--- /dev/null
+++ b/lisp/emacs-lisp/track-changes.el
@@ -0,0 +1,599 @@
+;;; track-changes.el --- API to react to buffer modifications  -*- lexical=
-binding: t; -*-
+
+;; Copyright (C) 2024  Free Software Foundation, Inc.
+
+;; Author: Stefan Monnier <monnier@HIDDEN>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library is a layer of abstraction above `before-change-functions'
+;; and `after-change-functions' which takes care of accumulating changes
+;; until a time when its client finds it convenient to react to them.
+;;
+;; It provides an API that is easier to use correctly than our
+;; `*-change-functions' hooks.  Problems that it claims to solve:
+;;
+;; - Before and after calls are not necessarily paired.
+;; - The beg/end values don't always match.
+;; - There's usually only one call to the hooks per command but
+;;   there can be thousands of calls from within a single command,
+;;   so naive users will tend to write code that performs poorly
+;;   in those rare cases.
+;; - The hooks are run at a fairly low-level so there are things they
+;;   really shouldn't do, such as modify the buffer or wait.
+;; - The after call doesn't get enough info to rebuild the before-change s=
tate,
+;;   so some callers need to use both before-c-f and after-c-f (and then
+;;   deal with the first two points above).
+;;
+;; The new API is almost like `after-change-functions' except that:
+;; - It provides the "before string" (i.e. the previous content of
+;;   the changed area) rather than only its length.
+;; - It can combine several changes into larger ones.
+;; - Clients do not have to process changes right away, instead they
+;;   can let changes accumulate (by combining them into a larger change)
+;;   until it is convenient for them to process them.
+;; - By default, changes are signaled at most once per command.
+
+;; The API consists in the following functions:
+;;
+;;     (track-changes-register SIGNAL &key NOBEFORE DISJOINT IMMEDIATE)
+;;     (track-changes-fetch ID FUNC)
+;;     (track-changes-unregister ID)
+;;
+;; A typical use case might look like:
+;;
+;;     (defvar my-foo--change-tracker nil)
+;;     (define-minor-mode my-foo-mode
+;;       "Fooing like there's no tomorrow."
+;;       (if (null my-foo-mode)
+;;           (when my-foo--change-tracker
+;;             (track-changes-unregister my-foo--change-tracker)
+;;             (setq my-foo--change-tracker nil))
+;;         (unless my-foo--change-tracker
+;;           (setq my-foo--change-tracker
+;;                 (track-changes-register
+;;                  (lambda (id)
+;;                    (track-changes-fetch
+;;                     id (lambda (beg end before)
+;;                          ..DO THE THING..))))))))
+
+;;; Code:
+
+(require 'cl-lib)
+
+;;;; Internal types and variables.
+
+(cl-defstruct (track-changes--tracker
+               (:noinline t)
+               (:constructor nil)
+               (:constructor track-changes--tracker ( signal state
+                                                      &optional
+                                                      nobefore immediate)))
+  signal state nobefore immediate)
+
+(cl-defstruct (track-changes--state
+               (:noinline t)
+               (:constructor nil)
+               (:constructor track-changes--state ()))
+  "Object holding a description of a buffer state.
+BEG..END is the area that was changed and BEFORE is its previous content.
+If the current buffer currently holds the content of the next state, you c=
an
+get the contents of the previous state with:
+
+    (concat (buffer-substring (point-min) beg)
+                before
+                (buffer-substring end (point-max)))
+
+NEXT is the next state object (i.e. a more recent state).
+If NEXT is nil it means it's most recent state and it may be incomplete
+\(BEG/END/BEFORE may be nil), in which case those fields will take their
+values from `track-changes--before-(beg|end|before)' when the next
+state is create."
+  (beg (point-max))
+  (end (point-min))
+  (before nil)
+  (next nil))
+
+(defvar-local track-changes--trackers ()
+  "List of trackers currently registered in the buffer.")
+(defvar-local track-changes--clean-trackers ()
+  "List of trackers that are clean.
+Those are the trackers that get signaled when a change is made.")
+
+(defvar-local track-changes--disjoint-trackers ()
+ "List of trackers that want to react to disjoint changes.
+These trackers are signaled every time track-changes notices
+that some upcoming changes touch another \"distant\" part of the buffer.")
+
+(defvar-local track-changes--state nil)
+
+;; `track-changes--before-*' keep track of the content of the
+;; buffer when `track-changes--state' was cleaned.
+(defvar-local track-changes--before-beg 0
+  "Beginning position of the remembered \"before string\".")
+(defvar-local track-changes--before-end 0
+  "End position of the text replacing the \"before string\".")
+(defvar-local track-changes--before-string ""
+  "String holding some contents of the buffer before the current change.
+This string is supposed to cover all the already modified areas plus
+the upcoming modifications announced via `before-change-functions'.
+If all trackers are `nobefore', then this holds the `buffer-size' before
+the current change.")
+(defvar-local track-changes--before-no t
+  "If non-nil, all the trackers are `nobefore'.
+Should be equal to (memq #\\=3D'track-changes--before before-change-functi=
ons).")
+
+(defvar-local track-changes--before-clean 'unset
+  "Status of `track-changes--before-*' vars.
+More specifically it indicates which \"before\" they hold.
+- nil: The vars hold the \"before\" info of the current state.
+- `unset': The vars hold the \"before\" info of some older state.
+  This is what it is set to right after creating a fresh new state.
+- `set': Like nil but the state is still clean because the buffer has not
+  been modified yet.  This is what it is set to after the first
+  `before-change-functions'  but before an `after-change-functions'.")
+
+(defvar-local track-changes--buffer-size nil
+  "Current size of the buffer, as far as this library knows.
+This is used to try and detect cases where buffer modifications are \"lost=
\".")
+
+;;;; Exposed API.
+
+(cl-defun track-changes-register ( signal &key nobefore disjoint immediate)
+  "Register a new tracker whose change-tracking function is SIGNAL.
+Return the ID of the new tracker.
+
+SIGNAL is a function that will be called with one argument (the tracker ID)
+after the current buffer is modified, so that it can react to the change.
+Once called, SIGNAL is not called again until `track-changes-fetch'
+is called with the corresponding tracker ID.
+
+If optional argument NOBEFORE is non-nil, it means that this tracker does
+not need the BEFORE strings (it will receive their size instead).
+
+If optional argument DISJOINT is non-nil, SIGNAL is called every time just
+before combining changes from \"distant\" parts of the buffer.
+This is needed when combining disjoint changes into one bigger change
+is unacceptable, typically for performance reasons.
+These calls are distinguished from normal calls by calling SIGNAL with
+a second argument which is the distance between the upcoming change and
+the previous changes.
+BEWARE: In that case SIGNAL is called directly from `before-change-functio=
ns'
+and should thus be extra careful: don't modify the buffer, don't call a fu=
nction
+that may block, ...
+In order to prevent the upcoming change from being combined with the previ=
ous
+changes, SIGNAL needs to call `track-changes-fetch' before it returns.
+
+By default SIGNAL is called after a change via a 0 seconds timer.
+If optional argument IMMEDIATE is non-nil it means SIGNAL should be called
+as soon as a change is detected,
+BEWARE: In that case SIGNAL is called directly from `after-change-function=
s'
+and should thus be extra careful: don't modify the buffer, don't call a fu=
nction
+that may block, do as little work as possible, ...
+When IMMEDIATE is non-nil, the SIGNAL should probably not always call
+`track-changes-fetch', since that would defeat the purpose of this library=
."
+  (when (and nobefore disjoint)
+    ;; FIXME: Without `before-change-functions', we can discover
+    ;; a disjoint change only after the fact, which is not good enough.
+    ;; But we could use a stripped down before-change-function,
+    (error "`disjoint' not supported for `nobefore' trackers"))
+  (track-changes--clean-state)
+  (unless nobefore
+    (setq track-changes--before-no nil)
+    (add-hook 'before-change-functions #'track-changes--before nil t))
+  (add-hook 'after-change-functions  #'track-changes--after  nil t)
+  (let ((tracker (track-changes--tracker signal track-changes--state
+                                         nobefore immediate)))
+    (push tracker track-changes--trackers)
+    (push tracker track-changes--clean-trackers)
+    (when disjoint
+      (push tracker track-changes--disjoint-trackers))
+    tracker))
+
+(defun track-changes-unregister (id)
+  "Remove the tracker denoted by ID.
+Trackers can consume resources (especially if `track-changes-fetch' is
+not called), so it is good practice to unregister them when you don't
+need them any more."
+  (unless (memq id track-changes--trackers)
+    (error "Unregistering a non-registered tracker: %S" id))
+  (setq track-changes--trackers (delq id track-changes--trackers))
+  (setq track-changes--clean-trackers (delq id track-changes--clean-tracke=
rs))
+  (setq track-changes--disjoint-trackers
+        (delq id track-changes--disjoint-trackers))
+  (when (cl-every #'track-changes--tracker-nobefore track-changes--tracker=
s)
+    (setq track-changes--before-no t)
+    (remove-hook 'before-change-functions #'track-changes--before t))
+  (when (null track-changes--trackers)
+    (mapc #'kill-local-variable
+          '(track-changes--before-beg
+            track-changes--before-end
+            track-changes--before-string
+            track-changes--buffer-size
+            track-changes--before-clean
+            track-changes--state))
+    (remove-hook 'after-change-functions  #'track-changes--after  t)))
+
+(defun track-changes-fetch (id func)
+  "Fetch the pending changes for tracker ID pass them to FUNC.
+ID is the tracker ID returned by a previous `track-changes-register'.
+FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
+where BEGIN..END delimit the region that was changed since the last
+time `track-changes-fetch' was called and BEFORE is a string containing
+the previous content of that region (or just its length as an integer
+if the tracker ID was registered with the `nobefore' option).
+If track-changes detected that some changes were missed, then BEFORE will
+be the symbol `error' to indicate that the buffer got out of sync.
+This reflects a bug somewhere, so please report it when it happens.
+
+If no changes occurred since the last time, it doesn't call FUNC and
+returns nil, otherwise it returns the value returned by FUNC
+and re-enable the TRACKER corresponding to ID."
+  (cl-assert (memq id track-changes--trackers))
+  (unless (equal track-changes--buffer-size (buffer-size))
+    (track-changes--recover-from-error))
+  (let ((beg nil)
+        (end nil)
+        (before t)
+        (lenbefore 0)
+        (states ()))
+    ;; Transfer the data from `track-changes--before-string'
+    ;; to the tracker's state object, if needed.
+    (track-changes--clean-state)
+    ;; We want to combine the states from most recent to oldest,
+    ;; so reverse them.
+    (let ((state (track-changes--tracker-state id)))
+      (while state
+        (push state states)
+        (setq state (track-changes--state-next state))))
+
+    (cond
+     ((eq (car states) track-changes--state)
+      (cl-assert (null (track-changes--state-before (car states))))
+      (setq states (cdr states)))
+     (t
+      ;; The states are disconnected from the latest state because
+      ;; we got out of sync!
+      (cl-assert (eq (track-changes--state-before (car states)) 'error))
+      (setq beg (point-min))
+      (setq end (point-max))
+      (setq before 'error)
+      (setq states nil)))
+
+    (dolist (state states)
+      (let ((prevbeg (track-changes--state-beg state))
+            (prevend (track-changes--state-end state))
+            (prevbefore (track-changes--state-before state)))
+        (if (eq before t)
+            (progn
+              ;; This is the most recent change.  Just initialize the vars.
+              (setq beg prevbeg)
+              (setq end prevend)
+              (setq lenbefore
+                    (if (stringp prevbefore) (length prevbefore) prevbefor=
e))
+              (setq before
+                    (unless (track-changes--tracker-nobefore id) prevbefor=
e)))
+          (let ((endb (+ beg lenbefore)))
+            (when (< prevbeg beg)
+              (if (not before)
+                  (setq lenbefore (+ (- beg prevbeg) lenbefore))
+                (setq before
+                      (concat (buffer-substring-no-properties
+                               prevbeg beg)
+                              before))
+                (setq lenbefore (length before)))
+              (setq beg prevbeg)
+              (cl-assert (=3D endb (+ beg lenbefore))))
+            (when (< endb prevend)
+              (let ((new-end (+ end (- prevend endb))))
+                (if (not before)
+                    (setq lenbefore (+ lenbefore (- new-end end)))
+                  (setq before
+                        (concat before
+                                (buffer-substring-no-properties
+                                 end new-end)))
+                  (setq lenbefore (length before)))
+                (setq end new-end)
+                (cl-assert (=3D prevend (+ beg lenbefore)))
+                (setq endb (+ beg lenbefore))))
+            (cl-assert (<=3D beg prevbeg prevend endb))
+            ;; The `prevbefore' is covered by the new one.
+            (if (not before)
+                (setq lenbefore
+                      (+ (- prevbeg beg)
+                         (if (stringp prevbefore)
+                             (length prevbefore) prevbefore)
+                         (- endb prevend)))
+              (setq before
+                    (concat (substring before 0 (- prevbeg beg))
+                            prevbefore
+                            (substring before (- (length before)
+                                                 (- endb prevend)))))
+              (setq lenbefore (length before)))))))
+    (if (null beg)
+        (progn
+          (cl-assert (null states))
+          (cl-assert (memq id track-changes--clean-trackers))
+          (cl-assert (eq (track-changes--tracker-state id)
+                         track-changes--state))
+          ;; Nothing to do.
+          nil)
+      (cl-assert (<=3D (point-min) beg end (point-max)))
+      ;; Update the tracker's state *before* running `func' so we don't ri=
sk
+      ;; mistakenly replaying the changes in case `func' exits non-locally.
+      (setf (track-changes--tracker-state id) track-changes--state)
+      (unwind-protect (funcall func beg end (or before lenbefore))
+        ;; Re-enable the tracker's signal only after running `func', so
+        ;; as to avoid recursive invocations.
+        (cl-pushnew id track-changes--clean-trackers)))))
+
+;;;; Auxiliary functions.
+
+(defun track-changes--clean-state ()
+  (cond
+   ((null track-changes--state)
+    (cl-assert track-changes--before-clean)
+    (cl-assert (null track-changes--buffer-size))
+    ;; No state has been created yet.  Do it now.
+    (setq track-changes--buffer-size (buffer-size))
+    (when track-changes--before-no
+      (setq track-changes--before-string (buffer-size)))
+    (setq track-changes--state (track-changes--state)))
+   (track-changes--before-clean nil)
+   (t
+    (cl-assert (<=3D (track-changes--state-beg track-changes--state)
+                   (track-changes--state-end track-changes--state)))
+    (let ((actual-beg (track-changes--state-beg track-changes--state))
+          (actual-end (track-changes--state-end track-changes--state)))
+      (if track-changes--before-no
+          (progn
+            (cl-assert (integerp track-changes--before-string))
+            (setf (track-changes--state-before track-changes--state)
+                  (- track-changes--before-string
+                     (- (buffer-size) (- actual-end actual-beg))))
+            (setq track-changes--before-string (buffer-size)))
+        (cl-assert (<=3D track-changes--before-beg
+                       actual-beg actual-end
+                       track-changes--before-end))
+        (cl-assert (null (track-changes--state-before track-changes--state=
)))
+        ;; The `track-changes--before-*' vars can cover more text than the
+        ;; actually modified area, so trim it down now to the relevant par=
t.
+        (unless (=3D (- track-changes--before-end track-changes--before-be=
g)
+                   (- actual-end actual-beg))
+          (setq track-changes--before-string
+                (substring track-changes--before-string
+                           (- actual-beg track-changes--before-beg)
+                           (- (length track-changes--before-string)
+                              (- track-changes--before-end actual-end))))
+          (setq track-changes--before-beg actual-beg)
+          (setq track-changes--before-end actual-end))
+        (setf (track-changes--state-before track-changes--state)
+              track-changes--before-string)))
+    ;; Note: We preserve `track-changes--before-*' because they may still
+    ;; be needed, in case `after-change-functions' are run before the next
+    ;; `before-change-functions'.
+    ;; Instead, we set `track-changes--before-clean' to `unset' to mean th=
at
+    ;; `track-changes--before-*' can be reset at the next
+    ;; `before-change-functions'.
+    (setq track-changes--before-clean 'unset)
+    (let ((new (track-changes--state)))
+      (setf (track-changes--state-next track-changes--state) new)
+      (setq track-changes--state new)))))
+
+(defvar track-changes--disjoint-threshold 100
+  "Number of chars below which changes are not considered disjoint.")
+
+(defvar track-changes--error-log ()
+  "List of errors encountered.
+Each element is a triplet (BUFFER-NAME BACKTRACE RECENT-KEYS).")
+
+(defun track-changes--recover-from-error ()
+  ;; We somehow got out of sync.  This is usually the result of a bug
+  ;; elsewhere that causes the before-c-f and after-c-f to be improperly
+  ;; paired, or to be skipped altogether.
+  ;; Not much we can do, other than force a full re-synchronization.
+  (warn "Missing/incorrect calls to `before/after-change-functions'!!
+Details logged to `track-changes--error-log'")
+  (push (list (buffer-name)
+              (backtrace-frames 'track-changes--recover-from-error)
+              (recent-keys 'include-cmds))
+        track-changes--error-log)
+  (setq track-changes--before-clean 'unset)
+  (setq track-changes--buffer-size (buffer-size))
+  ;; Create a new state disconnected from the previous ones!
+  ;; Mark the previous one as junk, just to be clear.
+  (setf (track-changes--state-before track-changes--state) 'error)
+  (setq track-changes--state (track-changes--state)))
+
+(defun track-changes--before (beg end)
+  (cl-assert track-changes--state)
+  (cl-assert (<=3D beg end))
+  (let* ((size (- end beg))
+         (reset (lambda ()
+                  (cl-assert track-changes--before-clean)
+                  (setq track-changes--before-clean 'set)
+                  (setf track-changes--before-string
+                        (buffer-substring-no-properties beg end))
+                  (setf track-changes--before-beg beg)
+                  (setf track-changes--before-end end)))
+
+         (signal-if-disjoint
+          (lambda (pos1 pos2)
+            (let ((distance (- pos2 pos1)))
+              (when (> distance
+                       (max track-changes--disjoint-threshold
+                            ;; If the distance is smaller than the size of=
 the
+                            ;; current change, then we may as well conside=
r it
+                            ;; as "near".
+                            (length track-changes--before-string)
+                            size
+                            (- track-changes--before-end
+                               track-changes--before-beg)))
+                (dolist (tracker track-changes--disjoint-trackers)
+                  (funcall (track-changes--tracker-signal tracker)
+                           tracker distance))
+                ;; Return non-nil if the state was cleaned along the way.
+                track-changes--before-clean)))))
+
+    (if track-changes--before-clean
+        (progn
+          ;; Detect disjointness with previous changes here as well,
+          ;; so that if a client calls `track-changes-fetch' all the time,
+          ;; it doesn't prevent others from getting a disjointness signal.
+          (when (and track-changes--before-beg
+                     (let ((found nil))
+                       (dolist (tracker track-changes--disjoint-trackers)
+                         (unless (memq tracker track-changes--clean-tracke=
rs)
+                           (setq found t)))
+                       found))
+            ;; There's at least one `tracker' that wants to know about dis=
joint
+            ;; changes *and* it has unseen pending changes.
+            ;; FIXME: This can occasionally signal a tracker that's clean.
+            (if (< beg track-changes--before-beg)
+                (funcall signal-if-disjoint end track-changes--before-beg)
+              (funcall signal-if-disjoint track-changes--before-end beg)))
+          (funcall reset))
+      (cl-assert (save-restriction
+                   (widen)
+                   (<=3D (point-min)
+                       track-changes--before-beg
+                       track-changes--before-end
+                       (point-max))))
+      (when (< beg track-changes--before-beg)
+        (if (and track-changes--disjoint-trackers
+                 (funcall signal-if-disjoint end track-changes--before-beg=
))
+            (funcall reset)
+          (let* ((old-bbeg track-changes--before-beg)
+                 ;; To avoid O(N=B2) behavior when faced with many small c=
hanges,
+                 ;; we copy more than needed.
+                 (new-bbeg (min (max (point-min)
+                                     (- old-bbeg
+                                        (length track-changes--before-stri=
ng)))
+                                beg)))
+            (setf track-changes--before-beg new-bbeg)
+            (cl-callf (lambda (old new) (concat new old))
+                track-changes--before-string
+              (buffer-substring-no-properties new-bbeg old-bbeg)))))
+
+      (when (< track-changes--before-end end)
+        (if (and track-changes--disjoint-trackers
+                 (funcall signal-if-disjoint track-changes--before-end beg=
))
+            (funcall reset)
+          (let* ((old-bend track-changes--before-end)
+                 ;; To avoid O(N=B2) behavior when faced with many small c=
hanges,
+                 ;; we copy more than needed.
+                 (new-bend (max (min (point-max)
+                                     (+ old-bend
+                                        (length track-changes--before-stri=
ng)))
+                                end)))
+            (setf track-changes--before-end new-bend)
+            (cl-callf concat track-changes--before-string
+              (buffer-substring-no-properties old-bend new-bend))))))))
+
+(defun track-changes--after (beg end len)
+  (cl-assert track-changes--state)
+  (and (eq track-changes--before-clean 'unset)
+       (not track-changes--before-no)
+       ;; This can be a sign that a `before-change-functions' went missing,
+       ;; or that we called `track-changes--clean-state' between
+       ;; a `before-change-functions' and `after-change-functions'.
+       (track-changes--before beg end))
+  (setq track-changes--before-clean nil)
+  (let ((offset (- (- end beg) len)))
+    (cl-incf track-changes--before-end offset)
+    (cl-incf track-changes--buffer-size offset)
+    (if (not (or track-changes--before-no
+                 (save-restriction
+                   (widen)
+                   (<=3D (point-min)
+                       track-changes--before-beg
+                       beg end
+                       track-changes--before-end
+                       (point-max)))))
+        ;; BEG..END is not covered by previous `before-change-functions'!!
+        (track-changes--recover-from-error)
+      ;; Note the new changes.
+      (when (< beg (track-changes--state-beg track-changes--state))
+        (setf (track-changes--state-beg track-changes--state) beg))
+      (cl-callf (lambda (old-end) (max end (+ old-end offset)))
+          (track-changes--state-end track-changes--state))
+      (cl-assert (or track-changes--before-no
+                     (<=3D track-changes--before-beg
+                         (track-changes--state-beg track-changes--state)
+                         beg end
+                         (track-changes--state-end track-changes--state)
+                         track-changes--before-end)))))
+  (while track-changes--clean-trackers
+    (let ((tracker (pop track-changes--clean-trackers)))
+      (if (track-changes--tracker-immediate tracker)
+          (funcall (track-changes--tracker-signal tracker) tracker)
+        (run-with-timer 0 nil #'track-changes--call-signal
+                        (current-buffer) tracker)))))
+
+(defun track-changes--call-signal (buf tracker)
+  (when (buffer-live-p buf)
+    (with-current-buffer buf
+      ;; Silence ourselves if `track-changes-fetch' was called in the mean=
 time.
+      (unless (memq tracker track-changes--clean-trackers)
+        (funcall (track-changes--tracker-signal tracker) tracker)))))
+
+;;;; Extra candidates for the API.
+
+;; This could be a good alternative to using a temp-buffer like I used in
+;; Eglot, since presumably we've just been changing this very area of the
+;; buffer, so the gap should be ready nearby,
+;; It may seem silly to go back to the previous state, since we could have
+;; used `before-change-functions' to run FUNC right then when we were in
+;; that state.  The advantage is that with track-changes we get to decide
+;; retroactively which state is the one for which we want to call FUNC and
+;; which BEG..END to use: when that state was current we may have known
+;; then that it would be "the one" but we didn't know what BEG and END
+;; should be because those depend on the changes that came afterwards.
+(defun track-changes--in-revert (beg end before func)
+  "Call FUNC with the buffer contents temporarily reverted to BEFORE.
+FUNC is called with no arguments and with point right after BEFORE.
+FUNC is not allowed to modify the buffer and it should refrain from using
+operations that use a cache populated from the buffer's content,
+such as `syntax-ppss'."
+  (catch 'track-changes--exit
+    (with-silent-modifications ;; This has to be outside `atomic-change-gr=
oup'.
+      (atomic-change-group
+        (goto-char end)
+        (insert-before-markers before)
+        (delete-region beg end)
+        (throw 'track-changes--exit
+               (let ((inhibit-read-only nil)
+                     (buffer-read-only t))
+                 (funcall func)))))))
+
+(defun track-changes--reset (id)
+  "Mark all past changes as handled for tracker ID.
+Does not re-enable ID's signal."
+  (track-changes--clean-state)
+  (setf (track-changes--tracker-state id) track-changes--state))
+
+(defun track-changes--pending-p (id)
+  "Return non-nil if there are pending changes for tracker ID."
+  (not (memq id track-changes--clean-trackers)))
+
+(defmacro with--track-changes (id vars &rest body)
+  (declare (indent 2) (debug (form sexp body)))
+  `(track-changes-fetch ,id (lambda ,vars ,@body)))
+
+(provide 'track-changes)
+;;; track-changes.el end here.
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 7f4284bf09d..478e7687bb3 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -110,6 +110,7 @@
 (require 'text-property-search nil t)
 (require 'diff-mode)
 (require 'diff)
+(require 'track-changes nil t)
=20
 ;; These dependencies are also GNU ELPA core packages.  Because of
 ;; bug#62576, since there is a risk that M-x package-install, despite
@@ -1732,6 +1733,9 @@ eglot-utf-16-linepos
   "Calculate number of UTF-16 code units from position given by LBP.
 LBP defaults to `eglot--bol'."
   (/ (- (length (encode-coding-region (or lbp (eglot--bol))
+                                      ;; FIXME: How could `point' ever be
+                                      ;; larger than `point-max' (sounds l=
ike
+                                      ;; a bug in Emacs).
                                       ;; Fix github#860
                                       (min (point) (point-max)) 'utf-16 t))
         2)
@@ -1749,6 +1753,24 @@ eglot--pos-to-lsp-position
          :character (progn (when pos (goto-char pos))
                            (funcall eglot-current-linepos-function)))))
=20
+(defun eglot--virtual-pos-to-lsp-position (pos string)
+  "Return the LSP position at the end of STRING if it were inserted at POS=
."
+  (eglot--widening
+   (goto-char pos)
+   (forward-line 0)
+   ;; LSP line is zero-origin; Emacs is one-origin.
+   (let ((posline (1- (line-number-at-pos nil t)))
+         (linebeg (buffer-substring (point) pos))
+         (colfun eglot-current-linepos-function))
+     ;; Use a temp buffer because:
+     ;; - I don't know of a fast way to count newlines in a string.
+     ;; - We currently don't have `eglot-current-linepos-function' for str=
ings.
+     (with-temp-buffer
+       (insert linebeg string)
+       (goto-char (point-max))
+       (list :line (+ posline (1- (line-number-at-pos nil t)))
+             :character (funcall colfun))))))
+
 (defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
   "Function to move to a position within a line reported by the LSP server.
=20
@@ -1946,6 +1968,8 @@ eglot-managed-mode-hook
   "A hook run by Eglot after it started/stopped managing a buffer.
 Use `eglot-managed-p' to determine if current buffer is managed.")
=20
+(defvar-local eglot--track-changes nil)
+
 (define-minor-mode eglot--managed-mode
   "Mode for source buffers managed by some Eglot project."
   :init-value nil :lighter nil :keymap eglot-mode-map
@@ -1959,8 +1983,13 @@ eglot--managed-mode
       ("utf-8"
        (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-li=
nepos)
        (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-=
utf-8-linepos)))
-    (add-hook 'after-change-functions #'eglot--after-change nil t)
-    (add-hook 'before-change-functions #'eglot--before-change nil t)
+    (if (fboundp 'track-changes-register)
+        (unless eglot--track-changes
+          (setq eglot--track-changes
+           (track-changes-register
+            #'eglot--track-changes-signal :disjoint t)))
+      (add-hook 'after-change-functions #'eglot--after-change nil t)
+      (add-hook 'before-change-functions #'eglot--before-change nil t))
     (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
     ;; Prepend "didClose" to the hook after the "nonoff", so it will run f=
irst
     (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil =
t)
@@ -1998,6 +2027,9 @@ eglot--managed-mode
                buffer
                (eglot--managed-buffers (eglot-current-server)))))
    (t
+    (when eglot--track-changes
+      (track-changes-unregister eglot--track-changes)
+      (setq eglot--track-changes nil))
     (remove-hook 'after-change-functions #'eglot--after-change t)
     (remove-hook 'before-change-functions #'eglot--before-change t)
     (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
@@ -2588,7 +2620,6 @@ eglot--document-changed-hook
 (defun eglot--after-change (beg end pre-change-length)
   "Hook onto `after-change-functions'.
 Records BEG, END and PRE-CHANGE-LENGTH locally."
-  (cl-incf eglot--versioned-identifier)
   (pcase (car-safe eglot--recent-changes)
     (`(,lsp-beg ,lsp-end
                 (,b-beg . ,b-beg-marker)
@@ -2616,6 +2647,29 @@ eglot--after-change
                `(,lsp-beg ,lsp-end ,pre-change-length
                           ,(buffer-substring-no-properties beg end)))))
     (_ (setf eglot--recent-changes :emacs-messup)))
+  (eglot--track-changes-signal nil))
+
+(defun eglot--track-changes-fetch (id)
+  (if (eq eglot--recent-changes :pending) (setq eglot--recent-changes nil))
+  (track-changes-fetch
+   id (lambda (beg end before)
+        (cond
+         ((eq eglot--recent-changes :emacs-messup) nil)
+         ((eq before 'error) (setf eglot--recent-changes :emacs-messup))
+         (t (push `(,(eglot--pos-to-lsp-position beg)
+                    ,(eglot--virtual-pos-to-lsp-position beg before)
+                    ,(length before)
+                    ,(buffer-substring-no-properties beg end))
+                  eglot--recent-changes))))))
+
+(defun eglot--track-changes-signal (id &optional distance)
+  (cl-incf eglot--versioned-identifier)
+  (cond
+   (distance (eglot--track-changes-fetch id))
+   (eglot--recent-changes nil)
+   ;; Note that there are pending changes, for the benefit of those
+   ;; who check it as a boolean.
+   (t (setq eglot--recent-changes :pending)))
   (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
   (let ((buf (current-buffer)))
     (setq eglot--change-idle-timer
@@ -2729,6 +2783,8 @@ eglot-handle-request
 (defun eglot--signal-textDocument/didChange ()
   "Send textDocument/didChange to server."
   (when eglot--recent-changes
+    (when eglot--track-changes
+      (eglot--track-changes-fetch eglot--track-changes))
     (let* ((server (eglot--current-server-or-lose))
            (sync-capability (eglot-server-capable :textDocumentSync))
            (sync-kind (if (numberp sync-capability) sync-capability
@@ -2750,7 +2806,7 @@ eglot--signal-textDocument/didChange
                    ;; empty entries in `eglot--before-change' calls
                    ;; without an `eglot--after-change' reciprocal.
                    ;; Weed them out here.
-                   when (numberp len)
+                   when (numberp len) ;FIXME: Not needed with `track-chang=
es'.
                    vconcat `[,(list :range `(:start ,beg :end ,end)
                                     :rangeLength len :text text)]))))
       (setq eglot--recent-changes nil)
diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el
index 66043059d14..0a618dc8f39 100644
--- a/lisp/vc/diff-mode.el
+++ b/lisp/vc/diff-mode.el
@@ -53,9 +53,10 @@
 ;; - Handle `diff -b' output in context->unified.
=20
 ;;; Code:
+(require 'easy-mmode)
+(require 'track-changes)
 (eval-when-compile (require 'cl-lib))
 (eval-when-compile (require 'subr-x))
-(require 'easy-mmode)
=20
 (autoload 'vc-find-revision "vc")
 (autoload 'vc-find-revision-no-save "vc")
@@ -1431,38 +1432,23 @@ diff-write-contents-hooks
   (if (buffer-modified-p) (diff-fixup-modifs (point-min) (point-max)))
   nil)
=20
-;; It turns out that making changes in the buffer from within an
-;; *-change-function is asking for trouble, whereas making them
-;; from a post-command-hook doesn't pose much problems
-(defvar diff-unhandled-changes nil)
-(defun diff-after-change-function (beg end _len)
-  "Remember to fixup the hunk header.
-See `after-change-functions' for the meaning of BEG, END and LEN."
-  ;; Ignoring changes when inhibit-read-only is set is strictly speaking
-  ;; incorrect, but it turns out that inhibit-read-only is normally not set
-  ;; inside editing commands, while it tends to be set when the buffer gets
-  ;; updated by an async process or by a conversion function, both of which
-  ;; would rather not be uselessly slowed down by this hook.
-  (when (and (not undo-in-progress) (not inhibit-read-only))
-    (if diff-unhandled-changes
-	(setq diff-unhandled-changes
-	      (cons (min beg (car diff-unhandled-changes))
-		    (max end (cdr diff-unhandled-changes))))
-      (setq diff-unhandled-changes (cons beg end)))))
-
-(defun diff-post-command-hook ()
-  "Fixup hunk headers if necessary."
-  (when (consp diff-unhandled-changes)
-    (ignore-errors
-      (save-excursion
-	(goto-char (car diff-unhandled-changes))
-	;; Maybe we've cut the end of the hunk before point.
-	(if (and (bolp) (not (bobp))) (backward-char 1))
-	;; We used to fixup modifs on all the changes, but it turns out that
-	;; it's safer not to do it on big changes, e.g. when yanking a big
-	;; diff, or when the user edits the header, since we might then
-	;; screw up perfectly correct values.  --Stef
-	(diff-beginning-of-hunk t)
+(defvar-local diff--track-changes nil)
+
+(defun diff--track-changes-signal (tracker)
+  (cl-assert (eq tracker diff--track-changes))
+  (track-changes-fetch tracker #'diff--track-changes-function))
+
+(defun diff--track-changes-function (beg end _before)
+  (with-demoted-errors "%S"
+    (save-excursion
+      (goto-char beg)
+      ;; Maybe we've cut the end of the hunk before point.
+      (if (and (bolp) (not (bobp))) (backward-char 1))
+      ;; We used to fixup modifs on all the changes, but it turns out that
+      ;; it's safer not to do it on big changes, e.g. when yanking a big
+      ;; diff, or when the user edits the header, since we might then
+      ;; screw up perfectly correct values.  --Stef
+      (when (ignore-errors (diff-beginning-of-hunk t))
         (let* ((style (if (looking-at "\\*\\*\\*") 'context))
                (start (line-beginning-position (if (eq style 'context) 3 2=
)))
                (mid (if (eq style 'context)
@@ -1470,17 +1456,16 @@ diff-post-command-hook
                           (re-search-forward diff-context-mid-hunk-header-=
re
                                              nil t)))))
           (when (and ;; Don't try to fixup changes in the hunk header.
-                 (>=3D (car diff-unhandled-changes) start)
+                 (>=3D beg start)
                  ;; Don't try to fixup changes in the mid-hunk header eith=
er.
                  (or (not mid)
-                     (< (cdr diff-unhandled-changes) (match-beginning 0))
-                     (> (car diff-unhandled-changes) (match-end 0)))
+                     (< end (match-beginning 0))
+                     (> beg (match-end 0)))
                  (save-excursion
-		(diff-end-of-hunk nil 'donttrustheader)
+		   (diff-end-of-hunk nil 'donttrustheader)
                    ;; Don't try to fixup changes past the end of the hunk.
-                   (>=3D (point) (cdr diff-unhandled-changes))))
-	  (diff-fixup-modifs (point) (cdr diff-unhandled-changes)))))
-      (setq diff-unhandled-changes nil))))
+                   (>=3D (point) end)))
+	    (diff-fixup-modifs (point) end)))))))
=20
 (defun diff-next-error (arg reset)
   ;; Select a window that displays the current buffer so that point
@@ -1560,9 +1545,8 @@ diff-mode
   ;; setup change hooks
   (if (not diff-update-on-the-fly)
       (add-hook 'write-contents-functions #'diff-write-contents-hooks nil =
t)
-    (make-local-variable 'diff-unhandled-changes)
-    (add-hook 'after-change-functions #'diff-after-change-function nil t)
-    (add-hook 'post-command-hook #'diff-post-command-hook nil t))
+    (setq diff--track-changes
+          (track-changes-register #'diff--track-changes-signal :nobefore t=
)))
=20
   ;; add-log support
   (setq-local add-log-current-defun-function #'diff-current-defun)
@@ -1581,12 +1565,15 @@ diff-minor-mode
 \\{diff-minor-mode-map}"
   :group 'diff-mode :lighter " Diff"
   ;; FIXME: setup font-lock
-  ;; setup change hooks
-  (if (not diff-update-on-the-fly)
-      (add-hook 'write-contents-functions #'diff-write-contents-hooks nil =
t)
-    (make-local-variable 'diff-unhandled-changes)
-    (add-hook 'after-change-functions #'diff-after-change-function nil t)
-    (add-hook 'post-command-hook #'diff-post-command-hook nil t)))
+  (when diff--track-changes (track-changes-unregister diff--track-changes))
+  (remove-hook 'write-contents-functions #'diff-write-contents-hooks t)
+  (when diff-minor-mode
+    (if (not diff-update-on-the-fly)
+        (add-hook 'write-contents-functions #'diff-write-contents-hooks ni=
l t)
+      (unless diff--track-changes
+        (setq diff--track-changes
+              (track-changes-register #'diff--track-changes-signal
+                                      :nobefore t))))))
=20
 ;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;=
;;;
=20
diff --git a/test/lisp/emacs-lisp/track-changes-tests.el b/test/lisp/emacs-=
lisp/track-changes-tests.el
new file mode 100644
index 00000000000..eab9197030f
--- /dev/null
+++ b/test/lisp/emacs-lisp/track-changes-tests.el
@@ -0,0 +1,156 @@
+;;; track-changes-tests.el --- tests for emacs-lisp/track-changes.el  -*- =
lexical-binding:t -*-
+
+;; Copyright (C) 2024  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'track-changes)
+(require 'cl-lib)
+(require 'ert)
+
+(defun track-changes-tests--random-word ()
+  (let ((chars ()))
+    (dotimes (_ (1+ (random 12)))
+      (push (+ ?A (random (1+ (- ?z ?A)))) chars))
+    (apply #'string chars)))
+
+(defvar track-changes-tests--random-verbose nil)
+
+(defun track-changes-tests--message (&rest args)
+  (when track-changes-tests--random-verbose (apply #'message args)))
+
+(defvar track-changes-tests--random-seed
+  (let ((seed (number-to-string (random (expt 2 24)))))
+    (message "Random seed =3D %S" seed)
+    seed))
+
+(ert-deftest track-changes-tests--random ()
+  ;; Keep 2 buffers in sync with a third one as we make random
+  ;; changes to that 3rd one.
+  ;; We have 3 trackers: a "normal" one which we sync
+  ;; at random intervals, one which syncs via the "disjoint" signal,
+  ;; plus a third one which verifies that "nobefore" gets
+  ;; information consistent with the "normal" tracker.
+  (with-temp-buffer
+    (dotimes (_ 100)
+      (insert (track-changes-tests--random-word) "\n"))
+    (let* ((buf1 (generate-new-buffer " *tc1*"))
+           (buf2 (generate-new-buffer " *tc2*"))
+           (char-counts (make-vector 2 0))
+           (sync-counts (make-vector 2 0))
+           (print-escape-newlines t)
+           (file (make-temp-file "tc"))
+           (id1 (track-changes-register #'ignore))
+           (id3 (track-changes-register #'ignore :nobefore t))
+           (sync
+            (lambda (id buf n)
+              (track-changes-tests--message "!! SYNC %d !!" n)
+              (track-changes-fetch
+               id (lambda (beg end before)
+                    (when (eq n 1)
+                      (track-changes-fetch
+                       id3 (lambda (beg3 end3 before3)
+                             (should (eq beg3 beg))
+                             (should (eq end3 end))
+                             (should (eq before3
+                                         (if (symbolp before)
+                                             before (length before)))))))
+                    (cl-incf (aref sync-counts (1- n)))
+                    (cl-incf (aref char-counts (1- n)) (- end beg))
+                    (let ((after (buffer-substring beg end)))
+                      (track-changes-tests--message
+                       "Sync:\n    %S\n=3D>  %S\nat %d .. %d"
+                       before after beg end)
+                      (with-current-buffer buf
+                        (if (eq before 'error)
+                            (erase-buffer)
+                          (should (equal before
+                                         (buffer-substring
+                                          beg (+ beg (length before)))))
+                          (delete-region beg (+ beg (length before))))
+                        (goto-char beg)
+                        (insert after)))
+                    (should (equal (buffer-string)
+                                   (with-current-buffer buf
+                                     (buffer-string))))))))
+           (id2 (track-changes-register
+                 (lambda (id2 &optional distance)
+                   (when distance
+                     (track-changes-tests--message "Disjoint distance: %d"
+                                                   distance)
+                     (funcall sync id2 buf2 2)))
+                 :disjoint t)))
+      (write-region (point-min) (point-max) file)
+      (insert-into-buffer buf1)
+      (insert-into-buffer buf2)
+      (should (equal (buffer-hash) (buffer-hash buf1)))
+      (should (equal (buffer-hash) (buffer-hash buf2)))
+      (message "seeding with: %S" track-changes-tests--random-seed)
+      (random track-changes-tests--random-seed)
+      (dotimes (_ 1000)
+        (pcase (random 15)
+          (0
+           (track-changes-tests--message "Manual sync1")
+           (funcall sync id1 buf1 1))
+          (1
+           (track-changes-tests--message "Manual sync2")
+           (funcall sync id2 buf2 2))
+          ((pred (< _ 5))
+           (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
+                  (end (min (+ beg (1+ (random 100))) (point-max))))
+             (track-changes-tests--message "Fill %d .. %d" beg end)
+             (fill-region-as-paragraph beg end)))
+          ((pred (< _ 8))
+           (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
+                  (end (min (+ beg (1+ (random 12))) (point-max))))
+             (track-changes-tests--message "Delete %S at %d .. %d"
+                                           (buffer-substring beg end) beg =
end)
+             (delete-region beg end)))
+          ((and 8 (guard (=3D (random 50) 0)))
+           (track-changes-tests--message "Silent insertion")
+           (let ((inhibit-modification-hooks t))
+             (insert "a")))
+          ((and 8 (guard (=3D (random 10) 0)))
+           (track-changes-tests--message "Revert")
+           (insert-file-contents file nil nil nil 'replace))
+          ((and 8 (guard (=3D (random 3) 0)))
+           (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
+                  (end (min (+ beg (1+ (random 12))) (point-max)))
+                  (after (eq (random 2) 0)))
+             (track-changes-tests--message "Bogus %S %d .. %d"
+                                           (if after 'after 'before) beg e=
nd)
+             (if after
+                 (run-hook-with-args 'after-change-functions
+                                     beg end (- end beg))
+               (run-hook-with-args 'before-change-functions beg end))))
+          (_
+           (goto-char (+ (point-min) (random (1+ (buffer-size)))))
+           (let ((word (track-changes-tests--random-word)))
+             (track-changes-tests--message "insert %S at %d" word (point))
+             (insert  word "\n")))))
+      (message "SCOREs: default: %d/%d=3D%d     disjoint: %d/%d=3D%d"
+               (aref char-counts 0) (aref sync-counts 0)
+               (/ (aref char-counts 0) (aref sync-counts 0))
+               (aref char-counts 1) (aref sync-counts 1)
+               (/ (aref char-counts 1) (aref sync-counts 1))))))
+
+
+
+;;; track-changes-tests.el ends here
--=20
2.43.0


--=-=-=--





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 18:37:06 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 14:37:06 2024
Received: from localhost ([127.0.0.1]:47616 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rttrd-0004cC-70
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 14:37:06 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:44470)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rttrX-0004au-UA
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 14:37:03 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rttrI-0003Fp-Jy; Mon, 08 Apr 2024 14:36:44 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-version:References:Subject:In-Reply-To:To:From:
 Date; bh=F1Wuv5edgL3s0imPNyBoYbOmI1JZHfkG494N5pHsBb8=; b=WK5YhrUU/vmzCnEL7X03
 szsTn0d2kSOSN8gQInEyw5YgXbgThxZ3wgAQ8eS+Q2ATgPbDSqAhtA+5UlkmW3ZR8oLwoeb6NTF+B
 /FZug3I2QaqDR8FL3lup0klf8Crym+PGKGRL0dfB+qHThGDN9BbcOWQoocxpXk98y8unsTfMUx3Jv
 OelMbwrSpNajX4A1MJWQxhDBaoqcj6lKnKDk0jJk7othXy9/URdCOs6wW5CLLtSveHH3Cqv3gkKmd
 ejfRsGh2MqO4IPT8E72NoR0SX/VOToMcm9pSwBkcqGkjfI7QxpWdJeWJyeS5vRYhjDzze9IjU0sc3
 +vTJslWEu+7zXw==;
Date: Mon, 08 Apr 2024 21:36:41 +0300
Message-Id: <86il0rya4m.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwvcyqzdcmx.fsf-monnier+emacs@HIDDEN> (message from Stefan
 Monnier on Mon, 08 Apr 2024 13:17:37 -0400)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN> <86msq3yhot.fsf@HIDDEN> 
 <jwvcyqzdcmx.fsf-monnier+emacs@HIDDEN>
MIME-version: 1.0
Content-type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

> From: Stefan Monnier <monnier@HIDDEN>
> Cc: 70077 <at> debbugs.gnu.org,  acm@HIDDEN,  yantar92@HIDDEN
> Date: Mon, 08 Apr 2024 13:17:37 -0400
> 
> >             Name    Type    Default
> >             ————    ————    ———————
> >             beg     t       (point-max)
> >             end     t       (point-min)
> >             before  t       nil
> >             next    t       nil
> >
> >   BEG..END is the area that was changed and BEFORE is its previous
> >   content[...]
> 
> OK, I'll switch the two, thanks.
> 
> > (Btw, those "t" under "Type" are also somewhat mysterious.  What do
> > they signify?)
> 
> `C-h o t RET` says:
> 
>     t’s value is t
>     
>     Not documented as a variable.
>     
>       Probably introduced at or before Emacs version 16.
>     
>     
>     
>     t is also a type.
>     
>     t is a type (of kind ‘built-in-class’).
>      Children ‘sequence’, ‘atom’.
>     
>     Abstract supertype of everything.
>     
>     This is a built-in type.

If this indicates that the slots are of built-in-class type, why do we
show the cryptic t there?

> >> >> +By default SIGNAL is called as soon as convenient after a change, which is
> >> >                                ^^^^^^^^^^^^^^^^^^^^^
> >> > "as soon as it's convenient", I presume?
> >> Other than the extra " it's", what is the difference?
> > Nothing.  I indeed thing "it's" is missing there.
> 
> My local native-English representative says that "both work fine"
> (somewhat dismissively, I must add).

In that case I'll yield, but do note that it got me stumbled.

> > In general, when I want to create a clean slate, I don't care too much
> > about the dirt I remove.  Why is it important to signal errors because
> > a state I am dumping had some errors?
> 
> I don't understand why you think it will signal an error?

Doesn't cl-assert signal an error if the condition is false?

> More to the point, it should signal an error only if I made a mistake in
> `track-changes.el` or if you messed with the internals.

I have the latter possibility in mind, yes.  Why catch me doing that
when I'm cleaning up my mess, _after_ all the damage, such as it is,
was already done?

> >> >> +;;;; Extra candidates for the API.
> >> >> +;; This could be a good alternative to using a temp-buffer like I used in
> >> >                                                                    ^^^^^^
> >> > "I"?
> >> Yes, that refers to the code I wrote.
> > We don't usually leave such style in long-term comments and
> > documentation.
> 
> `grep " I " lisp/**/*.el` suggests otherwise.

"A journey of a thousand miles begins with one first step."




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 17:28:18 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 13:28:18 2024
Received: from localhost ([127.0.0.1]:47561 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtsn3-0007Jg-NJ
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 13:28:18 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:56008)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <acorallo@HIDDEN>) id 1rtsmu-0007HC-TU
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 13:28:11 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <acorallo@HIDDEN>)
 id 1rtsmg-0008UV-5S; Mon, 08 Apr 2024 13:27:54 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-Version:Date:References:In-Reply-To:Subject:To:
 From; bh=XWmI97MSED0EBPvzrrhjZYNH+Tl9StLVLhe1LOLXVjA=; b=AY1DQQWO5XTXM3DEbfFP
 HZuWXkvvtWa1fOIsiegmem2cG1IEU2K+oLUzw5bAUYlcRxwkTI6hmsErdIwmgQpfSS2WPuqHc95ni
 iOiAvRgYTnw//KHoHdS0xPag23wOEsIaNjJRZkwH7N3YfVBnZT71UF7S6wbVvaP+HDfi12/KLUX2x
 6PyxKk4wIfddfGUZcubGXZ8121l0I8AeLD9Y8AiqnLg3XLR9SBd/74sZB1SicBIoU1tN+dbOeqTpu
 nmro+xy8dAvVORL7dodKtUVho7hoWsmwu1gKSQU8XCAXGZrx/ExDJzOD8yyRDkgH2IJv8X2vqqEjh
 IBl167CifOmTYQ==;
Received: from acorallo by fencepost.gnu.org with local (Exim 4.90_1)
 (envelope-from <acorallo@HIDDEN>)
 id 1rtsmZ-0007Rb-NL; Mon, 08 Apr 2024 13:27:49 -0400
From: Andrea Corallo <acorallo@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvcyqzdcmx.fsf-monnier+emacs@HIDDEN> (Stefan Monnier via's
 message of "Mon, 08 Apr 2024 13:17:37 -0400")
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN> <86msq3yhot.fsf@HIDDEN>
 <jwvcyqzdcmx.fsf-monnier+emacs@HIDDEN>
Date: Mon, 08 Apr 2024 13:27:47 -0400
Message-ID: <yp1o7ajkbn0.fsf@HIDDEN>
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, Stefan Monnier <monnier@HIDDEN>,
 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army knife of
text editors" <bug-gnu-emacs@HIDDEN> writes:

>>> Maybe `describe-type` should lists the slots first and the docstring
>>> underneath rather than other way around?
>>
>> That'd also be good.  Then the doc string should say something like
>>
>>   Object holding a description of a buffer state.
>>   It has the following Allocated Slots:
>>
>>             Name    Type    Default
>>             =E2=80=94=E2=80=94=E2=80=94=E2=80=94    =E2=80=94=E2=80=94=
=E2=80=94=E2=80=94    =E2=80=94=E2=80=94=E2=80=94=E2=80=94=E2=80=94=E2=80=
=94=E2=80=94
>>             beg     t       (point-max)
>>             end     t       (point-min)
>>             before  t       nil
>>             next    t       nil
>>
>>   BEG..END is the area that was changed and BEFORE is its previous
>>   content[...]
>
> OK, I'll switch the two, thanks.
>
>> (Btw, those "t" under "Type" are also somewhat mysterious.  What do
>> they signify?)
>
> `C-h o t RET` says:
>
>     t=E2=80=99s value is t
>=20=20=20=20=20
>     Not documented as a variable.
>=20=20=20=20=20
>       Probably introduced at or before Emacs version 16.
>=20=20=20=20=20
>=20=20=20=20=20
>=20=20=20=20=20
>     t is also a type.
>=20=20=20=20=20
>     t is a type (of kind =E2=80=98built-in-class=E2=80=99).
>      Children =E2=80=98sequence=E2=80=99, =E2=80=98atom=E2=80=99.
>=20=20=20=20=20
>     Abstract supertype of everything.
>=20=20=20=20=20
>     This is a built-in type.
>=20=20=20=20=20
>     [back]
>
> We could put buttons in the "Type" column, but I'm not sure it'd be
> better or worse (I'm worried about turning everything into a button).

FWIW I'd vote for buttonify every type in our *Help* buffer.

  Andrea




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 17:17:59 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 13:17:59 2024
Received: from localhost ([127.0.0.1]:47538 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtsd5-0006Z5-65
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 13:17:59 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:36878)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtsd1-0006Yj-1e
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 13:17:58 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 386E21000DD;
 Mon,  8 Apr 2024 13:17:41 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712596660;
 bh=xGWypuhh6SbD6ZvI4IwvfhQ+fbQU9hlauG6lKdXLz9w=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=QR42H4bZV1p907abQXrasSZiQVEOFjoDJqQvqlxI1vzy8HtiRl/attQqsmIv2PHsa
 usM13qPvXi57s7Ct0JhcqB3mmAD2ZBuV90B+1V6H2HlxvrQY5iGWppyPjVIM+lcbDG
 eKN74zq/5dYqbpzZs9NO1rbC2U/lYXVuhz89mVecpS71arZk8nmT97qRCQ8N2338VK
 Gqde0pU3AgJDx0BNsb2ScbTuiJUDaaWsZlqQXWSc2WXm6sRlgwonfDFnntsNHAitXH
 XhMdBNHf4+rrJeBTyCsMKr1aWo/mHXU7OZVWvd/8BsETV5lMv5TV37KyrFwfdaGmoO
 CdloiK11Cuvlg==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 3A25C100048;
 Mon,  8 Apr 2024 13:17:40 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 0F79B1202A2;
 Mon,  8 Apr 2024 13:17:40 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86msq3yhot.fsf@HIDDEN> (Eli Zaretskii's message of "Mon, 08 Apr
 2024 18:53:22 +0300")
Message-ID: <jwvcyqzdcmx.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN> <86msq3yhot.fsf@HIDDEN>
Date: Mon, 08 Apr 2024 13:17:37 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.017 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

>> Maybe `describe-type` should lists the slots first and the docstring
>> underneath rather than other way around?
>
> That'd also be good.  Then the doc string should say something like
>
>   Object holding a description of a buffer state.
>   It has the following Allocated Slots:
>
>             Name    Type    Default
>             =E2=80=94=E2=80=94=E2=80=94=E2=80=94    =E2=80=94=E2=80=94=E2=
=80=94=E2=80=94    =E2=80=94=E2=80=94=E2=80=94=E2=80=94=E2=80=94=E2=80=94=
=E2=80=94
>             beg     t       (point-max)
>             end     t       (point-min)
>             before  t       nil
>             next    t       nil
>
>   BEG..END is the area that was changed and BEFORE is its previous
>   content[...]

OK, I'll switch the two, thanks.

> (Btw, those "t" under "Type" are also somewhat mysterious.  What do
> they signify?)

`C-h o t RET` says:

    t=E2=80=99s value is t
=20=20=20=20
    Not documented as a variable.
=20=20=20=20
      Probably introduced at or before Emacs version 16.
=20=20=20=20
=20=20=20=20
=20=20=20=20
    t is also a type.
=20=20=20=20
    t is a type (of kind =E2=80=98built-in-class=E2=80=99).
     Children =E2=80=98sequence=E2=80=99, =E2=80=98atom=E2=80=99.
=20=20=20=20
    Abstract supertype of everything.
=20=20=20=20
    This is a built-in type.
=20=20=20=20
    [back]

We could put buttons in the "Type" column, but I'm not sure it'd be
better or worse (I'm worried about turning everything into a button).

>> >> +(cl-defun track-changes-register ( signal &key nobefore disjoint imm=
ediate)
>> >> +  "Register a new tracker and return a new tracker ID.
>> > Please mention SIGNAL in the first line of the doc string.
>> Hmm... having trouble making that fit on a single line.
>     Register a new tracker whose change-tracking function is SIGNAL.
>   Return the ID of the new tracker.

Thanks.

>> >> +By default SIGNAL is called as soon as convenient after a change, wh=
ich is
>> >                                ^^^^^^^^^^^^^^^^^^^^^
>> > "as soon as it's convenient", I presume?
>> Other than the extra " it's", what is the difference?
> Nothing.  I indeed thing "it's" is missing there.

My local native-English representative says that "both work fine"
(somewhat dismissively, I must add).

> My point is that by referencing funcall-later you can avoid the need
> to explain what is already explained in that function's doc string.

OK.

>> >> +In order to prevent the upcoming change from being combined with the=
 previous
>> >> +changes, SIGNAL needs to call `track-changes-fetch' before it return=
s."
>> >
>> > This seems to contradict what the doc string says previously: that
>> > SIGNAL should NOT call track-changes-fetch.
>>=20
>> I don't kow where you see the docstring saying that.
>> The closest I can find is:
>>=20
>>     When IMMEDIATE is non-nil, the SIGNAL should preferably not always c=
all
>>     `track-changes-fetch', since that would defeat the purpose of this l=
ibrary.
>>=20
>> Note the "When IMMEDIATE is non-nil", "preferably", and "not always",
>> and the fact that the reason is not that something will break but that
>> some other solution would probably work better.
>
> Then maybe the sentence on which I commented should say
>
>   Except when IMMEDIATE is non-nil, if SIGNAL needs to prevent the
>   upcoming change from being combined with the previous ones, it
>   should call `track-changes-fetch' before it returns.

But that wouldn't say what I want to say.  It'd want to call
`track-changes-fetch' before it returns even if IMMEDIATE is non-nil.
It just probably wouldn't want to do that for all calls to the signal.
E.g. only for those calls where the second argument is non-nil
(i.e. the disjointness signals).

> In general, when I want to create a clean slate, I don't care too much
> about the dirt I remove.  Why is it important to signal errors because
> a state I am dumping had some errors?

I don't understand why you think it will signal an error?
More to the point, it should signal an error only if I made a mistake in
`track-changes.el` or if you messed with the internals.
Note also that this function is itself internal.

>> >> +;;;; Extra candidates for the API.
>> >> +;; This could be a good alternative to using a temp-buffer like I us=
ed in
>> >                                                                    ^^^=
^^^
>> > "I"?
>> Yes, that refers to the code I wrote.
> We don't usually leave such style in long-term comments and
> documentation.

`grep " I " lisp/**/*.el` suggests otherwise.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 16:06:34 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 12:06:34 2024
Received: from localhost ([127.0.0.1]:47481 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtrVw-00011u-Ur
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 12:06:34 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:18304)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtrVs-00010N-U2
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 12:06:31 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 6848B1000DD;
 Mon,  8 Apr 2024 12:06:15 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712592374;
 bh=XrZIALznMd4ptZF6RpaujK5ZqFprQP2rWB3aztRKi80=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=kC0NMLAMeYZGhTChUwFcp5x9Ibeo4+mxGxFvYBVQ5ElLIbzdxYNDMpIiCDvceaC2H
 M51ZAIsniYD4DOgFyYErIva77A/R2jxmYViURtQ3F9pvlj5JZ1pa8/FsJK7wkemej3
 wqnOFjCd0ICN2epBhOVNCYHvajkhsGObOhBL7AyjP+7oYnAF08CMcunUXw2GrJcUMk
 FXcHmkLJ/umCcEeyic5+7qBT7kmj/aeeGNPyMSedENmv4jy3nokspfFmtNJHwB+D4c
 ZZYPF+vDbSGhGjr2dkQQPzsL3k8mPjvQik0hwB71I0UoBsqx4nkh73czRirjDkxTrm
 94aa1OM0GTwnQ==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 36431100048;
 Mon,  8 Apr 2024 12:06:14 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 0FD1D120495;
 Mon,  8 Apr 2024 12:06:14 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <87edbhnu53.fsf@localhost> (Ihor Radchenko's message of "Sun, 07
 Apr 2024 14:07:36 +0000")
Message-ID: <jwvjzl7dghm.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <87edbhnu53.fsf@localhost>
Date: Mon, 08 Apr 2024 12:06:12 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.018 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

>> +  "Object holding a description of a buffer state.
>> +BEG..END is the area that was changed and BEFORE is its previous conten=
t.
>> +If the current buffer currently holds the content of the next state, yo=
u can get
>> +the contents of the previous state with:
>> +
>> +    (concat (buffer-substring (point-min) beg)
>> +                before
>> +                (buffer-substring end (point-max)))
>> +
>> +NEXT is the next state object (i.e. a more recent state).
>> +If NEXT is nil it means it's most recent state and it may be incomplete
>> +\(BEG/END/BEFORE may be nil), in which case those fields will take their
>> +values from `track-changes--before-(beg|end|before)' when the next
>> +state is create."
>
> This docstring is a bit confusing.
> If a state object is not the most recent, how come=20
>
>> +    (concat (buffer-substring (point-min) beg)
>> +                before
>> +                (buffer-substring end (point-max)))
>
> produces the previous content?

Because it says "If the current buffer currently holds the content of
the next state".
[ "current ... currently" wasn't my best moment, admittedly.  ]

> And if the state object is the most recent, "it may be incomplete"...
> So, when is it safe to use the above (concat ... ) call?

You never want to use this call, it's only there to show in a concise
manner how BEG/END/BEFORE relate and what information they're supposed
to hold.

>> +(defvar-local track-changes--before-beg (point-min)
>> +  "Beginning position of the remembered \"before string\".")
>> +(defvar-local track-changes--before-end (point-min)
>> +  "End position of the text replacing the \"before string\".")
> Why (point-min)? It will make the values dependent on the buffer
> narrowing that happens to be active when the library if first loaded.
> Which cannot be right.

The precise value should hopefully never matter, barring bugs.
I changed them to 0.

>> +(defvar-local track-changes--buffer-size nil
>> +  "Current size of the buffer, as far as this library knows.
>> +This is used to try and detect cases where buffer modifications are \"l=
ost\".")
> Just looking at the buffer size may miss unregistered edits that do not
> change the total size of the buffer.  Although I do not know a better
> measure.  `buffer-chars-modified-tic' may lead to false-positives
> (Bug#51766).

Yup, hence the "try to".

>> +(cl-defun track-changes-register ( signal &key nobefore disjoint immedi=
ate)
>> +  "Register a new tracker and return a new tracker ID.
>> +SIGNAL is a function that will be called with one argument (the tracker=
 ID)
>> +after the current buffer is modified, so that we can react to the chang=
e.
>> + ...
>> +If optional argument DISJOINT is non-nil, SIGNAL is called every time w=
e are
>> +about to combine changes from \"distant\" parts of the buffer.
>> +This is needed when combining disjoint changes into one bigger change
>> +is unacceptable, typically for performance reasons.
>> +These calls are distinguished from normal calls by calling SIGNAL with
>> +a second argument which is the distance between the upcoming change and
>> +the previous changes.
>
> This is a bit confusing. The first paragraph says that SIGNAL is called
> with a single argument, but that it appears that two arguments may be
> passed. I'd rather tell the calling convention early in the docstring.

The second convention is used only when DISJOINT is non-nil, which is
why it's described where we document the effect of DISJOINT.
An alternative could be to make the `disjoint` arg hold the function to call
to signal disjoint changes.

>> +  (unless nobefore
>> +    (setq track-changes--before-no nil)
>> +    (add-hook 'before-change-functions #'track-changes--before nil t))
>> +  (add-hook 'after-change-functions  #'track-changes--after  nil t)
> Note that all the changes made in indirect buffers will be missed.
> See bug#60333.

Yup.  And misuses of `inhibit-modification-hooks` or
`with-silent-modifications`.  =F0=9F=99=81

>> +(defun track-changes-fetch (id func)
>> ...
>> +  (unless (equal track-changes--buffer-size (buffer-size))
>> +    (track-changes--recover-from-error))
>> +  (let ((beg nil)
>> +        (end nil)
>> +        (before t)
>> +        (lenbefore 0)
>> +        (states ()))
>> +    ;; Transfer the data from `track-changes--before-string'
>> +    ;; to the tracker's state object, if needed.
>> +    (track-changes--clean-state)
>
>> +(defun track-changes--recover-from-error ()
>> ...
>> +  (setq track-changes--state (track-changes--state)))
>
> This will create a dummy state with
>
>  (beg (point-max))
>  (end (point-min))
>
> such state will not pass (< beg end) assertion in
> `track-changes--clean-state' called in `track-changes-fetch' immediately
> after `track-changes--recover-from-error'

I can't reproduce that.  Do you have a recipe?

AFAICT all the (< beg end) tests in `track-changes--clean-state` are
conditional on `track-changes--before-clean` being nil, whereas
`track-changes--recover-from-error` sets that var to `unset`.

>> +(defun track-changes--in-revert (beg end before func)
>> ...
>> +      (atomic-change-group
>> +        (goto-char end)
>> +        (insert-before-markers before)
>> +        (delete-region beg end)
>
> What happens if there are markers inside beg...end?

During FUNC they're moved to BEG or END, and when we restore the
original state, well... the undo machinery has some support to restore
the markers where they were, but it's definitely not 100%.  =F0=9F=99=81

>> +(defun track-changes-tests--random-word ()
>> +  (let ((chars ()))
>> +    (dotimes (_ (1+ (random 12)))
>> +      (push (+ ?A (random (1+ (- ?z ?A)))) chars))
>> +    (apply #'string chars)))
>
> If you are using random values for edits, how can such test be
> reproduced?

Luck?

> Maybe first generate a random seed and then log it, so that the
> failing test can be repeated if necessary with seed assigned manually.

Good idea.
But my attempt (see patch below) failed.
I'm not sure what I'm doing wrong, but

    make test/lisp/emacs-lisp/track-changes-tests \
         EMACS_EXTRAOPT=3D"--eval '(setq track-changes-tests--random-seed \=
"15216888\")'"

gives a different score each time.  =F0=9F=99=81


        Stefan


diff --git a/test/lisp/emacs-lisp/track-changes-tests.el b/test/lisp/emacs-=
lisp/track-changes-tests.el
index cdccbe80299..eab9197030f 100644
--- a/test/lisp/emacs-lisp/track-changes-tests.el
+++ b/test/lisp/emacs-lisp/track-changes-tests.el
@@ -36,6 +36,11 @@ track-changes-tests--random-verbose
 (defun track-changes-tests--message (&rest args)
   (when track-changes-tests--random-verbose (apply #'message args)))
=20
+(defvar track-changes-tests--random-seed
+  (let ((seed (number-to-string (random (expt 2 24)))))
+    (message "Random seed =3D %S" seed)
+    seed))
+
 (ert-deftest track-changes-tests--random ()
   ;; Keep 2 buffers in sync with a third one as we make random
   ;; changes to that 3rd one.
@@ -97,6 +102,8 @@ track-changes-tests--random
       (insert-into-buffer buf2)
       (should (equal (buffer-hash) (buffer-hash buf1)))
       (should (equal (buffer-hash) (buffer-hash buf2)))
+      (message "seeding with: %S" track-changes-tests--random-seed)
+      (random track-changes-tests--random-seed)
       (dotimes (_ 1000)
         (pcase (random 15)
           (0





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 15:53:46 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 11:53:46 2024
Received: from localhost ([127.0.0.1]:47451 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtrJW-0008Pz-Hp
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 11:53:45 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:37480)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rtrJU-0008Pb-FA
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 11:53:41 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rtrJF-0001ie-VX; Mon, 08 Apr 2024 11:53:26 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-version:References:Subject:In-Reply-To:To:From:
 Date; bh=soUjVnmbpiQunzkAFi7eNvXJ9KyFxxLKVFXhWvos4U8=; b=V/9wpSM1SMevPJPHtKWz
 6bG2ElQjvGrij4pmQR0/QqYRuwp1P0XcIY6PdEY+3EhfQPYhgSS8lvGkduRWh15gHmu3YYbx4ymb1
 WfQVIUN/1sgtgGRdS3eYN85Cb9pwgzmgITFnSDlw4b22fJLKa9C95Gq/TXgYJP1pfHV4GXH2tVQj/
 lIp56ZygpliubZy332psj3ieeiYsq+U0G5mCPGC186PM9o2CdGKNgq9yN/I/AqCrwoD624ZQtOkPy
 FAV38qN08nwbBGrTugyiNCIxxoxYOKImqcqkZ2nVFs23bmxP7UwRsUEsEKx5Oa0OEZJFQmQIHlNBw
 gLJwrILTXhnIyQ==;
Date: Mon, 08 Apr 2024 18:53:22 +0300
Message-Id: <86msq3yhot.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwvplv0c662.fsf-monnier+emacs@HIDDEN> (message from Stefan
 Monnier on Mon, 08 Apr 2024 11:24:38 -0400)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
 <jwvplv0c662.fsf-monnier+emacs@HIDDEN>
MIME-version: 1.0
Content-type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

> From: Stefan Monnier <monnier@HIDDEN>
> Cc: 70077 <at> debbugs.gnu.org,  acm@HIDDEN,  yantar92@HIDDEN
> Date: Mon, 08 Apr 2024 11:24:38 -0400
> 
> >> +(cl-defstruct (track-changes--state
> >> +               (:noinline t)
> >> +               (:constructor nil)
> >> +               (:constructor track-changes--state ()))
> >> +  "Object holding a description of a buffer state.
> >> +BEG..END is the area that was changed and BEFORE is its previous content.
> >> +If the current buffer currently holds the content of the next state, you can get
> >> +the contents of the previous state with:
> >> +
> >> +    (concat (buffer-substring (point-min) beg)
> >> +                before
> >> +                (buffer-substring end (point-max)))
> >> +
> >> +NEXT is the next state object (i.e. a more recent state).
> >> +If NEXT is nil it means it's most recent state and it may be incomplete
> >> +\(BEG/END/BEFORE may be nil), in which case those fields will take their
> >> +values from `track-changes--before-(beg|end|before)' when the next
> >> +state is create."
> >
> > This doc string should IMO include the form of the object, because
> > without that BEG, END, BEFORE, and NEXT are "out of the blue", and
> > it's entirely unclear what they allude to.
> 
> AFAIK this docstring is displayed only by `describe-type` which shows
> something like:
> 
>     track-changes--state is a type (of kind ‘cl-structure-class’) in ‘track-changes.el’.
>      Inherits from ‘cl-structure-object’.
>     
>     Object holding a description of a buffer state.
>     BEG..END is the area that was changed and BEFORE is its previous content.
>     If the current buffer currently holds the content of the next state, you can get
>     the contents of the previous state with:
>     
>         (concat (buffer-substring (point-min) beg)
>                     before
>                     (buffer-substring end (point-max)))
>     
>     NEXT is the next state object (i.e. a more recent state).
>     If NEXT is nil it means it's most recent state and it may be incomplete
>     (BEG/END/BEFORE may be nil), in which case those fields will take their
>     values from `track-changes--before-(beg|end|before)' when the next
>     state is create.
>     
>     Instance Allocated Slots:
>     
>             Name    Type    Default
>             ————    ————    ———————
>             beg     t       (point-max)
>             end     t       (point-min)
>             before  t       nil
>             next    t       nil
>     
>     Specialized Methods:
>     
>     [...]
> 
> so the "form of the object" is included.

It's included, but _after_ explaining what each member of the object
form means.  That's bad from the methodological POV: we should first
show the form and only afterwards describe each of its members.

> Maybe `describe-type` should lists the slots first and the docstring
> underneath rather than other way around?

That'd also be good.  Then the doc string should say something like

  Object holding a description of a buffer state.
  It has the following Allocated Slots:

            Name    Type    Default
            ————    ————    ———————
            beg     t       (point-max)
            end     t       (point-min)
            before  t       nil
            next    t       nil

  BEG..END is the area that was changed and BEFORE is its previous
  content[...]

(Btw, those "t" under "Type" are also somewhat mysterious.  What do
they signify?)

> >> +(cl-defun track-changes-register ( signal &key nobefore disjoint immediate)
> >> +  "Register a new tracker and return a new tracker ID.
> > Please mention SIGNAL in the first line of the doc string.
> 
> Hmm... having trouble making that fit on a single line.

    Register a new tracker whose change-tracking function is SIGNAL.
  Return the ID of the new tracker.

> Could you clarify why you think SIGNAL needs to be on the first line?

It's our convention to mention the mandatory arguments on the first
line of the doc string.

> The best I could come up with so far is:
> 
>     Register a new change tracker handled via SIGNAL.

That's a good start, IMO, except that SIGNAL doesn't hadle the
tracker, it handles changes, right?

> >> +By default SIGNAL is called as soon as convenient after a change, which is
> >                                ^^^^^^^^^^^^^^^^^^^^^
> > "as soon as it's convenient", I presume?
> 
> Other than the extra " it's", what is the difference?

Nothing.  I indeed thing "it's" is missing there.

> >> +usually right after the end of the current command.
> > This should explicitly reference funcall-later, so that users could
> > understand what "as soon as convenient" means.
> 
> I don't see the point of telling which function we use: the programmers
> who want to know that, can consult the code.  As for explaining what "as
> soon as convenient" means, that's what "usually right after the end of
> the current command" is there for.

My point is that by referencing funcall-later you can avoid the need
to explain what is already explained in that function's doc string.
You could, for example, simply say

  By default, SIGNAL is arranged to be called later by using
  `funcall-later'.

> [ Also, I've changed the code to use `run-with-timer` in the mean time.  ]

That'd need a trivial change above, and I still think it's worthwhile,
as it makes this doc string easier to grasp: if one already knows how
run-with-timer works, they don't need anything else to be said; and if
they don't, they can later read on that separately.

IOW, you separate one complex description into two simpler ones: a win
IME.

> >> +In order to prevent the upcoming change from being combined with the previous
> >> +changes, SIGNAL needs to call `track-changes-fetch' before it returns."
> >
> > This seems to contradict what the doc string says previously: that
> > SIGNAL should NOT call track-changes-fetch.
> 
> I don't kow where you see the docstring saying that.
> The closest I can find is:
> 
>     When IMMEDIATE is non-nil, the SIGNAL should preferably not always call
>     `track-changes-fetch', since that would defeat the purpose of this library.
> 
> Note the "When IMMEDIATE is non-nil", "preferably", and "not always",
> and the fact that the reason is not that something will break but that
> some other solution would probably work better.

Then maybe the sentence on which I commented should say

  Except when IMMEDIATE is non-nil, if SIGNAL needs to prevent the
  upcoming change from being combined with the previous ones, it
  should call `track-changes-fetch' before it returns.

> > Are all those assertions a good idea in this function?
> 
> The library has a lot of `cl-assert`s which are all placed both as
> documentation and as sanity checks.  All those should hopefully be true
> all the time barring bugs in the code.
> 
> > I can envision using it as a cleanup, in which case assertions will
> > not be appreciated.
> 
> Not sure what you mean by "using it as a cleanup"?
> Its purpose is not to cleanup the general state of track-changes, but
> to create a new, clean `track-changes--state`.

In general, when I want to create a clean slate, I don't care too much
about the dirt I remove.  Why is it important to signal errors because
a state I am dumping had some errors?

> >> +;;;; Extra candidates for the API.
> >> +;; This could be a good alternative to using a temp-buffer like I used in
> >                                                                    ^^^^^^
> > "I"?
> 
> Yes, that refers to the code I wrote.

We don't usually leave such style in long-term comments and
documentation.

Thanks.




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 8 Apr 2024 15:25:16 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 08 11:25:16 2024
Received: from localhost ([127.0.0.1]:47387 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtqrx-0006Ac-Mw
	for submit <at> debbugs.gnu.org; Mon, 08 Apr 2024 11:25:16 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:26782)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtqrn-00068m-4q
 for 70077 <at> debbugs.gnu.org; Mon, 08 Apr 2024 11:25:13 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id A80A844170E;
 Mon,  8 Apr 2024 11:24:49 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712589887;
 bh=QrvdbIxaISx5Jy6QjtYdnyJxrb+xpLaFO1qeqETiNmI=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=mowZ3c/ze8WqYK7Zwqc3T7JKVkROEPdiB10YuSlGIHrBhgOSmYZGF6z3bKWl/LHql
 g2yugzfsmkj1u1agL80VqOkGOh520Vczg6q+9ECAlrOf8/2gJAbFQiKUI4RTIjei0a
 Lln28wJeH/V6t9qUw9qmIpG4xLe2l3grMPMvxYdd50bNNtwUjC1OBwqtGlSm8btFWD
 IvIjnpdJ3XdDbvRp7OPIy/qdq3wZhnpZn1NNWe1v70gMNsEjiLQnthDpHmsxBOA7u9
 Fyj2oVaLN+MKEC6t5DGa9Xh3vC/uKNM8fDtma6uMnhdIHl0Tm2Pe0UEQnjd7qE9EFb
 E/jE1Qe1otsCQ==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 774C24416D2;
 Mon,  8 Apr 2024 11:24:47 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 48BC8120679;
 Mon,  8 Apr 2024 11:24:47 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86frvy51af.fsf@HIDDEN> (Eli Zaretskii's message of "Sat, 06 Apr
 2024 11:43:36 +0300")
Message-ID: <jwvplv0c662.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> <86frvy51af.fsf@HIDDEN>
Date: Mon, 08 Apr 2024 11:24:38 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.026 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

>> +(cl-defstruct (track-changes--state
>> +               (:noinline t)
>> +               (:constructor nil)
>> +               (:constructor track-changes--state ()))
>> +  "Object holding a description of a buffer state.
>> +BEG..END is the area that was changed and BEFORE is its previous conten=
t.
>> +If the current buffer currently holds the content of the next state, yo=
u can get
>> +the contents of the previous state with:
>> +
>> +    (concat (buffer-substring (point-min) beg)
>> +                before
>> +                (buffer-substring end (point-max)))
>> +
>> +NEXT is the next state object (i.e. a more recent state).
>> +If NEXT is nil it means it's most recent state and it may be incomplete
>> +\(BEG/END/BEFORE may be nil), in which case those fields will take their
>> +values from `track-changes--before-(beg|end|before)' when the next
>> +state is create."
>
> This doc string should IMO include the form of the object, because
> without that BEG, END, BEFORE, and NEXT are "out of the blue", and
> it's entirely unclear what they allude to.

AFAIK this docstring is displayed only by `describe-type` which shows
something like:

    track-changes--state is a type (of kind =E2=80=98cl-structure-class=E2=
=80=99) in =E2=80=98track-changes.el=E2=80=99.
     Inherits from =E2=80=98cl-structure-object=E2=80=99.
=20=20=20=20
    Object holding a description of a buffer state.
    BEG..END is the area that was changed and BEFORE is its previous conten=
t.
    If the current buffer currently holds the content of the next state, yo=
u can get
    the contents of the previous state with:
=20=20=20=20
        (concat (buffer-substring (point-min) beg)
                    before
                    (buffer-substring end (point-max)))
=20=20=20=20
    NEXT is the next state object (i.e. a more recent state).
    If NEXT is nil it means it's most recent state and it may be incomplete
    (BEG/END/BEFORE may be nil), in which case those fields will take their
    values from `track-changes--before-(beg|end|before)' when the next
    state is create.
=20=20=20=20
    Instance Allocated Slots:
=20=20=20=20
            Name    Type    Default
            =E2=80=94=E2=80=94=E2=80=94=E2=80=94    =E2=80=94=E2=80=94=E2=
=80=94=E2=80=94    =E2=80=94=E2=80=94=E2=80=94=E2=80=94=E2=80=94=E2=80=94=
=E2=80=94
            beg     t       (point-max)
            end     t       (point-min)
            before  t       nil
            next    t       nil
=20=20=20=20
    Specialized Methods:
=20=20=20=20
    [...]

so the "form of the object" is included.  We don't have much practice
with docstrings for `cl-defstruct`, but I tried to follow the same kind
of conventions we use for functions, taking object slots as equivalent
to formal arguments.

Maybe `describe-type` should lists the slots first and the docstring
underneath rather than other way around?

>> +(cl-defun track-changes-register ( signal &key nobefore disjoint immedi=
ate)
>> +  "Register a new tracker and return a new tracker ID.
> Please mention SIGNAL in the first line of the doc string.

Hmm... having trouble making that fit on a single line.
Could you clarify why you think SIGNAL needs to be on the first line?

The best I could come up with so far is:

    Register a new change tracker handled via SIGNAL.

>> +By default SIGNAL is called as soon as convenient after a change, which=
 is
>                                ^^^^^^^^^^^^^^^^^^^^^
> "as soon as it's convenient", I presume?

Other than the extra " it's", what is the difference?

>> +usually right after the end of the current command.
> This should explicitly reference funcall-later, so that users could
> understand what "as soon as convenient" means.

I don't see the point of telling which function we use: the programmers
who want to know that, can consult the code.  As for explaining what "as
soon as convenient" means, that's what "usually right after the end of
the current command" is there for.

[ Also, I've changed the code to use `run-with-timer` in the mean time.  ]

>> +In order to prevent the upcoming change from being combined with the pr=
evious
>> +changes, SIGNAL needs to call `track-changes-fetch' before it returns."
>
> This seems to contradict what the doc string says previously: that
> SIGNAL should NOT call track-changes-fetch.

I don't kow where you see the docstring saying that.
The closest I can find is:

    When IMMEDIATE is non-nil, the SIGNAL should preferably not always call
    `track-changes-fetch', since that would defeat the purpose of this libr=
ary.

Note the "When IMMEDIATE is non-nil", "preferably", and "not always",
and the fact that the reason is not that something will break but that
some other solution would probably work better.
[ I changed "preferably" into "probably" since it's just a guess of mine
  rather than a request: there might be a good reason out there to
  prefer track-changes even for such a use case.  ]

>> +      ;; The states are disconnected from the latest state because
>> +      ;; we got out of sync!
>> +      (cl-assert (eq (track-changes--state-before (car states)) 'error))
> This seem to mean Emacs will signal an error in this case, not pass
> 'error' in BEFORE?

No, this verifies that the states were disconnected on purpose by
`track-changes--recover-from-error` rather than due to some bug in
the code.

>> +(defun track-changes--clean-state ()
>> +  (cond
>> +   ((null track-changes--state)
>> +    (cl-assert track-changes--before-clean)
>> +    (cl-assert (null track-changes--buffer-size))
>> +    ;; No state has been created yet.  Do it now.
>> +    (setq track-changes--buffer-size (buffer-size))
>> +    (when track-changes--before-no
>> +      (setq track-changes--before-string (buffer-size)))
>> +    (setq track-changes--state (track-changes--state)))
>> +   (track-changes--before-clean nil)
>> +   (t
>> +    (cl-assert (<=3D (track-changes--state-beg track-changes--state)
>> +                   (track-changes--state-end track-changes--state)))
>> +    (let ((actual-beg (track-changes--state-beg track-changes--state))
>> +          (actual-end (track-changes--state-end track-changes--state)))
>> +      (if track-changes--before-no
>> +          (progn
>> +            (cl-assert (integerp track-changes--before-string))
>> +            (setf (track-changes--state-before track-changes--state)
>> +                  (- track-changes--before-string
>> +                     (- (buffer-size) (- actual-end actual-beg))))
>> +            (setq track-changes--before-string (buffer-size)))
>> +        (cl-assert (<=3D track-changes--before-beg
>> +                       actual-beg actual-end
>> +                       track-changes--before-end))
>> +        (cl-assert (null (track-changes--state-before track-changes--st=
ate)))
>
> Are all those assertions a good idea in this function?

The library has a lot of `cl-assert`s which are all placed both as
documentation and as sanity checks.  All those should hopefully be true
all the time barring bugs in the code.

> I can envision using it as a cleanup, in which case assertions will
> not be appreciated.

Not sure what you mean by "using it as a cleanup"?
Its purpose is not to cleanup the general state of track-changes, but
to create a new, clean `track-changes--state`.

>> +;;;; Extra candidates for the API.
>> +;; This could be a good alternative to using a temp-buffer like I used =
in
>                                                                    ^^^^^^
> "I"?

Yes, that refers to the code I wrote.

>> +;; Eglot, since presumably we've just been changing this very area of t=
he
>> +;; buffer, so the gap should be ready nearby,
>> +;; It may seem silly to go back to the previous state, since we could h=
ave
>> +;; used `before-change-functions' to run FUNC right then when we were in
>> +;; that state.  The advantage is that with track-changes we get to deci=
de
>> +;; retroactively which state is the one for which we want to call FUNC =
and
>> +;; which BEG..END to use: when that state was current we may have known
>> +;; then that it would be "the one" but we didn't know what BEG and END
>> +;; should be because those depend on the changes that came afterwards.
>
> Suggest to reword (or remove) this comment, as it sounds like
> development-time notes.

This is in the "Extra candidates for the API" section, which holds
a bunch of things which might be useful or might not.

>> +(defun diff--track-changes-function (beg end _before)
>> +  (with-demoted-errors "%S"
> Why did you need with-demoted-errors here?

It used to be `ignore-errors` because back in 1999 we didn't have
`with-demoted-errors`.

> Last, but not least: this needs suitable changes in NEWS and ELisp
> manual.

Working on it.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 7 Apr 2024 15:48:22 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sun Apr 07 11:48:21 2024
Received: from localhost ([127.0.0.1]:44344 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtUkm-0006Ex-VM
	for submit <at> debbugs.gnu.org; Sun, 07 Apr 2024 11:48:21 -0400
Received: from wout3-smtp.messagingengine.com ([64.147.123.19]:47305)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <dmitry@HIDDEN>) id 1rtUkk-0006DQ-5l
 for 70077 <at> debbugs.gnu.org; Sun, 07 Apr 2024 11:48:19 -0400
Received: from compute6.internal (compute6.nyi.internal [10.202.2.47])
 by mailout.west.internal (Postfix) with ESMTP id 9D9453200A16;
 Sun,  7 Apr 2024 11:48:04 -0400 (EDT)
Received: from mailfrontend2 ([10.202.2.163])
 by compute6.internal (MEProxy); Sun, 07 Apr 2024 11:48:05 -0400
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gutov.dev; h=cc
 :cc:content-transfer-encoding:content-type:content-type:date
 :date:from:from:in-reply-to:in-reply-to:message-id:mime-version
 :references:reply-to:subject:subject:to:to; s=fm1; t=1712504884;
 x=1712591284; bh=BAJoCLaXy9xEiC7L9PrfUKbpV6z2Neymc/TM0bROAhc=; b=
 YAnvEkEHWiJ2InJJeIcBe0D2UEpvbk4dbdTGYfNHjvsZkn/QI5K7C9kzWZueSHK0
 jE+ReyORYrZdbSa86XsJqcnCkZaLOH2jmziGEPMYuv9DsRa/CWOm39VEUKagjKzx
 rQNegIVrJAy34LAzd/ONtBE9JDggSty2G8ZGT5aj5Klb7oMIhe8oJJtNpiAh3gJT
 un4z2eb3eynGUQ7hUmi/t/78KZTilSVSgF6wpSrCRbdMkbNmwheR0oD6NPOkiqMk
 Bda0SOdKMs/hNVM3sg9uer/FBM8HCP1pIAivR+j7P0f/2Jrz7aX4uJ1YevXDcVKP
 yYWrrTyKW5KfV44ANe7wvw==
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
 messagingengine.com; h=cc:cc:content-transfer-encoding
 :content-type:content-type:date:date:feedback-id:feedback-id
 :from:from:in-reply-to:in-reply-to:message-id:mime-version
 :references:reply-to:subject:subject:to:to:x-me-proxy:x-me-proxy
 :x-me-sender:x-me-sender:x-sasl-enc; s=fm2; t=1712504884; x=
 1712591284; bh=BAJoCLaXy9xEiC7L9PrfUKbpV6z2Neymc/TM0bROAhc=; b=A
 Dlqki/6KJR9dnaSHkcjPk/GzT0ScbKIMcmPg/Fl+bxIz3zPs5Zyk+IseQ7Pi4Vwu
 J3nJ+NonpFR6FsX/jAIG9LctH0mFBX5ySjWZibAHZCaBbxBRDpN6+wE31Tv80fY8
 zTUeUTHXmI9bskUhBWHh/t33fzV3ND/swJjGSZrt8mPlkwXN2LcICNS3QPfqvnXc
 Y1X5K/5FdwpWZhyUEZWYty16jJcHVdAzTr4qhZ6Egmp2+DseAtIFddMblfkfieCO
 VfoLvtVfN7XzwORIdSCxI1vxjKW0mfvzAMgy2Tl/9m3oQb32HBgX6IDRFavmTxie
 ieoQHDqAhHnzeOQlqGP8w==
X-ME-Sender: <xms:M8ASZg-8cEheTD7ghITuxJoC2r4iZfMnffRqP6GxO9dKbQuQlJht6A>
 <xme:M8ASZouov8i6Y20_Gl542UIsPhKqcqAP3USuyVk4qJav6j_qZ9jLvokE6FDE3uLEi
 iz0BONoZVkv0lUXrb4>
X-ME-Received: <xmr:M8ASZmD9i4yFkZzWrDwwlQJ9yMSPRCTrpqES44yxo0wCfSazxmBxnr1O1JhaHEs_z4pc>
X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvledrudeggedgledvucetufdoteggodetrfdotf
 fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen
 uceurghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmne
 cujfgurhepkfffgggfuffvvehfhfgjtgfgsehtjeertddtvdejnecuhfhrohhmpeffmhhi
 thhrhicuifhuthhovhcuoegumhhithhrhiesghhuthhovhdruggvvheqnecuggftrfgrth
 htvghrnhepteduleejgeehtefgheegjeekueehvdevieekueeftddvtdevfefhvdevgedu
 jeehnecuvehluhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepug
 hmihhtrhihsehguhhtohhvrdguvghv
X-ME-Proxy: <xmx:M8ASZgeOorZQqvCjB9HgKXLoDUvSY2sppQzdzKIt1EH9_OugV_RCOQ>
 <xmx:M8ASZlOHiAvAB-bgP6nF1lOQtuhADlltSAezkkcfCEx5nX0MQo8Rcw>
 <xmx:M8ASZqnMIM7wk1TcuRjH54hrqlKsPWV9P7wJylQaZKRYSBLcyZ2Blw>
 <xmx:M8ASZnuLQOjKmOsyQ9PK4BPGEDCBpEFHvWKeDaKbuVIrse8FcHc2Mg>
 <xmx:NMASZqruq-mBGjwiHo-opi8t2RQixe86S6NbqAYd3tyPdCEuFrPvMoNauNuN>
Feedback-ID: i0e71465a:Fastmail
Received: by mail.messagingengine.com (Postfix) with ESMTPA; Sun,
 7 Apr 2024 11:48:01 -0400 (EDT)
Message-ID: <b80a8d7a-e7cb-4057-a86e-e7d7012621ef@HIDDEN>
Date: Sun, 7 Apr 2024 18:47:59 +0300
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Subject: Re: bug#70077: An easier way to track buffer changes
Content-Language: en-US
To: Stefan Monnier <monnier@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
 <a4cdc627-1513-4d71-99b6-0f3e95870fd5@HIDDEN>
 <jwvmsq6gtyv.fsf-monnier+emacs@HIDDEN>
 <jwvr0fhz1az.fsf-monnier+emacs@HIDDEN>
From: Dmitry Gutov <dmitry@HIDDEN>
In-Reply-To: <jwvr0fhz1az.fsf-monnier+emacs@HIDDEN>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, Ihor Radchenko <yantar92@HIDDEN>,
 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

On 07/04/2024 17:40, Stefan Monnier wrote:
>>> Eglot is a "ELPA core" package.
>>> Will we put `track-changes' into ELPA as well?
>> The part of the patch that touches `eglot.el` is not indispensable, but
>> yes, that's indeed something I've been wondering as well, seeing how
>> it could be useful for third party packages like Lsp-mode, Crdt,
>> and more.
> BTW, by that I meant that maybe it should live in `elpa.git` (i.e. in
> GNU ELPA instead of in Emacs).  This is especially since I'm not sure we
> want to push this as "the" API.

In that case, the change to Eglot might have to be postponed (since we 
can't make a built-in package depend on code that could be absent).

I don't have an opinion on the API itself, FWIW.




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 7 Apr 2024 14:40:23 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sun Apr 07 10:40:23 2024
Received: from localhost ([127.0.0.1]:44296 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtTh0-0000HT-UY
	for submit <at> debbugs.gnu.org; Sun, 07 Apr 2024 10:40:23 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:7837)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtTgy-0000HD-BQ
 for 70077 <at> debbugs.gnu.org; Sun, 07 Apr 2024 10:40:21 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 1C8E544096B;
 Sun,  7 Apr 2024 10:40:07 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712500805;
 bh=PMnKQDiPaxHZbYnEEDCaypyXq1bK8Qz7/0sY/ereXkU=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=ctH4eLtSflRNs38skuqLjQcFvAaPj4IYuDFcxDAYeOSq5WNI2QJkrDiwHz3XsFkjs
 a/OHO+7zsCvcw1ZLAmDghdxSwybBKIu5clrp6wpNygz7/ls5eBv0j9zPik6dZ4uk67
 mlOxDxBrnEIA4QQXfMd5KtLJEr2+r3RPt9lLlLKy6nFZ4J1O1/fI6OaoBMxv/fRm9V
 Im1OzoY8CiMFZnS3+iAiv7hGqP4wO2l+aXCEUt4N5yY/Axpe2gbhN21zbvZq09P3Q0
 uMK2ExjBfsNfCsxkY3nbQjBxuQooiUemp2e/2tOILnV3wgsqs9kwtqfU+3/MFUso1C
 ulQxPRCCbARJQ==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id E700A44095E;
 Sun,  7 Apr 2024 10:40:05 -0400 (EDT)
Received: from alfajor (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id B2AF6120484;
 Sun,  7 Apr 2024 10:40:05 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Dmitry Gutov <dmitry@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvmsq6gtyv.fsf-monnier+emacs@HIDDEN> (Stefan Monnier's message
 of "Sat, 06 Apr 2024 15:44:09 -0400")
Message-ID: <jwvr0fhz1az.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
 <a4cdc627-1513-4d71-99b6-0f3e95870fd5@HIDDEN>
 <jwvmsq6gtyv.fsf-monnier+emacs@HIDDEN>
Date: Sun, 07 Apr 2024 10:40:05 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.010 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, Ihor Radchenko <yantar92@HIDDEN>,
 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

>> Eglot is a "ELPA core" package.
>> Will we put `track-changes' into ELPA as well?
>
> The part of the patch that touches `eglot.el` is not indispensable, but
> yes, that's indeed something I've been wondering as well, seeing how
> it could be useful for third party packages like Lsp-mode, Crdt,
> and more.

BTW, by that I meant that maybe it should live in `elpa.git` (i.e. in
GNU ELPA instead of in Emacs).  This is especially since I'm not sure we
want to push this as "the" API.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 7 Apr 2024 14:07:37 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sun Apr 07 10:07:37 2024
Received: from localhost ([127.0.0.1]:44261 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtTBG-0005ug-OY
	for submit <at> debbugs.gnu.org; Sun, 07 Apr 2024 10:07:36 -0400
Received: from mout01.posteo.de ([185.67.36.65]:33021)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rtTBB-0005tq-WC
 for 70077 <at> debbugs.gnu.org; Sun, 07 Apr 2024 10:07:32 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout01.posteo.de (Postfix) with ESMTPS id C4C3C240029
 for <70077 <at> debbugs.gnu.org>; Sun,  7 Apr 2024 16:07:16 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1712498836; bh=0q+XMW+bJ2nVOoAWS72MjFAriW4tJ+04YXa+tJ4erzU=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=OcccZB4hxJicjQVBmvGPEjYdzCPaTZ5ua7C6FgS9543MPBOQfnBE/LuR8CAnn5Orp
 FTTJkO2sWYC8H5tw06Ax5ZouWsa4eWwZnnI/0gMUI/2yEsFO2BhdPVC0lEqtZ+yPuU
 yGNg/7KsXv+sh9TxoSrvsU0DscW1doJEXfDPFhbZWlQ5/0Lj42xnJfmIntbZvx+bet
 15G44rVsHCz0Giu8VphsjozHlLrlAs/6NAlYcEsOHSUBD8AhQp0AIrekxRTB3Bytf3
 ns9cT5V+wPkmuD2cUjDuutWQBnTPWD2hewyeY1eMu94jpf7xKACCc2OjASDqdUdeas
 hGPEuEA/ZGegA==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4VCDYv5YKPz9rxK;
 Sun,  7 Apr 2024 16:07:15 +0200 (CEST)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
Date: Sun, 07 Apr 2024 14:07:36 +0000
Message-ID: <87edbhnu53.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

Stefan Monnier <monnier@HIDDEN> writes:

> +(cl-defstruct (track-changes--state
> +               (:noinline t)
> +               (:constructor nil)
> +               (:constructor track-changes--state ()))
> +  "Object holding a description of a buffer state.
> +BEG..END is the area that was changed and BEFORE is its previous content.
> +If the current buffer currently holds the content of the next state, you can get
> +the contents of the previous state with:
> +
> +    (concat (buffer-substring (point-min) beg)
> +                before
> +                (buffer-substring end (point-max)))
> +
> +NEXT is the next state object (i.e. a more recent state).
> +If NEXT is nil it means it's most recent state and it may be incomplete
> +\(BEG/END/BEFORE may be nil), in which case those fields will take their
> +values from `track-changes--before-(beg|end|before)' when the next
> +state is create."

This docstring is a bit confusing.
If a state object is not the most recent, how come 

> +    (concat (buffer-substring (point-min) beg)
> +                before
> +                (buffer-substring end (point-max)))

produces the previous content?

And if the state object is the most recent, "it may be incomplete"...

So, when is it safe to use the above (concat ... ) call?

> +(defvar-local track-changes--before-beg (point-min)
> +  "Beginning position of the remembered \"before string\".")
> +(defvar-local track-changes--before-end (point-min)
> +  "End position of the text replacing the \"before string\".")

Why (point-min)? It will make the values dependent on the buffer
narrowing that happens to be active when the library if first loaded.
Which cannot be right.

> +(defvar-local track-changes--buffer-size nil
> +  "Current size of the buffer, as far as this library knows.
> +This is used to try and detect cases where buffer modifications are \"lost\".")

Just looking at the buffer size may miss unregistered edits that do not
change the total size of the buffer. Although I do not know a better
measure. `buffer-chars-modified-tic' may lead to false-positives
(Bug#51766).

> +(cl-defun track-changes-register ( signal &key nobefore disjoint immediate)
> +  "Register a new tracker and return a new tracker ID.
> +SIGNAL is a function that will be called with one argument (the tracker ID)
> +after the current buffer is modified, so that we can react to the change.
> + ...
> +If optional argument DISJOINT is non-nil, SIGNAL is called every time we are
> +about to combine changes from \"distant\" parts of the buffer.
> +This is needed when combining disjoint changes into one bigger change
> +is unacceptable, typically for performance reasons.
> +These calls are distinguished from normal calls by calling SIGNAL with
> +a second argument which is the distance between the upcoming change and
> +the previous changes.

This is a bit confusing. The first paragraph says that SIGNAL is called
with a single argument, but that it appears that two arguments may be
passed. I'd rather tell the calling convention early in the docstring.

> +  (unless nobefore
> +    (setq track-changes--before-no nil)
> +    (add-hook 'before-change-functions #'track-changes--before nil t))
> +  (add-hook 'after-change-functions  #'track-changes--after  nil t)

Note that all the changes made in indirect buffers will be missed.
See bug#60333.

> +(defun track-changes-fetch (id func)
> ...
> +  (unless (equal track-changes--buffer-size (buffer-size))
> +    (track-changes--recover-from-error))
> +  (let ((beg nil)
> +        (end nil)
> +        (before t)
> +        (lenbefore 0)
> +        (states ()))
> +    ;; Transfer the data from `track-changes--before-string'
> +    ;; to the tracker's state object, if needed.
> +    (track-changes--clean-state)

> +(defun track-changes--recover-from-error ()
> ...
> +  (setq track-changes--state (track-changes--state)))

This will create a dummy state with

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

such state will not pass (< beg end) assertion in
`track-changes--clean-state' called in `track-changes-fetch' immediately
after `track-changes--recover-from-error'

> +(defun track-changes--in-revert (beg end before func)
> ...
> +      (atomic-change-group
> +        (goto-char end)
> +        (insert-before-markers before)
> +        (delete-region beg end)

What happens if there are markers inside beg...end?

> +(defun track-changes-tests--random-word ()
> +  (let ((chars ()))
> +    (dotimes (_ (1+ (random 12)))
> +      (push (+ ?A (random (1+ (- ?z ?A)))) chars))
> +    (apply #'string chars)))

If you are using random values for edits, how can such test be
reproduced? Maybe first generate a random seed and then log it, so that
the failing test can be repeated if necessary with seed assigned manually.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 6 Apr 2024 19:44:31 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Apr 06 15:44:31 2024
Received: from localhost ([127.0.0.1]:41001 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rtBxm-0001se-Pn
	for submit <at> debbugs.gnu.org; Sat, 06 Apr 2024 15:44:30 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:32148)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rtBxg-0001sN-K6
 for 70077 <at> debbugs.gnu.org; Sat, 06 Apr 2024 15:44:28 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 019BA4414D6;
 Sat,  6 Apr 2024 15:44:12 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712432650;
 bh=r7urAk381/5UGnTb/PgeAQMP//aio0QtsFxm9KBVhh0=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=W9mHsHFfssX5Jk7TfAjoKBw8z58W52KDgFt3V6qtzwHSdZrqkENp101jXgsjPJ+g8
 TPgzJvSS9Y1zoiwvMz8a7UxDMCZ7tZOHnNco3a7/X6qq0GUfhB4G08GIJy2QLcrJlN
 k3iBMmD1VkqBE7pAFgjIeBDT/RQIb+5kDuglQvtVR1Qnpm7FWYzSUiOOu9xxKwgnyw
 h1aj0UUl9NGjJDsEL2yAxzanNPx/rqEcx8kRz0I8AkEvmfnTpfGiKhSnrp8S3mHtgj
 ZmlOQMkKrC/v0oxgBWsR8BtTy47ePQtnWoNjfRWTNjJmyHwCVWkpjfMWcqffLXoOwI
 pWacVUVx/+nVg==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id C67C74414BF;
 Sat,  6 Apr 2024 15:44:10 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 93D8212024C;
 Sat,  6 Apr 2024 15:44:10 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Dmitry Gutov <dmitry@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <a4cdc627-1513-4d71-99b6-0f3e95870fd5@HIDDEN> (Dmitry Gutov's
 message of "Sat, 6 Apr 2024 20:37:29 +0300")
Message-ID: <jwvmsq6gtyv.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
 <a4cdc627-1513-4d71-99b6-0f3e95870fd5@HIDDEN>
Date: Sat, 06 Apr 2024 15:44:09 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.010 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, Ihor Radchenko <yantar92@HIDDEN>,
 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

>> diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
>> index 7f4284bf09d..00c09d7f06b 100644
>> --- a/lisp/progmodes/eglot.el
>> +++ b/lisp/progmodes/eglot.el
>> @@ -110,6 +110,7 @@
>>   (require 'text-property-search nil t)
>>   (require 'diff-mode)
>>   (require 'diff)
>> +(require 'track-changes)
> Eglot is a "ELPA core" package.
> Will we put `track-changes' into ELPA as well?

The part of the patch that touches `eglot.el` is not indispensable, but
yes, that's indeed something I've been wondering as well, seeing how
it could be useful for third party packages like Lsp-mode, Crdt,
and more.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 6 Apr 2024 17:37:49 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Apr 06 13:37:49 2024
Received: from localhost ([127.0.0.1]:40923 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rt9zA-0003HK-QK
	for submit <at> debbugs.gnu.org; Sat, 06 Apr 2024 13:37:49 -0400
Received: from wout2-smtp.messagingengine.com ([64.147.123.25]:34497)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <dmitry@HIDDEN>) id 1rt9z7-0003G6-0h
 for 70077 <at> debbugs.gnu.org; Sat, 06 Apr 2024 13:37:46 -0400
Received: from compute2.internal (compute2.nyi.internal [10.202.2.46])
 by mailout.west.internal (Postfix) with ESMTP id B0B5332009F8;
 Sat,  6 Apr 2024 13:37:32 -0400 (EDT)
Received: from mailfrontend2 ([10.202.2.163])
 by compute2.internal (MEProxy); Sat, 06 Apr 2024 13:37:33 -0400
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gutov.dev; h=cc
 :cc:content-transfer-encoding:content-type:content-type:date
 :date:from:from:in-reply-to:in-reply-to:message-id:mime-version
 :references:reply-to:subject:subject:to:to; s=fm1; t=1712425052;
 x=1712511452; bh=dsQC201Gioi/v8U+GO77GZcj49ZmKkQp05koM7v8aTk=; b=
 ocEAHTRMZ2rRP2P41qp9sQT8jjSDoj5Jw2QYCTDRphdrxJBPaWDUFairm/H7QqRX
 BT+iEXATJJJenLYivt4+RkK5YiNsMrK5knUld7piZQWIdYb0i2UmmWGGwZv0Hc8d
 sGHRaRu0DHwPUBfX8u1rrkRcRpf1nBGOVGd7TDKWvkiUiDiRsG9SyjbaMdODnwFn
 JJWxphqVEZnQRxDJhuAVOJ03GgBA1UrkERsLDHuELxxsSN08prBvwEracd90YB/r
 LdC/4gTVlCUU766MtJTk9WsprR+tsxba6NhRosvzvTchPVRKcrSYGLQDzigwTXR4
 5XcWLbO1c/ax+RHW3mnh2w==
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=
 messagingengine.com; h=cc:cc:content-transfer-encoding
 :content-type:content-type:date:date:feedback-id:feedback-id
 :from:from:in-reply-to:in-reply-to:message-id:mime-version
 :references:reply-to:subject:subject:to:to:x-me-proxy:x-me-proxy
 :x-me-sender:x-me-sender:x-sasl-enc; s=fm2; t=1712425052; x=
 1712511452; bh=dsQC201Gioi/v8U+GO77GZcj49ZmKkQp05koM7v8aTk=; b=R
 1H5MtAgAeLZ/lCw3b+/R3822SmnuDWSJfyx/ubg+CT4bpo12Pq7mwEQzV4FuPArY
 0qQoODf/CI27Cj9pBauPAhoFfihqHdc3Y/8Pi0sWMSHJl5c0Qi5Cia0Gv2dO8S0M
 PkEbKOuNeAztCuA4ECaL/SWmXgGVNve51pyg5Vt+oPp5NS2e7I/Aa5EjVSUIocll
 mc8wIblG/6w3fPM/Lig5GTEM0Q9dozYUa7oqVNyRPt1jByIwFpeevtaiXt2yM3YV
 QLThEDXlRiTMyu26eHzKkCFzrqA+GTcnsClTvHGc2+jgSgxuZ0eA8r1BIC16NFL6
 fI4O4JsU0JA2n5wmTX+qw==
X-ME-Sender: <xms:W4gRZi9tKwRD3wC110LMQM878ng5PE8kOJtdZvUMDtm_gYPlVznaxw>
 <xme:W4gRZivT33tKlvnlK0h0Ae0GX64LpB-YILSypKQVX35jjVQi2FNz8buZRCGI7yFSz
 kHsZ5qxnHBGSQwVEzA>
X-ME-Received: <xmr:W4gRZoDjCcOiBxgzREYTXmcPe-Y_tka78ZHH3f7Y297HtHLOrHTksDC0BAV8s1i53Oce>
X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvledrudegvddgudduiecutefuodetggdotefrod
 ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfgfvfdpuffrtefokffrpgfnqfgh
 necuuegrihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmd
 enucfjughrpefkffggfgfuvfevfhfhjggtgfesthejredttddvjeenucfhrhhomhepffhm
 ihhtrhihucfiuhhtohhvuceoughmihhtrhihsehguhhtohhvrdguvghvqeenucggtffrrg
 htthgvrhhnpeetudeljeegheetgfehgeejkeeuhedvveeikeeufedtvddtveefhfdvveeg
 udejheenucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpe
 gumhhithhrhiesghhuthhovhdruggvvh
X-ME-Proxy: <xmx:W4gRZqdJfuyF1LqSR09Oa660kNZjjSjc--hXv4JtfDrmPutV8HRTVA>
 <xmx:W4gRZnMqKRzWfFKle9tI6jffw-oN9BrqsEuAYFr4pvLbr4c31-XhvA>
 <xmx:W4gRZkkBlVZnsem_2B3v56yiR0Tm1gQ4uvpJ3LDxMI7BG9jYwqJHrQ>
 <xmx:W4gRZpuzgqz4w7wosiAwY9phHJlBnpTtPt8NkrrAH4nfcsCf79ri9A>
 <xmx:XIgRZsr6vvl14u7mfqcNi9QLKpY158EHmD0yj5G8Hx039WHkFwKx7h5Lindw>
Feedback-ID: i0e71465a:Fastmail
Received: by mail.messagingengine.com (Postfix) with ESMTPA; Sat,
 6 Apr 2024 13:37:30 -0400 (EDT)
Message-ID: <a4cdc627-1513-4d71-99b6-0f3e95870fd5@HIDDEN>
Date: Sat, 6 Apr 2024 20:37:29 +0300
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Subject: Re: bug#70077: An easier way to track buffer changes
Content-Language: en-US
To: Stefan Monnier <monnier@HIDDEN>, 70077 <at> debbugs.gnu.org
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
From: Dmitry Gutov <dmitry@HIDDEN>
In-Reply-To: <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, Ihor Radchenko <yantar92@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

On 06/04/2024 01:12, Stefan Monnier via Bug reports for GNU Emacs, the 
Swiss army knife of text editors wrote:
> diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
> index 7f4284bf09d..00c09d7f06b 100644
> --- a/lisp/progmodes/eglot.el
> +++ b/lisp/progmodes/eglot.el
> @@ -110,6 +110,7 @@
>   (require 'text-property-search nil t)
>   (require 'diff-mode)
>   (require 'diff)
> +(require 'track-changes)

Eglot is a "ELPA core" package.

Will we put `track-changes' into ELPA as well?




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 6 Apr 2024 08:43:54 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Apr 06 04:43:54 2024
Received: from localhost ([127.0.0.1]:38206 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rt1eT-0005gC-UP
	for submit <at> debbugs.gnu.org; Sat, 06 Apr 2024 04:43:54 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:46238)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rt1eR-0005fL-3V
 for 70077 <at> debbugs.gnu.org; Sat, 06 Apr 2024 04:43:53 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rt1eE-0003Cf-VI; Sat, 06 Apr 2024 04:43:38 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=Jiu2fiCBqPhYIp0Vdazq4VEmBhaG6Ji5mcRroBuSdU4=; b=AVkBQKTKRwk9
 PxedgmLasWc0LvWLX4fERpm17IFHfja+/PxpgqzlI5IMV5J6LX74cj86hoD26lt0IQFpGYqgcTYXO
 5NhXlUFqxZDNr/1CRssou3PLr/p2z9TNWYMIl23sg9bUSgPk2haHFHCfXFvpUk8RheJMPHKJS5IWi
 heREzv4f2m7SiVb2GhYLCF7ioXzMvY/HsbLfHoivLWaszYkP+x2FiG192uEOwgDa04CjQLkz9RVXo
 C+ig4v28n/tu4nLZXcalUPRYRFHRiM24+kXGeGoSsywO/KLb16fSP4me5z3DqDGEFchGNmU2O+uqd
 n21+0IsKUgs0IWra8KSgdg==;
Date: Sat, 06 Apr 2024 11:43:36 +0300
Message-Id: <86frvy51af.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwv7chba2wu.fsf-monnier+emacs@HIDDEN> (bug-gnu-emacs@HIDDEN)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN>
 <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: acm@HIDDEN, yantar92@HIDDEN, 70077 <at> debbugs.gnu.org
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

> Cc: Alan Mackenzie <acm@HIDDEN>, Ihor Radchenko <yantar92@HIDDEN>
> Date: Fri, 05 Apr 2024 18:12:55 -0400
> From:  Stefan Monnier via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <bug-gnu-emacs@HIDDEN>
> 
> +;; This library is a layer of abstraction above `before-change-functions'
> +;; and `after-change-functions' which takes care of accumulating changes
> +;; until a time when its client finds it convenient to react to them.
> +;;
> +;; It provides an API that is easier to use correctly than our
> +;; `*-change-functions` hooks.  Problems that it claims to solve:

Please redo all the quoting from `like this` to one of the two styles
we use in our documentation.

> +(unless (fboundp 'funcall-later)
> +  (defun funcall-later (&rest args)
> +    ;; FIXME: Not sure if `run-with-timer' preserves ordering between
> +    ;; different calls with the same target time.
> +   (apply #'run-with-timer 0 nil args)))

Both funcall-later and run-with-timer??

> +
> +;;;; Internal types and variables.
> +
> +(cl-defstruct (track-changes--tracker
> +               (:noinline t)
> +               (:constructor nil)
> +               (:constructor track-changes--tracker ( signal state
> +                                                      &optional
> +                                                      nobefore immediate)))
> +  signal state nobefore immediate)
> +
> +(cl-defstruct (track-changes--state
> +               (:noinline t)
> +               (:constructor nil)
> +               (:constructor track-changes--state ()))
> +  "Object holding a description of a buffer state.
> +BEG..END is the area that was changed and BEFORE is its previous content.
> +If the current buffer currently holds the content of the next state, you can get
> +the contents of the previous state with:
> +
> +    (concat (buffer-substring (point-min) beg)
> +                before
> +                (buffer-substring end (point-max)))
> +
> +NEXT is the next state object (i.e. a more recent state).
> +If NEXT is nil it means it's most recent state and it may be incomplete
> +\(BEG/END/BEFORE may be nil), in which case those fields will take their
> +values from `track-changes--before-(beg|end|before)' when the next
> +state is create."

This doc string should IMO include the form of the object, because
without that BEG, END, BEFORE, and NEXT are "out of the blue", and
it's entirely unclear what they allude to.

Also, this doc string (and a few others) have very long lines, so
please refill them.

> +(defvar-local track-changes--trackers ()
> +  "List of trackers currently registered in the current buffer.")
                                            ^^^^^^^^^^^^^^^^^^^^^
I think "in the buffer" is more accurate, since this is not limited to
the current buffer.

> +(defvar-local track-changes--disjoint-trackers ()
> + "List of trackers that want to react to disjoint changes.
> +These trackers' are signaled every time track-changes notices
                 ^
That apostrophe is redundant.

> +(defvar-local track-changes--before-clean 'unset
> +  "If non-nil, the `track-changes--before-*' vars are old.
> +More specifically it means they cover a part of the buffer relevant
> +for the previous state.
> +It can take two non-nil values:
> +- `unset': Means that the vars cover some older state.
> +  This is what it is set right after creating a fresh new state.
                        ^^^
"set to"

> +(cl-defun track-changes-register ( signal &key nobefore disjoint immediate)
> +  "Register a new tracker and return a new tracker ID.

Please mention SIGNAL in the first line of the doc string.

> +SIGNAL is a function that will be called with one argument (the tracker ID)
> +after the current buffer is modified, so that we can react to the change.
                                                 ^^
"we"?

> +By default SIGNAL is called as soon as convenient after a change, which is
                               ^^^^^^^^^^^^^^^^^^^^^
"as soon as it's convenient", I presume?

> +usually right after the end of the current command.

This should explicitly reference funcall-later, so that users could
understand what "as soon as convenient" means.

> +If optional argument DISJOINT is non-nil, SIGNAL is called every time we are
> +about to combine changes from \"distant\" parts of the buffer.        ^^

"we" again?

> +In order to prevent the upcoming change from being combined with the previous
> +changes, SIGNAL needs to call `track-changes-fetch' before it returns."

This seems to contradict what the doc string says previously: that
SIGNAL should NOT call track-changes-fetch.

> +(defun track-changes-fetch (id func)
> +  "Fetch the pending changes.

The first line of a doc string should mention the arguments.

> +ID is the tracker ID returned by a previous `track-changes-register'.
> +FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
> +where BEGIN..END delimit the region that was changed since the last
> +time `track-changes-fetch' was called and BEFORE is a string containing
> +the previous content of that region (or just its length as an integer
> +If the tracker ID was registered with the `nobefore' option).
   ^^
"if", lower-case.

> +If some error caused us to miss some changes, then BEFORE will be the
                        ^^
"we" again?

> +If no changes occurred since the last time, FUNC is not called and
> +we return nil, otherwise we return the value returned by FUNC,
   ^^
And again.

> +      ;; The states are disconnected from the latest state because
> +      ;; we got out of sync!
> +      (cl-assert (eq (track-changes--state-before (car states)) 'error))

This seem to mean Emacs will signal an error in this case, not pass
'error' in BEFORE?

> +(defun track-changes--clean-state ()
> +  (cond
> +   ((null track-changes--state)
> +    (cl-assert track-changes--before-clean)
> +    (cl-assert (null track-changes--buffer-size))
> +    ;; No state has been created yet.  Do it now.
> +    (setq track-changes--buffer-size (buffer-size))
> +    (when track-changes--before-no
> +      (setq track-changes--before-string (buffer-size)))
> +    (setq track-changes--state (track-changes--state)))
> +   (track-changes--before-clean nil)
> +   (t
> +    (cl-assert (<= (track-changes--state-beg track-changes--state)
> +                   (track-changes--state-end track-changes--state)))
> +    (let ((actual-beg (track-changes--state-beg track-changes--state))
> +          (actual-end (track-changes--state-end track-changes--state)))
> +      (if track-changes--before-no
> +          (progn
> +            (cl-assert (integerp track-changes--before-string))
> +            (setf (track-changes--state-before track-changes--state)
> +                  (- track-changes--before-string
> +                     (- (buffer-size) (- actual-end actual-beg))))
> +            (setq track-changes--before-string (buffer-size)))
> +        (cl-assert (<= track-changes--before-beg
> +                       actual-beg actual-end
> +                       track-changes--before-end))
> +        (cl-assert (null (track-changes--state-before track-changes--state)))

Are all those assertions a good idea in this function?  I can envision
using it as a cleanup, in which case assertions will not be
appreciated.

> +(defvar track-changes--disjoint-threshold 100
> +  "Distance below which changes are not considered disjoint.")

This should tell in what units the distance is measured.

> +;;;; Extra candidates for the API.
> +
> +;; This could be a good alternative to using a temp-buffer like I used in
                                                                   ^^^^^^
"I"?

> +;; Eglot, since presumably we've just been changing this very area of the
> +;; buffer, so the gap should be ready nearby,
> +;; It may seem silly to go back to the previous state, since we could have
> +;; used `before-change-functions' to run FUNC right then when we were in
> +;; that state.  The advantage is that with track-changes we get to decide
> +;; retroactively which state is the one for which we want to call FUNC and
> +;; which BEG..END to use: when that state was current we may have known
> +;; then that it would be "the one" but we didn't know what BEG and END
> +;; should be because those depend on the changes that came afterwards.

Suggest to reword (or remove) this comment, as it sounds like
development-time notes.

> +(defun diff--track-changes-function (beg end _before)
> +  (with-demoted-errors "%S"

Why did you need with-demoted-errors here?

Last, but not least: this needs suitable changes in NEWS and ELisp
manual.

Thanks.




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 5 Apr 2024 22:15:52 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Apr 05 18:15:52 2024
Received: from localhost ([127.0.0.1]:37693 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rsrqf-0004oa-VS
	for submit <at> debbugs.gnu.org; Fri, 05 Apr 2024 18:15:52 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:50849)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rsrqa-0004nq-83
 for 70077 <at> debbugs.gnu.org; Fri, 05 Apr 2024 18:15:47 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id E73C51000FC;
 Fri,  5 Apr 2024 18:15:31 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712355328;
 bh=dGUcmgOriDzAfuJNhgDWYDl5M/7cskGka7aJLPW4U/4=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=EB/F2k0MA+8Pt/f9T90Brx3sXzUWP3qLSrYT2JSiaRc43IYSpz1W8vgrgGnsrnzkW
 /f6uR7dDeEOOo8428CI80S4ITJ/HWZfnc/rPsMhzuh0V+rzflimPFnBnM5zY9b5u7l
 63I67OfvLBYqmG/QVM+9biB9iZacJxnM0+nlxQ3hFDjll26iKyKlJ1V1WoTOtwms7M
 VknyZEf4M6YS+XcJrDB5mTfIxSa18wDFZB3pH/0nwVZLq7Pt3Eoh+MOO1l8oImhEQe
 hBB1HWBwlaEjaD6xUIrcgWJ4/D5WGeSn5aZaRQv2+7ClSABGlVwb7Ftba3xfcjr6U+
 NoPUajk0d/2YQ==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 3D82D100046;
 Fri,  5 Apr 2024 18:15:28 -0400 (EDT)
Received: from lechazo (lechon.iro.umontreal.ca [132.204.27.242])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 19C61120667;
 Fri,  5 Apr 2024 18:15:28 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: 70077 <at> debbugs.gnu.org
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvle615806.fsf@HIDDEN> (Stefan Monnier's message of
 "Fri, 29 Mar 2024 12:15:53 -0400")
Message-ID: <jwv7chba2wu.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
Date: Fri, 05 Apr 2024 18:12:55 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.105 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See
 http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more
 information. [gnu.org]
X-SPAM-LEVEL: 
X-Spam-Score: -2.3 (--)
X-Debbugs-Envelope-To: 70077
Cc: Alan Mackenzie <acm@HIDDEN>, Ihor Radchenko <yantar92@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -3.3 (---)

--=-=-=
Content-Type: text/plain

My PoC has matured into something quite usable.  The maturing part
included making it robust against mismatched or missing
`*-change-functions` calls, which it tries to detect (the problem
is then passed on to the client in the form of a change with an
unknown "before" content).
I also wrote a test which makes random changes and verifies that
the clients receive correct descriptions.

I currently use it only for Eglot and diff-mode, but I'm quite happy
with it.  For Eglot, it nicely packs up consecutive changes (like
consecutive `self-insert-command`s) into a single change yet keeps
changes to different parts of the buffer nicely separate.

The API is still about the same as before except that
`track-changes-register` now takes 3 options:

- `immediate` to control when the presence of changes is signaled
  (default to use `funcall-later` but `immediate` makes it use `funcall`
  so there is no delay).
- `disjoint` to prevent changes to different parts of the buffer from
  being combined into too large a change change.
- `nobefore` which indicates that the client doesn't actually need the
  `before` contents, so will only get the length thereof (like
  `after-change-functions` does).

I'm proposing we include it into `master`.
I have pushed the patch to `scratch/track-changes` (and attached it below).


        Stefan

--=-=-=
Content-Type: text/x-diff; charset=iso-8859-1
Content-Disposition: inline;
 filename=0001-lisp-emacs-lisp-track-changes.el-New-file.patch
Content-Transfer-Encoding: quoted-printable

From 4fd5a97052472eb1c332ea9b3f9ff90e94ad0cd1 Mon Sep 17 00:00:00 2001
From: Stefan Monnier <monnier@HIDDEN>
Date: Fri, 5 Apr 2024 17:37:32 -0400
Subject: [PATCH] lisp/emacs-lisp/track-changes.el: New file

This new package provides an API that is easier to use right than
our `*-change-functions` hooks.

The patch includes changes to `diff-mode.el` and `eglot.el` to
make use of this new package.

* lisp/emacs-lisp/track-changes.el: New file.
* test/lisp/emacs-lisp/track-changes-tests.el: New file.

* lisp/progmodes/eglot.el: Require `track-changes`.
(eglot--virtual-pos-to-lsp-position): New function.
(eglot--track-changes): New var.
(eglot--managed-mode): Use `track-changes-register` i.s.o
`after/before-change-functions`.
(eglot--before-change): Delete function.
(eglot--track-changes-signal): Rename from `eglot--after-change`
and adjust arguments accordingly.
(eglot--track-changes-fetch): New function.
(eglot--signal-textDocument/didChange): Call it and simplify now that
corner-cases are handled by `track-changes`.

* lisp/vc/diff-mode.el: Require `track-changes`.
Also require `easy-mmode` before the `eval-when-compile`s.
(diff-unhandled-changes): Delete variable.
(diff-after-change-function): Delete function.
(diff--track-changes-function): Rename from `diff-post-command-hook`
and adjust to new calling convention.
(diff--track-changes): New variable.
(diff--track-changes-signal): New function.
(diff-mode, diff-minor-mode): Use it with `track-changes-register`.
---
 lisp/emacs-lisp/track-changes.el            | 605 ++++++++++++++++++++
 lisp/progmodes/eglot.el                     | 105 ++--
 lisp/vc/diff-mode.el                        | 107 ++--
 test/lisp/emacs-lisp/track-changes-tests.el | 149 +++++
 4 files changed, 852 insertions(+), 114 deletions(-)
 create mode 100644 lisp/emacs-lisp/track-changes.el
 create mode 100644 test/lisp/emacs-lisp/track-changes-tests.el

diff --git a/lisp/emacs-lisp/track-changes.el b/lisp/emacs-lisp/track-chang=
es.el
new file mode 100644
index 00000000000..7644a7de98d
--- /dev/null
+++ b/lisp/emacs-lisp/track-changes.el
@@ -0,0 +1,605 @@
+;;; track-changes.el --- API to react to buffer modifications  -*- lexical=
-binding: t; -*-
+
+;; Copyright (C) 2024  Free Software Foundation, Inc.
+
+;; Author: Stefan Monnier <monnier@HIDDEN>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library is a layer of abstraction above `before-change-functions'
+;; and `after-change-functions' which takes care of accumulating changes
+;; until a time when its client finds it convenient to react to them.
+;;
+;; It provides an API that is easier to use correctly than our
+;; `*-change-functions` hooks.  Problems that it claims to solve:
+;;
+;; - Before and after calls are not necessarily paired.
+;; - The beg/end values don't always match.
+;; - There's usually only one call to the hooks per command but
+;;   there can be thousands of calls from within a single command,
+;;   so naive users will tend to write code that performs poorly
+;;   in those rare cases.
+;; - The hooks are run at a fairly low-level so there are things they
+;;   really shouldn't do, such as modify the buffer or wait.
+;; - The after call doesn't get enough info to rebuild the before-change s=
tate,
+;;   so some callers need to use both before-c-f and after-c-f (and then
+;;   deal with the first two points above).
+;;
+;; The new API is almost like `after-change-functions` except that:
+;; - It provides the "before string" (i.e. the previous content of
+;;   the changed area) rather than only its length.
+;; - It can combine several changes into larger ones.
+;; - Clients do not have to process changes right away, instead they
+;;   can let changes accumulate (by combining them into a larger change)
+;;   until it is convenient for them to process them.
+;; - By default, changes are signaled at most once per command.
+
+;; The API consists in the following functions:
+;;
+;;     (track-changes-register SIGNAL &key NOBEFORE DISJOINT IMMEDIATE)
+;;     (track-changes-fetch ID FUNC)
+;;     (track-changes-unregister ID)
+;;
+;; A typical use case might look like:
+;;
+;;     (defvar my-foo--change-tracker nil)
+;;     (define-minor-mode my-foo-mode
+;;       "Fooing like there's no tomorrow."
+;;       (if (null my-foo-mode)
+;;           (when my-foo--change-tracker
+;;             (track-changes-unregister my-foo--change-tracker)
+;;             (setq my-foo--change-tracker nil))
+;;         (unless my-foo--change-tracker
+;;           (setq my-foo--change-tracker
+;;                 (track-changes-register
+;;                  (lambda (id)
+;;                    (track-changes-fetch
+;;                     id (lambda (beg end before)
+;;                          ..DO THE THING..))))))))
+
+;;; Code:
+
+(require 'cl-lib)
+
+(unless (fboundp 'funcall-later)
+  (defun funcall-later (&rest args)
+    ;; FIXME: Not sure if `run-with-timer' preserves ordering between
+    ;; different calls with the same target time.
+   (apply #'run-with-timer 0 nil args)))
+
+;;;; Internal types and variables.
+
+(cl-defstruct (track-changes--tracker
+               (:noinline t)
+               (:constructor nil)
+               (:constructor track-changes--tracker ( signal state
+                                                      &optional
+                                                      nobefore immediate)))
+  signal state nobefore immediate)
+
+(cl-defstruct (track-changes--state
+               (:noinline t)
+               (:constructor nil)
+               (:constructor track-changes--state ()))
+  "Object holding a description of a buffer state.
+BEG..END is the area that was changed and BEFORE is its previous content.
+If the current buffer currently holds the content of the next state, you c=
an get
+the contents of the previous state with:
+
+    (concat (buffer-substring (point-min) beg)
+                before
+                (buffer-substring end (point-max)))
+
+NEXT is the next state object (i.e. a more recent state).
+If NEXT is nil it means it's most recent state and it may be incomplete
+\(BEG/END/BEFORE may be nil), in which case those fields will take their
+values from `track-changes--before-(beg|end|before)' when the next
+state is create."
+  (beg (point-max))
+  (end (point-min))
+  (before nil)
+  (next nil))
+
+(defvar-local track-changes--trackers ()
+  "List of trackers currently registered in the current buffer.")
+(defvar-local track-changes--clean-trackers ()
+  "List of trackers that are clean.
+Those are the trackers that get signaled when a change is made.")
+
+(defvar-local track-changes--disjoint-trackers ()
+ "List of trackers that want to react to disjoint changes.
+These trackers' are signaled every time track-changes notices
+that some upcoming changes touch another \"distant\" part of the buffer.")
+
+(defvar-local track-changes--state nil)
+
+;; `track-changes--before-*' keep track of the content of the
+;; buffer when `track-changes--state' was cleaned.
+(defvar-local track-changes--before-beg (point-min)
+  "Beginning position of the remembered \"before string\".")
+(defvar-local track-changes--before-end (point-min)
+  "End position of the text replacing the \"before string\".")
+(defvar-local track-changes--before-string ""
+  "String holding some contents of the buffer before the current change.
+This string is supposed to cover all the already modified areas plus
+the upcoming modifications announced via `before-change-functions'.
+If all trackers are `nobefore', then this holds the `buffer-size' before
+the current change.")
+(defvar-local track-changes--before-no t
+  "If non-nil, all the trackers are `nobefore'.
+Should be equal to (memq #\\=3D'track-changes--before before-change-functi=
ons).")
+
+(defvar-local track-changes--before-clean 'unset
+  "If non-nil, the `track-changes--before-*' vars are old.
+More specifically it means they cover a part of the buffer relevant
+for the previous state.
+It can take two non-nil values:
+- `unset': Means that the vars cover some older state.
+  This is what it is set right after creating a fresh new state.
+- `set': Means the vars reflect the current buffer state.
+  This is what it is set to after the first `before-change-functions'
+  but before an `after-change-functions'.")
+
+(defvar-local track-changes--buffer-size nil
+  "Current size of the buffer, as far as this library knows.
+This is used to try and detect cases where buffer modifications are \"lost=
\".")
+
+;;;; Exposed API.
+
+(cl-defun track-changes-register ( signal &key nobefore disjoint immediate)
+  "Register a new tracker and return a new tracker ID.
+SIGNAL is a function that will be called with one argument (the tracker ID)
+after the current buffer is modified, so that we can react to the change.
+Once called, SIGNAL is not called again until `track-changes-fetch'
+is called with the corresponding tracker ID.
+
+If optional argument NOBEFORE is non-nil, it means that this tracker does
+not need the BEFORE strings (it will receive their size instead).
+
+By default SIGNAL is called as soon as convenient after a change, which is
+usually right after the end of the current command.
+If optional argument IMMEDIATE is non-nil it means SIGNAL should be called
+as soon as a change is detected,
+BEWARE: In that case SIGNAL is called directly from `after-change-function=
s'
+and should thus be extra careful: don't modify the buffer, don't call a fu=
nction
+that may block, do as little work as possible, ...
+When IMMEDIATE is non-nil, the SIGNAL should preferably not always call
+`track-changes-fetch', since that would defeat the purpose of this library.
+
+If optional argument DISJOINT is non-nil, SIGNAL is called every time we a=
re
+about to combine changes from \"distant\" parts of the buffer.
+This is needed when combining disjoint changes into one bigger change
+is unacceptable, typically for performance reasons.
+These calls are distinguished from normal calls by calling SIGNAL with
+a second argument which is the distance between the upcoming change and
+the previous changes.
+BEWARE: In that case SIGNAL is called directly from `before-change-functio=
ns'
+and should thus be extra careful: don't modify the buffer, don't call a fu=
nction
+that may block, ...
+In order to prevent the upcoming change from being combined with the previ=
ous
+changes, SIGNAL needs to call `track-changes-fetch' before it returns."
+  (when (and nobefore disjoint)
+    ;; FIXME: Without `before-change-functions', we can only discover
+    ;; a disjoint change after the fact, which is not good enough.
+    ;; But we could use  stripped down before-change-function,
+    (error "`disjoint' not supported for `nobefore' trackers"))
+  (track-changes--clean-state)
+  (unless nobefore
+    (setq track-changes--before-no nil)
+    (add-hook 'before-change-functions #'track-changes--before nil t))
+  (add-hook 'after-change-functions  #'track-changes--after  nil t)
+  (let ((tracker (track-changes--tracker signal track-changes--state
+                                         nobefore immediate)))
+    (push tracker track-changes--trackers)
+    (push tracker track-changes--clean-trackers)
+    (when disjoint
+      (push tracker track-changes--disjoint-trackers))
+    tracker))
+
+(defun track-changes-unregister (id)
+  "Remove the tracker denoted by ID.
+Trackers can consume resources (especially if `track-changes-fetch' is
+not called), so it is good practice to unregister them when you don't
+need them any more."
+  (unless (memq id track-changes--trackers)
+    (error "Unregistering a non-registered tracker: %S" id))
+  (setq track-changes--trackers (delq id track-changes--trackers))
+  (setq track-changes--clean-trackers (delq id track-changes--clean-tracke=
rs))
+  (setq track-changes--disjoint-trackers
+        (delq id track-changes--disjoint-trackers))
+  (when (cl-every #'track-changes--tracker-nobefore track-changes--tracker=
s)
+    (setq track-changes--before-no t)
+    (remove-hook 'before-change-functions #'track-changes--before t))
+  (when (null track-changes--trackers)
+    (mapc #'kill-local-variable
+          '(track-changes--before-beg
+            track-changes--before-end
+            track-changes--before-string
+            track-changes--buffer-size
+            track-changes--before-clean
+            track-changes--state))
+    (remove-hook 'after-change-functions  #'track-changes--after  t)))
+
+(defun track-changes-fetch (id func)
+  "Fetch the pending changes.
+ID is the tracker ID returned by a previous `track-changes-register'.
+FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
+where BEGIN..END delimit the region that was changed since the last
+time `track-changes-fetch' was called and BEFORE is a string containing
+the previous content of that region (or just its length as an integer
+If the tracker ID was registered with the `nobefore' option).
+If some error caused us to miss some changes, then BEFORE will be the
+symbol `error' to indicate that the buffer got out of sync.
+This reflects a bug somewhere, so please report it when it happens.
+
+If no changes occurred since the last time, FUNC is not called and
+we return nil, otherwise we return the value returned by FUNC,
+and re-enable the TRACKER corresponding to ID."
+  (cl-assert (memq id track-changes--trackers))
+  (unless (equal track-changes--buffer-size (buffer-size))
+    (track-changes--recover-from-error))
+  (let ((beg nil)
+        (end nil)
+        (before t)
+        (lenbefore 0)
+        (states ()))
+    ;; Transfer the data from `track-changes--before-string'
+    ;; to the tracker's state object, if needed.
+    (track-changes--clean-state)
+    ;; We want to combine the states from most recent to oldest,
+    ;; so reverse them.
+    (let ((state (track-changes--tracker-state id)))
+      (while state
+        (push state states)
+        (setq state (track-changes--state-next state))))
+
+    (cond
+     ((eq (car states) track-changes--state)
+      (cl-assert (null (track-changes--state-before (car states))))
+      (setq states (cdr states)))
+     (t
+      ;; The states are disconnected from the latest state because
+      ;; we got out of sync!
+      (cl-assert (eq (track-changes--state-before (car states)) 'error))
+      (setq beg (point-min))
+      (setq end (point-max))
+      (setq before 'error)
+      (setq states nil)))
+
+    (dolist (state states)
+      (let ((prevbeg (track-changes--state-beg state))
+            (prevend (track-changes--state-end state))
+            (prevbefore (track-changes--state-before state)))
+        (if (eq before t)
+            (progn
+              ;; This is the most recent change.  Just initialize the vars.
+              (setq beg prevbeg)
+              (setq end prevend)
+              (setq lenbefore
+                    (if (stringp prevbefore) (length prevbefore) prevbefor=
e))
+              (setq before
+                    (unless (track-changes--tracker-nobefore id) prevbefor=
e)))
+          (let ((endb (+ beg lenbefore)))
+            (when (< prevbeg beg)
+              (if (not before)
+                  (setq lenbefore (+ (- beg prevbeg) lenbefore))
+                (setq before
+                      (concat (buffer-substring-no-properties
+                               prevbeg beg)
+                              before))
+                (setq lenbefore (length before)))
+              (setq beg prevbeg)
+              (cl-assert (=3D endb (+ beg lenbefore))))
+            (when (< endb prevend)
+              (let ((new-end (+ end (- prevend endb))))
+                (if (not before)
+                    (setq lenbefore (+ lenbefore (- new-end end)))
+                  (setq before
+                        (concat before
+                                (buffer-substring-no-properties
+                                 end new-end)))
+                  (setq lenbefore (length before)))
+                (setq end new-end)
+                (cl-assert (=3D prevend (+ beg lenbefore)))
+                (setq endb (+ beg lenbefore))))
+            (cl-assert (<=3D beg prevbeg prevend endb))
+            ;; The `prevbefore' is covered by the new one.
+            (if (not before)
+                (setq lenbefore
+                      (+ (- prevbeg beg)
+                         (if (stringp prevbefore)
+                             (length prevbefore) prevbefore)
+                         (- endb prevend)))
+              (setq before
+                    (concat (substring before 0 (- prevbeg beg))
+                            prevbefore
+                            (substring before (- (length before)
+                                                 (- endb prevend)))))
+              (setq lenbefore (length before)))))))
+    (if (null beg)
+        (progn
+          (cl-assert (null states))
+          (cl-assert (memq id track-changes--clean-trackers))
+          (cl-assert (eq (track-changes--tracker-state id)
+                         track-changes--state))
+          ;; Nothing to do.
+          nil)
+      (cl-assert (<=3D (point-min) beg end (point-max)))
+      ;; Update the tracker's state *before* running `func' so we don't ri=
sk
+      ;; mistakenly replaying the changes in case `func' exits non-locally.
+      (setf (track-changes--tracker-state id) track-changes--state)
+      (unwind-protect (funcall func beg end (or before lenbefore))
+        ;; Re-enable the tracker's signal only after running `func', so
+        ;; as to avoid recursive invocations.
+        (cl-pushnew id track-changes--clean-trackers)))))
+
+;;;; Auxiliary functions.
+
+(defun track-changes--clean-state ()
+  (cond
+   ((null track-changes--state)
+    (cl-assert track-changes--before-clean)
+    (cl-assert (null track-changes--buffer-size))
+    ;; No state has been created yet.  Do it now.
+    (setq track-changes--buffer-size (buffer-size))
+    (when track-changes--before-no
+      (setq track-changes--before-string (buffer-size)))
+    (setq track-changes--state (track-changes--state)))
+   (track-changes--before-clean nil)
+   (t
+    (cl-assert (<=3D (track-changes--state-beg track-changes--state)
+                   (track-changes--state-end track-changes--state)))
+    (let ((actual-beg (track-changes--state-beg track-changes--state))
+          (actual-end (track-changes--state-end track-changes--state)))
+      (if track-changes--before-no
+          (progn
+            (cl-assert (integerp track-changes--before-string))
+            (setf (track-changes--state-before track-changes--state)
+                  (- track-changes--before-string
+                     (- (buffer-size) (- actual-end actual-beg))))
+            (setq track-changes--before-string (buffer-size)))
+        (cl-assert (<=3D track-changes--before-beg
+                       actual-beg actual-end
+                       track-changes--before-end))
+        (cl-assert (null (track-changes--state-before track-changes--state=
)))
+        ;; The `track-changes--before-*' vars can cover more text than the
+        ;; actually modified area, so trim it down now to the relevant par=
t.
+        (unless (=3D (- track-changes--before-end track-changes--before-be=
g)
+                   (- actual-end actual-beg))
+          (setq track-changes--before-string
+                (substring track-changes--before-string
+                           (- actual-beg track-changes--before-beg)
+                           (- (length track-changes--before-string)
+                              (- track-changes--before-end actual-end))))
+          (setq track-changes--before-beg actual-beg)
+          (setq track-changes--before-end actual-end))
+        (setf (track-changes--state-before track-changes--state)
+              track-changes--before-string)))
+    ;; Note: We preserve `track-changes--before-*' because they may still
+    ;; be needed, in case `after-change-functions' are run before the next
+    ;; `before-change-functions'.
+    ;; Instead, we set `track-changes--before-clean' to `unset' to mean th=
at
+    ;; `track-changes--before-*' can be reset at the next
+    ;; `before-change-functions'.
+    (setq track-changes--before-clean 'unset)
+    (let ((new (track-changes--state)))
+      (setf (track-changes--state-next track-changes--state) new)
+      (setq track-changes--state new)))))
+
+(defvar track-changes--disjoint-threshold 100
+  "Distance below which changes are not considered disjoint.")
+
+(defvar track-changes--error-log ()
+  "List of errors encountered.
+Each element is a triplet (BUFFER-NAME BACKTRACE RECENT-KEYS).")
+
+(defun track-changes--recover-from-error ()
+  ;; We somehow got out of sync.  This is usually the result of a bug
+  ;; elsewhere that causes the before-c-f and after-c-f to be improperly
+  ;; paired, or to be skipped altogether.
+  ;; Not much we can do, other than force a full re-synchronization.
+  (warn "Missing/incorrect calls to `before/after-change-functions'!!
+Details logged to `track-changes--error-log'")
+  (push (list (buffer-name)
+              (backtrace-frames 'track-changes--recover-from-error)
+              (recent-keys 'include-cmds))
+        track-changes--error-log)
+  (setq track-changes--before-clean 'unset)
+  (setq track-changes--buffer-size (buffer-size))
+  ;; Create a new state disconnected from the previous ones!
+  ;; Mark the previous one as junk, just to be clear.
+  (setf (track-changes--state-before track-changes--state) 'error)
+  (setq track-changes--state (track-changes--state)))
+
+(defun track-changes--before (beg end)
+  (cl-assert track-changes--state)
+  (cl-assert (<=3D beg end))
+  (let* ((size (- end beg))
+         (reset (lambda ()
+                  (cl-assert track-changes--before-clean)
+                  (setq track-changes--before-clean 'set)
+                  (setf track-changes--before-string
+                        (buffer-substring-no-properties beg end))
+                  (setf track-changes--before-beg beg)
+                  (setf track-changes--before-end end)))
+
+         (signal-if-disjoint
+          (lambda (pos1 pos2)
+            (let ((distance (- pos2 pos1)))
+              (when (> distance
+                       (max track-changes--disjoint-threshold
+                            ;; If the distance is smaller than the size of=
 the
+                            ;; current change, then we may as well conside=
r it
+                            ;; as "near".
+                            (length track-changes--before-string)
+                            size
+                            (- track-changes--before-end
+                               track-changes--before-beg)))
+                (dolist (tracker track-changes--disjoint-trackers)
+                  (funcall (track-changes--tracker-signal tracker)
+                           tracker distance))
+                ;; Return non-nil if the state was cleaned along the way.
+                track-changes--before-clean)))))
+
+    (if track-changes--before-clean
+        (progn
+          ;; Detect disjointness with previous changes here as well,
+          ;; so that if a client calls `track-changes-fetch' all the time,
+          ;; it doesn't prevent others from getting a disjointness signal.
+          (when (and track-changes--before-beg
+                     (let ((found nil))
+                       (dolist (tracker track-changes--disjoint-trackers)
+                         (unless (memq tracker track-changes--clean-tracke=
rs)
+                           (setq found t)))
+                       found))
+            ;; There's at least one `tracker' that wants to know about dis=
joint
+            ;; changes *and* it has unseen pending changes.
+            ;; FIXME: This can occasionally signal a tracker that's clean.
+            (if (< beg track-changes--before-beg)
+                (funcall signal-if-disjoint end track-changes--before-beg)
+              (funcall signal-if-disjoint track-changes--before-end beg)))
+          (funcall reset))
+      (cl-assert (save-restriction
+                   (widen)
+                   (<=3D (point-min)
+                       track-changes--before-beg
+                       track-changes--before-end
+                       (point-max))))
+      (when (< beg track-changes--before-beg)
+        (if (and track-changes--disjoint-trackers
+                 (funcall signal-if-disjoint end track-changes--before-beg=
))
+            (funcall reset)
+          (let* ((old-bbeg track-changes--before-beg)
+                 ;; To avoid O(N=B2) behavior when faced with many small c=
hanges,
+                 ;; we copy more than needed.
+                 (new-bbeg (min (max (point-min)
+                                     (- old-bbeg
+                                        (length track-changes--before-stri=
ng)))
+                                beg)))
+            (setf track-changes--before-beg new-bbeg)
+            (cl-callf (lambda (old new) (concat new old))
+                track-changes--before-string
+              (buffer-substring-no-properties new-bbeg old-bbeg)))))
+
+      (when (< track-changes--before-end end)
+        (if (and track-changes--disjoint-trackers
+                 (funcall signal-if-disjoint track-changes--before-end beg=
))
+            (funcall reset)
+          (let* ((old-bend track-changes--before-end)
+                 ;; To avoid O(N=B2) behavior when faced with many small c=
hanges,
+                 ;; we copy more than needed.
+                 (new-bend (max (min (point-max)
+                                     (+ old-bend
+                                        (length track-changes--before-stri=
ng)))
+                                end)))
+            (setf track-changes--before-end new-bend)
+            (cl-callf concat track-changes--before-string
+              (buffer-substring-no-properties old-bend new-bend))))))))
+
+(defun track-changes--after (beg end len)
+  (cl-assert track-changes--state)
+  (and (eq track-changes--before-clean 'unset)
+       (not track-changes--before-no)
+       ;; This can be a sign that a `before-change-functions' went missing,
+       ;; or that we called `track-changes--clean-state' between
+       ;; a `before-change-functions' and `after-change-functions'.
+       (track-changes--before beg end))
+  (setq track-changes--before-clean nil)
+  (let ((offset (- (- end beg) len)))
+    (cl-incf track-changes--before-end offset)
+    (cl-incf track-changes--buffer-size offset)
+    (if (not (or track-changes--before-no
+                 (save-restriction
+                   (widen)
+                   (<=3D (point-min)
+                       track-changes--before-beg
+                       beg end
+                       track-changes--before-end
+                       (point-max)))))
+        ;; BEG..END is not covered by previous `before-change-functions'!!
+        (track-changes--recover-from-error)
+      ;; Note the new changes.
+      (when (< beg (track-changes--state-beg track-changes--state))
+        (setf (track-changes--state-beg track-changes--state) beg))
+      (cl-callf (lambda (old-end) (max end (+ old-end offset)))
+          (track-changes--state-end track-changes--state))
+      (cl-assert (or track-changes--before-no
+                     (<=3D track-changes--before-beg
+                         (track-changes--state-beg track-changes--state)
+                         beg end
+                         (track-changes--state-end track-changes--state)
+                         track-changes--before-end)))))
+  (while track-changes--clean-trackers
+    (let ((tracker (pop track-changes--clean-trackers)))
+      (if (track-changes--tracker-immediate tracker)
+          (funcall (track-changes--tracker-signal tracker) tracker)
+        (funcall-later #'track-changes--call-signal
+                       (current-buffer) tracker)))))
+
+(defun track-changes--call-signal (buf tracker)
+  (when (buffer-live-p buf)
+    (with-current-buffer buf
+      ;; Silence ourselves if `track-changes-fetch' was called in the mean=
 time.
+      (unless (memq tracker track-changes--clean-trackers)
+        (funcall (track-changes--tracker-signal tracker) tracker)))))
+
+;;;; Extra candidates for the API.
+
+;; This could be a good alternative to using a temp-buffer like I used in
+;; Eglot, since presumably we've just been changing this very area of the
+;; buffer, so the gap should be ready nearby,
+;; It may seem silly to go back to the previous state, since we could have
+;; used `before-change-functions' to run FUNC right then when we were in
+;; that state.  The advantage is that with track-changes we get to decide
+;; retroactively which state is the one for which we want to call FUNC and
+;; which BEG..END to use: when that state was current we may have known
+;; then that it would be "the one" but we didn't know what BEG and END
+;; should be because those depend on the changes that came afterwards.
+(defun track-changes--in-revert (beg end before func)
+  "Call FUNC with the buffer contents temporarily reverted to BEFORE.
+FUNC is called with no arguments and with point right after BEFORE.
+FUNC is not allowed to modify the buffer and it should refrain from using
+operations that use a cache populated from the buffer's content,
+such as `syntax-ppss'."
+  (catch 'track-changes--exit
+    (with-silent-modifications ;; This has to be outside `atomic-change-gr=
oup'.
+      (atomic-change-group
+        (goto-char end)
+        (insert-before-markers before)
+        (delete-region beg end)
+        (throw 'track-changes--exit
+               (let ((inhibit-read-only nil)
+                     (buffer-read-only t))
+                 (funcall func)))))))
+
+(defun track-changes--reset (id)
+  "Mark all past changes as handled for tracker ID.
+Does not re-enable ID's signal."
+  (track-changes--clean-state)
+  (setf (track-changes--tracker-state id) track-changes--state))
+
+(defun track-changes--pending-p (id)
+  "Return non-nil if there are pending changes for tracker ID."
+  (not (memq id track-changes--clean-trackers)))
+
+(defmacro with--track-changes (id vars &rest body)
+  (declare (indent 2) (debug (form sexp body)))
+  `(track-changes-fetch ,id (lambda ,vars ,@body)))
+
+(provide 'track-changes)
+;;; track-changes.el end here.
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 7f4284bf09d..00c09d7f06b 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -110,6 +110,7 @@
 (require 'text-property-search nil t)
 (require 'diff-mode)
 (require 'diff)
+(require 'track-changes)
=20
 ;; These dependencies are also GNU ELPA core packages.  Because of
 ;; bug#62576, since there is a risk that M-x package-install, despite
@@ -1732,6 +1733,9 @@ eglot-utf-16-linepos
   "Calculate number of UTF-16 code units from position given by LBP.
 LBP defaults to `eglot--bol'."
   (/ (- (length (encode-coding-region (or lbp (eglot--bol))
+                                      ;; FIXME: How could `point' ever be
+                                      ;; larger than `point-max' (sounds l=
ike
+                                      ;; a bug in Emacs).
                                       ;; Fix github#860
                                       (min (point) (point-max)) 'utf-16 t))
         2)
@@ -1749,6 +1753,24 @@ eglot--pos-to-lsp-position
          :character (progn (when pos (goto-char pos))
                            (funcall eglot-current-linepos-function)))))
=20
+(defun eglot--virtual-pos-to-lsp-position (pos string)
+  "Return the LSP position at the end of STRING if it were inserted at POS=
."
+  (eglot--widening
+   (goto-char pos)
+   (forward-line 0)
+   ;; LSP line is zero-origin; Emacs is one-origin.
+   (let ((posline (1- (line-number-at-pos nil t)))
+         (linebeg (buffer-substring (point) pos))
+         (colfun eglot-current-linepos-function))
+     ;; Use a temp buffer because:
+     ;; - I don't know of a fast way to count newlines in a string.
+     ;; - We currently don't have `eglot-current-linepos-function' for str=
ings.
+     (with-temp-buffer
+       (insert linebeg string)
+       (goto-char (point-max))
+       (list :line (+ posline (1- (line-number-at-pos nil t)))
+             :character (funcall colfun))))))
+
 (defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
   "Function to move to a position within a line reported by the LSP server.
=20
@@ -1946,6 +1968,8 @@ eglot-managed-mode-hook
   "A hook run by Eglot after it started/stopped managing a buffer.
 Use `eglot-managed-p' to determine if current buffer is managed.")
=20
+(defvar-local eglot--track-changes nil)
+
 (define-minor-mode eglot--managed-mode
   "Mode for source buffers managed by some Eglot project."
   :init-value nil :lighter nil :keymap eglot-mode-map
@@ -1959,8 +1983,9 @@ eglot--managed-mode
       ("utf-8"
        (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-li=
nepos)
        (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-=
utf-8-linepos)))
-    (add-hook 'after-change-functions #'eglot--after-change nil t)
-    (add-hook 'before-change-functions #'eglot--before-change nil t)
+    (unless eglot--track-changes
+      (setq eglot--track-changes
+            (track-changes-register #'eglot--track-changes-signal :disjoin=
t t)))
     (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
     ;; Prepend "didClose" to the hook after the "nonoff", so it will run f=
irst
     (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil =
t)
@@ -1998,8 +2023,9 @@ eglot--managed-mode
                buffer
                (eglot--managed-buffers (eglot-current-server)))))
    (t
-    (remove-hook 'after-change-functions #'eglot--after-change t)
-    (remove-hook 'before-change-functions #'eglot--before-change t)
+    (when eglot--track-changes
+      (track-changes-unregister eglot--track-changes)
+      (setq eglot--track-changes nil))
     (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
     (remove-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose t)
     (remove-hook 'before-revert-hook #'eglot--signal-textDocument/didClose=
 t)
@@ -2568,54 +2594,29 @@ jsonrpc-connection-ready-p
=20
 (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signa=
ls.")
=20
-(defun eglot--before-change (beg end)
-  "Hook onto `before-change-functions' with BEG and END."
-  (when (listp eglot--recent-changes)
-    ;; Records BEG and END, crucially convert them into LSP
-    ;; (line/char) positions before that information is lost (because
-    ;; the after-change thingy doesn't know if newlines were
-    ;; deleted/added).  Also record markers of BEG and END
-    ;; (github#259)
-    (push `(,(eglot--pos-to-lsp-position beg)
-            ,(eglot--pos-to-lsp-position end)
-            (,beg . ,(copy-marker beg nil))
-            (,end . ,(copy-marker end t)))
-          eglot--recent-changes)))
-
 (defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChang=
e)
   "Internal hook for doing things when the document changes.")
=20
-(defun eglot--after-change (beg end pre-change-length)
-  "Hook onto `after-change-functions'.
-Records BEG, END and PRE-CHANGE-LENGTH locally."
+(defun eglot--track-changes-fetch (id)
+  (if (eq eglot--recent-changes 'pending) (setq eglot--recent-changes nil))
+  (track-changes-fetch
+   id (lambda (beg end before)
+        (if (stringp before)
+            (push `(,(eglot--pos-to-lsp-position beg)
+                    ,(eglot--virtual-pos-to-lsp-position beg before)
+                    ,(length before)
+                    ,(buffer-substring-no-properties beg end))
+                  eglot--recent-changes)
+          (setf eglot--recent-changes :emacs-messup)))))
+
+(defun eglot--track-changes-signal (id &optional distance)
   (cl-incf eglot--versioned-identifier)
-  (pcase (car-safe eglot--recent-changes)
-    (`(,lsp-beg ,lsp-end
-                (,b-beg . ,b-beg-marker)
-                (,b-end . ,b-end-marker))
-     ;; github#259 and github#367: with `capitalize-word' & friends,
-     ;; `before-change-functions' records the whole word's `b-beg' and
-     ;; `b-end'.  Similarly, when `fill-paragraph' coalesces two
-     ;; lines, `b-beg' and `b-end' mark end of first line and end of
-     ;; second line, resp.  In both situations, `beg' and `end'
-     ;; received here seemingly contradict that: they will differ by 1
-     ;; and encompass the capitalized character or, in the coalescing
-     ;; case, the replacement of the newline with a space.  We keep
-     ;; both markers and positions to detect and correct this.  In
-     ;; this specific case, we ignore `beg', `len' and
-     ;; `pre-change-len' and send richer information about the region
-     ;; from the markers.  I've also experimented with doing this
-     ;; unconditionally but it seems to break when newlines are added.
-     (if (and (=3D b-end b-end-marker) (=3D b-beg b-beg-marker)
-              (or (/=3D beg b-beg) (/=3D end b-end)))
-         (setcar eglot--recent-changes
-                 `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker)
-                            ,(buffer-substring-no-properties b-beg-marker
-                                                             b-end-marker)=
))
-       (setcar eglot--recent-changes
-               `(,lsp-beg ,lsp-end ,pre-change-length
-                          ,(buffer-substring-no-properties beg end)))))
-    (_ (setf eglot--recent-changes :emacs-messup)))
+  (cond
+   (distance (eglot--track-changes-fetch id))
+   (eglot--recent-changes nil)
+   ;; Note that there are pending changes, for the benefit of those
+   ;; who check it as a boolean.
+   (t (setq eglot--recent-changes 'pending)))
   (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
   (let ((buf (current-buffer)))
     (setq eglot--change-idle-timer
@@ -2729,6 +2730,7 @@ eglot-handle-request
 (defun eglot--signal-textDocument/didChange ()
   "Send textDocument/didChange to server."
   (when eglot--recent-changes
+    (eglot--track-changes-fetch eglot--track-changes)
     (let* ((server (eglot--current-server-or-lose))
            (sync-capability (eglot-server-capable :textDocumentSync))
            (sync-kind (if (numberp sync-capability) sync-capability
@@ -2745,13 +2747,8 @@ eglot--signal-textDocument/didChange
                               (buffer-substring-no-properties (point-min)
                                                               (point-max))=
)))
           (cl-loop for (beg end len text) in (reverse eglot--recent-change=
s)
-                   ;; github#259: `capitalize-word' and commands based
-                   ;; on `casify_region' will cause multiple duplicate
-                   ;; empty entries in `eglot--before-change' calls
-                   ;; without an `eglot--after-change' reciprocal.
-                   ;; Weed them out here.
-                   when (numberp len)
                    vconcat `[,(list :range `(:start ,beg :end ,end)
+                                    ;; `rangeLength' is obsolete.
                                     :rangeLength len :text text)]))))
       (setq eglot--recent-changes nil)
       (jsonrpc--call-deferred server))))
diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el
index 66043059d14..e7ac517b72f 100644
--- a/lisp/vc/diff-mode.el
+++ b/lisp/vc/diff-mode.el
@@ -53,9 +53,10 @@
 ;; - Handle `diff -b' output in context->unified.
=20
 ;;; Code:
+(require 'easy-mmode)
+(require 'track-changes)
 (eval-when-compile (require 'cl-lib))
 (eval-when-compile (require 'subr-x))
-(require 'easy-mmode)
=20
 (autoload 'vc-find-revision "vc")
 (autoload 'vc-find-revision-no-save "vc")
@@ -1431,56 +1432,40 @@ diff-write-contents-hooks
   (if (buffer-modified-p) (diff-fixup-modifs (point-min) (point-max)))
   nil)
=20
-;; It turns out that making changes in the buffer from within an
-;; *-change-function is asking for trouble, whereas making them
-;; from a post-command-hook doesn't pose much problems
-(defvar diff-unhandled-changes nil)
-(defun diff-after-change-function (beg end _len)
-  "Remember to fixup the hunk header.
-See `after-change-functions' for the meaning of BEG, END and LEN."
-  ;; Ignoring changes when inhibit-read-only is set is strictly speaking
-  ;; incorrect, but it turns out that inhibit-read-only is normally not set
-  ;; inside editing commands, while it tends to be set when the buffer gets
-  ;; updated by an async process or by a conversion function, both of which
-  ;; would rather not be uselessly slowed down by this hook.
-  (when (and (not undo-in-progress) (not inhibit-read-only))
-    (if diff-unhandled-changes
-	(setq diff-unhandled-changes
-	      (cons (min beg (car diff-unhandled-changes))
-		    (max end (cdr diff-unhandled-changes))))
-      (setq diff-unhandled-changes (cons beg end)))))
-
-(defun diff-post-command-hook ()
-  "Fixup hunk headers if necessary."
-  (when (consp diff-unhandled-changes)
-    (ignore-errors
-      (save-excursion
-	(goto-char (car diff-unhandled-changes))
-	;; Maybe we've cut the end of the hunk before point.
-	(if (and (bolp) (not (bobp))) (backward-char 1))
-	;; We used to fixup modifs on all the changes, but it turns out that
-	;; it's safer not to do it on big changes, e.g. when yanking a big
-	;; diff, or when the user edits the header, since we might then
-	;; screw up perfectly correct values.  --Stef
-	(diff-beginning-of-hunk t)
-        (let* ((style (if (looking-at "\\*\\*\\*") 'context))
-               (start (line-beginning-position (if (eq style 'context) 3 2=
)))
-               (mid (if (eq style 'context)
-                        (save-excursion
-                          (re-search-forward diff-context-mid-hunk-header-=
re
-                                             nil t)))))
-          (when (and ;; Don't try to fixup changes in the hunk header.
-                 (>=3D (car diff-unhandled-changes) start)
-                 ;; Don't try to fixup changes in the mid-hunk header eith=
er.
-                 (or (not mid)
-                     (< (cdr diff-unhandled-changes) (match-beginning 0))
-                     (> (car diff-unhandled-changes) (match-end 0)))
-                 (save-excursion
-		(diff-end-of-hunk nil 'donttrustheader)
-                   ;; Don't try to fixup changes past the end of the hunk.
-                   (>=3D (point) (cdr diff-unhandled-changes))))
-	  (diff-fixup-modifs (point) (cdr diff-unhandled-changes)))))
-      (setq diff-unhandled-changes nil))))
+(defvar-local diff--track-changes nil)
+
+(defun diff--track-changes-signal (tracker)
+  (cl-assert (eq tracker diff--track-changes))
+  (track-changes-fetch tracker #'diff--track-changes-function))
+
+(defun diff--track-changes-function (beg end _before)
+  (with-demoted-errors "%S"
+    (save-excursion
+      (goto-char beg)
+      ;; Maybe we've cut the end of the hunk before point.
+      (if (and (bolp) (not (bobp))) (backward-char 1))
+      ;; We used to fixup modifs on all the changes, but it turns out that
+      ;; it's safer not to do it on big changes, e.g. when yanking a big
+      ;; diff, or when the user edits the header, since we might then
+      ;; screw up perfectly correct values.  --Stef
+      (diff-beginning-of-hunk t)
+      (let* ((style (if (looking-at "\\*\\*\\*") 'context))
+             (start (line-beginning-position (if (eq style 'context) 3 2)))
+             (mid (if (eq style 'context)
+                      (save-excursion
+                        (re-search-forward diff-context-mid-hunk-header-re
+                                           nil t)))))
+        (when (and ;; Don't try to fixup changes in the hunk header.
+               (>=3D beg start)
+               ;; Don't try to fixup changes in the mid-hunk header either.
+               (or (not mid)
+                   (< end (match-beginning 0))
+                   (> beg (match-end 0)))
+               (save-excursion
+		 (diff-end-of-hunk nil 'donttrustheader)
+                 ;; Don't try to fixup changes past the end of the hunk.
+                 (>=3D (point) end)))
+	 (diff-fixup-modifs (point) end))))))
=20
 (defun diff-next-error (arg reset)
   ;; Select a window that displays the current buffer so that point
@@ -1560,9 +1545,8 @@ diff-mode
   ;; setup change hooks
   (if (not diff-update-on-the-fly)
       (add-hook 'write-contents-functions #'diff-write-contents-hooks nil =
t)
-    (make-local-variable 'diff-unhandled-changes)
-    (add-hook 'after-change-functions #'diff-after-change-function nil t)
-    (add-hook 'post-command-hook #'diff-post-command-hook nil t))
+    (setq diff--track-changes
+          (track-changes-register #'diff--track-changes-signal :nobefore t=
)))
=20
   ;; add-log support
   (setq-local add-log-current-defun-function #'diff-current-defun)
@@ -1581,12 +1565,15 @@ diff-minor-mode
 \\{diff-minor-mode-map}"
   :group 'diff-mode :lighter " Diff"
   ;; FIXME: setup font-lock
-  ;; setup change hooks
-  (if (not diff-update-on-the-fly)
-      (add-hook 'write-contents-functions #'diff-write-contents-hooks nil =
t)
-    (make-local-variable 'diff-unhandled-changes)
-    (add-hook 'after-change-functions #'diff-after-change-function nil t)
-    (add-hook 'post-command-hook #'diff-post-command-hook nil t)))
+  (when diff--track-changes (track-changes-unregister diff--track-changes))
+  (remove-hook 'write-contents-functions #'diff-write-contents-hooks t)
+  (when diff-minor-mode
+    (if (not diff-update-on-the-fly)
+        (add-hook 'write-contents-functions #'diff-write-contents-hooks ni=
l t)
+      (unless diff--track-changes
+        (setq diff--track-changes
+              (track-changes-register #'diff--track-changes-signal
+                                      :nobefore t))))))
=20
 ;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;=
;;;
=20
diff --git a/test/lisp/emacs-lisp/track-changes-tests.el b/test/lisp/emacs-=
lisp/track-changes-tests.el
new file mode 100644
index 00000000000..cdccbe80299
--- /dev/null
+++ b/test/lisp/emacs-lisp/track-changes-tests.el
@@ -0,0 +1,149 @@
+;;; track-changes-tests.el --- tests for emacs-lisp/track-changes.el  -*- =
lexical-binding:t -*-
+
+;; Copyright (C) 2024  Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'track-changes)
+(require 'cl-lib)
+(require 'ert)
+
+(defun track-changes-tests--random-word ()
+  (let ((chars ()))
+    (dotimes (_ (1+ (random 12)))
+      (push (+ ?A (random (1+ (- ?z ?A)))) chars))
+    (apply #'string chars)))
+
+(defvar track-changes-tests--random-verbose nil)
+
+(defun track-changes-tests--message (&rest args)
+  (when track-changes-tests--random-verbose (apply #'message args)))
+
+(ert-deftest track-changes-tests--random ()
+  ;; Keep 2 buffers in sync with a third one as we make random
+  ;; changes to that 3rd one.
+  ;; We have 3 trackers: a "normal" one which we sync
+  ;; at random intervals, one which syncs via the "disjoint" signal,
+  ;; plus a third one which verifies that "nobefore" gets
+  ;; information consistent with the "normal" tracker.
+  (with-temp-buffer
+    (dotimes (_ 100)
+      (insert (track-changes-tests--random-word) "\n"))
+    (let* ((buf1 (generate-new-buffer " *tc1*"))
+           (buf2 (generate-new-buffer " *tc2*"))
+           (char-counts (make-vector 2 0))
+           (sync-counts (make-vector 2 0))
+           (print-escape-newlines t)
+           (file (make-temp-file "tc"))
+           (id1 (track-changes-register #'ignore))
+           (id3 (track-changes-register #'ignore :nobefore t))
+           (sync
+            (lambda (id buf n)
+              (track-changes-tests--message "!! SYNC %d !!" n)
+              (track-changes-fetch
+               id (lambda (beg end before)
+                    (when (eq n 1)
+                      (track-changes-fetch
+                       id3 (lambda (beg3 end3 before3)
+                             (should (eq beg3 beg))
+                             (should (eq end3 end))
+                             (should (eq before3
+                                         (if (symbolp before)
+                                             before (length before)))))))
+                    (cl-incf (aref sync-counts (1- n)))
+                    (cl-incf (aref char-counts (1- n)) (- end beg))
+                    (let ((after (buffer-substring beg end)))
+                      (track-changes-tests--message
+                       "Sync:\n    %S\n=3D>  %S\nat %d .. %d"
+                       before after beg end)
+                      (with-current-buffer buf
+                        (if (eq before 'error)
+                            (erase-buffer)
+                          (should (equal before
+                                         (buffer-substring
+                                          beg (+ beg (length before)))))
+                          (delete-region beg (+ beg (length before))))
+                        (goto-char beg)
+                        (insert after)))
+                    (should (equal (buffer-string)
+                                   (with-current-buffer buf
+                                     (buffer-string))))))))
+           (id2 (track-changes-register
+                 (lambda (id2 &optional distance)
+                   (when distance
+                     (track-changes-tests--message "Disjoint distance: %d"
+                                                   distance)
+                     (funcall sync id2 buf2 2)))
+                 :disjoint t)))
+      (write-region (point-min) (point-max) file)
+      (insert-into-buffer buf1)
+      (insert-into-buffer buf2)
+      (should (equal (buffer-hash) (buffer-hash buf1)))
+      (should (equal (buffer-hash) (buffer-hash buf2)))
+      (dotimes (_ 1000)
+        (pcase (random 15)
+          (0
+           (track-changes-tests--message "Manual sync1")
+           (funcall sync id1 buf1 1))
+          (1
+           (track-changes-tests--message "Manual sync2")
+           (funcall sync id2 buf2 2))
+          ((pred (< _ 5))
+           (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
+                  (end (min (+ beg (1+ (random 100))) (point-max))))
+             (track-changes-tests--message "Fill %d .. %d" beg end)
+             (fill-region-as-paragraph beg end)))
+          ((pred (< _ 8))
+           (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
+                  (end (min (+ beg (1+ (random 12))) (point-max))))
+             (track-changes-tests--message "Delete %S at %d .. %d"
+                                           (buffer-substring beg end) beg =
end)
+             (delete-region beg end)))
+          ((and 8 (guard (=3D (random 50) 0)))
+           (track-changes-tests--message "Silent insertion")
+           (let ((inhibit-modification-hooks t))
+             (insert "a")))
+          ((and 8 (guard (=3D (random 10) 0)))
+           (track-changes-tests--message "Revert")
+           (insert-file-contents file nil nil nil 'replace))
+          ((and 8 (guard (=3D (random 3) 0)))
+           (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
+                  (end (min (+ beg (1+ (random 12))) (point-max)))
+                  (after (eq (random 2) 0)))
+             (track-changes-tests--message "Bogus %S %d .. %d"
+                                           (if after 'after 'before) beg e=
nd)
+             (if after
+                 (run-hook-with-args 'after-change-functions
+                                     beg end (- end beg))
+               (run-hook-with-args 'before-change-functions beg end))))
+          (_
+           (goto-char (+ (point-min) (random (1+ (buffer-size)))))
+           (let ((word (track-changes-tests--random-word)))
+             (track-changes-tests--message "insert %S at %d" word (point))
+             (insert  word "\n")))))
+      (message "SCOREs: default: %d/%d=3D%d     disjoint: %d/%d=3D%d"
+               (aref char-counts 0) (aref sync-counts 0)
+               (/ (aref char-counts 0) (aref sync-counts 0))
+               (aref char-counts 1) (aref sync-counts 1)
+               (/ (aref char-counts 1) (aref sync-counts 1))))))
+
+
+
+;;; track-changes-tests.el ends here
--=20
2.43.0


--=-=-=--





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 4 Apr 2024 17:58:43 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Thu Apr 04 13:58:43 2024
Received: from localhost ([127.0.0.1]:34681 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rsRMJ-0003dA-DU
	for submit <at> debbugs.gnu.org; Thu, 04 Apr 2024 13:58:43 -0400
Received: from mout01.posteo.de ([185.67.36.65]:35925)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rsRMF-0003cm-RT
 for 70077 <at> debbugs.gnu.org; Thu, 04 Apr 2024 13:58:42 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout01.posteo.de (Postfix) with ESMTPS id DC37124002A
 for <70077 <at> debbugs.gnu.org>; Thu,  4 Apr 2024 19:58:28 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1712253508; bh=LjJ6XTN/1S5DHovyKjjogMsgzDn0Gj9/0CCWnmp7jYk=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 Content-Transfer-Encoding:From;
 b=ABK15oYEwPbBH29hcjPk/QxCtzKWOPAQY/2iLtnWa/l3l2HpVCGNlz0dsVuNj+zoq
 G+RT9AAGV4up2OOG2PUAbg8/ZCEV0OBdpy8RNA+DdEUO5T3aiBk9VU+p1mZKJbX6tn
 PCDn4ugJvyqat1VvrbObYCX81jRohlXHAdShy6wAZhcaVgrS7luc5neyZVkjMAz0nX
 rYbyvp+FwGGI1B52VXnhzo31Pz5gTFNwa69Mya3a53yOvlSZwjl36EdoKxkEpsjQfJ
 NGJkhOC5rLIza8FUSP7+aLhEvknTp4JSYZNGR/z5m3RsZEyKM+CPQSMtDr40MyjvSr
 ixdni0ACMWjeg==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V9Tr25R3Bz9rxP;
 Thu,  4 Apr 2024 19:58:26 +0200 (CEST)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvmsqamxof.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN> <87wmpfsv2y.fsf@localhost>
 <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN> <87ttkjspkq.fsf@localhost>
 <jwvo7aroeqr.fsf-monnier+emacs@HIDDEN> <8734s2pqu0.fsf@localhost>
 <jwvmsqamxof.fsf-monnier+emacs@HIDDEN>
Date: Thu, 04 Apr 2024 17:58:43 +0000
Message-ID: <87frw1t3fw.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

Stefan Monnier <monnier@HIDDEN> writes:

>> I am not 100% where the O(N^2) is coming from.
>
> If you add N times 1 char to an (initially empty) string, the total cost
> of constructing the resulting N-char string is O(N=C2=B2).

I guess that another approach is not concatenating the strings and
instead accumulating them into a list (or two lists - before/after).
That will get rid of logN multiplier :)

--=20
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 3 Apr 2024 12:46:17 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Wed Apr 03 08:46:16 2024
Received: from localhost ([127.0.0.1]:57321 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rs00O-0006Y4-Gu
	for submit <at> debbugs.gnu.org; Wed, 03 Apr 2024 08:46:16 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:61231)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rs00L-0006XE-K4
 for 70077 <at> debbugs.gnu.org; Wed, 03 Apr 2024 08:46:15 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id A1AAF44156E;
 Wed,  3 Apr 2024 08:46:02 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712148361;
 bh=0NNAoRHup2M5HkdEB+wJIPY9juDlBcIMtAkJ8yVEbGM=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=LbxrB7hPef1MTmf9S/9lTubFaUudpPAQInwqRwtUU3GKgXQGWREctXnq1Q6hbSImr
 VIyHVVFVxJnMqC32EurH5ZAF6iC3ErCNT4fVH1ZYg8y/xpHkzs0YEDg77KYDkeBgIE
 45zhn0jOmckIXst2r/nF3nAGVh4e44FLxEOqTFlvAk3L3mcu49pggZFVCb8PuxBRWf
 L9hWh/RC9daVNzrT9Ftc1goOAHvHLKuPrraoA+nlsYRbXouyo3pIwhzf9MzS3RL021
 cax0ZBvoBKbI9dSBJ2PmxyliQyeXFgXFWBUvkPLXt/lmS8Mrza6JJqDmcNy06S6qBS
 ZEMHj23u+12IA==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 5BE88441503;
 Wed,  3 Apr 2024 08:46:01 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id F35E9120828;
 Wed,  3 Apr 2024 08:46:00 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <8734s2pqu0.fsf@localhost> (Ihor Radchenko's message of "Wed, 03
 Apr 2024 12:34:47 +0000")
Message-ID: <jwvmsqamxof.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN> <87wmpfsv2y.fsf@localhost>
 <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN> <87ttkjspkq.fsf@localhost>
 <jwvo7aroeqr.fsf-monnier+emacs@HIDDEN> <8734s2pqu0.fsf@localhost>
Date: Wed, 03 Apr 2024 08:45:59 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.051 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> I am not 100% where the O(N^2) is coming from.

If you add N times 1 char to an (initially empty) string, the total cost
of constructing the resulting N-char string is O(N=B2).

> In any case, while I do see where the idea of over-expanding the region
> comes from, it is not ideal for my use-case in Org mode - it is often
> critical to know precise region where the change happened.

The reported changes are precise (modulo the fact that they are
combined): the over-expanded `track-changes--before-string`
is trimmed to the actual changes when it gets moved to a "state"
(that's done in `track-changes--clean-state`).

Also, note that in 99% of the cases, a command performs only a single
change (insertion or deletion), in which case there's no combining nor
over-expansion before the signal gets called.  Of course, approximation
may still happen if you decide not to act immediately when the signal
is sent.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 3 Apr 2024 12:34:50 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Wed Apr 03 08:34:50 2024
Received: from localhost ([127.0.0.1]:57309 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrzpK-0005ax-Do
	for submit <at> debbugs.gnu.org; Wed, 03 Apr 2024 08:34:50 -0400
Received: from mout02.posteo.de ([185.67.36.66]:45619)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rrzpE-0005Zt-Lq
 for 70077 <at> debbugs.gnu.org; Wed, 03 Apr 2024 08:34:47 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout02.posteo.de (Postfix) with ESMTPS id AFFDA240103
 for <70077 <at> debbugs.gnu.org>; Wed,  3 Apr 2024 14:34:34 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1712147674; bh=Kn4hJ4Epmhcxy1k0wUp6vHoWYHsJbZ13QKr6Jw3RNgE=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 Content-Transfer-Encoding:From;
 b=QBBwst8Rdda11QPafUdYgdL48AyKZg5/Kl1+/qL3HSYNROjarP6uYFwR5GnyaNrPS
 cNlQc3IjQ5Rwjvs0XYsgQAsb6XAxTLhdXs78P9VrJSSTUiWyYelADpV5Obt4TBJt1o
 dNcg/GaW5JdXTs7DFuyBedeCpOETuSB6Pv+zDbIvfaYeborVF5jBzlVK5tXcO17T7R
 ZYNx9v0WOVQMo+HJ6uVtToqG3x8R9IlYzQiY19m7ycrIfrkMC+HthEe+XIzx8uSeWM
 MGIPPOTEXxJNclefNi9qte9fK3QfmKzhqOGd3JifbBYorrwWWM9b8fl5uyRuDPjnVU
 h3gDpMeD5epAg==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V8khm0r61z9rxM;
 Wed,  3 Apr 2024 14:34:31 +0200 (CEST)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvo7aroeqr.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN> <87wmpfsv2y.fsf@localhost>
 <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN> <87ttkjspkq.fsf@localhost>
 <jwvo7aroeqr.fsf-monnier+emacs@HIDDEN>
Date: Wed, 03 Apr 2024 12:34:47 +0000
Message-ID: <8734s2pqu0.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

Stefan Monnier <monnier@HIDDEN> writes:

>> My reading of `track-changes--before' is that `concat' is called on
>> every change except when a new change is already inside the previously
>> recorded one.
>
> The "previously recorded one" (i.e. the info stored in
> `track-changes--before-beg/end/string`) is a conservative
> over-approximation of the changed area.  We (at least) double its size
> every time we have to grow it, specifically to reduce the number of
> times we need to grow it (otherwise we quickly fall into the O(N=C2=B2)
> trap).

I am not 100% where the O(N^2) is coming from. I'd rather see the
"doubling" explained better in the comment before the code.

In any case, while I do see where the idea of over-expanding the region
comes from, it is not ideal for my use-case in Org mode - it is often
critical to know precise region where the change happened.

Larger region spanning over more lines than needed can easily trigger
re-parsing too much, especially when changes are being made near Org
heading lines. In worst-case scenario, Org mode has to drop parser cache
for the whole edited subtree repeatedly, slowing things down by orders
of magnitude.

And this is not a theoretical consideration - I had to tweak things
considerably in the past for certain Org mass-editing commands in order
to not slow them down because of repeated cache drops.

--=20
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 2 Apr 2024 17:52:00 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Apr 02 13:52:00 2024
Received: from localhost ([127.0.0.1]:56172 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rriIh-0001XP-I5
	for submit <at> debbugs.gnu.org; Tue, 02 Apr 2024 13:52:00 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:19075)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rriIf-0001Wz-Dj
 for 70077 <at> debbugs.gnu.org; Tue, 02 Apr 2024 13:51:58 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 520DA442379;
 Tue,  2 Apr 2024 13:51:47 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712080305;
 bh=a/oiLDnMuo7F/+MgqslqKeDcbREiovQzSFZZfyaMFWM=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=EqFYOoCMXO+apmnbz73SbAxvDyroxNSi8y+ESWH+UuhdwgN4WWhAryeWMXp+90eEN
 shZzaNvRUh8X5jk2ShmK0kwiQwCIPX4XYcUf2Gph6Pz4Vj/14m8Rawmqw0gAFj3yrN
 lLzZBvN0cCgxsnnjJuQIABf/MUzrXsuoTwSggOtjejVdIJMFKuiE+k1qnocL4XQfAY
 o/ylDq0PZ/kYdWcA+idozrgv8jQ8zy2fnX64MsdiHE3FIrhKYByI6RD3Buhw/uS3WN
 RuH/XM9iA/Z4eLmr02/6sDIPKrwGU3OLG2jwoXX9p1pipsLL0bJ97OS272O7X8GouN
 2F8ZuQoTJOaEg==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 4809E441FC1;
 Tue,  2 Apr 2024 13:51:45 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id E41D5120682;
 Tue,  2 Apr 2024 13:51:44 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <87ttkjspkq.fsf@localhost> (Ihor Radchenko's message of "Tue, 02
 Apr 2024 16:21:25 +0000")
Message-ID: <jwvo7aroeqr.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN> <87wmpfsv2y.fsf@localhost>
 <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN> <87ttkjspkq.fsf@localhost>
Date: Tue, 02 Apr 2024 13:51:42 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.057 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> I agree, but only when subsequent changes intersect each other.
> When distant changes are combined together, they cover a lot of buffer
> text that has not been changed - they may trigger a lot more work
> compared to more granular changes. Consider, for example, two changes
> near the beginning and the end of the buffer:
> (1..10) and (1000000...(point-max))
> If such changes are tracked by tree-sitter or other parser, there is a
> world of difference between requesting to re-parse individual segments
> and the whole buffer.

Oh, definitely.  For some clients, the amount of work doesn't really
depend on the size of the change, tho (OTOH the work done by
`track-changes.el` *is* affected by the size of the change).
Handling disjoint changes can be very important.

I'm fairly satisfied with my `track-changes-register-disjoint` solution
to this problem.

>>> The changes are stored in strings, which get allocated and
>>> re-allocated repeatedly.
>> Indeed.  Tho for N small changes, my code should perform only O(log N)
>> re-allocations.
> May you explain a bit more about this?
> My reading of `track-changes--before' is that `concat' is called on
> every change except when a new change is already inside the previously
> recorded one.

The "previously recorded one" (i.e. the info stored in
`track-changes--before-beg/end/string`) is a conservative
over-approximation of the changed area.  We (at least) double its size
every time we have to grow it, specifically to reduce the number of
times we need to grow it (otherwise we quickly fall into the O(N=C2=B2)
trap).

> (aside: I have a hard time reading the code because of
> confusing slot names: bbeg vs beg??)

"bbeg/bend" stands for "before beg" and "before end" (i.e. the positions as
they were before the change).  BTW, those two slots don't exist any more
in the latest code I sent (but vars with those names are still present
here and there).

>>>> We could expose a list of simultaneous (and thus disjoint) changes,
>>>> which avoids the last problem.  But it's a fair bit more work for us, =
it
>>>> makes the API more complex for the clients, and it's rarely what the
>>>> clients really want anyway.
>>> FYI, Org parser maintains such a list.
>> Could you point me to the relevant code (I failed to find it)?
> That code handles a lot more than just changes, so it might not be the
> best reference. Anyway...
> See the docstring of `org-element--cache-sync-requests', and
> the branch in `org-element--cache-submit-request' starting from
> 	  ;; Current changes can be merged with first sync request: we
> 	  ;; can save a partial cache synchronization.

Thanks.  [ I see I did search the right file, but the code was too
intertwined with other things for me to find that part.  ]

[ Further discussions of Org's code moved off-list.  ]

> Hmm. By referring to buffer-undo-list, I meant that intersecting edits
> will be automatically merged, as it is usually done in undo system.

[ Actually `buffer-undo-list` doesn't do very much merging, if any.
  AFAIK the only merging that happens there is to "amalgamate" consecutive
  `self-insert-command`s or consecutive single-char `delete-char`s. =F0=9F=
=99=82  ]

> I am also not 100% sure why edits being simultaneous is any relevant.

If they're not simultaneous it means that they describe N different
buffer states and that to interpret a specific change in the list
(e.g. to convert buffer positions to line+col positions) you
may have to reconstruct its before&after states by applying the
other changes.

For some use cases this is quite inconvenient.
In any case, it's not very important, I think.

> What I was talking about is (1) automatically merging subsequent edits
> like 1..5 2..7 7..9 into 1..9. (2) if a subsequent edit does not
> intersect with previous edited region, record it separately without
> merging.

That's what you can get now with `track-changes-register-disjoint`.

>> The code changes were actually quite simple, so I have now included it.
>> See my current PoC code below.
> Am I missing something, or will `track-changes--signal-if-disjoint'
> alter `track-changes--state' when it calls `track-changes-fetch' ->
> `track-changes-reset'?

[ I assume you meant `track-changes--clean-state` instead of
  `track-changes-reset`.  ]=20

Yes it will (and there's a bug in `track-changes--before` because of
that that I still need to fix =F0=9F=99=81), but that should not be a probl=
em for
the clients.

> Won't that interfere with the information passed to
> non-disjoint trackers?

No: the separate states will be (re)combined when those trackers call
`track-changes-fetch`.

There is a nearby poor interference between clients, tho: if one
client calls `track-changes-fetch` eagerly all the time, it may prevent
another client from getting a "disjoint change" signal because
disjointness is noticed only between changes that occurred since the
last call to `track-changes--clean-state` (which is called mostly by
`track-changes-fetch`).
I guess this deserves a FIXME.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 2 Apr 2024 16:21:36 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Apr 02 12:21:36 2024
Received: from localhost ([127.0.0.1]:55826 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrgtB-0008Iv-Qx
	for submit <at> debbugs.gnu.org; Tue, 02 Apr 2024 12:21:35 -0400
Received: from mout01.posteo.de ([185.67.36.65]:49755)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rrgt7-0008I5-EQ
 for 70077 <at> debbugs.gnu.org; Tue, 02 Apr 2024 12:21:31 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout01.posteo.de (Postfix) with ESMTPS id 68D2424002A
 for <70077 <at> debbugs.gnu.org>; Tue,  2 Apr 2024 18:21:19 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1712074879; bh=099D2+4u5fNlEq3tyHM7sYiP/H/3+hOxowmjOz+zMEE=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=bqFrDQfgdM08afCXDiK1xF+cU2jG8zl00tJ+WBZh5q+J6Or37KxviLNfWIwuDGi/S
 AG2WXlsod+kN7e2dw4/ThTEC3SiCrUWsftBruUSQeCDaxkN9+8rBen0pFpRrd+xE+M
 VnUyZy0k0GMNDTiBtpia54djbCYADFS++IvthpLwBkS31lA9un4q/nP/oNBlwhlCM0
 Kd90p3bNFglYmUVpy50jc6w2EhMPlNYiZtVZ7IY2nZdHzvGwEJGSQZdoZBXv3Rjaag
 zsODSfqm+TG9d9WfSW5xo2bIWKpnT5CocITFEoXsUS5C6TE6pZMx7lRbKAlS0Hi6H+
 +2abXf0T9XrCg==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V8Cms4bw9z6tm8;
 Tue,  2 Apr 2024 18:21:17 +0200 (CEST)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN> <87wmpfsv2y.fsf@localhost>
 <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN>
Date: Tue, 02 Apr 2024 16:21:25 +0000
Message-ID: <87ttkjspkq.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 1.4 (+)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 Content preview:  Stefan Monnier <monnier@HIDDEN> writes: >>> I'm
 not sure how to combine the benefits of combining small changes into >>>
 larger
 ones with the benefits of keeping distant changes separate. >> I am not sure
 if combining small changes into lar [...] 
 Content analysis details:   (1.4 points, 10.0 required)
 pts rule name              description
 ---- ---------------------- --------------------------------------------------
 -2.3 RCVD_IN_DNSWL_MED      RBL: Sender listed at https://www.dnswl.org/,
 medium trust [185.67.36.65 listed in list.dnswl.org]
 -0.0 RCVD_IN_MSPIKE_H3      RBL: Good reputation (+3)
 [185.67.36.65 listed in wl.mailspike.net]
 0.1 URIBL_SBL_A Contains URL's A record listed in the Spamhaus SBL
 blocklist [URIs: microsoft.github.io]
 0.6 URIBL_SBL Contains an URL's NS IP listed in the Spamhaus SBL
 blocklist [URIs: microsoft.github.io]
 3.0 MANY_TO_CC             Sent to 10+ recipients
 -0.0 SPF_PASS               SPF: sender matches SPF record
 0.0 SPF_HELO_NONE          SPF: HELO does not publish an SPF Record
 -0.0 RCVD_IN_MSPIKE_WL      Mailspike good senders
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: 0.4 (/)

Stefan Monnier <monnier@HIDDEN> writes:

>>> I'm not sure how to combine the benefits of combining small changes into
>>> larger ones with the benefits of keeping distant changes separate.
>> I am not sure if combining small changes into larger ones is at all a
>> good idea.
>
> In my experience, if the amount of work to be done "per change" is not
> completely trivial, combining small changes is indispensable (the worst
> part being that the cases where it's needed are sufficiently infrequent
> that naive users of `*-change-functions` won't know about that, so we
> end up with unacceptable slowdowns in corner cases).

I agree, but only when subsequent changes intersect each other.
When distant changes are combined together, they cover a lot of buffer
text that has not been changed - they may trigger a lot more work
compared to more granular changes. Consider, for example, two changes
near the beginning and the end of the buffer:
(1..10) and (1000000...(point-max))
If such changes are tracked by tree-sitter or other parser, there is a
world of difference between requesting to re-parse individual segments
and the whole buffer.

>> The changes are stored in strings, which get allocated and
>> re-allocated repeatedly.
>
> Indeed.  Tho for N small changes, my code should perform only O(log N)
> re-allocations.

May you explain a bit more about this?
My reading of `track-changes--before' is that `concat' is called on
every change except when a new change is already inside the previously
recorded one. (aside: I have a hard time reading the code because of
confusing slot names: bbeg vs beg??)

>> Repeated string allocations, especially when strings keep growing
>> towards the buffer size, is likely going to increase consing and make
>> GCs more frequent.
>
> Similar allocations presumably take place anyway while running the code
> (e.g. for the `buffer-undo-list`), so I'm hoping the effect will be
> "lost in the noise".

I disagree. `buffer-undo-list' only includes the text that has been
actually changed. It never creates strings that span between
distant regions. As a terminal case, consider alternating between first
and second half of the buffer, starting in the middle and editing
towards the buffer boundaries - this will involve re-allocation of
buffer-long strings on average.

>>> We could expose a list of simultaneous (and thus disjoint) changes,
>>> which avoids the last problem.  But it's a fair bit more work for us, it
>>> makes the API more complex for the clients, and it's rarely what the
>>> clients really want anyway.
>> FYI, Org parser maintains such a list.
>
> Could you point me to the relevant code (I failed to find it)?

That code handles a lot more than just changes, so it might not be the
best reference. Anyway...

See the docstring of `org-element--cache-sync-requests', and
the branch in `org-element--cache-submit-request' starting from
	  ;; Current changes can be merged with first sync request: we
	  ;; can save a partial cache synchronization.

>> We previously discussed a similar API in
>> https://yhetil.org/emacs-bugs/87o7iq1emo.fsf@localhost/
>
> IIUC this discusses a *sequence* of edits.  In the point to which you
> replied I was discussing keeping a list of *simultaneous* edits.

Hmm. By referring to buffer-undo-list, I meant that intersecting edits
will be automatically merged, as it is usually done in undo system.

I am also not 100% sure why edits being simultaneous is any relevant.
Maybe we are talking about different things?

What I was talking about is (1) automatically merging subsequent edits
like 1..5 2..7 7..9 into 1..9. (2) if a subsequent edit does not
intersect with previous edited region, record it separately without
merging.

> This said, the downside in both cases is that the specific data that we
> need from such a list tends to depend on the user.  E.g. you suggest
>
>     (BEG END_BEFORE END_AFTER COUNTER)
>
> but that is not sufficient reconstruct the corresponding buffer state,
> so things like Eglot/CRDT can't use it.  Ideally for CRDT I think you'd
> want a sequence of
>
>     (BEG END-BEFORE STRING-AFTER)

Right. It's just that Org mode does not need STRING-AFTER, which is why
I did not think about it in my proposal. Of course, having STRING-AFTER
is required to get full reconstruction of the buffer state.

> but for Eglot this is not sufficient because Eglot needs to convert BEG
> and END_BEFORE into LSP positions (i.e. "line+col") and for that it
> needs to reproduce the past buffer state.  So instead, what Eglot needs
> (and does indeed build using `*-change-functions`) is a sequence of
>
>     (LSP-BEG LSP-END-BEFORE STRING-AFTER)
>
> [ Tho it seems it also needs a "LENGTH" of the deleted chunk, not sure
>   exactly why, but I guess it's a piece of redundant info the servers
>   can use to sanity-check the data?  ]

It is to support deprecated LSP spec (I presume that older LSP servers
may still be using the old spec):

https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/
export type TextDocumentContentChangeEvent = {
	 * The range of the document that changed.
	range: Range;

	 * The optional length of the range that got replaced.
	 * @deprecated use range instead.
	rangeLength?: number;

	 * The new text for the provided range.
	text: string;

> The code changes were actually quite simple, so I have now included it.
> See my current PoC code below.

Am I missing something, or will `track-changes--signal-if-disjoint'
alter `track-changes--state' when it calls `track-changes-fetch' ->
`track-changes-reset'? Won't that interfere with the information passed
to non-disjoint trackers?

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 2 Apr 2024 15:17:56 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Apr 02 11:17:56 2024
Received: from localhost ([127.0.0.1]:54983 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrftc-0004mi-7u
	for submit <at> debbugs.gnu.org; Tue, 02 Apr 2024 11:17:56 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:27096)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rrfta-0004lr-IK
 for 70077 <at> debbugs.gnu.org; Tue, 02 Apr 2024 11:17:55 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id D0D3C10005D;
 Tue,  2 Apr 2024 11:17:44 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1712071063;
 bh=eeNWoHWR/wUiP2mhIy2njzwEpHTZJYqdVZ1Aqxh3DhM=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=WLVOCjS47f//H5W3OgEUuyNMycF9xhOwgoxoqjVbuygzfIUopH+GopUUTods8cC2L
 arW30CBKKzkED8+qaCkzEUQHdfbxEhqeTWeD68fwCKbbiE6J7MpvZN8QDZRAS5ml2K
 TaO4fecFA7iOoBrVDalNpRlHC6H2AitWQ1bSGP7EKD7BNms228NGY+MIdLqDV6qVem
 vD3vc71tz/u8P8Z5PVstwBEMtEF9jsmUCakSuyWTXXuTAQWFO5ZmCYh1s+Uwdsrlv3
 dktTtzDD0YAhQtGKJhD+B2W//ujVpY8wQjzpgqpqPgPDr4HFPjxj9P1ERFm1o8FsD4
 HSL1B1adk6Oiw==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id A7833100048;
 Tue,  2 Apr 2024 11:17:43 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 4972512082A;
 Tue,  2 Apr 2024 11:17:43 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <87wmpfsv2y.fsf@localhost> (Ihor Radchenko's message of "Tue, 02
 Apr 2024 14:22:29 +0000")
Message-ID: <jwv4jcjq16n.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN> <87wmpfsv2y.fsf@localhost>
Date: Tue, 02 Apr 2024 11:17:41 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.043 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

--=-=-=
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

>> I'm not sure how to combine the benefits of combining small changes into
>> larger ones with the benefits of keeping distant changes separate.
> I am not sure if combining small changes into larger ones is at all a
> good idea.

In my experience, if the amount of work to be done "per change" is not
completely trivial, combining small changes is indispensable (the worst
part being that the cases where it's needed are sufficiently infrequent
that naive users of `*-change-functions` won't know about that, so we
end up with unacceptable slowdowns in corner cases).

It also keeps the API simpler since the clients only need to care about
at most one (BEG END BEFORE) item at any given time.

> The changes are stored in strings, which get allocated and
> re-allocated repeatedly.

Indeed.  Tho for N small changes, my code should perform only O(log N)
re-allocations.

> Repeated string allocations, especially when strings keep growing
> towards the buffer size, is likely going to increase consing and make
> GCs more frequent.

Similar allocations presumably take place anyway while running the code
(e.g. for the `buffer-undo-list`), so I'm hoping the effect will be
"lost in the noise".

But admittedly for code which does not need the `before` string at all
(such as `diff-mode.el`), it's indeed a waste of effort.  I haven't been
able to come up with an API which is still simple but without such costs.

> Aside...
> How nice would it be if buffer state and buffer text were persistent.
> Like in https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimpl=
ementation

=F0=9F=99=82

>> We could expose a list of simultaneous (and thus disjoint) changes,
>> which avoids the last problem.  But it's a fair bit more work for us, it
>> makes the API more complex for the clients, and it's rarely what the
>> clients really want anyway.
> FYI, Org parser maintains such a list.

Could you point me to the relevant code (I failed to find it)?

> We previously discussed a similar API in
> https://yhetil.org/emacs-bugs/87o7iq1emo.fsf@localhost/

IIUC this discusses a *sequence* of edits.  In the point to which you
replied I was discussing keeping a list of *simultaneous* edits.

This said, the downside in both cases is that the specific data that we
need from such a list tends to depend on the user.  E.g. you suggest

    (BEG END_BEFORE END_AFTER COUNTER)

but that is not sufficient reconstruct the corresponding buffer state,
so things like Eglot/CRDT can't use it.  Ideally for CRDT I think you'd
want a sequence of

    (BEG END-BEFORE STRING-AFTER)

but for Eglot this is not sufficient because Eglot needs to convert BEG
and END_BEFORE into LSP positions (i.e. "line+col") and for that it
needs to reproduce the past buffer state.  So instead, what Eglot needs
(and does indeed build using `*-change-functions`) is a sequence of

    (LSP-BEG LSP-END-BEFORE STRING-AFTER)

[ Tho it seems it also needs a "LENGTH" of the deleted chunk, not sure
  exactly why, but I guess it's a piece of redundant info the servers
  can use to sanity-check the data?  ]

>> But it did occur to me that we could solve the "disjoint changes"
>> problem in the following way: signal the client (from
>> `before-change-functions`) when a change is about to be made "far" from
>> the currently pending changes.
> This makes sense.

The code changes were actually quite simple, so I have now included it.
See my current PoC code below.


        Stefan

--=-=-=
Content-Type: application/emacs-lisp
Content-Disposition: attachment; filename=track-changes.el
Content-Transfer-Encoding: quoted-printable

;;; track-changes.el --- API to react to buffer modifications  -*- lexical-=
binding: t; -*-

;; Copyright (C) 2024  Free Software Foundation, Inc.

;; Author: Stefan Monnier <monnier@HIDDEN>

;; This file is part of GNU Emacs.

;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This library is a layer of abstraction above `before-change-functions'
;; and `after-change-functions' which takes care of accumulating changes
;; until a time when its client finds it convenient to react to them.

;; It provides the following operations:
;;
;;     (track-changes-register SIGNAL)
;;     (track-changes-fetch ID FUNC)
;;     (track-changes-unregister ID)
;;     (track-changes-reset ID)
;;     (track-changes-register-disjoint ID)
;;
;; A typical use case might look like:
;;
;;     (defvar my-foo--change-tracker nil)
;;     (define-minor-mode my-foo-mode
;;       "Fooing like there's no tomorrow."
;;       (if (null my-foo-mode)
;;           (when my-foo--change-tracker
;;             (track-changes-unregister my-foo--change-tracker)
;;             (setq my-foo--change-tracker nil))
;;         (unless my-foo--change-tracker
;;           (setq my-foo--change-tracker
;;                 (track-changes-register
;;                  (lambda (id)
;;                    (track-changes-fetch
;;                     id (lambda (beg end before)
;;                          ..DO THE THING..))))))))

;;; Code:

;; FIXME: Try and do some sanity-checks (e.g. looking at `buffer-size'),
;; to detect if/when we somehow missed some changes.
;; FIXME: The API doesn't offer an easy way to signal a "full resync"
;; kind of change, as might be needed if we lost changes.

(eval-when-compile (require 'cl-lib))

(cl-defstruct (track-changes--tracker
               (:noinline t)
               (:constructor nil)
               (:constructor track-changes--tracker ( signal state)))
  ( signal nil :read-only t)
  state)

(cl-defstruct (track-changes--state
               (:noinline t)
               (:constructor nil)
               (:constructor track-changes--state ()))
  (beg (point-max))
  (end (point-min))
  (before nil)
  (next nil))

(defvar-local track-changes--trackers ())
(defvar-local track-changes--clean-trackers ()
  "List of trackers that are clean.
Those are the trackers that get signaled when a change is made.")

(defvar-local track-changes--disjoint-trackers ()
 "List of trackers that want to react to disjoint changes.
These trackers' are signaled every time track-changes notices
that some upcoming changes touch another \"distant\" part of the buffer.")

(defvar-local track-changes--state nil)

;; `track-changes--before-*' keep track of the content of the
;; buffer when `track-changes--state' was cleaned.
(defvar-local track-changes--before-beg nil)
(defvar-local track-changes--before-end nil)
(defvar-local track-changes--before-string nil)

(defvar-local track-changes--buffer-size nil)

(defun track-changes-register ( signal)
  "Register a new tracker and return a new tracker ID.
SIGNAL is a function that will be called with one argument (the tracker ID)=
 when
the current buffer is modified, so that we can react to the change.
Once called, SIGNAL is not called again until `track-changes-fetch'
is called with the corresponding tracker ID."
  ;; FIXME: Add an optional arg to choose between `funcall' and `funcall-la=
ter'?
  ;; FIXME: Add an optional arg to say we don't need the `before' info?
  (track-changes--clean-state)
  (add-hook 'before-change-functions #'track-changes--before nil t)
  (add-hook 'after-change-functions  #'track-changes--after  nil t)
  (let ((tracker (track-changes--tracker signal track-changes--state)))
    (push tracker track-changes--trackers)
    (push tracker track-changes--clean-trackers)
    tracker))

(defun track-changes-register-disjoint (id)
 "Enable disjoint change tracking (DCT) for tracker ID.

This is needed when combining disjoint changes into one bigger change
is unacceptable, typically for performance reasons.

When DCT is enabled, we call ID's SIGNAL function every time we are
about to combine changes from \"distant\" parts of the buffer.

These calls are distinguished from normal calls by calling SIGNAL with
a second argument which is the distance between the upcoming change and
the previous changes.
In that case SIGNAL is called directly from `before-change-functions' and
should thus be extra careful: don't modify the buffer, don't call a function
that may block, ...

In order to prevent the upcoming change from being combined with the previo=
us
changes, SIGNAL needs to call `track-changes-fetch' before it returns."
  (cl-assert (memq id track-changes--trackers))
  (unless (memq id track-changes--disjoint-trackers)
    (push id track-changes--disjoint-trackers)))


(defun track-changes-reset (id)
  "Mark all past changes as handled for tracker ID.
Does not re-enable ID's signal."
  (track-changes--clean-state)
  (setf (track-changes--tracker-state id) track-changes--state))

(defun track-changes-unregister (id)
  "Remove the tracker denoted by ID.
Trackers can consume resources (especially if `track-changes-fetch' is
not called), so it is good practice to unregister them when you don't
need them any more."
  (unless (memq id track-changes--trackers)
    (error "Unregistering a non-registered tracker: %S" id))
  (setq track-changes--trackers (delq id track-changes--trackers))
  (setq track-changes--clean-trackers (delq id track-changes--clean-tracker=
s))
  (setq track-changes--disjoint-trackers
        (delq id track-changes--disjoint-trackers))
  (when (null track-changes--trackers)
    (setq track-changes--state nil)
    (setq track-changes--buffer-size nil)
    (setq track-changes--before-string nil)
    (remove-hook 'before-change-functions #'track-changes--before t)
    (remove-hook 'after-change-functions  #'track-changes--after  t)))


(defun track-changes--clean-state ()
  (cond
   ((null track-changes--state)
    (cl-assert (null track-changes--before-string))
    (cl-assert (null track-changes--buffer-size))
    ;; No state has been created yet.  Do it now.
    (setq track-changes--buffer-size (buffer-size))
    (setq track-changes--state (track-changes--state)))
   ((null track-changes--before-string) nil)
   ((> (track-changes--state-beg track-changes--state)
       (track-changes--state-end track-changes--state))
    ;; before-c-f was run but not after-c-f, so there was really no change.
    nil)
   (t
    ;; FIXME: We may be in-between a before-c-f and an after-c-f, so we
    ;; should save some of the current buffer in case an after-c-f comes
    ;; before a before-c-f.
    (cl-assert (<=3D track-changes--before-beg
                   (track-changes--state-beg track-changes--state)
                   (track-changes--state-end track-changes--state)
                   track-changes--before-end))
    (cl-assert (null (track-changes--state-before track-changes--state)))
    (setf (track-changes--state-before track-changes--state)
          ;; The before-* vars can cover more text than the actually modifi=
ed
          ;; area, so trim it down now to the relevant part.
          (if (=3D (- track-changes--before-end track-changes--before-beg)
                 (- (track-changes--state-end track-changes--state)
                    (track-changes--state-beg track-changes--state)))
              ;; Common case.
              track-changes--before-string
            (substring track-changes--before-string
                       (- (track-changes--state-beg track-changes--state)
                          track-changes--before-beg)
                       (- (length track-changes--before-string)
                          (- track-changes--before-end
                             (track-changes--state-end
                              track-changes--state))))))
    (setq track-changes--before-beg nil
          track-changes--before-end nil
          track-changes--before-string nil)
    (let ((new (track-changes--state)))
      (setf (track-changes--state-next track-changes--state) new)
      (setq track-changes--state new)))))

(defvar track-changes-disjoint-threshold 100
  "Distance below which changes are not considered disjoint.")

(defun track-changes--signal-if-disjoint (pos1 pos2)
  (let ((distance (- pos2 pos1)))
    (when (> distance
             (max track-changes-disjoint-threshold
                  ;; If the distance is smaller than the size of the current
                  ;; change, then we may as well consider it as "near".
                  (length track-changes--before-string)
                  (- track-changes--before-end
                     track-changes--before-beg)))
      (dolist (tracker track-changes--disjoint-trackers)
        (funcall (track-changes--tracker-signal tracker) tracker distance))=
)))

(defun track-changes--before (beg end)
  (cl-assert (=3D track-changes--buffer-size (buffer-size)))
  (cl-assert track-changes--state)
  (cl-assert (<=3D beg end))
  (if (null track-changes--before-string)
      (progn
        (setf track-changes--before-string
              (buffer-substring-no-properties beg end))
        (setf track-changes--before-beg beg)
        (setf track-changes--before-end end))
    (cl-assert (save-restriction
                 (widen)
                 (<=3D (point-min)
                     track-changes--before-beg
                     track-changes--before-end
                     (point-max))))
    (when (< beg track-changes--before-beg)
      (when track-changes--disjoint-trackers
        (track-changes--signal-if-disjoint end track-changes--before-beg))
      (let* ((old-bbeg track-changes--before-beg)
             ;; To avoid O(N=C2=B2) behavior when faced with many small cha=
nges,
             ;; we copy more than needed.
             (new-bbeg (min (max (point-min)
                                 (- old-bbeg
                                    (length track-changes--before-string)))
                            beg)))
        (setf track-changes--before-beg new-bbeg)
        (cl-callf (lambda (old new) (concat new old))
            track-changes--before-string
          (buffer-substring-no-properties new-bbeg old-bbeg))))

    (when (< track-changes--before-end end)
      (when track-changes--disjoint-trackers
        (track-changes--signal-if-disjoint track-changes--before-end beg))
      (let* ((old-bend track-changes--before-end)
             ;; To avoid O(N=C2=B2) behavior when faced with many small cha=
nges,
             ;; we copy more than needed.
             (new-bend (max (min (point-max)
                                 (+ old-bend
                                    (length track-changes--before-string)))
                            end)))
        (setf track-changes--before-end new-bend)
        (cl-callf concat track-changes--before-string
          (buffer-substring-no-properties old-bend new-bend))))))

(defun track-changes--after (beg end len)
  (cl-assert track-changes--state)
  (cl-assert track-changes--before-string)
  (let ((offset (- (- end beg) len)))
    (cl-incf track-changes--before-end offset)
    (cl-incf track-changes--buffer-size offset)
    (cl-assert (=3D track-changes--buffer-size (buffer-size)))
    (cl-assert
     (save-restriction
       (widen)
       (<=3D (point-min)
           track-changes--before-beg
           beg end
           track-changes--before-end
           (point-max))))
    ;; Note the new changes.
    (when (< beg (track-changes--state-beg track-changes--state))
      (setf (track-changes--state-beg track-changes--state) beg))
    (cl-callf (lambda (old-end) (max end (+ old-end offset)))
        (track-changes--state-end track-changes--state)))
  (cl-assert (<=3D track-changes--before-beg
                 (track-changes--state-beg track-changes--state)
                 beg end
                 (track-changes--state-end track-changes--state)
                 track-changes--before-end))
  (while track-changes--clean-trackers
    (let ((tracker (pop track-changes--clean-trackers)))
      ;; FIXME: Use `funcall'?
      (funcall-later #'track-changes--call-signal
                     (list (current-buffer) tracker)))))

(defun track-changes--call-signal (buf tracker)
  (when (buffer-live-p buf)
    (with-current-buffer buf
      ;; Silence ourselves if `track-changes-fetch' was called in the mean =
time.
      (unless (memq tracker track-changes--clean-trackers)
        (funcall (track-changes--tracker-signal tracker) tracker)))))

(defun track-changes-fetch (id func)
  ;; FIXME: Bad name, "fetch" doesn't make much sense for it any more.
  "Fetch the pending changes.
ID is the tracker ID returned by a previous `track-changes-register'.
FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
where BEGIN..END delimit the region that was changed since the last
time `track-changes-fetch' was called and BEFORE is a string containing
the previous content of that region.

If no changes occurred since the last time, FUNC is not called and
we return nil, otherwise we return the value returned by FUNC,
and re-enable the TRACKER corresponding to ID."
  (let ((beg nil)
        (end nil)
        (before nil)
        (states ()))
    ;; Transfer the data from `track-changes--before-string'
    ;; to the tracker's state object, if needed.
    (track-changes--clean-state)
    ;; We want to combine the states from most recent to oldest,
    ;; so reverse them.
    (let ((state (track-changes--tracker-state id)))
      (while state
        (push state states)
        (setq state (track-changes--state-next state))))
    (when (null (track-changes--state-before (car states)))
      (cl-assert (eq (car states) track-changes--state))
      (setq states (cdr states)))
    (if (null states)
        (progn
          (cl-assert (memq id track-changes--clean-trackers))
          nil)
      (dolist (state states)
        (let ((prevbbeg (track-changes--state-beg state))
              (prevbend (track-changes--state-end state))
              (prevbefore (track-changes--state-before state)))
          (if (not before)
              (progn
                ;; This is the most recent change.  Just initialize the var=
s.
                (setq beg (track-changes--state-beg state))
                (setq end (track-changes--state-end state))
                (setq before prevbefore)
                (unless (and (=3D beg prevbbeg) (=3D end prevbend))
                  (setq before
                        (substring before
                                   (- beg (track-changes--state-beg state))
                                   (- (length before)
                                      (- (track-changes--state-end state)
                                         end))))))
            (let ((endb (+ beg (length before))))
              (when (< prevbbeg beg)
                (setq before (concat (buffer-substring-no-properties
                                      prevbbeg beg)
                                     before))
                (setq beg prevbbeg)
                (cl-assert (=3D endb (+ beg (length before)))))
              (when (< endb prevbend)
                (let ((new-end (+ end (- prevbend endb))))
                  (setq before (concat before
                                       (buffer-substring-no-properties
                                        end new-end)))
                  (setq end new-end)
                  (cl-assert (=3D prevbend (+ beg (length before))))
                  (setq endb (+ beg (length before)))))
              (cl-assert (<=3D beg prevbbeg prevbend endb))
              ;; The `prevbefore' is covered by the new one.
              (setq before
                    (concat (substring before 0 (- prevbbeg beg))
                            prevbefore
                            (substring before (- (length before)
                                                 (- endb prevbend)))))))))
      (cl-assert (<=3D (point-min) beg end (point-max)))
      ;; Update the tracker's state before running `func' so we don't risk
      ;; mistakenly replaying the changes in case `func' exits non-locally.
      (setf (track-changes--tracker-state id) track-changes--state)
      (unwind-protect (funcall func beg end before)
        ;; Re-enable the tracker's signal only after running `func', so
        ;; as to avoid recursive invocations.
        (cl-pushnew id track-changes--clean-trackers)))))

(defmacro with-track-changes (id vars &rest body)
  (declare (indent 2) (debug (form sexp body)))
  `(track-changes-fetch ,id (lambda ,vars ,@body)))
=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20

(provide 'track-changes)
;;; track-changes.el end here.

--=-=-=--





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 2 Apr 2024 14:22:42 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Tue Apr 02 10:22:42 2024
Received: from localhost ([127.0.0.1]:54889 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrf2A-00084J-1r
	for submit <at> debbugs.gnu.org; Tue, 02 Apr 2024 10:22:42 -0400
Received: from mout02.posteo.de ([185.67.36.66]:51447)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rrf27-00083M-NM
 for 70077 <at> debbugs.gnu.org; Tue, 02 Apr 2024 10:22:40 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout02.posteo.de (Postfix) with ESMTPS id EF7A9240103
 for <70077 <at> debbugs.gnu.org>; Tue,  2 Apr 2024 16:22:29 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1712067750; bh=KsuLsrFU8Ol8cigb1fjaOx9Agwj5H0AYLY41KtcG9oY=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=jKD7uwYngrTL8OoGjlW7uL5luUl8Kc6/DlV4K7P3RkUV13cFBxInT4Alg86nKJq9H
 q6ejhxeSJAfhFUrsdcF3gJ1Po/+OIwCoumN+9owqfXuyPMRYWRn8fQ8eIRQXodK93Q
 ifp9yP8Hpb4tU3SCHTM+c2Ak6pT8OBde1UKu9UnG+0j83YFEUBQXgcotUiOaVKv4eW
 jwn1i+mJo21RPH+LhUIEp1wIX6irSI2rUGC5RAbeI5rXgnhg5uKp6Zx4bfPbqJPzoo
 AwviuIzpitms5ftMRFNg8npFGrC88nxeoJ3y03NSNTnLUDq3KoJW2zDBBAscBx0KIY
 GT1cPP+2tGGHg==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V897m05gFz9rxD;
 Tue,  2 Apr 2024 16:22:27 +0200 (CEST)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
 <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN>
Date: Tue, 02 Apr 2024 14:22:29 +0000
Message-ID: <87wmpfsv2y.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

Stefan Monnier <monnier@HIDDEN> writes:

>>> Do I understand correctly that such bundling may result in a situation
>>> when BEFORE is very long? In particular, I am thinking of a situation
>>> when there are changes near the beginning and the end of buffer in quick
>>> succession.
>> Yes!
>
> I'm not sure how to combine the benefits of combining small changes into
> larger ones with the benefits of keeping distant changes separate.

I am not sure if combining small changes into larger ones is at all a
good idea.  The changes are stored in strings, which get allocated and
re-allocated repeatedly.  Repeated string allocations, especially when
strings keep growing towards the buffer size, is likely going to
increase consing and make GCs more frequent.

> I don't want to expose in the API a "sequence of changes", because
> that's difficult to use in general: the only thing a client can conveniently
> do with it is to keep their own copy of the buffer (e.g. in the LSP
> server) and apply the changes in the order given.  But if for some
> reason you need to do something else (e.g. convert the position from
> charpos to line+col) you're in a world of hurt because (except for the
> last element in the sequence) you don't have easy access to the state
> the buffer was in when the change was made.

Aside...
How nice would it be if buffer state and buffer text were persistent.
Like in https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation

> We could expose a list of simultaneous (and thus disjoint) changes,
> which avoids the last problem.  But it's a fair bit more work for us, it
> makes the API more complex for the clients, and it's rarely what the
> clients really want anyway.

FYI, Org parser maintains such a list.
We previously discussed a similar API in
https://yhetil.org/emacs-bugs/87o7iq1emo.fsf@localhost/

> But it did occur to me that we could solve the "disjoint changes"
> problem in the following way: signal the client (from
> `before-change-functions`) when a change is about to be made "far" from
> the currently pending changes.
>
> The API would still expose only (BEG END BEFORE) rather than
> lists/sequences of changes, but the clients could then decide to record
> disjoint changes as they occur and thus create&manage their own
> list/sequence of changes.  More specifically, someone could come
> a create a new API on top which exposes a list/sequence of changes.
>
> The main downside is that this signal of "upcoming disjoint change"
> would have to be called from `before-change-functions`, so the client
> would need to be careful as usual (e.g. don't modify the buffer, don't
> do any blocking operation, ...).

This makes sense.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 1 Apr 2024 17:50:03 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 01 13:50:03 2024
Received: from localhost ([127.0.0.1]:51499 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrLnG-0002Um-UF
	for submit <at> debbugs.gnu.org; Mon, 01 Apr 2024 13:50:03 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:15759)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rrLnE-0002U9-QS
 for 70077 <at> debbugs.gnu.org; Mon, 01 Apr 2024 13:50:01 -0400
Received: from pmg2.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg2.iro.umontreal.ca (Proxmox) with ESMTP id 7C59180B3D;
 Mon,  1 Apr 2024 13:49:51 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711993790;
 bh=VQHhZUhpG2vP8s07j656OeU+juE05tho/VcNcY6O10c=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=dgCgENIJ0EeQRg8CIbm+X3SCyigVLX0zg4QQDsHmklnGrRqjXPD0uAwEHHuXjhgp1
 uyCfQ7ISmCCyoDp67RuAPKSKKnAGsp4g1MvVYIhBtGx7CSt75yvCtm96Hs94Wccp3e
 /Lcd7ZJhCpLrBF/M7XAi5SuJTWEMqYrN1LjzzTRGkgaSoAnbm7pV5pqpfJIzqaj333
 VHyLHTsY/r7ImeyPWiu2kXVAS7RY4aWxTm9amedEawqwKHFU5lUYxc2h6ImJ2Z7Jmk
 Ee7NomeJ8sp3VzXsTLONnOiIx6n3oCDaJZcSx8dctku+ldLNu1cSl1LUHoRBDT4Pgp
 W8Gx7NFB6JDpw==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg2.iro.umontreal.ca (Proxmox) with ESMTP id 1ECF5806EF;
 Mon,  1 Apr 2024 13:49:50 -0400 (EDT)
Received: from alfajor (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id AAA7512046C;
 Mon,  1 Apr 2024 13:49:49 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN> (Stefan Monnier's message
 of "Mon, 01 Apr 2024 10:51:53 -0400")
Message-ID: <jwv1q7p2dqw.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
 <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
Date: Mon, 01 Apr 2024 13:49:48 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.142 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

>>>  ... That's why `track-changes.el` bundles those into a single (BEG END
>>> BEFORE) change, which makes sure BEG/END are currently-valid buffer
>>> positions and thus easy to use.
>>> The previous buffer state is not directly available but can be
>>> easily reconstructed from (BEG END BEFORE).
>> Do I understand correctly that such bundling may result in a situation
>> when BEFORE is very long? In particular, I am thinking of a situation
>> when there are changes near the beginning and the end of buffer in quick
>> succession.
> Yes!

I'm not sure how to combine the benefits of combining small changes into
larger ones with the benefits of keeping distant changes separate.

I don't want to expose in the API a "sequence of changes", because
that's difficult to use in general: the only thing a client can conveniently
do with it is to keep their own copy of the buffer (e.g. in the LSP
server) and apply the changes in the order given.  But if for some
reason you need to do something else (e.g. convert the position from
charpos to line+col) you're in a world of hurt because (except for the
last element in the sequence) you don't have easy access to the state
the buffer was in when the change was made.

We could expose a list of simultaneous (and thus disjoint) changes,
which avoids the last problem.  But it's a fair bit more work for us, it
makes the API more complex for the clients, and it's rarely what the
clients really want anyway.

But it did occur to me that we could solve the "disjoint changes"
problem in the following way: signal the client (from
`before-change-functions`) when a change is about to be made "far" from
the currently pending changes.

The API would still expose only (BEG END BEFORE) rather than
lists/sequences of changes, but the clients could then decide to record
disjoint changes as they occur and thus create&manage their own
list/sequence of changes.  More specifically, someone could come
a create a new API on top which exposes a list/sequence of changes.

The main downside is that this signal of "upcoming disjoint change"
would have to be called from `before-change-functions`, so the client
would need to be careful as usual (e.g. don't modify the buffer, don't
do any blocking operation, ...).


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 1 Apr 2024 14:52:18 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 01 10:52:18 2024
Received: from localhost ([127.0.0.1]:51354 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrJ1C-0002tB-Su
	for submit <at> debbugs.gnu.org; Mon, 01 Apr 2024 10:52:18 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:42354)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rrJ16-0002sp-Gi
 for 70077 <at> debbugs.gnu.org; Mon, 01 Apr 2024 10:52:13 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 70B3410004C;
 Mon,  1 Apr 2024 10:51:59 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711983118;
 bh=pb+RkplP5OqfoZO2aH/H6M/03bzldwj9nKqyfdGAgM0=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=bWRLq+Wauhhzj1ocpEOZ04pd0bfnynvRYQJf7KROmwqydenlfCipR1Nk4KLnj262Y
 PyYKGE7s35jc7lgbHbLccmfRQ29RZ6Lg3uxS81Iqx3OsRu7EFNkIDT7mL/Dng9yqMP
 dQ5z2yfETAndsRe1WbwQgVg/rqZ8l2FKxhjNixEdHi42G2lEVkTn7rikZHtzUyKcIn
 frruEDYSf9PN5f21x4qcwDHfj8ZmWGyxUUS25/lHpLZYkoee6MCAGUjc0J3ZO0N4tS
 tAyk2Ly+v1FBsfl8DzbyDSgRz6SduaFrbILIQ80czs2FxsjDpIcCThxIT0dUaa8NfF
 sRGB0caZFHhcQ==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 68D74100046;
 Mon,  1 Apr 2024 10:51:58 -0400 (EDT)
Received: from alfajor (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 0781F1206CD;
 Mon,  1 Apr 2024 10:51:57 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <877chh8fjk.fsf@localhost> (Ihor Radchenko's message of "Mon, 01
 Apr 2024 11:53:51 +0000")
Message-ID: <jwvr0fp2l3c.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <877chh8fjk.fsf@localhost>
Date: Mon, 01 Apr 2024 10:51:53 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.045 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

>>  ... That's why `track-changes.el` bundles those into a single (BEG END
>> BEFORE) change, which makes sure BEG/END are currently-valid buffer
>> positions and thus easy to use.
>> The previous buffer state is not directly available but can be
>> easily reconstructed from (BEG END BEFORE).
>
> Do I understand correctly that such bundling may result in a situation
> when BEFORE is very long? In particular, I am thinking of a situation
> when there are changes near the beginning and the end of buffer in quick
> succession.

Yes!


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 1 Apr 2024 11:53:57 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Mon Apr 01 07:53:57 2024
Received: from localhost ([127.0.0.1]:49752 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rrGEf-0002Wk-9R
	for submit <at> debbugs.gnu.org; Mon, 01 Apr 2024 07:53:57 -0400
Received: from mout01.posteo.de ([185.67.36.65]:45993)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rrGEZ-0002WQ-Oj
 for 70077 <at> debbugs.gnu.org; Mon, 01 Apr 2024 07:53:55 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout01.posteo.de (Postfix) with ESMTPS id 9F72F24002D
 for <70077 <at> debbugs.gnu.org>; Mon,  1 Apr 2024 13:53:42 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1711972422; bh=rKvUL8YUXnkpjBKeHmMFtY4LD3yIB6YLPmVOOVA9srM=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=XySvGXVMNDdDEwulu+Zedu3bYHTmtDm8oOYW7IihOayGJ19yoRfEDRJpVSl3ljFnV
 Zbu8IcqYxBk12eG/DudC0ryEFc8uj5AoqtUkYGgboQ+r/wW87jkC5j8P0MeDvGQgyL
 y8/IOvK9CKZZaB1zPoZW2Zd4pQW7aFb+bJLob2bEE4cwS5KuOw4Z99xiRTmCHwl4z6
 kTwG8poR/pQ3k0x2Et+i702uIX15QvkHcAe0DmcDo5HTy94Pupwpmkictj5GOe0PRP
 +l4V3Q1ELsm+F8ueNKOORs36UEDmWF0QNwu7kJ0lxkaPB31/wAqs05NWECe+j+WTsB
 6xAuGEivoeErQ==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V7TtW5c5lz9rxF;
 Mon,  1 Apr 2024 13:53:39 +0200 (CEST)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwv7chjvmho.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN>
Date: Mon, 01 Apr 2024 11:53:51 +0000
Message-ID: <877chh8fjk.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, qhong@HIDDEN,
 frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
 acm@HIDDEN, Eli Zaretskii <eliz@HIDDEN>, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

Stefan Monnier <monnier@HIDDEN> writes:

>  ... That's why `track-changes.el` bundles those into a single (BEG END
> BEFORE) change, which makes sure BEG/END are currently-valid buffer
> positions and thus easy to use.
> The previous buffer state is not directly available but can be
> easily reconstructed from (BEG END BEFORE).

Do I understand correctly that such bundling may result in a situation
when BEFORE is very long? In particular, I am thinking of a situation
when there are changes near the beginning and the end of buffer in quick
succession.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 31 Mar 2024 02:58:09 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 22:58:09 2024
Received: from localhost ([127.0.0.1]:46396 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqlOa-0007g0-SV
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 22:58:09 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:22171)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqlOW-0007fU-Ae
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 22:58:06 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id A00B410004C;
 Sat, 30 Mar 2024 22:57:56 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711853875;
 bh=fL5wdRPkydDRn4CcogctMmcdl293Bsos6U6rGGR1pPM=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=g5cr+6A5GDNyg292K6iJ4+hJ6tZIJUBYwm1xvQhbSZBFJP8Y6j34MmsNRG8wKLERx
 giZdBptXa1B872oQqWMbJdbBjmfZeOU/8vJZt60q/2K+WyPUnP/igk39ivBpaY0xWZ
 SIvRRP8zU4x+KohRgndkvXRmbZFhOZplTmdd0kHvjrU580hfmJ4DZl9OsuspGKnrDm
 Jsq5vWS+v+Ken+0efWCYNeXHev3PntXg4LT09rCWwXZU4NxNDXYzeItqrVMfMsP6qP
 AZFZD84/WE4IUdwg1OG0/gZJwLbi2mAJEKQjLGY/FjDee4qzkaVyE4c+DL+EQzsjJy
 8JqfwHEz1xHsw==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id A904D100046;
 Sat, 30 Mar 2024 22:57:55 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 4F01D120828;
 Sat, 30 Mar 2024 22:57:55 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <867chjd5yp.fsf@HIDDEN> (Eli Zaretskii's message of "Sat, 30 Mar
 2024 19:45:02 +0300")
Message-ID: <jwvedbrt8hi.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> <867chjd5yp.fsf@HIDDEN>
Date: Sat, 30 Mar 2024 22:57:54 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.115 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: yantar92@HIDDEN, 70077 <at> debbugs.gnu.org, casouri@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

>> `track-changes-fetch` will call its function argument only once.
>> If several changes happened since last time, `track-changes.el` will
>> summarize them into a single (BEG END BEFORE).
>
> Then I don't think you will be able to guarantee that in all cases.
> You are basically trying to solve a problem that many packages which
> used the modification hooks tried to solve, but where they relied on
> some specifics of the problem they wanted to solve, you are trying to
> solve it in general, and I just don't believe it's possible (but will
> be happy to learn I'm mistaken).

Indeed, there are cases where bugs in our C code will get in the way,
and I'll have to return something like (BEG END :error).
But other than that, the current code already handles
"arbitrary" merging.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 16:45:27 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 12:45:27 2024
Received: from localhost ([127.0.0.1]:46100 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqbpe-0001Zu-Q1
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 12:45:27 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:50908)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rqbpc-0001Z6-2I
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 12:45:24 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rqbpQ-0001e9-3C; Sat, 30 Mar 2024 12:45:13 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=0v1qgFXfC5slJ+ceh6JS1olKcKXd56/fKB9Jj+ODPLw=; b=LXg5lAaLdlPI
 hbvghrpfoePnMTkoO9PDkcry9CAXQ5bJt63OFtkU1oMHCEh4Paz/N3gCdwyHeHJFrWx2MQrC9ctNK
 71tnB2R0Y39X9va6mdE4yrdTbcw8VzjwdsuJnPIA2fZTKZiNdA7aWhb8vrtss+CMKTxL1681/CfYt
 L8gumBfzZcvpBvu/W7I8Q2SEPTrvn86X8eJsBHyFMghl2S+/GddfPzbs3DiPKMz/Srr6830cf0xVi
 xemacgUKNChxPymVsBLPLRU7tOt4A+BW19zr5b3X97aFBXmjMeLn7rggKIelijnwzTVLHAcr3tzcG
 q466FSQYxXjE0Yj1zhX3Ow==;
Date: Sat, 30 Mar 2024 19:45:02 +0300
Message-Id: <867chjd5yp.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwv7chjvmho.fsf-monnier+emacs@HIDDEN> (message from Stefan
 Monnier on Sat, 30 Mar 2024 10:58:40 -0400)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
 <jwv7chjvmho.fsf-monnier+emacs@HIDDEN>
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: yantar92@HIDDEN, 70077 <at> debbugs.gnu.org, casouri@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> From: Stefan Monnier <monnier@HIDDEN>
> Cc: 70077 <at> debbugs.gnu.org,  mail@HIDDEN,  yantar92@HIDDEN,
>   acm@HIDDEN,  joaotavora@HIDDEN,  alan.zimm@HIDDEN,
>   frederic.bour@HIDDEN,  phillip.lord@HIDDEN,
>   stephen_leake@HIDDEN,  casouri@HIDDEN,  qhong@HIDDEN
> Date: Sat, 30 Mar 2024 10:58:40 -0400
> 
> > Otherwise, the above looks like doing all the job in
> > after-change-functions, and it is not clear to me how is that better,
> > since if track-changes-fetch will fetch a series of changes,
> 
> `track-changes-fetch` will call its function argument only once.
> If several changes happened since last time, `track-changes.el` will
> summarize them into a single (BEG END BEFORE).

Then I don't think you will be able to guarantee that in all cases.
You are basically trying to solve a problem that many packages which
used the modification hooks tried to solve, but where they relied on
some specifics of the problem they wanted to solve, you are trying to
solve it in general, and I just don't believe it's possible (but will
be happy to learn I'm mistaken).




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 14:58:54 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 10:58:53 2024
Received: from localhost ([127.0.0.1]:46015 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqaAX-0004xL-Aj
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 10:58:53 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:59256)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqaAV-0004x6-H7
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 10:58:52 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id AAFD5441506;
 Sat, 30 Mar 2024 10:58:43 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711810721;
 bh=P1DGJ8aMWNYPARIfB+KpIiHJqtgrioIXFBM5CiRbo4w=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=em2LDzB5CYfPFTvTSBFwv+5F3DKRHp1Ib1gT0r8Cy5p5IypbqQkYBBypRzk7RfSyR
 i/GW9w1oZAmH6HUTAAsedFb90yVuNb1gJZT8D3bSPLHfztXlKwWkd6y07Oq6ju9d/z
 Ysk1cHZgtiNOS1PPeWvExUA+Bupiyrgqs+xrSxusoHQ5zLVPliWnV+WJTXxI3nFV8r
 qShktHowq3k5XzOU2vp+v5/U+yCmK4yL33XTb5UWGthvC64VYc7F9xmkTy/uPC6pNc
 PfCK9LhmCCFnUyJ8cd2NgjrlOofiGO54NJHygfQBJ3zUs8NcO9se5Qv6+3bop0NOe/
 ZadhanINy376g==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 7FD21441421;
 Sat, 30 Mar 2024 10:58:41 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 296221201E7;
 Sat, 30 Mar 2024 10:58:41 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86cyrcdy80.fsf@HIDDEN> (Eli Zaretskii's message of "Sat, 30 Mar
 2024 09:34:39 +0300")
Message-ID: <jwv7chjvmho.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> <86cyrcdy80.fsf@HIDDEN>
Date: Sat, 30 Mar 2024 10:58:40 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.157 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: yantar92@HIDDEN, 70077 <at> debbugs.gnu.org, casouri@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> If the last point, i.e. the problems caused by limitations on what can
> be safely done from modification hooks, is basically the main
> advantage, then I think I understand the rationale.

That's one of the purposes but not the only one.

> Otherwise, the above looks like doing all the job in
> after-change-functions, and it is not clear to me how is that better,
> since if track-changes-fetch will fetch a series of changes,

`track-changes-fetch` will call its function argument only once.
If several changes happened since last time, `track-changes.el` will
summarize them into a single (BEG END BEFORE).

> deciding how to handle them could be much harder than handling them
> one by one when each one happens.  For example, the BEGIN..END values
> no longer reflect the current buffer contents,

Indeed, one of the purposes of the proposed API is to allow delaying the
handling to a later time, and if you keep individual changes, then it
becomes very difficult to make sense of those changes because they refer
to buffer states that aren't available any more.

That's why `track-changes.el` bundles those into a single (BEG END
BEFORE) change, which makes sure BEG/END are currently-valid buffer
positions and thus easy to use.
The previous buffer state is not directly available but can be
easily reconstructed from (BEG END BEFORE).

> Also, all of your examples seem to have the signal function just call
> track-changes-fetch and do almost nothing else, so I wonder why we
> need a separate function for that,

It gives more freedom to the clients.  For example it would allow
`eglot.el` to get rid of the `eglot--recent-changes` list of changes:
instead of calling `track-changes-fetch` directly from the signal and
collect the changes in `eglot--recent-changes`, it would delay the call
to `track-changes-fetch` to the idle-timer where we run
`eglot--signal-textDocument/didChange` so it would never need to send
more than a "single change" to the server.

Similarly, it would allow changing the specific moment the signal is
called without necessarily moving the moment the changes are performed.
This may be necessary in `eglot.el`: there are various places where we
check the boolean value of `eglot--recent-changes` to know if we're
in sync with the server.  In the current code this value because non-nil
as soon as a modification is made, whereas with my patch it becomes
non-nil only after the end of the command that made the first change.
I don't know yet if the difference is important, but if it is, then
we'd want to ask `track-changes.el` to send the signal more promptly
(there is a FIXME to add such a feature), although we would still want
to delay the `track-changes-fetch` so it's called only at the end of
the command.

> and more specifically what would be a use case where the registered
> signal function does NOT call track-changes-fetch, but does something
> else, and track-changes-fetch is then called outside of the
> signal function.

As mentioned, a use-case I imagine are cases where we want to delay the
processing of changes to an idle timer.  In the current `eglot.el` that
idle timer processes a sequence of changes, but for some use cases it
may too difficult (for the reasons discussed above: it can be difficult
to make sense of earlier changes once they're disconnected from the
corresponding buffer state), so they'd instead prefer to call
`track-changes-fetch` from the idle timer (and thus let
`track-changes.el` combine all those changes).

> Finally, the doc string of track-changes-register does not describe
> the exact place in the buffer-change sequence where the signal
> function will be called, which makes it harder to reason about it.
> Will it be called where we now call signal_after_change or somewhere
> else?

Good point.  Currently it's called via `funcall-later` which isn't in
`master` but can be thought of as `run-with-timer` with a 0 delay.
[ In reality it relies on `pending_funcalls` instead.  ]

But indeed, I have a FIXME to let the caller request to signal
as soon as possible, i.e. directly from the `after-change-functions`.

I think it's better to use something like `funcall-later` by default
than to signal directly from `after-change-functions` because most
coders don't realize that `after-change-functions` can be called
thousands of times for a single command (and in normal testing, they'll
probably never bump into such a case either).  So waiting for the end of
the current command (via `funcall-later`) provides a behavior closer to
what most naive coders expect, I believe, and will save them from the
corner cased they weren't aware of.

> And how do you guarantee that the signal function will not be
> called again until track-changes-fetch is called?

By removing the tracker from `track-changes--clean-trackers` (and
re-adding it once `track-changes-fetch` is finished, which is the main
reason I make `track-changes-fetch` call a function argument rather
than making it return (BEG END CHANGES)).

In a later email you wrote:

>>     (concat (buffer-substring (point-min) beg)
>>             before
>>             (buffer-substring end (point-max)))
>
> But if you get several changes, the above will need to be done in
> reverse order, back-to-front, no?

No, because you (the client) never get several changes.

>> I don't mean to suggest to do that, since it's costly for large
>> buffers
> Exactly.  But if the buffer _is_ large, then what to do?

It all depends on the specific cases.  E.g. in the case of `eglot.el` we
don't need the full content of the buffer before the change.  We only
really need to know how many newlines were in `before` as well as some
kind of length of the last line of `before`.  I compute that
by inserting `before` into a temp buffer.  Note that this is
proportional to the size of `before` and not to the total size of
the buffer.

If we want to do better, I think we'd then need a more complex API where
the client can specify more precisely (presumably via some kind of
function) what information we need to record about the "before state"
(and how to update that information as new changes are performed).

I don't have a good idea for what such an API could look like.

>> Also, it would fix only the problem of pairing, and not the other ones.
> So the main/only problem this mechanism solves is the lack of pairing
> between before/after calls to modification hooks?

No, the text you cite is saying the opposite: that we don't want to
solve only the pairing.  I hope/want it to solve all the
problems I mentioned:

    - before and after calls are not necessarily paired.
    - the beg/end values don't always match.
    - there can be thousands of calls from within a single command.
    - these hooks are run at a fairly low-level so there are things they
      really shouldn't do, such as modify the buffer or wait.
    - the after call doesn't get enough info to rebuild the before-change state,
      so some callers need to use both before-c-f and after-c-f (and then
      deal with the first two points above).

I don't claim it solves them all in a perfect way, tho.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 14:09:52 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 10:09:52 2024
Received: from localhost ([127.0.0.1]:45921 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqZP3-0002U0-Qu
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 10:09:52 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:46519)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqZOy-0002Tg-Bv
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 10:09:48 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id D65084413F2;
 Sat, 30 Mar 2024 10:09:35 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711807774;
 bh=I+8yjTTQyk5OdK+fv8vRlM3c6J99C5fgX5uowxede/k=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=CFCDpo+eUwsx5QKS41cGWB1xQg1L98mG5R9OJ+2ZY5A0QFCrUZDssdUfES08/BxFX
 vh6L8giqk6TFGklZid0HHKNM2hrkFQfwthcPB+NUTXN/jSvQZEfcPijs/Q+krZFIeP
 HZdacnPuOiivyoXGpXLKmJtEHN+TwOJd13Te28edAFArc7w2yWQmbio3eMabNpDCvW
 FHA7BtCL1yOUsS5OdOT5qqJBr9tJ1IEYhS+i64D2oayen6Cixqs3WVezjnVV7H8T1V
 wG646GlTRjsIzoAJmJxZcsHErkxyr62GDTQ8DBJ5XCRDfFX1Jwv8dCRrrpbYKljvST
 zRjLdpAT41Q7w==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 53C0B4413C2;
 Sat, 30 Mar 2024 10:09:34 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id EBE5712077F;
 Sat, 30 Mar 2024 10:09:33 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <87sf082gku.fsf@localhost> (Ihor Radchenko's message of "Sat, 30
 Mar 2024 09:51:13 +0000")
Message-ID: <jwvh6gnvncn.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <87sf082gku.fsf@localhost>
Date: Sat, 30 Mar 2024 10:09:33 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.173 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: Ihor Radchenko <yantar92@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Yuan Fu <casouri@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?windows-1252?B?RnLpZOlyaWM=?= Bour <frederic.bour@HIDDEN>,
 =?windows-1252?B?Sm/jbyBU4XZvcmE=?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>,
 Phillip Lord <phillip.lord@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

>> The driving design was:
>>
>> - Try to provide enough info such that it is possible and easy to
>>   maintain a copy of the buffer simply by applying the reported changes.
>>   E.g. for uses such as `eglot.el` or `crdt.el`.
>> - Make the API less synchronous: take care of combining small changes
>>   into larger ones, and let the clients decide when they react to changes.
>
> Before we discuss the API, may you allow me to raise one critical
> concern: bug#65451.

Thanks, I wasn't aware of it.

Note that bug#65451 would affect `track-changes.el` but not its clients
nor its API.

Well, I guess it couldn't really insulate its clients from such bugs,
but it would be `track-changes.el`s responsibility to detect such
problems and pass down to its clients something saying "oops we have
a problem, you have to do a full-resync".

> If my reading of the patch is correct, your code is relying upon the
> buffer changes arriving in the same order the changes are being made.

Indeed.

> I am skeptical that you can achieve the desired patch goals purely
> relying upon before/after-change-functions, without reaching down to C
> internals.

There's a FIXME for that:

    ;; FIXME: Try and do some sanity-checks (e.g. looking at `buffer-size'),
    ;; to detect if/when we somehow missed some changes.

All the current non-trivial users of *-change-functions have such sanity
checks.  They're designed to handle those nasty cases where we have
a bug in the C code.  I don't claim my new API can magically paper over
those bugs.  The intention to deal with those bugs is:

- When they're detected, call the clients with a special value for
  `before` which says that we lost track of some change, so the client
  knows that it may be necessary to do a full resync.
  Luckily for many/most clients a full-resync is only a performance
  problem (e.g. having to reparse the whole file), tho for some (like
  lentic) I suspect it can result in an actual loss of information.

- Try and fix them, of course.  Alan has done a great job in the past
  fixing many of them (tho apparently still not all).
  [ And also a great job of convincing me that we *can* and thus
    *should* fix them.  ]

IOW, no magic bullet: the clients would still have to somehow handle such
a "full-resync"s.

The main advantage would be that the job of sanity-checking would be
taken care of by `track-changes.el` and the clients would have to check
only `before` for a special value to indicate a problem.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 13:39:36 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 09:39:36 2024
Received: from localhost ([127.0.0.1]:44232 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqYvn-0000Qc-Mc
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 09:39:36 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:7589)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqYvl-0000QP-IS
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 09:39:34 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 55FE210004C;
 Sat, 30 Mar 2024 09:39:25 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711805964;
 bh=d545QNFiFMZl8t0CHYjAOALXi3WjXLqTBerIP/ljxf4=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=V2wgVtIFBRNPk4yzG2094hC7tjhxbHDUzFLyB12RpizS9Dvkygn7MsOle6GkDGazi
 Qlzd3l+Lnx5vtqBZjgYq9KYTCpg9//JiDA/zIAgTCA+NPnxDHlEDK4GQk3g7wNJhmQ
 r8wgvw3MGh/bm1s0TcAK8BU6apHev4Wbl04BKcqeNTPWbHd04oEe8BhXNmZDfOzaAb
 a2+DJFFunetR7KwxjB44qqS+sG9EKUfTreGue/xtR1E6lnXXylKCYdriqL2aNxGxds
 I9bLLpuIIlo0BwvO+cSchwZzHAypV9C1kBFemgWwYG4uUjVRW+0PFeT6CjdYOvNi3l
 9ukf3ngNd3rkQ==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 67889100046;
 Sat, 30 Mar 2024 09:39:24 -0400 (EDT)
Received: from pastel (unknown [45.72.201.215])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 194A812016C;
 Sat, 30 Mar 2024 09:39:24 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: phillip.lord@HIDDEN
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <6081fabd1e9e701b1b26848fbe0e403d@HIDDEN> (phillip lord's
 message of "Sat, 30 Mar 2024 08:06:03 -0400")
Message-ID: <jwvsf07vo6j.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <e94e6fc3b497db294a87fa2b4c388a11@HIDDEN>
 <jwva5mgwt05.fsf-monnier+emacs@HIDDEN>
 <6081fabd1e9e701b1b26848fbe0e403d@HIDDEN>
Date: Sat, 30 Mar 2024 09:39:22 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL 0.000 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: Ihor Radchenko <yantar92@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Yuan Fu <casouri@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?windows-1252?B?RnLpZOlyaWM=?= Bour <frederic.bour@HIDDEN>,
 =?windows-1252?B?Sm/jbyBU4XZvcmE=?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> Ah, yes, you are correct, I had missed that one. As you note, it would
> be costly, especially because if you wanted to do anything with that
> data, you would probably end up dumping it into a temp buffer.

That's indeed what I end up doing in the `eglot.el` case.
But if the API wants to tackle the performance issue of combining many
small changes into a larger one, I don't know how we can do better.

Another idea I considered was to keep a whole buffer as "previous
contents" (instead of a string that covers the modified area).
In some cases it would be more efficient, but the common case would be
a lot more costly.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 13:32:13 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 09:32:13 2024
Received: from localhost ([127.0.0.1]:44215 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqYof-0008Ot-8W
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 09:32:13 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:48768)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rqYoc-0008ON-Kc
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 09:32:12 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rqYoS-0002CF-HS; Sat, 30 Mar 2024 09:32:00 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=rJNQWJE2QusLttobzi5Ikj72ifkE6XXcINgXyUtvyVs=; b=j/RdnMYnfnwj
 vT1dgV9cKl9UplMckdczuVP8Ry557uQvEBD/8V2d89ml/AaoLmyA2voQ+fYWw00qdpUf+jXZnyV7H
 m1tkS2n0t3kRLyF9MfWqzlxckjUbnf/SJjM86X2iQ0XyD06HUMtlT4NTxq6rc43DbCE+fAeFuryHi
 T9IeIGqwAknwF0Nb+yo1KUtm4ny9RNdOqBFPRadEwstVmg1N0l1FQnAXJdas/90iEtF095xjQEgta
 PGj4xFfJavUILgLKz21S+o1H5KNNF5sJ1DRswtSD9ThipZzCQ+mCCMV4kTmv3IrDwfEmS355hcMLu
 WbJh0jiS+mu8Ale3AHfTkg==;
Date: Sat, 30 Mar 2024 16:31:56 +0300
Message-Id: <86il13dewj.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
In-Reply-To: <87frw77t7g.fsf@localhost> (message from Ihor Radchenko on Sat,
 30 Mar 2024 13:19:31 +0000)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN> <87sf082gku.fsf@localhost>
 <86v853dguu.fsf@HIDDEN> <87frw77t7g.fsf@localhost>
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, yantar92@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, phillip.lord@HIDDEN,
 stephen_leake@HIDDEN, alan.zimm@HIDDEN, monnier@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> From: Ihor Radchenko <yantar92@HIDDEN>
> Cc: 70077 <at> debbugs.gnu.org, casouri@HIDDEN, yantar92@HIDDEN,
>  qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
>  mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
>  alan.zimm@HIDDEN, monnier@HIDDEN, phillip.lord@HIDDEN
> Date: Sat, 30 Mar 2024 13:19:31 +0000
> 
> Looking at the implementation of `track-changes--before/after', I can
> see that it is updating the BEFORE string and these updates implicitly
> assume that the changes arrive in order - which is not true in some edge
> cases described in the bug report.

So I guess you have detected a bug in the implementation before it was
even finalized and installed.




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 13:19:36 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 09:19:36 2024
Received: from localhost ([127.0.0.1]:44197 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqYcS-0007gt-0Z
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 09:19:36 -0400
Received: from mout02.posteo.de ([185.67.36.66]:40497)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rqYcO-0007g3-JX
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 09:19:34 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout02.posteo.de (Postfix) with ESMTPS id BECD4240109
 for <70077 <at> debbugs.gnu.org>; Sat, 30 Mar 2024 14:19:24 +0100 (CET)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1711804764; bh=18D/xibYEnioWpFFPVKAGiYFyDdZCmS+JhopXNpS6VE=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=dq7/JOhghVP5rfmVktZAiGB44wlFGtpMI93elaCyS/Ik5ZA7Uuw8J1jvjtBm8yPSs
 khz+aVeX+4PnYs3jAs9W7I7j11jx8RpYUrRUhMEUFwvF3FqyQwCsNfYu6m74D1x+Ia
 iWkbrIt37w3Aaq3slsrX4K0lKZvaHZuo48qKI2I9tJJyuubG0XkR+gsvXVgn5Q5Z43
 4gfHqTdwJ/oKRHV9zROh7TCvlLmu482AK/m+830OWC1ngAn1uz/ENJLcAkVGGkE45a
 Kqw5DhQZvJTHOk+Jy4M+TEROrgITZP+Yq6fE65eFjDrPnqxtyF7UBeg49P75Xte84t
 QsK5/LTQ5TK+w==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V6HtL0Ysbz6trs;
 Sat, 30 Mar 2024 14:19:21 +0100 (CET)
From: Ihor Radchenko <yantar92@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86v853dguu.fsf@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <87sf082gku.fsf@localhost>
 <86v853dguu.fsf@HIDDEN>
Date: Sat, 30 Mar 2024 13:19:31 +0000
Message-ID: <87frw77t7g.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, yantar92@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, phillip.lord@HIDDEN,
 stephen_leake@HIDDEN, alan.zimm@HIDDEN, monnier@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

Eli Zaretskii <eliz@HIDDEN> writes:

> In any case, who and where said the changes will be fetched by
> track-changes-fetch must be in the order they were made? why is the
> order at all significant?

I have concerns that the following API promise can be fulfilled:

    (defun track-changes-fetch (id func)
      "Fetch the pending changes.
    ID is the tracker ID returned by a previous `track-changes-register'.
    FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
    where BEGIN..END delimit the region that was changed since the last
    time `track-changes-fetch' was called and BEFORE is a string containing
                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    the previous content of that region.
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Looking at the implementation of `track-changes--before/after', I can
see that it is updating the BEFORE string and these updates implicitly
assume that the changes arrive in order - which is not true in some edge
cases described in the bug report.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 12:50:07 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 08:50:06 2024
Received: from localhost ([127.0.0.1]:44167 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqY9t-0006Dj-1j
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 08:50:06 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:35182)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rqY9q-0006Cr-KN
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 08:50:04 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rqY9e-0002Wk-Tk; Sat, 30 Mar 2024 08:49:50 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=jRUo7066nSNub89wxZGo6KS6XwyIoZIZVE6u3xzTo+c=; b=cGRhskEQCLCz
 dM1YU5AatkwF5J/kAf9WjSVae9MN4D6NgyBihPi2vz4eetiLmwoaS1G/8EGXFYjUj8r9PTXoZZJST
 YHXhFGFXspSzAtROZR41oQQ5sE0n3TXJjozI7ye5IXgwTmk+Nh4AtPt4S/VXaLkx80NFJ9/OnCEiu
 wBfYMaJPWGGANds2xwSuPWpL6bsPdVYYpxYY3meoNHRaPhUg9GlPP5gCmKrBMBB+RWw09e0busBwK
 rxrJkR4EuI/ZlZ+Lg4CxS1pdlPkqhLZ2wcWAM8T7u9o88bo+3sIeNud5pmSKOyqkS3ViADJJypF64
 FyC1VZrVsb25pkd947F2/w==;
Date: Sat, 30 Mar 2024 15:49:45 +0300
Message-Id: <86v853dguu.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Ihor Radchenko <yantar92@HIDDEN>
In-Reply-To: <87sf082gku.fsf@localhost> (message from Ihor Radchenko on Sat,
 30 Mar 2024 09:51:13 +0000)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN> <87sf082gku.fsf@localhost>
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, yantar92@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, phillip.lord@HIDDEN,
 stephen_leake@HIDDEN, alan.zimm@HIDDEN, monnier@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> Cc: casouri@HIDDEN, yantar92@HIDDEN, qhong@HIDDEN,
>  frederic.bour@HIDDEN, joaotavora@HIDDEN, mail@HIDDEN,
>  acm@HIDDEN, stephen_leake@HIDDEN, alan.zimm@HIDDEN,
>  monnier@HIDDEN, phillip.lord@HIDDEN
> From: Ihor Radchenko <yantar92@HIDDEN>
> Date: Sat, 30 Mar 2024 09:51:13 +0000
> 
> Before we discuss the API, may you allow me to raise one critical
> concern: bug#65451.
> 
> If my reading of the patch is correct, your code is relying upon the
> buffer changes arriving in the same order the changes are being made.
> However, it is not always the case, as demonstrated in the linked bug
> report.

That bug report is about after-change-functions.  Since Stefan didn't
yet describe where will the changes be recorded, it doesn't
necessarily follow that your worries are justified.  They could be, of
course, but we should decide that after we hear the details.

In any case, who and where said the changes will be fetched by
track-changes-fetch must be in the order they were made? why is the
order at all significant?




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 12:06:19 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 08:06:19 2024
Received: from localhost ([127.0.0.1]:44127 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqXTX-00042L-5n
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 08:06:19 -0400
Received: from cloud103.planethippo.com ([78.129.190.68]:35612)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <phillip.lord@HIDDEN>) id 1rqXTR-00041d-Gk
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 08:06:18 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;
 d=russet.org.uk; s=default; h=Content-Transfer-Encoding:Content-Type:
 Message-ID:References:In-Reply-To:Subject:Cc:To:From:Date:MIME-Version:Sender
 :Reply-To:Content-ID:Content-Description:Resent-Date:Resent-From:
 Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:
 List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive;
 bh=/iaqUNq9u594bqzNvPbwgEfgIFIkxnHjUwAGwN54tTc=; b=KDbVD7g4wxOqCr/WLU2r3ajabg
 oAEY4sB+rhOsKUietK94vpVJrFDIGQpLrH908XxQsHNzm2CgqsPUgn3cz4Kp2/e8RrfQXGi614g/i
 HonVgKixLxMRtTHdrqIFwwRiX3CZBM5UORNfeF+9WF27+6Ey7lpqrYijN4+lD2X3KAgS2E8pNhq8Z
 /3KnLh0+yK/bwPKFCCjgg7FNx8ZYevWTn1bNDpn08wi/3doPr7UrT/dd0ytqbHNeepAK3dHigkjSt
 9PYis+ncZNZ+jIdrP4fhWiaTBZr6jmdz+dsl1itngrrJ5hHNrGTxH1lVWSNKkW//zO7zldvv7Du1L
 zGDJyvpw==;
Received: from [::1] (port=45052 helo=cloud103.planethippo.com)
 by cloud103.planethippo.com with esmtpa (Exim 4.96.2)
 (envelope-from <phillip.lord@HIDDEN>) id 1rqXTA-000cwf-2M;
 Sat, 30 Mar 2024 08:06:04 -0400
MIME-Version: 1.0
Date: Sat, 30 Mar 2024 08:06:03 -0400
From: phillip.lord@HIDDEN
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwva5mgwt05.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <e94e6fc3b497db294a87fa2b4c388a11@HIDDEN>
 <jwva5mgwt05.fsf-monnier+emacs@HIDDEN>
User-Agent: Roundcube Webmail/1.6.0
Message-ID: <6081fabd1e9e701b1b26848fbe0e403d@HIDDEN>
X-Sender: phillip.lord@HIDDEN
Content-Type: text/plain; charset=US-ASCII;
 format=flowed
Content-Transfer-Encoding: 7bit
X-AntiAbuse: This header was added to track abuse,
 please include it with any abuse report
X-AntiAbuse: Primary Hostname - cloud103.planethippo.com
X-AntiAbuse: Original Domain - debbugs.gnu.org
X-AntiAbuse: Originator/Caller UID/GID - [47 12] / [47 12]
X-AntiAbuse: Sender Address Domain - russet.org.uk
X-Get-Message-Sender-Via: cloud103.planethippo.com: authenticated_id:
 phillip.lord@HIDDEN
X-Authenticated-Sender: cloud103.planethippo.com: phillip.lord@HIDDEN
X-Source: 
X-Source-Args: 
X-Source-Dir: 
X-Spam-Score: 3.0 (+++)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 Content preview:  On 2024-03-29 18:59, Stefan Monnier wrote: >> If I remember
 correctly, I think this wouldn't be enough for my >> use. You keep two buffers
 in sync, you have to use >> before-change-function -- it is o [...] 
 Content analysis details:   (3.0 points, 10.0 required)
 pts rule name              description
 ---- ---------------------- --------------------------------------------------
 3.0 MANY_TO_CC             Sent to 10+ recipients
 -0.0 SPF_PASS               SPF: sender matches SPF record
 0.0 SPF_HELO_NONE          SPF: HELO does not publish an SPF Record
X-Debbugs-Envelope-To: 70077
Cc: Ihor Radchenko <yantar92@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Yuan Fu <casouri@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?UTF-8?Q?Fr=C3=A9d=C3=A9ric_Bour?= <frederic.bour@HIDDEN>,
 =?UTF-8?Q?Jo=C3=A3o_T=C3=A1vora?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: 2.0 (++)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 
 Content preview:  On 2024-03-29 18:59, Stefan Monnier wrote: >> If I remember
    correctly, I think this wouldn't be enough for my >> use. You keep two buffers
    in sync, you have to use >> before-change-function -- it is o [...] 
 
 Content analysis details:   (2.0 points, 10.0 required)
 
  pts rule name              description
 ---- ---------------------- --------------------------------------------------
  3.0 MANY_TO_CC             Sent to 10+ recipients
 -0.0 SPF_PASS               SPF: sender matches SPF record
  0.0 SPF_HELO_NONE          SPF: HELO does not publish an SPF Record
 -1.0 MAILING_LIST_MULTI     Multiple indicators imply a widely-seen list
                             manager

On 2024-03-29 18:59, Stefan Monnier wrote:
>> If I remember correctly, I think this wouldn't be enough for my
>> use. You keep two buffers in sync, you have to use
>> before-change-function -- it is only before any change that the two
>> buffers are guaranteed to be in sync and it is this that allows you to
>> work out what the `start' and `end' positions mean in the copied
>> buffer. Afterward, you cannot work out what the end position because
>> you don't know if the change is a change, insertion, deletion or both.
> 
> I believe the API I propose does provide that information: you can
> recover the state of the buffer before the change (or more 
> specifically,
> the state of the buffer as of the last time you called
> track-changes-fetch) from the BEG/END/BEFORE arguments as follows:
> 
>     (concat (buffer-substring (point-min) beg)
>             before
>             (buffer-substring end (point-max)))
> 
> I don't mean to suggest to do that, since it's costly for large
> buffers, but to illustrate that the information is properly preserved.


Ah, yes, you are correct, I had missed that one. As you note, it would 
be costly,
especially because if you wanted to do anything with that data, you 
would probably
end up dumping it into a temp buffer.


>> Last time I checked, I did find relatively few primitives that were 
>> guilty
>> of being inconsistent -- in the case of `subst-char-in-region', it 
>> returned
>> the maximal area of effect before the and the minimal area of effect
>> after. Would it not be easier to fix these?
> 
> [ IIRC `revert-buffer` has a similar behavior, and in that case the
>   difference can be large since the "before" covers the whole buffer.  
> ]
> 
> Also, it would fix only the problem of pairing, and not the other ones.


Understood. It would be interesting to know how many primitives cause 
issues though.

Phil




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 09:51:18 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 05:51:18 2024
Received: from localhost ([127.0.0.1]:44016 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqVMr-0002gX-N8
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 05:51:18 -0400
Received: from mout02.posteo.de ([185.67.36.66]:50907)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rqVMp-0002fn-6S
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 05:51:16 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout02.posteo.de (Postfix) with ESMTPS id 129D6240105
 for <70077 <at> debbugs.gnu.org>; Sat, 30 Mar 2024 10:51:06 +0100 (CET)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1711792267; bh=kYgyL7i/Xnjy/Fx8yCO1vgSbFmBI3BXPCbHoWgqN3iI=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=OV1Ig1sw0Y66ZKpekqb6U4hCHl61Bb9rcYTw+syJP7wWeQVWAt8ExvDj1mbbWFNsv
 gnyi8/1U4/af2JcuLhzT1sKDlUkJkppdbeaKgVAupqzgXRb7wCVsTfdS3SP8GRw+rr
 mTjEhLiqgIpFq213saDGzgDH8SP3+wDBim/ILRSVGDGu5bvLlEAVF5rn9jcb20u+b1
 lRO5y3nGebpNdvksvN8SGNR7QxhoRybO+3Kp23JDZcb8DIuuRBs2PhEddM0RGF8HXZ
 95npXwt8jLhBNlM9LIN9NQoi2X7ezqVCgx5uY3t6ZFbSIGLS4+TL80LOxCm6EY4OsK
 iuqJQkUTMUpBg==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V6CG00yNGz9rxG;
 Sat, 30 Mar 2024 10:51:03 +0100 (CET)
From: Ihor Radchenko <yantar92@HIDDEN>
To: "Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of
 text editors" <bug-gnu-emacs@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvle615806.fsf@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
Date: Sat, 30 Mar 2024 09:51:13 +0000
Message-ID: <87sf082gku.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: Yuan Fu <casouri@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Ihor Radchenko <yantar92@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?utf-8?B?RnLDqWTDqXJpYw==?= Bour <frederic.bour@HIDDEN>,
 =?utf-8?B?Sm/Do28gVMOhdm9yYQ==?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>, monnier@HIDDEN,
 Phillip Lord <phillip.lord@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army knife of
text editors" <bug-gnu-emacs@HIDDEN> writes:

> The driving design was:
>
> - Try to provide enough info such that it is possible and easy to
>   maintain a copy of the buffer simply by applying the reported changes.
>   E.g. for uses such as `eglot.el` or `crdt.el`.
> - Make the API less synchronous: take care of combining small changes
>   into larger ones, and let the clients decide when they react to changes.

Before we discuss the API, may you allow me to raise one critical
concern: bug#65451.

If my reading of the patch is correct, your code is relying upon the
buffer changes arriving in the same order the changes are being made.
However, it is not always the case, as demonstrated in the linked bug
report.

I am skeptical that you can achieve the desired patch goals purely
relying upon before/after-change-functions, without reaching down to C
internals.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 30 Mar 2024 09:51:40 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 05:51:39 2024
Received: from localhost ([127.0.0.1]:44020 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqVN3-0002gv-5W
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 05:51:39 -0400
Received: from lists.gnu.org ([2001:470:142::17]:44746)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <yantar92@HIDDEN>) id 1rqVN1-0002gi-0S
 for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 05:51:27 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <yantar92@HIDDEN>)
 id 1rqVMt-0001no-0y
 for bug-gnu-emacs@HIDDEN; Sat, 30 Mar 2024 05:51:19 -0400
Received: from mout01.posteo.de ([185.67.36.65])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <yantar92@HIDDEN>)
 id 1rqVMn-0006Ch-UE
 for bug-gnu-emacs@HIDDEN; Sat, 30 Mar 2024 05:51:17 -0400
Received: from submission (posteo.de [185.67.36.169]) 
 by mout01.posteo.de (Postfix) with ESMTPS id E0A2B24002D
 for <bug-gnu-emacs@HIDDEN>; Sat, 30 Mar 2024 10:51:06 +0100 (CET)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.net; s=2017;
 t=1711792266; bh=kYgyL7i/Xnjy/Fx8yCO1vgSbFmBI3BXPCbHoWgqN3iI=;
 h=From:To:Cc:Subject:Date:Message-ID:MIME-Version:Content-Type:
 From;
 b=rDCSGP6/l1mL3bMtWWUnGOFi6EHUmKt5R5aLvVsw6foCWV4Ql2Il3N3YgvLub+kV3
 gQDGLVtbHWWfjWq7TFMPF1OS1fFAXbH7TyFLY8q+i6Kc0Psp1xT+pnQgSQetC4Sz1I
 eCTSHov87m26XscbzDux5fmyjPu6HBwIM6V/W66UTabFRCZxbx95TJgx0haSXxark6
 DG7j9TzaRLdJ64pIsRSo/YjFaT5aeqRmwmQ7C+USaQBHjwR611NJtIapQ/KPTvpr05
 IURJorJ1/qZSnb/5Xo5vAl481PtQtIfRIWJOmNP5Jsvm6kwiyIu9m8d4QheDTw3VeQ
 NiD/ZGQMmUrmA==
Received: from customer (localhost [127.0.0.1])
 by submission (posteo.de) with ESMTPSA id 4V6CG00yNGz9rxG;
 Sat, 30 Mar 2024 10:51:03 +0100 (CET)
From: Ihor Radchenko <yantar92@HIDDEN>
To: "Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of
 text editors" <bug-gnu-emacs@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvle615806.fsf@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
Date: Sat, 30 Mar 2024 09:51:13 +0000
Message-ID: <87sf082gku.fsf@localhost>
MIME-Version: 1.0
Content-Type: text/plain
Received-SPF: pass client-ip=185.67.36.65; envelope-from=yantar92@HIDDEN;
 helo=mout01.posteo.de
X-Spam_score_int: -43
X-Spam_score: -4.4
X-Spam_bar: ----
X-Spam_report: (-4.4 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,
 DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1,
 RCVD_IN_DNSWL_MED=-2.3, RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001,
 SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no
X-Spam_action: no action
X-Spam-Score: 4.0 (++++)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 Content preview:  Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army
 knife of text editors" <bug-gnu-emacs@HIDDEN> writes: > The driving design
 was: > > - Try to provide enough info such that it is possible and easy to
 > maintain a copy of the buffer simply by applying the reported changes.
 > E.g. for uses such as `eglot. [...] 
 Content analysis details:   (4.0 points, 10.0 required)
 pts rule name              description
 ---- ---------------------- --------------------------------------------------
 3.0 MANY_TO_CC             Sent to 10+ recipients
 1.0 SPF_SOFTFAIL           SPF: sender does not match SPF record (softfail)
 -0.0 SPF_HELO_PASS          SPF: HELO matches SPF record
X-Debbugs-Envelope-To: submit
Cc: Yuan Fu <casouri@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Ihor Radchenko <yantar92@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?utf-8?B?RnLDqWTDqXJpYw==?= Bour <frederic.bour@HIDDEN>,
 =?utf-8?B?Sm/Do28gVMOhdm9yYQ==?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>, monnier@HIDDEN,
 Phillip Lord <phillip.lord@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: 3.0 (+++)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 
 Content preview:  Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army
    knife of text editors" <bug-gnu-emacs@HIDDEN> writes: > The driving design
    was: > > - Try to provide enough info such that it is possible and easy to
    > maintain a copy of the buffer simply by applying the reported changes.
   > E.g. for uses such as `eglot. [...] 
 
 Content analysis details:   (3.0 points, 10.0 required)
 
  pts rule name              description
 ---- ---------------------- --------------------------------------------------
  3.0 MANY_TO_CC             Sent to 10+ recipients
  1.0 SPF_SOFTFAIL           SPF: sender does not match SPF record (softfail)
 -0.0 SPF_HELO_PASS          SPF: HELO matches SPF record
 -1.0 MAILING_LIST_MULTI     Multiple indicators imply a widely-seen list
                             manager

Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army knife of
text editors" <bug-gnu-emacs@HIDDEN> writes:

> The driving design was:
>
> - Try to provide enough info such that it is possible and easy to
>   maintain a copy of the buffer simply by applying the reported changes.
>   E.g. for uses such as `eglot.el` or `crdt.el`.
> - Make the API less synchronous: take care of combining small changes
>   into larger ones, and let the clients decide when they react to changes.

Before we discuss the API, may you allow me to raise one critical
concern: bug#65451.

If my reading of the patch is correct, your code is relying upon the
buffer changes arriving in the same order the changes are being made.
However, it is not always the case, as demonstrated in the linked bug
report.

I am skeptical that you can achieve the desired patch goals purely
relying upon before/after-change-functions, without reaching down to C
internals.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 06:46:35 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 02:46:35 2024
Received: from localhost ([127.0.0.1]:43804 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqSU6-0001xC-Uk
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 02:46:35 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:45126)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rqSU3-0001wy-VE
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 02:46:33 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rqSTq-0005zL-VJ; Sat, 30 Mar 2024 02:46:18 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-version:References:Subject:In-Reply-To:To:From:
 Date; bh=eM/X+2XyayQZ85/xhQiFHwO1OZXdHxTi+afP3z1LxM8=; b=sX1m+k5euJ055Q8vlmP+
 ACZu1bEILh82zeASRx3+pASQ5awOkiwZ5Mmorv4TPcEmmEPqoCrcqZC/OXLpDuB8MKCnxgF6Mvk/F
 ECjUezd4laitkubPDC1vEDXG13Vf6jSbF6z3FYKGepVh/8woPAZE3TK8hZVJoPvElM4eCQcjmC0mK
 RPn+dl1GmM0EB1zZVQQQfaQwssN5hP1lDR3leLUhBEw/pAMvZWnFx6d8Y1PCQrJWUQzaaG6VaV3PL
 gjdxHCs2WwMnhrKdNk+Xb7ME/H2HvbJJ1m9ZZo9i3/0kNg8y6a2w8IDwMG6p8J+iuAzSX+lRb89iU
 xpZwrL18+K0H4g==;
Date: Sat, 30 Mar 2024 09:46:15 +0300
Message-Id: <86bk6wdxoo.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwva5mgwt05.fsf-monnier+emacs@HIDDEN> (bug-gnu-emacs@HIDDEN)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN>
 <e94e6fc3b497db294a87fa2b4c388a11@HIDDEN>
 <jwva5mgwt05.fsf-monnier+emacs@HIDDEN>
MIME-version: 1.0
Content-type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: yantar92@HIDDEN, 70077 <at> debbugs.gnu.org, casouri@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> Cc: Ihor Radchenko <yantar92@HIDDEN>, 70077 <at> debbugs.gnu.org,
>  Yuan Fu <casouri@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
>  Frédéric Bour <frederic.bour@HIDDEN>,
>  João Távora <joaotavora@HIDDEN>,
>  Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
>  Stephen Leake <stephen_leake@HIDDEN>,
>  Alan Zimmerman <alan.zimm@HIDDEN>
> Date: Fri, 29 Mar 2024 18:59:38 -0400
> From:  Stefan Monnier via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <bug-gnu-emacs@HIDDEN>
> 
> > If I remember correctly, I think this wouldn't be enough for my
> > use. You keep two buffers in sync, you have to use
> > before-change-function -- it is only before any change that the two
> > buffers are guaranteed to be in sync and it is this that allows you to
> > work out what the `start' and `end' positions mean in the copied
> > buffer. Afterward, you cannot work out what the end position because
> > you don't know if the change is a change, insertion, deletion or both.
> 
> I believe the API I propose does provide that information: you can
> recover the state of the buffer before the change (or more specifically,
> the state of the buffer as of the last time you called
> track-changes-fetch) from the BEG/END/BEFORE arguments as follows:
> 
>     (concat (buffer-substring (point-min) beg)
>             before
>             (buffer-substring end (point-max)))

But if you get several changes, the above will need to be done in
reverse order, back-to-front, no?  And before you do that, you cannot
really handle the changes, right?

> I don't mean to suggest to do that, since it's costly for large
> buffers

Exactly.  But if the buffer _is_ large, then what to do?

> Also, it would fix only the problem of pairing, and not the other ones.

So the main/only problem this mechanism solves is the lack of pairing
between before/after calls to modification hooks?




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 06:35:00 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 02:35:00 2024
Received: from localhost ([127.0.0.1]:43793 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqSIt-0001NT-MI
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 02:35:00 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:56080)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rqSIr-0001Mv-1e
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 02:34:58 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rqSId-0000B0-56; Sat, 30 Mar 2024 02:34:43 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=References:Subject:In-Reply-To:To:From:Date:
 mime-version; bh=TCzeSQQItuKpPzmPouypKkrrX394edArCH9odaiuJkA=; b=cyDNHCrOXn6d
 WkLONLPweTEMKrvuFvB4exZjGlR+jNNNkhIBjAuOpw3wV+fx3L9f2C8q+kN6kh4TCLQbJTiPlVdFB
 +HhAfPh3Ch5Ri0HX4Yd8xjKDaaw7fz5mYr2uhG6V4TLV7vtGm0AdgjxOoiZF9DxSdf09dRXzgTG35
 axP0saa27hOY0/oNKhSHCQwRrK8EkY8goc9eTtUENQLR+1ZmkUh8ApTD7XBgdnfFX51Id1USNcpzT
 mzO+odezijEpYaeChsid1UY0gL1RS6XEwA9aRgIG5GKa3gA6BM8gFVkvtXKAvmNPqyvvho2oZ42Se
 vE/3MCDIOQNXpUpmaip2YA==;
Date: Sat, 30 Mar 2024 09:34:39 +0300
Message-Id: <86cyrcdy80.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwvle60x4d6.fsf-monnier+emacs@HIDDEN> (message from Stefan
 Monnier on Fri, 29 Mar 2024 14:53:41 -0400)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwvle60x4d6.fsf-monnier+emacs@HIDDEN>
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: yantar92@HIDDEN, 70077 <at> debbugs.gnu.org, casouri@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> From: Stefan Monnier <monnier@HIDDEN>
> Cc: 70077 <at> debbugs.gnu.org,  mail@HIDDEN,  yantar92@HIDDEN,
>   acm@HIDDEN,  joaotavora@HIDDEN,  alan.zimm@HIDDEN,
>   frederic.bour@HIDDEN,  phillip.lord@HIDDEN,
>   stephen_leake@HIDDEN,  casouri@HIDDEN,  qhong@HIDDEN
> Date: Fri, 29 Mar 2024 14:53:41 -0400
> 
> > I cannot imagine how applications would use these APIs.  I'm probably
> > missing something, org the above documentation does.  Can you show
> > some real-life examples?
> 
> Haven't written real code for it yes, no.
> The best I can offer is the sample code in the file:
> 
>     (defvar my-foo--change-tracker nil)
>     (define-minor-mode my-foo-mode
>       "Fooing like there's no tomorrow."
>       (if (null my-foo-mode)
>           (when my-foo--change-tracker
>             (track-changes-unregister my-foo--change-tracker)
>             (setq my-foo--change-tracker nil))
>         (unless my-foo--change-tracker
>           (setq my-foo--change-tracker
>                 (track-changes-register
>                  (lambda ()
>                    (track-changes-fetch
>                     my-foo--change-tracker
>                     (lambda (beg end before)
>                       ..DO THE THING..))))))))
> 
> Where "DO THE THING" is run similarly to what would happen in an
> `after-change-functions`, except:
> 
> - BEFORE is a string holding the content of what was in BEG..END
>   instead of being limited to its length.
> - It's run at most once per command, so there's no performance worries.
> - It's run "outside" of the modifications themselves,
>   so `inhibit-modification-hooks` is nil and the code can wait, modify
>   the buffer, or do any kind of crazy things.

Thanks.

I understand the last point, but that still doesn't explain enough for
me to see the light.  (I've also looked at the two real-life uses you
posted, and it didn't help, probably because the important ideas
drowned in the sea of modifications to code I'm not familiar with well
enough to understand what's important and what isn't.)  If the last
point, i.e. the problems caused by limitations on what can be safely
done from modification hooks, is basically the main advantage, then I
think I understand the rationale.  Otherwise, the above looks like
doing all the job in after-change-functions, and it is not clear to me
how is that better, since if track-changes-fetch will fetch a series
of changes, deciding how to handle them could be much harder than
handling them one by one when each one happens.  For example, the
BEGIN..END values no longer reflect the current buffer contents, and
each fetched change refers to a different content of the buffer (so
the same values of BEG..END don't necessarily mean the same places in
the buffer).  I think the need for the
eglot--virtual-pos-to-lsp-position function in one of your examples is
the tip of that iceberg.

Also, all of your examples seem to have the signal function just call
track-changes-fetch and do almost nothing else, so I wonder why we
need a separate function for that, and more specifically what would be
a use case where the registered signal function does NOT call
track-changes-fetch, but does something else, and track-changes-fetch
is then called outside of the signal function.

Finally, the doc string of track-changes-register does not describe
the exact place in the buffer-change sequence where the signal
function will be called, which makes it harder to reason about it.
Will it be called where we now call signal_after_change or somewhere
else?  And how do you guarantee that the signal function will not be
called again until track-changes-fetch is called?




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 05:09:56 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Sat Mar 30 01:09:56 2024
Received: from localhost ([127.0.0.1]:43719 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqQyZ-0002LR-Cd
	for submit <at> debbugs.gnu.org; Sat, 30 Mar 2024 01:09:56 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:59441)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqQyW-0002LD-K8
 for 70077 <at> debbugs.gnu.org; Sat, 30 Mar 2024 01:09:53 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id A19931000DD;
 Sat, 30 Mar 2024 01:09:44 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711775383;
 bh=oPJpqrXJloZ+Vj4qPyoHlTphSd1+E1YAC3BhOuiPyEE=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=JPZUl3tTNIaiVEUoULQkFrWwld2rfnexERFYiYM2yFcZwEqworokVaioxP0ulJwu2
 dMD/9kLTrlDVYqPX7G3cglRGMKDGXdQyxNvBRge5R7MFKyAoVZNuTJfEhnb9ANEgWe
 GDaQtqfb5iUJGXaK7XNhfHQFTr3T7QdcQURwmmhwKtJVnc+aXxFt6Ampd0tgJLQehA
 LwCnynhj49PqnhSRJH7wru9Dxpmgs8PPXKG6IRHrB/PQ03KbEuNTmb4KxHjMfdPU2B
 BXmDcoc3t0YDEl/ZyJzqb7VdPfXeqNSg/kQLRYcsnpJz5S0nrPWjWUCMgaAj3Ud6xU
 jedR4aQWpusIA==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 460A210004A;
 Sat, 30 Mar 2024 01:09:43 -0400 (EDT)
Received: from pastel (104-222-113-60.cpe.teksavvy.com [104.222.113.60])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id E3186120642;
 Sat, 30 Mar 2024 01:09:42 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwv4jcowguh.fsf-monnier+emacs@HIDDEN> (Stefan Monnier's message
 of "Fri, 29 Mar 2024 23:17:09 -0400")
Message-ID: <jwvy1a0uxd7.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
 <jwv4jcowguh.fsf-monnier+emacs@HIDDEN>
Date: Sat, 30 Mar 2024 01:09:41 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.162 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, yantar92@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

--=-=-=
Content-Type: text/plain

> Here's my first attempt at a real-life use.

And here's a second attempt, which is a tentative patch for `eglot.el`.
This one does make use of the `before` argument, so it exercises more
the API.

The `eglot--virtual-pos-to-lsp-position` is not completely satisfactory,
since to compute the LSP position of the end of the chunk before it was
modified, I end up creating a temp buffer to insert the part of the text
that changed (to count its line+column, which is much easier in a buffer
than in a string).  That kinda sucks performancewise, but we do it at
most once per command rather than once per buffer-modification, so it
should be lost in the noise.

The upside is that we're insulated from the quirks of the
after/before-change-functions evidenced by the copious comments
referring to various bug reports.


        Stefan

--=-=-=
Content-Type: text/x-diff
Content-Disposition: inline; filename=eglot.patch

diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 7d2f1a55165..d2268cea940 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -110,6 +110,7 @@
 (require 'text-property-search nil t)
 (require 'diff-mode)
 (require 'diff)
+(require 'track-changes)
 
 ;; These dependencies are also GNU ELPA core packages.  Because of
 ;; bug#62576, since there is a risk that M-x package-install, despite
@@ -1732,6 +1733,9 @@ eglot-utf-16-linepos
   "Calculate number of UTF-16 code units from position given by LBP.
 LBP defaults to `eglot--bol'."
   (/ (- (length (encode-coding-region (or lbp (eglot--bol))
+                                      ;; FIXME: How could `point' ever be
+                                      ;; larger than `point-max' (sounds like
+                                      ;; a bug in Emacs).
                                       ;; Fix github#860
                                       (min (point) (point-max)) 'utf-16 t))
         2)
@@ -1749,6 +1753,24 @@ eglot--pos-to-lsp-position
          :character (progn (when pos (goto-char pos))
                            (funcall eglot-current-linepos-function)))))
 
+(defun eglot--virtual-pos-to-lsp-position (pos string)
+  "Return the LSP position at the end of STRING if it were inserted at POS."
+  (eglot--widening
+   (goto-char pos)
+   (forward-char 0)
+   ;; LSP line is zero-origin; Emacs is one-origin.
+   (let ((posline (1- (line-number-at-pos nil t)))
+         (linebeg (buffer-substring (point) pos))
+         (colfun eglot-current-linepos-function))
+     ;; Use a temp buffer because:
+     ;; - I don't know of a fast way to count newlines in a string.
+     ;; - We currently don't have `eglot-current-linepos-function' for strings.
+     (with-temp-buffer
+       (insert linebeg string)
+       (goto-char (point-max))
+       (list :line (+ posline (1- (line-number-at-pos nil t)))
+             :character (funcall colfun))))))
+
 (defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
   "Function to move to a position within a line reported by the LSP server.
 
@@ -1946,6 +1968,8 @@ eglot-managed-mode-hook
   "A hook run by Eglot after it started/stopped managing a buffer.
 Use `eglot-managed-p' to determine if current buffer is managed.")
 
+(defvar-local eglot--track-changes nil)
+
 (define-minor-mode eglot--managed-mode
   "Mode for source buffers managed by some Eglot project."
   :init-value nil :lighter nil :keymap eglot-mode-map
@@ -1959,8 +1983,9 @@ eglot--managed-mode
       ("utf-8"
        (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos)
        (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos)))
-    (add-hook 'after-change-functions #'eglot--after-change nil t)
-    (add-hook 'before-change-functions #'eglot--before-change nil t)
+    (unless eglot--track-changes
+      (setq eglot--track-changes
+            (track-changes-register #'eglot--track-changes-signal)))
     (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
     ;; Prepend "didClose" to the hook after the "nonoff", so it will run first
     (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t)
@@ -1994,8 +2019,8 @@ eglot--managed-mode
       (eldoc-mode 1))
     (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server))))
    (t
-    (remove-hook 'after-change-functions #'eglot--after-change t)
-    (remove-hook 'before-change-functions #'eglot--before-change t)
+    (when eglot--track-changes
+      (track-changes-unregister eglot--track-changes))
     (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
     (remove-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose t)
     (remove-hook 'before-revert-hook #'eglot--signal-textDocument/didClose t)
@@ -2564,54 +2589,20 @@ jsonrpc-connection-ready-p
 
 (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.")
 
-(defun eglot--before-change (beg end)
-  "Hook onto `before-change-functions' with BEG and END."
-  (when (listp eglot--recent-changes)
-    ;; Records BEG and END, crucially convert them into LSP
-    ;; (line/char) positions before that information is lost (because
-    ;; the after-change thingy doesn't know if newlines were
-    ;; deleted/added).  Also record markers of BEG and END
-    ;; (github#259)
-    (push `(,(eglot--pos-to-lsp-position beg)
-            ,(eglot--pos-to-lsp-position end)
-            (,beg . ,(copy-marker beg nil))
-            (,end . ,(copy-marker end t)))
-          eglot--recent-changes)))
-
 (defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange)
   "Internal hook for doing things when the document changes.")
 
-(defun eglot--after-change (beg end pre-change-length)
-  "Hook onto `after-change-functions'.
-Records BEG, END and PRE-CHANGE-LENGTH locally."
+(defun eglot--track-changes-signal (id)
   (cl-incf eglot--versioned-identifier)
-  (pcase (car-safe eglot--recent-changes)
-    (`(,lsp-beg ,lsp-end
-                (,b-beg . ,b-beg-marker)
-                (,b-end . ,b-end-marker))
-     ;; github#259 and github#367: with `capitalize-word' & friends,
-     ;; `before-change-functions' records the whole word's `b-beg' and
-     ;; `b-end'.  Similarly, when `fill-paragraph' coalesces two
-     ;; lines, `b-beg' and `b-end' mark end of first line and end of
-     ;; second line, resp.  In both situations, `beg' and `end'
-     ;; received here seemingly contradict that: they will differ by 1
-     ;; and encompass the capitalized character or, in the coalescing
-     ;; case, the replacement of the newline with a space.  We keep
-     ;; both markers and positions to detect and correct this.  In
-     ;; this specific case, we ignore `beg', `len' and
-     ;; `pre-change-len' and send richer information about the region
-     ;; from the markers.  I've also experimented with doing this
-     ;; unconditionally but it seems to break when newlines are added.
-     (if (and (= b-end b-end-marker) (= b-beg b-beg-marker)
-              (or (/= beg b-beg) (/= end b-end)))
-         (setcar eglot--recent-changes
-                 `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker)
-                            ,(buffer-substring-no-properties b-beg-marker
-                                                             b-end-marker)))
-       (setcar eglot--recent-changes
-               `(,lsp-beg ,lsp-end ,pre-change-length
-                          ,(buffer-substring-no-properties beg end)))))
-    (_ (setf eglot--recent-changes :emacs-messup)))
+  (track-changes-fetch
+   id (lambda (beg end before)
+        (if (stringp before)
+            (push `(,(eglot--pos-to-lsp-position beg)
+                    ,(eglot--virtual-pos-to-lsp-position beg before)
+                    ,(length before)
+                    ,(buffer-substring-no-properties beg end))
+                  eglot--recent-changes)
+          (setf eglot--recent-changes :emacs-messup))))
   (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
   (let ((buf (current-buffer)))
     (setq eglot--change-idle-timer
@@ -2741,12 +2732,6 @@ eglot--signal-textDocument/didChange
                               (buffer-substring-no-properties (point-min)
                                                               (point-max)))))
           (cl-loop for (beg end len text) in (reverse eglot--recent-changes)
-                   ;; github#259: `capitalize-word' and commands based
-                   ;; on `casify_region' will cause multiple duplicate
-                   ;; empty entries in `eglot--before-change' calls
-                   ;; without an `eglot--after-change' reciprocal.
-                   ;; Weed them out here.
-                   when (numberp len)
                    vconcat `[,(list :range `(:start ,beg :end ,end)
                                     :rangeLength len :text text)]))))
       (setq eglot--recent-changes nil)

--=-=-=--





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 30 Mar 2024 03:17:23 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Mar 29 23:17:23 2024
Received: from localhost ([127.0.0.1]:43692 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqPDe-00053G-Nm
	for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 23:17:23 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:49544)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqPDd-000534-8V
 for 70077 <at> debbugs.gnu.org; Fri, 29 Mar 2024 23:17:21 -0400
Received: from pmg2.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg2.iro.umontreal.ca (Proxmox) with ESMTP id 9AE71803C1;
 Fri, 29 Mar 2024 23:17:13 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711768632;
 bh=l5FVBO2ixyFlfCyU6ciEHZJs/xQHsI14as4BFV3BnC4=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=HoDRsMA7Q8W8334kRhUq9qeeCAB7Pl2V+daqGvnllsYMBuq08kiw6c2ydYBIHrEtI
 InXI2LNsnjC4slEGAV5Hyc4Hci1PKyI+E3Hji/5EiX5FKpO0Aejpaqu/HqdCqPcaPo
 PjezL+IaioA+BrJLZOeIJj0023d+G99ZDV9ekCf2cxN1utEGF6oAT9xZB3cDzXqNYG
 XSBfBuKkqnNWRtoW7nNyviicX3vOGpq2lIEixDh1e5b6Puj/81R/0CYsJecyRK4kLY
 qHcByzzhbB8atuCvueZ3o03NaMe4JR2D/6fbffU7EDhyFL8AlaTscV79bAyPqWi4ON
 klWzmum4u17Lw==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg2.iro.umontreal.ca (Proxmox) with ESMTP id 3C04B8092A;
 Fri, 29 Mar 2024 23:17:12 -0400 (EDT)
Received: from pastel (104-222-113-60.cpe.teksavvy.com [104.222.113.60])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id A3251120857;
 Fri, 29 Mar 2024 23:17:11 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86frw8ewk9.fsf@HIDDEN> (Eli Zaretskii's message of "Fri, 29 Mar
 2024 21:12:54 +0300")
Message-ID: <jwv4jcowguh.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
Date: Fri, 29 Mar 2024 23:17:09 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.002 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, yantar92@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

--=-=-=
Content-Type: text/plain

> I cannot imagine how applications would use these APIs.  I'm probably
> missing something, org the above documentation does.  Can you show
> some real-life examples?

Here's my first attempt at a real-life use.
Note: this example doesn't make use of the full API (it doesn't need the
`before` argument).


        Stefan

--=-=-=
Content-Type: text/x-diff
Content-Disposition: inline; filename=diff-track-changes.patch

diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el
index d9146ea8cc5..f8c31fb6748 100644
--- a/lisp/vc/diff-mode.el
+++ b/lisp/vc/diff-mode.el
@@ -53,9 +53,10 @@
 ;; - Handle `diff -b' output in context->unified.
 
 ;;; Code:
+(require 'easy-mmode)
+(require 'track-changes)
 (eval-when-compile (require 'cl-lib))
 (eval-when-compile (require 'subr-x))
-(require 'easy-mmode)
 
 (autoload 'vc-find-revision "vc")
 (autoload 'vc-find-revision-no-save "vc")
@@ -1441,31 +1441,16 @@
   (if (buffer-modified-p) (diff-fixup-modifs (point-min) (point-max)))
   nil)
 
-;; It turns out that making changes in the buffer from within an
-;; *-change-function is asking for trouble, whereas making them
-;; from a post-command-hook doesn't pose much problems
-(defvar diff-unhandled-changes nil)
-(defun diff-after-change-function (beg end _len)
-  "Remember to fixup the hunk header.
-See `after-change-functions' for the meaning of BEG, END and LEN."
-  ;; Ignoring changes when inhibit-read-only is set is strictly speaking
-  ;; incorrect, but it turns out that inhibit-read-only is normally not set
-  ;; inside editing commands, while it tends to be set when the buffer gets
-  ;; updated by an async process or by a conversion function, both of which
-  ;; would rather not be uselessly slowed down by this hook.
-  (when (and (not undo-in-progress) (not inhibit-read-only))
-    (if diff-unhandled-changes
-	(setq diff-unhandled-changes
-	      (cons (min beg (car diff-unhandled-changes))
-		    (max end (cdr diff-unhandled-changes))))
-      (setq diff-unhandled-changes (cons beg end)))))
+(defvar-local diff--track-changes nil)
 
-(defun diff-post-command-hook ()
-  "Fixup hunk headers if necessary."
-  (when (consp diff-unhandled-changes)
-    (ignore-errors
+(defun diff--track-changes-signal (tracker)
+  (cl-assert (eq tracker diff--track-changes))
+  (track-changes-fetch tracker #'diff--track-changes-function))
+
+(defun diff--track-changes-function (beg end _before)
+  (with-demoted-errors "%S"
       (save-excursion
-	(goto-char (car diff-unhandled-changes))
+      (goto-char beg)
 	;; Maybe we've cut the end of the hunk before point.
 	(if (and (bolp) (not (bobp))) (backward-char 1))
 	;; We used to fixup modifs on all the changes, but it turns out that
@@ -1480,17 +1465,16 @@
                           (re-search-forward diff-context-mid-hunk-header-re
                                              nil t)))))
           (when (and ;; Don't try to fixup changes in the hunk header.
-                 (>= (car diff-unhandled-changes) start)
+               (>= beg start)
                  ;; Don't try to fixup changes in the mid-hunk header either.
                  (or (not mid)
-                     (< (cdr diff-unhandled-changes) (match-beginning 0))
-                     (> (car diff-unhandled-changes) (match-end 0)))
+                   (< end (match-beginning 0))
+                   (> beg (match-end 0)))
                  (save-excursion
 		(diff-end-of-hunk nil 'donttrustheader)
                    ;; Don't try to fixup changes past the end of the hunk.
-                   (>= (point) (cdr diff-unhandled-changes))))
-	  (diff-fixup-modifs (point) (cdr diff-unhandled-changes)))))
-      (setq diff-unhandled-changes nil))))
+                 (>= (point) end)))
+	 (diff-fixup-modifs (point) end))))))
 
 (defun diff-next-error (arg reset)
   ;; Select a window that displays the current buffer so that point
@@ -1572,9 +1557,8 @@ diff-mode
   ;; setup change hooks
   (if (not diff-update-on-the-fly)
       (add-hook 'write-contents-functions #'diff-write-contents-hooks nil t)
-    (make-local-variable 'diff-unhandled-changes)
-    (add-hook 'after-change-functions #'diff-after-change-function nil t)
-    (add-hook 'post-command-hook #'diff-post-command-hook nil t))
+    (setq diff--track-changes
+          (track-changes-register #'diff--track-changes-signal)))
 
   ;; add-log support
   (setq-local add-log-current-defun-function #'diff-current-defun)
@@ -1593,12 +1577,13 @@ diff-minor-mode
 \\{diff-minor-mode-map}"
   :group 'diff-mode :lighter " Diff"
   ;; FIXME: setup font-lock
-  ;; setup change hooks
+  (when diff--track-changes (track-changes-unregister diff--track-changes))
+  (remove-hook 'write-contents-functions #'diff-write-contents-hooks t)
   (if (not diff-update-on-the-fly)
       (add-hook 'write-contents-functions #'diff-write-contents-hooks nil t)
-    (make-local-variable 'diff-unhandled-changes)
-    (add-hook 'after-change-functions #'diff-after-change-function nil t)
-    (add-hook 'post-command-hook #'diff-post-command-hook nil t)))
+    (unless diff--track-changes
+     (setq diff--track-changes
+           (track-changes-register #'diff--track-changes-signal)))))
 
 ;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 

--=-=-=--





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 29 Mar 2024 22:59:53 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Mar 29 18:59:53 2024
Received: from localhost ([127.0.0.1]:43614 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqLCT-0000A3-Hf
	for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 18:59:53 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:64188)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqLCQ-00009k-12
 for 70077 <at> debbugs.gnu.org; Fri, 29 Mar 2024 18:59:51 -0400
Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id CEA964429F7;
 Fri, 29 Mar 2024 18:59:41 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711753180;
 bh=49XqOxnI8GhRIaUhGknCF7y19BWQaxFhEo758rOPwa4=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=mtixeX0K01O+3DrDkARYXWLDaaKptg7xRGC6bpGnxvaA+hsuEHmkHf93eW0ZApMBd
 UQTKUUVM0Hd06u+xIItUBU8ENXFSjVoE+B8Y97PAPoxoVXvZvgk6PutDKRyynElmlo
 fz+NrkCcfK1RHSwjEZ+aBuAyjwTmp2MVBUwaPiTSOTnkOqm/EzxMmjRWoJXeFbXpjE
 CXHpnSfK//xjIMU2MBJukB94wACVPcfGHDbvxXemUVBxiPDXAT9YQmzlHurIWnysUp
 NgMd1WcJBKfKL9B/OVZ/M35qMtEp3obCgl+uk06pxeutvRds18iqXZ+yYZqJ7ezEq3
 YIRfXrymneGgw==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 47B544429DC;
 Fri, 29 Mar 2024 18:59:40 -0400 (EDT)
Received: from pastel (104-222-113-60.cpe.teksavvy.com [104.222.113.60])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id B63F5120550;
 Fri, 29 Mar 2024 18:59:39 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: phillip.lord@HIDDEN
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <e94e6fc3b497db294a87fa2b4c388a11@HIDDEN> (phillip lord's
 message of "Fri, 29 Mar 2024 18:20:32 -0400")
Message-ID: <jwva5mgwt05.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
 <e94e6fc3b497db294a87fa2b4c388a11@HIDDEN>
Date: Fri, 29 Mar 2024 18:59:38 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: Ihor Radchenko <yantar92@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Yuan Fu <casouri@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?windows-1252?B?RnLpZOlyaWM=?= Bour <frederic.bour@HIDDEN>,
 =?windows-1252?B?Sm/jbyBU4XZvcmE=?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> If I remember correctly, I think this wouldn't be enough for my
> use. You keep two buffers in sync, you have to use
> before-change-function -- it is only before any change that the two
> buffers are guaranteed to be in sync and it is this that allows you to
> work out what the `start' and `end' positions mean in the copied
> buffer. Afterward, you cannot work out what the end position because
> you don't know if the change is a change, insertion, deletion or both.

I believe the API I propose does provide that information: you can
recover the state of the buffer before the change (or more specifically,
the state of the buffer as of the last time you called
track-changes-fetch) from the BEG/END/BEFORE arguments as follows:

    (concat (buffer-substring (point-min) beg)
            before
            (buffer-substring end (point-max)))

I don't mean to suggest to do that, since it's costly for large
buffers, but to illustrate that the information is properly preserved.

> Last time I checked, I did find relatively few primitives that were guilty
> of being inconsistent -- in the case of `subst-char-in-region', it returned
> the maximal area of effect before the and the minimal area of effect
> after. Would it not be easier to fix these?

[ IIRC `revert-buffer` has a similar behavior, and in that case the
  difference can be large since the "before" covers the whole buffer.  ]

Also, it would fix only the problem of pairing, and not the other ones.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 29 Mar 2024 22:20:44 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Mar 29 18:20:44 2024
Received: from localhost ([127.0.0.1]:43576 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqKaZ-0003bY-Hg
	for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 18:20:43 -0400
Received: from cloud103.planethippo.com ([78.129.190.68]:38172)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <phillip.lord@HIDDEN>) id 1rqKaX-0003aw-NM
 for 70077 <at> debbugs.gnu.org; Fri, 29 Mar 2024 18:20:42 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;
 d=russet.org.uk; s=default; h=Content-Transfer-Encoding:Content-Type:
 Message-ID:References:In-Reply-To:Subject:Cc:To:From:Date:MIME-Version:Sender
 :Reply-To:Content-ID:Content-Description:Resent-Date:Resent-From:
 Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:
 List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive;
 bh=j4VfnN77pwpLxdYLNCHa3x9FDkPfvg91FTM3VgVjRP4=; b=2G25W5MMAd+P38mfZ2A8kSm0so
 1yfxcQW1puIKjosDnIUSGa6XcDXWq6GB1ZRbdBdWNwjRU4tDClT0UW10d+zQVS8C7KzYrqakInpeO
 9UeMDD7dqmBwyvI2RlvrBPx9LpdJBu2ee6ngNrZDwsIUl2DnMJRUWIbhVFOoZstEpZ5rfdP3heUOo
 aje6o2mMc2K+d/Iyr7mWN1Bvavo6IofGC3T/35cxzHrIfwCMPGOPFjXFZbkdhiSTYmthMmjfGTQel
 1ieMknzPK50FV549Qp2hWUGZyv0KmHWdjxHDLXom6oEiuPXoxUhUnXVEF30yVxWwc/LqjbHshzlDE
 3TCBqc1Q==;
Received: from [::1] (port=51982 helo=cloud103.planethippo.com)
 by cloud103.planethippo.com with esmtpa (Exim 4.96.2)
 (envelope-from <phillip.lord@HIDDEN>) id 1rqKaR-005nA2-0I;
 Fri, 29 Mar 2024 18:20:33 -0400
MIME-Version: 1.0
Date: Fri, 29 Mar 2024 18:20:32 -0400
From: phillip.lord@HIDDEN
To: Stefan Monnier <monnier@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <jwvle615806.fsf@HIDDEN>
References: <jwvle615806.fsf@HIDDEN>
User-Agent: Roundcube Webmail/1.6.0
Message-ID: <e94e6fc3b497db294a87fa2b4c388a11@HIDDEN>
X-Sender: phillip.lord@HIDDEN
Content-Type: text/plain; charset=US-ASCII;
 format=flowed
Content-Transfer-Encoding: 7bit
X-AntiAbuse: This header was added to track abuse,
 please include it with any abuse report
X-AntiAbuse: Primary Hostname - cloud103.planethippo.com
X-AntiAbuse: Original Domain - debbugs.gnu.org
X-AntiAbuse: Originator/Caller UID/GID - [47 12] / [47 12]
X-AntiAbuse: Sender Address Domain - russet.org.uk
X-Get-Message-Sender-Via: cloud103.planethippo.com: authenticated_id:
 phillip.lord@HIDDEN
X-Authenticated-Sender: cloud103.planethippo.com: phillip.lord@HIDDEN
X-Source: 
X-Source-Args: 
X-Source-Dir: 
X-Spam-Score: 3.0 (+++)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 Content preview:  On 2024-03-29 12:15, Stefan Monnier wrote: > Tags: patch >
 > Our `*-change-functions` hook are fairly tricky to use right. > Some of
 the issues are: > > - before and after calls are not necessarily pa [...]
 Content analysis details:   (3.0 points, 10.0 required)
 pts rule name              description
 ---- ---------------------- --------------------------------------------------
 3.0 MANY_TO_CC             Sent to 10+ recipients
 -0.0 SPF_PASS               SPF: sender matches SPF record
 0.0 SPF_HELO_NONE          SPF: HELO does not publish an SPF Record
X-Debbugs-Envelope-To: 70077
Cc: Yuan Fu <casouri@HIDDEN>, 70077 <at> debbugs.gnu.org,
 Ihor Radchenko <yantar92@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 =?UTF-8?Q?Fr=C3=A9d=C3=A9ric_Bour?= <frederic.bour@HIDDEN>,
 =?UTF-8?Q?Jo=C3=A3o_T=C3=A1vora?= <joaotavora@HIDDEN>,
 Nicolas Goaziou <mail@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: 2.0 (++)
X-Spam-Report: Spam detection software, running on the system "debbugs.gnu.org",
 has NOT identified this incoming email as spam.  The original
 message has been attached to this so you can view it or label
 similar future email.  If you have any questions, see
 the administrator of that system for details.
 
 Content preview:  On 2024-03-29 12:15, Stefan Monnier wrote: > Tags: patch >
    > Our `*-change-functions` hook are fairly tricky to use right. > Some of
    the issues are: > > - before and after calls are not necessarily pa [...]
    
 
 Content analysis details:   (2.0 points, 10.0 required)
 
  pts rule name              description
 ---- ---------------------- --------------------------------------------------
  3.0 MANY_TO_CC             Sent to 10+ recipients
 -0.0 SPF_PASS               SPF: sender matches SPF record
  0.0 SPF_HELO_NONE          SPF: HELO does not publish an SPF Record
 -1.0 MAILING_LIST_MULTI     Multiple indicators imply a widely-seen list
                             manager

On 2024-03-29 12:15, Stefan Monnier wrote:
> Tags: patch
> 
> Our `*-change-functions` hook are fairly tricky to use right.
> Some of the issues are:
> 
> - before and after calls are not necessarily paired.
> - the beg/end values don't always match.
> - there can be thousands of calls from within a single command.
> - these hooks are run at a fairly low-level so there are things they
>   really shouldn't do, such as modify the buffer or wait.
> - the after call doesn't get enough info to rebuild the before-change 
> state,
>   so some callers need to use both before-c-f and after-c-f (and then
>   deal with the first two points above).
> 
> The worst part is that those problems occur rarely, so many coders 
> don't
> see it at first and have to learn them the hard way, sometimes forcing
> them to rethink their original design.
> 
> So I think we should provide something simpler.
> I attached a proof-of-concept API which aims to do that, with the
> following entry points:
> 
>     (defun track-changes-register ( signal)
>       "Register a new tracker and return a new tracker ID.
>     SIGNAL is a function that will be called with no argument when
>     the current buffer is modified, so that we can react to the change.
>     Once called, SIGNAL is not called again until `track-changes-fetch'
>     is called with the corresponding tracker ID."
> 
>     (defun track-changes-unregister (id)
>       "Remove the tracker denoted by ID.
>     Trackers can consume resources (especially if `track-changes-fetch' 
> is
>     not called), so it is good practice to unregister them when you 
> don't
>     need them any more."
> 
>     (defun track-changes-fetch (id func)
>       "Fetch the pending changes.
>     ID is the tracker ID returned by a previous 
> `track-changes-register'.
>     FUNC is a function.  It is called with 3 arguments (BEGIN END 
> BEFORE)
>     where BEGIN..END delimit the region that was changed since the last
>     time `track-changes-fetch' was called and BEFORE is a string 
> containing
>     the previous content of that region.
> 
>     If no changes occurred since the last time, FUNC is not called and
>     we return nil, otherwise we return the value returned by FUNC,
>     and re-enable the TRACKER corresponding to ID."
> 
> It's not meant as a replacement of the existing hooks since it doesn't
> try to accommodate some uses such as those that use before-c-f to
> implement a finer-grained form of read-only text.
> 
> The driving design was:
> 
> - Try to provide enough info such that it is possible and easy to
>   maintain a copy of the buffer simply by applying the reported 
> changes.
>   E.g. for uses such as `eglot.el` or `crdt.el`.
> - Make the API less synchronous: take care of combining small changes
>   into larger ones, and let the clients decide when they react to 
> changes.
> 
> If you're in the Cc, it's because I believe you have valuable 
> experience
> with those hooks, so I'd be happy to hear your thought about whether
> you think this would indeed (have) be(en) better than what we have.


Your description of the problem is entirely consistent with my 
experience. The last time I
checked it was `subst-char-in-region' which was causing most of the 
difficulties, normally as a result of `fill-paragraph'.

If I remember correctly, I think this wouldn't be enough for my use. You 
keep two buffers
in sync, you have to use before-change-function -- it is only before any 
change that the
two buffers are guaranteed to be in sync and it is this that allows you 
to work out what the
`start' and `end' positions mean in the copied buffer. Afterward, you 
cannot work out what the end position because you don't know if the 
change is a change, insertion, deletion or both.

Last time I checked, I did find relatively few primitives that were 
guilty of being inconsistent -- in the case of `subst-char-in-region', 
it returned the maximal area of effect before the and the minimal area 
of effect after. Would it not be easier to fix these?

Phil





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 29 Mar 2024 18:53:52 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Mar 29 14:53:52 2024
Received: from localhost ([127.0.0.1]:43428 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqHMO-0001M4-9A
	for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 14:53:52 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:39499)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqHMM-0001LS-G8
 for 70077 <at> debbugs.gnu.org; Fri, 29 Mar 2024 14:53:50 -0400
Received: from pmg1.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id 46E3910005D;
 Fri, 29 Mar 2024 14:53:43 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711738421;
 bh=zOuPkA9+KnkBmDhWM6pkYAbuAuNqNnLQt+Y6IxhMd+Q=;
 h=From:To:Cc:Subject:In-Reply-To:References:Date:From;
 b=PenlcIecRM4Ov26Y46ANcWAjIvOGnUaqlOW6MeWF9LD7PFeEywpkfSSzxeyTPAGB/
 jCvOvXHIMBT8wJX3li4kYhBlapZbZvMUr0IoUY5jbGkuLIitUdj7FUEDeIxhvuJuXO
 n0YsgeRUziSdEnjfVl0aR0PMGQRR5+v3K6JlUnE0pn1yzmUV5z1qdQIpJOlU6bwW59
 E4jW7NDsLlW81Fdjd8mNNHtPwydFvXmKwU6FPGP/4fwRQXivCwt7iOf9BOHw4Pyw9O
 clFNSf4LfT6srImj27nHLg+ns0tWRFk8z5/RH0XL4VzDdmWMahZc2qr0b2g0ZiHviz
 BbR7/BN0/LpTg==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg1.iro.umontreal.ca (Proxmox) with ESMTP id D48A0100046;
 Fri, 29 Mar 2024 14:53:41 -0400 (EDT)
Received: from pastel (104-222-113-60.cpe.teksavvy.com [104.222.113.60])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 78A36120185;
 Fri, 29 Mar 2024 14:53:41 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: Eli Zaretskii <eliz@HIDDEN>
Subject: Re: bug#70077: An easier way to track buffer changes
In-Reply-To: <86frw8ewk9.fsf@HIDDEN> (Eli Zaretskii's message of "Fri, 29 Mar
 2024 21:12:54 +0300")
Message-ID: <jwvle60x4d6.fsf-monnier+emacs@HIDDEN>
References: <jwvle615806.fsf@HIDDEN> <86frw8ewk9.fsf@HIDDEN>
Date: Fri, 29 Mar 2024 14:53:41 -0400
User-Agent: Gnus/5.13 (Gnus v5.13)
MIME-Version: 1.0
Content-Type: text/plain
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.243 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: yantar92@HIDDEN, 70077 <at> debbugs.gnu.org, casouri@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> I cannot imagine how applications would use these APIs.  I'm probably
> missing something, org the above documentation does.  Can you show
> some real-life examples?

Haven't written real code for it yes, no.
The best I can offer is the sample code in the file:

    (defvar my-foo--change-tracker nil)
    (define-minor-mode my-foo-mode
      "Fooing like there's no tomorrow."
      (if (null my-foo-mode)
          (when my-foo--change-tracker
            (track-changes-unregister my-foo--change-tracker)
            (setq my-foo--change-tracker nil))
        (unless my-foo--change-tracker
          (setq my-foo--change-tracker
                (track-changes-register
                 (lambda ()
                   (track-changes-fetch
                    my-foo--change-tracker
                    (lambda (beg end before)
                      ..DO THE THING..))))))))

Where "DO THE THING" is run similarly to what would happen in an
`after-change-functions`, except:

- BEFORE is a string holding the content of what was in BEG..END
  instead of being limited to its length.
- It's run at most once per command, so there's no performance worries.
- It's run "outside" of the modifications themselves,
  so `inhibit-modification-hooks` is nil and the code can wait, modify
  the buffer, or do any kind of crazy things.

The code can also be changed to:

                [...]
                (track-changes-register
                 (lambda ()
                   (run-with-idle-timer
                    2 nil
                    (lambda ()
                      (track-changes-fetch
                       my-foo--change-tracker
                       (lambda (beg end before)
                         ..DO THE THING..))))))))

without any extra work.


        Stefan





Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at 70077 <at> debbugs.gnu.org:


Received: (at 70077) by debbugs.gnu.org; 29 Mar 2024 18:13:14 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Mar 29 14:13:14 2024
Received: from localhost ([127.0.0.1]:43408 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqGj3-0007Z7-AS
	for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 14:13:14 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10]:51352)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <eliz@HIDDEN>) id 1rqGj0-0007Yt-V6
 for 70077 <at> debbugs.gnu.org; Fri, 29 Mar 2024 14:13:11 -0400
Received: from fencepost.gnu.org ([2001:470:142:3::e])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <eliz@HIDDEN>)
 id 1rqGio-0003TJ-HS; Fri, 29 Mar 2024 14:12:59 -0400
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org;
 s=fencepost-gnu-org; h=MIME-version:References:Subject:In-Reply-To:To:From:
 Date; bh=X8zNmtRQP0SLSAhAM7kbSkQ28wGf9dwkPmEku0/N82Y=; b=qbQ7X67dWxhtmuykI5tr
 1Ssi4B2AfE+T6CW4QD0O2+UShg/9sV5xfwMcM2yhijgqc1NFBw3eRB9s2mUsBfn7Wo/AXykVzYQtA
 QwNK7zu+RFqyoEMK2tEVurK+uB1hPs8p35zZxZYiLaGBKl3fXnMYhDpXIOaOip00/sTnnCP6qA6qQ
 Gkw31Z+dAXT6zNLG+K+kZjqTc2b2IImKwXoh0EHk8aJuWc74GuPcPWgGUgHdPuitpqRYxW5NM+iT7
 qAgui7RZlmDktB2pIGkP/RpTTrGM9j0Xrx/E2S5w75tzbeaJVuIhnEF9JdAh3H7yWd6Mj5TdcC1xb
 BPu19HDJS+TUOA==;
Date: Fri, 29 Mar 2024 21:12:54 +0300
Message-Id: <86frw8ewk9.fsf@HIDDEN>
From: Eli Zaretskii <eliz@HIDDEN>
To: Stefan Monnier <monnier@HIDDEN>
In-Reply-To: <jwvle615806.fsf@HIDDEN> (bug-gnu-emacs@HIDDEN)
Subject: Re: bug#70077: An easier way to track buffer changes
References: <jwvle615806.fsf@HIDDEN>
MIME-version: 1.0
Content-type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
X-Spam-Score: 0.7 (/)
X-Debbugs-Envelope-To: 70077
Cc: casouri@HIDDEN, 70077 <at> debbugs.gnu.org, yantar92@HIDDEN,
 qhong@HIDDEN, frederic.bour@HIDDEN, joaotavora@HIDDEN,
 mail@HIDDEN, acm@HIDDEN, stephen_leake@HIDDEN,
 alan.zimm@HIDDEN, monnier@HIDDEN, phillip.lord@HIDDEN
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -0.3 (/)

> Cc: Nicolas Goaziou <mail@HIDDEN>,
>  Ihor Radchenko <yantar92@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
>  João Távora <joaotavora@HIDDEN>,
>  Alan Zimmerman <alan.zimm@HIDDEN>,
>  Frédéric Bour <frederic.bour@HIDDEN>,
>  Phillip Lord <phillip.lord@HIDDEN>,
>  Stephen Leake <stephen_leake@HIDDEN>, Yuan Fu <casouri@HIDDEN>,
>  Qiantan Hong <qhong@HIDDEN>, monnier@HIDDEN
> Date: Fri, 29 Mar 2024 12:15:53 -0400
> From:  Stefan Monnier via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <bug-gnu-emacs@HIDDEN>
> 
>     (defun track-changes-register ( signal)
>       "Register a new tracker and return a new tracker ID.
>     SIGNAL is a function that will be called with no argument when
>     the current buffer is modified, so that we can react to the change.
>     Once called, SIGNAL is not called again until `track-changes-fetch'
>     is called with the corresponding tracker ID."
>     
>     (defun track-changes-unregister (id)
>       "Remove the tracker denoted by ID.
>     Trackers can consume resources (especially if `track-changes-fetch' is
>     not called), so it is good practice to unregister them when you don't
>     need them any more."
>     
>     (defun track-changes-fetch (id func)
>       "Fetch the pending changes.
>     ID is the tracker ID returned by a previous `track-changes-register'.
>     FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
>     where BEGIN..END delimit the region that was changed since the last
>     time `track-changes-fetch' was called and BEFORE is a string containing
>     the previous content of that region.
>     
>     If no changes occurred since the last time, FUNC is not called and
>     we return nil, otherwise we return the value returned by FUNC,
>     and re-enable the TRACKER corresponding to ID."

I cannot imagine how applications would use these APIs.  I'm probably
missing something, org the above documentation does.  Can you show
some real-life examples?




Information forwarded to bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.

Message received at submit <at> debbugs.gnu.org:


Received: (at submit) by debbugs.gnu.org; 29 Mar 2024 16:16:35 +0000
From debbugs-submit-bounces <at> debbugs.gnu.org Fri Mar 29 12:16:35 2024
Received: from localhost ([127.0.0.1]:43253 helo=debbugs.gnu.org)
	by debbugs.gnu.org with esmtp (Exim 4.84_2)
	(envelope-from <debbugs-submit-bounces <at> debbugs.gnu.org>)
	id 1rqEu9-0001jf-Tb
	for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 12:16:35 -0400
Received: from lists.gnu.org ([2001:470:142::17]:42846)
 by debbugs.gnu.org with esmtp (Exim 4.84_2)
 (envelope-from <monnier@HIDDEN>) id 1rqEu6-0001jP-IY
 for submit <at> debbugs.gnu.org; Fri, 29 Mar 2024 12:16:31 -0400
Received: from eggs.gnu.org ([2001:470:142:3::10])
 by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <monnier@HIDDEN>)
 id 1rqEty-00013T-QT
 for bug-gnu-emacs@HIDDEN; Fri, 29 Mar 2024 12:16:22 -0400
Received: from mailscanner.iro.umontreal.ca ([132.204.25.50])
 by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)
 (Exim 4.90_1) (envelope-from <monnier@HIDDEN>)
 id 1rqEtv-0008Hr-V8
 for bug-gnu-emacs@HIDDEN; Fri, 29 Mar 2024 12:16:22 -0400
Received: from pmg2.iro.umontreal.ca (localhost.localdomain [127.0.0.1])
 by pmg2.iro.umontreal.ca (Proxmox) with ESMTP id 7209080AD9
 for <bug-gnu-emacs@HIDDEN>; Fri, 29 Mar 2024 12:16:17 -0400 (EDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca;
 s=mail; t=1711728975;
 bh=HWHFnxY6gTAT9y6U3RemvbTpKbH0HnPlbAz3V/Y8dTo=;
 h=From:To:Subject:Date:From;
 b=nkz2zDG9v105OpPhH95wm9azILeG9I6q0U9ZGQgj3e1YduybOn/HOaJiaGSR+xQLw
 KZl34Cxt+fBsW0WwUMnzYSO2U+IoEwhQglRGZigpM+yAxOtNycuhwt3FfyyzEBGkIX
 Uqz1i03ow37zetuz8rI5f9QQwmSZlU4FwYLXyW2AV5uLAfX8FKh+VVnrVhAo+EU0WD
 CmsltX7dYZacV5p2MmgdtJ94UvYuMtkGdnitpFd82wwvPt9aRGfhNXYiBo7KszT4xT
 mF7sK5jsycsgURUz2m1l9DAbTbO+VbXU8xjescn6skKRs+4O/vm9iL1yKhJEYdj9UN
 tv4/xm3Fcv70A==
Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1])
 by pmg2.iro.umontreal.ca (Proxmox) with ESMTP id 1C30A8009D
 for <bug-gnu-emacs@HIDDEN>; Fri, 29 Mar 2024 12:16:15 -0400 (EDT)
Received: from alfajor (unknown [23.233.149.155])
 by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id 06D8D12077C
 for <bug-gnu-emacs@HIDDEN>; Fri, 29 Mar 2024 12:16:15 -0400 (EDT)
From: Stefan Monnier <monnier@HIDDEN>
To: bug-gnu-emacs@HIDDEN
Subject: An easier way to track buffer changes
X-Debbugs-Cc: Nicolas Goaziou <mail@HIDDEN>,
 Ihor Radchenko <yantar92@HIDDEN>, Alan Mackenzie <acm@HIDDEN>,
 =?iso-8859-1?Q?Jo=E3o_T=E1vora?= <joaotavora@HIDDEN>,
 Alan Zimmerman <alan.zimm@HIDDEN>,
 =?iso-8859-1?Q?Fr=E9d=E9ric?= Bour <frederic.bour@HIDDEN>,
 Phillip Lord <phillip.lord@HIDDEN>,
 Stephen Leake <stephen_leake@HIDDEN>,
 Yuan Fu <casouri@HIDDEN>, Qiantan Hong <qhong@HIDDEN>,
 monnier@HIDDEN
Date: Fri, 29 Mar 2024 12:15:53 -0400
Message-ID: <jwvle615806.fsf@HIDDEN>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="=-=-="
X-SPAM-INFO: Spam detection results:  0
 ALL_TRUSTED                -1 Passed through trusted hosts only via SMTP
 AWL -0.045 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's
 domain
 DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from
 domain
X-SPAM-LEVEL: 
Received-SPF: pass client-ip=132.204.25.50;
 envelope-from=monnier@HIDDEN; helo=mailscanner.iro.umontreal.ca
X-Spam_score_int: -42
X-Spam_score: -4.3
X-Spam_bar: ----
X-Spam_report: (-4.3 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,
 DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, RCVD_IN_DNSWL_MED=-2.3,
 SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no
X-Spam_action: no action
X-Spam-Score: 0.0 (/)
X-Debbugs-Envelope-To: submit
X-BeenThere: debbugs-submit <at> debbugs.gnu.org
X-Mailman-Version: 2.1.18
Precedence: list
List-Id: <debbugs-submit.debbugs.gnu.org>
List-Unsubscribe: <https://debbugs.gnu.org/cgi-bin/mailman/options/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=unsubscribe>
List-Archive: <https://debbugs.gnu.org/cgi-bin/mailman/private/debbugs-submit/>
List-Post: <mailto:debbugs-submit <at> debbugs.gnu.org>
List-Help: <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=help>
List-Subscribe: <https://debbugs.gnu.org/cgi-bin/mailman/listinfo/debbugs-submit>, 
 <mailto:debbugs-submit-request <at> debbugs.gnu.org?subject=subscribe>
Errors-To: debbugs-submit-bounces <at> debbugs.gnu.org
Sender: "Debbugs-submit" <debbugs-submit-bounces <at> debbugs.gnu.org>
X-Spam-Score: -1.0 (-)

--=-=-=
Content-Type: text/plain

Tags: patch

Our `*-change-functions` hook are fairly tricky to use right.
Some of the issues are:

- before and after calls are not necessarily paired.
- the beg/end values don't always match.
- there can be thousands of calls from within a single command.
- these hooks are run at a fairly low-level so there are things they
  really shouldn't do, such as modify the buffer or wait.
- the after call doesn't get enough info to rebuild the before-change state,
  so some callers need to use both before-c-f and after-c-f (and then
  deal with the first two points above).

The worst part is that those problems occur rarely, so many coders don't
see it at first and have to learn them the hard way, sometimes forcing
them to rethink their original design.

So I think we should provide something simpler.
I attached a proof-of-concept API which aims to do that, with the
following entry points:

    (defun track-changes-register ( signal)
      "Register a new tracker and return a new tracker ID.
    SIGNAL is a function that will be called with no argument when
    the current buffer is modified, so that we can react to the change.
    Once called, SIGNAL is not called again until `track-changes-fetch'
    is called with the corresponding tracker ID."
    
    (defun track-changes-unregister (id)
      "Remove the tracker denoted by ID.
    Trackers can consume resources (especially if `track-changes-fetch' is
    not called), so it is good practice to unregister them when you don't
    need them any more."
    
    (defun track-changes-fetch (id func)
      "Fetch the pending changes.
    ID is the tracker ID returned by a previous `track-changes-register'.
    FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
    where BEGIN..END delimit the region that was changed since the last
    time `track-changes-fetch' was called and BEFORE is a string containing
    the previous content of that region.
    
    If no changes occurred since the last time, FUNC is not called and
    we return nil, otherwise we return the value returned by FUNC,
    and re-enable the TRACKER corresponding to ID."

It's not meant as a replacement of the existing hooks since it doesn't
try to accommodate some uses such as those that use before-c-f to
implement a finer-grained form of read-only text.

The driving design was:

- Try to provide enough info such that it is possible and easy to
  maintain a copy of the buffer simply by applying the reported changes.
  E.g. for uses such as `eglot.el` or `crdt.el`.
- Make the API less synchronous: take care of combining small changes
  into larger ones, and let the clients decide when they react to changes.

If you're in the Cc, it's because I believe you have valuable experience
with those hooks, so I'd be happy to hear your thought about whether
you think this would indeed (have) be(en) better than what we have.


        Stefan



--=-=-=
Content-Type: text/x-emacs-lisp; charset=iso-8859-1
Content-Disposition: attachment; filename=track-changes.el
Content-Transfer-Encoding: quoted-printable

;;; track-changes.el --- API to react to buffer modifications  -*- lexical-=
binding: t; -*-

;; Copyright (C) 2024  Free Software Foundation, Inc.

;; Author: Stefan Monnier <monnier@HIDDEN>

;; This file is part of GNU Emacs.

;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This library is a layer of abstraction above `before-change-functions'
;; and `after-change-functions' which takes care of accumulating changes
;; until a time when its client finds it convenient to react to them.

;; It provides the following operations:
;;
;;     (track-changes-register SIGNAL)
;;     (track-changes-fetch ID FUNC)
;;     (track-changes-unregister ID)
;;
;; A typical use case might look like:
;;
;;     (defvar my-foo--change-tracker nil)
;;     (define-minor-mode my-foo-mode
;;       "Fooing like there's no tomorrow."
;;       (if (null my-foo-mode)
;;           (when my-foo--change-tracker
;;             (track-changes-unregister my-foo--change-tracker)
;;             (setq my-foo--change-tracker nil))
;;         (unless my-foo--change-tracker
;;           (setq my-foo--change-tracker
;;                 (track-changes-register
;;                  (lambda ()
;;                    (track-changes-fetch
;;                     my-foo--change-tracker
;;                     (lambda (beg end before)
;;                       ..DO THE THING..))))))))

;;; Code:

;; FIXME: Try and do some sanity-checks (e.g. looking at `buffer-size'),
;; to detect if/when we somehow missed some changes.
;; FIXME: The API doesn't offer an easy way to signal a "full resync"
;; kind of change, as might be needed if we lost changes.

(require 'cl-lib)

(cl-defstruct (track-changes--tracker
               (:noinline t)
               (:constructor nil)
               (:constructor track-changes--tracker ( signal state)))
  ( signal nil :read-only t)
  state)

(cl-defstruct (track-changes--state
               (:noinline t)
               (:constructor nil)
               (:constructor track-changes--state ()))
  (beg (point-max))
  (end (point-min))
  (bbeg (point-max))                    ;BEG of the BEFORE string,
  (bend (point-min))                    ;END of the BEFORE string.
  (before nil)
  (next nil))

(defvar-local track-changes--trackers ())
(defvar-local track-changes--clean-trackers ())

(defvar-local track-changes--state nil)

(defun track-changes-register ( signal)
  "Register a new tracker and return a new tracker ID.
SIGNAL is a function that will be called with no argument when
the current buffer is modified, so that we can react to the change.
Once called, SIGNAL is not called again until `track-changes-fetch'
is called with the corresponding tracker ID."
  ;; FIXME: Add an optional arg to choose between `funcall' and `funcall-la=
ter'?
  (track-changes--clean-state)
  (add-hook 'before-change-functions #'track-changes--before nil t)
  (add-hook 'after-change-functions  #'track-changes--after  nil t)
  (let ((tracker (track-changes--tracker signal track-changes--state)))
    (push tracker track-changes--trackers)
    (push tracker track-changes--clean-trackers)
    tracker))

(defun track-changes-unregister (id)
  "Remove the tracker denoted by ID.
Trackers can consume resources (especially if `track-changes-fetch' is
not called), so it is good practice to unregister them when you don't
need them any more."
  (unless (memq id track-changes--trackers)
    (error "Unregistering a non-registered tracker: %S" id))
  (setq track-changes--trackers (delq id track-changes--trackers))
  (setq track-changes--clean-trackers (delq id track-changes--clean-tracker=
s))
  (when (null track-changes--trackers)
    (setq track-changes--state nil)
    (remove-hook 'before-change-functions #'track-changes--before t)
    (remove-hook 'after-change-functions  #'track-changes--after  t)))


(defun track-changes--clean-p ()
  (null (track-changes--state-before track-changes--state)))

(defun track-changes--clean-state ()
  (cond
   ((null track-changes--state)
    ;; No state has been created yet.  Do it now.
    (setq track-changes--state (track-changes--state)))
   ((track-changes--clean-p) nil)
   (t
    ;; FIXME: We may be in-between a before-c-f and an after-c-f, so we
    ;; should save some of the current buffer in case an after-c-f comes
    ;; before a before-c-f.
    (let ((new (track-changes--state)))
      (setf (track-changes--state-next track-changes--state) new)
      (setq track-changes--state new)))))

(defun track-changes--before (beg end)
  (cl-assert track-changes--state)
  (cl-assert (<=3D beg end))
  (if (track-changes--clean-p)
      (progn
        (setf (track-changes--state-before track-changes--state)
              (buffer-substring-no-properties beg end))
        (setf (track-changes--state-bbeg track-changes--state) beg)
        (setf (track-changes--state-bend track-changes--state) end))
    (cl-assert
     (save-restriction
       (widen)
       (<=3D (point-min)
           (track-changes--state-bbeg track-changes--state)
           (track-changes--state-bend track-changes--state)
           (point-max))))
    (when (< beg (track-changes--state-bbeg track-changes--state))
      (let* ((old-bbeg (track-changes--state-bbeg track-changes--state))
             ;; To avoid O(N=B2) behavior when faced with many small change=
s,
             ;; we copy more than needed.
             (new-bbeg (min (max (point-min)
                                 (- old-bbeg
                                    (length (track-changes--state-before
                                             track-changes--state))))
                            beg)))
        (setf (track-changes--state-bbeg track-changes--state) beg)
        (cl-callf (lambda (old new) (concat new old))
            (track-changes--state-before track-changes--state)
          (buffer-substring-no-properties new-bbeg old-bbeg))))

    (when (< (track-changes--state-bend track-changes--state) end)
      (let* ((old-bend (track-changes--state-bend track-changes--state))
             ;; To avoid O(N=B2) behavior when faced with many small change=
s,
             ;; we copy more than needed.
             (new-bend (max (min (point-max)
                                 (+ old-bend
                                    (length (track-changes--state-before
                                             track-changes--state))))
                            end)))
        (setf (track-changes--state-bend track-changes--state) end)
        (cl-callf concat (track-changes--state-before track-changes--state)
          (buffer-substring-no-properties old-bend new-bend))))))

(defun track-changes--after (beg end len)
  (cl-assert track-changes--state)
  (cl-assert (track-changes--state-before track-changes--state))
  (let ((offset (- (- end beg) len)))
    (cl-incf (track-changes--state-bend track-changes--state) offset)
    (cl-assert
     (save-restriction
       (widen)
       (<=3D (point-min)
           (track-changes--state-bbeg track-changes--state)
           beg end
           (track-changes--state-bend track-changes--state)
           (point-max))))
    ;; Note the new changes.
    (when (< beg (track-changes--state-beg track-changes--state))
      (setf (track-changes--state-beg track-changes--state) beg))
    (cl-callf (lambda (old-end) (max end (+ old-end offset)))
        (track-changes--state-end track-changes--state)))
  (cl-assert (<=3D (track-changes--state-bbeg track-changes--state)
                 (track-changes--state-beg track-changes--state)
                 beg end
                 (track-changes--state-end track-changes--state)
                 (track-changes--state-bend track-changes--state)))
  (while track-changes--clean-trackers
    (let ((tracker (pop track-changes--clean-trackers)))
      ;; FIXME: Use `funcall'?
      (funcall-later (track-changes--tracker-signal tracker) ()))))

(defun track-changes-fetch (id func)
  "Fetch the pending changes.
ID is the tracker ID returned by a previous `track-changes-register'.
FUNC is a function.  It is called with 3 arguments (BEGIN END BEFORE)
where BEGIN..END delimit the region that was changed since the last
time `track-changes-fetch' was called and BEFORE is a string containing
the previous content of that region.

If no changes occurred since the last time, FUNC is not called and
we return nil, otherwise we return the value returned by FUNC,
and re-enable the TRACKER corresponding to ID."
  (let ((beg nil)
        (end nil)
        (before nil)
        (states ()))
    ;; We want to combine the states from most recent to oldest,
    ;; so reverse them.
    (let ((state (track-changes--tracker-state id)))
      (while state
        (push state states)
        (setq state (track-changes--state-next state))))
    (when (null (track-changes--state-before (car states)))
      (cl-assert (eq (car states) track-changes--state))
      (setq states (cdr states)))
    (if (null states)
        (progn
          (cl-assert (memq id track-changes--clean-trackers))
          nil)
      (dolist (state states)
        (let ((prevbbeg (track-changes--state-bbeg state))
              (prevbend (track-changes--state-bend state))
              (prevbefore (track-changes--state-before state)))
          (if (not before)
              (progn
                ;; This is the most recent change.  Just initialize the var=
s.
                (setq beg (track-changes--state-beg state))
                (setq end (track-changes--state-end state))
                (setq before prevbefore)
                (unless (and (=3D beg prevbbeg) (=3D end prevbend))
                  (setq before
                        (substring before
                                   (- beg (track-changes--state-bbeg state))
                                   (- (length before)
                                      (- (track-changes--state-bend state)
                                         end))))))
            ;; FIXME: When merging "states", we disregard the `beg/end'
            ;; in favor of `bbeg/bend' which also works but is conservative.
            (let ((endb (+ beg (length before))))
              (when (< prevbbeg beg)
                (setq before (concat (buffer-substring-no-properties
                                      prevbbeg beg)
                                     before))
                (setq beg prevbbeg)
                (cl-assert (=3D endb (+ beg (length before)))))
              (when (< endb prevbend)
                (let ((new-end (+ end (- prevbend endb))))
                  (setq before (concat before
                                       (buffer-substring-no-properties
                                        end new-end)))
                  (setq end new-end)
                  (cl-assert (=3D prevbend (+ beg (length before))))
                  (setq endb (+ beg (length before)))))
              (cl-assert (<=3D beg prevbbeg prevbend endb))
              ;; The `prevbefore' is covered by the new one.
              (setq before
                    (concat (substring before 0 (- prevbbeg beg))
                            prevbefore
                            (substring before (- (length before)
                                                 (- endb prevbend)))))))))
      (cl-assert (<=3D (point-min) beg end (point-max)))
      ;; Clean the state of the tracker before calling `func', in case
      ;; `func' performs buffer modifications.
      (track-changes--clean-state)
      ;; Update the tracker's state before running `func' so we don't risk
      ;; mistakenly replaying the changes in case `func' exits non-locally.
      (setf (track-changes--tracker-state id) track-changes--state)
      (unwind-protect (funcall func beg end before)
        ;; Re-enable the tracker's signal only after running `func', so
        ;; as to avoid recursive invocations.
        (cl-pushnew id track-changes--clean-trackers)))))

(defmacro with-track-changes (id vars &rest body)
  (declare (indent 2) (debug (form sexp body)))
  `(track-changes-fetch ,id (lambda ,vars ,@body)))
=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20

(provide 'track-changes)
;;; track-changes.el end here.

--=-=-=--





Acknowledgement sent to Stefan Monnier <monnier@HIDDEN>:
New bug report received and forwarded. Copy sent to mail@HIDDEN, yantar92@HIDDEN, acm@HIDDEN, joaotavora@HIDDEN, alan.zimm@HIDDEN, frederic.bour@HIDDEN, phillip.lord@HIDDEN, stephen_leake@HIDDEN, casouri@HIDDEN, qhong@HIDDEN, monnier@HIDDEN, bug-gnu-emacs@HIDDEN. Full text available.
Report forwarded to mail@HIDDEN, yantar92@HIDDEN, acm@HIDDEN, joaotavora@HIDDEN, alan.zimm@HIDDEN, frederic.bour@HIDDEN, phillip.lord@HIDDEN, stephen_leake@HIDDEN, casouri@HIDDEN, qhong@HIDDEN, monnier@HIDDEN, bug-gnu-emacs@HIDDEN:
bug#70077; Package emacs. Full text available.
Please note: This is a static page, with minimal formatting, updated once a day.
Click here to see this page with the latest information and nicer formatting.
Last modified: Tue, 9 Apr 2024 04:00:04 UTC

GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997 nCipher Corporation Ltd, 1994-97 Ian Jackson.