3

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? For example:

(setq myvar (propertize "testing"
                        'property1 "this is property1"
                        'property2 "this is property2"))

I would like to change the string of myvar to "mystring" without changing its properties.

0

3 Answers 3

4

Text properties are not globally defined for the string but for each character in the string. Therefore, text properties cannot be transposed literally if strings are different.

Although different objects may have similar characteristics, the properties are intrinsic to each object. The text properties are defined specifically for each object, whatever its value.

1
  • 1
    This shouldn't be a problem if the string is the same length or larger.
    – Smashgen
    Commented Jun 17, 2019 at 1:28
3

I think you can do this:

(setq myvar (apply #'propertize "mystring" (text-properties-at 0 myvar)))

I didn't find any way to use setf or some similar kind of thing to just modify the string contents while preserving the properties.

Update: You can redefine the setf capability of substring to add properties from the original string to the new string. This is a lightly adapted version of code from line 616 in cl-lib.el

(gv-define-expander substring
  (lambda (do place from &optional to)
    (gv-letplace (getter setter) place
      (macroexp-let2* nil ((start from) (end to))
        (funcall do `(substring ,getter ,start ,end)
                 (lambda (v)
                   (funcall setter
                            `(apply 'propertize (cl--set-substring
                                                 ,getter ,start ,end ,v)
                                    (text-properties-at 0 ,getter)))))))))

;; example use, returns original string and new string
(let ((s (propertize "testing" 'property1 "this is property1" 'property2 "this is property2")))
  (list s
        (setf (substring s 0 (length s)) "mystring")))


(#("testing" 0 7
   (property2 "this is property2" property1 "this is property1"))
 #("mystring" 0 8
   (property2 "this is property2" property1 "this is property1")))

I couldn't say if this is a good idea, as it only gets the properties from character 0. In your case that seems ok, but in general it might not be. It might or might not make sense though to get all properties from every character in that region to put on the new string.

1
  • 2
    I would suggest defining a new setf-able form, rather than redefining the built-in substring one.
    – Basil
    Commented Jun 16, 2019 at 14:23
3

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. ;)

1
  • Thanks @Fólkvangr, I'll mention that in the answer, but the OP is specifically titled "how to modify a string", and there are always cases where it might be useful/necessary.
    – Basil
    Commented Jun 17, 2019 at 7:02

Your Answer

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

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