Development Plugins Tips & Tricks

Building a Plugin for WebStorm – Tutorial for JavaScript Developers, Part 1

If you’ve ever wondered how to build a plugin for WebStorm or any other JetBrains IDE, you might know it’s not an easy task. There’s comprehensive documentation on plugin development, however:

  • This documentation is written for those who understand basic Java concepts.
  • There are thousands of open APIs for extending our IDEs and dozens of ways to do one simple task for a plugin.

Where do you start? How to work through if you don’t know any Java? We’ll try to answer these questions by walking you through the process of building three plugins for WebStorm.

In this part, we’ll cover some basic concepts for developing plugins for WebStorm and show you how to build a simple plugin without writing any code. In part 2 and part 3 of this series, we’ll build more complex things.

Step 0: Before you build anything

Like all other JetBrains IDEs, WebStorm is built on top of the open source IntelliJ Platform. Both the IntelliJ Platform and WebStorm are JVM applications, written mostly in Java and Kotlin. Because of this, you can’t build plugins for them using JavaScript or TypeScript. You can use JavaScript-based integrations in plugins, though. For example, the Import Cost plugin runs the import-cost npm package under the hood.

So, where do you start if you’re interested in building a plugin? First of all, double-check whether the functionality you’re looking for already exists. It may be available out of the box or as a plugin in our marketplace. If not, then you can try building a plugin.

There’s some good news for those who don’t know Java. First, you can build some plugins without writing any code, and we’ll see how in a moment. Second, Kotlin, which can be also used for writing plugins, is syntactically similar to TypeScript.

Step 1: Decide on what plugin you want to build

Let’s get started with building our first plugin. There are different types of plugins that we can build:

  • Frameworks. Depending on the framework, adding support for it can be similar to supporting a new language. If a framework extends some existing functionality, supporting it is relatively easy.
  • Tools. Usually, integration with external tools isn’t a big deal. You need to specify a place for execution of the tool and write simple code to run the tool and parse the results.
  • Languages. Full language support requires a lot of work. You need to write a parser / lexer and implement basic functionality like code completion.
  • Others. The IntelliJ Platform lets you change almost every part of the IDE, so the difficulty of such tasks can vary from very simple to extremely difficult.

Our first plugin will be a framework. We’ll use Alpine.js and implement support for its directives as an example. Although there’s an existing plugin for this framework, we decided to write the support from scratch because it’s a perfect example of how easy it can be to implement some functionality.

Step 2: Install IntelliJ IDEA

Now that we’ve decided what plugin we’re going to build, we need to install the IDE that will allow us to do that. We can’t use WebStorm for this purpose, since we need JVM support. Let’s install IntelliJ IDEA. Both Community and Ultimate editions will work. If you go with the latter, which is paid, you can use a free 30-day trial version.

You can download IntelliJ IDEA from our website or using the Toolbox App as shown below.

toolbox-app

Step 3: Get a plugin template

There are several ways to start building a plugin. We recommend using the GitHub plugin template. All you need is to click the Use this template button and specify the plugin name.

use-this-template-on-github

After that, clone the new project and open it in IntelliJ IDEA. You can do this right from the IDE Welcome screen by clicking the Get from VCS button in the top right corner, and specifying your Git repository in the dialog as follows.

get-from-version-control

Step 4: Configure Java

The next step is to configure Java and its related tools. For this, go to File | Project Structure in the main menu and specify the Java version under the Project SDK field. If you see <No SDK> there or the Java version is below 11, you can download a suitable version, as shown below.

configure-java-and-sdk

When downloading JDK, you can choose any vendor with a version above 11, for example, Amazon Corretto.

Step 5: Customize the plugin build

Gradle is the build tool that we’re going to use for running, debugging, and testing our new plugin. Additionally, this tool can help solve some package manager tasks, like downloading required libraries.

You don’t need to install Gradle, because it’s distributed as part of the plugin template (the gradle-wrapper.jar file).

The plugin template uses the gradle.properties file to describe the basic information about the plugin and how to build it.

The properties pluginGroup, pluginName, and pluginVersion define the namespace, name, and version of the plugin:

pluginGroup = mypluginname
pluginName = My Plugin Name
pluginVersion = 1.0

The properties pluginSinceBuild and pluginUntilBuild define a range of possible IDE versions that are compatible with the plugin. We’re building our plugin for WebStorm 2021.2, so let’s add the following values:

pluginSinceBuild = 212
pluginUntilBuild = 212.*

The property pluginVerifierIdeVersions is used to check that the plugin is still compatible with other versions (in terms of APIs) because usually when you write a plugin, you test it with only the latest version. In our case, we can specify the following:

pluginVerifierIdeVersions = 2021.2

The PlatformType and PlatformVersion fields are used to choose which of the IDEs Gradle will use for running and debugging the plugin. It is another IDE that will be downloaded automatically from Gradle and used exclusively by run configurations.

So, when you debug/run the plugin you will have two open IDEs: the first one is for developing the plugin, and the second one is for testing the plugin that we develop.

Although we’re writing a plugin for WebStorm, we have to use IU (IntelliJ IDEA Ultimate) for PlatformType, because currently WebStorm is not available for download from Gradle. This isn’t a big deal since IntelliJ IDEA Ultimate and WebStorm share the same JavaScript support. Since we want to write a plugin for 2021.2 the PlatformVersion should be set to 2021.2.

platformType = IU
platformVersion = 2021.2
platformDownloadSources = true

You can specify an existing WebStorm installation for testing purposes, but it requires additional steps, so we’ll skip it for now.

For more information about Gradle configuration see the documentation.

We also need to make sure that the latest version of the Gradle IntelliJ plugin is specified in the build.gradle.kts file. Right now, the latest version is 1.1.4:

plugins {
        //...
        id("org.jetbrains.intellij") version "1.1.4"
	//...
}

Lastly, we need to make sure that the Gradle model is applied to the project. To do so, click on Load Gradle Changes in the top right corner, or press the refresh button that can be found in the Gradle tool window. It can take some time depending on your internet connection. Gradle downloads all the required resources including IntelliJ IDEA, which will be used for testing.

load-gradle-changes

Step 6: Update plugin.xml

The plugin.xml file is the entry point for an IDE. We can use it to define dependencies, services, actions, and everything else required by the IDE to load a plugin.

Before starting, we need to update some basic information in the plugin.xml file located in src/main/resources/plugin.xml. We can remove all the registered extensions and services, and update the id, name, and vendor fields. The updated plugin.xml should look like this:

<idea-plugin>
   <id>my.plugin.id</id>
   <name>My plugin</name>
   <vendor>My name</vendor>
   <depends>com.intellij.modules.platform</depends>
</idea-plugin>

The name property is the public plugin name. The id property is the internal name of the plugin. After uploading the plugin to the JetBrains Marketplace you can still modify the public name, but id cannot be changed. Vendor is usually the name of a plugin author or a company. Optionally, you can specify more attributes under the vendor property.

<depends>com.intellij.modules.platform</depends> is the default dependency. It must be specified for the plugin to work correctly if there are no other dependencies.

Step 7: Clean up the project

There are a number of things in the plugin template that can be updated. First of all, we can remove all of the files under the src/main/kotlin directory, since we’ve removed the usages of the files from plugin.xml. We can also update README.md and replace the template text with a new plugin description. The file CHANGELOG.md also contains template text that can be updated now or later.

Step 8: Specify dependencies

The new plugin will extend the JavaScript functionality, so we need to specify the JavaScript plugin as a dependency in plugin.xml:

<depends>JavaScript</depends>

We also need to add the plugin to the gradle.properties file to notify Gradle that the plugin should be loaded while running and debugging:

platformPlugins = JavaScriptLanguage

Step 9: Implement Alpine.js simple directives

First of all, we need to find out what is necessary to support Alpine.js. Let’s create a new .html file with Alpine code and see how it works in WebStorm:

<div x-data="{ open: false }">
  <button @click="open = true">Expand</button>
    <span x-show="open">
      Content…
    </span>
</div>

The first thing to notice is that the directives aren’t supported and are marked as wrong attributes for the tags.

no-support-for-directives-in-webstorm

There are several ways to fix the issue, but the most obvious are:

  • Implement attribute descriptors, as is done in the existing Alpine.js plugin.
  • Use web-types for describing the directive, as is done for the Vue.js plugin.

The second way is much easier, so let’s go with that.

The idea behind web-types is that we use the JSON format for describing the elements, such as tags, attributes, directives, and components, instead of implementing them.

Register an extension

Now, back to our plugin template. Let’s start with creating a web-types extension:

  1. Create an empty JSON file, alpine.web-types.json and store it under the resources directory of the project.
  2. Register the new JSON file in the web-types extension point.

As we mentioned before, the entry point for extensions is the plugin.xml file. We need to add the following code inside of the root idea-plugin tag:

<extensions defaultExtensionNs="com.intellij">
   <javascript.webTypes source="alpine.web-types.json" enableByDefault="true"/>
</extensions>

Some notes:

  • The parent tag, extensions, describes that we want to register an extension in the standard com.intellij namespace. The nested tag describes that we’re registering an extension for the javascript.webTypes extension point.
  • The design is a bit verbose, but all it does is explain that we want to use the extension point registered with the path com.intellij.javascript.webTypes. WebStorm has hundreds of extension points, but usually one plugin uses only a few of them.
  • You should also copy the schema file to the root of the project, to provide proper coding assistance in web-types files. Otherwise, the IDE will use the old version of the schema and you will get a lot of “missing attribute” warnings.

Now, it’s time to write support for the first directive. Let’s add the following to our alpine.web-types.json file:

{
 "$schema": "../../../../schema.json",
 "name": "Alpine",
 "version": "1.0.0",
 "contributions": {
   "html": {
     "attributes": [
       {
         "name": "x-data"
       }
     ]
   }
 }
}

A couple of notes:

  • The contributors section defines possible extensions (we plan to use web-types not only to extend the HTML support, but also for JS and CSS).
  • The attributes section means that we add the attributes to all possible tags, as it should work for directives in Alpine.

Running and debugging

Now that we’ve implemented our first feature, let’s test it! The project template already has all the required configurations for running and debugging the plugin. We just need to click the Run or Debug button for the corresponding Run Plugin configuration on the navigation bar.

run-a-run-configuration

If the run configuration isn’t specified, you can run the plugin using the Gradle task runIde from the Gradle tool window.

runide-task

After running it, the IDE will likely ask you for a license, but you can choose Free evaluation if you don’t have an IntelliJ IDEA Ultimate license.

The highlighting works correctly for the directive x-data.

syntax-highlighting-for-directives

Also, the directive shows up in the code completion popup.

directives-in-completion-popup

The directives x-text and x-show can be handled in the same way – by adding them to the web-types JSON file.

Step 10: Implement Alpine.js complex directives

The directives x-on and x-bind are much trickier, working as prefixes for both events and other attributes. Both also have shorthands.

The web-types API provides a way to describe complex attributes. We need to add the pattern property with the following inner structure:

{
 "name": "x-bind",
 "pattern": {
   "items": [
     {
       "path": "/html/attributes",
     }
   ],
   "template": [
     "x-bind:",
     "#item"
   ]
 }
}

Let’s take a closer look at a few things in the code above. The items property is required for adding elements after :. The template property describes the structure of the directive. The first part is x-bind, then we need to insert all the possible elements from the items property.

There is only one problem with the solution: Alpine.js directives are included in the /html/attributes namespace. As a result, the directives x-bind:x-text will also be suggested. This can be fixed by adding a special marker to the attributes “virtual”: true, and excluding virtual elements from items:

"items": [
 {
   "path": "/html/attributes"
   "includeVirtual": false
 }

Now we’ve added support even for such a complex directive as x-bind. Support for x-on and shorthands can be implemented the same way, so let’s skip this part. The only thing to keep in mind is that the directives are very similar to the Vue ones, which are also described using web-types, so you can borrow some ideas from here.

You can find the final version of web-types for Alpine.js directives here. At this point we have simple HTML support for Alpine.js – WebStorm understands the directives and suggests them in the code completion popup.

Step 11: Build your plugin

The next step is to build the plugin. It can be done using the buildPlugin task from the Gradle tool window.

build-plugin

Once the plugin is built, it will go into the build/distributions directory as shown in the picture.

zipped-plugin-file

Step 12: Publish your plugin

We already have a working version of the plugin: it can be installed locally in your IDE using the Install Plugin From Disk action. If you’re ready to share your plugin with the community, you can publish it in our plugin repository.

First, you need to sign in – you can use your existing JetBrains account for this. Then you should be able to upload your plugin to the repository. We’ll need some time to verify the uploaded plugin on our end, but once it’s done it will be available to download for all IDE users!

upload-plugin

That’s it for the first part of this series. You are welcome to check out the second part, where we build a more complex plugin.

Let us know if you find this tutorial helpful and have any questions – just leave a comment below or reach out to us on Twitter. You can also contact us in our dedicated Slack channel for plugin developers.

The WebStorm team

image description