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>
This commit is contained in:
Kinneyzhang 2026-07-26 21:16:56 +08:00
parent 2b33495898
commit ecdd952b55
6 changed files with 66 additions and 22 deletions

View File

@ -140,6 +140,13 @@ Search and navigation (tp-search):
were previously skipped).
- The triplicated ~38-line replacement lambda was extracted into one
shared helper.
- `tp-forward-do` / `tp-backward-do` shortfall is now all-or-nothing on
both paths: TIMES targets the TIMES-th match specifically, so when
fewer matches exist nothing is applied and the available count is
returned. String paths previously acted on the last available match —
the wrong target; the two legacy tests codifying that
(`tp-test-forward-do-on-string-with-range` and its backward twin)
were updated.
Reactive rendering (tp-reactive / tp-render):
@ -172,15 +179,6 @@ Test infrastructure:
tests); the suite passes in randomized order.
- `tp-tests.el` header and `provide` renamed to match its file name.
### Known divergences
- On shortfall (fewer than TIMES matches in the range),
`tp-forward-do` / `tp-backward-do` string paths still apply the
function to the last available match while buffer paths apply
nothing. Two legacy tests codify the string behavior
(`tp-test-forward-do-on-string-with-range` and its backward twin), so
it was left unchanged; unifying it is a pending semantics decision.
### Added
- `tp-member`: like `tp-at`, but distinguishes "property present with

View File

@ -1376,7 +1376,7 @@ Search forward/backward for text with PROPERTY and apply FUNCTION **only to the
- **PROPERTY** is the text property to search for.
- **VALUE** is the optional value to match; nil means search for PROPERTY without matching value.
- **OBJECT** can be a buffer or string; nil defaults to current buffer.
- **TIMES** is the number of searches, defaulting to 1. The function searches TIMES times but only applies FUNCTION to the last (Nth) match found.
- **TIMES** is the number of searches, defaulting to 1. The function searches TIMES times but only applies FUNCTION to the TIMES-th match. All-or-nothing: if fewer than TIMES matches exist, FUNCTION is not applied at all and the number of available matches is returned.
- **START** and **END** define the search range; defaults are object start and end.
- Returns the number of successful matches.

View File

@ -1363,7 +1363,7 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的
- **PROPERTY** 是要搜索的文本属性。
- **VALUE** 为 nil 时,表示搜索 PROPERTY 属性,不用匹配值。
- **OBJECT** 默认是当前 buffer 或指定的字符串或指定的 buffer。
- **TIMES** 表示向前/向后搜索几次,默认搜索一次。该函数会搜索 TIMES 次,但仅对找到的最后(第 N 次)匹配应用 FUNCTION
- **TIMES** 表示向前/向后搜索几次,默认搜索一次。该函数会搜索 TIMES 次,但仅对第 TIMES 个匹配应用 FUNCTION。要么全有要么全无当匹配数量不足 TIMES 时,完全不应用 FUNCTION仅返回实际找到的匹配数量
- **START****END** 默认为 OBJECT 的起始和结束位置。
- 返回成功匹配的数量。

View File

@ -170,6 +170,40 @@ Old code silently wrote 10 chars, yielding \"hellohellod\"."
(should (= (tp-search-map #'upcase 'marker nil str) 2))
(should (equal (substring-no-properties str) "HELLO world HELLO"))))
;;; B43: -do shortfall is all-or-nothing on strings and buffers alike
(ert-deftest tp-search-test-forward-do-shortfall-string ()
"Requesting the Nth match when fewer exist applies nothing (string).
The count of available matches is still returned."
(let ((str (copy-sequence "hello world")))
(tp-set 0 5 '(marker t) str)
(should (= (tp-forward-do #'upcase 'marker nil str 3) 1))
(should (equal (substring-no-properties str) "hello world"))))
(ert-deftest tp-search-test-forward-do-shortfall-buffer ()
"Requesting the Nth match when fewer exist applies nothing (buffer)."
(with-temp-buffer
(insert "hello world")
(put-text-property 1 6 'marker t)
(should (= (tp-forward-do #'upcase 'marker t nil 3) 1))
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"hello world"))))
(ert-deftest tp-search-test-backward-do-shortfall-string ()
"tp-backward-do shortfall applies nothing on strings."
(let ((str (copy-sequence "hello world")))
(tp-set 6 11 '(marker t) str)
(should (= (tp-backward-do #'upcase 'marker nil str 2) 1))
(should (equal (substring-no-properties str) "hello world"))))
(ert-deftest tp-search-test-forward-do-exact-count-applies ()
"With exactly TIMES matches, FUNCTION is applied to the TIMES-th."
(let ((str (copy-sequence "aaa bbb aaa")))
(tp-set 0 3 '(marker t) str)
(tp-set 8 11 '(marker t) str)
(should (= (tp-forward-do #'upcase 'marker nil str 2) 2))
(should (equal (substring-no-properties str) "aaa bbb AAA"))))
(ert-deftest tp-search-test-forward-do-buffer-longer-replacement-grows ()
"Buffers may grow on longer replacements (delete-region + insert).
Uses an explicit VALUE: the buffer paths of the -do functions match

View File

@ -377,7 +377,9 @@ VALUE is the optional value to match.
OBJECT can be a buffer or string; nil defaults to current buffer.
START and END define the search range; defaults are object start and end.
Returns the number of successful matches."
FUNCTION is called only when the TIMES-th match exists; if fewer
matches are available, nothing is applied.
Returns the number of matches found (at most TIMES)."
(let ((count (or times 1)))
(cond
;; String object
@ -390,7 +392,10 @@ Returns the number of successful matches."
(<= (cadr m) end-pos)))
all-matches))
(matches (seq-take filtered-matches count)))
(when matches
;; All-or-nothing, mirroring the buffer path: FUNCTION targets
;; the TIMES-th match specifically, so when fewer matches exist
;; acting on a different one would hit the wrong target.
(when (= (length matches) count)
(funcall function (car (last matches)) object))
(length matches)))
;; Buffer or nil
@ -530,7 +535,9 @@ VALUE is the optional value to match.
OBJECT can be a buffer or string; nil defaults to current buffer.
START and END define the search range; defaults are object start and end.
Returns the number of successful matches."
FUNCTION is called only when the TIMES-th match exists; if fewer
matches are available, nothing is applied.
Returns the number of matches found (at most TIMES)."
(let ((count (or times 1)))
(cond
;; String object - reverse the matches
@ -544,7 +551,8 @@ Returns the number of successful matches."
(<= (cadr m) end-pos)))
all-matches))
(matches (seq-take (nreverse filtered-matches) count)))
(when matches
;; All-or-nothing; see tp--forward-do.
(when (= (length matches) count)
(funcall function (car (last matches)) object))
(length matches)))
;; Buffer or nil

View File

@ -718,7 +718,10 @@ leak between tests regardless of how BODY exits."
(should (equal (substring str 12 17) "HELLO")))))
(ert-deftest tp-test-forward-do-on-string-with-range ()
"Test tp-forward-do on string with start/end range."
"Test tp-forward-do on string with start/end range.
TIMES targets the TIMES-th match specifically; with only one match in
range, asking for the 2nd applies nothing (all-or-nothing, matching
the buffer path) and returns the available count."
(let ((str (copy-sequence "hello World hello")))
(tp-set 0 5 '(marker t) str)
(tp-set 12 17 '(marker t) str)
@ -727,8 +730,8 @@ leak between tests regardless of how BODY exits."
(should (= count 1)) ; Only one match in range 6-17
;; First match should NOT be upcased
(should (equal (substring str 0 5) "hello"))
;; Second match should be upcased
(should (equal (substring str 12 17) "HELLO")))))
;; The requested 2nd match does not exist: nothing is applied
(should (equal (substring str 12 17) "hello")))))
(ert-deftest tp-test-forward-do-function-receives-start-end ()
"Test tp-forward-do passes start and end to function."
@ -776,16 +779,17 @@ leak between tests regardless of how BODY exits."
(should (equal (substring str 12 17) "hello")))))
(ert-deftest tp-test-backward-do-on-string-with-range ()
"Test tp-backward-do on string with start/end range."
"Test tp-backward-do on string with start/end range.
All-or-nothing: with one match in range, requesting the 2nd applies
nothing and returns the available count."
(let ((str (copy-sequence "hello World hello")))
(tp-set 0 5 '(marker t) str)
(tp-set 12 17 '(marker t) str)
;; Search only in range 0-10 (before second match)
(let ((count (tp-backward-do #'upcase 'marker nil str 2 0 10)))
(should (= count 1)) ; Only one match in range 0-10
;; First match should be upcased
(should (equal (substring str 0 5) "HELLO"))
;; Second match should NOT be upcased
;; The requested 2nd match does not exist: nothing is applied
(should (equal (substring str 0 5) "hello"))
(should (equal (substring str 12 17) "hello")))))
(ert-deftest tp-test-backward-do-function-receives-start-end ()