63 assertions reproduce README code blocks and compare against the exact documented outputs; make doctest runs them in batch and exits non-zero on any mismatch, so docs and code cannot silently drift again. Wired into the Makefile and mentioned in both READMEs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.5 KiB
Changelog
All notable changes to the tp library are documented here.
0.2.0 (2026-07-26)
Architecture
-
tp.el was split into layered modules.
(require 'tp)still loads everything; nothing changes for users. Each module depends only on the ones before it, and the byte compiler enforces the order:Module Responsibility tp-core.elIntervals, plist/face merge engine, debug logging, $varutilitiestp-reactive.elReactive dependency registry, variable watchers, batching queue tp-layer.eldefine-tp/define-tps, layer registry and resolutiontp-ops.eltp-set/tp-reset/tp-add/tp-get/tp-at/tp-remove/tp-cleartp-search.eltp-match-*,tp-regexp-*,tp-search, navigationtp-render.elReactive re-rendering engine (installs itself into lower modules) tp-stack.elLayer stack operations (push/pop/move/merge/flatten/...) tp-palette.elLight/dark color palette data tp-builtins.elBuilt-in layers, palette gallery, display-buffer helpers -
The library now byte-compiles cleanly (previously
define-tpmacro-expansion failed at compile time). -
New shared engine
tp--map-intervals: a clipping interval walker that underlies region operations; property edits can no longer bleed outside the requested region. -
New public constant
tp-face-properties('(face font-lock-face mouse-face)): the property family that gets face-aware merging.
Fixed
Core operations:
(require 'text-property-search)was missing;tp-backwardsignaledvoid-functionin batch/fresh sessions.tp-removestring form silently dropped the 3rd and later properties.- String-form removal helpers sampled properties at position 0 and smeared them across the range, destroying neighboring intervals; they now work per-interval.
tp-clearcomputed default bounds from the current buffer even when clearing a string (silent no-op or range error).(tp-get STRING START END ...)returned nil silently; it now behaves like the buffer region form.tp-intervalsreturned unclipped intervals (including negative offsets); it now clips to[START, END).- Region-form calls with flat prop/val arguments —
(tp-set 1 4 'face 'bold)— silently discarded the value and failed later; they now signal a clear error immediately. - Face-family prepend semantics in
tp-addcovered onlyface; they now coverfont-lock-faceandmouse-facetoo. tp--parse-face-listno longer invents a(:key nil)pair for a trailing bare keyword.
Built-ins and palette:
- Emacs 28.1 compatibility restored (
plistpis Emacs 29+; a compat shim is used, andsubr-xis required where needed). tp-pop-to-buffer/tp-switch-to-bufferno longer bindqin the shared major-mode keymap (a buffer-local minor-mode keymap is used) and no longer capture abuffervariable from the caller.tp-linkresolved its palette color once at load time; the color is now resolved at application time, so theme switches are honored.define-tp-paletteno longer generates per-palette defvars;tp-palette-alistis the single source of truth and palette redefinition takes effect immediately.tp-headlineemitted invalid(:height nil)for integer heights.tp-spacenow produces the documented pixel(space :width (N))spec.tp-parse-coloraccepts one-sided cons colors like("white" . nil).
Layer definitions (tp-layer):
- Parameterized
define-tpsgroups: the documented format returned nil props viatp-group-props-with-arg; all documented element shapes now resolve correctly. - Cyclic layer references signal a clear error naming the cycle
(previously crashed with
excessive-lisp-nesting); diamond-shaped reuse is not a false positive. define-tperrors at macro-expansion time when extra body forms are present (previously silently discarded all but the first).$-symbols in parameterized layer bodies resolve to their variables' current values at evaluation time (previously leaked literally into the output props); parameterized layers remain non-reactive, and the choice is documented.tp-layer-props/tp-group-propsand their-with-argvariants return copies; mutating a returned plist can no longer corrupt the registry.:transformindefine-tpsgroup elements is honored (was silently dropped).- Group redefinition and
tp-undefine-groupclean up the layers the group generated, including their reactive deps and transforms (previously orphaned). - The group-element parser errors on unknown keywords instead of advancing by one and re-reading a value as a key.
- Anonymous reactive layers are interned: an
equal$varprops spec reuses the existing registry entry instead of minting a new one on everytp-set(unbounded leak fixed).
Layer stacks (tp-stack):
- All stack mutators were rewritten onto a shared clipped region walker;
region ops no longer alter text outside
[START, END), andtp-put-layeris region-local instead of switching behavior on whole-object emptiness. - The documented inline-plist spec (
(face bold ...)) and list-of-layer-names spec ('(layer-a layer-b)) fortp-put-layerwork (previously errored), handled at the call site. tp-region-layer-propsno longer double-offsets string positions.tp-merge-layers/tp-flatten-layersno longer drop explicitly-nil values (presence is checked withplist-member).- Single-layer stacks no longer carry a garbage
(tp-layers nil)property, and its absence is tolerated everywhere. tp-layer-toprespects the requested region instead of reading only the first interval.
Search and navigation (tp-search):
tp-backwardbuffer paths passed no predicate totext-property-search-backward, so matching was inverted relative totp-forward; backward now mirrors forward's equal-matching semantics. The legacy test that codified the inverted behavior (tp-test-backward) was updated to the symmetric contract.- Empty and zero-width patterns no longer loop forever in the match/regexp apply engines.
- Length-changing replacements work in buffers in
tp-forward-do/tp-backward-do/tp-search-map(previously signaledargs-out-of-rangeviastore-substring). On strings — which cannot change length in place — a length-changing replacement signals a clear error instead of silently truncating or leaving residue; same-length string replacements are unchanged. tp-search-mapwith a non-current buffer OBJECT operates on that buffer (previously read and mutated the current buffer) and no longer corrupts buffers on length-changing replacements.tp-match-addbuffer path uses face-family-aware merging like the string path, so existing faces are preserved.tp-search-mapcan remove properties on strings (nil-props ranges were previously skipped).- The triplicated ~38-line replacement lambda was extracted into one shared helper.
tp-forward-do/tp-backward-doshortfall is now all-or-nothing on both paths: TIMES targets the TIMES-th match specifically, so when fewer matches exist nothing is applied and the available count is returned. String paths previously acted on the last available match — the wrong target; the two legacy tests codifying that (tp-test-forward-do-on-string-with-rangeand its backward twin) were updated.
Reactive rendering (tp-reactive / tp-render):
- Sub-region
tp-texton a string no longer discards the rest of the string. - Computed-variable updates deep-merge resolved props with the layer definition, preserving sibling static attributes.
- Reactive refresh replaces the re-rendered layer's own property keys
instead of accumulating (bold→italic no longer yields
(italic bold)), while preserving other layers' properties. setq-localre-renders the buffer without leaking buffer-local values into the global layer definition.- Reactive
tp-textreplacement preserves unrelated existing properties. - Computed values of nil propagate (nil was conflated with the error sentinel).
- Variable-watcher reentrancy: nested
setcalls inside the update path queue their re-render through the batch queue instead of recursing. - Batched updates union their WHERE and tp-text flags at flush time instead of freezing the first change's.
- Reactive strings keep per-interval props on re-render (previously only position-0 props survived and were smeared).
:transformapplies on the first render too, not only on updates.
Test infrastructure:
- The test fixture now tears down with
unwind-protectand resets all registries includingtp-layer-transforms(previously leaked across tests); the suite passes in randomized order. tp-tests.elheader andproviderenamed to match its file name.
Added
tp-member: liketp-at, but distinguishes "property present with value nil" from "property absent" (plist-member-style result).Makefilewithtest/doctest/compile/cleantargets.tp-doctest.el: executable documentation tests — 63 assertions reproducing README examples and comparing against their exact documented outputs (make doctest).- Per-module regression test suites:
tp-core-tests.el,tp-ops-tests.el,tp-builtins-tests.el,tp-layer-tests.el,tp-stack-tests.el,tp-search-tests.el,tp-render-tests.el— the combined suite grew from 280 to 439 tests.
Changed
- License clarified to GPLv3+ in file headers, matching the shipped LICENSE file (headers previously said v2+).
0.1.0
Initial release (monolithic tp.el).