Use propertize for non-destructive string operations in tp-set/add/reset/remove

This change makes tp-set, tp-add, tp-reset, and tp-remove return a NEW
propertized string when operating on strings, rather than modifying the
original string in-place. This is more idiomatic for Emacs Lisp and
prevents unintended side effects.

For buffer operations, the existing in-place modification behavior is
preserved.

Key changes:
- Added tp--apply-props-to-string helper function that uses propertize
- Updated tp-set, tp-add, tp-reset to use the new helper for strings
- Updated tp-remove with new helper functions for non-destructive removal
- Updated tp--match-apply and tp--regexp-apply to accumulate changes
- Added tests verifying original strings are not modified
- Updated documentation to reflect the new behavior

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-06 16:05:58 +00:00
parent ba1f3f26c3
commit 70f60f1a25
3 changed files with 388 additions and 82 deletions

View File

@ -880,7 +880,7 @@ OBJECT is a buffer or string; nil means current buffer.
(tp-match-set "TODO" '(face warning))) (tp-match-set "TODO" '(face warning)))
;; => ((1 . 5) (17 . 21)) ;; => ((1 . 5) (17 . 21))
;; On string - returns modified string ;; On string - returns a NEW propertized string (original is not modified)
(tp-match-set "o" '(face bold) "Hello World") (tp-match-set "o" '(face bold) "Hello World")
;; => #("Hello World" 4 5 (face bold) 7 8 (face bold)) ;; => #("Hello World" 4 5 (face bold) 7 8 (face bold))
@ -2357,7 +2357,7 @@ Add or merge properties to specific layers in a region or string.
- **IDX-OR-LAYER-NAME-LIST** is a list of layer indices (integers) or layer names (symbols). For indices: 0 means top layer, -1 means bottom layer. - **IDX-OR-LAYER-NAME-LIST** is a list of layer indices (integers) or layer names (symbols). For indices: 0 means top layer, -1 means bottom layer.
- Properties are deeply merged into the specified layers (nested plists are merged, not replaced). - Properties are deeply merged into the specified layers (nested plists are merged, not replaced).
- OBJECT defaults to current buffer for region form. - OBJECT defaults to current buffer for region form.
- Returns the modified string or nil for buffer operations. - For strings, returns a NEW string (original is not modified). For buffers, returns nil.
**Examples:** **Examples:**
@ -2392,7 +2392,7 @@ Add or merge properties to all layers in a region or string.
- Properties are deeply merged into all existing layers. - Properties are deeply merged into all existing layers.
- OBJECT defaults to current buffer for region form. - OBJECT defaults to current buffer for region form.
- Returns the modified string or nil for buffer operations. - For strings, returns a NEW string (original is not modified). For buffers, returns nil.
**Examples:** **Examples:**

View File

@ -4041,5 +4041,95 @@ Regression test for: (tp-set \"emacs\" 'face nil) erroring with
(tp-set 1 6 '(face nil)) (tp-set 1 6 '(face nil))
(should (eq (tp-at 1 'face) nil)))) (should (eq (tp-at 1 'face) nil))))
;;; ============================================================
;;; Non-Destructive String Modification Tests
;;; ============================================================
(ert-deftest tp-test-set-does-not-modify-original-string ()
"Test that tp-set returns a new string and does not modify the original."
(let ((original "Hello"))
(let ((result (tp-set original 'face 'bold)))
;; Result should be a new string with properties
(should (stringp result))
(should (eq (get-text-property 0 'face result) 'bold))
;; Original should NOT be modified (no properties)
(should (null (get-text-property 0 'face original)))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(ert-deftest tp-test-reset-does-not-modify-original-string ()
"Test that tp-reset returns a new string and does not modify the original."
(let ((original "Hello"))
(let ((result (tp-reset original 'face 'bold)))
;; Result should be a new string with properties
(should (stringp result))
(should (eq (get-text-property 0 'face result) 'bold))
;; Original should NOT be modified (no properties)
(should (null (get-text-property 0 'face original)))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(ert-deftest tp-test-add-does-not-modify-original-string ()
"Test that tp-add returns a new string and does not modify the original."
(let ((original "Hello"))
(let ((result (tp-add original 'face 'bold)))
;; Result should be a new string with properties
(should (stringp result))
(should (eq (get-text-property 0 'face result) 'bold))
;; Original should NOT be modified (no properties)
(should (null (get-text-property 0 'face original)))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(ert-deftest tp-test-remove-does-not-modify-original-string ()
"Test that tp-remove returns a new string and does not modify the original."
;; First create a propertized string (using propertize to create the original)
(let ((original (propertize "Hello" 'face 'bold 'help-echo "tip")))
(let ((result (tp-remove original 'face)))
;; Result should be a new string without face property
(should (stringp result))
(should (null (get-text-property 0 'face result)))
(should (equal (get-text-property 0 'help-echo result) "tip"))
;; Original should still have face property
(should (eq (get-text-property 0 'face original) 'bold))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(ert-deftest tp-test-set-region-does-not-modify-original-string ()
"Test that tp-set with region returns a new string and does not modify the original."
(let ((original "Hello World"))
(let ((result (tp-set 0 5 '(face bold) original)))
;; Result should be a new string with properties on the region
(should (stringp result))
(should (eq (get-text-property 0 'face result) 'bold))
;; Original should NOT be modified (no properties)
(should (null (get-text-property 0 'face original)))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(ert-deftest tp-test-match-set-does-not-modify-original-string ()
"Test that tp-match-set returns a new string and does not modify the original."
(let ((original "Hello World"))
(let ((result (tp-match-set "Hello" '(face bold) original)))
;; Result should be a new string with properties
(should (stringp result))
(should (eq (get-text-property 0 'face result) 'bold))
;; Original should NOT be modified (no properties)
(should (null (get-text-property 0 'face original)))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(ert-deftest tp-test-regexp-set-does-not-modify-original-string ()
"Test that tp-regexp-set returns a new string and does not modify the original."
(let ((original "abc 123 def"))
(let ((result (tp-regexp-set "[0-9]+" '(face bold) original)))
;; Result should be a new string with properties on the match
(should (stringp result))
(should (eq (get-text-property 4 'face result) 'bold))
;; Original should NOT be modified (no properties)
(should (null (get-text-property 4 'face original)))
;; Strings should not be eq (different objects)
(should (not (eq original result))))))
(provide 'tp-ert-tests) (provide 'tp-ert-tests)
;;; tp-ert-tests.el ends here ;;; tp-ert-tests.el ends here

328
tp.el
View File

@ -1235,6 +1235,60 @@ Supports multiple calling conventions:
(setq props (or (tp--resolve-props props) props))) (setq props (or (tp--resolve-props props) props)))
(list object start finish props))) (list object start finish props)))
(defun tp--apply-props-to-string (str start end props &optional merge-mode)
"Apply PROPS to string STR from START to END, returning a NEW string.
This function does not modify the original string.
MERGE-MODE controls how properties are applied:
nil or :set - Set properties, preserving existing unspecified ones
:reset - Completely replace all properties
:add - Merge properties deeply (for face, prepend symbols)
Returns a new propertized string."
(let* ((len (length str))
;; Ensure bounds are valid
(start (max 0 start))
(end (min end len))
;; Build the result string piece by piece
(before (when (> start 0)
(substring str 0 start)))
(middle-text (substring-no-properties str start end))
(after (when (< end len)
(substring str end len)))
;; Get existing properties for the middle section
(existing-props (when (not (eq merge-mode :reset))
(text-properties-at start str)))
;; Calculate final properties for the middle section
(final-props
(cond
;; :reset - use only new props
((eq merge-mode :reset)
props)
;; :add - deep merge with face prepending
((eq merge-mode :add)
(let ((result existing-props))
(cl-loop
for (key val) on props by #'cddr
do (let* ((current-val (plist-get result key))
(new-val (cond
((eq key 'face) (tp--prepend-face val current-val))
((and (listp val) (keywordp (car-safe val))
(listp current-val) (keywordp (car-safe current-val)))
(tp--deep-merge-plist current-val val))
(t val))))
(setq result (plist-put result key new-val))))
result))
;; nil/:set - set properties, preserving unspecified ones
(t
(let ((result (copy-sequence existing-props)))
(cl-loop for (key val) on props by #'cddr
do (setq result (plist-put result key val)))
result))))
;; Create the middle section with properties using propertize
(middle-propertized (apply #'propertize middle-text final-props)))
;; Concatenate the parts
(concat before middle-propertized after)))
;;;============================================================================ ;;;============================================================================
;;; Layer 2: Core Property Functions - Set/Reset/Add ;;; Layer 2: Core Property Functions - Set/Reset/Add
;;;============================================================================ ;;;============================================================================
@ -1250,7 +1304,9 @@ Supports four calling conventions:
PROPS can be a plist or a layer/group name symbol. PROPS can be a plist or a layer/group name symbol.
Preserves existing properties not specified in PROPS. Preserves existing properties not specified in PROPS.
For tp-text, props override embedded text properties. For tp-text, props override embedded text properties.
Returns modified string or (START . END) cons for buffer."
For strings, returns a NEW propertized string (original is not modified).
For buffers, returns (START . END) cons."
(pcase-let ((`(,object ,start ,finish ,props) (pcase-let ((`(,object ,start ,finish ,props)
(tp--parse-args start-or-string end-or-prop props-or-val rest))) (tp--parse-args start-or-string end-or-prop props-or-val rest)))
;; Handle tp-text property specially - :override means props override embedded props ;; Handle tp-text property specially - :override means props override embedded props
@ -1259,25 +1315,25 @@ Returns modified string or (START . END) cons for buffer."
(setq props new-props finish new-finish object new-object) (setq props new-props finish new-finish object new-object)
(when (and (stringp object) (plist-member props 'tp-text)) (when (and (stringp object) (plist-member props 'tp-text))
(setq start 0))) (setq start 0)))
;; Check if we have any existing properties in the range (if (stringp object)
;; For strings: create a new propertized string (non-destructive)
(tp--apply-props-to-string object start finish props nil)
;; For buffers: modify in place (standard behavior)
(let ((has-existing-props (text-properties-at start object))) (let ((has-existing-props (text-properties-at start object)))
(if (and (not has-existing-props) (if (and (not has-existing-props)
;; Also check if this is a uniform range (no intervals) (= start (or (next-single-property-change start nil object finish) finish)))
(or (stringp object)
(= start (or (next-single-property-change start nil object finish) finish))))
;; No existing properties - can use set-text-properties to preserve duplicate keys
(set-text-properties start finish props object) (set-text-properties start finish props object)
;; Has existing properties - use put-text-property for proper interval handling
;; This may lose duplicate keys but correctly handles overlapping regions
(cl-loop for (key val) on props by #'cddr (cl-loop for (key val) on props by #'cddr
do (put-text-property start finish key val object)))) do (put-text-property start finish key val object))))
(if (stringp object) object (cons start finish)))) (cons start finish))))
(defun tp-reset (start-or-string &optional end-or-prop props-or-val &rest rest) (defun tp-reset (start-or-string &optional end-or-prop props-or-val &rest rest)
"Completely replace all text properties with PROPS. "Completely replace all text properties with PROPS.
Like `tp-set' but replaces ALL existing properties. Like `tp-set' but replaces ALL existing properties.
For tp-text, embedded text properties are ignored - only props are used. For tp-text, embedded text properties are ignored - only props are used.
Returns modified string or (START . END) cons for buffer."
For strings, returns a NEW propertized string (original is not modified).
For buffers, returns (START . END) cons."
(pcase-let ((`(,object ,start ,finish ,props) (pcase-let ((`(,object ,start ,finish ,props)
(tp--parse-args start-or-string end-or-prop props-or-val rest))) (tp--parse-args start-or-string end-or-prop props-or-val rest)))
;; Handle tp-text property - :reset means only use props, ignore embedded props ;; Handle tp-text property - :reset means only use props, ignore embedded props
@ -1286,9 +1342,12 @@ Returns modified string or (START . END) cons for buffer."
(setq props new-props finish new-finish object new-object) (setq props new-props finish new-finish object new-object)
(when (and (stringp object) (plist-member props 'tp-text)) (when (and (stringp object) (plist-member props 'tp-text))
(setq start 0))) (setq start 0)))
;; Completely replace all properties (if (stringp object)
;; For strings: create a new propertized string (non-destructive)
(tp--apply-props-to-string object start finish props :reset)
;; For buffers: modify in place (standard behavior)
(set-text-properties start finish props object) (set-text-properties start finish props object)
(if (stringp object) object (cons start finish)))) (cons start finish))))
(defun tp--prepend-face (new-face existing-face) (defun tp--prepend-face (new-face existing-face)
"Prepend NEW-FACE to EXISTING-FACE for the face property. "Prepend NEW-FACE to EXISTING-FACE for the face property.
@ -1378,7 +1437,9 @@ Duplicate faces are not added."
Unlike `tp-set', deeply merges nested properties. Unlike `tp-set', deeply merges nested properties.
For `face' property, symbol faces are prepended to existing face list. For `face' property, symbol faces are prepended to existing face list.
For tp-text, embedded text properties are merged with props. For tp-text, embedded text properties are merged with props.
Returns modified string or (START . END) cons for buffer."
For strings, returns a NEW propertized string (original is not modified).
For buffers, returns (START . END) cons."
(pcase-let ((`(,object ,start ,finish ,props) (pcase-let ((`(,object ,start ,finish ,props)
(tp--parse-args start-or-string end-or-prop props-or-val rest))) (tp--parse-args start-or-string end-or-prop props-or-val rest)))
;; Handle tp-text property - :merge means embedded props are merged with props ;; Handle tp-text property - :merge means embedded props are merged with props
@ -1388,10 +1449,14 @@ Returns modified string or (START . END) cons for buffer."
(setq props new-props finish new-finish object new-object) (setq props new-props finish new-finish object new-object)
(when (and (stringp object) has-tp-text) (when (and (stringp object) has-tp-text)
(setq start 0)))) (setq start 0))))
;; For strings with tp-text, properties are already merged - just apply them (if (stringp object)
;; For other cases, process each property with deep merging ;; For strings: create a new propertized string (non-destructive)
(if (and (stringp object) (plist-member props 'tp-text)) (if (plist-member props 'tp-text)
(set-text-properties start finish props object) ;; For tp-text, properties are already merged
(tp--apply-props-to-string object start finish props :reset)
;; Otherwise use :add mode for deep merging
(tp--apply-props-to-string object start finish props :add))
;; For buffers: modify in place with deep merging
(let ((pos start)) (let ((pos start))
(while (< pos finish) (while (< pos finish)
(let* ((current-props (text-properties-at pos object)) (let* ((current-props (text-properties-at pos object))
@ -1406,8 +1471,8 @@ Returns modified string or (START . END) cons for buffer."
(tp--deep-merge-plist current-val val)) (tp--deep-merge-plist current-val val))
(t val)))) (t val))))
(put-text-property pos next-pos key new-val object))) (put-text-property pos next-pos key new-val object)))
(setq pos next-pos))))) (setq pos next-pos))))
(if (stringp object) object (cons start finish)))) (cons start finish))))
;;;============================================================================ ;;;============================================================================
;;; Layer 2: Core Property Functions - Get/At ;;; Layer 2: Core Property Functions - Get/At
@ -1705,39 +1770,39 @@ This function supports multiple calling conventions:
(tp-remove STRING PROPERTY SUB-KEY \\='(NESTED-KEYS...)) (tp-remove STRING PROPERTY SUB-KEY \\='(NESTED-KEYS...))
(tp-remove \"Hello\" \\='face :underline \\='(:style :position)) (tp-remove \"Hello\" \\='face :underline \\='(:style :position))
Returns the modified string for string input, or nil for buffer operations." For strings, returns a NEW string with properties removed (original is not modified).
For buffers, returns nil."
(cond (cond
;; First arg is a string - apply to entire string ;; First arg is a string - apply to entire string, non-destructively
((stringp start-or-string) ((stringp start-or-string)
(let ((str start-or-string) (let* ((str start-or-string)
(start 0) (start 0)
(end (length start-or-string))) (end (length str)))
(cond (cond
;; (tp-remove str 'face :underline '(:style :position)) - nested sub-property removal with list ;; (tp-remove str 'face :underline '(:style :position)) - nested sub-property removal with list
((and (symbolp end-or-prop) ((and (symbolp end-or-prop)
(keywordp prop-or-sub) (keywordp prop-or-sub)
rest rest
(listp (car rest))) (listp (car rest)))
(tp--remove-property start end (list end-or-prop prop-or-sub (car rest)) str)) (tp--remove-property-from-string str start end (list end-or-prop prop-or-sub (car rest))))
;; (tp-remove str 'face :underline :position :style ...) - nested sub-property removal with keywords ;; (tp-remove str 'face :underline :position :style ...) - nested sub-property removal with keywords
((and (symbolp end-or-prop) ((and (symbolp end-or-prop)
(keywordp prop-or-sub) (keywordp prop-or-sub)
rest rest
(keywordp (car rest))) (keywordp (car rest)))
(tp--remove-property start end (list end-or-prop prop-or-sub rest) str)) (tp--remove-property-from-string str start end (list end-or-prop prop-or-sub rest)))
;; (tp-remove str 'face :underline) - sub-property removal ;; (tp-remove str 'face :underline) - sub-property removal
((and (symbolp end-or-prop) (keywordp prop-or-sub)) ((and (symbolp end-or-prop) (keywordp prop-or-sub))
(tp--remove-sub start end end-or-prop prop-or-sub str)) (tp--remove-sub-from-string str start end end-or-prop prop-or-sub))
;; (tp-remove str 'face 'help-echo ...) - multiple properties ;; (tp-remove str 'face 'help-echo ...) - multiple properties
((symbolp end-or-prop) ((symbolp end-or-prop)
(let ((props (cons end-or-prop (cons prop-or-sub rest)))) (let ((props-to-remove (cl-remove-if-not #'symbolp
;; Filter to only include valid property symbols (not nil) (cons end-or-prop (cons prop-or-sub rest)))))
(dolist (prop (cl-remove-if-not #'symbolp props)) (tp--remove-props-from-string str start end props-to-remove)))
(remove-text-properties start end (list prop nil) str))))
;; (tp-remove str '(face :underline)) - nested property spec ;; (tp-remove str '(face :underline)) - nested property spec
((listp end-or-prop) ((listp end-or-prop)
(tp--remove-property start end end-or-prop str))) (tp--remove-property-from-string str start end end-or-prop))
str)) (t str))))
;; First arg is a number - buffer region ;; First arg is a number - buffer region
((numberp start-or-string) ((numberp start-or-string)
(let* ((start start-or-string) (let* ((start start-or-string)
@ -1748,6 +1813,127 @@ Returns the modified string for string input, or nil for buffer operations."
nil)) nil))
(t (error "Invalid arguments to tp-remove")))) (t (error "Invalid arguments to tp-remove"))))
(defun tp--remove-props-from-string (str start end props-to-remove)
"Create a new string from STR with PROPS-TO-REMOVE removed from START to END.
Returns a new string (original is not modified)."
(let* ((len (length str))
(start (max 0 start))
(end (min end len))
(before (when (> start 0)
(substring str 0 start)))
(middle-text (substring-no-properties str start end))
(after (when (< end len)
(substring str end len)))
;; Get existing properties and remove the specified ones
(existing-props (text-properties-at start str))
(final-props (let ((result nil))
(cl-loop for (key val) on existing-props by #'cddr
unless (memq key props-to-remove)
do (setq result (plist-put result key val)))
result))
(middle-propertized (if final-props
(apply #'propertize middle-text final-props)
middle-text)))
(concat before middle-propertized after)))
(defun tp--remove-sub-from-string (str start end property sub-key)
"Create a new string from STR with SUB-KEY removed from PROPERTY.
Returns a new string (original is not modified)."
(let* ((len (length str))
(start (max 0 start))
(end (min end len))
(before (when (> start 0)
(substring str 0 start)))
(middle-text (substring-no-properties str start end))
(after (when (< end len)
(substring str end len)))
;; Get existing properties and modify the face
(existing-props (text-properties-at start str))
(prop-value (plist-get existing-props property))
(new-value (when (and prop-value (listp prop-value) (keywordp (car-safe prop-value)))
(let ((result nil))
(cl-loop for (k v) on prop-value by #'cddr
unless (eq k sub-key)
do (setq result (plist-put result k v)))
result)))
(final-props (let ((result nil))
(cl-loop for (key val) on existing-props by #'cddr
do (setq result (plist-put result key
(if (eq key property)
new-value
val))))
result))
(middle-propertized (if final-props
(apply #'propertize middle-text final-props)
middle-text)))
(concat before middle-propertized after)))
(defun tp--remove-property-from-string (str start end property-spec)
"Create a new string from STR with PROPERTY-SPEC removed from START to END.
PROPERTY-SPEC can be a symbol or a nested spec like (PROPERTY SUB-KEY ...).
Returns a new string (original is not modified)."
(cond
((symbolp property-spec)
(tp--remove-props-from-string str start end (list property-spec)))
((listp property-spec)
(let ((property (car property-spec))
(sub-key (cadr property-spec))
(nested-keys (caddr property-spec)))
(cond
;; Nested sub-property removal
((and sub-key nested-keys)
;; For complex nested removal, we need to handle this specially
(let* ((len (length str))
(start (max 0 start))
(end (min end len))
(before (when (> start 0)
(substring str 0 start)))
(middle-text (substring-no-properties str start end))
(after (when (< end len)
(substring str end len)))
(existing-props (text-properties-at start str))
(prop-value (plist-get existing-props property))
(new-value (when (and prop-value (listp prop-value))
(tp--remove-nested-keys prop-value sub-key nested-keys)))
(final-props (let ((result nil))
(cl-loop for (key val) on existing-props by #'cddr
do (setq result (plist-put result key
(if (eq key property)
new-value
val))))
result))
(middle-propertized (if final-props
(apply #'propertize middle-text final-props)
middle-text)))
(concat before middle-propertized after)))
;; Simple sub-property removal
(sub-key
(tp--remove-sub-from-string str start end property sub-key))
;; Just a property name
(t
(tp--remove-props-from-string str start end (list property))))))
(t str)))
(defun tp--remove-nested-keys (plist sub-key nested-keys)
"Remove NESTED-KEYS from the SUB-KEY value within PLIST.
Returns the modified plist."
(let* ((sub-value (plist-get plist sub-key))
(keys-to-remove (if (listp nested-keys) nested-keys (list nested-keys)))
(new-sub-value (when (and sub-value (listp sub-value))
(let ((result nil))
(cl-loop for (k v) on sub-value by #'cddr
unless (memq k keys-to-remove)
do (setq result (plist-put result k v)))
result))))
(if new-sub-value
(plist-put plist sub-key new-sub-value)
;; Remove the sub-key entirely if no value left
(let ((result nil))
(cl-loop for (k v) on plist by #'cddr
unless (eq k sub-key)
do (setq result (plist-put result k v)))
result))))
;;;###autoload ;;;###autoload
(defun tp-clear (&optional start end object) (defun tp-clear (&optional start end object)
"Clear all text properties from START to END in OBJECT. "Clear all text properties from START to END in OBJECT.
@ -1762,18 +1948,22 @@ If START and END are not provided, clear the entire buffer."
;;;============================================================================ ;;;============================================================================
(defun tp--match-apply-single (pattern properties apply-fn object) (defun tp--match-apply-single (pattern properties apply-fn object)
"Apply APPLY-FN to matches of single PATTERN in OBJECT." "Apply APPLY-FN to matches of single PATTERN in OBJECT.
For strings, returns a new string with properties applied (non-destructive).
For buffers, modifies in-place and returns list of regions."
(cond (cond
;; String object ;; String object
((stringp object) ((stringp object)
(let ((pos 0)) (let ((result object)
(while (string-match (regexp-quote pattern) object pos) (pos 0)
(offset 0)) ; Track offset for position changes (though properties shouldn't change length)
(while (string-match (regexp-quote pattern) result pos)
(let ((beg (match-beginning 0)) (let ((beg (match-beginning 0))
(end (match-end 0))) (end (match-end 0)))
(when properties (when properties
(funcall apply-fn beg end properties object)) (setq result (funcall apply-fn beg end properties result)))
(setq pos (if (= beg end) (1+ beg) end)))) (setq pos (if (= beg end) (1+ beg) end))))
object)) result))
;; Buffer or nil (current buffer) ;; Buffer or nil (current buffer)
(t (t
(let ((buf (or object (current-buffer)))) (let ((buf (or object (current-buffer))))
@ -1794,14 +1984,16 @@ If START and END are not provided, clear the entire buffer."
PATTERN can be a string or a list of strings (multiple patterns). PATTERN can be a string or a list of strings (multiple patterns).
When PATTERN is a list, each element is a pattern to match. When PATTERN is a list, each element is a pattern to match.
APPLY-FN is called with (START END PROPS OBJECT) for each match. APPLY-FN is called with (START END PROPS OBJECT) for each match.
Returns modified object or list of regions." For strings, returns a NEW string with properties applied (non-destructive).
For buffers, returns list of regions."
(let ((patterns (if (listp pattern) pattern (list pattern)))) (let ((patterns (if (listp pattern) pattern (list pattern))))
(cond (cond
;; String object ;; String object
((stringp object) ((stringp object)
(let ((result object))
(dolist (p patterns) (dolist (p patterns)
(tp--match-apply-single p properties apply-fn object)) (setq result (tp--match-apply-single p properties apply-fn result)))
object) result))
;; Buffer or nil (current buffer) ;; Buffer or nil (current buffer)
(t (t
(let ((all-regions nil)) (let ((all-regions nil))
@ -1813,18 +2005,20 @@ Returns modified object or list of regions."
(defun tp--regexp-apply-single (pattern properties apply-fn object) (defun tp--regexp-apply-single (pattern properties apply-fn object)
"Apply APPLY-FN to regexp matches of single PATTERN in OBJECT. "Apply APPLY-FN to regexp matches of single PATTERN in OBJECT.
APPLY-FN is called with (START END PROPS OBJECT) for each match. APPLY-FN is called with (START END PROPS OBJECT) for each match.
Returns modified object or list of regions." For strings, returns a NEW string with properties applied (non-destructive).
For buffers, modifies in-place and returns list of regions."
(cond (cond
;; String object ;; String object
((stringp object) ((stringp object)
(let ((pos 0)) (let ((result object)
(while (string-match pattern object pos) (pos 0))
(while (string-match pattern result pos)
(let ((beg (match-beginning 0)) (let ((beg (match-beginning 0))
(end (match-end 0))) (end (match-end 0)))
(when properties (when properties
(funcall apply-fn beg end properties object)) (setq result (funcall apply-fn beg end properties result)))
(setq pos (if (= beg end) (1+ beg) end)))) (setq pos (if (= beg end) (1+ beg) end))))
object)) result))
;; Buffer or nil (current buffer) ;; Buffer or nil (current buffer)
(t (t
(let ((buf (or object (current-buffer)))) (let ((buf (or object (current-buffer))))
@ -1845,14 +2039,16 @@ Returns modified object or list of regions."
PATTERN can be a string (single regexp) or a list of strings (multiple regexps). PATTERN can be a string (single regexp) or a list of strings (multiple regexps).
When PATTERN is a list, each element is a regexp to match. When PATTERN is a list, each element is a regexp to match.
APPLY-FN is called with (START END PROPS OBJECT) for each match. APPLY-FN is called with (START END PROPS OBJECT) for each match.
Returns modified object or list of regions." For strings, returns a NEW string with properties applied (non-destructive).
For buffers, returns list of regions."
(let ((patterns (if (listp pattern) pattern (list pattern)))) (let ((patterns (if (listp pattern) pattern (list pattern))))
(cond (cond
;; String object ;; String object
((stringp object) ((stringp object)
(let ((result object))
(dolist (p patterns) (dolist (p patterns)
(tp--regexp-apply-single p properties apply-fn object)) (setq result (tp--regexp-apply-single p properties apply-fn result)))
object) result))
;; Buffer or nil (current buffer) ;; Buffer or nil (current buffer)
(t (t
(let ((all-regions nil)) (let ((all-regions nil))
@ -1863,7 +2059,13 @@ Returns modified object or list of regions."
(defun tp--deep-merge-apply (start end props obj) (defun tp--deep-merge-apply (start end props obj)
"Apply PROPS to OBJ from START to END with deep merge. "Apply PROPS to OBJ from START to END with deep merge.
Merges nested plists instead of replacing them." Merges nested plists instead of replacing them.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place."
(if (stringp obj)
;; For strings: create a new propertized string using tp--apply-props-to-string with :add mode
(tp--apply-props-to-string obj start end props :add)
;; For buffers: modify in-place
(let ((pos start)) (let ((pos start))
(while (< pos end) (while (< pos end)
(let* ((current-props (text-properties-at pos obj)) (let* ((current-props (text-properties-at pos obj))
@ -1878,7 +2080,8 @@ Merges nested plists instead of replacing them."
(tp--deep-merge-plist current-val val)) (tp--deep-merge-plist current-val val))
(t val)))) (t val))))
(put-text-property pos next-pos key new-val obj))) (put-text-property pos next-pos key new-val obj)))
(setq pos next-pos))))) (setq pos next-pos))))
obj))
(defun tp-match-set (pattern plist &optional object) (defun tp-match-set (pattern plist &optional object)
"Set properties on all occurrences of PATTERN. "Set properties on all occurrences of PATTERN.
@ -1908,12 +2111,23 @@ or a symbol representing a layer/group name defined by `define-tp'
or `define-tp-group'. or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer. OBJECT is a buffer or string; nil means current buffer.
Unlike `tp-match-set', this completely replaces all existing properties." Unlike `tp-match-set', this completely replaces all existing properties.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place and returns list of regions."
(tp--match-apply pattern (tp--ensure-props plist) (tp--match-apply pattern (tp--ensure-props plist)
(lambda (start end props obj) #'tp--reset-apply
(set-text-properties start end props obj))
object)) object))
(defun tp--reset-apply (start end props obj)
"Apply PROPS to OBJ from START to END, completely replacing existing properties.
For strings, returns a NEW string.
For buffers, modifies in-place."
(if (stringp obj)
(tp--apply-props-to-string obj start end props :reset)
(set-text-properties start end props obj)
obj))
(defun tp-match-add (pattern plist &optional object) (defun tp-match-add (pattern plist &optional object)
"Add/update properties on all occurrences of PATTERN. "Add/update properties on all occurrences of PATTERN.
@ -1956,10 +2170,12 @@ or a symbol representing a layer/group name defined by `define-tp'
or `define-tp-group'. or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer. OBJECT is a buffer or string; nil means current buffer.
Unlike `tp-regexp-set', this completely replaces all existing properties." Unlike `tp-regexp-set', this completely replaces all existing properties.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place and returns list of regions."
(tp--regexp-apply pattern (tp--ensure-props plist) (tp--regexp-apply pattern (tp--ensure-props plist)
(lambda (start end props obj) #'tp--reset-apply
(set-text-properties start end props obj))
object)) object))
(defun tp-regexp-add (pattern plist &optional object) (defun tp-regexp-add (pattern plist &optional object)