Define TP as the independent retained text-property runtime and reserve complete stylesheet winner semantics for ECSS.
Verified: make test EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs LOAD_EXTRA="-L ../twidget2/dependencies" (729 passed); git diff --check
Give independent consumers their own rules, cascade layer ordering, and source-order counters so packages such as Ebox cannot pollute TP's default stylesheet or each other. Match generic class and state tokens by value and document caller-owned stylesheet lifecycle.
Verified: make clean; make test (728/728); make compile WERROR=t; checkdoc tp-style.el; git diff --check; Ebox make test against ../tp.
Allow callers to authorize one atomic surface update through generic retained object handles while TP remains the sole owner of mount resolution, buffer publication, and rollback.
Verified: byte compilation with warnings as errors; 726 ERT tests; 92 doctests; targeted scoped-update tests; checkdoc; git diff --check.
Replace the two Dash list helpers in tp-core with cl-lib primitives so tp-surface can load through its minimal public module chain. The legacy facade continues to declare Dash for layer and stack APIs.
Verified: standalone tp-surface load without Dash, 718 ERT tests, WERROR compile-all, and git diff --check.
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.
Publish client-owned side state after retained surfaces and roll it back in reverse order on any transaction failure.\n\nVerified: 715 ERT tests, 92 doctests, WERROR compile-all, checkdoc, git diff --check.
Expose one-shot string and buffer styling plus reactive range watches over the same schema, mutation, binding, and retained surface core.
Verification: 712 ERT tests; 92 doctests; shuffled seed 20260806; compile-all WERROR=t.
Introduce the pure TP 1.0 property schema, selector, cascade, custom-property, computed-value, provenance, and Emacs projection kernel. Compile static legacy layer definitions into canonical named styles without freezing parameterized or reactive definitions.\n\nVerified with 667 ERT tests, 92 doctests, shuffled seed 20260806, and compile-all with warnings as errors.
Add self-contained Chinese and English TP 1.0 contracts for objects, bindings, anchors, computed values, transactions, and public API levels without requiring a sibling Ebox checkout.
Verification: make doctest EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs LOAD_EXTRA=-L\\ /Users/geekinney/IPARA/3-RESOURCES/emacs/config/github/test/libs; git diff --check; local Markdown link check.
Make the TP current-architecture document point to the approved TP/Ebox target and refactor plan while clearly retaining TP 0.3.x as the current implementation.\n\nVerified: relative documentation links resolve\nVerified: git diff --cached --check
README.md/README_CN.md gain verified sections for every 0.3.0 feature:
layer visibility (hide/show model, flatten/merge exclusion), lower and
canonical rotate orders, stack introspection (tp-layer-stack-at,
tp-describe-layer), the reactive layer-buffer registry and lifecycle
(track-buffer, anonymous-layer GC, minimal-diff tp-text), search
bounds/SUBEXP/PREDICATE, multi-argument parameterized layers, ABSOLUTE
interval coordinates, palette primaries, prefix-conforming define
aliases, and deprecation boxes for the raw search wrappers. Return
conventions corrected against probed behavior (add-to-layers mutates
in place; tp-clear now explicitly returns nil in code and docs), the
in-place string-mutation warning added for all 16 stack mutators, and
the stale pre-B43 with-range example fixed in both languages.
EN/CN parity is exact (132 headings, 158 fenced blocks each); all 316
blocks executed with 0 failures. tp-doctest.el grows 63 -> 88
assertions, all accessor-based for Emacs-28 print-order safety.
docs/ARCHITECTURE.md rewritten to the post-review truth: two hook
variables, verified per-module dependency table, relocated subsystem
homes, and a new mutable-state inventory.
Suite 578/578, doctests 88/88, shuffled green, compile clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DOC-STR-01: tp-set's docstring claimed "four calling conventions"
and listed three; tp--parse-args actually implements five. The
block now enumerates all of them - (START END PROPS), (START END
PROPS OBJECT), (STRING PROP VAL ...), (STRING LAYER-NAME [ARG]) and
(STRING LAYER-NAME ARG PROP VAL ...) - and the same enumeration is
repeated in tp-reset and tp-add, which previously inherited the
conventions only implicitly via "Like tp-set".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API-CONC-01: tp-palette-color (SYMBOL KEY) is now the public generic
accessor - a documented wrapper over the existing private getter -
and tp-palette-has-p (SYMBOL &optional KIND) the consolidated
predicate: nil KIND tests registration, :fg/:bg/:border test that
the palette's definition contains the key. All ten existing query
functions stay as compatible conveniences, now each carrying a
docstring that says which question it answers (the suffix predicates
test NAME-fg style variant names, not palette contents) and
cross-referencing the primaries.
DOC-STR-02: tp-palette-show (interactive, autoloaded) finally has a
docstring describing the gallery it renders. tp-suffix-symbol - a
generic two-line intern helper with no tp semantics - is renamed to
tp--suffix-symbol; the old name survives as a compatibility
define-obsolete-function-alias (0.3.0) and tp-palette-show now
calls the private name, leaving the alias without in-tree callers.
tp-theme-dark-p / tp-theme-light-p get their missing docstrings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API-NAME-02: tp-define-layer, tp-define-group and tp-define-palette
are new permanent aliases of define-tp, define-tps and
define-tp-palette, each with a real docstring and the defun indent
spec, so C-h f tp-TAB finds the library's definition macros and
package-prefix review passes. The historical names stay forever;
zero callers change.
API-SYM-01: tp-group-props-with-args is now public, delegating to
the private implementation, mirroring tp-layer-props-with-args -
multi-argument parameterized groups are a public feature (reachable
via tp-put-layer specs) and previously had no public introspection
path. All four -with-arg/-with-args docstrings now cross-reference
their singular/plural sibling (calling out the one-character name
difference) and their layer/group counterpart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API-ARG-01: tp-rotate-layer's region form now also accepts the
canonical (START END DIRECTION &optional COUNT OBJECT) order, with
OBJECT last like every sibling. Dispatch is unambiguous: the
symbols up/down can never be a valid OBJECT, so a third argument of
up/down selects the canonical order and anything else (nil, buffer,
string) keeps the legacy (START END OBJECT DIRECTION COUNT) order
bit-identically. (tp-rotate-layer 1 5 'up) now works without a nil
placeholder. The docstring lists the canonical order first and the
legacy order as a permanent compatibility form.
API-MUT-01: every string-form stack mutator (all 16: put/push/pop/
delete/move/raise/lower/rotate/pin/switch/hide/show/merge/flatten/
add-to-layers/add-to-all-layers) now carries an explicit warning
block: the string form modifies STRING destructively in place,
unlike tp-set's copy semantics - never pass literals or shared
strings. Semantics unification is deferred to 0.4; no behavior
changes.
API-NAME-03: tp-pin-layer's summary now says what it does - a
one-shot move to the top of the stack; nothing stays pinned, and
later pushes cover it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API-COORD-01: both functions take a new trailing optional ABSOLUTE
argument. When non-nil, buffer results use native 1-based buffer
positions - directly reusable in tp-set/tp-remove calls - instead of
the legacy START-relative 0-based offsets, which stay the default
this cycle (the default flip is deferred to 0.4). String results
were always absolute and are unchanged.
DOC-STR-02: tp-intervals-map's one-line docstring is rewritten to
its real contract: the tp-layers layer-stack split handed to
FUNCTION (TOP-PROPS vs BELOW-PROPS-LST), the coordinate system
including ABSOLUTE, and the return value (FUNCTION's non-nil
results in interval order).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API-NAME-01: tp-search-forward / tp-search-backward are marked
obsolete since 0.3.0 with tp-forward / tp-backward as the recorded
replacements, and their docstrings now document all four arguments,
the primitive's nil-PREDICATE not-`equal' default, and the raw-use
advice (call the Emacs primitives directly - the wrappers add
nothing). The wrappers keep their exact primitive-delegating bodies
so every existing call stays bit-identical; a defalias onto
tp-forward/tp-backward would have flipped both the argument order
(OBJECT/N vs PREDICATE/NOT-CURRENT) and the default matching
semantics, and would have recursed through tp-forward's own calls.
Internal callers (tp-forward, tp--forward-do) now call the
primitives, leaving the deprecated names with zero in-tree callers.
The tp-forward-do / tp-backward-do summaries now read "search TIMES
times; apply FUNCTION at the Nth match" and explicitly name
tp-search-map as the for-each, so the -do suffix stops reading as
one. tp-forward's docstring states the string-path contract
precisely (list of the FIRST N matches from position 0, point never
involved) next to the buffer path's point motion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SRC-1: tp--pattern-apply-single now swaps START > END bounds before
branching on the object type, so the string path stops signaling a
raw substring args-out-of-range while the buffer path keeps its
historical narrow-to-region swap; the behavior is now uniform and
documented in all six tp-match-*/tp-regexp-* docstrings.
SRC-2: a SUBEXP larger than the pattern's capture-group count (per
regexp-opt-depth) signals "Regexp X has no group N" instead of
silently matching nothing, while legal non-participating and
zero-width groups keep working quietly.
Also corrects the return wording of tp-match-set/tp-regexp-set (a
NEW string for string objects, not "the modified string") and adds
the missing return sections to tp-match-add/tp-regexp-add, matching
the verified tp-match-reset behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp--anonymous-layer-counter and tp--generate-anonymous-layer-name
lived in tp-core, two modules below their only caller
tp--anonymous-layer-name-for - and the counter was tp-core's only
piece of mutable non-defcustom state. Anonymous-layer naming is layer
machinery, so move both into tp-layer.el right beside the registry and
the interning function. tp-core is now pure utilities plus defcustoms.
Also document a deliberate asymmetry that was easy to mistake for an
oversight: tp-layer-reset clears the anonymous-layer registry but the
counter INTENTIONALLY survives it. Detached strings can outlive a
reset while still carrying tp-anon-N property values, so the counter
must keep increasing monotonically - a post-reset anonymous layer must
never be minted under a name a stale string still holds. The comment
protects the future registry refactor from "fixing" this wrongly.
Internal symbols only; no API or behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp--expand-layer-to-props-list and tp--expand-props-to-remove were an
orphaned early draft of "expand a layer name into the property keys it
contributes": nothing calls them - the only reference in the whole
tree (sources, tests, doctest, README) is the pair calling each other.
The maintained equivalent of this logic is the inline expansion in
tp-ops's tp--remove-property, which is what tp-remove actually runs;
if deduplication is ever wanted, that copy is the one to extract.
Keeping a second, uncalled implementation of layer expansion around
invites the two to drift apart silently, so remove the 67 dead lines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp-stack.el required tp-ops but used nothing from it: a whole-word
sweep of every symbol tp-ops defines finds exactly one hit in
tp-stack, and it is a docstring cross-reference to tp-set, not code.
The require only made the module graph look denser than it is and
suggested a stack->ops edge that never existed.
The stack module's real dependencies are tp-core (interval walking),
tp-reactive (the buffer registry it feeds via
tp-reactive--register-layer-buffer) and tp-layer (the stack-storage
codec). All three requires are already present, and tp-stack.el
byte-compiles independently with warnings-as-errors after the
deletion. Load order through tp.el is unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp--flush-batch-updates existed to re-render buffers, which is
tp-render's whole job - but it lived down in tp-reactive and reached
the renderer through the tp--reactive-flush-function inversion,
complete with a guard that silently DROPPED queued re-renders when no
hook was installed. A partial load could thus discard updates without
a trace.
Move tp--flush-batch-updates and the public tp-with-batch-updates
macro (same name, same behavior - only the home file changes) into
tp-render.el, next to tp--reactive-flush-entry, which the flush now
calls directly. Delete the tp--reactive-flush-function defvar, its
silent-drop guard, and the install line. The queue state
(tp--batch-update-pending, tp--batch-update-active,
tp--reactive-updating) and tp--queue-batch-update stay in tp-reactive;
the relocated macro let-binds them downward, which is legal.
Together with the tp-text handler move this takes the sanctioned
upward hooks from four to two - only the genuine lower-layer event
sources remain (tp--reactive-update-function in tp-reactive,
tp--layer-refresh-function in tp-layer) - and a degraded partial load
now fails honestly with a void-function error instead of silently
discarding queued re-renders. The ARCH-4 unwind-protect around the
flush tail in tp--reactive-apply-update is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reactive tp-text property is consumed by tp-set/tp-reset/tp-add,
which live in tp-ops - yet the code implementing it (the transform,
embedded-props merge, minimal put, replace and handler entry) sat two
layers up in tp-render, wired back down through the
tp--tp-text-handler-function inversion. That made the handler an
upward hook for no reason: none of the seven functions touches
anything above tp-layer, so they belong at the layer of their callers.
Move tp--find-tp-text-reactive-var, tp--tp-text-transform,
tp--merge-embedded-props, tp--put-text-property-unless-equal,
tp--apply-reactive-text-props, tp--tp-text-replace and
tp--handle-tp-text-property into tp-ops.el unchanged; delete the
tp--tp-text-handler-function defvar, the tp--handle-tp-text
dispatcher and the install line, and let tp-set/tp-reset/tp-add call
tp--handle-tp-text-property directly.
tp-render keeps (require 'tp-ops): its updaters
(tp--update-reactive-text, tp--replace-reactive-text-in-buffer) still
call tp--tp-text-transform and tp--apply-reactive-text-props, so the
require flips from phantom (hook-only) to a genuine downward
dependency. tp--replace-reactive-text-in-buffer itself stays in
tp-render - it is re-render machinery, not a property primitive.
Behavior under (require 'tp) is bit-identical. A partial load of
tp-core..tp-ops now handles tp-text for real instead of silently
treating it as an inert property:
(tp-set "hello" 'tp-text "world") => "world" with tp-render absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp-reactive-reset cleared the four reactive registries and the buffer
registry but not the global tp--batch-update-pending queue. Entries
queued there by a nested variable write during a non-batched update
were flushed by a tail that sat OUTSIDE any unwind-protect, so an
error escaping the re-render (for example from a modification hook)
stranded them; a later tp-layer-reset left the ghost entries naming
layers that no longer existed, and the next completed update's flush
replayed them against the fresh registry.
Fix:
- tp-reactive-reset now also clears tp--batch-update-pending (and so
does tp-layer-reset, which delegates to it).
- tp--reactive-apply-update wraps the non-nested update body in
unwind-protect with the existing flush tail as the cleanup form, so
nested queue entries flush even when the re-render signals; the
reentrancy guard is unbound by then, so the cleanup flush
re-renders exactly like the success path.
Tests port the review's probes: a queued ghost entry disappears on
tp-reactive-reset, and a before-change-functions hook that writes a
second reactive variable and then signals leaves the queue drained
with the nested variable's re-render applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The minimal-diff reactive text edit deleted the differing span first
and inserted the replacement after. Deleting collapsed an
insertion-type-nil marker sitting on the FIRST character of the
preserved suffix onto the edit start, so it ended up stranded before
the inserted text instead of on its (unchanged) character -
contradicting the documented "markers sitting in unchanged text keep
their positions". The drift happened even for same-length
replacements ("0" -> "9").
tp--edit-region-minimal-diff now inserts the replacement at
edit-start first and then deletes the shifted old span
((delete-region (point) (+ (point) (- edit-end edit-start)))): the
insertion shifts the suffix-boundary marker right with its character
and the deletion pulls it back into place. Markers whose characters
were deleted now end at the END of the edit rather than the start
(documented side effect, still inside the replacement span).
Point clamping semantics are preserved: the documented "point inside
the edited span ends up at the start of the edit" was previously an
artifact of save-excursion's own marker collapsing under
delete-then-insert, which the new edit order breaks.
tp--replace-reactive-text-in-buffer therefore owns point restoration
now - it remembers point in a marker, clamps it to the edit start
when an edit swallowed it, and restores it at the end - and
tp--update-reactive-text's save-excursion wrapper is removed (it
would have overridden the clamp with its drifted marker). All four
shipped point/marker behavior tests pass unchanged.
New regressions port verify-txt1: suffix-start marker tracking for
grow, same-length and shrink edits (direct calls), the deleted-char
marker landing at the edit end, and a real setq-driven suffix-marker
case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-argument parameterized layers silently mis-handled every
wrong-arity call:
- Missing args: cl-progv (since its Emacs 27 rewrite) binds
parameters beyond the supplied values to nil, never leaving them
unbound, so (tp-set "s" '(two-arg-layer "red")) quietly produced
(:foreground "red" :background nil) - while the docstring of
tp-layer-props-with-args falsely promised an unbound-variable
error.
- Excess flat-form args fell into extra-props and were applied as
garbage text-property KEYS with value nil ("green" as a property
name).
- The stack path (tp-push-layer with one arg for a two-parameter
layer) fell through tp--normalize-layer-spec's named-inline branch,
building an odd-length plist and dying with the cryptic "Odd length
text property list".
Fixes, per site:
- tp-layer-props-with-args checks (length args) against the arglist
and signals "tp layer NAME takes N argument(s), got M"; its
docstring now documents the explicit error instead of the
impossible unbound-parameter story. Extra values remain ignored
in this direct call, per the documented contract.
- tp--resolve-props signals the same error after the arity split when
fewer values were available (covering parameterized groups too),
and rejects flat-form excess positional args whose first extra
element is not a symbol (i.e. can never be a property key).
- tp--normalize-layer-spec signals "tp layer NAME expects N args,
got M" for any parameterized-layer arity mismatch instead of
falling through to the named-inline branch.
Correct-arity flat, wrapped, extra-props and group behavior is
unchanged (covered by controls in the new tests, which port the
review's verify-arg1 probe).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp-hide-layer's contract says a hidden layer "no longer renders", but
tp-flatten-layers merged the whole stack unfiltered and
tp-merge-layers merged every listed layer unfiltered (only the
tp-hidden bookkeeping flag was stripped), so flattening or merging a
stack with a hidden layer silently un-hid it: the hidden layer's
props became the visible rendering (green -> red in the review
probe). The shipped flatten test asserted only flag absence, missing
the rendering flip.
New semantics, stated in both docstrings:
- tp-flatten-layers discards hidden layers (image-editor flatten):
only visible layers' props merge; a run whose every layer is hidden
flattens to bare text, consistent with all-hidden rendering.
- tp-merge-layers merges hidden matched layers away but excludes
their props, so a merge can never render what was hidden. When ALL
matched layers are hidden the merged layer keeps their merged props
but carries tp-hidden itself - data preserved, nothing un-hidden,
tp-show-layer reveals it.
API-RET-01 slice: both functions now return the number of modified
runs, counting exactly like tp-delete-layer (their previous hardcoded
nil return was undocumented, so no documented behavior changes).
Tests: the flatten-drops-tp-hidden-flag test now asserts the rendered
face; new tests port the hid2-probe scenarios (flatten with hidden
top, all-hidden flatten to bare text, merge excluding hidden props,
all-hidden merge staying hidden and revealable) plus count-return
coverage for both functions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp--buffer-has-layer-region-p tested liveness with a direct tp-name
property search, which only sees the rendered top layer of each run.
A layer buried below a pushed top, or hidden via tp-hide-layer, keeps
its tp-name inside the tp-layers storage property, so:
- tp-gc-anonymous-layers undefined layers a live buffer still held,
destroying their definitions and reactive deps; pop/show later
resurfaced text whose reactivity was permanently dead;
- tp-reactive-track-buffer (the documented remedy for the
string-insertion registration gap) never registered buried or
hidden layers either, so the gap-closer itself had the gap.
Fix:
- tp-reactive.el gains the shared stack-aware scan
tp-reactive--buffer-layer-names: a tp--map-intervals walk that
collects the direct tp-name of each run plus the tp-name of every
plist inside the run's tp-layers value (read as a plain list of
plists, so tp-reactive needs nothing from the stack module).
tp-reactive-track-buffer is rewritten on it.
- tp--buffer-has-layer-region-p is rewritten on the same scan and
moves - together with tp-gc-anonymous-layers (autoload kept) - from
tp-render.el into tp-layer.el beside tp--anonymous-layer-registry,
so the whole anonymous-layer lifecycle (mint, intern, undefine,
collect) lives in one module and tp-render sheds its only
non-rendering responsibility. tp-render's tp--map-layer-buffers
keeps calling the predicate downward.
Regression tests port the review's gc1/xm02 probes: buried-under-push
kept alive with reactivity surviving a later pop; hidden layer kept
alive with show+setq re-rendering; track-buffer registering buried
and all-hidden layers from inserted strings. Existing tests cover
the visible-kept and killed-buffer-collected control cases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reactive engine located regions solely via the direct tp-name
text property - the render cache of the topmost visible layer - and
wrote only direct properties. The resolved-value snapshots stored
inside the tp-layers property (hidden layers, layers buried below the
top, and in full-stack mode even the visible top's own snapshot) were
never refreshed, so:
- an update received while a layer was hidden rendered stale values
after tp-show-layer, permanently (re-setting the same value is a
watcher no-op and could not repair it);
- with any layer hidden, the very next stack operation - even a no-op
tp-move-layer - rebuilt from the stale snapshot and silently
reverted the reactive update;
- a below-top reactive layer revealed by tp-pop-layer rendered its
stale snapshot;
- one buffer could render two different values of one variable at
once (mixed visible/hidden regions);
- reactive tp-text never reached hidden regions at all.
Fix in tp-render.el:
- tp--write-layer-through-stack-storage: for every run whose
tp-layers holds an entry of the updated layer, replace the layer's
own keys in that entry (preserving its tp-hidden flag) and rebuild
the run via the tp--stack-props-to-list / tp--stack-build-props
codec, which also refreshes the topmost-visible render cache in
full-stack mode. Unchanged runs are left untouched.
- tp--update-layer-regions calls it after the direct render pass.
- tp--replace-reactive-text-in-buffer gains a second pass replacing
text in regions where the layer sits only inside storage (text is
physical - hide/show toggles properties, never text), carrying all
existing properties over the edit, then writes the refreshed props
including tp-text through to storage. The minimal-diff edit is
factored into tp--edit-region-minimal-diff, shared by both passes.
Acceptance tests port the review's XM-01 probe scenarios: update
while hidden renders after show (A3); no-op stack ops never revert
and same-value re-set is never needed (B1-B4); pop reveals current
values (C2); reactive tp-text reaches hidden text (T1); mixed
visible/hidden regions stay in sync (X1); plus the hid1-probe
show+hide round-trip of an unrelated hidden top.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Behavior-preserving relocation of tp--stack-hidden-p,
tp--stack-props-to-list and tp--stack-build-props into tp-layer.el,
beside their existing dependencies tp--layer-stack-to-list and
tp--build-layer-props. The codec's only inputs are dash/seq/plist
utilities, so it sits naturally at the layer-definition level, and
the move lets the reactive re-render engine (tp-render.el, which may
not require tp-stack) read and write stack storage without
duplicating format knowledge - the prerequisite for making reactive
updates write through to hidden/buried layer storage. tp-stack keeps
calling the codec downward; no callers change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 0.3.0 buffer registry made reactive updates walk only registered
buffers, but only tp-set/tp-reset/tp-add registered. Layer stack
mutators (tp-push-layer, tp-put-layer, hide/show, move/raise/rotate,
merge/flatten, add-to-layers) and the pattern-apply paths
(tp-match-add/-reset, tp-regexp-add/-reset) stamped tp-name via
direct set-text-properties without registering, so the moment a layer
was known from one tp-set anywhere, every buffer reached only through
those paths was silently and permanently skipped by reactive updates
- including the README's own flagship tp-push-layer example. This
regressed 0.2.0, whose full buffer-list scan reached every buffer.
Fix: every buffer write that stamps tp-name now registers the buffer
via tp-reactive--register-layer-buffer (called directly, so tp-stack
and tp-search gain no tp-ops edge):
- tp-stack.el: new tp--stack-register-layers registers every named
plist of the freshly written stack - buried and hidden layers
included, which the stack-aware GC liveness fix builds on - beside
all eleven set-text-properties sites; requires tp-reactive.
- tp-search.el: tp--deep-merge-apply and tp--reset-apply register the
target buffer for the applied props' tp-name (and any group
tp-layers entries); requires tp-reactive.
Regression tests: reactive updates reach a second buffer whose layer
arrived via tp-push-layer / tp-regexp-add / tp-match-reset after a
tp-set registered the layer elsewhere, and stack writes register
buried and hidden layers that arrived via string insertion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp-put-layer now dispatches multi-argument layer and group specs —
flat (NAME A1 .. AN) and wrapped (NAME (A1 .. AN)) — checked before
the list-of-specs branch so argument values that are themselves layer
names are not misread as a stack of layers. tp-remove's layer-key
extraction binds every parameter with dummy args via
tp-layer-props-with-args instead of passing a single dummy to the
first parameter only. Five regression tests; suite 527/527, doctests
63/63, compile clean.
Known pre-existing gap surfaced while testing (deliberately not fixed
here, queued for the design review): parameterized layers applied via
the tp-set plist forms never stamp tp-name, so tp-remove by layer
name is a silent no-op for them regardless of arity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reactive performance (tp-reactive/tp-render/tp-ops): layer->buffer
registry replaces full buffer-list scans on reactive updates, with a
conservative unknown->learning-scan fallback, kill-buffer pruning,
and tp-reactive-track-buffer to close the propertized-string-insert
gap; minimal-diff tp-text replacement preserves point and markers and
makes identical-text updates true no-ops; tp-gc-anonymous-layers
collects interned anonymous layers no registered buffer still shows.
Stack capabilities (tp-stack): tp-hide-layer/tp-show-layer visibility
(hidden layers stay in the stack but do not render; hiding the top
reveals the next visible layer), tp-lower-layer, tp-rotate-layer
DIRECTION/COUNT, tp-layer-stack-at, and modified-run-count returns
with NOERROR options.
Search and layer APIs (tp-search/tp-layer): SUBEXP capture groups and
START/END bounds for tp-regexp-*/tp-match-*, PREDICATE/NOT-CURRENT
exposure on tp-forward/tp-backward/-do (defaults keep 0.2.0 semantics),
multi-argument parameterized layers (define-tp/define-tps arglists of
any arity, tp-layer-props-with-args, tp-layer-arglist), and the
interactive tp-describe-layer.
79 new regression tests; combined suite 522/522 green, shuffled run
green, doctests 63/63, byte-compile clean with warnings-as-errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Emacs 28 stores/prints text-property plists in a different order than
29+; the S-mystyle and L-paramgroup assertions compared whole prin1
strings and failed on 28.1 only. Compare per property instead (face,
tp-name, tp-layers — the stack order inside tp-layers is stable), and
note the version-dependent print order beside both README examples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Job logs are not anonymously readable; emit FAIL/expected/got lines
(or the log tail on a load error) as ::error:: annotations so the
failing assertion is visible through the public Checks API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dash install succeeded; the step failed writing GITHUB_ENV because
ls -d .elpa/dash-* matched both the package directory and the adjacent
dash-N.N.N.signed marker, producing a second non-KEY=value line
("Invalid format"). Restrict the glob to directories. Also bump
actions/checkout to v6 to clear the Node 20 deprecation warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Byte-compile warnings swept 57 -> 0 across all modules, tests, and
doctest (docstring rewraps and quoting, defvar declarations for the
reactive test variables, prefixed doctest counters, dead-binding
removal, one impossible eq -> equal in a face-merge assertion) with
behavior preserved. GitHub Actions workflow runs an Emacs 28.1/29.4/
30.1 matrix: compile-all with warnings-as-errors, the 443-test suite,
a genuinely shuffled-order rerun (tp-run-shuffled.el runs each test
individually; ERT's member selector cannot reorder), and the 63
README doctests. Makefile gains WERROR, compile-all, and
test-shuffled. Autoload cookies added for the four interactive
commands and the define-tp/define-tps macros. package-lint: 0
findings (main file tp.el); draft MELPA recipe in docs/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Splits tp.el into nine layered modules behind the tp.el umbrella,
fixes ~43 confirmed bugs across every subsystem, adds tp-member,
grows the ERT suite from 280 to 443 tests plus 63 executable README
doctests, and aligns CHANGELOG, bilingual READMEs, and docs/ with the
actual behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>