Skip to content

Commit ae2e248

Browse files
committed
First pass at semantic indentation
Needs work - Threading macros - functions like `if` `let` also need some special handling to indent the first argument always 1 space after first child (no matter what line) Working, maybe So far "body" expression macros like `when` and `defn` work well, as do vertically aligning calls to functions.
1 parent 2225190 commit ae2e248

File tree

1 file changed

+112
-1
lines changed

1 file changed

+112
-1
lines changed

clojure-ts-mode.el

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,24 @@ Includes a dispatch value when applicable (defmethods)."
497497
By default `treesit-defun-name-function' is used to extract definition names.
498498
See `clojure-ts--standard-definition-node-name' for the implementation used.")
499499

500+
(defcustom clojure-ts-indent-style 'semantic
501+
"Automatic indentation style to use when mode clojure-ts-mode is run
502+
503+
The possible values for this variable are
504+
`semantic' - Tries to follow the same rules as the clojure style guide.
505+
See: https://guide.clojure.style/
506+
`fixed' - A simpler set of indentation rules that can be summarized as
507+
1. Multi-line lists that start with a symbol are always indented with
508+
two spaces.
509+
2. Other multi-line lists, vectors, maps and sets are aligned with the
510+
first element (1 or 2 spaces).
511+
See: https://tonsky.me/blog/clojurefmt/"
512+
:safe #'symbolp
513+
:type
514+
'(choice (const :tag "Semantic indent rules, matching clojure style guide." semantic)
515+
(const :tag "Simple fixed indent rules." fixed))
516+
:package-version '(clojure-ts-mode . "0.2.0"))
517+
500518
(defvar clojure-ts--fixed-indent-rules
501519
;; This is in contrast to semantic
502520
;; fixed-indent-rules come from https://tonsky.me/blog/clojurefmt/
@@ -536,6 +554,99 @@ See `clojure-ts--standard-definition-node-name' for the implementation used.")
536554
((sexp ,(regexp-opt clojure-ts--sexp-nodes))
537555
(text ,(regexp-opt '("comment")))))))
538556

557+
(defvar clojure-ts--symbols-with-body-expressions-regexp
558+
(eval-and-compile
559+
(rx (or
560+
;; Match def* symbols,
561+
;; we also explicitly do not match symbols beginning with
562+
;; "default" "deflate" and "defer", like cljfmt
563+
(and line-start "def")
564+
;; Match with-* symbols
565+
(and line-start "with-")
566+
;; Exact matches
567+
(and line-start
568+
(or "alt!" "alt!!" "are" "as->"
569+
"binding" "bound-fn"
570+
"case" "catch" "comment" "cond" "condp" "cond->" "cond->>"
571+
"delay" "do" "doseq" "dotimes" "doto"
572+
"extend" "extend-protocol" "extend-type"
573+
"fdef" "finally" "fn" "for" "future"
574+
"go" "go-loop"
575+
"if" "if-let" "if-not" "if-some"
576+
"let" "letfn" "locking" "loop"
577+
"match" "ns" "proxy" "reify" "struct-map"
578+
"testing" "thread" "try"
579+
"use-fixtures"
580+
"when" "when-first" "when-let" "when-not" "when-some" "while")
581+
line-end))))
582+
"A regex to match symbols that are functions/macros with a body argument.
583+
Taken from cljfmt:
584+
https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de898c3/cljfmt/resources/cljfmt/indents/clojure.clj")
585+
586+
(defun clojure-ts--symbols-with-body-expressions-p (node)
587+
"Return non-nil if NODE is a function/macro symbol with a body argument."
588+
(and
589+
(not
590+
(clojure-ts--symbol-matches-p
591+
;; Symbols starting with this are false positives
592+
(rx line-start (or "default" "deflate" "defer"))
593+
node))
594+
(clojure-ts--symbol-matches-p
595+
clojure-ts--symbols-with-body-expressions-regexp
596+
node)))
597+
598+
(defvar clojure-ts--threading-macro
599+
(eval-and-compile
600+
(rx (and "->" (? ">") line-end)))
601+
"A regular expression matching a threading macro.")
602+
603+
(defun clojure-ts--threading-macro-p (node)
604+
"Return non-nil if NODE is a threading macro symbol like ->>."
605+
(clojure-ts--symbol-matches-p clojure-ts--threading-macro node))
606+
607+
(defvar clojure-ts--semantic-indent-rules
608+
`((clojure
609+
((parent-is "source") parent-bol 0)
610+
((lambda (node parent _) ;; https://guide.clojure.style/#body-indentation
611+
(and (clojure-ts--list-node-p parent)
612+
(let ((first-child (treesit-node-child parent 0 t)))
613+
(clojure-ts--symbols-with-body-expressions-p first-child))))
614+
parent 2)
615+
;; ;; We want threading macros to indent 2 only if the ->> is on it's own line.
616+
;; ;; If not, then align functoin args.
617+
;; ((lambda (node parent _)
618+
;; (and (clojure-ts--list-node-p parent)
619+
;; (let ((first-child (treesit-node-child parent 0 t))
620+
;; (second-child (treesit-node-child parent 1 t)))
621+
;; (clojure-ts--debug "Second-child %S" (treesit-node))
622+
;; (clojure-ts--threading-macro-p first-child))))
623+
;; parent 2)
624+
((lambda (node parent _) ;; https://guide.clojure.style/#vertically-align-fn-args
625+
(and (clojure-ts--list-node-p parent)
626+
;; Can the following two clauses be replaced by checking indexes?
627+
;; Does the second child exist, and is it not equal to the current node?
628+
(treesit-node-child parent 1 t)
629+
(not (treesit-node-eq (treesit-node-child parent 1 t) node))
630+
(let ((first-child (treesit-node-child parent 0 t)))
631+
(or (clojure-ts--symbol-node-p first-child)
632+
(clojure-ts--keyword-node-p first-child)))))
633+
(nth-sibling 2 nil) 0)
634+
;; Literal Sequences
635+
((parent-is "list_lit") parent 1) ;; https://guide.clojure.style/#one-space-indent
636+
((parent-is "vec_lit") parent 1) ;; https://guide.clojure.style/#bindings-alignment
637+
((parent-is "map_lit") parent 1) ;; https://guide.clojure.style/#map-keys-alignment
638+
((parent-is "set_lit") parent 2))))
639+
640+
(defun clojure-ts--configured-indent-rules ()
641+
"Gets the configured choice of indent rules."
642+
(cond
643+
((eq clojure-ts-indent-style 'semantic) clojure-ts--semantic-indent-rules)
644+
((eq clojure-ts-indent-style 'fixed) clojure-ts--fixed-indent-rules)
645+
(t (error
646+
(format
647+
"Invalid value for clojure-ts-indent-style. Valid values are 'semantic or 'fixed. Found %S"
648+
clojure-ts-indent-style)))))
649+
539650
(defvar clojure-ts-mode-map
540651
(let ((map (make-sparse-keymap)))
541652
;(set-keymap-parent map clojure-mode-map)
@@ -581,7 +692,7 @@ See `clojure-ts--standard-definition-node-name' for the implementation used.")
581692
treesit-defun-prefer-top-level t
582693
treesit-defun-tactic 'top-level
583694
treesit-defun-type-regexp (rx (or "list_lit" "vec_lit" "map_lit"))
584-
treesit-simple-indent-rules clojure-ts--fixed-indent-rules
695+
treesit-simple-indent-rules (clojure-ts--configured-indent-rules)
585696
treesit-defun-name-function #'clojure-ts--standard-definition-node-name
586697
treesit-simple-imenu-settings clojure-ts--imenu-settings
587698
treesit-font-lock-feature-list

0 commit comments

Comments
 (0)