aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/lispref/text.texi148
-rw-r--r--etc/NEWS18
-rw-r--r--lisp/emacs-lisp/track-changes.el619
-rw-r--r--lisp/progmodes/eglot.el64
-rw-r--r--lisp/vc/diff-mode.el85
-rw-r--r--test/lisp/emacs-lisp/track-changes-tests.el156
6 files changed, 1037 insertions, 53 deletions
diff --git a/doc/lispref/text.texi b/doc/lispref/text.texi
index 18f0ee88fe5..8774801f41f 100644
--- a/doc/lispref/text.texi
+++ b/doc/lispref/text.texi
@@ -6375,3 +6375,151 @@ hooks during a series of changes (typically for performance reasons),
6375use @code{combine-change-calls} or @code{combine-after-change-calls} 6375use @code{combine-change-calls} or @code{combine-after-change-calls}
6376instead. 6376instead.
6377@end defvar 6377@end defvar
6378
6379@node Tracking changes
6380@subsection Tracking changes
6381@cindex track-changes
6382@cindex change tracker
6383
6384Using @code{before-change-functions} and @code{after-change-functions}
6385can be difficult in practice because of a number of pitfalls, such as
6386the fact that the two calls are not always properly paired, or some
6387calls may be missing, either because some Emacs primitives failed to
6388properly pair them or because of incorrect use of
6389@code{inhibit-modification-hooks}. Furthermore,
6390many restrictions apply to those hook functions, such as the fact that
6391they basically should never modify the current buffer, nor use an
6392operation that may block, and they proceed quickly because
6393some commands may call these hooks a large number of times.
6394
6395The Track-Changes library fundamentally provides an alternative API,
6396built on top of those hooks. Compared to @code{after-change-functions},
6397the first important difference is that, instead of providing the bounds
6398of the change and the previous length, it provides the bounds of the
6399change and the actual previous content of that region. The need to
6400extract information from the original contents of the buffer is one of
6401the main reasons why some packages need to use both
6402@code{before-change-functions} and @code{after-change-functions} and
6403then try to match them up.
6404
6405The second difference is that it decouples the notification of a change
6406from the act of processing it, and it automatically combines into
6407a single change operation all the changes that occur between the first
6408change and the actual processing. This makes it natural and easy to
6409process the changes at a larger granularity, such as once per command,
6410and eliminates most of the restrictions that apply to the usual change
6411hook functions, making it possible to use blocking operations or to
6412modify the buffer.
6413
6414To start tracking changes, you have to call
6415@code{track-changes-register}, passing it a @var{signal} function as
6416argument. This returns a tracker @var{id} which is used to identify
6417your change tracker to the other functions of the library.
6418When the buffer is modified, the library calls the @var{signal}
6419function to inform you of that change and immediately starts
6420accumulating subsequent changes into a single combined change.
6421The @var{signal} function serves only to warn that a modification
6422occurred but does not receive a description of the change. Also the
6423library will not call it again until after you retrieved the change.
6424
6425To retrieve changes, you need to call @code{track-changes-fetch}, which
6426provides you with the bounds of the changes accumulated since the
6427last call, as well as the previous content of that region. It also
6428``re-arms'' the @var{signal} function so that the library will call it
6429again after the next buffer modification.
6430
6431@defun track-changes-register signal &key nobefore disjoint immediate
6432This function creates a new @dfn{change tracker}. Change trackers are kept
6433abstract, so we refer to them as mere identities, and the function thus
6434returns the tracker's @var{id}.
6435
6436@var{signal} is a function that the library will call to notify of
6437a change. It will sometimes call it with a single argument and
6438sometimes with two. Upon the first change to the buffer since this
6439tracker last called @code{track-changes-fetch}, the library calls this
6440@var{signal} function with a single argument holding the @var{id} of
6441the tracker.
6442
6443By default, the call to the @var{signal} function does not happen
6444immediately, but is instead postponed with a 0 seconds timer
6445(@pxref{Timers}). This is usually desired to make sure the @var{signal}
6446function is not called too frequently and runs in a permissive context,
6447freeing the client from performance concerns or worries about which
6448operations might be problematic. If a client wants to have more
6449control, they can provide a non-@code{nil} value as the @var{immediate}
6450argument in which case the library calls the @var{signal} function
6451directly from @code{after-change-functions}. Beware that it means that
6452the @var{signal} function has to be careful not to modify the buffer or
6453use operations that may block.
6454
6455If you're not interested in the actual previous content of the buffer,
6456but are using this library only for its ability to combine many small
6457changes into a larger one and to delay the processing to a more
6458convenient time, you can specify a non-@code{nil} value for the
6459@var{nobefore} argument. In that case, @code{track-change-fetch}
6460provides you only with the length of the previous content, just like
6461@code{after-change-functions}. It also allows the library to save
6462some work.
6463
6464While you may like to accumulate many small changes into larger ones,
6465you may not want to do that if the changes are too far apart. If you
6466specify a non-@code{nil} value for the @var{disjoint} argument, the library
6467will let you know when a change is about to occur ``far'' from the
6468currently pending ones by calling the @var{signal} function right away,
6469passing it two arguments this time: the @var{id} of the tracker, and the
6470number of characters that separates the upcoming change from the
6471already pending changes. This in itself does not prevent combining this
6472new change with the previous ones, so if you think the upcoming change
6473is indeed too far, you need to call @code{track-change-fetch}
6474right away.
6475Beware that when the @var{signal} function is called because of
6476a disjoint change, this happens directly from
6477@code{before-change-functions}, so the usual restrictions apply about
6478modifying the buffer or using operations that may block.
6479@end defun
6480
6481@defun track-changes-fetch id func
6482This is the function that lets you find out what has changed in the
6483buffer. By providing the tracker @var{id} you let the library figure
6484out which changes have already been seen by your tracker. Instead of
6485returning a description of the changes, @code{track-changes-fetch} calls
6486the @var{func} function with that description in the form of
64873 arguments: @var{beg}, @var{end}, and @var{before}, where
6488@code{@var{beg}..@var{end}} delimit the region that was modified and
6489@var{before} describes the previous content of that region.
6490Usually @var{before} is a string containing the previous text of the
6491modified region, but if you specified a non-@code{nil} @var{nobefore} argument
6492to @code{track-changes-register}, then it is replaced by the number of
6493characters of that previous text.
6494
6495In case no changes occurred since the last call,
6496@code{track-changes-fetch} simply does not call @var{func} and returns
6497@code{nil}. If changes did occur, it calls @var{func} and returns the value
6498returned by @var{func}. But note that @var{func} is called just once
6499regardless of how many changes occurred: those are summarized into
6500a single @var{beg}/@var{end}/@var{before} triplet.
6501
6502In some cases, the library is not properly notified of all changes,
6503for example because of a bug in the low-level C code or because of an
6504improper use of @code{inhibit-modification-hooks}. When it detects such
6505a problem, @var{func} receives a @code{@var{beg}..@var{end}} region
6506which covers the whole buffer and the @var{before} argument is the
6507symbol @code{error} to indicate that the library was unable to determine
6508what was changed.
6509
6510Once @var{func} finishes, @code{track-changes-fetch} re-enables the
6511@var{signal} function so that it will be called the next time a change
6512occurs. This is the reason why it calls @var{func} instead of returning
6513a description: it lets you process the change without worrying about the
6514risk that the @var{signal} function gets triggered in the middle of it,
6515because the @var{signal} is re-enabled only after @var{func} finishes.
6516@end defun
6517
6518@defun track-changes-unregister id
6519This function tells the library that the tracker @var{id} does not need
6520to know about buffer changes any more. Most clients will never want to
6521stop tracking changes, but for clients such as minor modes, it is
6522important to call this function when the minor mode is disabled,
6523otherwise the tracker will keep accumulating changes and consume more
6524and more resources.
6525@end defun
diff --git a/etc/NEWS b/etc/NEWS
index a2a3fe494cb..2cf6477ba99 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -15,6 +15,12 @@ in older Emacs versions.
15You can narrow news to a specific version by calling 'view-emacs-news' 15You can narrow news to a specific version by calling 'view-emacs-news'
16with a prefix argument or by typing 'C-u C-h C-n'. 16with a prefix argument or by typing 'C-u C-h C-n'.
17 17
18Temporary note:
19+++ indicates that all relevant manuals in doc/ have been updated.
20--- means no change in the manuals is needed.
21When you add a new item, use the appropriate mark if you are sure it
22applies, and please also update docstrings as needed.
23
18 24
19* Installation Changes in Emacs 30.1 25* Installation Changes in Emacs 30.1
20 26
@@ -1586,6 +1592,18 @@ options of GNU 'ls'.
1586 1592
1587* New Modes and Packages in Emacs 30.1 1593* New Modes and Packages in Emacs 30.1
1588 1594
1595+++
1596** New package Track-Changes.
1597This library is a layer of abstraction above 'before-change-functions'
1598and 'after-change-functions' which provides a superset of
1599the functionality of 'after-change-functions':
1600- It provides the actual previous text rather than only its length.
1601- It takes care of accumulating and bundling changes until a time when
1602 its client finds it convenient to react to them.
1603- It detects most cases where some changes were not properly
1604 reported (calls to 'before/after-change-functions' that are
1605 incorrectly paired, missing, etc...) and reports them adequately.
1606
1589** New major modes based on the tree-sitter library 1607** New major modes based on the tree-sitter library
1590 1608
1591+++ 1609+++
diff --git a/lisp/emacs-lisp/track-changes.el b/lisp/emacs-lisp/track-changes.el
new file mode 100644
index 00000000000..3479fecef25
--- /dev/null
+++ b/lisp/emacs-lisp/track-changes.el
@@ -0,0 +1,619 @@
1;;; track-changes.el --- API to react to buffer modifications -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2024 Free Software Foundation, Inc.
4
5;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
6
7;; This file is part of GNU Emacs.
8
9;; GNU Emacs is free software: you can redistribute it and/or modify
10;; it under the terms of the GNU General Public License as published by
11;; the Free Software Foundation, either version 3 of the License, or
12;; (at your option) any later version.
13
14;; GNU Emacs is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;; GNU General Public License for more details.
18
19;; You should have received a copy of the GNU General Public License
20;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
21
22;;; Commentary:
23
24;; This library is a layer of abstraction above `before-change-functions'
25;; and `after-change-functions' which takes care of accumulating changes
26;; until a time when its client finds it convenient to react to them.
27;;
28;; It provides an API that is easier to use correctly than our
29;; `*-change-functions' hooks. Problems that it claims to solve:
30;;
31;; - Before and after calls are not necessarily paired.
32;; - The beg/end values don't always match.
33;; - There's usually only one call to the hooks per command but
34;; there can be thousands of calls from within a single command,
35;; so naive users will tend to write code that performs poorly
36;; in those rare cases.
37;; - The hooks are run at a fairly low-level so there are things they
38;; really shouldn't do, such as modify the buffer or wait.
39;; - The after call doesn't get enough info to rebuild the before-change state,
40;; so some callers need to use both before-c-f and after-c-f (and then
41;; deal with the first two points above).
42;;
43;; The new API is almost like `after-change-functions' except that:
44;; - It provides the "before string" (i.e. the previous content of
45;; the changed area) rather than only its length.
46;; - It can combine several changes into larger ones.
47;; - Clients do not have to process changes right away, instead they
48;; can let changes accumulate (by combining them into a larger change)
49;; until it is convenient for them to process them.
50;; - By default, changes are signaled at most once per command.
51
52;; The API consists in the following functions:
53;;
54;; (track-changes-register SIGNAL &key NOBEFORE DISJOINT IMMEDIATE)
55;; (track-changes-fetch ID FUNC)
56;; (track-changes-unregister ID)
57;;
58;; A typical use case might look like:
59;;
60;; (defvar my-foo--change-tracker nil)
61;; (define-minor-mode my-foo-mode
62;; "Fooing like there's no tomorrow."
63;; (if (null my-foo-mode)
64;; (when my-foo--change-tracker
65;; (track-changes-unregister my-foo--change-tracker)
66;; (setq my-foo--change-tracker nil))
67;; (unless my-foo--change-tracker
68;; (setq my-foo--change-tracker
69;; (track-changes-register
70;; (lambda (id)
71;; (track-changes-fetch
72;; id (lambda (beg end before)
73;; ..DO THE THING..))))))))
74
75;;; Code:
76
77;; Random ideas:
78;; - We could let trackers specify a function to record auxiliary info
79;; about a state. This would be called from the first before-c-f
80;; and then provided to FUNC. TeXpresso could use it to avoid needing
81;; the BEFORE string: it could record the total number of bytes
82;; in the "before" state so that from `track-changes-fetch' it could
83;; compute the number of bytes that used to be in BEG/END.
84
85(require 'cl-lib)
86
87;;;; Internal types and variables.
88
89(cl-defstruct (track-changes--tracker
90 (:noinline t)
91 (:constructor nil)
92 (:constructor track-changes--tracker ( signal state
93 &optional
94 nobefore immediate)))
95 signal state nobefore immediate)
96
97(cl-defstruct (track-changes--state
98 (:noinline t)
99 (:constructor nil)
100 (:constructor track-changes--state ()))
101 "Object holding a description of a buffer state.
102A buffer state is described by a BEG/END/BEFORE triplet which say how to
103recover that state from the next state. I.e. if the buffer's contents
104reflects the next state, you can recover the previous state by replacing
105the BEG..END region with the BEFORE string.
106
107NEXT is the next state object (i.e. a more recent state).
108If NEXT is nil it means it's the most recent state and it may be incomplete
109\(BEG/END/BEFORE may be nil), in which case those fields will take their
110values from `track-changes--before-(beg|end|before)' when the next
111state is created."
112 (beg (point-max))
113 (end (point-min))
114 (before nil)
115 (next nil))
116
117(defvar-local track-changes--trackers ()
118 "List of trackers currently registered in the buffer.")
119(defvar-local track-changes--clean-trackers ()
120 "List of trackers that are clean.
121Those are the trackers that get signaled when a change is made.")
122
123(defvar-local track-changes--disjoint-trackers ()
124 "List of trackers that want to react to disjoint changes.
125These trackers are signaled every time track-changes notices
126that some upcoming changes touch another \"distant\" part of the buffer.")
127
128(defvar-local track-changes--state nil)
129
130;; `track-changes--before-*' keep track of the content of the
131;; buffer when `track-changes--state' was cleaned.
132(defvar-local track-changes--before-beg 0
133 "Beginning position of the remembered \"before string\".")
134(defvar-local track-changes--before-end 0
135 "End position of the text replacing the \"before string\".")
136(defvar-local track-changes--before-string ""
137 "String holding some contents of the buffer before the current change.
138This string is supposed to cover all the already modified areas plus
139the upcoming modifications announced via `before-change-functions'.
140If all trackers are `nobefore', then this holds the `buffer-size' before
141the current change.")
142(defvar-local track-changes--before-no t
143 "If non-nil, all the trackers are `nobefore'.
144Should be equal to (memq #\\='track-changes--before before-change-functions).")
145
146(defvar-local track-changes--before-clean 'unset
147 "Status of `track-changes--before-*' vars.
148More specifically it indicates which \"before\" they hold.
149- nil: The vars hold the \"before\" info of the current state.
150- `unset': The vars hold the \"before\" info of some older state.
151 This is what it is set to right after creating a fresh new state.
152- `set': Like nil but the state is still clean because the buffer has not
153 been modified yet. This is what it is set to after the first
154 `before-change-functions' but before an `after-change-functions'.")
155
156(defvar-local track-changes--buffer-size nil
157 "Current size of the buffer, as far as this library knows.
158This is used to try and detect cases where buffer modifications are \"lost\".")
159
160;;;; Exposed API.
161
162(cl-defun track-changes-register ( signal &key nobefore disjoint immediate)
163 "Register a new tracker whose change-tracking function is SIGNAL.
164Return the ID of the new tracker.
165
166SIGNAL is a function that will be called with one argument (the tracker ID)
167after the current buffer is modified, so that it can react to the change.
168Once called, SIGNAL is not called again until `track-changes-fetch'
169is called with the corresponding tracker ID.
170
171If optional argument NOBEFORE is non-nil, it means that this tracker does
172not need the BEFORE strings (it will receive their size instead).
173
174If optional argument DISJOINT is non-nil, SIGNAL is called every time just
175before combining changes from \"distant\" parts of the buffer.
176This is needed when combining disjoint changes into one bigger change
177is unacceptable, typically for performance reasons.
178These calls are distinguished from normal calls by calling SIGNAL with
179a second argument which is the distance between the upcoming change and
180the previous changes.
181BEWARE: In that case SIGNAL is called directly from `before-change-functions'
182and should thus be extra careful: don't modify the buffer, don't call a function
183that may block, ...
184In order to prevent the upcoming change from being combined with the previous
185changes, SIGNAL needs to call `track-changes-fetch' before it returns.
186
187By default SIGNAL is called after a change via a 0 seconds timer.
188If optional argument IMMEDIATE is non-nil it means SIGNAL should be called
189as soon as a change is detected,
190BEWARE: In that case SIGNAL is called directly from `after-change-functions'
191and should thus be extra careful: don't modify the buffer, don't call a function
192that may block, do as little work as possible, ...
193When IMMEDIATE is non-nil, the SIGNAL should probably not always call
194`track-changes-fetch', since that would defeat the purpose of this library."
195 (when (and nobefore disjoint)
196 ;; FIXME: Without `before-change-functions', we can discover
197 ;; a disjoint change only after the fact, which is not good enough.
198 ;; But we could use a stripped down before-change-function,
199 (error "`disjoint' not supported for `nobefore' trackers"))
200 (track-changes--clean-state)
201 (unless nobefore
202 (setq track-changes--before-no nil)
203 (add-hook 'before-change-functions #'track-changes--before nil t))
204 (add-hook 'after-change-functions #'track-changes--after nil t)
205 (let ((tracker (track-changes--tracker signal track-changes--state
206 nobefore immediate)))
207 (push tracker track-changes--trackers)
208 (push tracker track-changes--clean-trackers)
209 (when disjoint
210 (push tracker track-changes--disjoint-trackers))
211 tracker))
212
213(defun track-changes-unregister (id)
214 "Remove the tracker denoted by ID.
215Trackers can consume resources (especially if `track-changes-fetch' is
216not called), so it is good practice to unregister them when you don't
217need them any more."
218 (unless (memq id track-changes--trackers)
219 (error "Unregistering a non-registered tracker: %S" id))
220 (setq track-changes--trackers (delq id track-changes--trackers))
221 (setq track-changes--clean-trackers (delq id track-changes--clean-trackers))
222 (setq track-changes--disjoint-trackers
223 (delq id track-changes--disjoint-trackers))
224 (when (cl-every #'track-changes--tracker-nobefore track-changes--trackers)
225 (setq track-changes--before-no t)
226 (remove-hook 'before-change-functions #'track-changes--before t))
227 (when (null track-changes--trackers)
228 (mapc #'kill-local-variable
229 '(track-changes--before-beg
230 track-changes--before-end
231 track-changes--before-string
232 track-changes--buffer-size
233 track-changes--before-clean
234 track-changes--state))
235 (remove-hook 'after-change-functions #'track-changes--after t)))
236
237(defun track-changes-fetch (id func)
238 "Fetch the pending changes for tracker ID pass them to FUNC.
239ID is the tracker ID returned by a previous `track-changes-register'.
240FUNC is a function. It is called with 3 arguments (BEGIN END BEFORE)
241where BEGIN..END delimit the region that was changed since the last
242time `track-changes-fetch' was called and BEFORE is a string containing
243the previous content of that region (or just its length as an integer
244if the tracker ID was registered with the `nobefore' option).
245If track-changes detected that some changes were missed, then BEFORE will
246be the symbol `error' to indicate that the buffer got out of sync.
247This reflects a bug somewhere, so please report it when it happens.
248
249If no changes occurred since the last time, it doesn't call FUNC and
250returns nil, otherwise it returns the value returned by FUNC
251and re-enable the TRACKER corresponding to ID."
252 (cl-assert (memq id track-changes--trackers))
253 (unless (equal track-changes--buffer-size (buffer-size))
254 (track-changes--recover-from-error))
255 (let ((beg nil)
256 (end nil)
257 (before t)
258 (lenbefore 0)
259 (states ()))
260 ;; Transfer the data from `track-changes--before-string'
261 ;; to the tracker's state object, if needed.
262 (track-changes--clean-state)
263 ;; We want to combine the states from most recent to oldest,
264 ;; so reverse them.
265 (let ((state (track-changes--tracker-state id)))
266 (while state
267 (push state states)
268 (setq state (track-changes--state-next state))))
269
270 (cond
271 ((eq (car states) track-changes--state)
272 (cl-assert (null (track-changes--state-before (car states))))
273 (setq states (cdr states)))
274 (t
275 ;; The states are disconnected from the latest state because
276 ;; we got out of sync!
277 (cl-assert (eq (track-changes--state-before (car states)) 'error))
278 (setq beg (point-min))
279 (setq end (point-max))
280 (setq before 'error)
281 (setq states nil)))
282
283 (dolist (state states)
284 (let ((prevbeg (track-changes--state-beg state))
285 (prevend (track-changes--state-end state))
286 (prevbefore (track-changes--state-before state)))
287 (if (eq before t)
288 (progn
289 ;; This is the most recent change. Just initialize the vars.
290 (setq beg prevbeg)
291 (setq end prevend)
292 (setq lenbefore
293 (if (stringp prevbefore) (length prevbefore) prevbefore))
294 (setq before
295 (unless (track-changes--tracker-nobefore id) prevbefore)))
296 (let ((endb (+ beg lenbefore)))
297 (when (< prevbeg beg)
298 (if (not before)
299 (setq lenbefore (+ (- beg prevbeg) lenbefore))
300 (setq before
301 (concat (buffer-substring-no-properties
302 prevbeg beg)
303 before))
304 (setq lenbefore (length before)))
305 (setq beg prevbeg)
306 (cl-assert (= endb (+ beg lenbefore))))
307 (when (< endb prevend)
308 (let ((new-end (+ end (- prevend endb))))
309 (if (not before)
310 (setq lenbefore (+ lenbefore (- new-end end)))
311 (setq before
312 (concat before
313 (buffer-substring-no-properties
314 end new-end)))
315 (setq lenbefore (length before)))
316 (setq end new-end)
317 (cl-assert (= prevend (+ beg lenbefore)))
318 (setq endb (+ beg lenbefore))))
319 (cl-assert (<= beg prevbeg prevend endb))
320 ;; The `prevbefore' is covered by the new one.
321 (if (not before)
322 (setq lenbefore
323 (+ (- prevbeg beg)
324 (if (stringp prevbefore)
325 (length prevbefore) prevbefore)
326 (- endb prevend)))
327 (setq before
328 (concat (substring before 0 (- prevbeg beg))
329 prevbefore
330 (substring before (- (length before)
331 (- endb prevend)))))
332 (setq lenbefore (length before)))))))
333 (if (null beg)
334 (progn
335 (cl-assert (null states))
336 (cl-assert (memq id track-changes--clean-trackers))
337 (cl-assert (eq (track-changes--tracker-state id)
338 track-changes--state))
339 ;; Nothing to do.
340 nil)
341 (cl-assert (not (memq id track-changes--clean-trackers)))
342 (cl-assert (<= (point-min) beg end (point-max)))
343 ;; Update the tracker's state *before* running `func' so we don't risk
344 ;; mistakenly replaying the changes in case `func' exits non-locally.
345 (setf (track-changes--tracker-state id) track-changes--state)
346 (unwind-protect (funcall func beg end (or before lenbefore))
347 ;; Re-enable the tracker's signal only after running `func', so
348 ;; as to avoid recursive invocations.
349 (cl-pushnew id track-changes--clean-trackers)))))
350
351;;;; Auxiliary functions.
352
353(defun track-changes--clean-state ()
354 (cond
355 ((null track-changes--state)
356 (cl-assert track-changes--before-clean)
357 (cl-assert (null track-changes--buffer-size))
358 ;; No state has been created yet. Do it now.
359 (setq track-changes--buffer-size (buffer-size))
360 (when track-changes--before-no
361 (setq track-changes--before-string (buffer-size)))
362 (setq track-changes--state (track-changes--state)))
363 (track-changes--before-clean
364 ;; If the state is already clean, there's nothing to do.
365 nil)
366 (t
367 (cl-assert (<= (track-changes--state-beg track-changes--state)
368 (track-changes--state-end track-changes--state)))
369 (let ((actual-beg (track-changes--state-beg track-changes--state))
370 (actual-end (track-changes--state-end track-changes--state)))
371 (if track-changes--before-no
372 (progn
373 (cl-assert (integerp track-changes--before-string))
374 (setf (track-changes--state-before track-changes--state)
375 (- track-changes--before-string
376 (- (buffer-size) (- actual-end actual-beg))))
377 (setq track-changes--before-string (buffer-size)))
378 (cl-assert (<= track-changes--before-beg
379 actual-beg actual-end
380 track-changes--before-end))
381 (cl-assert (null (track-changes--state-before track-changes--state)))
382 ;; The `track-changes--before-*' vars can cover more text than the
383 ;; actually modified area, so trim it down now to the relevant part.
384 (unless (= (- track-changes--before-end track-changes--before-beg)
385 (- actual-end actual-beg))
386 (setq track-changes--before-string
387 (substring track-changes--before-string
388 (- actual-beg track-changes--before-beg)
389 (- (length track-changes--before-string)
390 (- track-changes--before-end actual-end))))
391 (setq track-changes--before-beg actual-beg)
392 (setq track-changes--before-end actual-end))
393 (setf (track-changes--state-before track-changes--state)
394 track-changes--before-string)))
395 ;; Note: We preserve `track-changes--before-*' because they may still
396 ;; be needed, in case `after-change-functions' are run before the next
397 ;; `before-change-functions'.
398 ;; Instead, we set `track-changes--before-clean' to `unset' to mean that
399 ;; `track-changes--before-*' can be reset at the next
400 ;; `before-change-functions'.
401 (setq track-changes--before-clean 'unset)
402 (let ((new (track-changes--state)))
403 (setf (track-changes--state-next track-changes--state) new)
404 (setq track-changes--state new)))))
405
406(defvar track-changes--disjoint-threshold 100
407 "Number of chars below which changes are not considered disjoint.")
408
409(defvar track-changes--error-log ()
410 "List of errors encountered.
411Each element is a triplet (BUFFER-NAME BACKTRACE RECENT-KEYS).")
412
413(defun track-changes--recover-from-error ()
414 ;; We somehow got out of sync. This is usually the result of a bug
415 ;; elsewhere that causes the before-c-f and after-c-f to be improperly
416 ;; paired, or to be skipped altogether.
417 ;; Not much we can do, other than force a full re-synchronization.
418 (warn "Missing/incorrect calls to `before/after-change-functions'!!
419Details logged to `track-changes--error-log'")
420 (push (list (buffer-name)
421 (backtrace-frames 'track-changes--recover-from-error)
422 (recent-keys 'include-cmds))
423 track-changes--error-log)
424 (setq track-changes--before-clean 'unset)
425 (setq track-changes--buffer-size (buffer-size))
426 ;; Create a new state disconnected from the previous ones!
427 ;; Mark the previous one as junk, just to be clear.
428 (setf (track-changes--state-before track-changes--state) 'error)
429 (setq track-changes--state (track-changes--state)))
430
431(defun track-changes--before (beg end)
432 (cl-assert track-changes--state)
433 (cl-assert (<= beg end))
434 (let* ((size (- end beg))
435 (reset (lambda ()
436 (cl-assert track-changes--before-clean)
437 (setq track-changes--before-clean 'set)
438 (setf track-changes--before-string
439 (buffer-substring-no-properties beg end))
440 (setf track-changes--before-beg beg)
441 (setf track-changes--before-end end)))
442
443 (signal-if-disjoint
444 (lambda (pos1 pos2)
445 (let ((distance (- pos2 pos1)))
446 (when (> distance
447 (max track-changes--disjoint-threshold
448 ;; If the distance is smaller than the size of the
449 ;; current change, then we may as well consider it
450 ;; as "near".
451 (length track-changes--before-string)
452 size
453 (- track-changes--before-end
454 track-changes--before-beg)))
455 (dolist (tracker track-changes--disjoint-trackers)
456 (funcall (track-changes--tracker-signal tracker)
457 tracker distance))
458 ;; Return non-nil if the state was cleaned along the way.
459 track-changes--before-clean)))))
460
461 (if track-changes--before-clean
462 (progn
463 ;; Detect disjointness with previous changes here as well,
464 ;; so that if a client calls `track-changes-fetch' all the time,
465 ;; it doesn't prevent others from getting a disjointness signal.
466 (when (and track-changes--before-beg
467 (let ((found nil))
468 (dolist (tracker track-changes--disjoint-trackers)
469 (unless (memq tracker track-changes--clean-trackers)
470 (setq found t)))
471 found))
472 ;; There's at least one `tracker' that wants to know about disjoint
473 ;; changes *and* it has unseen pending changes.
474 ;; FIXME: This can occasionally signal a tracker that's clean.
475 (if (< beg track-changes--before-beg)
476 (funcall signal-if-disjoint end track-changes--before-beg)
477 (funcall signal-if-disjoint track-changes--before-end beg)))
478 (funcall reset))
479 (cl-assert (save-restriction
480 (widen)
481 (<= (point-min)
482 track-changes--before-beg
483 track-changes--before-end
484 (point-max))))
485 (when (< beg track-changes--before-beg)
486 (if (and track-changes--disjoint-trackers
487 (funcall signal-if-disjoint end track-changes--before-beg))
488 (funcall reset)
489 (let* ((old-bbeg track-changes--before-beg)
490 ;; To avoid O(N²) behavior when faced with many small changes,
491 ;; we copy more than needed.
492 (new-bbeg (min (max (point-min)
493 (- old-bbeg
494 (length track-changes--before-string)))
495 beg)))
496 (setf track-changes--before-beg new-bbeg)
497 (cl-callf (lambda (old new) (concat new old))
498 track-changes--before-string
499 (buffer-substring-no-properties new-bbeg old-bbeg)))))
500
501 (when (< track-changes--before-end end)
502 (if (and track-changes--disjoint-trackers
503 (funcall signal-if-disjoint track-changes--before-end beg))
504 (funcall reset)
505 (let* ((old-bend track-changes--before-end)
506 ;; To avoid O(N²) behavior when faced with many small changes,
507 ;; we copy more than needed.
508 (new-bend (max (min (point-max)
509 (+ old-bend
510 (length track-changes--before-string)))
511 end)))
512 (setf track-changes--before-end new-bend)
513 (cl-callf concat track-changes--before-string
514 (buffer-substring-no-properties old-bend new-bend))))))))
515
516(defun track-changes--after (beg end len)
517 (cl-assert track-changes--state)
518 (and (eq track-changes--before-clean 'unset)
519 (not track-changes--before-no)
520 ;; This can be a sign that a `before-change-functions' went missing,
521 ;; or that we called `track-changes--clean-state' between
522 ;; a `before-change-functions' and `after-change-functions'.
523 (track-changes--before beg end))
524 (setq track-changes--before-clean nil)
525 (let ((offset (- (- end beg) len)))
526 (cl-incf track-changes--before-end offset)
527 (cl-incf track-changes--buffer-size offset)
528 (if (not (or track-changes--before-no
529 (save-restriction
530 (widen)
531 (<= (point-min)
532 track-changes--before-beg
533 beg end
534 track-changes--before-end
535 (point-max)))))
536 ;; BEG..END is not covered by previous `before-change-functions'!!
537 (track-changes--recover-from-error)
538 ;; Note the new changes.
539 (when (< beg (track-changes--state-beg track-changes--state))
540 (setf (track-changes--state-beg track-changes--state) beg))
541 (cl-callf (lambda (old-end) (max end (+ old-end offset)))
542 (track-changes--state-end track-changes--state))
543 (cl-assert (or track-changes--before-no
544 (<= track-changes--before-beg
545 (track-changes--state-beg track-changes--state)
546 beg end
547 (track-changes--state-end track-changes--state)
548 track-changes--before-end)))))
549 (while track-changes--clean-trackers
550 (let ((tracker (pop track-changes--clean-trackers)))
551 (if (track-changes--tracker-immediate tracker)
552 (funcall (track-changes--tracker-signal tracker) tracker)
553 (run-with-timer 0 nil #'track-changes--call-signal
554 (current-buffer) tracker)))))
555
556(defun track-changes--call-signal (buf tracker)
557 (when (buffer-live-p buf)
558 (with-current-buffer buf
559 ;; Silence ourselves if `track-changes-fetch' was called in the mean time.
560 (unless (memq tracker track-changes--clean-trackers)
561 (funcall (track-changes--tracker-signal tracker) tracker)))))
562
563;;;; Extra candidates for the API.
564
565;; The functions below came up during the design of this library, but
566;; I'm not sure if they're worth the trouble or not, so for now I keep
567;; them here (with a "--" in the name) for documentation. --Stef
568
569;; This could be a good alternative to using a temp-buffer like in
570;; `eglot--virtual-pos-to-lsp-position': since presumably we've just
571;; been changing this very area of the buffer, the gap should be
572;; ready nearby, so the operation should be fairly cheap, while
573;; giving you the comfort of having access to the *full* buffer text.
574;;
575;; It may seem silly to go back to the previous state, since we could have
576;; used `before-change-functions' to run FUNC right then when we were in
577;; that state. The advantage is that with track-changes we get to decide
578;; retroactively which state is the one for which we want to call FUNC and
579;; which BEG..END to use: when that state was current we may have known
580;; then that it would be "the one" but we didn't know what BEG and END
581;; should be because those depend on the changes that came afterwards.
582(defun track-changes--in-revert (beg end before func)
583 "Call FUNC with the buffer contents temporarily reverted to BEFORE.
584FUNC is called with no arguments and with point right after BEFORE.
585FUNC is not allowed to modify the buffer and it should refrain from using
586operations that use a cache populated from the buffer's content,
587such as `syntax-ppss'."
588 (catch 'track-changes--exit
589 (with-silent-modifications ;; This has to be outside `atomic-change-group'.
590 (atomic-change-group
591 (goto-char end)
592 (insert-before-markers before)
593 (delete-region beg end)
594 (throw 'track-changes--exit
595 (let ((inhibit-read-only nil)
596 (buffer-read-only t))
597 (funcall func)))))))
598
599;; This one might be useful for example if a client makes changes to
600;; the buffer and doesn't want to see its own changes.
601(defun track-changes--reset (id)
602 "Mark all past changes as handled for tracker ID.
603Re-arms ID's signal."
604 (track-changes--clean-state)
605 (setf (track-changes--tracker-state id) track-changes--state)
606 (cl-pushnew id track-changes--clean-trackers)
607 (cl-assert (not (track-changes--pending-p id))))
608
609(defun track-changes--pending-p (id)
610 "Return non-nil if there are pending changes for tracker ID."
611 (or (not track-changes--before-clean)
612 (track-changes--state-next id)))
613
614(defmacro with--track-changes (id vars &rest body)
615 (declare (indent 2) (debug (form sexp body)))
616 `(track-changes-fetch ,id (lambda ,vars ,@body)))
617
618(provide 'track-changes)
619;;; 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 @@
110(require 'text-property-search nil t) 110(require 'text-property-search nil t)
111(require 'diff-mode) 111(require 'diff-mode)
112(require 'diff) 112(require 'diff)
113(require 'track-changes nil t)
113 114
114;; These dependencies are also GNU ELPA core packages. Because of 115;; These dependencies are also GNU ELPA core packages. Because of
115;; bug#62576, since there is a risk that M-x package-install, despite 116;; bug#62576, since there is a risk that M-x package-install, despite
@@ -1732,6 +1733,9 @@ return value is fed through the corresponding inverse function
1732 "Calculate number of UTF-16 code units from position given by LBP. 1733 "Calculate number of UTF-16 code units from position given by LBP.
1733LBP defaults to `eglot--bol'." 1734LBP defaults to `eglot--bol'."
1734 (/ (- (length (encode-coding-region (or lbp (eglot--bol)) 1735 (/ (- (length (encode-coding-region (or lbp (eglot--bol))
1736 ;; FIXME: How could `point' ever be
1737 ;; larger than `point-max' (sounds like
1738 ;; a bug in Emacs).
1735 ;; Fix github#860 1739 ;; Fix github#860
1736 (min (point) (point-max)) 'utf-16 t)) 1740 (min (point) (point-max)) 'utf-16 t))
1737 2) 1741 2)
@@ -1749,6 +1753,24 @@ LBP defaults to `eglot--bol'."
1749 :character (progn (when pos (goto-char pos)) 1753 :character (progn (when pos (goto-char pos))
1750 (funcall eglot-current-linepos-function))))) 1754 (funcall eglot-current-linepos-function)))))
1751 1755
1756(defun eglot--virtual-pos-to-lsp-position (pos string)
1757 "Return the LSP position at the end of STRING if it were inserted at POS."
1758 (eglot--widening
1759 (goto-char pos)
1760 (forward-line 0)
1761 ;; LSP line is zero-origin; Emacs is one-origin.
1762 (let ((posline (1- (line-number-at-pos nil t)))
1763 (linebeg (buffer-substring (point) pos))
1764 (colfun eglot-current-linepos-function))
1765 ;; Use a temp buffer because:
1766 ;; - I don't know of a fast way to count newlines in a string.
1767 ;; - We currently don't have `eglot-current-linepos-function' for strings.
1768 (with-temp-buffer
1769 (insert linebeg string)
1770 (goto-char (point-max))
1771 (list :line (+ posline (1- (line-number-at-pos nil t)))
1772 :character (funcall colfun))))))
1773
1752(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos 1774(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
1753 "Function to move to a position within a line reported by the LSP server. 1775 "Function to move to a position within a line reported by the LSP server.
1754 1776
@@ -1946,6 +1968,8 @@ For example, to keep your Company customization, add the symbol
1946 "A hook run by Eglot after it started/stopped managing a buffer. 1968 "A hook run by Eglot after it started/stopped managing a buffer.
1947Use `eglot-managed-p' to determine if current buffer is managed.") 1969Use `eglot-managed-p' to determine if current buffer is managed.")
1948 1970
1971(defvar-local eglot--track-changes nil)
1972
1949(define-minor-mode eglot--managed-mode 1973(define-minor-mode eglot--managed-mode
1950 "Mode for source buffers managed by some Eglot project." 1974 "Mode for source buffers managed by some Eglot project."
1951 :init-value nil :lighter nil :keymap eglot-mode-map 1975 :init-value nil :lighter nil :keymap eglot-mode-map
@@ -1959,8 +1983,13 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
1959 ("utf-8" 1983 ("utf-8"
1960 (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos) 1984 (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos)
1961 (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos))) 1985 (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos)))
1962 (add-hook 'after-change-functions #'eglot--after-change nil t) 1986 (if (fboundp 'track-changes-register)
1963 (add-hook 'before-change-functions #'eglot--before-change nil t) 1987 (unless eglot--track-changes
1988 (setq eglot--track-changes
1989 (track-changes-register
1990 #'eglot--track-changes-signal :disjoint t)))
1991 (add-hook 'after-change-functions #'eglot--after-change nil t)
1992 (add-hook 'before-change-functions #'eglot--before-change nil t))
1964 (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) 1993 (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
1965 ;; Prepend "didClose" to the hook after the "nonoff", so it will run first 1994 ;; Prepend "didClose" to the hook after the "nonoff", so it will run first
1966 (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t) 1995 (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t)
@@ -1998,6 +2027,9 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
1998 buffer 2027 buffer
1999 (eglot--managed-buffers (eglot-current-server))))) 2028 (eglot--managed-buffers (eglot-current-server)))))
2000 (t 2029 (t
2030 (when eglot--track-changes
2031 (track-changes-unregister eglot--track-changes)
2032 (setq eglot--track-changes nil))
2001 (remove-hook 'after-change-functions #'eglot--after-change t) 2033 (remove-hook 'after-change-functions #'eglot--after-change t)
2002 (remove-hook 'before-change-functions #'eglot--before-change t) 2034 (remove-hook 'before-change-functions #'eglot--before-change t)
2003 (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t) 2035 (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
@@ -2588,7 +2620,6 @@ buffer."
2588(defun eglot--after-change (beg end pre-change-length) 2620(defun eglot--after-change (beg end pre-change-length)
2589 "Hook onto `after-change-functions'. 2621 "Hook onto `after-change-functions'.
2590Records BEG, END and PRE-CHANGE-LENGTH locally." 2622Records BEG, END and PRE-CHANGE-LENGTH locally."
2591 (cl-incf eglot--versioned-identifier)
2592 (pcase (car-safe eglot--recent-changes) 2623 (pcase (car-safe eglot--recent-changes)
2593 (`(,lsp-beg ,lsp-end 2624 (`(,lsp-beg ,lsp-end
2594 (,b-beg . ,b-beg-marker) 2625 (,b-beg . ,b-beg-marker)
@@ -2616,6 +2647,29 @@ Records BEG, END and PRE-CHANGE-LENGTH locally."
2616 `(,lsp-beg ,lsp-end ,pre-change-length 2647 `(,lsp-beg ,lsp-end ,pre-change-length
2617 ,(buffer-substring-no-properties beg end))))) 2648 ,(buffer-substring-no-properties beg end)))))
2618 (_ (setf eglot--recent-changes :emacs-messup))) 2649 (_ (setf eglot--recent-changes :emacs-messup)))
2650 (eglot--track-changes-signal nil))
2651
2652(defun eglot--track-changes-fetch (id)
2653 (if (eq eglot--recent-changes :pending) (setq eglot--recent-changes nil))
2654 (track-changes-fetch
2655 id (lambda (beg end before)
2656 (cond
2657 ((eq eglot--recent-changes :emacs-messup) nil)
2658 ((eq before 'error) (setf eglot--recent-changes :emacs-messup))
2659 (t (push `(,(eglot--pos-to-lsp-position beg)
2660 ,(eglot--virtual-pos-to-lsp-position beg before)
2661 ,(length before)
2662 ,(buffer-substring-no-properties beg end))
2663 eglot--recent-changes))))))
2664
2665(defun eglot--track-changes-signal (id &optional distance)
2666 (cl-incf eglot--versioned-identifier)
2667 (cond
2668 (distance (eglot--track-changes-fetch id))
2669 (eglot--recent-changes nil)
2670 ;; Note that there are pending changes, for the benefit of those
2671 ;; who check it as a boolean.
2672 (t (setq eglot--recent-changes :pending)))
2619 (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) 2673 (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
2620 (let ((buf (current-buffer))) 2674 (let ((buf (current-buffer)))
2621 (setq eglot--change-idle-timer 2675 (setq eglot--change-idle-timer
@@ -2729,6 +2783,8 @@ When called interactively, use the currently active server"
2729(defun eglot--signal-textDocument/didChange () 2783(defun eglot--signal-textDocument/didChange ()
2730 "Send textDocument/didChange to server." 2784 "Send textDocument/didChange to server."
2731 (when eglot--recent-changes 2785 (when eglot--recent-changes
2786 (when eglot--track-changes
2787 (eglot--track-changes-fetch eglot--track-changes))
2732 (let* ((server (eglot--current-server-or-lose)) 2788 (let* ((server (eglot--current-server-or-lose))
2733 (sync-capability (eglot-server-capable :textDocumentSync)) 2789 (sync-capability (eglot-server-capable :textDocumentSync))
2734 (sync-kind (if (numberp sync-capability) sync-capability 2790 (sync-kind (if (numberp sync-capability) sync-capability
@@ -2750,7 +2806,7 @@ When called interactively, use the currently active server"
2750 ;; empty entries in `eglot--before-change' calls 2806 ;; empty entries in `eglot--before-change' calls
2751 ;; without an `eglot--after-change' reciprocal. 2807 ;; without an `eglot--after-change' reciprocal.
2752 ;; Weed them out here. 2808 ;; Weed them out here.
2753 when (numberp len) 2809 when (numberp len) ;FIXME: Not needed with `track-changes'.
2754 vconcat `[,(list :range `(:start ,beg :end ,end) 2810 vconcat `[,(list :range `(:start ,beg :end ,end)
2755 :rangeLength len :text text)])))) 2811 :rangeLength len :text text)]))))
2756 (setq eglot--recent-changes nil) 2812 (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 @@
53;; - Handle `diff -b' output in context->unified. 53;; - Handle `diff -b' output in context->unified.
54 54
55;;; Code: 55;;; Code:
56(require 'easy-mmode)
57(require 'track-changes)
56(eval-when-compile (require 'cl-lib)) 58(eval-when-compile (require 'cl-lib))
57(eval-when-compile (require 'subr-x)) 59(eval-when-compile (require 'subr-x))
58(require 'easy-mmode)
59 60
60(autoload 'vc-find-revision "vc") 61(autoload 'vc-find-revision "vc")
61(autoload 'vc-find-revision-no-save "vc") 62(autoload 'vc-find-revision-no-save "vc")
@@ -1431,38 +1432,23 @@ else cover the whole buffer."
1431 (if (buffer-modified-p) (diff-fixup-modifs (point-min) (point-max))) 1432 (if (buffer-modified-p) (diff-fixup-modifs (point-min) (point-max)))
1432 nil) 1433 nil)
1433 1434
1434;; It turns out that making changes in the buffer from within an 1435(defvar-local diff--track-changes nil)
1435;; *-change-function is asking for trouble, whereas making them 1436
1436;; from a post-command-hook doesn't pose much problems 1437(defun diff--track-changes-signal (tracker)
1437(defvar diff-unhandled-changes nil) 1438 (cl-assert (eq tracker diff--track-changes))
1438(defun diff-after-change-function (beg end _len) 1439 (track-changes-fetch tracker #'diff--track-changes-function))
1439 "Remember to fixup the hunk header. 1440
1440See `after-change-functions' for the meaning of BEG, END and LEN." 1441(defun diff--track-changes-function (beg end _before)
1441 ;; Ignoring changes when inhibit-read-only is set is strictly speaking 1442 (with-demoted-errors "%S"
1442 ;; incorrect, but it turns out that inhibit-read-only is normally not set 1443 (save-excursion
1443 ;; inside editing commands, while it tends to be set when the buffer gets 1444 (goto-char beg)
1444 ;; updated by an async process or by a conversion function, both of which 1445 ;; Maybe we've cut the end of the hunk before point.
1445 ;; would rather not be uselessly slowed down by this hook. 1446 (if (and (bolp) (not (bobp))) (backward-char 1))
1446 (when (and (not undo-in-progress) (not inhibit-read-only)) 1447 ;; We used to fixup modifs on all the changes, but it turns out that
1447 (if diff-unhandled-changes 1448 ;; it's safer not to do it on big changes, e.g. when yanking a big
1448 (setq diff-unhandled-changes 1449 ;; diff, or when the user edits the header, since we might then
1449 (cons (min beg (car diff-unhandled-changes)) 1450 ;; screw up perfectly correct values. --Stef
1450 (max end (cdr diff-unhandled-changes)))) 1451 (when (ignore-errors (diff-beginning-of-hunk t))
1451 (setq diff-unhandled-changes (cons beg end)))))
1452
1453(defun diff-post-command-hook ()
1454 "Fixup hunk headers if necessary."
1455 (when (consp diff-unhandled-changes)
1456 (ignore-errors
1457 (save-excursion
1458 (goto-char (car diff-unhandled-changes))
1459 ;; Maybe we've cut the end of the hunk before point.
1460 (if (and (bolp) (not (bobp))) (backward-char 1))
1461 ;; We used to fixup modifs on all the changes, but it turns out that
1462 ;; it's safer not to do it on big changes, e.g. when yanking a big
1463 ;; diff, or when the user edits the header, since we might then
1464 ;; screw up perfectly correct values. --Stef
1465 (diff-beginning-of-hunk t)
1466 (let* ((style (if (looking-at "\\*\\*\\*") 'context)) 1452 (let* ((style (if (looking-at "\\*\\*\\*") 'context))
1467 (start (line-beginning-position (if (eq style 'context) 3 2))) 1453 (start (line-beginning-position (if (eq style 'context) 3 2)))
1468 (mid (if (eq style 'context) 1454 (mid (if (eq style 'context)
@@ -1470,17 +1456,16 @@ See `after-change-functions' for the meaning of BEG, END and LEN."
1470 (re-search-forward diff-context-mid-hunk-header-re 1456 (re-search-forward diff-context-mid-hunk-header-re
1471 nil t))))) 1457 nil t)))))
1472 (when (and ;; Don't try to fixup changes in the hunk header. 1458 (when (and ;; Don't try to fixup changes in the hunk header.
1473 (>= (car diff-unhandled-changes) start) 1459 (>= beg start)
1474 ;; Don't try to fixup changes in the mid-hunk header either. 1460 ;; Don't try to fixup changes in the mid-hunk header either.
1475 (or (not mid) 1461 (or (not mid)
1476 (< (cdr diff-unhandled-changes) (match-beginning 0)) 1462 (< end (match-beginning 0))
1477 (> (car diff-unhandled-changes) (match-end 0))) 1463 (> beg (match-end 0)))
1478 (save-excursion 1464 (save-excursion
1479 (diff-end-of-hunk nil 'donttrustheader) 1465 (diff-end-of-hunk nil 'donttrustheader)
1480 ;; Don't try to fixup changes past the end of the hunk. 1466 ;; Don't try to fixup changes past the end of the hunk.
1481 (>= (point) (cdr diff-unhandled-changes)))) 1467 (>= (point) end)))
1482 (diff-fixup-modifs (point) (cdr diff-unhandled-changes))))) 1468 (diff-fixup-modifs (point) end)))))))
1483 (setq diff-unhandled-changes nil))))
1484 1469
1485(defun diff-next-error (arg reset) 1470(defun diff-next-error (arg reset)
1486 ;; Select a window that displays the current buffer so that point 1471 ;; Select a window that displays the current buffer so that point
@@ -1560,9 +1545,8 @@ a diff with \\[diff-reverse-direction].
1560 ;; setup change hooks 1545 ;; setup change hooks
1561 (if (not diff-update-on-the-fly) 1546 (if (not diff-update-on-the-fly)
1562 (add-hook 'write-contents-functions #'diff-write-contents-hooks nil t) 1547 (add-hook 'write-contents-functions #'diff-write-contents-hooks nil t)
1563 (make-local-variable 'diff-unhandled-changes) 1548 (setq diff--track-changes
1564 (add-hook 'after-change-functions #'diff-after-change-function nil t) 1549 (track-changes-register #'diff--track-changes-signal :nobefore t)))
1565 (add-hook 'post-command-hook #'diff-post-command-hook nil t))
1566 1550
1567 ;; add-log support 1551 ;; add-log support
1568 (setq-local add-log-current-defun-function #'diff-current-defun) 1552 (setq-local add-log-current-defun-function #'diff-current-defun)
@@ -1581,12 +1565,15 @@ a diff with \\[diff-reverse-direction].
1581\\{diff-minor-mode-map}" 1565\\{diff-minor-mode-map}"
1582 :group 'diff-mode :lighter " Diff" 1566 :group 'diff-mode :lighter " Diff"
1583 ;; FIXME: setup font-lock 1567 ;; FIXME: setup font-lock
1584 ;; setup change hooks 1568 (when diff--track-changes (track-changes-unregister diff--track-changes))
1585 (if (not diff-update-on-the-fly) 1569 (remove-hook 'write-contents-functions #'diff-write-contents-hooks t)
1586 (add-hook 'write-contents-functions #'diff-write-contents-hooks nil t) 1570 (when diff-minor-mode
1587 (make-local-variable 'diff-unhandled-changes) 1571 (if (not diff-update-on-the-fly)
1588 (add-hook 'after-change-functions #'diff-after-change-function nil t) 1572 (add-hook 'write-contents-functions #'diff-write-contents-hooks nil t)
1589 (add-hook 'post-command-hook #'diff-post-command-hook nil t))) 1573 (unless diff--track-changes
1574 (setq diff--track-changes
1575 (track-changes-register #'diff--track-changes-signal
1576 :nobefore t))))))
1590 1577
1591;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1578;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1592 1579
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..ed35477cafd
--- /dev/null
+++ b/test/lisp/emacs-lisp/track-changes-tests.el
@@ -0,0 +1,156 @@
1;;; track-changes-tests.el --- tests for emacs-lisp/track-changes.el -*- lexical-binding:t -*-
2
3;; Copyright (C) 2024 Free Software Foundation, Inc.
4
5;; This file is part of GNU Emacs.
6
7;; GNU Emacs is free software: you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation, either version 3 of the License, or
10;; (at your option) any later version.
11
12;; GNU Emacs is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;; GNU General Public License for more details.
16
17;; You should have received a copy of the GNU General Public License
18;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
19
20;;; Commentary:
21
22;;; Code:
23
24(require 'track-changes)
25(require 'cl-lib)
26(require 'ert)
27
28(defun track-changes-tests--random-word ()
29 (let ((chars ()))
30 (dotimes (_ (1+ (random 12)))
31 (push (+ ?A (random (1+ (- ?z ?A)))) chars))
32 (apply #'string chars)))
33
34(defvar track-changes-tests--random-verbose nil)
35
36(defun track-changes-tests--message (&rest args)
37 (when track-changes-tests--random-verbose (apply #'message args)))
38
39(defvar track-changes-tests--random-seed
40 (let ((seed (number-to-string (random (expt 2 24)))))
41 (message "Random seed = %S" seed)
42 seed))
43
44(ert-deftest track-changes-tests--random ()
45 ;; Keep 2 buffers in sync with a third one as we make random
46 ;; changes to that 3rd one.
47 ;; We have 3 trackers: a "normal" one which we sync
48 ;; at random intervals, one which syncs via the "disjoint" signal,
49 ;; plus a third one which verifies that "nobefore" gets
50 ;; information consistent with the "normal" tracker.
51 (with-temp-buffer
52 (random track-changes-tests--random-seed)
53 (dotimes (_ 100)
54 (insert (track-changes-tests--random-word) "\n"))
55 (let* ((buf1 (generate-new-buffer " *tc1*"))
56 (buf2 (generate-new-buffer " *tc2*"))
57 (char-counts (make-vector 2 0))
58 (sync-counts (make-vector 2 0))
59 (print-escape-newlines t)
60 (file (make-temp-file "tc"))
61 (id1 (track-changes-register #'ignore))
62 (id3 (track-changes-register #'ignore :nobefore t))
63 (sync
64 (lambda (id buf n)
65 (track-changes-tests--message "!! SYNC %d !!" n)
66 (track-changes-fetch
67 id (lambda (beg end before)
68 (when (eq n 1)
69 (track-changes-fetch
70 id3 (lambda (beg3 end3 before3)
71 (should (eq beg3 beg))
72 (should (eq end3 end))
73 (should (eq before3
74 (if (symbolp before)
75 before (length before)))))))
76 (cl-incf (aref sync-counts (1- n)))
77 (cl-incf (aref char-counts (1- n)) (- end beg))
78 (let ((after (buffer-substring beg end)))
79 (track-changes-tests--message
80 "Sync:\n %S\n=> %S\nat %d .. %d"
81 before after beg end)
82 (with-current-buffer buf
83 (if (eq before 'error)
84 (erase-buffer)
85 (should (equal before
86 (buffer-substring
87 beg (+ beg (length before)))))
88 (delete-region beg (+ beg (length before))))
89 (goto-char beg)
90 (insert after)))
91 (should (equal (buffer-string)
92 (with-current-buffer buf
93 (buffer-string))))))))
94 (id2 (track-changes-register
95 (lambda (id2 &optional distance)
96 (when distance
97 (track-changes-tests--message "Disjoint distance: %d"
98 distance)
99 (funcall sync id2 buf2 2)))
100 :disjoint t)))
101 (write-region (point-min) (point-max) file)
102 (insert-into-buffer buf1)
103 (insert-into-buffer buf2)
104 (should (equal (buffer-hash) (buffer-hash buf1)))
105 (should (equal (buffer-hash) (buffer-hash buf2)))
106 (message "seeding with: %S" track-changes-tests--random-seed)
107 (dotimes (_ 1000)
108 (pcase (random 15)
109 (0
110 (track-changes-tests--message "Manual sync1")
111 (funcall sync id1 buf1 1))
112 (1
113 (track-changes-tests--message "Manual sync2")
114 (funcall sync id2 buf2 2))
115 ((pred (< _ 5))
116 (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
117 (end (min (+ beg (1+ (random 100))) (point-max))))
118 (track-changes-tests--message "Fill %d .. %d" beg end)
119 (fill-region-as-paragraph beg end)))
120 ((pred (< _ 8))
121 (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
122 (end (min (+ beg (1+ (random 12))) (point-max))))
123 (track-changes-tests--message "Delete %S at %d .. %d"
124 (buffer-substring beg end) beg end)
125 (delete-region beg end)))
126 ((and 8 (guard (= (random 50) 0)))
127 (track-changes-tests--message "Silent insertion")
128 (let ((inhibit-modification-hooks t))
129 (insert "a")))
130 ((and 8 (guard (= (random 10) 0)))
131 (track-changes-tests--message "Revert")
132 (insert-file-contents file nil nil nil 'replace))
133 ((and 8 (guard (= (random 3) 0)))
134 (let* ((beg (+ (point-min) (random (1+ (buffer-size)))))
135 (end (min (+ beg (1+ (random 12))) (point-max)))
136 (after (eq (random 2) 0)))
137 (track-changes-tests--message "Bogus %S %d .. %d"
138 (if after 'after 'before) beg end)
139 (if after
140 (run-hook-with-args 'after-change-functions
141 beg end (- end beg))
142 (run-hook-with-args 'before-change-functions beg end))))
143 (_
144 (goto-char (+ (point-min) (random (1+ (buffer-size)))))
145 (let ((word (track-changes-tests--random-word)))
146 (track-changes-tests--message "insert %S at %d" word (point))
147 (insert word "\n")))))
148 (message "SCOREs: default: %d/%d=%d disjoint: %d/%d=%d"
149 (aref char-counts 0) (aref sync-counts 0)
150 (/ (aref char-counts 0) (aref sync-counts 0))
151 (aref char-counts 1) (aref sync-counts 1)
152 (/ (aref char-counts 1) (aref sync-counts 1))))))
153
154
155
156;;; track-changes-tests.el ends here