20 KiB
Changelog
All notable changes to the tp library are documented here.
2.0.0 (Unreleased)
Added
- A standalone retained surface runtime with pure defensive plans, prepare-scoped stable objects, keyed/positional reconciliation, content and properties capabilities, marker-backed range anchors, object/mount indexes, scoped updates, opaque client state, generic reports, and lifecycle cleanup.
- Exact signal-to-binding and binding-to-binding dependency tracking with conditional rewiring, equality cutoffs, batched transactions, nested-write stabilization, owner disposal, buffer-scoped sources, variable adapters, cycle paths, and public structural counters.
- Atomic single- and multi-surface publication with candidate source values, prepare-all/publish-all ordering, explicit property journals, rollback-capable transaction participants, observer isolation, and authoritative kill-buffer cleanup.
- Transaction protocol v2: transaction-scoped publication batches, structured
participants, bounded opaque final-accept markers using the closed
tp-vector-slots/v1primitive, immutable tagged outcomes, and one live writer. tp-runtime-manifest, advertisingtp-transaction-protocol-v2and package version 2.0.0.- Public
tp-transaction-participate-v2registration for cross-package structured participants. - Native property policies and contribution composition with explicit nil/absence, normalization, validation, equality, merge, projection, named direct styles, and explicit
tp-computedvalue sources. tp-propertize,tp-apply, andtp-watchas the one-shot string, one-shot buffer-range, and reactive existing-text conveniences over the same direct property/surface core.- Retained logical objects with
tp-object-retainandtp-object-attach-fragment, allowing one object to own multiple disjoint physical fragments without placing handles or positions in plans. - Standalone examples for static properties, reactive status decoration, retained dashboards, and editable diagnostics.
Changed
- Structured publication batches own the single live surface-entry loop, participant stage/precommit/commit vector, and candidate-bound final accept.
- Package metadata now identifies the v2-only breaking transaction contract.
define-tpanddefine-tpsnow define static or parameterized direct declaration recipes. Applying a recipe produces ordinary properties and never publishes runtime identity metadata.- TP is no longer a CSS engine. Selector, stylesheet, specificity, origin/importance, cascade layer, CSS-wide value, custom property, winner, and provenance behavior belongs to the independent ECSS package.
- Function-valued properties are always literal. Only values wrapped by
tp-computedexecute and participate in dependency collection. - Runtime identity and provenance live exclusively in side state. Normal updates follow source to binding to object to marker-backed mount without scanning buffers or displayed text.
- Package documentation, API semantics, architecture, doctests, and tests now describe the single TP 1.0 runtime rather than the transitional 0.3 managed model.
Removed
-
The public
tp-transaction-participatev1 facade. Replace(tp-transaction-participate KEY PUBLISH ROLLBACK)with(tp-transaction-participate-v2 :key KEY :stage PUBLISH :rollback ROLLBACK). -
The v1 publication writer, execution-route kill switch, artifact-mode switch, and their runtime manifest claims.
-
tp-render.el,tp-stack.el, the scan-driven renderer, layer-to-buffer registry, and duplicate managed transaction path. -
Managed stack mutation, attach/detach, diagnostics, and lifecycle APIs that depended on inline stack storage.
-
tp-text,$variabledeclaration syntax, automatic layer refresh, and character-leveltp-name/tp-layers/tp-metaruntime storage. -
TP-owned CSS selector/stylesheet/cascade APIs and compatibility aliases.
-
The unused Dash runtime dependency.
-
Unused global theme lifecycle advice and managed-refresh bookkeeping from the deleted renderer.
0.3.0 (2026-07-27)
Added
Layer stack:
- Layer visibility:
tp-hide-layer/tp-show-layer— a hidden layer stays in the stack (and keeps receiving reactive updates) but does not render; hiding the visible top reveals the next visible layer, and with every layer hidden the text renders bare.tp-flatten-layersmerges only visible layers;tp-merge-layersexcludes hidden matched layers' props. tp-lower-layer(mirror oftp-raise-layer) and a family-consistenttp-rotate-layercalling order(START END DIRECTION [COUNT] [OBJECT]), selected unambiguously by the symbolsup/down; the legacy order keeps working.tp-layer-stack-at— the full ordered stack at one position as(NAME . PROPS)conses, hidden layers marked by atp-hiddenentry.- Stack mutators return the number of property runs they modified
(including
tp-merge-layers/tp-flatten-layers), and layer-name lookups gained optional NOERROR arguments where they previously signaled. tp-describe-layer— interactive help-buffer description of a layer: storage format, arglist, stored body, expanded props, reactive deps, transform, owning group.
Reactive engine:
- Layer→buffer registry: reactive updates now visit only the
buffers registered as showing the affected layer instead of scanning
the whole
(buffer-list); every buffer-mutating write path registers (tp-set family, stack mutators, match/regexp appliers), killed buffers are pruned, and an unknown layer falls back to one learning full scan.tp-reactive-layer-buffersexposes the registry;tp-reactive-track-buffercloses the insert-a-propertized-string gap. - Minimal-diff
tp-textre-render: only the differing span is edited (insert-before-delete), so point and markers in unchanged text stay put and identical-text updates no longer touch the buffer at all (buffer-modified flag preserved). tp-gc-anonymous-layers— collects interned anonymous layers that no registered live buffer still shows (stack-aware: buried and hidden layers count as alive; string-only layers are conservatively kept).
Search and matching:
tp-regexp-set/reset/addaccept SUBEXP: properties apply to that capture group per match (non-participating groups contribute nothing); SUBEXP beyond the pattern's group count signals a clear error.tp-match-*/tp-regexp-*accept START/END bounds with as-if-only-that-portion semantics; reversed bounds are swapped.tp-forward/tp-backward/tp-forward-do/tp-backward-doaccept PREDICATE and NOT-CURRENT, passed through to the text-property-search machinery; defaults keep the 0.2.0 symmetric equal-matching contract exactly.
Layer definitions:
- Multi-argument parameterized layers:
define-tp/define-tpsarglists may declare any number of parameters;(LAYER ARG1 ... ARGN)and wrapped(LAYER (ARG1 ... ARGN))specs work intp-setandtp-put-layer; newtp-layer-props-with-args/tp-group-props-with-args/tp-layer-arglist. Wrong-arity calls signal clear errors naming the layer and both counts. - Prefix-conforming aliases
tp-define-layer/tp-define-group/tp-define-palettefor discoverability (C-h f tp-…).
Core and palette:
tp-intervals/tp-intervals-mapaccept an optional ABSOLUTE argument returning native buffer coordinates (feedable straight back intotp-set); the range-relative default is unchanged.tp-palette-color(generic theme-resolved accessor) andtp-palette-has-pconsolidate the palette query surface; all existing query functions remain.
Fixed
All six were found by an adversarial architecture/API review of the new 0.3.0 code and confirmed with minimal reproductions before fixing:
- The reactive buffer registry only registered
tp-set-family writes; layers applied viatp-push-layer,tp-match-set, etc. never re-rendered on variable updates. - Reactive updates wrote only the rendered top layer; hidden or
buried layers kept stale props (visible again on
tp-show-layer). tp-gc-anonymous-layersandtp-reactive-track-bufferscanned only directtp-nameproperties, so a layer buried in a stack (or hidden) could be wrongly collected / missed.tp-flatten-layers/tp-merge-layersrendered hidden layers' properties despitetp-hide-layer's documented contract.- Minimal-diff
tp-textedits deleted before inserting, so markers at the suffix boundary drifted to the wrong character. - An error escaping a reactive update could strand queued batch
entries (now drained under
unwind-protect;tp-reactive-resetclears the queue).
Changed
- Module boundaries tightened (behavior identical under
(require 'tp)): thetp-texthandler chain moved from tp-render into tp-ops — partial loads now get workingtp-textreplacement — andtp-with-batch-updatesmoved up into tp-render; two of the four upward hook variables are gone (tp--tp-text-handler-function,tp--reactive-flush-function). The layer-stack storage codec and the anonymous-layer machinery now live in tp-layer; tp-stack's phantom dependency on tp-ops is gone; 67 lines of dead code deleted. tp-core holds no mutable state. - String forms of all 16 stack mutators document that they modify the
string in place (unlike
tp-set's copy semantics); unifying this is on the 0.4 ledger.
Deprecated
tp-search-forward/tp-search-backward(0.3.0) — thin wrappers whose nil-PREDICATE default contradicts the rest of the library's equal-matching; usetp-forward/tp-backward, or the Emacs primitives for raw access.tp-suffix-symbol(0.3.0) — internal helper now private astp--suffix-symbol; a compatibility alias remains.
Infrastructure
- GitHub Actions CI: Emacs 28.1 / 29.4 / 30.1 matrix running
byte-compilation with warnings-as-errors, the full ERT suite, a
shuffled-order rerun of every test (
make test-shuffled,tp-run-shuffled.el;SHUFFLE_SEED=Nreproduces an order), and the README doctests. - The whole tree byte-compiles with zero warnings (57 fixed:
docstring rewraps and quoting,
defvardeclarations for reactive test variables, prefixed doctest counters, one impossibleeqcomparison corrected toequal). - Autoload cookies for the interactive commands (
tp-debug-show,tp-debug-clear,tp-reactive-reset,tp-layer-reset,tp-palette-show,tp-clear) and thedefine-tp/define-tpsmacros. - Two doctest assertions made property-order-insensitive (Emacs 28 prints text-property plists in a different order than 29+).
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).