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))
abo-abo
  • 13,943
  • 1
  • 29
  • 43
  • 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 http://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 04:25

4 Answers4

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.

Lindydancer
  • 6,095
  • 1
  • 13
  • 25
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

Jordon Biondo
  • 12,332
  • 2
  • 41
  • 62
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")))
Lindydancer
  • 6,095
  • 1
  • 13
  • 25
  • 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)))
abo-abo
  • 13,943
  • 1
  • 29
  • 43