/
format.clj
151 lines (138 loc) · 6.69 KB
/
format.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
(ns clojure-lsp.feature.format
(:require
[cljfmt.config :as cljfmt.config]
[cljfmt.core :as cljfmt]
[clojure-lsp.feature.file-management :as f.file-management]
[clojure-lsp.parser :as parser]
[clojure-lsp.queries :as q]
[clojure-lsp.refactor.edit :as edit]
[clojure-lsp.settings :as settings]
[clojure-lsp.shared :as shared]
[clojure.core.memoize :as memoize]
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as string]
[medley.core :as medley]
[rewrite-clj.node :as n]
[rewrite-clj.zip :as z]))
(set! *warn-on-reflection* true)
(defn resolve-user-cljfmt-config [db]
(when-let [project-root (some-> db :project-root-uri shared/uri->filename)]
(let [config-path (settings/get db [:cljfmt-config-path] ".cljfmt.edn")
cljfmt-config-file (if (string/starts-with? config-path "/")
(io/file config-path)
(io/file project-root config-path))]
(medley/deep-merge
(settings/get db [:cljfmt] {})
(when (shared/file-exists? cljfmt-config-file)
(if (string/ends-with? cljfmt-config-file ".clj")
(binding [*read-eval* false]
(read-string (slurp cljfmt-config-file)))
(edn/read-string {:readers {'re re-pattern}} (slurp cljfmt-config-file))))))))
(defn ^:private arg-spec->cljfmt-arg [index argspec]
(letfn [(depth [x]
(cond
(vector? x) (inc (depth (first x)))
(or (number? x)
(= :defn x)) 0
:else -1000))]
(let [d (depth argspec)]
(when-not (neg? d)
[:inner d index]))))
(defn ^:private style-indent->cljfmt-spec
"Converts Cider's `:style/indent` metadata into a cljfmt `:indents` spec.
See the details at https://docs.cider.mx/cider/indent_spec.html but a quick sketch follows.
- Top-level numbers or keywords are shorthand for [x].
- The first element of the list is a number, `:defn` or `:form`.
- Numbers give the number of special args (`[:block N]` in cljfmt)
- `:defn` means indent like a `defn` (`[:inner 0]` in cljfmt)
- `:form` means indent like a normal `(f a b)` form.
- Each following value is a nested indent spec for the argument at that position.
- cljfmt doesn't support full nesting, but it can approximate with `[:inner depth pos]`.
- The final spec applies to all args; this corresponds to an `[:inner depth]` with no index."
[spec]
(let [[sym & args] (if (vector? spec)
spec
[spec])
sym-spec (cond
(number? sym) [:block sym]
(= sym :defn) [:inner 0])
arg-specs (keep-indexed arg-spec->cljfmt-arg args)
[tk td ti :as tail] (last arg-specs)]
(->> (concat (when sym-spec [sym-spec])
(butlast arg-specs)
;; The last arg spec in :style/indent applies to all remaining args.
;; So if it generated a [:inner depth index], strip off the index.
;; But only some args generate arg-specs, and there might be no args at all.
(cond
;; Last arg generated [:inner depth index], remove the index
(and (= tk :inner)
(= ti (dec (count args)))) [[:inner td]]
;; Last argspec doesn't match the last arg.
tail [tail]
;; No argspecs at all.
:else nil))
vec
not-empty)))
(defn ^:private extract-style-indent-metadata [db]
{:indents (into {}
(comp q/xf-analysis->var-definitions
(keep (fn [var-def]
(when-let [indent (some-> var-def
:meta
:style/indent
style-indent->cljfmt-spec)]
[(if (:macro var-def)
(:name var-def)
(symbol (name (:ns var-def))
(name (:name var-def))))
indent]))))
(:analysis db))})
(defn ^:private merge-configs
"Merge two or more cljfmt configuration maps together."
([a b]
(-> (merge a b)
(assoc :indents (merge (:indents a {}) (:indents b)))
(assoc :extra-indents (merge (:extra-indents a {}) (:extra-indents b)))))
([a b & more]
(reduce merge-configs (merge-configs a b) more)))
(defn ^:private resolve-cljfmt-config [db]
(-> cljfmt.config/default-config
(merge-configs (extract-style-indent-metadata db))
(merge-configs (resolve-user-cljfmt-config db))
;; There is a bug in cljfmt where the namespace's aliases are ignored if
;; :alias-map is provided. This avoids the bug in the common case where no
;; :alias-map is needed.
(update :alias-map not-empty)))
(def memoize-ttl-threshold-milis 3000)
(def cljfmt-config
(memoize/ttl resolve-cljfmt-config :ttl/threshold memoize-ttl-threshold-milis))
(defn formatting [uri {:keys [db*] :as components}]
(if-let [text (f.file-management/force-get-document-text uri components)]
(let [cljfmt-settings (cljfmt-config @db*)
new-text ((cljfmt/wrap-normalize-newlines #(cljfmt/reformat-string % cljfmt-settings)) text)]
(if (= new-text text)
[]
[{:range shared/full-file-range
:new-text new-text}]))
[]))
(defn range-formatting [doc-id format-pos db]
(when-let [root-loc (parser/safe-zloc-of-file db doc-id)]
(let [cljfmt-settings (cljfmt-config db)
start-loc (or (parser/to-pos root-loc (:row format-pos) (:col format-pos))
(z/leftmost* root-loc))
start-top-loc (edit/to-top start-loc)
end-loc (or (parser/to-pos start-top-loc (:end-row format-pos) (:end-col format-pos))
(z/rightmost* root-loc))
end-top-loc (edit/to-top end-loc)
forms (->> start-top-loc
(iterate z/right*) ;; maintain comments and whitespace between nodes
(take-while (complement z/end?))
(medley/take-upto #(= % end-top-loc)))
span (merge (-> start-top-loc z/node meta (select-keys [:row :col]))
(-> end-top-loc z/node meta (select-keys [:end-row :end-col])))]
[{:range (shared/->range span)
:new-text (-> (map z/node forms)
n/forms-node
(cljfmt/reformat-form cljfmt-settings)
n/string)}])))