10

I have a propertized string. I want to make a deep copy of it to add more properties, while preserving the properties in the original string. How can I do that (easily)?

Example

Evaluate one-by-one :

(setq test-str-1
      #(";; This `is' a test"
        0 3 (fontified nil face font-lock-comment-delimiter-face)
        3 9 (fontified nil face font-lock-comment-face)
        9 11 (fontified nil face (font-lock-constant-face font-lock-comment-face))
        11 19 (fontified nil face font-lock-comment-face)))
(setq test-str-2 (concat test-str-1))
(add-face-text-property 0 (length test-str-2) 'foobar t test-str-2)

And the result:

test-str-2
;; =>
#(";; This `is' a test" 0 3 (fontified nil face (font-lock-comment-delimiter-face foobar))
  3 9 (fontified nil face (font-lock-comment-face foobar))
  9 11 (fontified nil face (font-lock-constant-face font-lock-comment-face foobar))
  11 19 (fontified nil face (font-lock-comment-face foobar)))
test-str-1
;; =>
#(";; This `is' a test" 0 3 (face font-lock-comment-delimiter-face fontified nil)
  3 9 (face font-lock-comment-face fontified nil)
  9 11 (face (font-lock-constant-face font-lock-comment-face foobar) ; <= foobar is here
        fontified nil)
  11 19 (face font-lock-comment-face fontified nil))
3
  • 2
    I would report this as a bug in add-face-text-property. It should not destructively modify the list, as it fails when that list is referred to by others. Mar 20, 2015 at 12:23
  • 1
    OK, reported the bug at debbugs.gnu.org/cgi/bugreport.cgi?bug=20153
    – abo-abo
    Mar 20, 2015 at 12:35
  • Thanks for reporting the bug. Too bad no one has responded to it yet. It would be good to get this utility function (coded in C) fixed.
    – Drew
    Jul 11, 2015 at 4:25

4 Answers 4

7

You can use the function font-lock-append-text-property to add the text property. It does not modify the value destructively.

For example:

(setq test-str-1
      #(";; This `is' a test"
        0 3 (fontified nil face font-lock-comment-delimiter-face)
        3 9 (fontified nil face font-lock-comment-face)
        9 11 (fontified nil face (font-lock-constant-face font-lock-comment-face))
        11 19 (fontified nil face font-lock-comment-face)))
(setq test-str-2 (concat test-str-1))
(font-lock-append-text-property 0 (length test-str-2) 'face '(foobar t) test-str-2)


test-str-1
#(";; This `is' a test"
  0 3 (face font-lock-comment-delimiter-face fontified nil)
  3 9 (face font-lock-comment-face fontified nil)
  9 11 (face (font-lock-constant-face font-lock-comment-face) fontified nil)
  11 19 (face font-lock-comment-face fontified nil))

test-str-2
#(";; This `is' a test"
  0 3 (fontified nil face (font-lock-comment-delimiter-face foobar t))
  3 9 (fontified nil face (font-lock-comment-face foobar t))
  9 11 (fontified nil face (font-lock-constant-face font-lock-comment-face foobar t))
  11 19 (fontified nil face (font-lock-comment-face foobar t)))

Here, in test-str-1, has retained its original value.

4

I found you can do this by iterating over the text properties, copying the underlying property data and overwriting the existing properties with new copies.

(defun deep-copy-text-properties (str)
  (with-temp-buffer
    (insert str)
    (goto-char 1)
    (while (not (eobp))
      (set-text-properties (point)
                           (goto-char (next-char-property-change (point) (point-max)))
                           ;; copy-tree is the important part
                           (copy-tree (text-properties-at (1- (point))))))
    (buffer-string)))

In my tests, this was about 20% faster than your read solution. I also wrote a version that did not use a temp buffer and modified the properties of a string which was less code but was slower.

Looking at the C code it copies the property plists, with copy_sequence which will rebuild the list structure but not copy the elements by value, so the properties like face in your example that have a list value are copied over by reference and modified. Bug or not, I don't know

3

You can use (concat the-original-string).

For example:

(let ((s "TEXT"))
  (set-text-properties 2 3 '(:foreground "blue") s)
  (let ((q (concat s)))
    (add-text-properties 2 3 '(:background "red") q)
    (cons s q)))
;; Returns:
(#("TEXT" 2 3 (:foreground "blue")) . #("TEXT" 2 3 (:foreground "blue" :background "red")))
3
  • 1
    Doesn't work, I'll add an example.
    – abo-abo
    Mar 20, 2015 at 12:04
  • 1
    The trick is to have a nested list in the properties, like I do. Then concat doesn't work.
    – abo-abo
    Mar 20, 2015 at 12:10
  • @abo-abo. Ok, now I see. I didn't spot that in your added example. In that case I have no answer, but I think there is a real need for such a function. (One potential problem is that it's impossible to know if an unknown property might expect to refer to a shared object of some kind.) Mar 20, 2015 at 12:17
1

Found a (not very efficient) work-around:

(setq test-str-2
      (read (prin1-to-string test-str-1)))
3
  • 2
    The work-around fails if proprties contain the # character.
    – abo-abo
    Mar 20, 2015 at 14:54
  • do you mean if the # character is part of the symbol name? Or do mean properties that are buffers or other non-printable data? If it's the first, you should file a bug.
    – Malabarba
    Mar 24, 2015 at 11:39
  • buffers in properties
    – abo-abo
    Mar 24, 2015 at 11:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.