From 49d63a2163754521921c529408b2de1b8f1fbacf Mon Sep 17 00:00:00 2001 From: David Rolle Date: Mon, 20 Nov 2023 19:49:08 +0100 Subject: [PATCH] Support two space list indentation via a new opt - Added config option `:function-arguments-indentation`, with 3 possible values: `:community`, `:cursive`, or `:zprint`. - This controls the behaviour described in https://guide.clojure.style/#one-space-indent - When set to `:community` (or not set), current behaviour is unchanged - When set to `:cursive`, or `:zprint`, function arguments will be indented 2 spaces when there are no arguments on the same line as the function name. - See README difference between `:cursive` and `:zprint` is how they behave for - Added a CLI flag to control this new config - Added tests --- README.md | 14 ++ cljfmt/src/cljfmt/core.cljc | 72 ++++-- cljfmt/src/cljfmt/main.clj | 10 +- cljfmt/test/cljfmt/core_test.cljc | 385 ++++++++++++++++++++++++++++-- 4 files changed, 432 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index efbaf0e..30feb03 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,21 @@ In order to load the standard configuration file from Leiningen, add the other references in the `ns` forms at the top of your namespaces. Defaults to false. +* `:function-arguments-indentation` - + - `:community` if cljfmt should follow the [community style recommendation][] + to indent function/macro arguments by a single space when there + are no arguments on the same line as the function name. + - `:cursive` if two spaces should be used instead, unless the first + thing in the list (not counting metadata) is a data structure + literal. This should replicate Cursive's default behaviour. + - `:zprint` if two spaces should be used instead if the first thing + in the list is a symbol or keyword. This should replicate zprint's + default behaviour. + + Defaults to `:community` + [indents.md]: docs/INDENTS.md +[community style recommendation]: https://guide.clojure.style/#one-space-indent ### Runtime Options diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index 13abf49..1e9f0e8 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -187,10 +187,28 @@ (count) (dec))) -(defn- list-indent [zloc] +(defn- skip-meta [zloc] + (if (#{:meta :meta*} (z/tag zloc)) + (-> zloc z/down z/right) + zloc)) + +(defn- cursive-two-space-list-indent? [zloc] + (-> zloc z/leftmost* skip-meta z/tag #{:vector :map :list :set} not)) + +(defn- zprint-two-space-list-indent? [zloc] + (-> zloc z/leftmost* z/tag #{:token :list})) + +(defn two-space-list-indent? [zloc context] + (case (:function-arguments-indentation context) + :community false + :cursive (cursive-two-space-list-indent? zloc) + :zprint (zprint-two-space-list-indent? zloc))) + +(defn- list-indent [zloc context] (if (> (index-of zloc) 1) (-> zloc z/leftmost* z/right margin) - (coll-indent zloc))) + (cond-> (coll-indent zloc) + (two-space-list-indent? zloc context) inc))) (def indent-size 2) @@ -288,13 +306,27 @@ (if (and (or (nil? zloc-after-idx) (first-form-in-line? zloc-after-idx)) (> (index-of zloc) idx)) (inner-indent zloc key 0 nil context) - (list-indent zloc))))) + (list-indent zloc context))))) (def default-indents (merge (read-resource "cljfmt/indents/clojure.clj") (read-resource "cljfmt/indents/compojure.clj") (read-resource "cljfmt/indents/fuzzy.clj"))) +(def default-options + {:indentation? true + :insert-missing-whitespace? true + :remove-consecutive-blank-lines? true + :remove-multiple-non-indenting-spaces? false + :remove-surrounding-whitespace? true + :remove-trailing-whitespace? true + :split-keypairs-over-multiple-lines? false + :sort-ns-references? false + :function-arguments-indentation :community + :indents default-indents + :extra-indents {} + :alias-map {}}) + (defmulti ^:private indenter-fn (fn [_sym _context [type & _args]] type)) @@ -315,12 +347,12 @@ (defn- custom-indent [zloc indents context] (if (empty? indents) - (list-indent zloc) + (list-indent zloc context) (let [indenter (->> indents (map #(make-indenter % context)) (apply some-fn))] (or (indenter zloc) - (list-indent zloc))))) + (list-indent zloc context))))) (defn- indent-amount [zloc indents context] (let [tag (-> zloc z/up z/tag) @@ -346,11 +378,15 @@ ([form indents] (indent form indents {})) ([form indents alias-map] + (indent form indents alias-map default-options)) + ([form indents alias-map opts] (let [ns-name (find-namespace (z/of-node form)) - sorted-indents (sort-by indent-order indents)] + sorted-indents (sort-by indent-order indents) + context (merge (select-keys opts [:function-arguments-indentation]) + {:alias-map alias-map + :ns-name ns-name})] (transform form edit-all should-indent? - #(indent-line % sorted-indents {:alias-map alias-map - :ns-name ns-name}))))) + #(indent-line % sorted-indents context))))) (defn- map-key? [zloc] (and (z/map? (z/up zloc)) @@ -381,7 +417,9 @@ ([form indents] (indent (unindent form) indents)) ([form indents alias-map] - (indent (unindent form) indents alias-map))) + (indent (unindent form) indents alias-map)) + ([form indents alias-map opts] + (indent (unindent form) indents alias-map opts))) (defn final? [zloc] (and (nil? (z/right* zloc)) (root? (z/up* zloc)))) @@ -486,19 +524,6 @@ (defn sort-ns-references [form] (transform form edit-all ns-reference? sort-arguments)) -(def default-options - {:indentation? true - :insert-missing-whitespace? true - :remove-consecutive-blank-lines? true - :remove-multiple-non-indenting-spaces? false - :remove-surrounding-whitespace? true - :remove-trailing-whitespace? true - :split-keypairs-over-multiple-lines? false - :sort-ns-references? false - :indents default-indents - :extra-indents {} - :alias-map {}}) - (defn reformat-form ([form] (reformat-form form {})) @@ -519,7 +544,8 @@ remove-multiple-non-indenting-spaces) (cond-> (:indentation? opts) (reindent (merge (:indents opts) (:extra-indents opts)) - (:alias-map opts))) + (:alias-map opts) + opts)) (cond-> (:remove-trailing-whitespace? opts) remove-trailing-whitespace))))) diff --git a/cljfmt/src/cljfmt/main.clj b/cljfmt/src/cljfmt/main.clj index 73dca07..09faf9f 100644 --- a/cljfmt/src/cljfmt/main.clj +++ b/cljfmt/src/cljfmt/main.clj @@ -51,7 +51,15 @@ :id :split-keypairs-over-multiple-lines?] [nil "--[no-]sort-ns-references" :default (:sort-ns-references? defaults) - :id :sort-ns-references?]]) + :id :sort-ns-references?] + [nil "--function-arguments-indentation STYLE" + "STYLE may be community, cursive, or zprint" + :default (:function-arguments-indentation defaults) + :default-desc (name (:function-arguments-indentation defaults)) + :parse-fn keyword + :validate [#{:community :cursive :zprint} + "Must be one of community, cursive, or zprint"] + :id :function-arguments-indentation]]) (defn- abort [& msg] (binding [*out* *err*] diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index ae0fa75..7721d1e 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -1,7 +1,7 @@ (ns cljfmt.core-test (:require #?(:clj [cljfmt.test-util.clojure]) [#?@(:clj (clojure.test :refer) - :cljs (cljs.test :refer-macros)) [deftest testing is]] + :cljs (cljs.test :refer-macros)) [deftest testing is are]] [cljfmt.core :refer [reformat-string default-line-separator normalize-newlines find-line-separator replace-newlines wrap-normalize-newlines]] @@ -10,22 +10,35 @@ (deftest test-indent (testing "list indentation" - (is (reformats-to? - ["(foo bar" - "baz" - "quz)"] - ["(foo bar" - " baz" - " quz)"])) + (are [fn-args-indent] + (reformats-to? + ["(foo bar" + "baz" + "quz)"] + ["(foo bar" + " baz" + " quz)"] + {:function-arguments-indentation fn-args-indent}) + :community + :cursive + :zprint) - (is (reformats-to? - ["(foo" - "bar" - "baz)"] - ["(foo" - " bar" - " baz)"]) - "to first arg")) + (are [fn-args-indent expected] + (reformats-to? + ["(foo" + "bar" + "baz)"] + expected + {:function-arguments-indentation fn-args-indent}) + :community ["(foo" + " bar" + " baz)"] + :cursive ["(foo" + " bar" + " baz)"] + :zprint ["(foo" + " bar" + " baz)"])) (testing "block indentation" (is (reformats-to? @@ -88,11 +101,15 @@ ["(defn foo" "([] 0)" "([x]" - "(+ x 1)))"] + "(+ x 1))" + "(^Number [x y]" + "(+ x y)))"] ["(defn foo" " ([] 0)" " ([x]" - " (+ x 1)))"]) + " (+ x 1))" + " (^Number [x y]" + " (+ x y)))"]) "defn multi-arity") (is (reformats-to? ["(fn [x]" @@ -531,7 +548,17 @@ ["(;a" "" " ;b" - " )"]))) + " )"])) + (is (reformats-to? + ["(;a" + "" + " ;b" + " )"] + ["(;a" + "" + " ;b" + " )"] + {:function-arguments-indentation :cursive}))) (testing "empty indent blocks" (is (reformats-to? @@ -575,6 +602,13 @@ ["(foo" " )"] {:remove-surrounding-whitespace? false})) + (is (reformats-to? + ["(foo" + ")"] + ["(foo" + " )"] + {:remove-surrounding-whitespace? false + :function-arguments-indentation :cursive})) (is (reformats-to? ["(foo (bar (baz)))"] ["(foo (bar (baz)))"])) @@ -1007,7 +1041,19 @@ " )" " )"] {:remove-surrounding-whitespace? false}) - "indents properly"))) + "indents properly") + (is (reformats-to? + ["(foo" + "(bar" + ")" + ")"] + ["(foo" + " (bar" + " )" + " )"] + {:remove-surrounding-whitespace? false + :function-arguments-indentation :cursive}) + "indents properly with :function-arguments-indentation :cursive"))) (deftest test-options (is (reformats-to? @@ -1062,6 +1108,16 @@ " bar)"] {:indents {}}) "can clear all indents rules") + (is (reformats-to? + ["(do" + "foo" + "bar)"] + ["(do" + " foo" + " bar)"] + {:indents {} + :function-arguments-indentation :cursive}) + "can clear indents rules with :function-arguments-indentation :cursive") (is (reformats-to? ["(do" "foo" @@ -1092,6 +1148,16 @@ " )"] {:remove-surrounding-whitespace? false :remove-trailing-whitespace? false})) + (is (reformats-to? + ["(foo" + " " + ")"] + ["(foo" + " " + " )"] + {:remove-surrounding-whitespace? false + :remove-trailing-whitespace? false + :function-arguments-indentation :cursive})) (is (reformats-to? ["( " "foo" @@ -1101,6 +1167,16 @@ " )"] {:remove-surrounding-whitespace? false :remove-trailing-whitespace? false})) + (is (reformats-to? + ["( " + "foo" + " )"] + ["( " + " foo" + " )"] + {:remove-surrounding-whitespace? false + :remove-trailing-whitespace? false + :function-arguments-indentation :cursive})) (is (reformats-to? ["(foo" " bar " @@ -1110,6 +1186,16 @@ " )"] {:remove-surrounding-whitespace? false :remove-trailing-whitespace? false})) + (is (reformats-to? + ["(foo" + " bar " + ")"] + ["(foo" + " bar " + " )"] + {:remove-surrounding-whitespace? false + :remove-trailing-whitespace? false + :function-arguments-indentation :cursive})) (is (reformats-to? ["{:one two :three four}"] ["{:one two" @@ -1190,10 +1276,45 @@ " 3 4)"] {:alias-map {"other" "another.lib"} :sort-ns-references? true - :indents {'block1 [[:block 1]] - 'other.lib/overridden [[:block 2]] ;; This one is ignored - 'another.lib/overridden [[:block 1]] ;; As this one overrides. - 'some.lib/block2 [[:block 2]]}})))) + :indents + {'block1 [[:block 1]] + 'other.lib/overridden [[:block 2]] ;; This one is ignored + 'another.lib/overridden [[:block 1]] ;; As this one overrides. + 'some.lib/block2 [[:block 2]]}}))) + #?(:clj + (is (reformats-to? + ["(ns foo.bar" + " (:require" + " [some.lib :as lib]" + " [other.lib :as other]))" + "(lib/block2 1 2" + " 3 4)" + "(other/block1 1" + " 2" + " 3 4)" + "(other/overridden 1" + " 2" + " 3 4)"] + ["(ns foo.bar" + " (:require" + " [other.lib :as other]" + " [some.lib :as lib]))" + "(lib/block2 1 2" + " 3 4)" + "(other/block1 1" + " 2" + " 3 4)" + "(other/overridden 1" + " 2" + " 3 4)"] + {:alias-map {"other" "another.lib"} + :sort-ns-references? true + :function-arguments-indentation :cursive + :indents + {'block1 [[:block 1]] + 'other.lib/overridden [[:block 2]] ;; This one is ignored + 'another.lib/overridden [[:block 1]] ;; As this one overrides. + 'some.lib/block2 [[:block 2]]}})))) (deftest test-parsing (is (reformats-to? @@ -1214,6 +1335,12 @@ "bar)"] ["#_(foo" " bar)"])) + (is (reformats-to? + ["#_(foo" + "bar)"] + ["#_(foo" + " bar)"] + {:function-arguments-indentation :cursive})) (is (reformats-to? ["(juxt +' -')"] ["(juxt +' -')"])) @@ -1340,6 +1467,21 @@ " b" " [c]))"] {:sort-ns-references? true})) + (is (reformats-to? + ["(ns foo.bar" + " (:require" + " [c]" + " [a.b :as b] ;; aabb" + " ;; bbb" + " b))"] + ["(ns foo.bar" + " (:require" + " [a.b :as b] ;; aabb" + " ;; bbb" + " b" + " [c]))"] + {:sort-ns-references? true + :function-arguments-indentation :cursive})) (is (reformats-to? ["(ns foo.bar" " (:require" @@ -1353,4 +1495,197 @@ " ^:keep a" " ^{:x 1} b" " [c]))"] - {:sort-ns-references? true}))) + {:sort-ns-references? true})) + (is (reformats-to? + ["(ns foo.bar" + " (:require" + " [c]" + " ^:keep a" + " #?(:clj d)" + " ^{:x 1} b))"] + ["(ns foo.bar" + " (:require" + " #?(:clj d)" + " ^:keep a" + " ^{:x 1} b" + " [c]))"] + {:sort-ns-references? true + :function-arguments-indentation :cursive}))) + +(deftest cursive-and-zprint-function-argument-indents-depend-on-first-element + (let [input ["(foo" + "bar)" + "(:foo" + "bar)" + "(^:foo bar" + "baz)" + "(^:foo [bar]" + "baz)" + "(^{:foo 1} bar" + "baz)" + "(^{:foo 1} [bar]" + "baz)" + "(#^:foo bar" + "baz)" + "(#^:foo [bar]" + "baz)" + "(#^{:foo 1} bar" + "baz)" + "(#^{:foo 1} [bar]" + "baz)" + "([foo]" + "bar)" + "({:foo 1}" + "bar)" + "((foo)" + "bar)" + "(#=foo" + "bar)" + "(#=[foo]" + "bar)" + "(#_foo" + "bar)" + "(#_[foo]" + "bar)" + "(#(foo)" + "bar)" + "(#{foo}" + "bar)" + "(@foo" + "bar)" + "(#?(:clj foo)" + "bar)" + "(#?(:clj [foo])" + "bar)" + "(~foo" + "bar)" + "(#'foo" + "bar)" + "('foo" + "bar)" + "(`foo" + "bar)" + "(~@foo" + "bar)" + "(#:foo{:bar 1}" + "baz)"]] + (testing ":cursive style uses 2 spaces unless starting with a collection" + (is (reformats-to? + input + ["(foo" + " bar)" + "(:foo" + " bar)" + "(^:foo bar" + " baz)" + "(^:foo [bar]" + " baz)" + "(^{:foo 1} bar" + " baz)" + "(^{:foo 1} [bar]" + " baz)" + "(#^:foo bar" + " baz)" + "(#^:foo [bar]" + " baz)" + "(#^{:foo 1} bar" + " baz)" + "(#^{:foo 1} [bar]" + " baz)" + "([foo]" + " bar)" + "({:foo 1}" + " bar)" + "((foo)" + " bar)" + "(#=foo" + " bar)" + "(#=[foo]" + " bar)" + "(#_foo" + " bar)" + "(#_[foo]" + " bar)" + "(#(foo)" + " bar)" + "(#{foo}" + " bar)" + "(@foo" + " bar)" + "(#?(:clj foo)" + " bar)" + "(#?(:clj [foo])" + " bar)" + "(~foo" + " bar)" + "(#'foo" + " bar)" + "('foo" + " bar)" + "(`foo" + " bar)" + "(~@foo" + " bar)" + "(#:foo{:bar 1}" + " baz)"] + {:function-arguments-indentation :cursive}))) + (testing ":zprint uses 2 spaces if starting with a symbol, keyword, or list" + (is (reformats-to? + input + ["(foo" + " bar)" + "(:foo" + " bar)" + "(^:foo bar" + " baz)" + "(^:foo [bar]" + " baz)" + "(^{:foo 1} bar" + " baz)" + "(^{:foo 1} [bar]" + " baz)" + "(#^:foo bar" + " baz)" + "(#^:foo [bar]" + " baz)" + "(#^{:foo 1} bar" + " baz)" + "(#^{:foo 1} [bar]" + " baz)" + "([foo]" + " bar)" + "({:foo 1}" + " bar)" + "((foo)" + " bar)" + "(#=foo" + " bar)" + "(#=[foo]" + " bar)" + "(#_foo" + " bar)" + "(#_[foo]" + " bar)" + "(#(foo)" + " bar)" + "(#{foo}" + " bar)" + "(@foo" + " bar)" + "(#?(:clj foo)" + " bar)" + "(#?(:clj [foo])" + " bar)" + "(~foo" + " bar)" + "(#'foo" + " bar)" + "('foo" + " bar)" + "(`foo" + " bar)" + "(~@foo" + " bar)" + "(#:foo{:bar 1}" + " baz)"] + {:function-arguments-indentation :zprint})))))