How to Create an IntelliJ Plugin — Let‘s Build a Dictionary Finder

IntelliJ IDEA is one of the most popular IDEs used by developers worldwide. Out of the box, it provides a comprehensive set of features for productive coding in Java and many other languages. But one of IntelliJ‘s greatest strengths is its extensibility through plugins.

As developers, we often find ourselves wishing our IDE had some additional functionality that would make our lives easier. The great news is, with IntelliJ plugins, we can build those features ourselves! Plugins allow us to enhance and customize the IDE to perfectly fit our development workflow.

In this article, we‘ll walk through the process of creating a simple yet useful IntelliJ plugin from scratch. Our plugin will add dictionary search functionality, allowing you to easily look up definitions of words from within the IDE. Let‘s dive in!

Setting Up the Project

The recommended way to create an IntelliJ plugin is by using the Gradle build system along with the IntelliJ Platform Plugin Template. This template makes it super easy to get a new plugin project up and running quickly.

First, make sure you have the Gradle and Plugin DevKit plugins installed in IntelliJ IDEA. Open the Plugins settings and verify they are enabled:

Enable the Gradle and Plugin DevKit plugins in IntelliJ

With those plugins active, create a new project by clicking New Project on the welcome screen. Choose Gradle as the build system and select IntelliJ Platform Plugin as the project type:

Create a new IntelliJ Platform Plugin project with Gradle

Fill out the project name and location. For this example, let‘s call our plugin "Dictionary Finder". You can leave the other settings at their defaults and click Finish.

Configure the plugin project name, location and other settings

IntelliJ will create the project structure and set up the necessary configuration files for the plugin. The most important file is plugin.xml under the src/main/resources/META-INF directory. This is where we‘ll configure our plugin‘s metadata, declare dependencies, and register any components.

Creating the Plugin

Now that we have our project skeleton ready, let‘s start implementing the plugin functionality. The core class for any IntelliJ plugin is a ProjectComponent. This allows the plugin to be notified when a project is opened and closed.

Right-click on the src/main/java directory and create a new Java class named DictionaryProjectComponent. Have it implement ProjectComponent:

Create the DictionaryProjectComponent class implementing ProjectComponent

public class DictionaryProjectComponent implements ProjectComponent {
    private Project project;

    public DictionaryProjectComponent(Project project) {
        this.project = project;
    }

    @Override
    public void projectOpened() {
        // TODO
    }
}

After creating the class, you‘ll see a notification in the top right that "Component is not registered in plugin.xml". Click the "Register Project Component" link to automatically add it to plugin.xml:

Register the DictionaryProjectComponent in plugin.xml

With the component registered, we can now start adding the actual logic for finding and registering dictionaries. The idea is to look for files named project.dic at the root of the project and in any included packages/libraries. We‘ll use IntelliJ‘s Virtual File System to search for the files.

But before we jump into the implementation, let‘s think about how to register the found dictionaries in IntelliJ. Words that the IDE should recognize as valid are stored in dictionary files with a .dic extension. IntelliJ‘s spellchecker allows plugins to register "custom dictionaries" on a project-level basis.

The key class we‘ll need to interact with is SpellCheckerSettings. We can obtain an instance of it by calling SpellCheckerSettings.getInstance(project). It provides a method called getCustomDictionariesPaths() that returns a list of paths to the currently registered custom dictionaries. We simply need to add the paths of any project.dic files we find to that list.

Armed with that knowledge, let‘s fill out the projectOpened method:

@Override
public void projectOpened() {
    List<String> dictionaryPaths = findDictionaryPaths(project);

    SpellCheckerSettings spellCheckerSettings = SpellCheckerSettings.getInstance(project);
    List<String> registeredDictionaries = spellCheckerSettings.getCustomDictionariesPaths();

    for (String path : dictionaryPaths) {
        if (!registeredDictionaries.contains(path)) {
            registeredDictionaries.add(path);
        }
    }
}

private List<String> findDictionaryPaths(Project project) {
    List<String> paths = new ArrayList<>();

    // Find project.dic in root directory
    VirtualFile baseDir = project.getBaseDir();
    VirtualFile rootDictionary = baseDir.findChild("project.dic");
    if (rootDictionary != null) {
        paths.add(rootDictionary.getPath());
    }

    // TODO: Find project.dic in packages

    return paths;
}

The projectOpened method first calls a helper function findDictionaryPaths to search for any project.dic files in the project. It then retrieves the list of registered custom dictionaries from SpellCheckerSettings. Finally, it loops through the found dictionary paths and adds any that aren‘t already registered.

In findDictionaryPaths, we use VirtualFile to check if a project.dic file exists in the project‘s root directory. If found, its path is added to the list. We‘ve left a TODO comment for finding dictionaries in packages, which we‘ll come back to later.

Testing the Plugin

Let‘s test out what we have so far! Since we used the IntelliJ Platform Plugin Template, running the plugin in a development sandbox is super straightforward. Open the Gradle tool window on the right side of the screen and double-click the runIde task:

Use the Gradle runIde task to test the plugin

IntelliJ will build the plugin and launch a fresh instance of the IDE with the plugin automatically installed. Create a new project in the sandbox IDE and add a few dictionary words to a project.dic file in the root directory:

lorem
ipsum
dolor
sit
amet

Reopen the project so projectOpened gets called. Then navigate to File | Settings | Editor | Proofreading | Dictionaries. You should see the path to project.dic listed under "Custom Dictionaries":

The project.dic file was registered as a custom dictionary

Try typing some of the words from project.dic in a code file. They should be recognized as valid words and not marked as misspellings. Awesome, it works! Our plugin successfully registered the dictionary.

Finding Dictionaries in Packages

Many real-world projects utilize packages and libraries in the form of .jar files. It would be great if our plugin could look inside those .jar files for any project.dic files as well. That way, library authors can ship a dictionary of domain-specific terms along with their code.

To find dictionary files inside .jar packages, we can utilize IntelliJ‘s FileTypeIndex:

private List<String> findDictionaryPaths(Project project) {
    List<String> paths = new ArrayList<>();
    // ...

    FileTypeIndex.processFiles(
            PlainTextFileType.INSTANCE, 
            virtualFile -> {
                if (virtualFile.getName().equals("project.dic")) {
                    paths.add(virtualFile.getPath());
                }
                return true;
            },
            GlobalSearchScope.allScope(project)
    );

    return paths;
}

Here FileTypeIndex.processFiles searches for all files of the given file type (PlainTextFileType) in the specified scope (the entire project including libraries). The second argument is a processor function that gets called for each matching file. We check if the file name is project.dic and if so, add its path to our list.

With that change, the plugin will now find and register project.dic files both in the project root and inside any included .jar packages. Pretty slick!

Listening for Dictionary File Changes

At this point, our plugin will register any dictionary files that exist when the project is opened. But what if the user adds, removes, or modifies project.dic files while the project is already open?

To handle that scenario, we need to add a listener for virtual file system changes:

public class DictionaryProjectComponent implements ProjectComponent {

    // ...
    private VirtualFileListener listener;

    @Override
    public void projectOpened() {
        // ...
        listener = new VirtualFileListener() {
            @Override
            public void fileCreated(@NotNull VirtualFileEvent event) {
                handleFileChange(event);
            }

            @Override
            public void fileDeleted(@NotNull VirtualFileEvent event) {
                handleFileChange(event);
            }

            @Override
            public void fileMoved(@NotNull VirtualFileMoveEvent event) {
                handleFileChange(event);
            }

            @Override
            public void fileCopied(@NotNull VirtualFileCopyEvent event) {
                handleFileChange(event);
            }

            private void handleFileChange(VirtualFileEvent event) {
                VirtualFile file = event.getFile();
                if (file != null && file.getName().equals("project.dic")) {
                    updateDictionaries();
                }
            }
        };

        VirtualFileManager.getInstance().addVirtualFileListener(listener);
    }

    @Override
    public void projectClosed() {
        VirtualFileManager.getInstance().removeVirtualFileListener(listener);
    }

    private void updateDictionaries() {
        List<String> dictionaryPaths = findDictionaryPaths(project);
        SpellCheckerSettings spellCheckerSettings = SpellCheckerSettings.getInstance(project);
        spellCheckerSettings.setCustomDictionariesPaths(dictionaryPaths);
    }
}

We create an instance of VirtualFileListener and add it to the VirtualFileManager in projectOpened. The listener methods get called whenever a file is created, deleted, moved, or copied. In each case, we check if the affected file is named project.dic. If so, we call updateDictionaries to re-scan the project and update the registered dictionaries in SpellCheckerSettings.

When the project is closed, we make sure to remove the listener to avoid memory leaks.

Adding Plugin Metadata

We‘re almost ready to ship our plugin! The last thing we need to do is fill out some metadata about the plugin that will be displayed to users in the JetBrains Marketplace.

Open the build.gradle.kts file in the project root. Toward the bottom you‘ll find a section for pluginBundle:

pluginBundle {
    website = "https://www.example.com"
    description = "Adds project-level dictionaries to IntelliJ."
    changeNotes = """
        <p>1.0.0:</p>
        <ul>
            <li>Initial release</li>
        </ul>
    """
    tags = listOf("spellchecker", "dictionary")
}

Fill out appropriate values for the website, description, and tags properties. The description should explain what the plugin does in a concise way. Tags help users find the plugin when searching the Marketplace.

Since this is our initial release, we can leave the changeNotes as-is. But in future versions, we should list any notable changes or new features here.

Deploying the Plugin

The plugin is now ready to deploy! We can publish it to the JetBrains Marketplace where other IntelliJ users can easily find and install it.

Before we can publish, we need to build the plugin as a distributable .zip file. Open the Gradle tool window and run the buildPlugin task:

Use the Gradle buildPlugin task to create the plugin .zip file

The output .zip file will be generated in build/distributions. Now head over to the Publish a Plugin page on the JetBrains Marketplace. You‘ll need to create a free JetBrains account if you don‘t already have one.

Click the "Add new plugin" button and fill out the form with your plugin details. You can upload the plugin .zip file from the last step:

Upload the plugin .zip file to the JetBrains Marketplace

Once you submit the form, your plugin will undergo a short review process by the JetBrains team to ensure it meets their guidelines. Typically within a few business days, you‘ll receive an email notification once the plugin is published.

Congratulations, you‘ve just shipped your first IntelliJ plugin! Users can now find and install "Dictionary Finder" from directly within the IDE under Settings | Plugins.

Conclusion

In this article, we walked through the process of creating an IntelliJ plugin to add project-level dictionary support. We utilized Gradle and the IntelliJ Platform Plugin Template to bootstrap the project. Then we implemented the core functionality of finding dictionary files and registering them with the spellchecker.

Along the way, we learned how to:

  • Set up the plugin project structure and metadata
  • Create a ProjectComponent to listen for project events
  • Search for files using VirtualFile and FileTypeIndex
  • Register and update custom dictionaries in SpellCheckerSettings
  • Listen for virtual file system changes
  • Test the plugin in a development sandbox
  • Build the plugin for distribution and publish it to the JetBrains Marketplace

I hope this guide inspires you to start extending IntelliJ with your own plugins! The plugin we built is just a small example, but the possibilities are endless.

For example, we could extend our dictionary plugin further by:

  • Adding settings to let the user customize the dictionary file name or location
  • Providing quick-fixes to add unknown words to the dictionary
  • Supporting different file formats or sources for dictionary words
  • Integrating the dictionaries with other plugins like code completions

To learn more about IntelliJ plugin development, check out the excellent IntelliJ Platform SDK Docs. The JetBrains Plugin Developers Gitter chat is also a great place to ask questions and get help from the community.

Happy coding, and may your plugin ideas come to life!

Similar Posts