How I made the world's worst clojurescript REPL
Sean Walker
Posted on May 26, 2019
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.
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))
...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);
}
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
- Connect to the running clojurescript prepl socket
- Send the code over
- Parse the EDN that comes back
- ..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})
Step 2. Run it
clj build.clj
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"})
ā¦ 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 [])))))
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))))
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)))))))
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)))
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
Posted on May 26, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.