tp/CHANGELOG.md
Kinneyzhang e3d7d1dfc6 feat(tp): support disjoint logical object mounts
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.
2026-08-06 03:50:59 +08:00

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-mounts exposes defensive numeric range/tag snapshots through an object-keyed side index.
  • tp-transaction-participate lets 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, and tp-watch now 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, content and properties capabilities, 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.el provides 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, explicit tp-computed value 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, and tp--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, and tp-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, and tp-layer-transaction.
  • Stage 5 overlay-aware lookup modes for tp-lookup: :char and :char-source report Emacs character-property values and the winning overlay identity when an overlay wins.
  • docs/API-SEMANTICS.md centralizes 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-layer and tp-layer-conflict error types.
  • tp-reactive-observer-errors, a newest-first structured record of isolated watcher callback failures.

Fixed

  • Initial tp-text rendering now preserves every embedded property interval for strings and buffers instead of spreading position-zero properties across the replacement.
  • tp-text application preserves the caller's tp-set, tp-reset, or tp-add write semantics even when replacement text is unchanged.
  • Explicit nil properties override embedded tp-text values.
  • 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-conflict before 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-meta in authoritative tp-layers storage, including parameterized args and definition versions. Direct rendered properties and public stack queries strip tp-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-policy accepts 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-layers merges only visible layers; tp-merge-layers excludes hidden matched layers' props.
  • tp-lower-layer (mirror of tp-raise-layer) and a family-consistent tp-rotate-layer calling order (START END DIRECTION [COUNT] [OBJECT]), selected unambiguously by the symbols up / 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 a tp-hidden entry.
  • 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-buffers exposes the registry; tp-reactive-track-buffer closes the insert-a-propertized-string gap.
  • Minimal-diff tp-text re-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/add accept 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-do accept 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-tps arglists may declare any number of parameters; (LAYER ARG1 ... ARGN) and wrapped (LAYER (ARG1 ... ARGN)) specs work in tp-set and tp-put-layer; new tp-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-palette for discoverability (C-h f tp-…).

Core and palette:

  • tp-intervals / tp-intervals-map accept an optional ABSOLUTE argument returning native buffer coordinates (feedable straight back into tp-set); the range-relative default is unchanged.
  • tp-palette-color (generic theme-resolved accessor) and tp-palette-has-p consolidate 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 via tp-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-layers and tp-reactive-track-buffer scanned only direct tp-name properties, so a layer buried in a stack (or hidden) could be wrongly collected / missed.
  • tp-flatten-layers / tp-merge-layers rendered hidden layers' properties despite tp-hide-layer's documented contract.
  • Minimal-diff tp-text edits 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-reset clears the queue).

Changed

  • Module boundaries tightened (behavior identical under (require 'tp)): the tp-text handler chain moved from tp-render into tp-ops — partial loads now get working tp-text replacement — and tp-with-batch-updates moved 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; use tp-forward / tp-backward, or the Emacs primitives for raw access.
  • tp-suffix-symbol (0.3.0) — internal helper now private as tp--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=N reproduces an order), and the README doctests.
  • The whole tree byte-compiles with zero warnings (57 fixed: docstring rewraps and quoting, defvar declarations for reactive test variables, prefixed doctest counters, one impossible eq comparison corrected to equal).
  • Autoload cookies for the interactive commands (tp-debug-show, tp-debug-clear, tp-reactive-reset, tp-layer-reset, tp-palette-show, tp-clear) and the define-tp / define-tps macros.
  • 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.el Intervals, plist/face merge engine, debug logging, $var utilities
    tp-reactive.el Reactive dependency registry, variable watchers, batching queue
    tp-layer.el define-tp / define-tps, layer registry and resolution
    tp-ops.el tp-set / tp-reset / tp-add / tp-get / tp-at / tp-remove / tp-clear
    tp-search.el tp-match-*, tp-regexp-*, tp-search, navigation
    tp-render.el Reactive re-rendering engine (installs itself into lower modules)
    tp-stack.el Layer stack operations (push/pop/move/merge/flatten/...)
    tp-palette.el Light/dark color palette data
    tp-builtins.el Built-in layers, palette gallery, display-buffer helpers
  • The library now byte-compiles cleanly (previously define-tp macro-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-backward signaled void-function in batch/fresh sessions.
  • tp-remove string 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-clear computed 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-intervals returned 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-add covered only face; they now cover font-lock-face and mouse-face too.
  • tp--parse-face-list no longer invents a (:key nil) pair for a trailing bare keyword.

Built-ins and palette:

  • Emacs 28.1 compatibility restored (plistp is Emacs 29+; a compat shim is used, and subr-x is required where needed).
  • tp-pop-to-buffer / tp-switch-to-buffer no longer bind q in the shared major-mode keymap (a buffer-local minor-mode keymap is used) and no longer capture a buffer variable from the caller.
  • tp-link resolved its palette color once at load time; the color is now resolved at application time, so theme switches are honored.
  • define-tp-palette no longer generates per-palette defvars; tp-palette-alist is the single source of truth and palette redefinition takes effect immediately.
  • tp-headline emitted invalid (:height nil) for integer heights.
  • tp-space now produces the documented pixel (space :width (N)) spec.
  • tp-parse-color accepts one-sided cons colors like ("white" . nil).

Layer definitions (tp-layer):

  • Parameterized define-tps groups: the documented format returned nil props via tp-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-tp errors 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-props and their -with-arg variants return copies; mutating a returned plist can no longer corrupt the registry.
  • :transform in define-tps group elements is honored (was silently dropped).
  • Group redefinition and tp-undefine-group clean 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 $var props spec reuses the existing registry entry instead of minting a new one on every tp-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), and tp-put-layer is 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)) for tp-put-layer work (previously errored), handled at the call site.
  • tp-region-layer-props no longer double-offsets string positions.
  • tp-merge-layers / tp-flatten-layers no longer drop explicitly-nil values (presence is checked with plist-member).
  • Single-layer stacks no longer carry a garbage (tp-layers nil) property, and its absence is tolerated everywhere.
  • tp-layer-top respects the requested region instead of reading only the first interval.

Search and navigation (tp-search):

  • tp-backward buffer paths passed no predicate to text-property-search-backward, so matching was inverted relative to tp-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 signaled args-out-of-range via store-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-map with 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-add buffer path uses face-family-aware merging like the string path, so existing faces are preserved.
  • tp-search-map can 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-do shortfall 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-range and its backward twin) were updated.

Reactive rendering (tp-reactive / tp-render):

  • Sub-region tp-text on 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-local re-renders the buffer without leaking buffer-local values into the global layer definition.
  • Reactive tp-text replacement preserves unrelated existing properties.
  • Computed values of nil propagate (nil was conflated with the error sentinel).
  • Variable-watcher reentrancy: nested set calls 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).
  • :transform applies on the first render too, not only on updates.

Test infrastructure:

  • The test fixture now tears down with unwind-protect and resets all registries including tp-layer-transforms (previously leaked across tests); the suite passes in randomized order.
  • tp-tests.el header and provide renamed to match its file name.

Added

  • tp-member: like tp-at, but distinguishes "property present with value nil" from "property absent" (plist-member-style result).
  • Makefile with test / doctest / compile / clean targets.
  • 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).