If I propertize a string and save it to a variable, how can I change the string within that variable without altering its text properties?
AFAIK, this isn't possible (or practical) in the most general case, since different strings can differ in length, and so their respective text property intervals may not match, as mentioned by Fólkvangr. If you know ahead of time how to reconcile such discrepancies, there is possibly a better algorithmic approach to whatever you are trying to achieve.
For the simple case of text properties which remain constant throughout the entire length of a string, see John Kitchin's answer.
However, this approach breaks down with multiple text property intervals:
(setq s (propertize "foo" 'a t))
;; => #("foo" 0 3 (a t))
(put-text-property 1 2 'b t s)
s
;; => #("foo" 0 1 (a t) 1 2 (b t a t) 2 3 (a t))
(setq s (apply #'propertize "bar" (text-properties-at 0 s)))
;; => #("bar" 0 3 (a t))
Notice the loss of the text property b
that was added to the middle character. One possible workaround is to copy text properties character by character, but this too may not work in the general case.
In the other simple case of modifying string characters in-place without affecting the length of the string, you can use functions such as aset
, fillaray
, subst-char-in-string
, and store-substring
, which preserve text properties:
(setq s (propertize "foo" 'a t))
;; => #("foo" 0 3 (a t))
(aset s 0 ?b)
s
;; => #("boo" 0 3 (a t))
(fillarray s ?x)
;; => #("xxx" 0 3 (a t))
(setq s (subst-char-in-string ?x ?o s))
;; => #("ooo" 0 3 (a t))
(subst-char-in-string ?o ?f s t)
;; => #("fff" 0 3 (a t))
(store-substring s 1 "oo")
;; => #("foo" 0 3 (a t))
As pointed out by Fólkvangr in a comment, modifying strings in-place is not recommended in general, however; see (elisp) Sequence Functions
.
Another approach in Emacs 26 (which works in very limited cases) is to use the function replace-buffer-contents
:
(setq s (propertize "foo" 'a t))
;; => #("foo" 0 3 (a t))
(put-text-property 1 2 'b t s)
s
;; => #("foo" 0 1 (a t) 1 2 (b t a t) 2 3 (a t))
(with-temp-buffer
(insert "foobar")
(let ((buf (current-buffer)))
(with-temp-buffer
(insert s)
(replace-buffer-contents buf)
(setq s (buffer-string)))))
;; => #("foobar" 0 1 (a t) 1 2 (b t a t) 2 3 (a t))
Having said all this, Elisp does provide a way of dynamically propertizing text: buffer overlays. Overlay intervals are determined by markers which move around in a deterministic way as text is inserted and deleted, so you could change the buffer contents while preserving the relevant overlay properties. Converting a buffer with overlays to a buffer string with text properties is left as an exercise to the reader, though. ;)