require
s with rewrite-cljI've seen Emacs functionality that updates a Clojure file's require
form to add a library based on the aliases used in the file, but I never tried to get something similar in Neovim because adding require
forms manually doesn't bothered me very much. However, writing more scratch code lately, I wanted to move a little faster and thought I'd make a script to update require
s. It was also a good excuse to finally try out rewrite-clj, which comes packaged with Babashka.
At a high level, the script reads stdin, parses it as Clojure forms, modifies it with rewrite-clj's zipper functions, then prints the modified code. I added a command to Neovim which pipes the current file through the script and updates the cursor position based on how much longer the new file is than the original.
In the Clojure script, let's parse the contents of stdin and get an S-expression of the code:
(require '[rewrite-clj.zip :as z])
(let [zloc (z/of-string (slurp *in*))
sexpr (-> zloc z/up z/sexpr)])
To find the namespaces that are used, we can walk the S-expression with tree-seq
and collect results:
(defn namespaces [form]
(into #{}
(comp
(filter (every-pred symbol? namespace))
(map (comp symbol namespace)))
(tree-seq coll? seq form)))
(let [zloc (z/of-string (slurp *in*))
sexpr (-> zloc z/up z/sexpr)
nses (namespaces sexpr)])
The namespaces
function isn't as robust as I'd like. It picks up symbol namespaces that don't need to be required, such as System
and java.util.UUID
, as well as namespaces on quoted symbols. I haven't though of a simple way to address that yet, short of specifically listing namespaces to ignore. If you have an idea, email me.
To support short aliases for libraries I commonly use, I created an import-aliases.edn
file that looks like this:
{cli clojure.tools.cli
curl babashka.curl
deps babashka.deps
io clojure.java.io
json cheshire.core
pods babashka.pods
shell clojure.java.shell
str clojure.string
walk clojure.walk}
Let's create vectors to pass to require
. We'll look up the namespace as an alias, and when no alias is found fall back to the original, presumably fully-qualified namespace name:
(def aliases
(edn/read-string (slurp "import-aliases.edn")))
(defn require-vector [lib-or-alias]
(let [lib (get aliases lib-or-alias lib-or-alias)
a lib-or-alias]
[lib :as a]))
(let [zloc (z/of-string (slurp *in*))
sexpr (-> zloc z/up z/sexpr)
nses (namespaces sexpr)
needed (map require-vector nses)])
Now that we know what namespaces need to be required, there are four scenarios to handle:
ns
form with a :require
form inside itns
form but no :require
form inside itrequire
formns
form nor a require
formHere's a function that uses rewrite-clj's zipper functions to find list forms and determines which scenario to handle, where to insert new forms, and what libraries are already required:
(defn find-invoke [zloc prec]
(z/find zloc
(fn [zloc]
(let [form (z/sexpr zloc)]
(and (list? form)
(pred (first form)))))))
(defn existing-requires [[_ & forms]]
(map (fn [form]
(if (and (list? form)
(= 'quote (first form)))
(second form)
form))
forms))
(defn find-insert-point [zloc]
(if-let [ns-loc (find-invoke zloc #{'ns})]
(if-let [require-loc (-> ns-loc z/down (find-invoke #{:require}))]
[:ns-require-update
require-loc
(existing-requires (z/sexpr require-loc))]
[:ns-require-create
(-> ns-loc z/down z/rightmost)])
(if-let [require-loc (find-invoke zloc #{'require})]
[:require-update
require-loc
(existing-requires (z/sexpr require-loc))]
[:require-create
zloc])))
To generate a new set of require
forms, we'll concatenate what we have with what's needed, normalize their forms, deduplicate, and sort:
(defn concat-requires [existing needed]
(->> (concat existing needed)
(map #(if (symbol? %) [% :as %] %))
distinct
(sort-by first)))
(let [zloc (z/of-string (slurp *in*))
sexpr (-> zloc z/up z/sexpr)
nses (namespaces sexpr)
needed (map require-vector nses)
[kind zloc existing] (find-insert-point zloc)
new-requires (concat-requires existing needed)])
If you want a particular ordering, such as sorting all clojure
libraries to be first, you can define a custom comparator and use it in sort-by
:
(defn clojure-first [a b]
(cond
(and (str/starts-with? a "clojure.")
(not (str/starts-with? b "clojure.")))
, -1
(and (not (str/starts-with? a "clojure."))
(str/starts-with? b "clojure."))
, 1
:else
, (compare a b)))
The final step is to edit the require
form depending on which scenario we're in. Let's use a multimethod:
(defmulti edit-requires (fn [_ kind _] kind))
(let [zloc (z/of-string (slurp *in*))
sexpr (-> zloc z/up z/sexpr)
nses (namespaces sexpr)
needed (map require-vector nses)
[kind zloc existing] (find-insert-point zloc)
new-requires (concat-requires existing needed)]
(-> zloc
(edit-requires kind new-requires)
z/root-string
println))
The four different scenarios need to be handled slightly differently, but they all need a list of forms interposed with a newline and some indentation. Here's a function that constructs the sequence of rewrite-clj nodes, with an optional transformation:
(require '[rewrite-clj.node :as n])
(defn lib-forms
([forms indent]
(lib-forms forms identity indent))
([forms transform indent]
(transduce
(comp
(map format-lib-form)
(map transform)
(interpose (n/whitespace-node (str "\n" indent))))
conj forms)))
For the case where there's an existing ns
form but it doesn't have a :require
clause, we can insert the (:require ...)
form to the right of the zipper's location, then insert the whitespace that should precede the form:
(defmethod edit-requires :ns-require-create [zloc _ forms]
(let [nodes (lib-forms forms " ")]
(-> zloc
(z/insert-right* (n/list-node (list* :require (n/spaces 1) nodes)))
(z/insert-right* (n/whitespace-node "\n ")))))
That's the reverse of the way you'd type it, adding the newline and whitespace first then all the new forms, but doing it this order avoids having to explicitly move right to the added node before adding the sibling node.
When updating an existing require
form, we can replace the entire form with rewrite-clj.zip/replace*
:
(defmethod edit-requires :ns-require-update [zloc _ forms]
(let [nodes (lib-forms forms " ")]
(z/replace* zloc (n/list-node (list* :require (n/spaces 1) nodes)))))
When there is no ns
form but there is a require
form, we replace it the same way:
(defmethod edit-requires :require-update [zloc _ forms]
(let [nodes (lib-forms forms n/quote-node " ")]
(z/replace* zloc (n/list-node (list* 'require (n/spaces 1) nodes)))))
And finally, when there's neither a ns
nor a require
form, we add a require
form at the top of the code:
(defmethod edit-requires :require-create [zloc _ forms]
(let [nodes (lib-forms forms n/quote-node " ")]
(-> zloc
(z/insert-left* (n/list-node (list* 'require (n/spaces 1) nodes)))
(z/insert-left* (n/newlines 2)))))
If you prefer, you could add a ns
form, but for me it was simpler not to choose a name for the namespace.
The total script is only about 100 lines long, thanks largely to rewrite-clj. I'm still fiddling with how it works on real-world code, so if you have any suggestions, please email me.