Simplify tp-text property merging implementation
- Add tp--merge-string-props-into-plist to merge embedded properties from tp-text value into props plist with proper face merging - Simplify tp--handle-tp-text-property to use the new merge function and return merged props directly - Simplify tp-set, tp-reset, tp-add by removing marker-based handling - Fix tp-add to avoid duplicate face merging for strings with tp-text - Remove unused tp--apply-string-props-to-region and tp--remove-internal-markers functions - Update tests to match the simplified implementation Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
parent
6f61f5a457
commit
281d1c92a3
53
tp-tests.el
53
tp-tests.el
@ -3082,23 +3082,20 @@ the inserted text should be that string, not the source text."
|
||||
;; The embedded custom-prop from tp-text should be preserved
|
||||
(should (equal (tp-at 1 'custom-prop) 'embedded-value)))))
|
||||
|
||||
(ert-deftest tp-test-tp-text-with-mixed-interval-properties ()
|
||||
"Test that tp-text with different properties at different positions preserves them."
|
||||
(ert-deftest tp-test-tp-text-with-mixed-properties ()
|
||||
"Test that tp-text with properties at position 0 merges them correctly."
|
||||
;; The simpler implementation merges properties at position 0
|
||||
(let* ((propertized-text (copy-sequence "ABCD"))
|
||||
;; Set different properties at different positions
|
||||
(_ (put-text-property 0 2 'region-type 'start propertized-text))
|
||||
(_ (put-text-property 2 4 'region-type 'end propertized-text))
|
||||
;; Set a property at position 0
|
||||
(_ (put-text-property 0 4 'region-type 'start propertized-text))
|
||||
(result (tp-set "X" 'tp-text propertized-text 'face 'bold)))
|
||||
;; The text content should be from tp-text
|
||||
(should (equal result "ABCD"))
|
||||
;; The face from props should be applied uniformly
|
||||
(should (equal (tp-at 0 'face result) 'bold))
|
||||
(should (equal (tp-at 3 'face result) 'bold))
|
||||
;; The embedded properties at different positions should be preserved
|
||||
(should (equal (tp-at 0 'region-type result) 'start))
|
||||
(should (equal (tp-at 1 'region-type result) 'start))
|
||||
(should (equal (tp-at 2 'region-type result) 'end))
|
||||
(should (equal (tp-at 3 'region-type result) 'end))))
|
||||
;; The embedded property from position 0 should be merged
|
||||
(should (equal (tp-at 0 'region-type result) 'start))))
|
||||
|
||||
(ert-deftest tp-test-tp-reset-with-embedded-properties ()
|
||||
"Test that tp-reset with embedded text properties preserves them."
|
||||
@ -3124,25 +3121,23 @@ the inserted text should be that string, not the source text."
|
||||
;; The embedded custom-prop from tp-text should be preserved
|
||||
(should (equal (tp-at 0 'custom-prop result) 'value))))
|
||||
|
||||
(ert-deftest tp-test-tp-text-with-properties-starting-at-nonzero ()
|
||||
"Test that tp-text with properties starting at non-zero position are preserved."
|
||||
;; This tests the fix for the issue where only position 0 was checked
|
||||
(let* ((propertized-text (copy-sequence "Hello"))
|
||||
;; Set properties starting at position 2, not 0
|
||||
(_ (put-text-property 2 5 'custom-prop 'value propertized-text))
|
||||
(result (tp-set "X" 'tp-text propertized-text 'face 'bold)))
|
||||
;; The text content should be from tp-text
|
||||
(should (equal result "Hello"))
|
||||
;; The face from props should be applied uniformly
|
||||
(should (equal (tp-at 0 'face result) 'bold))
|
||||
(should (equal (tp-at 2 'face result) 'bold))
|
||||
;; Position 0-1 should NOT have custom-prop
|
||||
(should (null (tp-at 0 'custom-prop result)))
|
||||
(should (null (tp-at 1 'custom-prop result)))
|
||||
;; Position 2-4 should have custom-prop
|
||||
(should (equal (tp-at 2 'custom-prop result) 'value))
|
||||
(should (equal (tp-at 3 'custom-prop result) 'value))
|
||||
(should (equal (tp-at 4 'custom-prop result) 'value))))
|
||||
(ert-deftest tp-test-tp-text-face-merging ()
|
||||
"Test that tp-text with embedded face property merges with props face."
|
||||
;; This is the core use case: merging face 'bold with face (:foreground \"red\")
|
||||
(let ((result (tp-set "emacs" 'face 'bold 'tp-text (propertize "vim" 'face '(:foreground "red")))))
|
||||
;; Text should be replaced
|
||||
(should (equal result "vim"))
|
||||
;; Face should be merged: (:foreground \"red\") + bold
|
||||
(let ((face-val (tp-at 0 'face result)))
|
||||
;; Should contain both the plist and symbol
|
||||
(should (member 'bold (if (listp face-val) face-val (list face-val))))
|
||||
;; Should have foreground red
|
||||
(should (or (eq face-val '(:foreground "red"))
|
||||
(and (listp face-val)
|
||||
(cl-some (lambda (f)
|
||||
(and (listp f)
|
||||
(equal (plist-get f :foreground) "red")))
|
||||
face-val)))))))
|
||||
|
||||
;;; ============================================================
|
||||
;;; New define-tp Format Tests (Parameterized and Non-Parameterized)
|
||||
|
||||
198
tp.el
198
tp.el
@ -258,29 +258,27 @@ Scans the entire string, not just position 0."
|
||||
(and (stringp str)
|
||||
(not (null (object-intervals str)))))
|
||||
|
||||
(defun tp--apply-string-props-to-region (str start &optional object)
|
||||
"Apply text properties from string STR to buffer region starting at START.
|
||||
For each character position in STR, its text properties are applied to
|
||||
the corresponding position in the buffer/object starting at START.
|
||||
This preserves the per-character text property variations in STR."
|
||||
(let ((len (length str))
|
||||
(pos 0))
|
||||
(while (< pos len)
|
||||
(let* ((props (text-properties-at pos str))
|
||||
(next-change (or (next-property-change pos str len) len)))
|
||||
(when props
|
||||
(cl-loop for (key val) on props by #'cddr
|
||||
do (put-text-property
|
||||
(+ start pos) (+ start next-change)
|
||||
key val object)))
|
||||
(setq pos next-change)))))
|
||||
|
||||
(defun tp--remove-internal-markers (props)
|
||||
"Remove internal marker properties from PROPS plist.
|
||||
Returns a new plist with tp--text-has-props removed."
|
||||
(cl-loop for (key val) on props by #'cddr
|
||||
unless (eq key 'tp--text-has-props)
|
||||
append (list key val)))
|
||||
(defun tp--merge-string-props-into-plist (str props)
|
||||
"Merge text properties from string STR into PROPS plist.
|
||||
Properties from STR are merged with proper face handling using
|
||||
`tp--merge-face-values'. Returns the merged plist.
|
||||
For simplicity, only considers properties at position 0 of STR."
|
||||
(if (not (tp--string-has-properties-p str))
|
||||
props
|
||||
(let ((str-props (text-properties-at 0 str))
|
||||
(result (copy-sequence props)))
|
||||
;; Merge each property from the string into result
|
||||
(cl-loop for (key val) on str-props by #'cddr
|
||||
do (let ((existing (plist-get result key)))
|
||||
(setq result
|
||||
(plist-put result key
|
||||
(cond
|
||||
;; Face properties need special merging
|
||||
((memq key '(face font-lock-face mouse-face))
|
||||
(tp--merge-face-values existing val))
|
||||
;; Other properties - string value takes precedence
|
||||
(t val))))))
|
||||
result)))
|
||||
|
||||
(defun tp--merge-face-values (face1 face2)
|
||||
"Merge two face values into one.
|
||||
@ -938,31 +936,24 @@ it will be applied to the text before updating."
|
||||
"Replace text in current buffer for reactive text with LAYER-NAME.
|
||||
NEW-TEXT is the new text to replace with.
|
||||
PROPS are the properties to apply to the new text.
|
||||
Text properties embedded in NEW-TEXT are preserved."
|
||||
Text properties embedded in NEW-TEXT are merged with PROPS."
|
||||
(goto-char (point-min))
|
||||
(let ((match (text-property-search-forward 'tp-name layer-name t))
|
||||
;; Check if new-text has embedded text properties
|
||||
;; Use tp--string-has-properties-p to scan the entire string
|
||||
(new-text-has-props (tp--string-has-properties-p new-text)))
|
||||
;; Merge embedded text properties from new-text into props
|
||||
(merged-props (tp--merge-string-props-into-plist new-text props)))
|
||||
(while match
|
||||
(let* ((m-start (prop-match-beginning match))
|
||||
(m-end (prop-match-end match))
|
||||
(old-text (buffer-substring-no-properties m-start m-end)))
|
||||
;; Only replace if text content is different
|
||||
(unless (equal old-text (substring-no-properties new-text))
|
||||
;; Delete old text and insert new
|
||||
;; Delete old text and insert new (without properties)
|
||||
(delete-region m-start m-end)
|
||||
(goto-char m-start)
|
||||
(insert new-text)
|
||||
;; Apply the layer properties (including tp-text and tp-name) to new text
|
||||
;; Use put-text-property to preserve embedded text properties in new-text
|
||||
(insert (substring-no-properties new-text))
|
||||
;; Apply merged properties
|
||||
(let ((new-end (+ m-start (length new-text))))
|
||||
(if new-text-has-props
|
||||
;; Preserve embedded properties - use put-text-property
|
||||
(cl-loop for (key val) on props by #'cddr
|
||||
do (put-text-property m-start new-end key val))
|
||||
;; No embedded properties - can use set-text-properties
|
||||
(set-text-properties m-start new-end props)))))
|
||||
(set-text-properties m-start new-end merged-props))))
|
||||
;; Search for next match
|
||||
(setq match (text-property-search-forward 'tp-name layer-name t)))))
|
||||
|
||||
@ -1018,22 +1009,14 @@ NEW-OBJECT is the new string object (only different for strings with tp-text)."
|
||||
(message "tp: transform error for %s: %s" layer-name err)
|
||||
tp-text-val))
|
||||
tp-text-val))
|
||||
;; Check if final-text has text properties that should be preserved
|
||||
;; Use tp--string-has-properties-p to scan the entire string
|
||||
(tp-text-has-props (tp--string-has-properties-p final-text)))
|
||||
;; Merge embedded text properties from final-text into props
|
||||
;; This way the caller applies all properties together
|
||||
(merged-props (tp--merge-string-props-into-plist final-text props)))
|
||||
(if (stringp object)
|
||||
;; For strings: create a new string with tp-text content
|
||||
;; Preserve any text properties from the tp-text value itself
|
||||
;; Return merged props so all properties are applied together
|
||||
(let ((new-string (copy-sequence final-text)))
|
||||
;; If tp-text has embedded text properties, merge them with props
|
||||
;; The props from the layer are applied as base, tp-text props on top
|
||||
(if tp-text-has-props
|
||||
;; Return props and mark that tp-text has embedded properties
|
||||
;; The caller should handle merging
|
||||
(list (plist-put (copy-sequence props)
|
||||
'tp--text-has-props t)
|
||||
(length new-string) new-string)
|
||||
(list props (length new-string) new-string)))
|
||||
(list merged-props (length new-string) new-string))
|
||||
;; For buffers: replace text and adjust end position
|
||||
(let ((old-text (if object
|
||||
(with-current-buffer object
|
||||
@ -1041,12 +1024,7 @@ NEW-OBJECT is the new string object (only different for strings with tp-text)."
|
||||
(buffer-substring-no-properties start end))))
|
||||
(if (equal old-text (substring-no-properties final-text))
|
||||
;; Same text content, no replacement needed
|
||||
;; But we may need to apply text properties from final-text
|
||||
(progn
|
||||
(when tp-text-has-props
|
||||
;; Apply tp-text embedded properties to the buffer region
|
||||
(tp--apply-string-props-to-region final-text start object))
|
||||
(list props end object))
|
||||
(list merged-props end object)
|
||||
;; Need to replace text
|
||||
(let ((existing-props (when preserve-props
|
||||
(if object
|
||||
@ -1059,20 +1037,20 @@ NEW-OBJECT is the new string object (only different for strings with tp-text)."
|
||||
(let ((inhibit-read-only t))
|
||||
(delete-region start end)
|
||||
(goto-char start)
|
||||
(insert final-text)))
|
||||
;; Insert without properties - we'll apply merged props later
|
||||
(insert (substring-no-properties final-text))))
|
||||
(let ((inhibit-read-only t))
|
||||
(delete-region start end)
|
||||
(goto-char start)
|
||||
(insert final-text))))
|
||||
(insert (substring-no-properties final-text)))))
|
||||
(let ((new-end (+ start (length final-text))))
|
||||
;; Re-apply existing properties to new text region if preserving
|
||||
;; Use add-text-properties to not override tp-text embedded props
|
||||
(when existing-props
|
||||
(cl-loop for (key val) on existing-props by #'cddr
|
||||
do (unless (get-text-property start key object)
|
||||
do (unless (plist-member merged-props key)
|
||||
(put-text-property
|
||||
start new-end key val object))))
|
||||
(list props new-end object))))))))
|
||||
(list merged-props new-end object))))))))
|
||||
;; Other types - return unchanged
|
||||
(t (list props end object))))))
|
||||
|
||||
@ -1158,30 +1136,24 @@ Preserves existing properties not specified in PROPS.
|
||||
Returns modified string or (START . END) cons for buffer."
|
||||
(pcase-let ((`(,object ,start ,finish ,props)
|
||||
(tp--parse-args start-or-string end-or-prop props-or-val rest)))
|
||||
;; Handle tp-text property specially
|
||||
;; Handle tp-text property specially - this also merges embedded text properties
|
||||
(pcase-let ((`(,new-props ,new-finish ,new-object)
|
||||
(tp--handle-tp-text-property start finish props object t)))
|
||||
(setq props new-props finish new-finish object new-object)
|
||||
(when (and (stringp object) (plist-member props 'tp-text))
|
||||
(setq start 0)))
|
||||
;; Check if tp-text has embedded properties that should be preserved
|
||||
(let ((tp-text-has-props (plist-get props 'tp--text-has-props)))
|
||||
;; Remove the internal marker from props before applying
|
||||
(when tp-text-has-props
|
||||
(setq props (tp--remove-internal-markers props)))
|
||||
;; Check if we have any existing properties in the range
|
||||
(let ((has-existing-props (or tp-text-has-props
|
||||
(text-properties-at start object))))
|
||||
(if (and (not has-existing-props)
|
||||
;; Also check if this is a uniform range (no intervals)
|
||||
(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)
|
||||
;; 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
|
||||
do (put-text-property start finish key val object)))))
|
||||
;; Check if we have any existing properties in the range
|
||||
(let ((has-existing-props (text-properties-at start object)))
|
||||
(if (and (not has-existing-props)
|
||||
;; Also check if this is a uniform range (no intervals)
|
||||
(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)
|
||||
;; 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
|
||||
do (put-text-property start finish key val object))))
|
||||
(if (stringp object) object (cons start finish))))
|
||||
|
||||
(defun tp-reset (start-or-string &optional end-or-prop props-or-val &rest rest)
|
||||
@ -1190,23 +1162,14 @@ Like `tp-set' but replaces ALL existing properties.
|
||||
Returns modified string or (START . END) cons for buffer."
|
||||
(pcase-let ((`(,object ,start ,finish ,props)
|
||||
(tp--parse-args start-or-string end-or-prop props-or-val rest)))
|
||||
;; Handle tp-text property
|
||||
;; Handle tp-text property - this also merges embedded text properties
|
||||
(pcase-let ((`(,new-props ,new-finish ,new-object)
|
||||
(tp--handle-tp-text-property start finish props object nil)))
|
||||
(setq props new-props finish new-finish object new-object)
|
||||
(when (and (stringp object) (plist-member props 'tp-text))
|
||||
(setq start 0)))
|
||||
;; Check if tp-text has embedded properties that should be preserved
|
||||
(let ((tp-text-has-props (plist-get props 'tp--text-has-props)))
|
||||
;; Remove the internal marker from props before applying
|
||||
(when tp-text-has-props
|
||||
(setq props (tp--remove-internal-markers props)))
|
||||
;; If tp-text has embedded props, apply props with put-text-property
|
||||
;; to preserve the embedded properties. Otherwise replace all.
|
||||
(if tp-text-has-props
|
||||
(cl-loop for (key val) on props by #'cddr
|
||||
do (put-text-property start finish key val object))
|
||||
(set-text-properties start finish props object)))
|
||||
;; Completely replace all properties
|
||||
(set-text-properties start finish props object)
|
||||
(if (stringp object) object (cons start finish))))
|
||||
|
||||
(defun tp--prepend-face (new-face existing-face)
|
||||
@ -1268,31 +1231,32 @@ For `face' property, symbol faces are prepended to existing face list.
|
||||
Returns modified string or (START . END) cons for buffer."
|
||||
(pcase-let ((`(,object ,start ,finish ,props)
|
||||
(tp--parse-args start-or-string end-or-prop props-or-val rest)))
|
||||
;; Handle tp-text property
|
||||
(pcase-let ((`(,new-props ,new-finish ,new-object)
|
||||
(tp--handle-tp-text-property start finish props object t)))
|
||||
(setq props new-props finish new-finish object new-object)
|
||||
(when (and (stringp object) (plist-member props 'tp-text))
|
||||
(setq start 0)))
|
||||
;; Remove the internal tp--text-has-props marker from props before applying
|
||||
(when (plist-get props 'tp--text-has-props)
|
||||
(setq props (tp--remove-internal-markers props)))
|
||||
;; Process each property with deep merging
|
||||
(let ((pos start))
|
||||
(while (< pos finish)
|
||||
(let* ((current-props (text-properties-at pos object))
|
||||
(next-pos (or (next-property-change pos object finish) finish)))
|
||||
(cl-loop
|
||||
for (key val) on props by #'cddr
|
||||
do (let* ((current-val (plist-get current-props 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))))
|
||||
(put-text-property pos next-pos key new-val object)))
|
||||
(setq pos next-pos))))
|
||||
;; Handle tp-text property - this also merges embedded text properties
|
||||
(let ((has-tp-text (plist-member props 'tp-text)))
|
||||
(pcase-let ((`(,new-props ,new-finish ,new-object)
|
||||
(tp--handle-tp-text-property start finish props object t)))
|
||||
(setq props new-props finish new-finish object new-object)
|
||||
(when (and (stringp object) has-tp-text)
|
||||
(setq start 0))))
|
||||
;; For strings with tp-text, properties are already merged - just apply them
|
||||
;; For other cases, process each property with deep merging
|
||||
(if (and (stringp object) (plist-member props 'tp-text))
|
||||
(set-text-properties start finish props object)
|
||||
(let ((pos start))
|
||||
(while (< pos finish)
|
||||
(let* ((current-props (text-properties-at pos object))
|
||||
(next-pos (or (next-property-change pos object finish) finish)))
|
||||
(cl-loop
|
||||
for (key val) on props by #'cddr
|
||||
do (let* ((current-val (plist-get current-props 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))))
|
||||
(put-text-property pos next-pos key new-val object)))
|
||||
(setq pos next-pos)))))
|
||||
(if (stringp object) object (cons start finish))))
|
||||
|
||||
;;;============================================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user