Why we want to serve the browser’s content locally

For the CodeTrail IntelliJ plugin, we use a CEF browser Tool Window to have a unified editor across our extensions. Because we’re building that editor in React, we need to serve it some way. While serving it over the internet would be the easiest, there are good reasons to serve it locally:

  • Developers don’t always have an internet connection while working
  • …if they do, the connection might be slow (I feel that pain regularly when working from the train, where a connection is hard to maintain at 300km/h)
  • Hard-disk latency is of course much lower than network latency
  • The version of the React application is sure to be compatible with the plugin version

Instead, we’ll be serving it from the plugin’s bundle. The resources folder is a natural fit for this. We have a simple build step that copies over the built React application to the resources folder. This is a manual step for now but could be automated with a Gradle action in the future.

JetBrains’ CefLocalRequestHandler

Luckily, JetBrains got us covered. They even briefly mention the CefLocalRequestHandler in the Loading Resources from Plugin Distribution section.# While we could roll our own implementation, theirs is used in production and readily available.

To use it, we need to tell our plugin that it depends on com.intellij.platform.images, which is the module that contains the CefLocalRequestHandler class. For a detailed guide, see the Plugin Dependencies page. First, we add it to the plugin’s build.gradle.kts file:

intellij {
    version.set("2023.2.5")
    type.set("IC") // Target IDE Platform

    plugins.set(listOf("com.intellij.platform.images"))
}

We also need to add it to the plugin.xml file, beneath the other <depends> tags:

<depends>com.intellij.platform.images</depends>

Serving the files

With the CefLocalRequestHandler class available, we need to communicate it which files it should serve. We add it in the place where the JBCefBrowser is created:

JBCefBrowser browser = ...;
CefLocalRequestHandler requestHandler = new CefLocalRequestHandler("http", "localhost");
browser.getJBCefClient().addRequestHandler(requestHandler, browser.getCefBrowser());

// todo: load resources

This will make our files available at http://localhost/ and registers the handler with the browser. Before we use the browser to navigate, we also need to load the files. The RequestHandler provides a handy addResource method for this.

requestHandler.addResource("/app.js", () -> new CefStreamResourceHandler(getClass().getResourceAsStream("/web/react/main.js"), "text/javascript", this));
requestHandler.addResource("/main.css", () -> new CefStreamResourceHandler(getClass().getResourceAsStream("/web/react/main.css"), "text/css", this));

// we can also use Strings instead
requestHandler.addResource("/index.html", () -> new CefStreamResourceHandler(new ByteArrayInputStream(loadAndInjectPluginHtml().getBytes()), "text/html", this));

For the index.html, I am using a String instead that I load from the plugin’s resources folder and do some work on. For this, we need to wrap it in a ByteArrayInputStream to make it available as a stream.

Let me explain what happens here:

  1. We tell the RequestHandler that we want to serve our resources at the given path (/app.js, /main.css, /index.html)
  2. We tell it how to load the resource. In this case, we’re using the getResourceAsStream method available on the Class object. This will load the resource from the plugin’s resources folder. The path is relative to the resources folder.

Loading a page

With all that prep work done, we can finally load our page. We do this by calling the loadURL method on the JBCefBrowser object:

browser.loadURL("http://localhost/index.html");

This should load our previously registered index.html file and display it in the browser.