Skip to content

Commit dd25ae7

Browse files
authored
Merge pull request #701 from clojure-emacs/improve-indentation-tests
Improve indentation test coverage and documentation
2 parents 6456b5d + 40c9c76 commit dd25ae7

3 files changed

Lines changed: 575 additions & 29 deletions

File tree

README.md

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -239,28 +239,75 @@ prefixed with some ns (or ns alias):
239239
(put-clojure-indent 'my-ns/do 1)
240240
```
241241

242-
The bodies of certain more complicated macros and special forms
243-
(e.g. `letfn`, `deftype`, `extend-protocol`, etc) are indented using
244-
a contextual backtracking indentation method, require more sophisticated
245-
indent specifications. Here are a few examples:
242+
##### Backtracking (contextual) indentation
243+
244+
Certain macros and special forms (e.g. `letfn`, `deftype`,
245+
`extend-protocol`) contain *nested* sub-forms that each need their
246+
own indentation style. For these, `clojure-mode` uses **backtracking
247+
indentation**: when indenting a line, it walks up the sexp tree to
248+
find a parent form with an indent spec, then uses the current
249+
position within that spec to decide how to indent.
250+
251+
A backtracking indent spec is a **quoted list** where each element
252+
controls indentation at the corresponding argument position
253+
(0-indexed). The allowed elements are:
254+
255+
| Element | Meaning |
256+
|---------|---------|
257+
| An integer N | First N args are "special" (indented further); rest are body |
258+
| `:defn` | Indent like a function/macro body |
259+
| `:form` | Indent like a regular form |
260+
| `nil` | Use default indentation rules |
261+
| A list `(SPEC)` | This position holds a **list of forms**, each indented according to SPEC |
262+
263+
For example, `letfn` uses `'(1 ((:defn)) nil)`:
264+
265+
```clojure
266+
(letfn [(twice [x] ;; pos 0 → spec 1 (1 special arg = the binding vector)
267+
(* x 2)) ;; inside binding → spec ((:defn)) applies:
268+
(thrice [x] ;; each binding is a list of :defn-style forms
269+
(* x 3))] ;; so function bodies get :defn indentation
270+
(+ (twice 5) ;; pos 1+ → spec nil (default → body indentation)
271+
(thrice 5)))
272+
```
273+
274+
And `defrecord` uses `'(2 nil nil (:defn))`:
275+
276+
```clojure
277+
(defrecord MyRecord ;; pos 0 → spec 2 (2 special args: name + fields)
278+
[field1 field2] ;; pos 1 → spec nil (within special args zone)
279+
SomeProtocol ;; pos 2 → spec nil
280+
(some-method [this] ;; pos 3+ → spec (:defn) — each method gets :defn-style
281+
(do-stuff this)))
282+
```
283+
284+
Here are the built-in backtracking specs:
246285

247286
```el
248287
(define-clojure-indent
249-
(implement '(1 (1)))
250-
(letfn '(1 ((:defn)) nil))
251-
(proxy '(2 nil nil (1)))
252-
(reify '(:defn (1)))
253-
(deftype '(2 nil nil (1)))
254-
(defrecord '(2 nil nil (1)))
255-
(specify '(1 (1)))
256-
(specify '(1 (1))))
288+
(letfn '(1 ((:defn)) nil))
289+
(deftype '(2 nil nil (:defn)))
290+
(defrecord '(2 nil nil (:defn)))
291+
(defprotocol '(1 (:defn)))
292+
(definterface '(1 (:defn)))
293+
(reify '(:defn (1)))
294+
(proxy '(2 nil nil (:defn)))
295+
(extend-protocol '(1 :defn))
296+
(extend-type '(1 :defn))
297+
(specify '(1 :defn))
298+
(specify! '(1 :defn)))
257299
```
258300

259301
These follow the same rules as the `:style/indent` metadata specified by [cider-nrepl][].
260-
For instructions on how to write these specifications, see
302+
For more details on writing indent specifications, see
261303
[this document](https://docs.cider.mx/cider/indent_spec.html).
262304
The only difference is that you're allowed to use lists instead of vectors.
263305

306+
Backtracking is controlled by `clojure-use-backtracking-indent`
307+
(default `t`) and limited to `clojure-max-backtracking` levels
308+
(default 3). Disabling backtracking will break indentation for
309+
all forms with list-based specs.
310+
264311
The indentation of [special arguments](https://docs.cider.mx/cider/indent_spec.html#special-arguments) is controlled by
265312
`clojure-special-arg-indent-factor`, which by default indents special arguments
266313
a further `lisp-body-indent` when compared to ordinary arguments.
@@ -724,13 +771,16 @@ See [this ticket](https://github.com/clojure-emacs/clojure-mode/issues/270) for
724771

725772
### Indentation Performance
726773

727-
`clojure-mode`'s indentation engine is a bit slow. You can speed things up
728-
significantly by disabling `clojure-use-backtracking-indent`, but this will
729-
break the indentation of complex forms like `deftype`, `defprotocol`, `reify`,
730-
`letfn`, etc.
774+
`clojure-mode`'s indentation engine is a bit slow due to the
775+
[backtracking indentation](#backtracking-contextual-indentation) logic
776+
that walks up the sexp tree for context. You can speed things up
777+
significantly by setting `clojure-use-backtracking-indent` to `nil`,
778+
but this will break the indentation of forms with list-based specs
779+
(`deftype`, `defrecord`, `defprotocol`, `definterface`, `reify`,
780+
`proxy`, `letfn`, `extend-type`, `extend-protocol`, `specify`,
781+
`specify!`). Simple integer and `:defn` specs will continue to work.
731782

732-
We should look into ways to optimize the performance of the backtracking
733-
indentation logic. See [this ticket](https://github.com/clojure-emacs/clojure-mode/issues/606) for more
783+
See [this ticket](https://github.com/clojure-emacs/clojure-mode/issues/606) for more
734784
details.
735785

736786
### Font-locking Implementation

clojure-mode.el

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,19 @@ to indent keyword invocation forms.
197197
:package-version '(clojure-mode . "5.19.0"))
198198

199199
(defcustom clojure-use-backtracking-indent t
200-
"When non-nil, enable context sensitive indentation."
200+
"When non-nil, enable context-sensitive indentation.
201+
When indenting a line, walk up the sexp tree to find a parent
202+
form with an indent spec, then use the current position within
203+
that spec to determine indentation. This is required for forms
204+
with list-based indent specs like `letfn', `deftype', `defrecord',
205+
`reify', `proxy', etc. Disabling this speeds up indentation but
206+
breaks those forms."
201207
:type 'boolean
202208
:safe 'booleanp)
203209

204210
(defcustom clojure-max-backtracking 3
205-
"Maximum amount to backtrack up a list to check for context."
211+
"Maximum number of levels to walk up the sexp tree for indent context.
212+
Only relevant when `clojure-use-backtracking-indent' is non-nil."
206213
:type 'integer
207214
:safe 'integerp)
208215

@@ -1694,8 +1701,15 @@ symbol properties."
16941701
(defvar clojure--current-backtracking-depth 0)
16951702

16961703
(defun clojure--find-indent-spec-backtracking ()
1697-
"Return the indent sexp that applies to the sexp at point.
1698-
Implementation function for `clojure--find-indent-spec'."
1704+
"Return the indent spec that applies to the sexp at point.
1705+
Walk up the sexp tree (up to `clojure-max-backtracking' levels)
1706+
to find a parent form with an indent spec, then use the current
1707+
position within that parent to index into its spec.
1708+
1709+
For a list spec like (1 ((:defn)) nil), position 0 yields 1,
1710+
position 1 yields ((:defn)), and position 2+ yields nil. A
1711+
sub-spec wrapped in a list like ((:defn)) means \"this position
1712+
holds a list of forms, each indented with :defn style\"."
16991713
(when (and (>= clojure-max-backtracking clojure--current-backtracking-depth)
17001714
(not (looking-at "^")))
17011715
(let ((clojure--current-backtracking-depth (1+ clojure--current-backtracking-depth))
@@ -1836,14 +1850,23 @@ the indentation.
18361850
18371851
The property value can be
18381852
1839-
- `:defn', meaning indent `defn'-style;
1853+
- `:defn', meaning indent `defn'-style (body indentation);
18401854
- an integer N, meaning indent the first N arguments specially
1841-
like ordinary function arguments and then indent any further
1842-
arguments like a body;
1855+
(further indented) and then indent any further arguments like
1856+
a body;
18431857
- a function to call just as this function was called.
18441858
If that function returns nil, that means it doesn't specify
1845-
the indentation.
1846-
- a list, which is used by `clojure-backtracking-indent'.
1859+
the indentation;
1860+
- a list, used for backtracking indentation of complex forms.
1861+
Each element controls indentation at the corresponding argument
1862+
position. Elements can be integers, `:defn', `:form', nil, or
1863+
a nested list like ((:defn)) meaning \"a list of :defn-style
1864+
forms\". See `clojure--find-indent-spec-backtracking' for
1865+
details.
1866+
1867+
When no indent spec is found, forms starting with `def' or `with-'
1868+
get body-style indentation, and forms starting with `:' use
1869+
`clojure-indent-keyword-style'.
18471870
18481871
This function also returns nil meaning don't specify the indentation."
18491872
;; Goto to the open-paren.
@@ -1904,7 +1927,19 @@ This function also returns nil meaning don't specify the indentation."
19041927

19051928
;;; Setting indentation
19061929
(defun put-clojure-indent (sym indent)
1907-
"Instruct `clojure-indent-function' to indent the body of SYM by INDENT."
1930+
"Set the indentation spec of SYM to INDENT.
1931+
1932+
INDENT can be:
1933+
- `:defn' — indent like a function/macro body
1934+
- an integer N — N special args, then body
1935+
- a function — custom indentation function
1936+
- a quoted list — positional backtracking spec (see
1937+
`clojure--find-indent-spec-backtracking')
1938+
1939+
Examples:
1940+
(put-clojure-indent \\='when 1)
1941+
(put-clojure-indent \\='>defn :defn)
1942+
(put-clojure-indent \\='letfn \\='(1 ((:defn)) nil))"
19081943
(put sym 'clojure-indent-function indent))
19091944

19101945
(defun clojure--maybe-quoted-symbol-p (x)

0 commit comments

Comments
 (0)