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>
tp-forward-do/tp-backward-do target the TIMES-th match specifically;
when fewer matches exist the string paths applied FUNCTION to the last
available match -- the wrong target -- while buffer paths applied
nothing. Both now apply nothing on shortfall and return the available
count. Updates the two legacy with-range tests that codified the
string behavior, adds four shortfall/exact-count regression tests, and
documents the contract in both READMEs and the docstrings. Suite
443/443 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README.md/README_CN.md: new verified Quick Start (fixes the dead
#quick-start nav link); Installation rewritten for the tp-*.el module
family; every broken or drifted example fixed and executed in batch
Emacs (mandatory () ARGLIST and quoted reactive keywords in all
define-tp/define-tps calls, corrected tp-search-map argument order,
interval-list returns, stacked duplicate-face results, gap intervals,
last-wins tp-plist, non-destructive string forms, per-pattern match
ordering, case-fold regexp outputs, rewired end-to-end theme example);
documents the symmetric tp-backward contract, the length-changing
replacement rules, all four tp-put-layer layer specs, and the
previously-missing tp-member, buffer/display macros, and palette
system; state resets now use tp-layer-reset; license corrected to
GPLv3+. CN mirrors EN exactly (119 headings / 258 fences each; code
blocks identical, comments translated).
docs/ARCHITECTURE.md rewritten around the real nine-module layering
and hook-variable inversions; nonexistent helper names removed.
docs/CODE-ANALYSIS.md marked as pre-split historical analysis with
locations/counts corrected. Reactive docs aligned with the fixed
engine semantics (replace-not-accumulate, buffer-local isolation,
nil computed values, batching union, first-render transform).
287 fenced blocks from both READMEs executed: 0 failures; 56-example
assertion suite passes; combined ERT suite 439/439 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp--replace-match-text's string branch silently truncated longer
replacements and left residue on shorter ones ("<foo>" into a 3-char
match yielded "<fo"). Strings cannot change length in place, so the
honest contract is: same-length replacements mutate in place as before;
a different-length replacement signals a clear error directing the
caller to a buffer OBJECT. Buffer paths (delete+insert) are unchanged.
Updates the five tests that codified truncation/residue and adds a
same-length regression test; suite 439/439 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
35 fixes across tp-layer.el, tp-stack.el, tp-search.el, tp-reactive.el
and tp-render.el, each empirically reproduced before and after:
layer-definition resolution/cycles/copying/cleanup, clipped region-local
stack mutators, symmetric backward search matching, length-changing
replacements, and reactive re-render correctness (replace-not-accumulate,
buffer-local isolation, batching union, per-interval props).
Adds four per-module regression suites (100 new tests); combined suite
is 438/438 green. tp-test-backward updated to the now-symmetric backward
matching contract it previously codified inverted. The string-vs-buffer
shortfall divergence in tp-forward-do/tp-backward-do is documented in
CHANGELOG as a known divergence rather than changed. Adds CHANGELOG.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Like tp-at but distinguishes a property present with value nil from an
absent property, returning (PROPERTY VALUE) or nil. Covered by four
new tests in tp-ops-tests.el; full suite 338/338 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ops/core (B1-B8): tp-remove no longer drops 3rd+ properties; string
removal is per-interval via tp--map-intervals instead of smearing
position-0 props; tp-clear defaults bounds from OBJECT; (tp-get STR N N)
works like the buffer region form; no bogus (:key nil) from trailing
bare keywords; region form signals immediately on flat PROP/VAL args;
face-family prepend semantics extended to font-lock-face/mouse-face.
builtins/palette (B45-B51): Emacs 28.1 compat for plistp/subr-x;
display-buffer macros use a minor-mode keymap instead of mutating the
major-mode map; tp-link resolves palette colors lazily (theme-correct);
tp-palette-alist is the single source of truth (stale defvars dropped);
tp-headline handles integer heights; tp-space matches its documented
pixel spec; tp-parse-color accepts one-sided cons colors.
Test infra: fixture gains unwind-protect teardown via tp-layer-reset
(incl. transforms); file header/provide renamed to tp-tests; suite is
order-independent (verified with shuffled runs). Adds Makefile with
test/compile/clean targets.
334 tests green (280 legacy + 54 new regression tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tp.el (4866 lines) is now an umbrella over nine modules with an
enforceable dependency order: tp-core -> tp-reactive -> tp-layer ->
tp-ops -> tp-search -> tp-render -> tp-stack -> tp-palette ->
tp-builtins. Upward dependencies are inverted through four hook
variables installed by tp-render.el.
Also: require text-property-search (fixes tp-backward void-function),
clip tp-intervals to the requested range, add the shared clipping
interval walker tp--map-intervals and tp-face-properties, remove
synced-conflict junk files, byte-compilation now succeeds (define-tp
macroexpansion previously failed at compile time).
All 280 legacy tests pass plus 8 new tp-core tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When (frame-parameter nil 'background-mode) returns nil (e.g., in
batch mode or certain configurations), both tp-theme-light-p and
tp-theme-dark-p return nil, causing color parsing to fail for
cons cell ("light" . "dark") and plist (:light "l" :dark "d") formats.
Added default fallback to light color when background-mode is unknown.
This is the root cause of tp-palette settings not taking effect.
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
Fixed the pcase branches for tp-palette-bg-p and tp-palette-fbg-p
predicates which had an extra `symbol` keyword incorrectly placed
between the pattern and the return expression. This caused these
branches to return the original symbol instead of the processed
intern result, leading to tp-palette settings not taking effect
when using palettes with -bg or -fbg suffixes.
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>