Allow retained logical objects to own multiple marker-backed output fragments without leaking handles or positions into pure plans. Expose only defensive numeric mount snapshots through the public object index. Verified: 718 ERT tests, 92 doctests, WERROR compile-all, checkdoc, and git diff --check.
22 KiB
Changelog
All notable changes to the tp library are documented here.
Unreleased
Added
- Retained content surfaces now support explicitly retained logical objects and
tp-object-attach-fragment, so one stable object can own several disjoint marker-backed output fragments without putting runtime handles or positions into the pure plan.tp-object-mountsexposes defensive numeric range/tag snapshots through an object-keyed side index. tp-transaction-participatelets a client promote rollback-capable opaque side state after all affected surfaces publish but before source values commit. Participant keys are unique per outer transaction, failure rolls participants back in reverse publication order, and observers still run only after the transaction exits.tp-propertize,tp-apply, andtp-watchnow provide simple one-shot string, one-shot buffer-range, and reactive existing-text entry points over the same schema/cascade/projector and retained properties-surface core.- TP 1.0 retained surfaces now provide defensive pure plans, prepare-scoped object identity, keyed/positional reconciliation,
contentandpropertiescapabilities, marker-backed range anchors, same-surface overlapping property contributions, compare-before-write conflicts and explicit rebase, common-prefix/suffix text edits, property-run diffs, side indexes, opaque client state, generic reports, lifecycle cleanup, and atomic multi-buffer publication with exact rollback. Pure materialization uses the same plan semantics without leaving live handles or subscriptions. - TP 1.0 signals and bindings now form an exact source→binding and binding→binding dependency graph with conditional rewiring, memoized equality cutoffs, transaction-local candidate signal values, deduplicated topological flushing, nested-write stabilization, rollback, cycle paths, owner disposal, buffer-scoped sources, variable adapters, and public scheduler counters. The legacy layer scanner remains isolated only until the retained-surface cutover.
- The first TP 1.0 runtime slice:
tp-style.elprovides atomic namespaced property schemas, structured subject selectors and combinators, deterministic origin/importance/layer/specificity/scope/source-order cascade, property-specific inheritance, tagged CSS-wide values, custom-property fallback/cycle handling, explicittp-computedvalue sources, named declarations, provenance, and final Emacs-property projection. Ordinary function values remain literal. - Internal Stage 2 canonical façade records and dataflow:
tp--native-range,tp--presence,tp--request,tp--match, andtp--result. Public entry points and historical return shapes remain compatible. - Stage 3 text-only native façade in
tp-query.el:tp-lookup-result,tp-lookup,tp-property-change,tp-property-any,tp-property-not-all, andtp-with-mutation-policy. - Stage 4 managed lifecycle APIs:
tp-attach-managed-layers,tp-detach-managed-layers,tp-managed-layer-diagnostics,tp-managed-buffer-diagnostics,tp-managed-diagnostics, andtp-layer-transaction. - Stage 5 overlay-aware lookup modes for
tp-lookup::charand:char-sourcereport Emacs character-property values and the winning overlay identity when an overlay wins. docs/API-SEMANTICS.mdcentralizes the current object/coordinate, mutation, presence/nil, search-result, layer-ownership,tp-text, error, and hidden-stack conflict contracts.tp-any-value, a unique public sentinel for property-search wildcard matching when later positional arguments also need to be supplied.tp-unresolved-layerandtp-layer-conflicterror types.tp-reactive-observer-errors, a newest-first structured record of isolated watcher callback failures.
Fixed
- Initial
tp-textrendering now preserves every embedded property interval for strings and buffers instead of spreading position-zero properties across the replacement. tp-textapplication preserves the caller'stp-set,tp-reset, ortp-addwrite semantics even when replacement text is unchanged.- Explicit nil properties override embedded
tp-textvalues. - Non-parameterized layer redefinition refreshes managed regions with old/new ownership reconciliation: removed keys disappear, new keys replace them, and unrelated or externally changed values survive.
- Search APIs distinguish omitted VALUE (any directly present value) from explicit nil (a present nil value), with one presence-aware run scanner shared by strings and buffers.
- Removing the last nested sub-property removes the empty parent key consistently for strings and buffers.
- Stack NOERROR catches only unresolved layer specifications; errors from layer bodies and internal operations propagate.
- When hidden-layer full-stack storage detects an external direct
property edit, stack decoding now signals
tp-layer-conflictbefore any managed write instead of silently discarding the external value. - Buffer transaction rollback tracks its live range with markers, so insertions and deletions inside the range are removed or restored together with the original text-property snapshot.
Changed
- Transform and compute failures now propagate as business-output errors. A transform returning a non-string also signals. Watcher failures remain isolated observers, but are recorded structurally while the managed update continues.
- Managed layer entries now store
tp-metain authoritativetp-layersstorage, including parameterized args and definition versions. Direct rendered properties and public stack queries striptp-meta; historical returns remain unchanged. - Parameterized mounted layer entries refresh from stored args after redefinition.
- Insert/copy/yank/stickiness/narrowing/indirect-buffer behavior is now documented as direct Emacs delegation with no tp wrapper.
- Overlay creation, movement, deletion, priority management, and lifecycle remain native Emacs responsibilities; tp only reports overlay-aware lookup results.
tp-with-mutation-policyaccepts only ordinary+respect, ordinary+inhibit, and silent+inhibit; silent+respect is rejected.- Theme enable/disable events now increment theme generation and expose
conservative refresh diagnostics. Reproducible benchmark evidence is
recorded in
docs/BENCHMARKS.md; timings are advisory baseline data, not release thresholds.
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).