Skip to content

Commit 99629de

Browse files
committed
Rewrite the indent logic
[Fix #282 Fix #296] Use custom-code to calculate normal-indent. Add tests for backtracking indent and several other scenarios. Support a new notation for indent specs. An indent spec can be: - `nil` (or absent), meaning “indent like a regular function”. - A list/vector meaning that this function/macro takes a number of “special” arguments which are indented by more spaces (in CIDER that's 4 spaces), and then all other arguments are indented like a body (in CIDER that's 2 spaces). - The first element is a number indicating how many "special" arguments this function/macro takes. - Each following element is an indent spec on its own, and it applies to the argument on the same position as this element. So, when the argument is a form, it specifies how to indent that argument internally (if it's not a form the spec is irrelevant). - If the function/macro has more aguments than the list has elements, the last element of the list applies to all remaining arguments. So, for instance, if I specify the `deftype` spec as `[2 nil nil fn]` (equivalent to `[2 nil nil [1]]`), it would be indented like this: ``` (deftype ImageSelection [data] Transferable (getTransferDataFlavors [this] (some-function)) SomethingElse (isDataFlavorSupported [this flavor] (= imageFlavor flavor))) ``` (I put `ImageSelection` and `[data]` on their own lines just to show the indentation). Another example, `reify` as `[1 nil fn]` ``` (reify Object (toString [this] (f) asodkaodkap)) ``` Or something more complicated, `letfn` as `[1 [fn] nil]` ``` (letfn [(twice [x] (* x 2)) (six-times [y] (* (twice y) 3))] (println "Twice 15 =" (twice 15)) (println "Six times 15 =" (six-times 15))) ```
1 parent 8431e0d commit 99629de

File tree

2 files changed

+225
-152
lines changed

2 files changed

+225
-152
lines changed

clojure-mode.el

Lines changed: 146 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -649,19 +649,89 @@ symbol property, and its return value should match one of the
649649
allowed values of this property. See `clojure-indent-function'
650650
for more information.")
651651

652-
(defun clojure--symbol-get (function-name)
653-
"Return the symbol PROPERTY for the symbol named FUNCTION-NAME.
652+
(defun clojure--get-indent-method (function-name)
653+
"Return the indent spec for the symbol named FUNCTION-NAME.
654654
FUNCTION-NAME is a string. If it contains a `/', also try only
655-
the part after the `/'."
656-
(or (get (intern-soft function-name) 'clojure-indent-function)
655+
the part after the `/'.
656+
657+
Look for a spec using `clojure-get-indent-function', then try the
658+
`clojure-indent-function' and `clojure-backtracking-indent'
659+
symbol properties."
660+
(or (when (functionp clojure-get-indent-function)
661+
(funcall clojure-get-indent-function function-name))
662+
(get (intern-soft function-name) 'clojure-indent-function)
657663
(get (intern-soft function-name) 'clojure-backtracking-indent)
658664
(when (string-match "/\\([^/]+\\)\\'" function-name)
659665
(or (get (intern-soft (match-string 1 function-name))
660666
'clojure-indent-function)
661667
(get (intern-soft (match-string 1 function-name))
662-
'clojure-backtracking-indent)))
663-
(when (functionp clojure-get-indent-function)
664-
(funcall clojure-get-indent-function function-name))))
668+
'clojure-backtracking-indent)))))
669+
670+
(defvar clojure--current-backtracking-depth 0)
671+
672+
(defun clojure--find-indent-spec-backtracking ()
673+
"Return the indent sexp that applies to the sexp at point.
674+
Implementation function for `clojure--find-indent-spec'."
675+
(when (and (>= clojure-max-backtracking clojure--current-backtracking-depth)
676+
(not (looking-at "^")))
677+
(let ((clojure--current-backtracking-depth (1+ clojure--current-backtracking-depth))
678+
(pos 0))
679+
;; Count how far we are from the start of the sexp.
680+
(while (ignore-errors (clojure-backward-logical-sexp 1) t)
681+
(cl-incf pos))
682+
(let* ((function (thing-at-point 'symbol))
683+
(method (or (when function ;; Is there a spec here?
684+
(clojure--get-indent-method function))
685+
(progn (up-list) ;; Otherwise look higher up.
686+
(clojure-backward-logical-sexp 1)
687+
(clojure--find-indent-spec-backtracking)))))
688+
(when (numberp method)
689+
(setq method (list method)))
690+
(pcase method
691+
((pred sequencep)
692+
(pcase (length method)
693+
(`0 nil)
694+
(`1 (let ((head (elt method 0)))
695+
(when (or (= pos 0) (sequencep head))
696+
head)))
697+
(l (if (>= pos l)
698+
(elt method (1- l))
699+
(elt method pos)))))
700+
((or `defun `:defn)
701+
(when (= pos 0)
702+
:defn))
703+
(_
704+
(message "Invalid indent spec for `%s': %s" function method)
705+
nil))))))
706+
707+
(defun clojure--find-indent-spec ()
708+
"Return the indent spec that applies to current sexp.
709+
If `clojure-use-backtracking-indent' is non-nil, also do
710+
backtracking up to a higher-level sexp in order to find the
711+
spec."
712+
(if clojure-use-backtracking-indent
713+
(save-excursion
714+
(clojure--find-indent-spec-backtracking))
715+
(let ((function (thing-at-point 'symbol)))
716+
(clojure--get-indent-method function))))
717+
718+
(defun clojure--normal-indent (last-sexp)
719+
"Return the normal indentation column for a sexp.
720+
LAST-SEXP is the start of the previous sexp."
721+
(goto-char last-sexp)
722+
(let ((last-sexp-start nil))
723+
(unless (ignore-errors
724+
(while (progn (skip-chars-backward "#?'`~@[:blank:]")
725+
(not (looking-at "^")))
726+
(setq last-sexp-start (prog1 (point)
727+
(forward-sexp -1))))
728+
t)
729+
;; If the last sexp was on the same line.
730+
(when (and last-sexp-start
731+
(> (line-end-position) last-sexp-start))
732+
(goto-char last-sexp-start)))
733+
(skip-chars-forward "[:blank:]")
734+
(current-column)))
665735

666736
(defun clojure-indent-function (indent-point state)
667737
"When indenting a line within a function call, indent properly.
@@ -686,122 +756,52 @@ The property value can be
686756
- a list, which is used by `clojure-backtracking-indent'.
687757
688758
This function also returns nil meaning don't specify the indentation."
689-
(let ((normal-indent (current-column)))
690-
(goto-char (1+ (elt state 1)))
691-
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
692-
(if (and (elt state 2)
693-
(not (looking-at "\\sw\\|\\s_")))
694-
;; car of form doesn't seem to be a symbol
695-
(progn
696-
(if (not (> (save-excursion (forward-line 1) (point))
697-
calculate-lisp-indent-last-sexp))
698-
(progn (goto-char calculate-lisp-indent-last-sexp)
699-
(skip-chars-backward "[:blank:]")
700-
;; We're done if we find the start of line,
701-
(while (and (not (looking-at-p "^"))
702-
;; or start of sexp.
703-
(ignore-errors (forward-sexp -1) t))
704-
(skip-chars-backward "[:blank:]"))
705-
(parse-partial-sexp (point)
706-
calculate-lisp-indent-last-sexp 0 t)))
707-
;; Indent under the list or under the first sexp on the same
708-
;; line as calculate-lisp-indent-last-sexp. Note that first
709-
;; thing on that line has to be complete sexp since we are
710-
;; inside the innermost containing sexp.
711-
(backward-prefix-chars)
712-
(current-column))
713-
(let* ((function (buffer-substring (point)
714-
(progn (forward-sexp 1) (point))))
715-
(open-paren (elt state 1))
716-
(forward-sexp-function #'clojure-forward-logical-sexp)
717-
(method (clojure--symbol-get function)))
718-
;; Maps, sets, vectors and reader conditionals.
719-
(cond ((or (member (char-after open-paren) '(?\[ ?\{))
720-
(ignore-errors
721-
(and (eq (char-before open-paren) ?\?)
722-
(eq (char-before (1- open-paren)) ?\#))))
723-
(goto-char open-paren)
724-
(1+ (current-column)))
725-
((or (eq method 'defun)
726-
(and clojure-defun-style-default-indent
727-
;; largely to preserve useful alignment of :require, etc in ns
728-
(not (string-match "^:" function))
729-
(not method))
730-
(and (null method)
731-
(> (length function) 3)
732-
(string-match "\\`\\(?:\\S +/\\)?\\(def\\|with-\\)"
733-
function)))
734-
(lisp-indent-defform state indent-point))
735-
((integerp method)
736-
(lisp-indent-specform method state
737-
indent-point normal-indent))
738-
((functionp method)
739-
(funcall method indent-point state))
740-
(clojure-use-backtracking-indent
741-
(clojure-backtracking-indent
742-
indent-point state normal-indent)))))))
743-
744-
(defun clojure-backtracking-indent (indent-point state _normal-indent)
745-
"Experimental backtracking support.
746-
747-
Given an INDENT-POINT, the STATE, and the NORMAL-INDENT, will
748-
move upwards in an sexp to check for contextual indenting."
749-
(let (indent (path) (depth 0))
759+
(let* ((forward-sexp-function #'clojure-forward-logical-sexp))
760+
;; Goto to the open-paren.
750761
(goto-char (elt state 1))
751-
(while (and (not indent)
752-
(< depth clojure-max-backtracking))
753-
(let ((containing-sexp (point)))
754-
(parse-partial-sexp (1+ containing-sexp) indent-point 1 t)
755-
(when (looking-at "\\sw\\|\\s_")
756-
(let* ((start (point))
757-
(fn (buffer-substring start (progn (forward-sexp 1) (point))))
758-
(meth (clojure--symbol-get fn)))
759-
(let ((n 0))
760-
(when (< (point) indent-point)
761-
(condition-case ()
762-
(progn
763-
(forward-sexp 1)
764-
(while (< (point) indent-point)
765-
(parse-partial-sexp (point) indent-point 1 t)
766-
(cl-incf n)
767-
(forward-sexp 1)))
768-
(error nil)))
769-
(push n path))
770-
(when (and (listp meth)
771-
(not (functionp meth)))
772-
(let ((def meth))
773-
(dolist (p path)
774-
(if (and (listp def)
775-
(< p (length def)))
776-
(setq def (nth p def))
777-
(if (listp def)
778-
(setq def (car (last def)))
779-
(setq def nil))))
780-
(goto-char (elt state 1))
781-
(when def
782-
(setq indent (+ (current-column) def)))))))
783-
(goto-char containing-sexp)
784-
(condition-case ()
785-
(progn
786-
(backward-up-list 1)
787-
(cl-incf depth))
788-
(error (setq depth clojure-max-backtracking)))))
789-
indent))
790-
791-
;; clojure backtracking indent is experimental and the format for these
792-
;; entries are subject to change
793-
(put 'implement 'clojure-indent-function '(4 (2)))
794-
(put 'letfn 'clojure-indent-function '((2) 2))
795-
(put 'proxy 'clojure-indent-function '(4 4 (2)))
796-
(put 'reify 'clojure-indent-function '((2)))
797-
(put 'deftype 'clojure-indent-function '(4 4 (2)))
798-
(put 'defrecord 'clojure-indent-function '(4 4 (2)))
799-
(put 'defprotocol 'clojure-indent-function '(4 (2)))
800-
(put 'extend-type 'clojure-indent-function '(4 (2)))
801-
(put 'extend-protocol 'clojure-indent-function '(4 (2)))
802-
(put 'specify 'clojure-indent-function '(4 (2)))
803-
(put 'specify! 'clojure-indent-function '(4 (2)))
804-
762+
;; Maps, sets, vectors and reader conditionals.
763+
(if (or (member (char-after) '(?\[ ?\{))
764+
(and (eq (char-before) ?\?)
765+
(eq (char-before (1- (point))) ?\#))
766+
;; Car of form is not a symbol.
767+
(and (elt state 2)
768+
(not (looking-at ".\\sw\\|.\\s_"))))
769+
(1+ (current-column))
770+
;; Function or macro call.
771+
(forward-char 1)
772+
(let ((method (clojure--find-indent-spec))
773+
(containing-form-column (1- (current-column))))
774+
(pcase method
775+
((or (pred integerp) `(,method))
776+
(let ((pos -1))
777+
;; `forward-sexp' will error if indent-point is after
778+
;; the last sexp in the current sexp.
779+
(ignore-errors
780+
(while (<= (point) indent-point)
781+
(clojure-forward-logical-sexp 1)
782+
(cl-incf pos)))
783+
(cond
784+
((= pos (1+ method))
785+
(+ lisp-body-indent containing-form-column))
786+
((> pos (1+ method))
787+
(clojure--normal-indent calculate-lisp-indent-last-sexp))
788+
(t
789+
(+ (* 2 lisp-body-indent) containing-form-column)))))
790+
(`:defn
791+
(+ lisp-body-indent containing-form-column))
792+
((pred functionp)
793+
(funcall method indent-point state))
794+
((and `nil
795+
(guard (let ((function (thing-at-point 'sexp)))
796+
(or (and clojure-defun-style-default-indent
797+
;; largely to preserve useful alignment of :require, etc in ns
798+
(not (string-match "^:" function)))
799+
(string-match "\\`\\(?:\\S +/\\)?\\(def\\|with-\\)"
800+
function)))))
801+
(+ lisp-body-indent containing-form-column))
802+
(_ (clojure--normal-indent calculate-lisp-indent-last-sexp)))))))
803+
804+
;;; Setting indentation
805805
(defun put-clojure-indent (sym indent)
806806
"Instruct `clojure-indent-function' to indent the body of SYM by INDENT."
807807
(put sym 'clojure-indent-function indent))
@@ -827,18 +827,18 @@ Requires the macro's NAME and a VALUE."
827827
828828
You can use this to let Emacs indent your own macros the same way
829829
that it indents built-in macros like with-open. To manually set
830-
it from Lisp code, use (put-clojure-indent 'some-symbol 'defun)."
830+
it from Lisp code, use (put-clojure-indent 'some-symbol :defn)."
831831
:type '(repeat symbol)
832832
:group 'clojure
833833
:set 'add-custom-clojure-indents)
834834

835835
(define-clojure-indent
836836
;; built-ins
837837
(ns 1)
838-
(fn 'defun)
839-
(def 'defun)
840-
(defn 'defun)
841-
(bound-fn 'defun)
838+
(fn :defn)
839+
(def :defn)
840+
(defn :defn)
841+
(bound-fn :defn)
842842
(if 1)
843843
(if-not 1)
844844
(case 1)
@@ -855,26 +855,26 @@ it from Lisp code, use (put-clojure-indent 'some-symbol 'defun)."
855855
(comment 0)
856856
(doto 1)
857857
(locking 1)
858-
(proxy 2)
858+
(proxy '(2 nil nil (1)))
859859
(as-> 2)
860860

861-
(reify 'defun)
862-
(deftype 2)
863-
(defrecord 2)
864-
(defprotocol 1)
861+
(reify '(1 nil (1)))
862+
(deftype '(2 nil nil (1)))
863+
(defrecord '(2 nil nil (1)))
864+
(defprotocol '(1))
865865
(extend 1)
866-
(extend-protocol 1)
867-
(extend-type 1)
868-
(specify 1)
869-
(specify! 1)
870-
866+
(extend-protocol '(1 (1)))
867+
(extend-type '(1 (1)))
868+
(specify '(1 (1)))
869+
(specify! '(1 (1)))
870+
(implement '(1 (1)))
871871
(try 0)
872872
(catch 2)
873873
(finally 0)
874874

875875
;; binding forms
876876
(let 1)
877-
(letfn 1)
877+
(letfn '(1 ((1)) nil))
878878
(binding 1)
879879
(loop 1)
880880
(for 1)
@@ -885,18 +885,18 @@ it from Lisp code, use (put-clojure-indent 'some-symbol 'defun)."
885885
(when-some 1)
886886
(if-some 1)
887887

888-
(defmethod 'defun)
888+
(defmethod :defn)
889889

890890
;; clojure.test
891891
(testing 1)
892-
(deftest 'defun)
892+
(deftest :defn)
893893
(are 2)
894-
(use-fixtures 'defun)
894+
(use-fixtures :defn)
895895

896896
;; core.logic
897-
(run 'defun)
898-
(run* 'defun)
899-
(fresh 'defun)
897+
(run :defn)
898+
(run* :defn)
899+
(fresh :defn)
900900

901901
;; core.async
902902
(alt! 0)
@@ -1108,7 +1108,7 @@ Returns a list pair, e.g. (\"defn\" \"abc\") or (\"deftest\" \"some-test\")."
11081108
Sexps that don't represent code are ^metadata or #reader.macros."
11091109
(forward-sexp 1)
11101110
(forward-sexp -1)
1111-
(not (looking-at-p "\\^\\|#[[:alpha:]]")))
1111+
(not (looking-at-p "\\^\\|#[?[:alpha:]]")))
11121112

11131113
(defun clojure-forward-logical-sexp (&optional n)
11141114
"Move forward N logical sexps.
@@ -1140,7 +1140,7 @@ This will skip over sexps that don't represent objects, so that ^hints and
11401140
(ignore-errors
11411141
(save-excursion
11421142
(backward-sexp 1)
1143-
(looking-at-p "\\^\\|#[[:alpha:]]"))))
1143+
(not (clojure--looking-at-logical-sexp)))))
11441144
(backward-sexp 1))
11451145
(setq n (1- n)))))
11461146

0 commit comments

Comments
 (0)