Fix tp-search-map to properly copy text properties for strings

store-substring only copies text content, not properties.
Added explicit property copying after store-substring to ensure
both text content and property modifications are applied.

Also updated tp-forward-do and tp-backward-do with the same fix.
Added test for property modification via tp-search-map.

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-12-15 09:42:11 +00:00
parent 798c88e559
commit 2128f78509
2 changed files with 95 additions and 29 deletions

View File

@ -770,6 +770,26 @@
(should (equal (buffer-substring 1 6) "HELLO")) (should (equal (buffer-substring 1 6) "HELLO"))
(should (equal (buffer-substring 13 18) "HELLO"))))) (should (equal (buffer-substring 13 18) "HELLO")))))
(ert-deftest tp-test-search-map-property-modification ()
"Test tp-search-map applies property modifications to matched text."
(let ((str (copy-sequence "hello World hello")))
(tp-set 0 5 '(marker t) str)
(tp-set 12 17 '(marker t) str)
;; First upcase the text
(tp-search-map #'upcase str 'marker)
;; Then add face property
(tp-search-map (lambda (txt)
(tp-add txt 'face '(:background "orange")))
str 'marker)
;; Check text was upcased
(should (equal (substring str 0 5) "HELLO"))
(should (equal (substring str 12 17) "HELLO"))
;; Check face property was added
(let ((props-0 (text-properties-at 0 str))
(props-12 (text-properties-at 12 str)))
(should (equal (plist-get (plist-get props-0 'face) :background) "orange"))
(should (equal (plist-get (plist-get props-12 'face) :background) "orange")))))
;;; ============================================================ ;;; ============================================================
;;; Utility Function Tests ;;; Utility Function Tests
;;; ============================================================ ;;; ============================================================

84
tp.el
View File

@ -1117,17 +1117,29 @@ Example:
(substring obj start end) (substring obj start end)
(buffer-substring start end))) (buffer-substring start end)))
(new-text (funcall function text))) (new-text (funcall function text)))
(when (and new-text (not (equal new-text text))) (when (stringp new-text)
(if (stringp obj) (if (stringp obj)
;; For strings, we need to replace in-place ;; For strings: copy text content and properties separately
;; Note: store-substring has length restrictions (let ((len (min (length new-text) (- end start))))
(progn ;; Copy text content
(store-substring obj start new-text)) (store-substring obj start new-text)
;; Copy properties from new-text to obj
(let ((pos 0))
(while (< pos len)
(let* ((props (text-properties-at pos new-text))
(next-change (or (next-property-change pos new-text) len)))
(when props
(set-text-properties (+ start pos)
(+ start (min next-change len))
props
obj))
(setq pos next-change)))))
;; For buffers, delete and insert ;; For buffers, delete and insert
(unless (equal new-text text)
(save-excursion (save-excursion
(delete-region start end) (delete-region start end)
(goto-char start) (goto-char start)
(insert new-text)))))) (insert new-text)))))))
property value object n)) property value object n))
(defun tp--backward-do (function property &optional value object n) (defun tp--backward-do (function property &optional value object n)
@ -1187,17 +1199,29 @@ Example:
(substring obj start end) (substring obj start end)
(buffer-substring start end))) (buffer-substring start end)))
(new-text (funcall function text))) (new-text (funcall function text)))
(when (and new-text (not (equal new-text text))) (when (stringp new-text)
(if (stringp obj) (if (stringp obj)
;; For strings, we need to replace in-place ;; For strings: copy text content and properties separately
;; Note: store-substring has length restrictions (let ((len (min (length new-text) (- end start))))
(progn ;; Copy text content
(store-substring obj start new-text)) (store-substring obj start new-text)
;; Copy properties from new-text to obj
(let ((pos 0))
(while (< pos len)
(let* ((props (text-properties-at pos new-text))
(next-change (or (next-property-change pos new-text) len)))
(when props
(set-text-properties (+ start pos)
(+ start (min next-change len))
props
obj))
(setq pos next-change)))))
;; For buffers, delete and insert ;; For buffers, delete and insert
(unless (equal new-text text)
(save-excursion (save-excursion
(delete-region start end) (delete-region start end)
(goto-char start) (goto-char start)
(insert new-text)))))) (insert new-text)))))))
property value object n)) property value object n))
(defun tp-search (start-or-string &optional end-or-property property-or-value value object) (defun tp-search (start-or-string &optional end-or-property property-or-value value object)
@ -1315,19 +1339,27 @@ This function supports two calling conventions:
2. Entire string: 2. Entire string:
(tp-search-map FUNCTION STRING PROPERTY &optional VALUE) (tp-search-map FUNCTION STRING PROPERTY &optional VALUE)
FUNCTION receives the matched text as its only argument. The return value FUNCTION receives a copy of the matched text as its only argument.
of FUNCTION replaces the matched text in the string or buffer. FUNCTION can either:
- Return a new/modified string to replace the matched text
- Modify the text properties of the argument and return it
- Return nil to skip replacement
For text content changes, the return value replaces the matched text.
For property-only changes, the properties are merged back to the original.
Returns the number of matches processed. Returns the number of matches processed.
Note: For string objects, the replacement text must have the same length Note: For string objects, replacement text must have the same length
as the original matched text, since strings have fixed length in Emacs. as the original matched text, since strings have fixed length in Emacs.
If the replacement is shorter, only that portion will be replaced. If the replacement is shorter, only that portion will be replaced.
If the replacement is longer, it will be truncated. If the replacement is longer, it will be truncated.
Example: Example:
;; Upcase all matched text ;; Upcase all matched text
(tp-search-map #\\='upcase my-string \\='marker)" (tp-search-map #\\='upcase my-string \\='marker)
;; Add properties to matched text
(tp-search-map (lambda (txt) (tp-add txt \\='face \\='bold)) str \\='marker)"
(let ((obj (cond (let ((obj (cond
((stringp start-or-string) start-or-string) ((stringp start-or-string) start-or-string)
((numberp start-or-string) (or object (current-buffer))) ((numberp start-or-string) (or object (current-buffer)))
@ -1340,15 +1372,29 @@ Example:
(substring obj start end) (substring obj start end)
(buffer-substring start end))) (buffer-substring start end)))
(new-text (funcall function text))) (new-text (funcall function text)))
(when (and new-text (not (equal new-text text))) (when (stringp new-text)
(if (stringp obj) (if (stringp obj)
;; For strings, replace in-place ;; For strings: copy text content and properties separately
(let ((len (min (length new-text) (- end start))))
;; Copy text content
(store-substring obj start new-text) (store-substring obj start new-text)
;; Copy properties from new-text to obj
(let ((pos 0))
(while (< pos len)
(let* ((props (text-properties-at pos new-text))
(next-change (or (next-property-change pos new-text) len)))
(when props
(set-text-properties (+ start pos)
(+ start (min next-change len))
props
obj))
(setq pos next-change)))))
;; For buffers, delete and insert ;; For buffers, delete and insert
(unless (equal new-text text)
(save-excursion (save-excursion
(delete-region start end) (delete-region start end)
(goto-char start) (goto-char start)
(insert new-text)))))) (insert new-text)))))))
start-or-string end-or-property property-or-value value object))) start-or-string end-or-property property-or-value value object)))