Last time I talked about plug-ins in atom with ClojureScript, I was using Weasel. Since then, I tried figwheel but it never worked as good as I wanted.
Then, I’ve decided to try shadow-cljs. Also, with shadow, we can build a node library instead of a “generic node application”, and this helped a lot in my current tests. So, right now, I’m going to show how I am developing the next version of Clojure Plus plug-in, and what to expect in the future.
Also, I must add that this is so far the best experience ever on creating Atom packages, so I’ll probably stick with ClojureScript for every future package too (I just need a way to make atom’s spec tests work better with ClojureScript – I’m thinking about using a helper library or something).
Preparation
First, you’ll use atom to create a package. It doesn’t matter if you produce a CoffeeScript or Javascript version, because we’ll delete all source files.
On package.json
, we’ll modify the entrypoint: on the key "main":...
, we’ll write "main": "./lib/main"
.
Then, we’ll create our shadow-cljs.edn
file:
{:source-paths ["src" "test"] ; Put your libs here, or create a project with lein. ; I personally use lein mostly because I already have ; a "profiles" dependencies ready :dependencies [] :builds {:dev {:target :node-library :exports {:config your-lib.core/config :activate your-lib.core/activate :deactivate your-lib.core/deactivate :status-bar-consumer clojure-plus.providers-consumers.status-bar/activate} :output-dir "lib/js" :output-to "lib/main.js" :devtools {:before-load-async your-lib.core/before}}}}
Then, you just need to create a namespace (here, your-lib.core
) and populate it with config
(for configuration parameters), activate
(will be run when your plugin activates) and deactivate
. Also, for REPL-driven it’s ideal to create a before
function, that runs while your code is reloading… and that’s the magical part: we can make our plug-in simply reload Atom’s state when we save a file!
Now, the specifics are kinda tricky (and it gets worse if you’re using “provided/consumed services”), but the idea is that you’ll trigger some functions to be called when you reload your code.
Safe reload of your plug-in
What I have done is to create a namespace called aux
, and there I created an Atom’s CompositeDisposable
: basically, a big collection of Disposable
objects that are disposed together. When before
runs, we .dispose
everything, re-create the CompositeDisposable
and then run activate
again:
(ns my-plugin.aux) (def ^:private atom-ed (js/require "atom")) (def ^:private CompositeDisposable (.-CompositeDisposable atom-ed)) (def subscriptions nil) (defn create-subscriptions [] (reset! subscriptions (atom (CompositeDisposable.)))) ; This, for example, creates a command in Atom: (defn command-for [name f] (let [disp (-> js/atom .-commands (.add "atom-text-editor" (str "my-plugin:" name) f))] (.add @subscriptions disp)))
Then, on your core namespace:
(ns my-plugin.core (:require [my-plugin.aux :as aux])) (defn- toggle [] (prn :TOGGLED!)) (defn activate [] (aux/create-subscriptions) (aux/command-for "toggle" toggle)) (defn deactivate [] (.dispose @aux/subscriptions)) (defn before [done] (deactivate) (done) (activate) (println "Reloaded"))
This needs to be done because shadow-cljs never changes the original defn
– it creates a new one, so you need to de-register everything that you registered in Atom, then re-register everything.
With these simple commands, working in Atom is like a bliss: you fire up shadow-cljs with npx shadow-cljs watch dev
, waits till it compiles, then fire up Atom. When your plugin activates, you’ll see a notification on shadow-cljs: JS runtime connected.. Then, you can fire a ClojureScript REPL with npx shadow-cljs cljs-repl dev
, you can issue commands, inspect your state, and do whatever you want to do.
Workarounds
It’s waaay easier to work with Reagent than with atom-space-pen-views and other packages that creates views “the Atom way”. The problem is that Atom re-binds lots of keybindings, so “tab”, “enter”, and other keys will not work. What I did was to create a “root div” with classes native-key-bindings
and tab-able
, and on my keyamaps .cson
file I added:
'div.tab-able': 'tab': 'native!' 'shift-tab': 'native!'
Then, things are working again. Also, as you’re using Atom (and targeting Node.JS library, not browser), React will not work properly so you’ll need to add cljsjs/react
to your dependencies (in my case, I’ve added [reagent "0.8.1"]
and [cljsjs/react "16.4.1-0"]
, and my views are working fine).
The future of Clojure-Plus
Two of the main pain points in Clojure-plus is performance and ClojureScript support. I’ve tried to debug both of these things, and performance is a nREPL issue – the protocol is simply too complex, adds a lot of overhead, and ClojureScript’s performance degrades REALLY FAST. Also, because of the way the protocol is implemented, I can only use “PiggieBack” on ClojureScript, and sometimes it simply don’t work (and as of now, I’m still not sure why – I simply close my nRepl connection and try again, and most of the times it works correctly).
Also, Clojure-PLUS if for Atom only. This is not really a problem, and I really love the Atom editor, but let’s be realistic – with Microsoft buying github (and having its own Electron-based editor VS Code), Atom’s future became uncertain (also, there are lots of complaints about they breaking their public API, mine included, and simple bugs that don’t get fixed ever). One more thing is that I’m beginning to use some ARM devices (and ClojureScript on they), and Atom simply don’t run on ARM. I could, in theory, open up a nREPL on ARM, connect on Atom, but probably it wouldn’t be the best experience, specially if I want to use Lumo (which I do want).
So, I’m starting two projects: REPL Tooling (that connects on multiple socket REPLs and wraps autocomplete, implementation details, some parsing, UnREPL support, block/top block detection, in summary, everything that don’t depend on the editor), and Chlorine (that’s the “new” Clojure-Plus).
Also, I’m developing both on ClojureScript with Shadow-CLJS, which means that there’s no “compatibility layer” between Javascript and Clojure (and I don’t intend to add one). Also, I’m developing both of these libs inside Atom using both of these libs to evaluate/autocomplete/create documentation from then (it’s kinda fun until you break your code in a way that you need to stop everything, close Atom and shadow-cljs, start again, but this is happening less and less :)). It’s a new kind of inception that’s both fun and really productive.
So, keep watching for news. Probably very soon we’ll have a new plug-in on Atom, and probably on NeoVIM too!