Adding and removing fields from a PSIClass programatically through com.jetbrains.openapi

Answered
I have the following maven project pom.xml as given below, with an additional source folder in temp/src/main/java, under the basedir.
 
  <modelVersion>4.0.0</modelVersion>
  <groupId>p1</groupId>
  <artifactId>p1</artifactId>
  <name>p1</name>
  <version>1.0</version>
  <packaging>jar</packaging>
  <build>
    <finalName>p1</finalName>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <version>3.2.0</version>
        <executions>
          <execution>
            <id>add-source</id>
            <phase>generate-sources</phase>
            <goals>
              <goal>add-source</goal>
            </goals>
            <configuration>
              <sources>
                <source>temp/src/main/java/</source>
              </sources>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
 
In the folder temp/src/main/java/org, the following files exist with the below contents
 
temp/src/main/java/org/Class1.java
package org;
public class Class1 {
public String b = "b";
}
 
temp/src/main/java/org/Class3.java
package org;
public class Class3 {
public String x = "x";
}
 
 
I have another file by name "changes.txt", which has the following contents
+Class1.a
-Class1.b
+Class2
+Class2.p
Class2.p,q
Class2,Class4
-Class3
 
+ at the beginning of a line indicates class/member to be created/added, - at the beginning of a line indicates class/member to be deleted & no +/- at the beginning of a line indicates rename refactoring of a class/member.
 
For the above file, the objective is to have the following done.
Add the line "public String a = "a"; as a member in Class1.java
Remove the line "public String b = "b"; from the file Class1.java
Create a new Class in the folder temp/src/main/java/org/Class2.java
Add the line "public String p = "p"; as a member in Class2.java
Rename Refactor member from p to q in Class2.java
Rename Refactor Class2 to Class4
Remove Class3
 
The final contents in "temp/src/main/java/org/" should be as below:
 
temp/src/main/java/org/Class1.java
package org;
public class Class1 {
public String a = "a";
}
 
temp/src/main/java/org/Class4.java
package org;
public class Class4 {
public String q = "q";
}
 
Please note that Class2.java & Class3.java should not exist in the folder temp/src/main/java/org/, after the AnActionEvent is trigerred.
 
 
I have a MyAction class which extends from (com.intellij.openapi.actionSystem.AnAction), with the member function @Override public void actionPerformed(AnActionEvent event).   In the member function I iterate through each line in the changes.txt file. The rename of class/field i am able to do. But addition of class/fields & removal of class/fields I am facing issues. While trying to add a field to the PsiClass, it gives an error, "com.intellij.util.IncorrectOperationException: Must not change PSI outside command or undo-transparent action. "
 
Could you please send me the relevant code snippet to resolve this.  
1
18 comments

The following code worked like a charm.  Thanks for the valuable information.

However I need the additional things to be done.
While adding a field to the class, I use the following snippet
        final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory();
        PsiField psiField = elementFactory.createField(fieldName, PsiType.INT);
        psiClass.add(psiField);

This adds a integer field as the statement "private int <fieldName>;"
However I want the field to be added in the format "public String <fieldName> = "<fieldName>";

Similarly I am not able to add a new Class.
Could you please share the relevant code snippet?

The addition, deletion & rename refactoring of fields is possible.
The deletion & rename refactoring of classes is possible.

--------------------------------------------------------------------------------------------
final String stmt = line;
WriteCommandAction writeCommandAction = new WriteCommandAction(project) {
  @Override
  protected void run(final Result result) throws Throwable {
    String className = null;
    String newClassName = null;
    String fieldName = null;
    String newFieldName = null;
    if (!stmt.contains(".")) {
      if ( (stmt.startsWith("+")) || (stmt.startsWith("-")) )  {
        className = stmt.substring(1);
      } else {
        className = stmt.split(",")[0];
        newClassName = stmt.split(",")[1];
      }
    } else {
      if ( (stmt.startsWith("+")) || (stmt.startsWith("-")) ) {
        className = stmt.substring(1).split("\\.")[0];
        fieldName = stmt.substring(1).split("\\.")[1];
      } else {
        className = stmt.split("\\.")[0];
        fieldName = stmt.split("\\.")[1].split(",")[0];
        newFieldName = stmt.split("\\.")[1].split(",")[1];
      }
    }
    VirtualFile vf = LocalFileSystem.getInstance().findFileByIoFile(new File(basePath + "/temp/src/main/java/com/" + className + ".java"));
    PsiFile psiFile = PsiManager.getInstance(project).findFile(vf);
    PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
    PsiClass psiClass = psiJavaFile.getClasses()[0];
    if (fieldName == null) {
      changeClass(psiClass, newClassName, stmt);
    } else {
      changeField(psiClass, fieldName, newFieldName, stmt);
    }
  }

  private void changeClass(PsiClass psiClass, String newClassName, String stmt) {
    if (stmt.startsWith("-")) {
      psiClass.delete();
    } else {
      JavaRefactoringFactory javaRefactoringFactory = JavaRefactoringFactory.getInstance(project);
      JavaRenameRefactoring javaRenameRefactoring = javaRefactoringFactory.createRename(psiClass, newClassName);
      javaRenameRefactoring.setSearchInComments(false);
      UsageInfo[] usages = javaRenameRefactoring.findUsages();
      javaRenameRefactoring.doRefactoring(usages);
    }
  }

  private void changeField(PsiClass psiClass, String fieldName, String newFieldName, String stmg) {
    if (stmt.startsWith("+")) {
      boolean found = false;
      for (PsiField psiField : psiClass.getFields()) {
        if (psiField.getName().equals(fieldName)) {
          found = true;
          break;
        }
      }
      if (!found) {
        final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory();
        PsiField psiField = elementFactory.createField(fieldName, PsiType.INT);
        psiClass.add(psiField);
      }
    } else if (stmt.startsWith("-")) {
      for (PsiField psiField : psiClass.getFields()) {
        if (psiField.getName().equals(fieldName)) {
          psiField.delete();
          break;
        }
      }
    } else {
      for (PsiField psiField : psiClass.getFields()) {
        if (psiField.getName().equals(fieldName)) {
          JavaRefactoringFactory javaRefactoringFactory = JavaRefactoringFactory.getInstance(project);
          JavaRenameRefactoring javaRenameRefactoring = javaRefactoringFactory.createRename(psiField, newFieldName);
          javaRenameRefactoring.setSearchInComments(false);
          UsageInfo[] usages = javaRenameRefactoring.findUsages();
          javaRenameRefactoring.doRefactoring(usages);
          break;
        }
      }
    }
  }
};
writeCommandAction.execute();

1

The full error message points to the API that must be used here:
Must not change PSI outside command or undo-transparent action. See com.intellij.openapi.command.WriteCommandAction or com.intellij.openapi.command.CommandProcessor.

You can simply wrap your PSI modifications using `com.intellij.openapi.command.WriteCommandAction#writeCommandAction()`.

0
final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory();
PsiField psiField = elementFactory.createField(fieldName, PsiType.INT);

//Newly added line---------------------
PsiUtil.setModifierProperty(psiField, PsiModifier.PUBLIC, true);
//This additional line ensured the field added is "public"

psiClass.add(psiField);




Now how to make the field type "String" instead of "int"
0

com.intellij.psi.PsiJavaParserFacade#createTypeFromText using com.intellij.psi.CommonClassNames#JAVA_LANG_STRING

0

Is the above lines to resolve the issue of creating a field of type String.

Not able to resolve this.

Appreciate if you can give code snippets only for this.

0

Pass in return value from createTypeFromText() instead of PsiType.INT for createField().

0

Thanks a ton...Made the appropriate changes as below and it works.

final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory();
PsiField psiField = elementFactory.createField(fieldName, elementFactory.createTypeFromText(CommonClassNames.JAVA_LANG_STRING,null));
PsiExpression psiInitializer = elementFactory.createExpressionFromText("\"" + fieldName + "\"", psiField);
psiField.setInitializer(psiInitializer);
PsiUtil.setModifierProperty(psiField, PsiModifier.PUBLIC, true);
psiClass.add(psiField);
0

Now the only thing remaining is creation of a new class (say Class1), which extends from another class, with a constructor body and the physical file (Class1.java) being available in the folder \temp\src\main\java\, which is specified in the pom.xml  as a source folder.

Let me know, what functions to be used.

0

Might be easier to just generate the full .java file as text and use com.intellij.psi.PsiFileFactory#createFileFromText()

0

I find different signatures for createFileFromText in the PsiFileFactory

What are the parameters to be passed

& how is that the file will be stored in /temp/src/main/java/com

0

Which one of the below signature is to be applied?

Given that I have a certain "text" to be put in a file "Class1.java" at the location "/temp/src/main/java/com"

 

1. PsiFileFactory.createFileFromText(String,String)

2. PsiFileFactory.createFileFromText(String,FileType,CharSequence)

3. PsiFileFactory.createFileFromText(String,FileType,CharSequence,long,boolean)

4. PsiFileFactory.createFileFromText(String,FileType,CharSequence,long,boolean,boolean)

5. PsiFileFactory.createFileFromText(String,Language,CharSequence)

6. PsiFileFactory.createFileFromText(Language,CharSequence)

7. PsiFileFactory.createFileFromText(String,Language,CharSequence,boolean,boolean)

8. PsiFileFactory.createFileFromText(String,Language,CharSequence,boolean,boolean,boolean)

9. PsiFileFactory.createFileFromText(String,Language,CharSequence,boolean,boolean,boolean,VirtualFile)

10.PsiFileFactory.createFileFromText(FileType,String,CharSequence,int,int)


PsiFileFactory.createFileFromText(CharSequence,PsiFile)

0

Please see https://plugins.jetbrains.com/docs/intellij/psi-files.html#how-do-i-create-a-psi-file There are thousands of sample usages in IntelliJ Community sources.

0

I tried the below code.  It gives an error

non-static method add(PsiElement) cannot be referenced from a static context
                            PsiDirectory.add(psiFile);
                                               ^

final PsiFileFactory factory = PsiFileFactory.getInstance(project);
String text = "package com;\n";
text += "public class " + className + " {\n";
text += "}\n";
File file = new File(basePath + "/temp/src/main/java/com/" + className + ".java");
final PsiFile psiFile = factory.createFileFromText(file.getAbsolutePath(), text);
PsiDirectory.add(psiFile);
0

is is the only issue that needs resolution now....all other features of adding, rename & removing fields is successfully done.

Rename of classes & deletion of classes is successfuly happening.  Only for addition I am stuck with this issue.

0

Anything else to be added in my code

0

You need to locate and provide that PsiDirectory instance, e.g. coming from VirtualFile https://plugins.jetbrains.com/docs/intellij/virtual-file.html and then locating it as described in https://plugins.jetbrains.com/docs/intellij/psi-files.html

0

The below code worked for me.   Succesfully the class gets created in the folder "/temp/src/main/java/com/".   The only  thing required now is to add " extends net.DataRecord" and adding a constructor body

public <className> (String str) {

     super(str);

}

What function needs to be called at the psiClass to implement this?

File file = new File(basePath + "/temp/src/main/java/com/" + className + ".java");
if (!file.exists()) {
File folder = new File(basePath + "/temp/src/main/java/com/");
VirtualFile vFolder = LocalFileSystem.getInstance().findFileByIoFile(folder);
final PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(vFolder);
psiClass = PsiElementFactory.getInstance(project).createClass(className);

psiClass = (PsiClass) psiDirectory.add(psiClass);
psiClass.navigate(true);
}

 

0

Everything worked finally successfully with the below code.

 

if (stmt.startsWith("+")) {
File file = new File(basePath + "/temp/src/main/java/com/" + className + ".java");
if (!file.exists()) {
File folder = new File(basePath + "/temp/src/main/java/com/");
VirtualFile vFolder = LocalFileSystem.getInstance().findFileByIoFile(folder);
final PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(vFolder);
//psiClass = PsiElementFactory.getInstance(project).createClass(className);
String text = "package com;\n";
text += "import net.DataRecord;\n";
text += "public class " + className + " extends DataRecord {\n";
text += "\tpublic " + className + "(String " + className + ") {\n";
text += "\t\tsuper(" + className + ");\n\t}\n}";
PsiFile psiFile = PsiFileFactory.getInstance(project).createFileFromText(className + ".java",text);
PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile;
psiClass = psiJavaFile.getClasses()[0];
psiClass = (PsiClass) psiDirectory.add(psiClass);
psiClass.navigate(true);
}
}

 

0

Please sign in to leave a comment.