Commit Graph

341 Commits

Author SHA1 Message Date
Kinneyzhang
a65d79921a Merge dev/0.3.0: CI matrix, feature tracks, architecture review fixes
0.3.0: GitHub Actions CI (Emacs 28.1/29.4/30.1, warnings-as-errors,
shuffled-order rerun, doctests); layer visibility and stack
introspection; reactive layer-buffer registry, minimal-diff tp-text,
anonymous-layer GC; search capture groups, bounds, predicates;
multi-argument parameterized layers; a five-dimension adversarial
architecture/API review whose 29-item fix plan is fully applied
(six confirmed seam bugs fixed, module boundaries tightened to two
hook variables, additive API polish, deprecations); bilingual docs
with 88 executable doctests. Suite 578/578 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 03:44:45 +08:00
Kinneyzhang
fe2c03d2bd Document 0.3.0 across bilingual READMEs, doctests, and ARCHITECTURE.md
Some checks are pending
CI / test (28.1) (push) Waiting to run
CI / test (29.4) (push) Waiting to run
CI / test (30.1) (push) Waiting to run
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>
2026-07-27 03:44:45 +08:00
Kinneyzhang
aa214456df Complete the 0.3.0 changelog: features, review fixes, boundary moves
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 02:41:23 +08:00
Kinneyzhang
363045ac7c Enumerate the five real tp-set calling conventions
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>
2026-07-27 02:37:16 +08:00
Kinneyzhang
698dc54b1c Expose the palette primaries and finish the builtin docstrings
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>
2026-07-27 02:36:19 +08:00
Kinneyzhang
8c28361640 Add prefix-conforming define aliases and export the group props plural
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>
2026-07-27 02:34:31 +08:00
Kinneyzhang
f6032a891e Accept the family-consistent rotate order; document stack string mutation
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>
2026-07-27 02:31:45 +08:00
Kinneyzhang
b4dc8abc8b Add ABSOLUTE coordinates to tp-intervals and tp-intervals-map
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>
2026-07-27 02:27:53 +08:00
Kinneyzhang
985b51d2c6 Deprecate the raw search wrappers and state the nav contracts
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>
2026-07-27 02:26:36 +08:00
Kinneyzhang
575c99be88 Normalize reversed bounds and validate SUBEXP in the pattern engine
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>
2026-07-27 02:24:11 +08:00
Kinneyzhang
e870b87941 Move the anonymous-layer counter and name generator into tp-layer
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>
2026-07-27 02:09:58 +08:00
Kinneyzhang
95fb4ae862 Delete the dead layer-expansion pair in tp-layer
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>
2026-07-27 02:08:41 +08:00
Kinneyzhang
99bd6f7c15 Drop tp-stack's phantom dependency on tp-ops
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>
2026-07-27 02:07:43 +08:00
Kinneyzhang
6295d5ccbc Move the batch flush and tp-with-batch-updates up into tp-render
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>
2026-07-27 02:06:28 +08:00
Kinneyzhang
2b000b5f0e Move the tp-text handler chain from tp-render into tp-ops
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>
2026-07-27 02:04:40 +08:00
Kinneyzhang
8f50231695 Clear the pending queue on reset and drain it under unwind-protect
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>
2026-07-27 01:56:59 +08:00
Kinneyzhang
a0d0e71ff8 Insert before deleting in minimal-diff tp-text edits so markers track
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>
2026-07-27 01:54:43 +08:00
Kinneyzhang
117c714b97 Signal clear arity errors for wrong-arity parameterized-layer calls
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>
2026-07-27 01:49:47 +08:00
Kinneyzhang
e15a18d718 Flatten/merge no longer render hidden layers; both return run counts
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>
2026-07-27 01:47:03 +08:00
Kinneyzhang
b49e2740e8 Make GC liveness and track-buffer scans stack-aware; GC moves to tp-layer
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>
2026-07-27 01:44:06 +08:00
Kinneyzhang
cd31652c6e Write reactive updates through to tp-layers stack storage
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>
2026-07-27 01:41:16 +08:00
Kinneyzhang
c6fbccf659 Move the layer-stack storage codec from tp-stack down to tp-layer
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>
2026-07-27 01:36:31 +08:00
Kinneyzhang
28acdae183 Register every buffer-mutating layer write in the reactive registry
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>
2026-07-27 01:35:05 +08:00
Kinneyzhang
27ad7325c5 Close cross-module seams for multi-argument parameterized specs
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>
2026-07-27 00:36:19 +08:00
Kinneyzhang
507ccf798c Integrate 0.3.0 feature tracks: reactive perf, stack visibility, search/API
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>
2026-07-27 00:32:47 +08:00
Kinneyzhang
7f0da36ada Bump version to 0.3.0; start changelog with the infrastructure work
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 00:12:22 +08:00
Kinneyzhang
b454cbb061 Make two doctest assertions property-order-insensitive for Emacs 28
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>
2026-07-26 23:58:12 +08:00
Kinneyzhang
ca405f977d CI: surface doctest failures as annotations
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>
2026-07-26 23:55:20 +08:00
Kinneyzhang
41e3de5865 Fix CI env export: glob matched GNU ELPA's .signed marker too
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>
2026-07-26 23:53:10 +08:00
Kinneyzhang
f422a0f3a9 Remove MELPA recipe draft; publication is not planned
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 23:49:56 +08:00
Kinneyzhang
36328a1cca Phase 1: CI matrix, zero-warning compile, shuffled runner, autoloads
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>
2026-07-26 23:46:52 +08:00
Kinneyzhang
2c5ce6c27b Merge refactor/modular-architecture: 0.2.0 modular split, bug fixes, docs
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>
2026-07-26 21:19:05 +08:00
Kinneyzhang
ca2f02dcdf Add tp-doctest.el: README examples as executable tests
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>
2026-07-26 21:18:44 +08:00
Kinneyzhang
ecdd952b55 Unify -do shortfall to all-or-nothing on strings and buffers
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>
2026-07-26 21:16:56 +08:00
Kinneyzhang
2b33495898 Align README (EN/CN) and docs/ with modular architecture and 0.2.0 semantics
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>
2026-07-26 21:01:01 +08:00
Kinneyzhang
9aca18979b Reject length-changing string replacements instead of corrupting
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>
2026-07-26 20:12:37 +08:00
Kinneyzhang
ceb0dfb0bc Fix confirmed bugs in layer, stack, search, and reactive-render modules
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>
2026-07-26 20:01:20 +08:00
Kinneyzhang
08b5d9aa1c Add tp-member: plist-member-style property lookup
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>
2026-07-26 19:33:34 +08:00
Kinneyzhang
49cb8d8062 Fix confirmed bugs in core ops and builtins/palette modules
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>
2026-07-26 18:31:59 +08:00
Kinneyzhang
5e5017a726 Split tp.el monolith into layered modules (behavior-preserving)
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>
2026-07-26 17:13:44 +08:00
Kinneyzhang
af0980efef update 2026-07-26 16:31:36 +08:00
Kinneyzhang
29b2ee54d3 update 2026-01-13 18:11:40 +08:00
copilot-swe-agent[bot]
9aace2f6fe Update docs/CODE-ANALYSIS.md to include self-reference in file structure
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
2026-01-10 09:31:01 +00:00
copilot-swe-agent[bot]
715ceaac1e Add comprehensive code analysis report for developers
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
2026-01-10 09:29:32 +00:00
copilot-swe-agent[bot]
ce06c76089 Initial plan 2026-01-10 09:22:55 +00:00
Kinneyzhang
a1ce94c0dd add heatmap tp-palette 2026-01-08 15:48:34 +08:00
Kinneyzhang
5d75ff434c update tests 2026-01-08 11:52:16 +08:00
copilot-swe-agent[bot]
6b96d8575f Simplify reactive text replacement to directly reset properties with new values
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
2026-01-08 03:43:55 +00:00
copilot-swe-agent[bot]
7d7979c54c Revert changes to tp--replace-reactive-text-in-buffer to fix checkbox strikethrough toggle
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
2026-01-08 03:17:05 +00:00
copilot-swe-agent[bot]
89d76fa0cc Address code review feedback: improve docstring and null handling
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
2026-01-08 03:00:59 +00:00