Autocollapse namespace definitions (and other things) in Emacs

Any sufficiently advanced Clojure namespace is indistinguishable from a Java class. Gosh, thankfully that's false. But what does actually happen is that the ns form gets large and bloaty, just like the imports blob in your everyday Java file. Gone are the times when Clojure namespace declarations were small cozy gardens that you wanted to cultivate and tend to manually. The tooling kept up, so now Cursive promises you "to never have to look at your namespace form again," and clj-refactor also has very handy features to auto-require namespaces, sort and cleanup requires, etc.

Yet that huge intimidating 30-something LoC ns hasn't gone anywhere. I have to see it every time I jump to the top of the file, which I often do to inspect the atoms and refs stored in the current namespace. From mildly inconvenient, it turned into outright annoying, and every Emacs user knows that is the time to get your hands dirty.

HideShow is an awesome Emacs extensions that can collapse/fold parts of the code, just like many IDEs do. HideShow works with many programming languages, but the support for Lisps is especially good since every expression has unambiguous structure. To enable the extension you only need to add hs-minor-mode to the hooks of the programming mode, and then bind some custom keybinding for hs-toggle-hiding (the default one is really atrocious). I roll with <C-tab>.

What about the promised automatic folding? There is no such off-the-shelf functionality, but that's the beauty of Emacs.

(defun hs-clojure-hide-namespace-and-folds ()
  "Hide the first (ns ...) expression in the file, and also all
the (^:fold ...) expressions."
  (interactive)
  (hs-life-goes-on
   (save-excursion
     (goto-char (point-min))
     (when (ignore-errors (re-search-forward "^(ns "))
       (hs-hide-block))

     (while (ignore-errors (re-search-forward "\\^:fold"))
       (hs-hide-block)
       (next-line)))))

(defun hs-clojure-mode-hook ()
  (interactive)
  (hs-minor-mode 1)
  (hs-clojure-hide-namespace-and-folds))

(add-hook 'clojure-mode-hook 'hs-clojure-mode-hook)

You may notice this weird ^:fold thing in the code. Well, with the advent of clojure.spec there's now yet another thing at the top of my file that I love to have, but hate to see constantly. Hence, I "invented" a fold metadata that does nothing inside Clojure itself, but merely instructs our auto-folder to hide the marked block. Let's say I have a namespace like this:

(ns com.example.srsbsns.core
  (:require [buddy.auth :as b.auth]
            buddy.auth.backends.session
            buddy.auth.middleware
            [clojure.core.async :as a :refer [>! <! >!! <!! chan go go-loop]]
            [clojure.java.io :as io]
            [clojure.java.shell :as sh]
            [clojure.string :as str]
            [clojure.spec :as s]
            [clostache.parser :as tmpl]
            [compojure.core :refer [defroutes GET POST]]
            (com.example.srsbsns
             [api :as api]
             [auth :as auth]
             [instance :as instance]
             [project :as project]
             [util :refer :all]
             [state :as state])
            [com.example.srsbsns.util.transit :refer
             [wrap-transit wrap-transit encode-transit]]
            [org.httpkit.client :as http]
            [org.httpkit.server :as hk]
            ring.middleware.defaults
            ring.middleware.session)
  (:import clojure.lang.ExceptionInfo java.util.UUID
           (java.io File FileNotFoundException)))

(^:fold do ;; Define specs
 (s/def ::name (s/with-gen (and string? #(re-matches #"[\w\d-]+" %))
                 #(gen/string-alphanumeric)))
 (s/def ::description string?)
 (s/def ::value string?)
 (s/def ::version nat-int?)
 (s/def ::latest-version nat-int?)
 (s/def ::updated inst?)
 (s/def ::revision (s/keys :req-un [::version ::value]))
 (s/def ::revisions (s/coll-of ::revision))

 (s/def ::full-widget
   (s/keys :req-un [::name ::description ::revisions ::updated]))

 (s/def ::widget-revision
   (s/and (s/keys :req-un [::name ::description ::value ::version ::latest-version])
          #(<= (:version %) (:latest-version %))))

 (s/def ::widget-info
   (s/keys :req-un [::name ::description ::latest-version ::updated])))

(defn do-stuff
  "The one that actually does something!"
  [urgent?]
  (delegate-to-someone-else :urgent true))

Painful, right? With our new hook in place, when we reopen this file again it becomes:

(ns com.example.srsbsns.core...)

(^:fold do ;; Define specs...)

(defn do-stuff
  "The one that actually does the something!"
  [urgent?]
  (delegate-to-someone-else :urgent true))

I rest my case.

One weird trick

If your file is really huge (like, for instance, my ~/.emacs/init.el), you may want to collapse every form of it when first opened. You can achieve this by putting the following block in the bottom of that file:

;; Local Variables:
;; eval: (hs-hide-all)
;; End:

Happy folding!