How to automatically add parentheses in Clojure REPL

In previous post we discussed about how Clojure REPL works and the basic component in a REPL. In this post we will introduce how to automatically add parentheses to your one line of Clojure expression entered in REPL.

Suppose you have function named search, what it do is launch a browser and open Google search engine the parameters are search query, you will use it like this

 
user=>(search what is clojure)
 

This is a macro, so you don't have to use double quotes to the parameters, the macro can only do so much for you. But we want go further, remove the parentheses, it becomes

 
user=>search what is clojure
 

Now it more like a command line such as Windows command line or Linux terminal. If you are using Clojure as a scripting language, I have to say it's the best of its kind. You may wish to call Clojure functions like command line tool.

To get it work we need to modify Clojure source code because there are no workarounds solely by Clojure. Download Clojure 1.6.0 and unpack it.

The first modification is change the construction of *in*, the standard input stream.

In LineNumberingPushbackReader, add a constructor

 
 
public LineNumberingPushbackReader(Reader r, int size, int pushbackSize){
    super(new LineNumberReader(r), pushbackSize);
}
 

We need to change the content of input stream, the only way is use the unread method of PushbackReader, the default read only support unread one character.

We can set it to 1000. In RT.java

 
final static public Var IN =
        Var.intern(CLOJURE_NS, Symbol.intern("*in*"),
                   new LineNumberingPushbackReader(new InputStreamReader(System.in),  1000, 1000)).setDynamic();
 
 

Find clj/clojure/main.clj and change skip-whitespace function as below

 
 
(defn is-char-letter [ch]
  (if (or
       (and (>= ch (int \a)) (<= ch (int \z)))
       (and (>= ch (int \A)) (<= ch (int \Z)))
      )
    true
    nil
  )
)
 
(defn auto-paren [s]
  (let [first-ch (.read s)]
    (.unread s first-ch) 
    (if (= first-ch (int \() )
      nil ;; do nothing in this case
      (if (is-char-letter first-ch) ;; else if it start with a-z A-Z
        (let [command-line-no-paren 
                (clojure.string/reverse 
                  (loop [ch (.read s) acc ""]
                    (if (or (= ch (int \newline))(= ch -1))
                      acc
                      (recur (.read s) (str (char ch) acc))
                    )
                  )
                )
          ]
          (.unread s (char-array (str "(" command-line-no-paren ")" \newline)))
        )
      ) 
    )
  )
)
 
(defn skip-whitespace
  [s]
  (loop [c (.read s)]
    (cond
     (= c (int \newline)) :line-start
     (= c -1) :stream-end
     (= c (int \;)) (do (.readLine s) :line-start)
     (or (Character/isWhitespace (char c)) (= c (int \,))) (recur (.read s))
     :else (do (.unread s c) (auto-paren s) :body))))
 

The idea is simple, if the expression not start with '(', we will read the whole expression from input stream and add parentheses to it then push the string back to input stream.

There is one more problem, it works in Windows cmd window, but when I send expression in Emacs cider REPL, it doesn't work, as if they use two different Clojure instance.

Obviously, the cider REPL didn't use the repl loop defined in main.clj.

So I looked the code of nrepl, in interruptible_eval, it accept Clojure code from socket and call the clojure.main/repl but the :read function it uses is not the clojure.main/repl-read, but a customized one.

The pushback reader also different. But we can do the same thing to the reader here, changes two lines of code

 
                    (let [reader (LineNumberingPushbackReader. (StringReader. code) 1000 1000)]
                      (clojure.main/skip-whitespace reader)
 

This is for version 0.2.3, for newer version you should change the source-logging-pushback-reader to the followings

 
(defn- source-logging-pushback-reader
  [code line column]
  (let [reader (LineNumberingPushbackReader. (StringReader. code) 1000 1000)]
    (clojure.main/skip-whitespace reader)
    (when line (set-line! reader (int (dec line))))
    (when column (set-column! reader (int column)))
    reader))
 

Now you can evaluate code like the below

 
user> println "hello"
hello
nil
 

For both Emacs repl and command terminal repl. Happy hacking!