DEV Community

Cover image for How I made the world's worst clojurescript REPL
Sean Walker
Sean Walker

Posted on

How I made the world's worst clojurescript REPL

Read the previous post here

TL;DR
I tried to run two clojure web servers and call a JSON endpoint and the JVM fell over on my $3.50/mo 512 MB RAM VPS server, so I switched to clojurescript and wrote my own prepl client for atom

Atom package development

If you've never done any atom package development, you're in for a wild, documentation reading ride! They made it really easy to get started. Of course at first I didn't even know where to start.

You start here btw.

What's funny is that post is 3 years old, so I thought, there's no way this will work with how many releases of atom there have been between then and now, but it does work.

I was initially confused because I kept searching "atom package development" but the blog post is title "… ATOM PLUGIN".

Classic half-baked intro coding tutorial (of which I have written many).

Alright so now I that I kind of understood what was going on and where the files go, it was time to head over to the documentation to learn how to get text out of an editor and put it into another editor and for that I consulted the all knowing, all seeing, all dancing atom documentation.

I initially used the getWordUnderCursor function which was great, but I needed to get the "outer most form" in a clojure form like this 👇

(defn say-hello [s] ; <—— outer most form is `defn`
  (println "hello" s))
Enter fullscreen mode Exit fullscreen mode

...and what I was doing was not working.

So I did what every great artist does and I stole from the pre-eminent clojure repl in atom: proto-repl.

Clearly that code is much, much better than what I would write initially, so I decided to just stick that in my project and I was off to the races!

console.log('cljs-repl:send');

let editor;
if (editor = atom.workspace.getActiveTextEditor()) {
  let range = EditorUtils.getCursorInClojureTopBlockRange(editor);
  let s = editor.getTextInBufferRange(range);
  console.log(s);
}
Enter fullscreen mode Exit fullscreen mode

That's the whole bit of code now using proto-repl's code that gets the outer most form in a clojurescript. After this I was a hop, skip and a jump away from a fully running clojurescript prepl client, since the prepl part does all the heavy lifting for you.

At this point all I had to do now was

  1. Connect to the running clojurescript prepl socket
  2. Send the code over
  3. Parse the EDN that comes back
  4. ..and show it in a new pane!

Wait, parse the EDN, I'm writing this plugin/package/whatever in javascript, how do I parse EDN with javascript?

You know what? Put some fs in the chat because this js is going bye bye.

Clojurescript can parse EDN, no problem.

Switching from javascript to clojurescript

It sounds horrible, rewriting everything, (all 50 lines or something 😅) and starting over with a new thing I don't have any understanding of. I've seen a ton of blog posts with lines and lines of edn and complicated installation procedures to get clojurescript compiling with something called figwheel? It's intimidating. Luckily the clojurescript folks have made things really easy, but it's hard to tell if you're just starting out. Here's the secret sauce to making clojurescript running in node.js work for you.

Step 1. Make a file named build.clj

; build.clj

(ns build
  (:require [cljs.build.api :as b]))

(b/build "src"
  {:output-to "whatever/whatever.js"
   :output-dir "target"
   :optimizations :simple
   :target :nodejs
   :output-wrapper true
   :pretty-print false
   :hashbang false
   :main 'your-app.core})

Enter fullscreen mode Exit fullscreen mode

Step 2. Run it

clj build.clj
Enter fullscreen mode Exit fullscreen mode

Step 3. Profit

That's it, if you really want to get fancy and don't feel like re-running that every time you change something, you can make a watch file that is the same:

; watch.clj

(ns watch
  (:require [cljs.build.api]))

(cljs.build.api/watch "src"
  {:main 'your-app.core
   :output-dir "target"
   :target :nodejs
   :output-wrapper true
   :pretty-print false
   :hashbang false
   :optimizations :simple
   :output-to "whatever/whatever.js"})

Enter fullscreen mode Exit fullscreen mode

… and run clj watch.clj instead. Now when you change any clojurescript file in the src directory, the js will be output to the :output-to directory.

That was easy.

With that out of the way I could finally finish my prepl client:

1. Connect to the running clojurescript prepl socket

(def element (.createElement js/document "div"))
(-> element .-classList (.add "cljs-repl"))


(def modalPanel (-> js/atom .-workspace (.addModalPanel #js {:item element :visible false})))

(def input (.createElement js/document "input"))
(.setAttribute input "style" "padding: 7px")
(set! (.-type input) "input")
(-> input .-classList (.add "input-text"))
(-> input .-classList (.add "native-key-bindings"))
(set! (.-value input) "localhost:5555")
(aset input "onkeydown" (fn [event] (condp = (.-key event)
                                      "Escape" (.hide modalPanel)
                                      "Enter" (connect input)
                                      "")))
(.appendChild element input)


(defn connection []
  (.show modalPanel)
  (.focus input))


(defn connect [input]
  (let [value (.-value input)
        [url port] (.split value ":")]

    (-> client (.connect port url (fn [])))))

Enter fullscreen mode Exit fullscreen mode

2. Send the code over

(def client (net/Socket.))

(defn send []
  (let [editor (-> js/atom .-workspace .getActiveTextEditor)
        range (.getCursorInClojureTopBlockRange EditorUtils editor)
        s (str (.getTextInBufferRange editor range) "\n")]
    (helpers/log s)
    (-> client (.write s))))
Enter fullscreen mode Exit fullscreen mode

3. Parse the EDN

(-> client (.on "data" (fn [s]
                         (helpers/log (pr-str s))
                         (let [data (cljs.reader/read-string (str s))]
                           (-> js/atom .-workspace (.observeTextEditors #(update-repl % data)))))))
Enter fullscreen mode Exit fullscreen mode

4. Show it in a new pane!

(defn update-repl [editor m]
  (when (= "CLJS REPL" (.getTitle editor))
    (when (contains? m :form)
      (do
        (.insertText editor (str (:form m)))
        (.insertNewline editor)))
    (.insertText editor (str (:val m)))
    (.insertNewline editor)))
Enter fullscreen mode Exit fullscreen mode

Everything is awesome

Saying this is the world's worst clojurescript REPL is pretty negative, so I wanted to say something positive at the end of this post and now I have. Everything is awesome.

If you want to make a much better repl than the one I've made, you can stand on the shoulders of regular sized me and get a head start: https://github.com/swlkr/cljs-repl

Top comments (1)

Collapse
 
mauricioszabo profile image
Maurício Szabo

Can I point to a better way to develop atom packages in ClojureScript? Use shadow-cljs :D. I have a post about it in my blog: mauricio.szabo.link/blog/2018/10/0...

(not trying to self promote in any way, just because this approach allows for hot-reload and lots of other interesting things)