Simple IntelliJ plugin example to help me bookmark code

ama

Adrian Matei

Posted on June 15, 2020

Simple IntelliJ plugin example to help me bookmark code

So, I wrote my first IntelliJ plugin to help me save code snippets easier to Codever. How does it work? Select text > Right mouse click > Save to Codever.

Save to Codever IntellIJ showcase

The plugin is available to download and install in
JetBrain Plugins Repository and the source code is available on Github

In this blog post we'll cover the steps to develop this simple IntelliJ plugin.

Creating the plugin - generate new project for the plugin

Start with the Plugin DevKit, which is a bundled IntelliJ IDEA plugin for developing plugins for the IntelliJ Platform using IntelliJ IDEA’s own build system. It provides its custom SDK type and a set of actions for building plugins within the IDE.

Creating a Plugin Project

In IntelliJ, on the main menu, choose File | New | Project. The New Project wizard starts and select IntelliJ PLatform Plugin

New Plugin Project

Once you select a name (in our case bookmarks.dev-intellij-plugin), go to File | Project Structure
and select the IntelliJ Platform SDK as the default SDK for the plugin module

The plugin implementation

To achieve the goal of the plugin we need to define a custom action

I hope you are all aware of the Ctrl/Cmd+Shift+A shortcut to search and call all the possible actions in IntelliJ

Creating the Custom Action

Before going any further I recommend you read the Action System Docs

To create our custom action, we need to extend the abstract class AnAction.
Classes that extend it, should override AnAction.update() and AnAction.actionPerformed().

  • The update() method implements the code that enables or disables an action.
  • The actionPerformed() method implements the code that executes when an action is invoked by the user.

Let's see the main class and then split into pieces and explain it individually:

package codever.intellij.plugin;

import com.intellij.ide.BrowserUtil;
import com.intellij.lang.Language;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class SaveToCodeverAction extends AnAction {

    /**
     * Only make this action visible when text is selected.
     * <p>
     * The update method below is only called periodically so need
     * to be careful to check for selected text
     * https://jetbrains.org/intellij/sdk/docs/basics/action_system.html#overriding-the-anactionupdate-method
     *
     * @param e
     */
    @Override
    public void update(AnActionEvent e) {
        // Get required data keys
        final Project project = e.getProject();
        final Editor editor = e.getData(CommonDataKeys.EDITOR);

        // Set visibility only in case of existing project and editor and if a selection exists
        e.getPresentation().setEnabledAndVisible(project != null
                && editor != null
                && editor.getSelectionModel().hasSelection());
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
        final String selectedCode = getSelectedCode(e);
        final String languageTag = getLanguageTag(e);
        final String title = getTitle(e);
        final String comment = getComment(e);
        final String sourceUrl = getSourceUrl(e);

        String url = getUrl(languageTag, sourceUrl, title, selectedCode, comment);
        if (url != null) {
            BrowserUtil.browse(url);
        }
    }

    private String getSelectedCode(AnActionEvent e) {
        final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
        return editor.getSelectionModel().getSelectedText();
    }

    private String getLanguageTag(AnActionEvent e) {
        String languageTag = "";
        PsiFile file = e.getData(CommonDataKeys.PSI_FILE);
        if (file != null) {
            Language lang = e.getData(CommonDataKeys.PSI_FILE).getLanguage();
            languageTag = lang != null ? lang.getDisplayName().toLowerCase() : null;
        }
        return languageTag;
    }

    private String getTitle(AnActionEvent e) {
        return "Change me";
    }

    private String getComment(AnActionEvent e) {
        VirtualFile vFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
        String fileName = vFile != null ? vFile.getName() : null;

        final Project project = e.getData(PlatformDataKeys.PROJECT);
        String projectName = project != null ? project.getName() : null;
        StringBuilder sb = new StringBuilder();
        if (projectName != null) {
            sb.append("**Project**: `" + projectName + "`" );
        }
        if (fileName != null) {
            sb.append(" - **File**:  `" + fileName + "`" );
        }
        return sb.length() > 0 ? sb.toString() : null;
    }

    private String getSourceUrl(AnActionEvent e) {
        VirtualFile vFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
        return vFile != null ? vFile.getUrl() : null;
    }

    private String getUrl(String languageTag, String sourceUrl, String title, String selectedCode, String comment) {
        try {
            StringBuilder sb = new StringBuilder("https://www.codever.land/my-snippets/new?") ;
            sb.append("code=" + URLEncoder.encode(selectedCode, "UTF-8"));
            if (title != null) {
                sb.append("&title=" + URLEncoder.encode(title, "UTF-8"));
            }
            if (comment != null) {
                sb.append("&comment=" + URLEncoder.encode(comment, "UTF-8"));
            }
            if (sourceUrl != null) {
                sb.append("&sourceUrl=" + URLEncoder.encode(sourceUrl, "UTF-8"));
            }
            if (languageTag != null) {
                sb.append("&tags=" + URLEncoder.encode(languageTag, "UTF-8"));
            }

            return sb.toString();
        } catch (UnsupportedEncodingException unsupportedEncodingException) {
            unsupportedEncodingException.printStackTrace();
            return null;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The update() method

After making sure a project is open, and an instance of the Editor is obtained, we need to check if any selection is available. The SelectionModel interface is accessed from the Editor object. Determining whether some text is selected is accomplished by calling the SelectionModel.hasSelection() method. The enabled/disabled state and visibility of an action is set using Presentation.setEnabled():

    @Override
    public void update(AnActionEvent e) {
        // Get required data keys
        final Project project = e.getProject();
        final Editor editor = e.getData(CommonDataKeys.EDITOR);

        // Set visibility only in case of existing project and editor and if a selection exists
        e.getPresentation().setEnabledAndVisible(project != null
                && editor != null
                && editor.getSelectionModel().hasSelection());
    }
Enter fullscreen mode Exit fullscreen mode

When an action is disabled AnAction.actionPerformed() will not be called.

See Working with Text for
more details about how to use actions to access caret placed in a document open in an editor.


The actionPerformed() method

This is the code executed when the user triggers the action:

    @Override
    public void actionPerformed(AnActionEvent e) {
        final String selectedCode = getSelectedCode(e);
        final String languageTag = getLanguageTag(e);
        final String title = getTitle(e);
        final String comment = getComment(e);
        final String sourceUrl = getSourceUrl(e);

        String url = getUrl(languageTag, sourceUrl, title, selectedCode, comment);
        if (url != null) {
            BrowserUtil.browse(url);
        }
    }
Enter fullscreen mode Exit fullscreen mode

This fills the required metadata for the codelet and passes it as query parameters (don't forget to encode the values) to www.bookmarks.dev url via BrowserUtil.browse(url) method. This opens a new browser tab with the given url.

You will now go through the individual steps and see how you can achieve that with IntelliJ's SDK.

Get selected text in Editor

You can use the SelectionModel to access the selected code:

    private String getSelectedCode(AnActionEvent e) {
        final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
        return editor.getSelectionModel().getSelectedText();
    }
Enter fullscreen mode Exit fullscreen mode

For multi caret scenarios you should use the CaretModel, this does no apply in this context

Get the language tag of the current file in the IntelliJ project

When saving snippets, you should define tags to easily identify them later. I set the first tag of the snippet with the language of the file where you select the code from. You can do this by accessing the PsiFile and then its Language object:

    private String getLanguageTag(AnActionEvent e) {
        String languageTag = "";
        PsiFile file = e.getData(CommonDataKeys.PSI_FILE);
        if (file != null) {
            Language lang = e.getData(CommonDataKeys.PSI_FILE).getLanguage();
            languageTag = lang != null ? lang.getDisplayName().toLowerCase() : null;
        }
        return languageTag;
    }
Enter fullscreen mode Exit fullscreen mode

Get project and file name

The start of snippet's comment is automatically filled with the project's name plus the file where the code snippet
is selected from. You can access these attributes from a virtual file VirtualFile:

    private String getTitle(AnActionEvent e) {
        VirtualFile vFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
        String fileName = vFile != null ? vFile.getName() : null;

        final Project project = e.getData(PlatformDataKeys.PROJECT);
        String projectName = project != null ? project.getName() : null;
        StringBuilder sb = new StringBuilder();
        if (projectName != null) {
            sb.append(projectName);
        }
        if (fileName != null) {
            sb.append(" - " + fileName);
        }
        return sb.length() > 0 ? sb.toString() : null;
    }
Enter fullscreen mode Exit fullscreen mode

Get the file's path

Last but not least, I want to have the file's path to fill the sourceUrl attribute of the snippet. You can get it by accessing the getUrl() method of the same VirtualFile:

    private String getSourceUrl(AnActionEvent e) {
        VirtualFile vFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
        return vFile != null ? vFile.getUrl() : null;
    }
Enter fullscreen mode Exit fullscreen mode

Registering the action

There are two main ways to register an action: either by listing it in the <actions> section of a plugin's plugin.xml file, or through code.

In this plugin we use the configuration plugin.xml possibility:

    <actions>
        <action
                id="Codever.Save.Editor"
                class="codever.intellij.plugin.SaveToCodeverAction"
                text="Save to Codever"
                description="Save snippet to Codever"
                icon="SaveToCodeverPluginIcons.CODEVER_ACTION_ICON">
            <add-to-group group-id="EditorPopupMenu" anchor="last"/>
        </action>
        <action
                id="Codever.Search.Editor"
                class="codever.intellij.plugin.SearchOnCodeverDialog"
                text="Search on Codever"
                description="Launches dialog to input query search on Codever"
                icon="SaveToCodeverPluginIcons.CODEVER_ACTION_ICON">
            <add-to-group group-id="EditorPopupMenu" anchor="last"/>
        </action>
        <action
                id="Codever.Save.Console"
                class="codever.intellij.plugin.SaveToCodeverAction"
                text="Save to Codever"
                description="Save snippet to Codever"
                icon="SaveToCodeverPluginIcons.CODEVER_ACTION_ICON">
            <add-to-group group-id="ConsoleEditorPopupMenu" anchor="last"/>
        </action>
        <action
                id="Codever.Search.Console"
                class="codever.intellij.plugin.SearchOnCodeverDialog"
                text="Search on Codever"
                description="Launches dialog to input query search on Codever"
                icon="SaveToCodeverPluginIcons.CODEVER_ACTION_ICON">
            <add-to-group group-id="ConsoleEditorPopupMenu" anchor="last"/>
        </action>
    </actions>
Enter fullscreen mode Exit fullscreen mode

For the configuration via code possibility and further details check the Register actions official docs

Plugin icon

This is supported beginning in version 2019.1. The plugin Logos are shown in Plugins Repository and in Marketplace. They also appear in the Settings/Preferences Plugin Manager UI in IntelliJ Platform-based IDEs.

Whether online or in the product UI, a Plugin Logo helps users to identify a plugin more quickly in a list.

Plugin Logo Requirements

Naming convention

All the Plugin Logo images must be in SVG format and adhere to the following naming convention:

  • pluginIcon.svg is the default Plugin Logo. If a separate Logo file for dark UI Themes exists in the plugin, then this file is used solely for light UI Themes,
  • pluginIcon_dark.svg is an optional, alternative Plugin Logo for use solely with dark IDE UI Themes.

Location

The .svg files must be placed in the META-INF folder of the plugin distribution file.

For further details see the official documentation - Plugin Logo / IntelliJ Platform SDK DevGuide

Action icon

Also to make it easier to visually recognise the desired action add an icon to it:

Action icon in right click menu

The recommended way to organise icons is to put them to a dedicated icons source root marked as Resources Root, let's say icons under resources.

Then define a class or an interface with icon constants in a top-level package called icons:

package icons;

import com.intellij.openapi.util.IconLoader;

import javax.swing.*;

public interface SaveToCodeverPluginIcons {
    Icon CODEVER_ACTION_ICON = IconLoader.getIcon("/icons/icon.svg");
}
Enter fullscreen mode Exit fullscreen mode

NOTE The path to the icon passed in as argument to IconLoader.getIcon() must start with leading /

Use these constants inside plugin.xml. Note that the package of icons will be automatically prefixed, and must not be added
manually:

        <action
                id="Codever.Search.Console"
                class="codever.intellij.plugin.SearchOnCodeverDialog"
                text="Search on Codever"
                description="Launches dialog to input query search on Codever"
                icon="SaveToCodeverPluginIcons.CODEVER_ACTION_ICON">
            <add-to-group group-id="ConsoleEditorPopupMenu" anchor="last"/>
        </action>
Enter fullscreen mode Exit fullscreen mode

Testing the plugin

It's possible to run and debug a plugin directly from the IntelliJ IDEA. You need a configured special profile (a Plugin Run/Debug configuration) that specifies the plugin module, VM parameters and other specific options. When you run such profile, it launches the IDE with your plugin installed.

Plugin run configuration

For information on how to change the Run/Debug configuration profile, refer to Run/Debug Configuration and Run/Debug Configuration: Plugin in IntelliJ IDEA Web Help.

Deploying/installing the plugin

Right click the project in IntelliJ and select "Prepare Plugin Module for Deployment". This will generate a JAR
file inside the project's directory, which you can install then in Plugins > Install plugin from disk.... You can
also use it to publish the plugin to the JetBrains Plugins Repository.

Conclusion

You've learned in this blog post how to create a basic IntelliJ platform plugin. For more advanced functions the best place to start is the docs of IntelliJ Platform SDK.

Although I primarily use IntelliJ for development (thank you JetBrains for supporting me with an open source license), I switch to Visual Studio Code from time to time. So, my next thing is to write a plugin to save snippets from Visual Studio Code - stay tuned!

💖 💪 🙅 🚩
ama
Adrian Matei

Posted on June 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related