Home Development for Android Fantastic Plugins,vol.2.Practice

Fantastic Plugins,vol.2.Practice

by admin

Here you can read the first article with the theory of plugin building.

And in this part I will tell you what problems we encountered during the creation of the plugin and how we tried to solve them.
Fantastic Plugins,vol.2.Practice

What will I talk about?

  • Practical part
  • Multipage UI
  • DI in plugins
  • Code generation
  • Code modification
  • What to do next?
    • Tips
    • FAQ
    • Multipage UI

      The first thing we needed to do was create a multi-page UI.We made the first complex form with a lot of checkboxes, input fields.A little later we decided to addthe ability to select a list of modules that the user can connect to the new module. And we also want to select the application modules to which we plan to connect the created module.

      On one form to place so many controls is not very convenient, so we made three separate pages, three separate forms. In short, a Wizard dialog.

      Fantastic Plugins,vol.2.Practice

      But since it’s a pain to make a multipage UI in plugins, we wanted to find something ready-made. And in the depths of IDEA we found a class called WizardDialog

      Fantastic Plugins,vol.2.Practice

      This is a wrapper class on top of the regular dialog, which independently tracks the user’s progress through the wizard, and displays the necessary buttons (Previous, Next, Finish, etc.). A special wizard is attached to the WizardDialog. WizardModel to which individual WizardSteps are added. Each WizardStep represents a separate WizardStep.

      In its simplest form, the dialog implementation looks like this :


      class MyWizardDialog(model: MyWizardModel, private val onFinishButtonClickedListener: (MyWizardModel) -> Unit): WizardDialog<MyWizardModel> (true, true, model) {override fun onWizardGoalAchieved() {super.onWizardGoalAchieved()onFinishButtonClickedListener.invoke(myModel)}}

      Inherited from class WizardDialog , parameterize it with the class of our WizardModel This class has a special callback ( onWizardGoalAchieved ) which tells us that the user has finished the wizard and clicked on the "Finish" button.
      It is important to note that from within this class, it is only possible to reach WizardModel This means you have to put all the data the user types in as you walk through the wizard into WizardModel


      class MyWizardModel: WizardModel("Title for my wizard") {init {this.add(MyWizardStep1())this.add(MyWizardStep2())this.add(MyWizardStep3())}}

      The model looks like this: we inherit from the class WizardModel and using the built-in method add add the individual WizardStep -s.


      class MyWizardStep1: WizardStep<MyWizardModel> () {private lateinit var contentPanel: JPaneloverride fun prepare(state: WizardNavigationState?): JComponent{return contentPanel}}

      WizardStep -s are also arranged simply : we inherit from the class WizardStep class, parameterize it with our model class and, most importantly, override the prepare method, which returns the root component of your future form.

      In its simplest form, it really looks like this. But in the real world, your form would probably resemble something like this :

      Fantastic Plugins,vol.2.Practice

      Here you can remember the days when we in the Android world didn’t know yet what Clean Architecture, MVP and wrote all the code in one Activity. Here is a new field for architectural battles, and if you want to get excited, you can implement your own architecture for plugins.


      If you need a multi-page UI, use WizardDialog – will be easier.

      Moving on to the next topic, DI in plugins.

      DI in plugins

      Why would you even need Dependency Injection inside a plugin?
      The first reason is to organize the architecture within the plugin.

      It would seem, why do you need to observe any architecture inside a plugin at all? A plugin is a utility thing, once written, that’s it, forget it.
      Yes, but no.
      When your plugin grows, when you write a lot of code, the question of code structuring arises by itself. This is where DI comes in handy.

      The second, more important reason is that you can use DI to reach components written by developers of other plugins. This can be event buses, loggers, and more.

      Although you are free to use any DI framework (Spring, Dagger, etc.), within IntelliJ IDEA there is its own DI framework, which is based on the first three levels of abstraction I already mentioned : Application , Project and Module

      Fantastic Plugins,vol.2.Practice

      Associated with each of these levels is a different abstraction called Component The component of the desired level is created per instance of that level’s object. So ApplicationComponent is created once per instance of the class Application , similarly ProjectComponent to instances Project , and so on.

      What does it take to use a DI framework?

      First, we create a class that implements one of the interface components we need – for example, a class that implements ApplicationComponent , or ProjectComponent , or ModuleComponent In this case, we have the possibility to inject the object of the level whose interface we are implementing. That is, for example, in ProjectComponent you can inject an object of the Project

      Creating component classes

      class MyAppComponent(val application: Application, val anotherApplicationComponent: AnotherAppComponent): ApplicationComponentclass MyProjectComponent(val project: Project, val anotherProjectComponent: AnotherProjectComponent, val myAppComponent: MyAppComponent): ProjectComponentclass MyModuleComponent(val module: Module, val anotherModuleComponent: AnotherModuleComponent, val myProjectComponent: MyProjectComponent, val myAppComponent: MyAppComponent): ModuleComponent

      Secondly, it is possible to inject other components of the same level or higher. That is, for example, in ProjectComponent you can inject other ProjectComponent or ApplicationComponent Just here you can access instances of "alien" components.

      In doing so, IDEA ensures that the entire dependency graph is assembled correctly, all objects are created in the correct order and initialized correctly.

      The next thing you will need to do is register the component in the plugin.xml Once you implement one of the Component interfaces (e.g, ApplicationComponent ), IDEA will immediately prompt you to register your component in plugin.xml.

      Register component in plugin.xml


      How is this done? A special tag appears. <project-component> ( <application-component> , <module-component> – depending on the level). There is a tag inside it. , it has two more tags : <interface-class> where you specify the interface name of your component, and <implementation-class> where the implementation class is specified. The same class can be an interface of a component as well as its implementation, so you can do with a single tag <implementation-class>

      The last thing to do is to get the component from the corresponding object, i.e. ApplicationComponent get it from the instance Application , ProjectComponent – of Project etc.

      We get the component

      val myAppComponent = application.getComponent(MyAppComponent::class.java)val myProjectComponent = project.getComponent(MyProjectComponent::class.java)val myModuleComponent = module.getComponent(MyModuleComponent::class.java)


      1. There is a DI framework inside IDEA – you don’t need to drag anything yourself: neither Dagger, nor Spring. Although, of course, you can.
      2. With this DI, you can reach components that are already prefabricated, and that’s the juice.

      Let’s move on to the third task, code generation.

      Code generation

      Remember in the checklist we had the task to generate a lot of files? Every time we create a new module, we create a bunch of files : interactors, presenters, fragments. When we create a new module, these components are very similar to each other, and we would like to learn how to generate this framework automatically.


      What’s the easiest way to generate a ton of similar code? Use templates. First, you need to look at your templates and figure out what your requirements are for a code generator.

      A piece of the build template.gradle file

      apply plugin:'com.android.library'<if (isKotlinProject) {apply plugin: 'kotlin-android'apply plugin: 'kotlin-kapt'<if (isModuleWithUI) {apply plugin: 'kotlin-android-extensions'}>}>...android {...<if (isMoxyEnabled) {kapt {arguments {arg("moxyReflectorPackage", '<include var="packageName"> ')}}}>...}...dependencies {compileOnly project(':common')compileOnly project(':core-utils')<for (moduleName in enabledModules) {compileOnly project('<include var="moduleName"> ')}>...}

      First : we wanted to be able to use conditions inside these templates.Here’s an example: if a plugin is somehow related to the UI, we want to plug in a special Gradle plugin kotlin-android-extensions

      Condition inside the template

      <if (isKotlinProject) {apply plugin: 'kotlin-android'apply plugin: 'kotlin-kapt'<if (isModuleWithUI) {apply plugin: 'kotlin-android-extensions'}>}>

      The second thing we want is the ability to use a variable within this pattern.For example, when we configure kapt for Moxy, we want to put in the name of the package as an argument to the annotation processor.

      Substitute the value of a variable inside the pattern

      kapt {arguments {arg("moxyReflectorPackage", '<include var="packageName"> ')}}

      Another thing we need is the ability to handle loops within the template. Remember the form where we chose the list of modules we want to connect to the new module we’re creating? We want to loop around them and add the same line.

      Let’s use a loop in the template

      <for (moduleName in enabledModules) {compileOnly project('<include var="moduleName"> ')}>

      Thus, we put three conditions on the code generator :

      • We want to use the conditions
      • Ability to substitute variable values
      • We need loops in templates


      What are the options for implementing a code generator? You can, for example, write your own code generator. That’s what the guys from Uber did for example: they wrote their plugin to generate the ribbets (that’s the name of their architectural units). They came up with their pattern language. in which they only used the ability to insert variables. They put the conditions on the generator level But we thought we wouldn’t do that.

      The second option is to use the utility class built into IDEA FileTemplateManager , but I wouldn’t recommend doing this. Because it has as its engine. Velocity which has some problems with passing Java objects into templates. Also, FileTemplateManager doesn’t know how to generate files other than Java or XML out of the box. And we needed to generate Groovy files, Kotlin, Proguardand other file types as well.

      The third option was… FreeMarker If you have ready-made templates FreeMarker , don’t rush to throw them away – you may need them inside the plugin.

      What to do, what to use FreeMarker inside the plugin? First, add file templates. You can create a folder /templates inside the folder /resources and add there all our templates for all files – presenters, fragments, etc.

      Fantastic Plugins,vol.2.Practice

      After that you will need to add the FreeMarkerlibrary dependency. Since the plugin uses Gradle, adding a dependency is easy.

      Adding FreeMarker library dependency

      dependencies {...compile 'org.freemarker:freemarker:2.3.28'}

      After that we configure FreeMarker inside our plugin. I suggest you just copy this configuration – it’s exhausted, exhausted, copy it and everything will just work.

      FreeMarker configuration

      class TemplatesFactory(val project: Project) : ProjectComponent {private val freeMarkerConfig by lazy {Configuration(Configuration.VERSION_2_3_28).apply {setClassForTemplateLoading(TemplatesFactory::class.java, "/templates")defaultEncoding = Charsets.UTF_8.name()templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLERlogTemplateExceptions = falsewrapUncheckedExceptions = true}}...

      It’s time to create files with FreeMarker To do this, we retrieve the template by its name from the configuration and with the usual FileWriter create a file with the needed text right on the disk.

      File creation via FileWriter

      class TemplatesFactory(val project: Project) : ProjectComponent {...fun generate(pathToFile: String, templateFileName: String, data: Map<String, Any>) {val template = freeMarkerConfig.getTemplate(templateFileName)FileWriter(pathToFile, false).use { writer ->template.process(data, writer)}}}

      And it seems like the problem is solved, but no. In the theoretical part, I mentioned that the PSI structure permeates the whole IDEA, and you need to take this into account. If you create files bypassing the PSI structure (for example, via FileWriter), IDEA simply won’t understand that you created something and won’t display the files in the project tree. We waited about seven minutes before IDEA indexed and saw the created files.

      Conclusion – do it right, create files with PSI structure in mind.

      Create PSI structure for files

      First, we saw the folder structure with PsiDirectory You can get the start directory of your project with the guessProjectDir and toPsiDirectory :

      Get PsiDirectoryof the project

      val projectPsiDirectory= project.guessProjectDir()?.toPsiDirectory(project)

      Subsequent directories can either be found using the PsiDirectory findSubdirectory , or create with the method createSubdirectory

      Find and create PsiDirectory

      val coreModuleDir = projectPsiDirectory.findSubdirectory("core")val newModulePsiDir = coreModuleDir.createSubdirectory(config.mainParams.moduleName)

      I also recommend that you create Map -cube from which you can retrieve all the PsiDirectory folder structures, so that you can then add created files to any of these folders.

      Create a Map of the folder structure

      return mutableMapOf<String, PsiDirectory?> ().apply {
      this["root"] = modulePsiDir
      this["src"]= modulePsiDir.createSubdirectory("src")
      this["main"] = this["src"]?.createSubdirectory("main")
      this["java"] = this["main"]?.createSubdirectory("java")
      this["res"] = this["main"]?.createSubdirectory("res")

      // the function will create a PsiDirectory for the specified package name:// ru.hh.feature_worknear → ru / hh / feature_worknearcreatePackageNameFolder(config)// datathis["data"] = this["package"]?.createSubdirectory("data")// ...


      Folders have been created. Create PsiFile -we will do it with the help of PsiFileFactory This class has a special method called createFileFromText The method takes three parameters as input: the name (String fileName), the text (String text) and the type (FileTypefileType) of the output file. It is clear where to take two of the three parameters: we know the name, we get the text from the FreeMarker. But where to get FileType ? And what is this even?


      FileType – is a special class that denotes the type of a file. There are only two FileType available out of the box: JavaFileType and XmlFileType, respectively for Java and XML files. But the question is: where do we get the types for build.gradle file, for Kotlin -files, for Proguard for gitignore , finally?!

      First, most of these FileType -s can be taken from other pluginsthat have already been written by someone else. GroovyFileType can be taken from Groovy plugin , KotlinFileType can be taken from Kotlin-plugin , Proguard – from Android plugin

      How do we add another plugin’s dependency to ours? We use gradle-intellij-plugin It adds a special intellij block to the build.gradle file of the plugin, which has a special property inside – plugins In this property we can list the list of plugin IDs that we want to depend on.

      Adding dependencies on other plugins

      //build.gradle pluginintellij {...plugins = ['android', 'Groovy', 'kotlin']}

      Keys are taken from the official JetBrains plugin repository For plugins built into IDEA (which are Groovy, Kotlin, and Android), the name of the plugin folder within IDEA is sufficient. For the others, you will need to go to the official JetBrains plugins repository, where the property Plugin XML ID , as well as the version (e.g. Docker plugin page ). More about plugging in other plugins read on GitHub

      Secondly, we need to add a description of the dependency in the file plugin.xml This is done with the tag

      Connect plugins in plugin.xml

      <idea-plugin>...<depends> org.jetbrains.android</depends><depends> org.jetbrains.kotlin</depends><depends> org.intellij.groovy</depends></idea-plugin>

      After we synchronize the project, we will get dependencies from other plugins and we will be able to use them.

      But what if we don’t want to depend on other plugins? In that case, we can create a stub for the file type we want. To do this we first create a class which we want to inherit from the Language Into this class we will pass the unique identifier of our programming language (in our case it is "ru.hh.plugins.Ignore" ).

      Create a language for GitIgnore files

      class IgnoreLanguageprivate constructor(): Language("ru.hh.plugins.Ignore", "ignore", null), InjectableLanguage {companion object {val INSTANCE = IgnoreLanguage()}override fun getDisplayName(): String {return "Ignore()($id)"}}

      There is a peculiarity here: some developers add non-unique string as an identifier. Because of this, the integration of your plugin with other plugins can break. We’re good, we have a unique string.

      The next thing to do after we have created the Language , is to create FileType Inherit from class LanguageFileType , use the language instance we defined to initialize it, override a few very simple methods. That’s it. Now we can use the newly created FileType

      Create your own FileTypefor .gitignore

      class IgnoreFileType(language: Language): LanguageFileType(language) {companion object {val INSTANCE = IgnoreFileType(IgnoreLanguage.INSTANCE)}override fun getName(): String = "gitignore file"override fun getDescription(): String = "gitignore files"override fun getDefaultExtension(): String = "gitignore"override fun getIcon(): Icon? = null}

      Completing the creation of the file

      After you find all the necessary FileType -s, I recommend that you create a special container called TemplateData – it will contain all the data about the template you want to generate code from. It will contain the name of the template file, the name output -file, which you will get after the code generation, the desired FileType And finally, PsiDirectory where you will add the file you created.


      data class TemplateData(val templateFileName: String, val outputFileName: String, val outputFileType: FileType, val outputFilePsiDirectory: PsiDirectory?)

      Then we come back to FreeMarker -from it, we get the template file, using the StringWriter we get the text, in PsiFileFactory generate PsiFile with the needed text and type. We add the created file to the desired directory.

      Create a PsiFilein the desired folder

      fun createFromTemplate(data: FileTemplateData, properties: Map<String, Any> ): PsiFile{val template = freeMarkerConfig.getTemplate(data.templateFileName)val text = StringWriter().use { writer ->template.process(properties, writer)writer.buffer.toString()}return psiFileFactory.createFileFromText(data.outputFileName, data.outputFileType, text)}

      That way, the PSI structure is accounted for, and IDEA and other plugins will see what we did. This can have benefits: For example, if the plugin for Git sees that you added a new file, it will show a dialog asking if you want to add those files to Git.

      Conclusions about code generation

      • File text can be generated by FreeMarker. Very handy.
      • You have to consider the PSI structure when generating files, otherwise it will go wrong.
      • If you want to generate files with PsiFileFactory, you will have to find FileType-types somewhere.

      And now we come to the last, most delicious practical part, which is the modification of the code.

      Code modification

      Actually it is nonsense to create a plugin for code generation only, because it is possible to generate code with other tools, e.g. FreeMarker -tool. But this is what FreeMarker -you can’t do with – is modify the code.

      There are several code modification tasks on our checklist, let’s start with the simplest one, modifying settings.gradle file.

      Modification of settings.gradle

      Let me remind you what we want to do: we need to add to this file a couple of lines which will describe the path to the new module created :

      Description of the module path

      // settings.gradleinclude ':analyticsproject(':analytics').projectDir = new File(settingsDir, 'core/framework-metrics/analytics)...include ':feature-worknear'project(':feature-worknear').projectDir = new File(settingsDir, 'feature/feature-worknear')

      I scared you a little earlier that you should always be sure to consider the PSI structure when working with files, otherwise everything will burn out won’t work. In fact, in simple tasks, like adding a couple of lines to the end of a file, you don’t need to do that. You can add some lines to the file by using the usual java.io.File To do this, we find the path to the file, create an instance java.io.File , and with the Cotlin extensions -functions, we add two lines to the end of this file. You can do so, IDEA will see your changes.

      Adding lines to the settings.gradle file

      val projectBaseDirPath = project.basePath ?: return
      val settingsPathFile = projectBaseDirPath + "/settings.gradle"

      val settingsFile = File(settingsPathFile)

      settingsFile.appendText("include ‘:$moduleName’")
      "project(‘:$moduleName’).projectDir = new File(settingsDir, ‘$folderPath’)"

      But ideally, of course, it’s better through a PSI structure – it’s more reliable.

      Donastroyka kapt-a for Toothpick

      Once again, let me remind you of the problem: The application module has build.gradle file, and inside it, the annotation processor settings. And we want to add the package of our created module in a specific place.


      Fantastic Plugins,vol.2.Practice

      Our goal is to find a certain PsiElement after which we plan to add our string. The search for an element starts with a search for PsiFile -a, which stands for build.gradle application module file. And to do that, we need to find the module inside which we are going to look for the file.

      Search for a module by name

      val appModule = ModuleManager.getInstance(project).modules.toList().first { it.name == "headhunter-applicant" }

      Next, using the utility class FilenameIndex it is possible to find PsiFile by its name, specifying the found module as the search area.

      Search for PsiFile by name

      val buildGradlePsiFile = FilenameIndex.getFilesByName(appModule.project, "build.gradle", appModule.moduleContentScope).first()

      After we find PsiFile, we can start looking for PsiElement. In order to find it, I recommend to install a special plugin. PSI Viewer This plugin adds a special tab to IDEA, there you can see the PSI structure of the open file.

      Fantastic Plugins,vol.2.Practice

      If you open some file (e.g. build.gradle) and move the cursor to the line of code you are interested in, this plugin will take you in the PSI structure to the corresponding element.

      Fantastic Plugins,vol.2.Practice

      This is very handy – you’ll be able to figure out exactly what element you’re looking for inside your PsiFile -a.

      Let’s go back to our problem. We found PsiFile Inside it, you can use this sheet to find the item you are looking for.

      Find the right PsiElement

      val toothpickRegistryPsiElement= buildGradlePsiFile.originalFile.collectDescendantsOfType<GrAssignmentExpression> ().firstOrNull { it.text.startsWith("arguments") }?.lastChild?.children?.firstOrNull { it.text.startsWith("toothpick_registry_children_package_names") }?.collectDescendantsOfType<GrListOrMap> ()?.first()?: return

      Who’s there…? What’s going on here? We sequentially go down to the right element from the very top of the PSI tree. First we get all the descendants of the tree with type GrAssignmentExpression , then we choose the one which describes the expression arguments = [ … ] Then we go down from this element further and find among its descendants the element toothpick_registry_children_package_names = [.] , and take the Groovy-map element itself out of it.

      When the right one is found PsiElement , it remains to modify it. We need to add a line with the name of the package of the new module to the found mappu. To do this, we will have to create it correctly.

      For each programming language the PSI elements are unique, which means that to create the PsiElementFactory of the programming language in whose context we work. Modifying a Java file? Need a factory for Java elements. Working with Groovy? Then GroovyPsiElementFactory And so on.

      Take PsiElementFactory of the desired language is easiest from other plugins. Since we have already plugged in the Groovy and Kotlin plugin dependencies, we have the element factories of these languages in our pocket.

      Creating a PsiElement with package name

      val factory = GroovyPsiElementFactory.getInstance(buildGradlePsiFile.project)val packageName = config.mainParams.packageNameval newArgumentItem = factory.createStringLiteralForReference(packageName)

      All that remains is to add the created element to the previously found PsiElement
      Adding a line to the Map-ku


      Donastroyka kapt-a for Moxy in application module

      The last code modification task on the checklist is to fine-tune the kapt for Moxy in the application module.Just a reminder: we want to add the package name of the new module to the annotation @RegisterMoxyReflectorPackages


      Fantastic Plugins,vol.2.Practice

      In fact, this problem can be solved in the same way as the previous one: find PsiFile , find PsiElement , modify it… But I’ll show a slightly different way to demonstrate a few more possibilities when working with PsiElement -s.

      You could have gone the following way: look for a class that is annotated @RegisterMoxyReflectorPackages , then get the list of values in the attribute value of this annotation, modify it, and recreate the annotation from scratch with the new list.

      Let’s start by finding the module in which we will look for our file. Then, using the utility class PsiManager , we will find PsiClass of the desired annotation.

      Find PsiClass annotations @RegisterMoxyReflectorPackages

      val appModule = ModuleManager.getInstance(project).modules.toList().first { it.name == "headhunter-applicant" }val psiManager= PsiManager.getInstance(appModule.project)val annotationPsiClass = ClassUtil.findPsiClass(psiManager, "com.arellomobile.mvp.RegisterMoxyReflectorPackages") ?: return

      With the utility class AnnotatedMembersSearch searches for all classes marked with this annotation within a given module.

      Looking for a class marked with an annotation

      val annotatedPsiClass = AnnotatedMembersSearch.search(annotationPsiClass, appModule.moduleContentScope).findAll()?.firstOrNull() ?: return

      Finding the class, we get PsiElement of the annotation itself, pull the list of values inside the value attribute from it. Then we generate an updated list of packages, which we then use to recreate the annotation.

      We get the list of packages for the new version of the annotation

      val annotationPsiElement = (annotatedPsiClass.annotations.first() as KtLightAnnotationForSourceEntry).kotlinOriginval packagesPsiElements = annotationPsiElement.collectDescendantsOfType<KtValueArgumentList> ().first().collectDescendantsOfType<KtValueArgument> ()val updatedPackagesList = packagesPsiElements.mapTo(mutableListOf()) { it.text }.apply { this += ""${config.packageName}"" }val newAnnotationValue = updatedPackagesList.joinToString(separator = ", n")

      By means of KtPsiFactory we create a new PsiElement – annotation with the updated list and replace the old annotation with the new one.

      Re-create the annotation and replace the old annotation with the new one

      val kotlinPsiFactory = KtPsiFactory(project)val newAnnotationPsiElement = kotlinPsiFactory.createAnnotationEntry("@RegisterMoxyReflectorPackages(n$newAnnotationValuen)")val replaced = annotationPsiElement.replace(newAnnotationPsiElement)

      Problem solved.

      What can go wrong? Our code style can go wrong. Don’t worry, there is also a utility class inside IDEA to solve this problem: CodeStyleManager.

      Fixing code style


      We have solved all the problems on our checklist, let’s summarize the part about code modification.


      • It is possible to modify the code inside the plugin, but you have to do it through the PSI-structure, which means you have to figure it out.
      • Remember that PSI is tied to a specific programming language, and you have to use the right PsiElements to make it work.

      What to do next?

      Let’s summarize.

      • Developing a plugin is not very difficult – at least now that you have read this article to the end.
      • Plugins really help automate some of your tasks. For example, we were able to automate the creation of modules. This one has its own benefits: in our case, we were able to speed up module creation by almost two times.
      • Where do you get your plugin information from? I suggest just looking at other people’s plugins. Unfortunately, IDEA plugin documentation. is not as good as we would like it to be. It has a lot of theoretical information that may not be useful from a practical point of view. – To solve a particular problem, just search GitHub for other people’s plugins. Study them, and you’ll find what you need.
      • If you need some utility class, you probably have it inside IntelliJ IDEA a long time ago. You type in what you need, add the word Util or Manager and you will probably find a utility class that will solve your problem.
      • One last piece of advice: do your debugging on small projects. Unfortunately, the challenge runIde which runs a separate IDEA instance, takes a very long time to get up. It’s very slow, and if you try to debug your plugin on a hh.ru-level project, checking your changes will take quite a long time.

      That’s about it. Fork over our project , see the inside, ask questions – we’ll answer them.


      • Did you spend a lot of time developing the plugin?

      I did this task on a residual basis, so in terms of time it all stretched out quite a bit. If you try to calculate the net time, though, it’s about 2 or 3 weeks.

      • Did you encounter any problems when upgrading IDEA to the new version, did anything break in the plugin?

      Yes, with IDEA updates some parts of the IDEA SDK change, methods become deprecated, some disappear altogether, you have to adjust. But the SDK methods have good documentation, they usually write there what to replace the call.

      • Have there been problems integrating the plugin with plugins already installed?

      Only once – when working with gitignore files. Just because of non-unique language identifier.

      • Have there been problems running the plugin on different operating systems?

      We’ve run our plugin on Android Studio on Mac OS as well as under Ubuntu, no problems. Heard there are problems with Windows, but we haven’t tried it.

      You may also like