Recently, I developed an Intellij Idea plug-in for my work, which can be used to parse the file structure in Android and modify it, and realize one-click introduction of dependency and initialization. All kinds of sad things happened in the process. Now I can record and make a note, and it is also a pleasure if I can help others.

New plug-in Project

2. New Project -> Select Intellij Platform Plugin->Project SDK select Intellij Idea Community Edition IC-[Version number] 3. Click Next and fill in the Project Name and Project Location. Click Finish to generate the initial plug-in Project with the following directory structure

Add functionality to the plug-in

Here are two plugins for different application times

  • One is inherited fromAnAction, this plug-in is the function of executing the plug-in after clicking a button of Intellij IDEA.
  • One is implementationProjectComponentIntellij IDEA or Android Studio will automatically execute the plugin after opening Project. These two plug-ins are actually common, but during the development process,ProjectComponentClass plug-ins are more limited and require special attention.

AnAction plug-in

The new method is shown as follows:

  • Id: indicates the unique identifier of a label. Usually in the form of < project name >.< class name >.
  • Class: is our custom AnAction class
  • Text: Display text, such as our custom plug-in in the menu list, this text is the corresponding menu item
  • Description: The description of the AnAction
  • Groups: Indicates where the plug-in entry will appear. For example, if you want the plug-in to appear on the Tools menu, select ToolsMenu. If you want the plug-in to appear in the Generate menu in the edit box, select GenerateGroup
  • Actions: existing Actions, i.e. existing plug-in functions. By selecting the Action here in conjunction with the Anchor option, we can specify whether our new Action will appear before or after the existing Action.
  • Anchor: Specifies the position of the Action option in Groups, with Frist being the top and Last the bottom, or Keyboard Shortcuts above and below an option

After filling in the necessary information, you can see that the resources/ meta-INF /plugin.xml file has an additional Actions node, and the new Action is added to the Action node

public class ExampleAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        // TODO: insert action logic here}}Copy the code

A new Action inherits the AnAction class and performs the Action performed method.

Create ActionGroup (resources/ meta-INF /plugin.xml); create ActionGroup (resources/ meta-INF /plugin.xml); Here’s an example:

<actions>
    <group id="com.example.MyGroup" text="MyGroup" popup="true">
        <add-to-group group-id="HelpMenu" anchor="first"/>
        <action  id="MyGroup.FirstAction" class="com.example.plugin.FirstAction" text="FirstAction"/>
        <action id="MyGroup.SecondAction"  class="com.example.plugin.SecondAction" text="SecondAction"/>
    </group>
</actions>
Copy the code

This will add a MyGroup menu at the top of the HelpMenu, and the MyGroup menu will have two submenus, FirstAction and SecondAction

ProjectComponent plug-in

This type of plug-in needs to implement the projectOpened and projectClosed methods of the ProjectComponent interface, mainly in the projectOpened method when the project is opened. Note that The operation performed here is performed every time the project is opened, so it needs to be very careful. I suggest that it is best to add a conditional judgment to be executed every time the project is opened to avoid unnecessary repeated operations. Ps. There are significant restrictions on write operations that need special attention.

public class OppoProjectComponent implements ProjectComponent {
    private Project mProject;

    public OppoProjectComponent(Project inProject) {
        mProject = inProject;
    }

    @Override
    public void projectOpened(a) {}@Override
    public void projectClosed(a) {}}Copy the code

Plug-ins develop some related concepts and apis

concept

Before starting real development, it is necessary to understand the common concepts and apis in the Intellij Idea SDK. There are two types of File concepts in the Intellij Idea SDK, one is VitrualFile, the other is PSIFile (PSI: Program Structure Interface). VitrualFile can be approximated as File in Java, and traditional File manipulation methods are supported by VirtualFile. PSI Element is a general term for all the different types of objects in the PSI system. Everything in the PSI system is a PSI Element, including a method, an open parenthesis, a space, a newline. The collection of all PSI elements in a file is called a PSIFile.

API

There are many apis for plug-in development, most of which can be found in the official documentation. By default, portal supports Java files and XML files. If you want to support Kotlin files and Gradle files, you can use the API to parse and modify these two kinds of files. Properties file or to make it easier to use the Androidmanifest.xml file, you must add additional dependencies. Take the Gradle file as an example. Gradle file content is written in the Groovy language, so to parse gradle files you need to add Groovy dependencies. There are two steps to adding.

  1. Open the Project Structure, click SDKs item, select the Intellij IDEA, and then click on the right side of + number, select [Intellij IDEA to install directory] / plugins/Groovy/lib/Groovy jar and add.

After the first configuration step, you can now use the Groovy-related plug-in API. At this point, you can compile and package with no problems, but when you actually use it, you will report problems that the Groovy plug-in API did not find. Therefore, the second step is to add Depends

2. Add Depends to resources/ meta-INF /plugin.xml

  <depends>org.intellij.groovy</depends>
Copy the code

You can use Groovy’s plug-in API properly through the above two steps.

So if your development for Android Studio needs to parse Kotlin, Gradle, Properties, and Androidmanifest.xml files, add the following dependencies

Next we use plug-ins to implement some small functions.

Generate Java code through plug-ins

In fact, there are two cases, one is to generate a complete class file, and one is to add some code, such as a new class and call it.

In either case, it is recommended to use templates to generate the code we need. Here we put the template file in the resouces directory

xxxx.class.getClassLoader(a).getResource(templateFileName);
Copy the code

Method to get a stream of files.

Generate the complete class file

Take generating a custom Application class as an example. In this case, the custom Application class needs to be parsed from androidmanifest.xml to see if there is any custom Application class, assuming that there is no custom Application class. The entire process can be divided into the following steps: 1. Read the template file from Resources

    /** * Reads the character contents of the template file **@paramFileName Specifies the name of the template file *@return* /
    private String readTemplateFile(String fileName) {
        InputStream in = null;
        String content = "";
        try {
            in = ApplicationClassOperator.class.getClassLoader().getResource(fileName).openStream();
            content = StreamUtil.inputStream2String(in);
        } catch (IOException e) {
            Loger.error("getResource error");
            e.printStackTrace();
        }
        return content;
    }
Copy the code

2. Replace the template information

    /** * replaces the character ** in the template@return* /
    private String replaceTemplateContent(String source, String packageName) {
        source = source.replace("$packageName", packageName);
        return source;
    }
Copy the code

3. Generate a Java file

  /** * Generate Java file **@paramContent The contents of the class *@paramClassPath Class file path *@paramClassName Name of the class file */
    private void writeToFile(String content, String classPath, String className) {
        try {
            File folder = new File(classPath);
            if(! folder.exists()) { folder.mkdirs(); } File file =new File(classPath + "/" + className);
            if(! file.exists()) { file.createNewFile(); } FileWriter fw =new FileWriter(file.getAbsoluteFile());
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(content);
            bw.close();
        } catch (IOException e) {
            e.printStackTrace();
            Loger.error("write to file error! "+e.getMessage()); }}Copy the code

On the original basis for modification, add part of the code

Adding part of the code is not the same as adding the whole file, because it does not use the PSI-related API. Again, to modify the Application, add a method and call it in the onCreate method.

  1. Find the custom Application class where the custom Application class is required fromAndroidManifest.xmlParse to get,AndroidManifest.xmlLet’s leave the analysis for now and talk about it later. Let’s say we get the application in androidmanifest.xmlandroid:nameAnd through thisandroid:nameYou can find your custom Application class.
    /** * If you have a custom application class, Check whether you need to add a template initialization method * * @param manifestModel * @return If application. Java already has an initTemplate method that returns true + custom Application class */
    public Pair<Boolean, PsiClass> checkApplication(Project project, ManifestModel manifestModel) {
        PsiClass appClass = null;
        if(manifestModel.applicationName ! =null) {
            String fullApplicationName = manifestModel.applicationName;
            if (manifestModel.applicationName.startsWith(".")) {
                fullApplicationName = manifestModel.packageName + manifestModel.applicationName;
            }

            appClass = JavaPsiFacade.getInstance(project).findClass(fullApplicationName, GlobalSearchScope.projectScope(project));
            PsiMethod[] psiMethods = appClass.getAllMethods();
            for (PsiMethod method : psiMethods) {
                if (Constants.INIT_METHOD_IN_APP.equals(method.getName())) {
                    return new Pair<>(true, appClass); }}}return new Pair<>(false, appClass);
    }
Copy the code
  1. Add methods
    /** * Add the initTemplate() method */ to the custom Application class
    public void addInitTemplateMethod(Project project, PsiClass psiClass) {
        String method = null;
        try {
            InputStream in = getClass().getClassLoader().getResource("/templates/initTemplateInAppMethod.txt").openStream();
            method = StreamUtil.inputStream2String(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (method == null) {
            throw new RuntimeException("initTemplateInAppMethod shuold not be null");
        }
        final String finalMethod = method.replace("\r\n"."\n");
        WriteCommandAction.runWriteCommandAction(project, () -> { PsiMethod psiMethod = PsiElementFactory.SERVICE.getInstance(project).createMethodFromText(finalMethod, psiClass); psiClass.add(psiMethod); });
    }
Copy the code
  1. Find the location of the call
    /** * find super.oncreate () in onCreate; Location * *@param psiClass
     * @return* /
    public static PsiElement findCallPlaceInOnCreate(PsiClass psiClass) {
        PsiMethod[] psiMethods = psiClass.getAllMethods();

        for (PsiMethod psiMethod : psiMethods) {
            if ("onCreate".equals(psiMethod.getName())) {
                PsiCodeBlock psiCodeBlock = psiMethod.getBody();
                if (psiCodeBlock == null) {
                    return null;
                }
                for (PsiElement psiElement : psiCodeBlock.getChildren()) {
                    if ("super.onCreate();".equals(psiElement.getText())) {
                        returnpsiElement; }}}}return null;
    }
Copy the code
  1. Add call statement
    /** * add a call to initTemplate() in the onCreate method; Statement * *@param project
     * @param psiClass
     * @param anchor
     */
    public static void addCallInitMethod(Project project, PsiClass psiClass, PsiElement anchor) {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            PsiStatement psiStatement = PsiElementFactory.SERVICE.getInstance(project)
                    .createStatementFromText(Constants.INIT_METHOD_CALL_IN_APP_ONCREATE, psiClass);
            psiClass.addAfter(psiStatement, anchor);
        });
    }
Copy the code

Add Gradle dependencies through plug-ins

To add gradle Dependencies, find the build.gradle file in the app Module and add Dependencies to it.

1. Locate the build.gradle file for the App Module

      /** * Get the PsiElement of buildScript in PisFile where apply plugin: 'com.android.application' is located@param project
     * @return* /
    public static PsiElement getAppBuildScriptFile(Project project) {
        PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, "build.gradle", GlobalSearchScope.projectScope(project));
        for (PsiFile psiFile : psiFiles) {
            PsiElement[] psiElements = psiFile.getChildren();
            for (PsiElement psiElement : psiElements) {
                if ( psiElement instanceof GrMethodCallExpressionImpl && "buildscript".equals(psiElement.getFirstChild().getText())) {
                    returnpsiElement; }}}return null;
    }
Copy the code

2. Locate the Dependencies node

  /** * Find the Dependencies node ** under the buildScript node@param buildscriptElement
     * @return* /
    public static PsiElement findBuildScriptDependencies(PsiElement buildscriptElement) {
        //buildscriptElement The last child is codeBlock
        PsiElement[] psiElements = buildscriptElement.getLastChild().getChildren();
        for (PsiElement psiElement : psiElements) {
            if (psiElement instanceof GrMethodCallExpressionImpl && "dependencies".equals(psiElement.getFirstChild().getText())) {
                returnpsiElement; }}return null;
    }
Copy the code
  1. Add the dependent
    /** * add Dependencies, **@param project
     * @paramDependenciesElement Entire Dependencies parent */
    public static void addDependencies(Project project, PsiElement dependenciesElement, List<String> depends) {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            for (String depend : depends) {
                GrStatement statement = GroovyPsiElementFactory.getInstance(project).createStatementFromText(depend);
                PsiElement dependenciesClosableBlock = dependenciesElement.getLastChild();
                Add a new dependency at the end of the dependencies dependencies before}
                dependenciesClosableBlock.addBefore(statement, dependenciesClosableBlock.getLastChild());
            }
            Loger.info("addDependencies success!");
        });
    }
Copy the code

Androidmanifest.xml parsing

    /** * Use Dom to parse manifest.xml **@param project
     * @return* /
    public static ManifestModel resolveManifestModel(Project project) {
        DomManager manager = DomManager.getDomManager(project);
        ManifestModel manifestModel = new ManifestModel();
        PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, "AndroidManifest.xml", GlobalSearchScope.projectScope(project));
        if (psiFiles.length <= 0) {
            Loger.error("this project is not an Android Project!");
        }
        for (PsiFile psiFile : psiFiles) {
            if(! (psiFileinstanceof XmlFile)) {
                Loger.error("this file cannot cast to XmlFile,just ignore!");
                continue;
            }
            DomFileElement<Manifest> domFileElement = manager.getFileElement((XmlFile) psiFile, Manifest.class);
            if(domFileElement ! =null) {
                Manifest manifest = domFileElement.getRootElement();
                if(manifest.getPackage().getXmlAttributeValue() ! =null) {
                    manifestModel.packageName = manifest.getPackage().getXmlAttributeValue().getValue();
                }
                Application application = manifest.getApplication();

                manifestModel.application = application;
                if (application.exists()) {
                    // Application is the androidmanifest.xml of the main App Module
                    if (application.getName().exists()) {
                        // Android :name already exists
                        Loger.info("application.getName()=="+ application.getName().getRawText()); manifestModel.applicationName = application.getName().getRawText(); }}else {
                    Loger.info("application section not exist,just ignore this xml file!"); }}}return manifestModel;
    }
Copy the code

If you need to add Android :name, you can do this:

    /** * Add custom application attribute * to androidmanifest.xml@param project
     * @param application
     */
    public static void addApplicationName(Project project, Application application) {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            application.getXmlTag().setAttribute("android:name"."."+Constants.APPLICATION_CLASS_NAME);
            CommonUtils.refreshProject(project);
            Loger.info("addApplicationName success!!");
        });
    }
Copy the code

A little trick

View the FILE’s PSI structure

If you need to add, delete or change a file, you first need to parse the file’s PsiElement structure. The view structure IntelliJ IDEA itself supports.

Plugin Project run debugging can run debugging directly

conclusion

The development process of this plug-in is a painful and happy experience, because most of the existing articles about IDEA plug-in development are relatively simple introduction, especially for Android files (including gradle files, properties files, Androidmanifest.xml file) is even harder to find. Therefore, the modification and development of these files were completed by reasoning by analogy with Java file structure, checking IDEA plug-in SDK API and continuous attempts.

The resources

IntelliJ Platform SDK DevGuide

AndroidStudio plugin tutorial in detail

Android Studio Plugin development tutorial