Replacing a PsiElement but keeping the newlines and spaces


I am extending an IntelliJ Plugin to add a Quick Fix via an Annotator and an IntentionAction. The PsiElement I find containing the text I want to fix is just text typed inside an xml file, and it turns out in the IntentAction as an XmlText containing for example the following:

\n\n\n #ddd\n\n

The goal is to take the `#ddd` color and change it to the following:

<color name="alto">#ddd</color>

To do so, I replace the PsiElement with a newly created XmlTag:

val insert = "<color name=\"$name\">${hexColor.inputToString()}</color>"
val newElement = XmlElementFactory.getInstance(project).createTagFromText(insert)

The missing part now is the newlines. I want the user to keep the newlines he added before and after the color code. I tried using addBefore() and addAfter():

val split = oldElement.text.split(hexColor.input)
if (split.isNotEmpty()) rootTag.addBefore(XmlElementFactory.getInstance(project).createDisplayText(split[0]), newElement)
if (split.size > 1) rootTag.addAfter(XmlElementFactory.getInstance(project).createDisplayText(split[1].trim()), newElement)

But then I get a NullPointerException, saying that the "parent is null" on the newly created element:

Element: class com.intellij.psi.impl.source.xml.XmlTextImpl because: parent is null
invalidated at: see attachment
com.intellij.psi.PsiInvalidElementAccessException: Element: class com.intellij.psi.impl.source.xml.XmlTextImpl because: parent is null

I tried replace() after addBefore() or addAfter(), anchored on the oldElement or on the newElement, nothing works :/

So what is the right way to keep the newlines/spaces that were inside the XmlText so that the user doesn't lose those?

Thank you in advance for your help!




There is a chance that the call to `replace` is making a copy of the new element to insert into the tree rather than actually using the `newElement` instance, which would explain the `null` parent value. It is a good idea to always use the return value of the `replace` method call:

newElement = oldElement.replace(newElement)

This should hopefully give you a valid element to which you can add the preceding and following whitespace.


My mind is literally blown away. It works!

val insert = "<color name=\"$name\">${hexColor.inputToString()}</color>"
var newElement: PsiElement = XmlElementFactory.getInstance(project).createTagFromText(insert)
val split = oldElement.text.split(hexColor.input)
newElement = oldElement.replace(newElement)
if (split.isNotEmpty()) rootTag.addBefore(XmlElementFactory.getInstance(project).createDisplayText(split[0]), newElement)
if (split.size > 1) rootTag.addAfter(XmlElementFactory.getInstance(project).createDisplayText(split[1]), newElement)

This produces the following: 


]]><color name="aqua_haze">#dee</color><![CDATA[


Almost there! So using createDisplayText() is not the way to create white spaces in a XmlFile, so how should I do this?

I tried to create a new WhitespaceImpl() myself but that just triggered the same "parent is null" error.

Any idea?


Found a solution! I copied what the XmlElementFactory.getInstance(project).createDisplayText() does and removed the creation of the CDATA:

private fun whitespace(project: Project, text: String): XmlText { 
val tagFromText = XmlElementFactory.getInstance(project).createTagFromText("<a>$text</a>")
val textElements = tagFromText.value.textElements
return if (textElements.isEmpty()) ASTFactory.composite(XmlElementType.XML_TEXT) as XmlText else textElements[0]

Unbelievable how hard it was to find how to insert some newlines :/


Please sign in to leave a comment.