-
-
Notifications
You must be signed in to change notification settings - Fork 149
/
rename.clj
297 lines (259 loc) · 12.6 KB
/
rename.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
(ns clojure-lsp.feature.rename
(:require
[clojure-lsp.queries :as q]
[clojure-lsp.settings :as settings]
[clojure-lsp.shared :as shared]
[clojure.string :as string]))
(set! *warn-on-reflection* true)
(defn ident-split [ident-str]
(let [ident-conformed (some-> ident-str (string/replace #"^::?" ""))
prefix (string/replace ident-str #"^(::?)?.*" "$1")
idx (string/index-of ident-conformed "/")]
(if (and idx (not= idx (dec (count ident-conformed))))
(into [prefix] (string/split ident-conformed #"/" 2))
[prefix nil ident-conformed])))
(defn ^:private rename-other
[replacement db reference]
(let [name-start (- (:name-end-col reference) (count (name (:name reference))))
ref-doc-uri (:uri reference)
version (get-in db [:documents ref-doc-uri :v] 0)]
{:range (shared/->range (assoc reference :name-col name-start))
:new-text replacement
:text-document {:version version :uri ref-doc-uri}}))
(defn ^:private rename-keyword
[replacement
replacement-raw
db
{:keys [ns alias name uri
name-col name-end-col
namespace-from-prefix
keys-destructuring] :as reference}]
(let [version (get-in db [:documents uri :v] 0)
;; Infers if the qualified keyword is of the ::kw-name kind
;; So the same-ns style or the full qualified name can be preserved
;; The 2 accounts for the 2 colons in same-namespace qualified keyword
qualified-same-ns? (= (- name-end-col name-col)
(+ 2 (count name)))
;; Extracts the name of the keyword
;; Maybe have the replacement analyzed by clj-kondo instead?
replacement-name (string/replace replacement #":+(.+/)?" "")
;; Extracts the namespace of the keyword
;; Maybe have the replacement analyzed by clj-kondo instead?
replacement-ns (string/replace replacement-raw #":+(.+)/.+" "$1")
namespace-changed? (and ns
replacement-ns
;; allow only simple namespaced keywords, not aliased keywords
(re-matches #"^:(.+)/.+" replacement-raw))
;; we find the locals analysis since when destructuring we have both
;; keyword and a locals analysis for the same position
local-element (when keys-destructuring
(q/find-local-by-destructured-keyword db uri reference))
text (cond
(and local-element
(string/includes? (:str local-element) "/")
(string/starts-with? (:str local-element) ":"))
(str ":" ns "/" replacement-name)
(and local-element
(string/includes? (:str local-element) "/"))
(str ns "/" replacement-name)
local-element
(str replacement-name)
alias
(str "::" alias "/" replacement-name)
(and qualified-same-ns?
;; check if it is from aliased keyword -> namespaced keyword
(string/starts-with? replacement-raw "::"))
(str "::" replacement-name)
(and qualified-same-ns?
;; check if is from aliased keyword -> namespaced keyword
(string/starts-with? replacement-raw ":"))
replacement-raw
namespace-from-prefix
(str ":" replacement-name)
namespace-changed?
(str ":" replacement-ns "/" replacement-name)
ns
(str ":" ns "/" replacement-name)
;; There shouldn't be another case, since renaming
;; unqualified keywords is currently disallowed
:else
replacement)]
(concat
[{:range (shared/->range reference)
:new-text text
:text-document {:version version :uri uri}}]
(when local-element
(->> (q/find-references db local-element false)
(map (fn [reference]
{:range (shared/->range reference)
:new-text replacement-name
:text-document {:version version :uri uri}})))))))
(defn ^:private rename-ns-definition [replacement db reference]
(let [ref-doc-uri (:uri reference)
version (get-in db [:documents ref-doc-uri :v] 0)
text (if (contains? #{:keyword-definitions :keyword-usages} (:bucket reference))
(str ":" replacement "/" (:name reference))
replacement)]
{:range (shared/->range reference)
:new-text text
:text-document {:version version :uri ref-doc-uri}}))
(defn ^:private rename-alias-definition [replacement db reference]
(let [alias? (= :namespace-alias (:bucket reference))
keyword? (contains? #{:keyword-definitions :keyword-usages} (:bucket reference))
ref-doc-uri (:uri reference)
[u-prefix _ u-name] (when-not alias?
(ident-split (:name reference)))
version (get-in db [:documents ref-doc-uri :v] 0)]
(if keyword?
{:range (shared/->range reference)
:new-text (str "::" replacement "/" (:name reference))
:text-document {:version version :uri ref-doc-uri}}
{:range (shared/->range reference)
:new-text (if alias? replacement (str u-prefix replacement "/" u-name))
:text-document {:version version :uri ref-doc-uri}})))
(defn ^:private rename-usages-with-alias
[{:keys [uri] :as element} replacement replacement-raw db references]
(let [new-alias (first (string/split replacement-raw #"/"))
old-alias (:alias (first (filter #(and (:alias %)
(= uri (:uri %))) references)))
alias-definition (q/find-namespace-alias-by-alias db uri old-alias)]
(conj
(mapv (fn [reference]
(cond
(= element reference)
{:range (shared/->range reference)
:new-text replacement-raw
:text-document {:version (get-in db [:documents uri :v] 0) :uri uri}}
(and (= (:uri reference) uri)
(:alias reference))
{:range (shared/->range reference)
:new-text replacement-raw
:text-document {:version (get-in db [:documents (:uri reference) :v] 0) :uri (:uri reference)}}
:else
(rename-other replacement db reference))) references)
(rename-alias-definition new-alias db alias-definition))))
(defn ^:private rename-local
[replacement db reference]
(let [name-start (- (:name-end-col reference) (count (name (:name reference))))
ref-doc-uri (:uri reference)
version (get-in db [:documents ref-doc-uri :v] 0)]
(if (string/starts-with? replacement ":")
{:range (shared/->range (assoc reference
:name-col name-start))
:new-text (subs replacement 1)
:text-document {:version version :uri ref-doc-uri}}
{:range (shared/->range (assoc reference :name-col name-start))
:new-text replacement
:text-document {:version version :uri ref-doc-uri}})))
(defn ^:private rename-defrecord
[replacement db reference]
(let [current-name (str (:name reference))
map->? (string/starts-with? current-name "map->")
->? (string/starts-with? current-name "->")
name-end (+ (:name-col reference) (count (name current-name)))
ref-doc-uri (:uri reference)
version (get-in db [:documents ref-doc-uri :v] 0)]
{:new-text (cond
map->?
(str "map->" replacement)
->?
(str "->" replacement)
:else
replacement)
:text-document {:version version :uri ref-doc-uri}
:range (shared/->range (assoc reference :name-end-col name-end))}))
(defn ^:private rename-changes
[element definition references replacement replacement-raw db]
(cond
(identical? :namespace-alias (:bucket element))
(mapv (partial rename-alias-definition replacement db) references)
(and (identical? :var-usages (:bucket element))
(string/includes? replacement-raw "/"))
(rename-usages-with-alias element replacement replacement-raw db references)
(identical? :namespace-definitions (:bucket definition))
(mapv (partial rename-ns-definition replacement db) references)
(contains? #{:keyword-definitions :keyword-usages} (:bucket definition))
(vec (mapcat (partial rename-keyword replacement replacement-raw db) references))
(identical? :locals (:bucket definition))
(mapv (partial rename-local replacement db) references)
(and (identical? :var-definitions (:bucket definition))
(contains? '#{clojure.core/defrecord cljs.core/defrecord}
(:defined-by definition)))
(->> references
(remove #(and (identical? :var-definitions (:bucket %))
(or (string/starts-with? (str (:name %)) "->")
(string/starts-with? (str (:name %)) "map->"))))
(mapv (partial rename-defrecord replacement db)))
:else
(mapv (partial rename-other replacement db) references)))
(defn ^:private rename-status [db element]
(let [references (q/find-references db element true)
definition (q/find-definition db element)
client-capabilities (:client-capabilities db)
source-paths (settings/get db [:source-paths])
source-path (shared/uri->source-path (:uri definition) source-paths)]
(cond
(empty? references)
{:error {:message "Can't rename, no other references found."
:code :invalid-params}}
(and (= :namespace-definitions (:bucket definition))
(not= :namespace-alias (:bucket element))
(not source-path))
{:error {:code :invalid-params
:message "Can't rename namespace, invalid source-paths. Are project :source-paths configured correctly?"}}
(and (= :namespace-definitions (:bucket definition))
(not= :namespace-alias (:bucket element))
(not (get-in client-capabilities [:workspace :workspace-edit :document-changes])))
{:error {:code :invalid-params
:message "Can't rename namespace, client does not support file renames."}}
(and (contains? #{:keyword-definitions :keyword-usages} (:bucket definition))
(not (:ns definition)))
{:error {:code :invalid-params
:message "Can't rename, only namespaced keywords can be renamed."}}
:else
{:result :success
:references references
:definition definition
:source-path source-path})))
(def ^:private error-no-element
{:error {:code :invalid-params
:message "Can't rename, no element found."}})
(defn prepare-rename
[uri row col db]
(let [element (q/find-element-under-cursor db uri row col)]
(if-not element
error-no-element
(let [{:keys [error] :as result} (rename-status db element)]
(if error
result
(shared/->range element))))))
(defn rename-element [uri new-name db element source]
(let [{:keys [error] :as result} (rename-status db element)]
(if error
result
(let [{:keys [references definition source-path]} result
replacement (string/replace new-name #".*/([^/]*)$" "$1")
changes (rename-changes element definition references replacement new-name db)
doc-changes (->> changes
(group-by :text-document)
(remove (comp empty? val))
(map (fn [[text-document edits]]
{:text-document text-document
:edits (mapv #(dissoc % :text-document) edits)})))]
(if (and (identical? :namespace-definitions (:bucket definition))
(not (identical? :namespace-alias (:bucket element)))
(not= :rename-file source))
(let [def-uri (:uri definition)
file-type (shared/uri->file-type def-uri)
new-uri (shared/namespace->uri replacement source-path file-type db)]
(shared/client-changes (concat doc-changes
[{:kind "rename"
:old-uri uri
:new-uri new-uri}])
db))
(shared/client-changes doc-changes db))))))
(defn rename-from-position
[uri new-name row col db]
(if-let [element (q/find-element-under-cursor db uri row col)]
(rename-element uri new-name db element :rename)
error-no-element))