9

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))
  • 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. – Lindydancer Mar 20 '15 at 12:23
  • 1
    OK, reported the bug at debbugs.gnu.org/cgi/bugreport.cgi?bug=20153 – abo-abo Mar 20 '15 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 '15 at 4:25
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

2

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")))
  • 1
    Doesn't work, I'll add an example. – abo-abo Mar 20 '15 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 '15 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.) – Lindydancer Mar 20 '15 at 12:17
1

Found a (not very efficient) work-around:

(setq test-str-2
      (read (prin1-to-string test-str-1)))
  • 2
    The work-around fails if proprties contain the # character. – abo-abo Mar 20 '15 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 '15 at 11:39
  • buffers in properties – abo-abo Mar 24 '15 at 11:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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