From d960661cc87232fd4d647071d460c9574e2f55a1 Mon Sep 17 00:00:00 2001 From: RJ Sheperd Date: Mon, 6 Apr 2026 12:39:49 -0700 Subject: [PATCH 1/2] Add script to auto-format CLJ/S/C files --- .dir-locals.el | 6 +++- scripts/align_requires.clj | 70 ++++++++++++++++++++++++++++++++++++++ scripts/behave.el | 32 +++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 scripts/align_requires.clj create mode 100644 scripts/behave.el diff --git a/.dir-locals.el b/.dir-locals.el index 89b3942e1..07077edd7 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,5 +1,9 @@ ((nil . ((cider-clojure-cli-aliases . ":dev:behave/app") - (cider-default-cljs-repl . figwheel-main)))) + (cider-default-cljs-repl . figwheel-main) + (eval . (progn + (load (expand-file-name "scripts/behave.el" + (locate-dominating-file buffer-file-name ".dir-locals.el"))) + (add-hook 'after-save-hook #'behave-format-on-save nil t)))))) ;; VMS Configuration ;; TODO: Fix to avoid having two separate aliases for projects diff --git a/scripts/align_requires.clj b/scripts/align_requires.clj new file mode 100644 index 000000000..abc62a6c5 --- /dev/null +++ b/scripts/align_requires.clj @@ -0,0 +1,70 @@ +#!/usr/bin/env bb +(ns align-requires + (:require [babashka.fs :as fs] + [clojure.string :as str] + [rewrite-clj.zip :as z] + [rewrite-clj.node :as n])) + +(defn keyword-entry? + "True if a require vector has :as or :refer as its second element. + Uses z/right (skips whitespace) to find the keyword directly." + [zloc] + (when-let [second-child (-> zloc z/down z/right)] + (try (#{:as :refer} (z/sexpr second-child)) + (catch Exception _ nil)))) + +(defn require-vectors + "Returns all zipper locations of [ns-sym :as/:refer ...] vectors + within the :require clause of the given file zipper." + [root-zloc] + (->> root-zloc + (iterate z/next) + (take-while (complement z/end?)) + (filter #(and (z/vector? %) (keyword-entry? %))))) + +(defn ns-sym-len [entry-zloc] + (count (str (z/sexpr (z/down entry-zloc))))) + +(defn set-whitespace-after-ns-sym [entry-zloc spaces] + (let [ws-node (n/whitespace-node (str/join (repeat spaces " ")))] + (-> entry-zloc + z/down ; at ns-sym + z/right* ; at raw whitespace node between ns-sym and keyword + (z/replace ws-node) + z/up))) + +(defn align-file! [path] + (let [content (slurp path) + zloc (z/of-string content {:track-position? true}) + entries (require-vectors zloc)] + (when (seq entries) + (let [max-len (apply max (map ns-sym-len entries)) + ;; Re-parse to get fresh zipper for mutations + aligned (loop [acc (z/of-string content)] + (let [es (require-vectors acc) + ;; Find first entry whose spacing doesn't match target + target (fn [e] (- (inc max-len) (ns-sym-len e))) + bad-e (first (filter (fn [e] + (let [ws (-> e z/down z/right*)] + (not= (str/join (repeat (target e) " ")) + (z/string ws)))) + es))] + (if bad-e + (recur (set-whitespace-after-ns-sym bad-e (target bad-e))) + acc))) + new-content (z/root-string aligned)] + (when (not= content new-content) + (println "Aligned:" path) + (spit path new-content)))))) + +(let [arg (or (first *command-line-args*) ".") + paths (if (fs/regular-file? arg) + [arg] + (->> (fs/glob arg "**/*.{clj,cljs,cljc}") + (map str) + (remove #(re-find #"/(target|node_modules|resources/public/cljs)/" %))))] + (doseq [path paths] + (align-file! path))) + +;; usage +;; bb align_requires.clj diff --git a/scripts/behave.el b/scripts/behave.el new file mode 100644 index 000000000..a9e374a22 --- /dev/null +++ b/scripts/behave.el @@ -0,0 +1,32 @@ +;;; behave.el --- Project-local Emacs helpers for behave-polylith + +(defun behave-format-on-save () + "Run cljfmt fix + align_requires on save, then silently revert buffer." + (let* ((project-root (locate-dominating-file buffer-file-name ".dir-locals.el")) + (align-script (expand-file-name "scripts/align_requires.clj" project-root)) + (file (buffer-file-name)) + (buf (current-buffer)) + (cmd (format "cd %s && bb %s %s && clojure -M:format fix %s" + (shell-quote-argument project-root) + (shell-quote-argument align-script) + (shell-quote-argument file) + (shell-quote-argument file)))) + (make-process + :name "clj-format-on-save" + :buffer nil + :command (list "bash" "-c" cmd) + :sentinel (lambda (_proc event) + (when (and (string-prefix-p "finished" event) + (buffer-live-p buf)) + (with-current-buffer buf + (unless (buffer-modified-p) + (revert-buffer t t t)))))))) + +(defun behave-jack-in-cms () + "Start the VMS/CMS REPL via `cider-jack-in-clj&cljs` with the :dev:behave/cms alias." + (interactive) + (let ((cider-clojure-cli-aliases ":dev:behave/cms")) + (cider-jack-in-clj&cljs nil))) + +(provide 'behave) +;;; behave.el ends here From a080b0d47cd5d5df2459dbaca65f2c589f083e2d Mon Sep 17 00:00:00 2001 From: RJ Sheperd Date: Tue, 7 Apr 2026 10:07:41 -0700 Subject: [PATCH 2/2] Ignore missing docstrings, use private fns --- .clj-kondo/config.edn | 2 +- scripts/align_requires.clj | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index fce8f976d..487692376 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,6 +1,6 @@ {:linters {:shadowed-var {:level :warning} :single-key-in {:level :warning} - :missing-docstring {:level :warning}} + :missing-docstring {:level :ignore}} :lint-as {;; Herb herb.core/defglobal clojure.core/def diff --git a/scripts/align_requires.clj b/scripts/align_requires.clj index abc62a6c5..4a71841de 100644 --- a/scripts/align_requires.clj +++ b/scripts/align_requires.clj @@ -5,7 +5,7 @@ [rewrite-clj.zip :as z] [rewrite-clj.node :as n])) -(defn keyword-entry? +(defn- keyword-entry? "True if a require vector has :as or :refer as its second element. Uses z/right (skips whitespace) to find the keyword directly." [zloc] @@ -13,7 +13,7 @@ (try (#{:as :refer} (z/sexpr second-child)) (catch Exception _ nil)))) -(defn require-vectors +(defn- require-vectors "Returns all zipper locations of [ns-sym :as/:refer ...] vectors within the :require clause of the given file zipper." [root-zloc] @@ -22,10 +22,12 @@ (take-while (complement z/end?)) (filter #(and (z/vector? %) (keyword-entry? %))))) -(defn ns-sym-len [entry-zloc] +(defn- ns-sym-len + [entry-zloc] (count (str (z/sexpr (z/down entry-zloc))))) -(defn set-whitespace-after-ns-sym [entry-zloc spaces] +(defn- set-whitespace-after-ns-sym + [entry-zloc spaces] (let [ws-node (n/whitespace-node (str/join (repeat spaces " ")))] (-> entry-zloc z/down ; at ns-sym @@ -33,7 +35,9 @@ (z/replace ws-node) z/up))) -(defn align-file! [path] +(defn- align-file! + "Aligns all `:imports`/`:requires` dependencies." + [path] (let [content (slurp path) zloc (z/of-string content {:track-position? true}) entries (require-vectors zloc)]