Cómo crear un complemento IntelliJ: construyamos un buscador de diccionario simple

La mayoría de los desarrolladores usamos plataformas IntelliJ, ya sea IDEA, PHPStorm, WebStorm, Android Studio, PyCharm y la lista sigue y sigue. Sin embargo, a veces, cuando lo usamos, descubrimos que falta una función, pero no tenemos idea de cómo agregar esa función y, finalmente, vivir sin ella.

En este artículo, cubriré cómo podemos crear un complemento simple para todos los IDE de IntelliJ, de modo que cuando agregue un project.dicarchivo, lo agregará automáticamente como uno de sus diccionarios. También buscará el archivo en paquetes, por lo que los paquetes pueden agregar palabras personalizadas al diccionario. Un .dicarchivo es un diccionario simple donde cada línea es una palabra en el diccionario.

El proyecto es solo una muestra para comenzar a desarrollar sus propios complementos. Pero en realidad también es una característica que me faltaba, ya que cuando desarrollo un paquete personalizado con mis propias palabras, odio tener que agregarlas cada vez en el diccionario de nivel de proyecto.

Creando el proyecto

Al crear complementos para IntelliJ, tenemos la opción de hacerlo en Java o Kotlin. Lo haré en Java, ya que la mayoría de los usuarios están familiarizados con eso. Como este es un proyecto Java, usaremos IntelliJ IDEA como nuestro IDE.

Según la guía de desarrollo, la forma recomendada de crear un proyecto es mediante Gradle. Empezamos abriendo preferencesy compruebe si Gradley Plugin DevKitestán instalados los plugins.

Después de instalar los complementos y reiniciar el IDE, vamos al flujo de nuevos proyectos y debajo Gradle. Aquí ahora hay una opción llamada IntelliJ Platform Pluginque es la que necesitamos.

Luego, realice el resto del flujo de creación del proyecto como de costumbre; en este proyecto, elijo la siguiente configuración.

Configurar plugin.xml

Ahora que tenemos un proyecto, tenemos que configurar nuestro plugin.xmlarchivo y build.gradle. El plugin.xmlarchivo es un archivo utilizado por IntelliJ que define toda la información sobre el complemento. Esto incluye el nombre, las dependencias, las acciones que debe agregar o si debe extender algo en IntelliJ. Básicamente, este archivo define todo lo que debe hacer su complemento y es la raíz de su proyecto. En nuestro build.gradlearchivo, podemos definir algunos de los valores plugin.xmle información como en qué versión de IntelliJ queremos probar nuestro complemento al compilar con gradle.

Comencemos por definir nuestro plugin.xmlarchivo. Puede encontrar el archivo en formato src/main/resources/META-INF/plugin.xml. Queremos que nuestro plugin para estar disponible en todos IntelliJ IDE de lo que establecer nuestra dependenciesa com.intellij.modules.lang. En este momento, nuestro archivo se ve así:

 dk.lost_world.Dictionary Dictionary GitHub com.intellij.modules.lang

Sin embargo, en este momento esto no tiene ninguna lógica y no registramos nada en la plataforma IntelliJ.

Como este proyecto encontrará project.dicarchivos dentro de un proyecto y los registrará como diccionarios en ese proyecto, tendremos que registrar un componente de nivel de proyecto. Este componente se llamará cuando se abra y se cierre un proyecto. Creemos una clase e implementemos la ProjectComponentinterfaz. Cuando pasamos el cursor sobre el nombre de la clase, nos dice que el componente no está registrado.

Entonces podemos llamar a la acción llamada Register Project Componenty la registrará por nosotros en el plugin.xmlarchivo.

Si abrimos se plugin.xmldebe agregar el siguiente código. Si no se agregó al llamar a la acción, simplemente agréguelo manualmente.

  dk.lost_world.dictionary.DictionaryProjectComponent 

Sistema de archivos IntelliJ

Cuando se trabaja con archivos en IntelliJ, utilizamos una V irtual F ile S istema (VFS). El VFS nos brinda una API universal para hablar con archivos, sin que tengamos que pensar si son de FTP, un servidor HTTP o simplemente en el disco local.

A medida que nuestra apariencia del plugin para los archivos de llamada project.dicque lo hará de la necesidad supuesto para hablar con el V irtual F ile S istema. Todos los archivos del VFS son archivos virtuales. Esto puede sonar un poco intimidante, pero en realidad es solo una API para un sistema de archivos y para un archivo. La manera de pensar en ello es sólo que el V irtual F ile S istema es su interfaz de sistema de archivos y los archivos virtuales son sus archivos.

Configuración del corrector ortográfico

Como IntelliJ ya tiene soporte para .dicarchivos y corrector ortográfico en general, lo único que tenemos que hacer es registrar nuestros project.dicarchivos en la configuración del corrector ortográfico.

Todos los ajustes del corrector ortográfico se guardan en una clase llamada com.intellij.spellchecker.settings.SpellCheckerSettings. Para obtener una instancia de él, simplemente llame al getInstancemétodo (la mayoría de las clases de IntelliJ tienen un getInstancemétodo que usa IntelliJ ServiceManagerdebajo).

La clase de configuración obtuvo un método llamado getCustomDictionariesPathsque devuelve todas las rutas a los diccionarios instalados por el usuario.

Al mirar la firma del método, también vemos una anotación llamada AvailableSince. Luego usaremos el valor en esta anotación para especificar la versión mínima requerida para que funcione nuestro complemento.

Como el método devuelve una lista, simplemente podemos llamar addal método para agregar una nueva ruta a un diccionario.

Ejecutando nuestro complemento (build.gradle)

Como ahora sabemos cómo agregar un diccionario al corrector ortográfico, agreguemos un pequeño ejemplo de código en nuestra DictionaryProjectComponentclase para hacer esto.

public class DictionaryProjectComponent implements ProjectComponent { private Project project; public DictionaryProjectComponent(Project project) { this.project = project; } @Override public void projectOpened() { SpellCheckerSettings .getInstance(project) .getCustomDictionariesPaths() .add("./project.dic"); }}

Este código registrará un project.dicarchivo desde la raíz de nuestro proyecto cada vez que se abra el proyecto.

Para probar nuestro pequeño ejemplo, necesitamos actualizar nuestro build.gradlearchivo. En la intellijsección del archivo gradle agregamos qué versión de IntelliJ queremos usar. Este número de versión es el de la AvailableSinceanotación en la SpellCheckerSettingsclase.

plugins { id 'java' id 'org.jetbrains.intellij' version '0.4.4'}group 'dk.lost_world'version '1.0-SNAPSHOT'sourceCompatibility = 1.8repositories { mavenCentral()}dependencies { testCompile group: 'junit', name: 'junit', version: '4.12'}// See //github.com/JetBrains/gradle-intellij-plugin/intellij { pluginName 'Dictionary' version '181.2784.17' type 'IC' downloadSources true}

Al ejecutar el runIdecomando desde gradle, se iniciará una instancia de IntelliJ de la versión específica. Después de iniciar el IDE de prueba, nuestro complemento debería haberse ejecutado. Si abrimos los preferences > Editor > Spelling > Diccorredores, podemos ver en los diccionarios personalizados que ahora se agrega la ruta que especificamos en nuestro ejemplo.

Ahora podemos probar nuestro complemento, por lo que ahora es el momento de compilarlo correctamente para que encuentre los project.dicarchivos y los registre.

En el DictionaryProjectComponent::projectOpenedmétodo, primero debemos buscar todos los archivos llamados project.dicy registrarlos y también agregar un detector de archivos para que cuando project.dicse agreguen nuevos archivos, se registren automáticamente.

Clase de diccionario

Tendremos una clase llamada Dictionary, esta clase contendrá la lógica para que registremos y eliminemos archivos del diccionario. La clase tendrá los siguientes métodos públicos:

void registerAndNotify(Collection files)

void registerAndNotify(VirtualFile file)

void removeAndNotify(VirtualFile file)

void moveAndNotify(VirtualFile oldFile, VirtualFile newFile)

Estos métodos también crearán una notificación sobre lo sucedido, para que el usuario final sepa qué cambió con los diccionarios personalizados. El archivo final para esto se verá de la siguiente manera:

Encontrar todos los archivos de diccionario

Para encontrar todos los archivos de diccionario en el proyecto llamado project.dicusamos la clase FilenameIndex. El archivo está en el espacio de nombres com.intellij.psi.search.FilenameIndex, tiene un método getVirtualFilesByNameque podemos usar para encontrar nuestros project.dicarchivos.

FilenameIndex.getVirtualFilesByName( project, "project.dic", false, GlobalSearchScope.allScope(project))

Esta llamada devolverá todos los archivos virtuales que coincidan con los criterios de búsqueda. Luego colocamos el resultado devuelto en el método de la clase Diccionario registerAndNotify.

@Overridepublic void projectOpened() { Dictionary dictionary = new Dictionary(project); dictionary.registerAndNotify( FilenameIndex.getVirtualFilesByName( project, "project.dic", false, GlobalSearchScope.allScope(project) ) );}

Our code is now able to find project.dic files at start up and register them, if they are not already registered. It will also notify about the newly registered files.

Adding a Virtual File Listener

The next part is for us to listen for changes in virtual files. To do this we need a listener. For this we need the com.intellij.openapi.vfs.VirtualFileListener.

In the docblock for the listener class we can see that to register it we can use VirtualFilemanager#addVirtualFileListener.

Let’s create a class named DictionaryFileListener and implement the methods which we need for our project.

Then we update our projectOpened class to also add the VirtualFileListener.

@Overridepublic void projectOpened() { Dictionary dictionary = new Dictionary(project); dictionary.registerAndNotify( FilenameIndex.getVirtualFilesByName( project, "project.dic", false, GlobalSearchScope.allScope(project) ) ); VirtualFileManager.getInstance().addVirtualFileListener( new DictionaryFileListener(dictionary) );}

Our plugin is now able to find our dictionary files at startup, but also listen for if a dictionary file is added later on. The next thing we need is to add information for our plugin listing.

Adding plugin information

To add information about the plugin, we open the build.gradle file and edit the object patchPluginXml. In here we need to specify which build version is required for the plugin, version of the plugin, description and change notes.

patchPluginXml { sinceBuild intellij.version untilBuild null version project.version pluginDescription """Plugin for having a shared dictionary for all members of your project.

It will automatically find any project.dic files and add themto the list of dictionaries.

It will also search packages for dictionary files and add them to our list of dictionaries. """ changeNotes """

0.2

  • Added support for listening for when a project.dic file is added, moved, deleted, copied.

0.1

  • First edition of the plugin.
"""}

We also update the version property to '0.2'of the gradle project itself. The plugin can now run on all versions since the method for registering custom dictionaries was added.

To test if it generates the desired output, we can run the gradle task patchPluginXml and under build/patchedPluginXmlFiles our generated plugin.xml file will be there.

Since IntelliJ version 2019.1, all plugins supports icons. As this is fairly new a lot of plugins do not have an icon, and your plugin can stand out a lot by having one. The naming convention is pluginIcon.svg as the default icon and pluginIcon_dark.svg for the darcula theme.

The plugin icons should be listed together with the plugin.xml file in the path resources/META-INF.

Building for distribution

The plugin is now ready to be built and shipped. To do this we run the gradle task buildPlugin. Under build/distributions a zip file will appear which you can distribute and install manually in your IDE. Add this zip file as a release under your github repo, so users have the option to download it manually from you repo.

Publishing a plugin

To publish our plugin so it can be downloaded directly from IntelliJ’s plugin repository, we need to login on our JetBrains account on the Plugin Repository website. When in here, a dropdown from your profile name shows an option to upload a plugin.

Input all the information in the dialog (you have to add a license, but that is pretty straightforward with Github). Here we add the distribution zip file.

When you submit the form, you can now see your plugin in the plugin repository. However other users do not have access to it before IntelliJ has approved it. Approving your plugin normally takes 2–3 days.

Updating your plugin via Gradle

After the plugin has been created, we can update it programmatically. To do this the best practice is to create a token. Open up jetbrains hub and go to the authentification tab. From here press New token... and add the scope Plugin Repository.

When pressing create you get a token. Create a file called gradle.properties and add the token under the key intellijPublishToken (remember to git ignore this file).

In our build.gradle file, we simply add the following:

publishPlugin { token intellijPublishToken}

And we can now run the gradle task publishPlugin for publishing our new version. All versions numbers have to be unique or else it will fail updating. When an update is created, you have to wait 2–3 days again for them to approve the update.

After waiting some days our plugin has now been approved and can now be found in the plugin marketplace by searching for dictionary!

Conclusion

I hope this article has given you more courage to start developing your own plugins. One of the biggest problems I had while developing it was to find out which classes to use. IntelliJ has an extensive guide which I would recommend that you read from start to end, however a lot of classes are not mentioned in there. In cases where you get stuck, they have a Gitter chat which is really helpful and there are people from IntelliJ on there to help also.

The source code for this project can be found on Github and the plugin we created is in the JetBrains marketplace.