Up: Change Hooks   [Contents][Index]

33.34.1 Keeping track of buffer modifications

Using before-change-functions and after-change-functions can be difficult in practice because of a number of pitfalls, such as the fact that the two calls are not always properly paired, or some calls may be missing, either because some Emacs primitives failed to properly pair them or because of incorrect use of inhibit-modification-hooks. Furthermore, many restrictions apply to those hook functions, such as the fact that they basically should never modify the current buffer, nor use an operation that may block, and they proceed quickly because some commands may call these hooks a large number of times.

The Track-Changes library fundamentally provides an alternative API, built on top of those hooks. Compared to after-change-functions, the first important difference is that, instead of providing the bounds of the change and the previous length, it provides the bounds of the change and the actual previous content of that region. The need to extract information from the original contents of the buffer is one of the main reasons why some packages need to use both before-change-functions and after-change-functions and then try to match them up.

The second difference is that it decouples the notification of a change from the act of processing it, and it automatically combines into a single change operation all the changes that occur between the first change and the actual processing. This makes it natural and easy to process the changes at a larger granularity, such as once per command, and eliminates most of the restrictions that apply to the usual change hook functions, making it possible to use blocking operations or to modify the buffer.

To start tracking changes, you have to call track-changes-register, passing it a signal function as argument. This returns a tracker id which is used to identify your change tracker to the other functions of the library. When the buffer is modified, the library calls the signal function to inform you of that change and immediately starts accumulating subsequent changes into a single combined change. The signal function serves only to warn that a modification occurred but does not receive a description of the change. Also the library will not call it again until after you retrieved the change.

To retrieve changes, you need to call track-changes-fetch, which provides you with the bounds of the changes accumulated since the last call, as well as the previous content of that region. It also “re-arms” the signal function so that the library will call it again after the next buffer modification.

Function: track-changes-register signal &key nobefore disjoint immediate

This function creates a new change tracker. Change trackers are kept abstract, so we refer to them as mere identities, and the function thus returns the tracker’s id.

signal is a function that the library will call to notify of a change. It will sometimes call it with a single argument and sometimes with two. Upon the first change to the buffer since this tracker last called track-changes-fetch, the library calls this signal function with a single argument holding the id of the tracker.

By default, the call to the signal function does not happen immediately, but is instead postponed with a 0 seconds timer (see Timers for Delayed Execution). This is usually desired to make sure the signal function is not called too frequently and runs in a permissive context, freeing the client from performance concerns or worries about which operations might be problematic. If a client wants to have more control, they can provide a non-nil value as the immediate argument in which case the library calls the signal function directly from after-change-functions. Beware that it means that the signal function has to be careful not to modify the buffer or use operations that may block.

If you’re not interested in the actual previous content of the buffer, but are using this library only for its ability to combine many small changes into a larger one and to delay the processing to a more convenient time, you can specify a non-nil value for the nobefore argument. In that case, track-change-fetch provides you only with the length of the previous content, just like after-change-functions. It also allows the library to save some work.

While you may like to accumulate many small changes into larger ones, you may not want to do that if the changes are too far apart. If you specify a non-nil value for the disjoint argument, the library will let you know when a change is about to occur “far” from the currently pending ones by calling the signal function right away, passing it two arguments this time: the id of the tracker, and the number of characters that separates the upcoming change from the already pending changes. This in itself does not prevent combining this new change with the previous ones, so if you think the upcoming change is indeed too far, you need to call track-change-fetch right away. Beware that when the signal function is called because of a disjoint change, this happens directly from before-change-functions, so the usual restrictions apply about modifying the buffer or using operations that may block.

Function: track-changes-fetch id func

This is the function that lets you find out what has changed in the buffer. By providing the tracker id you let the library figure out which changes have already been seen by your tracker. Instead of returning a description of the changes, track-changes-fetch calls the func function with that description in the form of 3 arguments: beg, end, and before, where beg..end delimit the region that was modified and before describes the previous content of that region. Usually before is a string containing the previous text of the modified region, but if you specified a non-nil nobefore argument to track-changes-register, then it is replaced by the number of characters of that previous text.

In case no changes occurred since the last call, track-changes-fetch simply does not call func and returns nil. If changes did occur, it calls func and returns the value returned by func. But note that func is called just once regardless of how many changes occurred: those are summarized into a single beg/end/before triplet.

In some cases, the library is not properly notified of all changes, for example because of a bug in the low-level C code or because of an improper use of inhibit-modification-hooks. When it detects such a problem, func receives a beg..end region which covers the whole buffer and the before argument is the symbol error to indicate that the library was unable to determine what was changed.

Once func finishes, track-changes-fetch re-enables the signal function so that it will be called the next time a change occurs. This is the reason why it calls func instead of returning a description: it lets you process the change without worrying about the risk that the signal function gets triggered in the middle of it, because the signal is re-enabled only after func finishes.

Function: track-changes-unregister id

This function tells the library that the tracker id does not need to know about buffer changes any more. Most clients will never want to stop tracking changes, but for clients such as minor modes, it is important to call this function when the minor mode is disabled, otherwise the tracker will keep accumulating changes and consume more and more resources.

Up: Change Hooks   [Contents][Index]