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:
- We tell the
RequestHandler
that we want to serve our resources at the given path (/app.js
,/main.css
,/index.html
) - We tell it how to load the resource. In this case, we’re using the
getResourceAsStream
method available on theClass
object. This will load the resource from the plugin’sresources
folder. The path is relative to theresources
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.