IntelliJ IDEA ate my nREPL

A challenge I’ve recently come across during my work at Nokia is how to enable users of different editing systems than Emacs to use Clojure over the network. For Emacs, it’s just straightforward nrepl.el, Vi users (aka masochists) have something called Nailgun (no, not the one from the Quake game series) to connect to with VimClojure.
As it turns out, however, not everyone is ready for the red pill, yet, and would rather stick to their familiar Java’esque working environment. Among the variety of these, IntelliJ IDEA seems to be quite popular these days and so it became my duty^Wpleasure to enable the poor souls connect to Clojure instances that would run in some different network. The recommended solution for working with Clojure in IntelliJ IDEA is called La Clojure and it packs quite a bunch of Le Features. Configuring it to not start La REPL locally but connect to one over La Network is not among them. Merde!
Le Me to the rescue.

REPL-y in-code

La Clojure is designed to call Java directly, providing all classpath elements determined through the IDE directly as part of the invocation, instead of relying on Leiningen’s or Maven’s built-in swank-clojure or REPL-y support.
At least, it allows one to configure what class to use to invoke Main, so what if we could just trick it to think it was running a local REPL, when it’s a remote one, really?

As La Clojure relies on a certain input/output communication scheme when attaching to the local process, Swank was out of the game – nREPL, on the other hand, is designed to provide a communication scheme together with a server implementation, and all it needs to implement a REPL client – but not the client itself. The existing documentation demonstrates how to communicate separate messages over a connection, but not how to turn the local REPL into a networked one.
On the other end of the spectrum, REPL-y provides a batteries-included solution of an improved Clojure REPL that has built-in nREPL support, but it doesn’t tell you how to use that from your own code. It’s easy if you dig and grock the code, but not obvious. Enough talking – let’s get to the action!

A Guide in PicturesCode

REPL-y launches nREPL through some high-level code that passes down options, normally taken from command line. We need to somehow resemble its behavior.

(defn launch-nrepl [options]
  "Launches the nREPL version of REPL-y, with options already
  parsed out"
  (with-launching-context options
    (reader.jline/with-jline-in
      (set-prompt options)
      (eval-modes.nrepl/main options))))

The options get passed down to the code that establishes the connection.

(defn main
  "Mostly ripped from nREPL's cmdline namespace."
  [options]
  (let [connection         (get-connection options)
        ...]
    ...))
(defn get-connection [{:keys [attach host port]}]
  (let [port (if-not attach
               (-> (nrepl.server/start-server :port (Integer. (or port 0)))
                   deref :ss .getLocalPort))
        url (url-for attach host port)]
    (when (-> url java.net.URI. .getScheme .toLowerCase #{"http" "https"})
      (load-drawbridge))
    (nrepl/url-connect url)))

As it appears, the :attach keyword argument determines whether to start a new server or to connect to one. Somehow. Let’s take a look at url-for.

;; TODO: this could be less convoluted if we could break backwards-compat
(defn- url-for [attach host port]
  (if (and attach (re-find #"^\w+://" attach))
    attach
    (let [[port host] (if attach
                        (reverse (.split attach ":"))
                        [port host])]
      (format "nrepl://%s:%s" (or host "localhost") port))))

It may not seem intuitive, but if attach is set, it takes precedence over both host and port. Without a colon contained in attach, it’s assumed to be a port number to connect to (at localhost), but with a colon, the first part is assumed to be the host instead.

One possible way to come to this conclusion, instead of monkey hacking, could be to run different possible combinations of parameters against the implementation of url-for:

(map (fn [[attach host port]]
      (if (and attach (re-find #"^\w+://" attach))
          attach
        (let [[port host] (if attach
                              (reverse (.split attach ":"))
                            [port host])]
          (format "nrepl://%s:%s" (or host "localhost") port))))
  [["foo:456" "bar" 123]
   ["456" "bar" 123]
   ["456" nil nil]
   [nil "bar" 123]
   ["456 "foo" nil]])
=> ("nrepl://foo:456" "nrepl://localhost:456" "nrepl://localhost:456" "nrepl://bar:123" "nrepl://localhost:456")

At this point, it should be possible to put together the code required for Main.

(ns com.my-company.foobar.core
  (:require [reply.main :as reply]
            (clojure.tools.nrepl.server :as nrepl))) ; note: need to use parentheses here because SyntaxHighlighter parser wants to eat clojure with brackets

(gen-class
 :name com.my-company.foobar.core.nrepl
 :main true
 :prefix "nrepl-")

(defn nrepl-server [port]
  (print (format "Attempting to start nREPL server on localhost:%s..." port))
  (let [server (nrepl/start-server :port (Integer/parseInt port))]
    (println "done")
    server))

(defn nrepl-client [attach]
  (reply/launch-nrepl {:attach attach}))

(defn nrepl-main
  [mode & opts]
  (apply (case mode
           ":server" nrepl-server
           ":client" nrepl-client
           (throw (IllegalArgumentException. "Please specify either :server or :client")))
         opts))

This would go into a file located at src/main/clojure/com/my_company/foobar/core.clj, relative to your project’s source directory. At this point, it should be possible to fire up nREPL servers and REPL-y shells through Java invocations of your JAR. Just what we need for IntelliJ IDEA.

Hooking Up

All dependencies introduced this far, REPL-y and nREPL (the latter is actually a dependency of the former already), need to be handled by your project’s dependency system. La Clojure seems to have a hard time supporting at least Leiningen 2 so far, which is state of the art, so the rest of this guide assumes you’re using Maven for your project, which is very well supported by the IDE in general. Furthermore, it is assumed that clojure-maven-plugin is used for Clojure integration. Add the following dependencies to your pom.xml:

<dependency>
    <groupId>reply</groupId>
    <artifactId>reply</artifactId>
    <version>0.1.2</version>
    <exclusions>
        <exclusion>
            <groupId>ring</groupId>
            <artifactId>ring-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.clojure</groupId>
    <artifactId>tools.nrepl</artifactId>
    <version>0.2.0-beta10</version>
</dependency>

At the time of writing, the latest version of clojure-maven-plugin, 1.3.13, doesn’t include the advertised built-in nREPL server goal, yet. Hence, the manual implementation of an nREPL server from above proves to be useful:

$ mvn clojure:run -Dclojure.mainClass=com.my-company.foobar.core.nrepl -Dclojure.args=:server\ 1234
(...)
Attempting to start nREPL server on localhost:1234...done

So, what’s left… right, connecting! To avoid bad surprises, let’s try from command line first.

$ mvn clojure:run -Dclojure.mainClass=com.my-company.foobar.core.nrepl -Dclojure.args=:client\ 1234
(...)
REPL-y 0.1.2
Clojure 1.4.0
user=> (quit)
Bye for now!

Excellent! If we can achieve the same from the Clojure facet in IntelliJ IDEA, we’ve (almost) reached our goal. Visit the project settings (Ctrl-Alt-Shift-s), add the Clojure facet if necessary, and modify the options to look as follows:

  • JVM arguments: -Xss1m -server
  • REPL options: :client 1234
  • REPL main class: com.my-company.foobar.core.nrepl

Perform a Maven build and fire up the Clojure Console (Ctrl-Shift-F10). If everything goes well, you’ll be greeted with a fresh REPL-y prompt. Hooray! Let’s try evaluating some code.

user=> (+ 1 1)
(+ 1 1))
2

What’s that garbled output? JLine, which is responsible for the readline support of REPL-y, assumes an underlying UNIX-style terminal by default. It needs to be instructed to use a scheme compatible with La Clojure’s console. Re-visit the project settings screen, this time touching just the JVM arguments again:

  • JVM arguments: -Xss1m -server -Djline.terminal=jline
  • REPL options: :client 1234
  • REPL main class: com.my-company.foobar.core.nrepl

Setting -Djline.terminal=jline will make REPL-y and the console get along with each other just fine, so no more garbled output.

If you want to connect to a remote running nREPL, change the REPL options to something like :client host:port.

Putting it all together

For your convenience, all the custom code and settings quoted in this article are made available as Free Software in the public domain over at github.

The final conclusion is that I totally kick butt, once again. Oh, and please tell your friends. Over and out.