From ecdd952b559b9cda13fbcf7735f7b0a4d192ad07 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Sun, 26 Jul 2026 21:16:56 +0800 Subject: [PATCH] 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 --- CHANGELOG.md | 16 +++++++--------- README.md | 2 +- README_CN.md | 2 +- tp-search-tests.el | 34 ++++++++++++++++++++++++++++++++++ tp-search.el | 16 ++++++++++++---- tp-tests.el | 18 +++++++++++------- 6 files changed, 66 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 287b068..d903d10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 0a4bfe2..13a4259 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README_CN.md b/README_CN.md index a43d610..7f40035 100644 --- a/README_CN.md +++ b/README_CN.md @@ -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 的起始和结束位置。 - 返回成功匹配的数量。 diff --git a/tp-search-tests.el b/tp-search-tests.el index 611ef73..c51c2ad 100644 --- a/tp-search-tests.el +++ b/tp-search-tests.el @@ -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 diff --git a/tp-search.el b/tp-search.el index 5f28951..f83b2cc 100644 --- a/tp-search.el +++ b/tp-search.el @@ -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 diff --git a/tp-tests.el b/tp-tests.el index da2e23c..a021dee 100644 --- a/tp-tests.el +++ b/tp-tests.el @@ -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 ()