diff options
| -rw-r--r-- | doc/lispref/text.texi | 148 | ||||
| -rw-r--r-- | etc/NEWS | 18 | ||||
| -rw-r--r-- | lisp/emacs-lisp/track-changes.el | 619 | ||||
| -rw-r--r-- | lisp/progmodes/eglot.el | 64 | ||||
| -rw-r--r-- | lisp/vc/diff-mode.el | 85 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/track-changes-tests.el | 156 |
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), | |||
| 6375 | use @code{combine-change-calls} or @code{combine-after-change-calls} | 6375 | use @code{combine-change-calls} or @code{combine-after-change-calls} |
| 6376 | instead. | 6376 | instead. |
| 6377 | @end defvar | 6377 | @end defvar |
| 6378 | |||
| 6379 | @node Tracking changes | ||
| 6380 | @subsection Tracking changes | ||
| 6381 | @cindex track-changes | ||
| 6382 | @cindex change tracker | ||
| 6383 | |||
| 6384 | Using @code{before-change-functions} and @code{after-change-functions} | ||
| 6385 | can be difficult in practice because of a number of pitfalls, such as | ||
| 6386 | the fact that the two calls are not always properly paired, or some | ||
| 6387 | calls may be missing, either because some Emacs primitives failed to | ||
| 6388 | properly pair them or because of incorrect use of | ||
| 6389 | @code{inhibit-modification-hooks}. Furthermore, | ||
| 6390 | many restrictions apply to those hook functions, such as the fact that | ||
| 6391 | they basically should never modify the current buffer, nor use an | ||
| 6392 | operation that may block, and they proceed quickly because | ||
| 6393 | some commands may call these hooks a large number of times. | ||
| 6394 | |||
| 6395 | The Track-Changes library fundamentally provides an alternative API, | ||
| 6396 | built on top of those hooks. Compared to @code{after-change-functions}, | ||
| 6397 | the first important difference is that, instead of providing the bounds | ||
| 6398 | of the change and the previous length, it provides the bounds of the | ||
| 6399 | change and the actual previous content of that region. The need to | ||
| 6400 | extract information from the original contents of the buffer is one of | ||
| 6401 | the main reasons why some packages need to use both | ||
| 6402 | @code{before-change-functions} and @code{after-change-functions} and | ||
| 6403 | then try to match them up. | ||
| 6404 | |||
| 6405 | The second difference is that it decouples the notification of a change | ||
| 6406 | from the act of processing it, and it automatically combines into | ||
| 6407 | a single change operation all the changes that occur between the first | ||
| 6408 | change and the actual processing. This makes it natural and easy to | ||
| 6409 | process the changes at a larger granularity, such as once per command, | ||
| 6410 | and eliminates most of the restrictions that apply to the usual change | ||
| 6411 | hook functions, making it possible to use blocking operations or to | ||
| 6412 | modify the buffer. | ||
| 6413 | |||
| 6414 | To start tracking changes, you have to call | ||
| 6415 | @code{track-changes-register}, passing it a @var{signal} function as | ||
| 6416 | argument. This returns a tracker @var{id} which is used to identify | ||
| 6417 | your change tracker to the other functions of the library. | ||
| 6418 | When the buffer is modified, the library calls the @var{signal} | ||
| 6419 | function to inform you of that change and immediately starts | ||
| 6420 | accumulating subsequent changes into a single combined change. | ||
| 6421 | The @var{signal} function serves only to warn that a modification | ||
| 6422 | occurred but does not receive a description of the change. Also the | ||
| 6423 | library will not call it again until after you retrieved the change. | ||
| 6424 | |||
| 6425 | To retrieve changes, you need to call @code{track-changes-fetch}, which | ||
| 6426 | provides you with the bounds of the changes accumulated since the | ||
| 6427 | last 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 | ||
| 6429 | again after the next buffer modification. | ||
| 6430 | |||
| 6431 | @defun track-changes-register signal &key nobefore disjoint immediate | ||
| 6432 | This function creates a new @dfn{change tracker}. Change trackers are kept | ||
| 6433 | abstract, so we refer to them as mere identities, and the function thus | ||
| 6434 | returns the tracker's @var{id}. | ||
| 6435 | |||
| 6436 | @var{signal} is a function that the library will call to notify of | ||
| 6437 | a change. It will sometimes call it with a single argument and | ||
| 6438 | sometimes with two. Upon the first change to the buffer since this | ||
| 6439 | tracker last called @code{track-changes-fetch}, the library calls this | ||
| 6440 | @var{signal} function with a single argument holding the @var{id} of | ||
| 6441 | the tracker. | ||
| 6442 | |||
| 6443 | By default, the call to the @var{signal} function does not happen | ||
| 6444 | immediately, but is instead postponed with a 0 seconds timer | ||
| 6445 | (@pxref{Timers}). This is usually desired to make sure the @var{signal} | ||
| 6446 | function is not called too frequently and runs in a permissive context, | ||
| 6447 | freeing the client from performance concerns or worries about which | ||
| 6448 | operations might be problematic. If a client wants to have more | ||
| 6449 | control, they can provide a non-@code{nil} value as the @var{immediate} | ||
| 6450 | argument in which case the library calls the @var{signal} function | ||
| 6451 | directly from @code{after-change-functions}. Beware that it means that | ||
| 6452 | the @var{signal} function has to be careful not to modify the buffer or | ||
| 6453 | use operations that may block. | ||
| 6454 | |||
| 6455 | If you're not interested in the actual previous content of the buffer, | ||
| 6456 | but are using this library only for its ability to combine many small | ||
| 6457 | changes into a larger one and to delay the processing to a more | ||
| 6458 | convenient time, you can specify a non-@code{nil} value for the | ||
| 6459 | @var{nobefore} argument. In that case, @code{track-change-fetch} | ||
| 6460 | provides you only with the length of the previous content, just like | ||
| 6461 | @code{after-change-functions}. It also allows the library to save | ||
| 6462 | some work. | ||
| 6463 | |||
| 6464 | While you may like to accumulate many small changes into larger ones, | ||
| 6465 | you may not want to do that if the changes are too far apart. If you | ||
| 6466 | specify a non-@code{nil} value for the @var{disjoint} argument, the library | ||
| 6467 | will let you know when a change is about to occur ``far'' from the | ||
| 6468 | currently pending ones by calling the @var{signal} function right away, | ||
| 6469 | passing it two arguments this time: the @var{id} of the tracker, and the | ||
| 6470 | number of characters that separates the upcoming change from the | ||
| 6471 | already pending changes. This in itself does not prevent combining this | ||
| 6472 | new change with the previous ones, so if you think the upcoming change | ||
| 6473 | is indeed too far, you need to call @code{track-change-fetch} | ||
| 6474 | right away. | ||
| 6475 | Beware that when the @var{signal} function is called because of | ||
| 6476 | a disjoint change, this happens directly from | ||
| 6477 | @code{before-change-functions}, so the usual restrictions apply about | ||
| 6478 | modifying the buffer or using operations that may block. | ||
| 6479 | @end defun | ||
| 6480 | |||
| 6481 | @defun track-changes-fetch id func | ||
| 6482 | This is the function that lets you find out what has changed in the | ||
| 6483 | buffer. By providing the tracker @var{id} you let the library figure | ||
| 6484 | out which changes have already been seen by your tracker. Instead of | ||
| 6485 | returning a description of the changes, @code{track-changes-fetch} calls | ||
| 6486 | the @var{func} function with that description in the form of | ||
| 6487 | 3 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. | ||
| 6490 | Usually @var{before} is a string containing the previous text of the | ||
| 6491 | modified region, but if you specified a non-@code{nil} @var{nobefore} argument | ||
| 6492 | to @code{track-changes-register}, then it is replaced by the number of | ||
| 6493 | characters of that previous text. | ||
| 6494 | |||
| 6495 | In 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 | ||
| 6498 | returned by @var{func}. But note that @var{func} is called just once | ||
| 6499 | regardless of how many changes occurred: those are summarized into | ||
| 6500 | a single @var{beg}/@var{end}/@var{before} triplet. | ||
| 6501 | |||
| 6502 | In some cases, the library is not properly notified of all changes, | ||
| 6503 | for example because of a bug in the low-level C code or because of an | ||
| 6504 | improper use of @code{inhibit-modification-hooks}. When it detects such | ||
| 6505 | a problem, @var{func} receives a @code{@var{beg}..@var{end}} region | ||
| 6506 | which covers the whole buffer and the @var{before} argument is the | ||
| 6507 | symbol @code{error} to indicate that the library was unable to determine | ||
| 6508 | what was changed. | ||
| 6509 | |||
| 6510 | Once @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 | ||
| 6512 | occurs. This is the reason why it calls @var{func} instead of returning | ||
| 6513 | a description: it lets you process the change without worrying about the | ||
| 6514 | risk that the @var{signal} function gets triggered in the middle of it, | ||
| 6515 | because the @var{signal} is re-enabled only after @var{func} finishes. | ||
| 6516 | @end defun | ||
| 6517 | |||
| 6518 | @defun track-changes-unregister id | ||
| 6519 | This function tells the library that the tracker @var{id} does not need | ||
| 6520 | to know about buffer changes any more. Most clients will never want to | ||
| 6521 | stop tracking changes, but for clients such as minor modes, it is | ||
| 6522 | important to call this function when the minor mode is disabled, | ||
| 6523 | otherwise the tracker will keep accumulating changes and consume more | ||
| 6524 | and more resources. | ||
| 6525 | @end defun | ||
| @@ -15,6 +15,12 @@ in older Emacs versions. | |||
| 15 | You can narrow news to a specific version by calling 'view-emacs-news' | 15 | You can narrow news to a specific version by calling 'view-emacs-news' |
| 16 | with a prefix argument or by typing 'C-u C-h C-n'. | 16 | with a prefix argument or by typing 'C-u C-h C-n'. |
| 17 | 17 | ||
| 18 | Temporary note: | ||
| 19 | +++ indicates that all relevant manuals in doc/ have been updated. | ||
| 20 | --- means no change in the manuals is needed. | ||
| 21 | When you add a new item, use the appropriate mark if you are sure it | ||
| 22 | applies, 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. | ||
| 1597 | This library is a layer of abstraction above 'before-change-functions' | ||
| 1598 | and 'after-change-functions' which provides a superset of | ||
| 1599 | the 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. | ||
| 102 | A buffer state is described by a BEG/END/BEFORE triplet which say how to | ||
| 103 | recover that state from the next state. I.e. if the buffer's contents | ||
| 104 | reflects the next state, you can recover the previous state by replacing | ||
| 105 | the BEG..END region with the BEFORE string. | ||
| 106 | |||
| 107 | NEXT is the next state object (i.e. a more recent state). | ||
| 108 | If 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 | ||
| 110 | values from `track-changes--before-(beg|end|before)' when the next | ||
| 111 | state 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. | ||
| 121 | Those 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. | ||
| 125 | These trackers are signaled every time track-changes notices | ||
| 126 | that 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. | ||
| 138 | This string is supposed to cover all the already modified areas plus | ||
| 139 | the upcoming modifications announced via `before-change-functions'. | ||
| 140 | If all trackers are `nobefore', then this holds the `buffer-size' before | ||
| 141 | the current change.") | ||
| 142 | (defvar-local track-changes--before-no t | ||
| 143 | "If non-nil, all the trackers are `nobefore'. | ||
| 144 | Should 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. | ||
| 148 | More 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. | ||
| 158 | This 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. | ||
| 164 | Return the ID of the new tracker. | ||
| 165 | |||
| 166 | SIGNAL is a function that will be called with one argument (the tracker ID) | ||
| 167 | after the current buffer is modified, so that it can react to the change. | ||
| 168 | Once called, SIGNAL is not called again until `track-changes-fetch' | ||
| 169 | is called with the corresponding tracker ID. | ||
| 170 | |||
| 171 | If optional argument NOBEFORE is non-nil, it means that this tracker does | ||
| 172 | not need the BEFORE strings (it will receive their size instead). | ||
| 173 | |||
| 174 | If optional argument DISJOINT is non-nil, SIGNAL is called every time just | ||
| 175 | before combining changes from \"distant\" parts of the buffer. | ||
| 176 | This is needed when combining disjoint changes into one bigger change | ||
| 177 | is unacceptable, typically for performance reasons. | ||
| 178 | These calls are distinguished from normal calls by calling SIGNAL with | ||
| 179 | a second argument which is the distance between the upcoming change and | ||
| 180 | the previous changes. | ||
| 181 | BEWARE: In that case SIGNAL is called directly from `before-change-functions' | ||
| 182 | and should thus be extra careful: don't modify the buffer, don't call a function | ||
| 183 | that may block, ... | ||
| 184 | In order to prevent the upcoming change from being combined with the previous | ||
| 185 | changes, SIGNAL needs to call `track-changes-fetch' before it returns. | ||
| 186 | |||
| 187 | By default SIGNAL is called after a change via a 0 seconds timer. | ||
| 188 | If optional argument IMMEDIATE is non-nil it means SIGNAL should be called | ||
| 189 | as soon as a change is detected, | ||
| 190 | BEWARE: In that case SIGNAL is called directly from `after-change-functions' | ||
| 191 | and should thus be extra careful: don't modify the buffer, don't call a function | ||
| 192 | that may block, do as little work as possible, ... | ||
| 193 | When 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. | ||
| 215 | Trackers can consume resources (especially if `track-changes-fetch' is | ||
| 216 | not called), so it is good practice to unregister them when you don't | ||
| 217 | need 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. | ||
| 239 | ID is the tracker ID returned by a previous `track-changes-register'. | ||
| 240 | FUNC is a function. It is called with 3 arguments (BEGIN END BEFORE) | ||
| 241 | where BEGIN..END delimit the region that was changed since the last | ||
| 242 | time `track-changes-fetch' was called and BEFORE is a string containing | ||
| 243 | the previous content of that region (or just its length as an integer | ||
| 244 | if the tracker ID was registered with the `nobefore' option). | ||
| 245 | If track-changes detected that some changes were missed, then BEFORE will | ||
| 246 | be the symbol `error' to indicate that the buffer got out of sync. | ||
| 247 | This reflects a bug somewhere, so please report it when it happens. | ||
| 248 | |||
| 249 | If no changes occurred since the last time, it doesn't call FUNC and | ||
| 250 | returns nil, otherwise it returns the value returned by FUNC | ||
| 251 | and 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. | ||
| 411 | Each 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'!! | ||
| 419 | Details 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. | ||
| 584 | FUNC is called with no arguments and with point right after BEFORE. | ||
| 585 | FUNC is not allowed to modify the buffer and it should refrain from using | ||
| 586 | operations that use a cache populated from the buffer's content, | ||
| 587 | such 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. | ||
| 603 | Re-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. |
| 1733 | LBP defaults to `eglot--bol'." | 1734 | LBP 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. |
| 1947 | Use `eglot-managed-p' to determine if current buffer is managed.") | 1969 | Use `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'. |
| 2590 | Records BEG, END and PRE-CHANGE-LENGTH locally." | 2622 | Records 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 | |
| 1440 | See `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 | ||