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>
This commit is contained in:
Kinneyzhang 2026-07-27 02:31:45 +08:00
parent b4dc8abc8b
commit f6032a891e
2 changed files with 183 additions and 14 deletions

View File

@ -796,6 +796,72 @@ nothing matched."
(should (= (tp-rotate-layer 4 6) 0))
(should (eq (get-text-property 1 'tp-name) 'la))))
;;; API-ARG-01: canonical (START END DIRECTION COUNT OBJECT) rotate order
(ert-deftest tp-stack-test-rotate-layer-canonical-order ()
"The canonical order needs no nil OBJECT placeholder."
(tp-stack-tests--with-env
(insert "abcdef")
(define-tp la () '(face bold))
(define-tp lb () '(face italic))
(define-tp lc () '(face underline))
(tp-push-layer 1 6 'la)
(tp-push-layer 1 6 'lb)
(tp-push-layer 1 6 'lc) ; top->bottom: lc lb la
(should (= (tp-rotate-layer 1 6 'up) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(la lc lb)))
(should (= (tp-rotate-layer 1 6 'down) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(lc lb la)))
;; COUNT rides fourth in the canonical order.
(should (= (tp-rotate-layer 1 6 'down 2) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(la lc lb)))
(should (= (tp-rotate-layer 1 6 'up 2) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(lc lb la)))))
(ert-deftest tp-stack-test-rotate-layer-canonical-order-object-last ()
"OBJECT rides last in the canonical order (buffer and string)."
(tp-stack-tests--with-env
(insert "abcdef")
(define-tp la () '(face bold))
(define-tp lb () '(face italic))
(tp-push-layer 1 6 'la)
(tp-push-layer 1 6 'lb) ; top->bottom: lb la
(let ((buf (current-buffer)))
(with-temp-buffer ; a different current buffer
(should (= (tp-rotate-layer 1 6 'up 1 buf) 1))))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(la lb)))
;; nil COUNT in the canonical order still defaults to 1.
(let ((buf (current-buffer)))
(with-temp-buffer
(should (= (tp-rotate-layer 1 6 'down nil buf) 1))))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(lb la)))
;; A string OBJECT in the canonical order's last slot.
(let ((str (copy-sequence "xyz")))
(tp-push-layer str 'la)
(tp-push-layer str 'lb)
(should (= (tp-rotate-layer 0 3 'up 1 str) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 0 str)) '(la lb))))))
(ert-deftest tp-stack-test-rotate-layer-legacy-order-still-works ()
"The legacy (START END OBJECT DIRECTION COUNT) order keeps working.
A non-up/down third argument - nil, a buffer or a string - still
selects the legacy order."
(tp-stack-tests--with-env
(let ((str (copy-sequence "abcdef")))
(define-tp la () '(face bold))
(define-tp lb () '(face italic))
(tp-push-layer str 'la)
(tp-push-layer str 'lb) ; top->bottom: lb la
(should (= (tp-rotate-layer 0 6 str 'up 1) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 0 str)) '(la lb))))
(insert "abcdef")
(tp-push-layer 1 6 'la)
(tp-push-layer 1 6 'lb)
(should (= (tp-rotate-layer 1 6 nil 'up 1) 1))
(should (equal (mapcar #'car (tp-layer-stack-at 1)) '(la lb)))
;; Canonical-order direction errors still signal.
(should-error (tp-rotate-layer 1 6 'sideways))))
;;; 0.3.0 S3: tp-layer-stack-at
(ert-deftest tp-stack-test-layer-stack-at-shape ()

View File

@ -306,6 +306,11 @@ A LAYER naming an undefined layer or group normally signals an
error. If NOERROR is non-nil, return nil instead of signaling when
LAYER cannot be resolved; nothing is modified in that case.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own. The returned string is
that same mutated object.
Returns OBJECT when one was given (in particular the string in
string forms), otherwise the cons (START . END)."
(pcase-let ((`(,start ,end ,obj ,layer-spec ,idx)
@ -352,6 +357,11 @@ A LAYER naming an undefined layer or group normally signals an
error. If NOERROR is non-nil, return nil instead of signaling when
LAYER cannot be resolved; nothing is modified in that case.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own. The returned string is
that same mutated object.
Returns what `tp-put-layer' returns: OBJECT when one was given (in
particular the string in string forms), otherwise (START . END)."
(pcase-let ((`(,start ,end ,obj ,layer)
@ -377,6 +387,10 @@ LAYER-NAME/IDX can be:
Only text inside [START, END) is modified.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. A LAYER-NAME/IDX
matching no layer never signals: unmatched runs are silently left
alone and a return value of 0 means nothing matched at all."
@ -409,6 +423,10 @@ Calling conventions:
2. Entire string:
(tp-pop-layer STRING)
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified; 0 means no run in the
region had a layer to pop."
(pcase-let ((`(,start ,end ,obj)
@ -497,6 +515,10 @@ Both indices refer to positions before the move.
The layer at FROM-ID is removed and inserted at TO-IDX position.
OBJECT defaults to current buffer for region form.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. A FROM-ID matching no
layer never signals: unmatched runs are silently left alone and a
return value of 0 means nothing matched at all."
@ -533,6 +555,10 @@ N defaults to 1. The resulting position is clamped to the stack.
Uses `tp--raise-layer-in-stack' internally, which is built on
`tp--move-layer-in-stack'.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. An IDX/LAYER-NAME
matching no layer never signals: unmatched runs are silently left
alone and a return value of 0 means nothing matched at all."
@ -576,6 +602,10 @@ N defaults to 1. The resulting position is clamped to the stack.
OBJECT defaults to current buffer for region form.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. An IDX/LAYER-NAME
matching no layer never signals: unmatched runs are silently left
alone and a return value of 0 means nothing matched at all."
@ -586,16 +616,29 @@ alone and a return value of 0 means nothing matched at all."
(setq n (or n 1))
(tp-raise-layer start end layer-id (- n) obj)))
(defun tp-rotate-layer (start-or-string &optional end-or-direction object-or-count direction count)
(defun tp-rotate-layer (start-or-string &optional end-or-direction
direction-object-or-count
count-or-direction object-or-count)
"Rotate layers, by default moving the top layer to the bottom.
Calling conventions:
1. Buffer/string region:
(tp-rotate-layer START END OBJECT DIRECTION COUNT)
1. Buffer/string region (canonical order, OBJECT last like the rest
of the stack family):
(tp-rotate-layer START END DIRECTION &optional COUNT OBJECT)
2. Entire string:
(tp-rotate-layer STRING DIRECTION COUNT)
3. Buffer/string region (legacy 0.3.0 order, kept working forever):
(tp-rotate-layer START END OBJECT DIRECTION COUNT)
The two region orders are told apart by the third argument: the
symbols `up' and `down' are never valid OBJECTs, so a third argument
of `up'/`down' unambiguously selects the canonical order, e.g.
\(tp-rotate-layer 1 5 \\='up) - no nil OBJECT placeholder needed.
Any other third argument (a buffer, a string, or nil for the current
buffer) selects the legacy order.
DIRECTION is `down' or nil to move the top layer to the bottom (the
historical behavior), or `up' to move the bottom layer to the top;
any other value signals an error. COUNT is the number of rotation
@ -603,18 +646,41 @@ steps and defaults to 1; a COUNT below 1 rotates nothing. Layers
keep their relative order; hidden layers rotate with the rest of the
stack.
OBJECT defaults to current buffer for region form.
OBJECT defaults to current buffer for region forms.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified; 0 means no run in the
region had layers to rotate (or COUNT was below 1)."
(pcase-let ((`(,start ,end ,obj)
(tp--parse-layer-args
start-or-string
(list end-or-direction object-or-count) 0)))
(let* ((string-form (stringp start-or-string))
(dir (or (if string-form end-or-direction direction) 'down))
(cnt (or (if string-form object-or-count count) 1))
(applied 0))
(let (start end obj dir cnt)
(cond
;; Entire string form: (STRING DIRECTION COUNT).
((stringp start-or-string)
(setq start 0
end (length start-or-string)
obj start-or-string
dir end-or-direction
cnt direction-object-or-count))
((numberp start-or-string)
(setq start start-or-string
end end-or-direction)
(if (memq direction-object-or-count '(up down))
;; Canonical region order: (START END DIRECTION COUNT OBJECT).
(setq dir direction-object-or-count
cnt count-or-direction
obj object-or-count)
;; Legacy region order: (START END OBJECT DIRECTION COUNT).
(setq obj direction-object-or-count
dir count-or-direction
cnt object-or-count)))
(t (error "Invalid layer arguments: %S"
(cons start-or-string
(list end-or-direction direction-object-or-count)))))
(let ((applied 0))
(setq dir (or dir 'down)
cnt (or cnt 1))
(unless (memq dir '(up down))
(error "Invalid rotate direction: %S" dir))
(when (>= cnt 1)
@ -634,7 +700,12 @@ region had layers to rotate (or COUNT was below 1)."
applied)))
(defun tp-pin-layer (start-or-string &optional end-or-idx idx-or-object object)
"Pin a layer to the top (make it visible).
"Move layer IDX/LAYER-NAME to the top of the stack (one-shot).
Despite the name, nothing stays pinned: this is a single move to
index 0, exactly (tp-move-layer ... IDX/LAYER-NAME 0 ...), and
nothing prevents a later `tp-push-layer' or `tp-put-layer' from
covering the moved layer again.
Calling conventions:
1. Buffer/string region:
@ -643,7 +714,9 @@ Calling conventions:
2. Entire string:
(tp-pin-layer STRING IDX/LAYER-NAME)
Uses `tp-move-layer' internally to move the specified layer to index 0 (top).
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. An IDX/LAYER-NAME
matching no layer never signals: unmatched runs are silently left
@ -666,6 +739,10 @@ Calling conventions:
Uses `tp--switch-layers-in-stack' internally.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. When either layer is
missing from a run's stack nothing signals: such runs are silently
left alone and a return value of 0 means nothing matched at all."
@ -714,6 +791,10 @@ property name inside layers, like `tp-name'.
OBJECT defaults to current buffer for region form.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. A NAME matching no
layer never signals; runs whose match is already hidden are left
alone as well, so a return value of 0 means nothing changed."
@ -757,6 +838,10 @@ again, restoring its properties onto the text.
OBJECT defaults to current buffer for region form.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified. A NAME matching no
layer never signals; runs whose match is not hidden are left alone
as well, so a return value of 0 means nothing changed."
@ -820,6 +905,10 @@ hidden, the merged layer keeps their merged properties but carries
the `tp-hidden' flag itself: the data is preserved without un-hiding
anything, and `tp-show-layer' on the merged layer renders it.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified, counting like
`tp-delete-layer': a run counts when at least one listed layer
matched and the merge rewrote it, and 0 means nothing matched at
@ -888,6 +977,10 @@ was hidden. When EVERY layer of a run is hidden, the run's
properties are cleared entirely (bare text), consistent with the
all-hidden rendering of `tp-hide-layer'.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own.
Returns the number of property runs modified, counting like
`tp-delete-layer': every run that had layers to flatten counts, and
0 means no run in the region had any layers."
@ -929,6 +1022,11 @@ Properties are deeply merged (nested plists are merged, not replaced).
OBJECT defaults to current buffer for region form.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own. The returned string is
that same mutated object.
Returns the modified object (string) or nil for buffer operations."
(let (start end plist obj layer-ids)
(setq layer-ids idx-or-layer-name-list)
@ -1002,6 +1100,11 @@ OBJECT defaults to current buffer for region form.
This function uses `tp-add-to-layers' internally, collecting all
layer indices and passing them to add the plist to every layer.
Unlike `tp-set', the string form modifies STRING destructively (in
place) rather than returning a propertized copy: never pass a string
literal or a shared string you do not own. The returned string is
that same mutated object.
Returns the modified object (string) or nil for buffer operations."
(let (start end plist obj)
(cond