/
file_management.clj
221 lines (205 loc) · 11 KB
/
file_management.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
(ns clojure-lsp.feature.file-management
(:require
[clojure-lsp.clojure-producer :as clojure-producer]
[clojure-lsp.crawler :as crawler]
[clojure-lsp.db :as db]
[clojure-lsp.feature.diagnostics :as f.diagnostic]
[clojure-lsp.kondo :as lsp.kondo]
[clojure-lsp.queries :as q]
[clojure-lsp.settings :as settings]
[clojure-lsp.shared :as shared]
[clojure.core.async :as async]
[clojure.java.io :as io]
[clojure.string :as string]
[lsp4clj.protocols.logger :as logger]
[lsp4clj.protocols.producer :as producer]
[medley.core :as medley]))
(set! *warn-on-reflection* true)
(defn ^:private update-analysis [db uri new-analysis]
(assoc-in db [:analysis (shared/uri->filename uri)] (lsp.kondo/normalize-analysis new-analysis)))
(defn ^:private update-findings [db uri new-findings]
(assoc-in db [:findings (shared/uri->filename uri)] new-findings))
(defn did-open [uri text db allow-create-ns]
(when-let [kondo-result (lsp.kondo/run-kondo-on-text! text uri db)]
(swap! db (fn [state-db]
(-> state-db
(assoc-in [:documents uri] {:v 0 :text text :saved-on-disk false})
(update-analysis uri (:analysis kondo-result))
(update-findings uri (:findings kondo-result))
(assoc :kondo-config (:config kondo-result)))))
(f.diagnostic/async-lint-file! uri db))
(when-let [new-ns (and allow-create-ns
(string/blank? text)
(contains? #{:clj :cljs :cljc} (shared/uri->file-type uri))
(not (get (:create-ns-blank-files-denylist @db) uri))
(shared/uri->namespace uri db))]
(when (settings/get db [:auto-add-ns-to-new-files?] true)
(let [new-text (format "(ns %s)" new-ns)
changes [{:text-document {:version (get-in @db [:documents uri :v] 0) :uri uri}
:edits [{:range (shared/->range {:row 1 :end-row 999999 :col 1 :end-col 999999})
:new-text new-text}]}]]
(async/>!! db/edits-chan (shared/client-changes changes db))))))
(defn ^:private find-changed-var-definitions [old-local-analysis new-local-analysis]
(let [old-var-defs (filter #(identical? :var-definitions (:bucket %)) old-local-analysis)
new-var-defs (filter #(identical? :var-definitions (:bucket %)) new-local-analysis)
compare-fn (fn [other-var-defs {:keys [name fixed-arities]}]
(if-let [var-def (first (filter #(= name (:name %)) other-var-defs))]
(and (not (= fixed-arities (:fixed-arities var-def)))
(not= 'clojure.core/declare (:defined-by var-def)))
true))]
(->> (concat
(filter (partial compare-fn new-var-defs) old-var-defs)
(filter (partial compare-fn old-var-defs) new-var-defs))
(medley/distinct-by (juxt :name)))))
(defn ^:private find-changed-var-usages
[old-local-analysis new-local-analysis]
(let [old-var-usages (filter #(identical? :var-usages (:bucket %)) old-local-analysis)
new-var-usages (filter #(identical? :var-usages (:bucket %)) new-local-analysis)
compare-fn (fn [other-var-usages current-var-usages var-usage]
(= (count (filter #(= (:name var-usage) (:name %)) current-var-usages))
(count (filter #(= (:name var-usage) (:name %)) other-var-usages))))]
(->> (concat
(remove (partial compare-fn new-var-usages old-var-usages) old-var-usages)
(remove (partial compare-fn old-var-usages new-var-usages) new-var-usages))
(medley/distinct-by (juxt :name)))))
(defn ^:private notify-references [filename old-local-analysis new-local-analysis {:keys [db producer]}]
(async/go
(let [project-analysis (q/filter-project-analysis (:analysis @db) db)
source-paths (settings/get db [:source-paths])
changed-var-definitions (find-changed-var-definitions old-local-analysis new-local-analysis)
references-filenames (->> changed-var-definitions
(map #(q/find-references project-analysis % false db))
flatten
(map :filename))
changed-var-usages (find-changed-var-usages old-local-analysis new-local-analysis)
definitions-filenames (->> changed-var-usages
(map #(q/find-definition project-analysis % db))
(remove nil?)
(filter (fn [d]
(and (not (:private d))
(some #(string/starts-with? (:filename d) %) source-paths))))
(map :filename))
filenames (->> definitions-filenames
(concat references-filenames)
(remove #(= filename %))
set)]
(when (seq filenames)
(logger/debug "Analyzing references for files:" filenames)
(crawler/analyze-reference-filenames! filenames db)
(doseq [filename filenames]
(f.diagnostic/sync-lint-file! (shared/filename->uri filename db) db))
(producer/refresh-code-lens producer)))))
(defn ^:private offsets [lines line col end-line end-col]
(loop [lines (seq lines)
offset 0
idx 0]
(if (or (not lines)
(= line idx))
[(+ offset col line)
(loop [lines lines
offset offset
idx idx]
(if (or (not lines)
(= end-line idx))
(+ offset end-col end-line)
(recur (next lines)
(+ offset (count (first lines)))
(inc idx))))]
(recur (next lines)
(+ offset (count (first lines)))
(inc idx)))))
(defn replace-text [original replacement line col end-line end-col]
(let [lines (string/split original #"\n") ;; don't use OS specific line delimiter!
[start-offset end-offset] (offsets lines line col end-line end-col)
[prefix suffix] [(subs original 0 start-offset)
(subs original (min end-offset (count original)) (count original))]]
(string/join [prefix replacement suffix])))
(defn ^:private handle-change
"Handle a TextDocumentContentChangeEvent"
[old-text change]
(let [new-text (:text change)]
(if-let [r (:range change)]
(let [{{start-line :line
start-character :character} :start
{end-line :line
end-character :character} :end} r]
(replace-text old-text new-text start-line start-character end-line end-character))
;; If range and rangeLength are omitted the new text is considered to be
;; the full content of the document.
new-text)))
(defn analyze-changes [{:keys [uri text version]} {:keys [producer db] :as components}]
(loop [state-db @db]
(when (>= version (get-in state-db [:documents uri :v] -1))
(when-let [kondo-result (shared/logging-time
(str "changes analyzed by clj-kondo took %s secs")
(lsp.kondo/run-kondo-on-text! text uri db))]
(let [filename (shared/uri->filename uri)
old-local-analysis (get-in @db [:analysis filename])]
(if (compare-and-set! db state-db (-> state-db
(update-analysis uri (:analysis kondo-result))
(update-findings uri (:findings kondo-result))
(update :processing-changes disj uri)
(assoc :kondo-config (:config kondo-result))))
(do
(f.diagnostic/sync-lint-file! uri db)
(when (settings/get db [:notify-references-on-file-change] true)
(notify-references filename old-local-analysis (get-in @db [:analysis filename]) components))
(clojure-producer/refresh-test-tree producer [uri]))
(recur @db)))))))
(defn did-change [uri changes version db]
(let [old-text (get-in @db [:documents uri :text])
final-text (reduce handle-change old-text changes)]
(swap! db (fn [state-db] (-> state-db
(assoc-in [:documents uri :v] version)
(assoc-in [:documents uri :text] final-text)
(update :processing-changes conj uri))))
(async/>!! db/current-changes-chan {:uri uri
:text final-text
:version version})))
(defn analyze-watched-created-files! [uris {:keys [db producer] :as components}]
(let [filenames (map shared/uri->filename uris)
result (shared/logging-time
"Created watched files analyzed, took %s secs"
(lsp.kondo/run-kondo-on-paths! filenames false components))
analysis (->> (:analysis result)
lsp.kondo/normalize-analysis
(group-by :filename))]
(swap! db (fn [state-db]
(-> state-db
(update :analysis merge analysis)
(assoc :kondo-config (:config result))
(update :findings merge (group-by :filename (:findings result))))))
(f.diagnostic/lint-project-files! filenames db)
(clojure-producer/refresh-test-tree producer uris)))
(defn did-change-watched-files [changes db]
(doseq [{:keys [uri type]} changes]
(case type
:created (async/>!! db/created-watched-files-chan uri)
;; TODO Fix outdated changes overwriting newer changes.
:changed nil #_(did-change uri
[{:text (slurp filename)}]
(inc (get-in @db [:documents uri :v] 0))
db)
:deleted (let [filename (shared/uri->filename uri)]
(swap! db (fn [state-db]
(-> state-db
(shared/dissoc-in [:documents uri])
(shared/dissoc-in [:analysis filename])
(shared/dissoc-in [:findings filename]))))))))
(defn did-close [uri db]
(let [filename (shared/uri->filename uri)
source-paths (settings/get db [:source-paths])]
(when (and (not (shared/external-filename? filename source-paths))
(not (shared/file-exists? (io/file filename))))
(swap! db (fn [state-db] (-> state-db
(shared/dissoc-in [:documents uri])
(shared/dissoc-in [:analysis filename])
(shared/dissoc-in [:findings filename]))))
(f.diagnostic/clean! uri db))))
(defn force-get-document-text
"Get document text from db, if document not found, tries to open the document"
[uri db]
(or (get-in @db [:documents uri :text])
(do
(did-open uri (slurp uri) db false)
(get-in @db [:documents uri :text]))))