在实际使用中,before-change-functions 与 after-change-functions
存在诸多陷阱,例如两类钩子调用未必成对、部分调用可能丢失(原因包括 Emacs 原语未正确配对调用
或不当使用 inhibit-modification-hooks)。
此外,这类钩子函数有诸多限制:基本不应修改当前缓冲区、不能执行阻塞操作,
且必须快速执行,因为部分命令可能高频调用这些钩子。
变更跟踪库在这些钩子之上提供了一套替代 API,从根本上解决了上述问题。
与 after-change-functions 相比,第一个重要区别是:
它不提供变更边界与旧长度,而是提供变更边界与该区域实际的旧内容。
很多包需要同时使用 before-change-functions 与 after-change-functions
并尝试配对调用,主要原因就是需要从缓冲区原始内容中提取信息。
第二个区别是,它将变更通知与变更处理解耦, 并自动将第一次变更到实际处理之间的所有修改合并为单次变更操作。 这使得以更大粒度(例如每命令一次)处理变更变得自然简单, 同时解除了普通变更钩子的大部分限制,允许执行阻塞操作或修改缓冲区。
要开始跟踪变更,需调用 track-changes-register,
并传入一个 signal 函数作为参数。该函数返回一个跟踪器 id,
用于在库的其他函数中标识你的变更跟踪器。
当缓冲区被修改时,库会调用 signal 函数通知发生变更,
并立即将后续变更累积为单次合并变更。
signal 函数仅用于提示发生了修改,不接收变更详情。
且在你获取本次变更前,库不会再次调用该函数。
要获取变更,需调用 track-changes-fetch,
它会返回自上次调用以来累积的所有变更的边界,以及该区域的旧内容。
同时它会重新 “武装(re-arms)” signal 函数,使库在下次缓冲区修改时能再次调用它。
该函数创建一个新的 变更跟踪器(change tracker)。变更跟踪器为抽象对象,仅以标识区分, 因此函数返回跟踪器的 id。
signal 是库用于通知变更的函数,调用时可能传入一个或两个参数。
当跟踪器上次调用 track-changes-fetch 后缓冲区发生首次变更时,
库会以跟踪器 id 为单一参数调用该函数。
默认情况下,signal 函数不会立即调用,
而是通过 0 秒定时器延迟执行(see 用于延迟执行的定时器)。
这通常是为了避免 signal 被过于频繁地调用,
并使其运行在宽松的上下文环境中,让使用者无需担心性能问题或潜在的异常操作。
若使用者需要更精细的控制,可将 immediate 参数设为非 nil,
此时库会直接从 after-change-functions 中调用 signal。
注意这意味着 signal 函数必须避免修改缓冲区或执行阻塞操作。
若你不关心缓冲区实际的旧内容,仅使用该库将大量小变更合并为大变更,
并将处理延迟到更合适的时机,可将 nobefore 参数设为非 nil。
此时 track-change-fetch 仅返回旧内容长度,与 after-change-functions 行为一致,
同时库也能减少部分计算开销。
虽然你希望累积小变更为大变更,但如果变更位置相距过远,可能不希望合并。
若将 disjoint 参数设为非 nil,
当新变更与当前待处理变更位置“相距较远”时,库会立即调用 signal 函数并传入两个参数:
跟踪器 id 与新变更和待处理变更之间的字符间隔数。
这一行为本身不会阻止新变更与旧变更合并,因此若你认为距离过远,
应立即调用 track-change-fetch。
注意:因不连续变更触发的 signal 调用直接来自 before-change-functions,
因此同样遵循不可修改缓冲区、不可执行阻塞操作的限制。
该函数用于获取缓冲区中的变更内容。
传入跟踪器 id 后,库会判断哪些变更已被你的跟踪器处理过。
track-changes-fetch 不会直接返回变更描述,
而是调用 func 并以三个参数传递变更信息:beg、end 与 before。
其中 beg..end 为修改区域,before 为该区域的旧内容。
通常 before 是包含旧文本的字符串;
但若你在 track-changes-register 中指定了非 nil 的 nobefore,
则该参数会替换为旧内容的字符长度。
若自上次调用后未发生任何变更,
track-changes-fetch 不会调用 func 并直接返回 nil。
若发生变更,则调用 func 并返回其执行结果。
注意无论发生多少次变更,func 只会被调用一次:
所有变更会被汇总为单个 beg/end/before 组合。
在某些情况下,库无法正确获取所有变更通知,
例如底层 C 代码存在 bug 或不当使用 inhibit-modification-hooks。
当检测到此类问题时,func 接收的 beg..end
会覆盖整个缓冲区,且 before 为符号 error,
表示库无法确定具体变更内容。
func 执行完毕后,track-changes-fetch 会重新启用 signal 函数,
以便下次变更时再次触发。这也是它调用 func 而非直接返回结果的原因:
确保你处理变更时不会被中途触发的 signal 打断,
因为信号会在 func 结束后才重新启用。
该函数告知库,跟踪器 id 不再需要监听缓冲区变更。 大多数使用者不会主动停止跟踪,但对于次要模式等场景, 在禁用次要模式时必须调用该函数,否则跟踪器会持续累积变更并不断消耗资源。