Repository: clojure-lsp/clojure-lsp
Pull request #224 (2020-12-28 19:45:05)

Refactor project arch
* Move most features from `handler` ns to its own feature ns.
* Refactor `refactor` handler to use defmulti instead of map of key<->function

It didn't changes any logic regarding the features


Patch:
diff --git a/src/clojure_lsp/crawler.clj b/src/clojure_lsp/crawler.clj
index 30de09d9..1573a569 100644
--- a/src/clojure_lsp/crawler.clj
+++ b/src/clojure_lsp/crawler.clj
@@ -1,21 +1,20 @@
 (ns clojure-lsp.crawler
   (:require
-    [clj-kondo.core :as kondo]
-    [clojure-lsp.db :as db]
-    [clojure-lsp.parser :as parser]
-    [clojure-lsp.shared :as shared]
-    [clojure.core.async :as async]
-    [clojure.edn :as edn]
-    [clojure.java.io :as io]
-    [clojure.java.shell :as shell]
-    [clojure.set :as set]
-    [clojure.string :as string]
-    [clojure.tools.logging :as log]
-    [digest :as digest])
+   [clojure-lsp.db :as db]
+   [clojure-lsp.feature.diagnostics :as f.diagnostic]
+   [clojure-lsp.feature.references :as f.references]
+   [clojure-lsp.shared :as shared]
+   [clojure.core.async :as async]
+   [clojure.edn :as edn]
+   [clojure.java.io :as io]
+   [clojure.java.shell :as shell]
+   [clojure.set :as set]
+   [clojure.string :as string]
+   [clojure.tools.logging :as log]
+   [digest :as digest])
   (:import
-    [java.net URI]
-    [java.util.jar JarFile]
-    [java.nio.file Paths]))
+   [java.util.jar JarFile]
+   [java.nio.file Paths]))
 
 (defn ^:private file->uri [file]
   (str (.toUri (.toPath file))))
@@ -23,54 +22,6 @@
 (defn ^:private to-file [path child]
   (.toFile (.resolve path child)))
 
-(defn ^:private uri->path [uri]
-  (Paths/get (URI. uri)))
-
-(defn ^:private diagnose-unknown-forward-declarations [usages]
-  (let [forward-usages (seq (filter (fn [usage] (contains? (:tags usage) :forward))
-                                    usages))
-        forward-syms (set (map :sym forward-usages))
-        found-declarations (set (keep (fn [usage]
-                                        (when (and (contains? forward-syms (:sym usage))
-                                                   (contains? (:tags usage) :declare))
-                                          (:sym usage)))
-                                      usages))
-        unknown-forwards (filter (fn [usage]
-                                   (not (contains? found-declarations (:sym usage))))
-                                 forward-usages)]
-    (for [usage unknown-forwards]
-      {:range (shared/->range usage)
-       :code :unknown
-       :source "clojure-lsp"
-       :message (str "Unknown forward declaration: " (:str usage))
-       :severity 1})))
-
-(defn ^:private diagnose-unused-references [uri declared-references all-envs excluded-unused-ns-declarations]
-  (let [references (->> all-envs
-                        (mapcat (comp val))
-                        (remove (comp #(contains? % :declare) :tags))
-                        (remove (comp #(contains? % :forward) :tags))
-                        (map :sym)
-                        set)
-        unused-syms (set/difference (set (map :sym declared-references)) references)]
-    (for [usage (filter (comp unused-syms :sym) declared-references)
-          :let [code (condp set/subset? (:tags usage)
-                       #{:param} :unused-param
-                       #{:ns} :unused-ns
-                       #{:public} :unused-public
-                       :unused)]
-          :when (and (not= code :unused-param)
-                     (or (not= :unused-ns code)
-                         (not-any? #(string/index-of uri %)
-                                   (set/union #{"test/"} excluded-unused-ns-declarations))))]
-      {:range (shared/->range usage)
-       :code code
-       :source "clojure-lsp"
-       :message (case code
-                  :unused-ns (str "Unused namespace: " (:str usage))
-                  (str "Unused declaration: " (:str usage)))
-       :severity 2})))
-
 (defn ^:private process-unused
   [usages declarations]
   (let [ensure-sym (fn [s] (when-not (string? s) s))]
@@ -81,95 +32,9 @@
          set
          (set/difference (set (map :ns declarations))))))
 
-(defn ^:private usages->declarations [usages]
-  (->> usages
-       (filter (comp #(and (or (contains? % :declare)
-                               (contains? % :refer))
-                           (not (contains? % :factory))
-                           (not (contains? % :unused))) :tags))
-       (remove (comp #(string/starts-with? % "_") name :sym))))
-
-(defn ^:private diagnose-unused [uri usages excluded-unused-ns-declarations]
-  (let [all-envs (assoc (:file-envs @db/db) uri usages)
-        declarations (usages->declarations usages)
-        declared-references (remove (comp #(contains? % :alias) :tags) declarations)]
-    (concat (diagnose-unused-references uri declared-references all-envs excluded-unused-ns-declarations))))
-
-(defn ^:private kondo-finding->diagnostic [{:keys [type message level row col] :as finding}]
-  (let [expression? (not= row (:end-row finding))
-        finding (cond-> (merge {:end-row row :end-col col} finding)
-                  expression? (assoc :end-row row :end-col col))]
-    {:range (shared/->range finding)
-     :message message
-     :code (name type)
-     :severity (case level
-                 :error   1
-                 :warning 2
-                 :info    3)
-     :source "clj-kondo"}))
-
-(defn ^:private kondo-args [extra]
-  (let [root-path (uri->path (:project-root @db/db))
-        user-config (get-in @db/db [:settings :clj-kondo])
-        kondo-dir (.resolve root-path ".clj-kondo")]
-    (cond-> {:cache true
-             :cache-dir ".clj-kondo/cache"}
-      (.exists (.toFile kondo-dir))
-      (assoc :cache-dir (str (.resolve kondo-dir ".cache")) :config-dir (str kondo-dir))
-
-      :always
-      (merge extra)
-
-      user-config
-      (update-in [:config] merge user-config)
-
-      ;; TODO Duplicated linter. Remove after using clj-kondo for all linters
-      :always
-      (update-in [:config :linters] merge {:unused-private-var {:level :off}}))))
-
-(defn ^:private run-kondo-on-paths! [paths]
-  (kondo/run! (kondo-args {:lint [(string/join (System/getProperty "path.separator") paths)]})))
-
-(defn ^:private run-kondo-on-text! [text lang]
-  (with-in-str text (kondo/run! (kondo-args {:lint ["-"] :lang lang}))))
-
-(defn ^:private kondo-find-diagnostics [uri text]
-  (let [file-type (shared/uri->file-type uri)
-        {:keys [findings]} (run-kondo-on-text! text file-type)]
-    (->> findings
-         (filter #(= "<stdin>" (:filename %)))
-         (map kondo-finding->diagnostic))))
-
-(defn ^:private find-diagnostics [uri text usages excluded-unused-ns-declarations]
-  (let [kondo-diagnostics (kondo-find-diagnostics uri text)
-        unused (diagnose-unused uri usages excluded-unused-ns-declarations)
-        unknown-forwards (diagnose-unknown-forward-declarations usages)
-        result (concat unused unknown-forwards kondo-diagnostics)]
-    result))
-
-(defn safe-find-references
-  ([uri text]
-   (safe-find-references uri text true false))
-  ([uri text diagnose? remove-private?]
-   (try
-     (let [file-type (shared/uri->file-type uri)
-           macro-defs (get-in @db/db [:settings :macro-defs])
-           excluded-unused-ns-declarations (get-in @db/db [:settings :linters :unused-namespace-declarations] #{})
-           references (cond->> (parser/find-usages uri text file-type macro-defs)
-                        remove-private? (filter (fn [{:keys [tags]}] (and (:public tags) (:declare tags)))))]
-       (when diagnose?
-         (async/put! db/diagnostics-chan
-                     {:uri uri
-                      :diagnostics (find-diagnostics uri text references excluded-unused-ns-declarations)}))
-       references)
-     (catch Throwable e
-       (log/warn e "Cannot parse: " uri (.getMessage e))
-       ;; On purpose
-       nil))))
-
 (defn find-unused-aliases [uri]
-  (let [usages (safe-find-references uri (slurp uri) false false)
-        declarations (usages->declarations usages)
+  (let [usages (f.references/safe-find-references uri (slurp uri) false false)
+        declarations (f.diagnostic/usages->declarations usages)
         excludes (-> (get-in @db/db [:settings :linters :unused-namespace :exclude] #{}) set)
         declared-aliases (->> declarations
                               (filter (comp #(contains? % :alias) :tags))
@@ -177,8 +42,8 @@
     (process-unused usages declared-aliases)))
 
 (defn find-unused-refers [uri]
-  (let [usages (safe-find-references uri (slurp uri) false false)
-        declarations (usages->declarations usages)
+  (let [usages (f.references/safe-find-references uri (slurp uri) false false)
+        declarations (f.diagnostic/usages->declarations usages)
         excludes (-> (get-in @db/db [:settings :linters :unused-namespace :exclude] #{}) set)
         declared-refers (->> declarations
                               (filter (comp #(contains? % :refer) :tags))
@@ -208,7 +73,7 @@
              (map (fn [[uri entry jar]]
                     (let [text (with-open [stream (.getInputStream jar entry)]
                                  (slurp stream))]
-                      [uri (safe-find-references uri text false true)])))
+                      [uri (f.references/safe-find-references uri text false true)])))
              (remove (comp nil? second)))
         output-chan (async/chan 5)]
     (async/pipeline-blocking 5 output-chan xf (async/to-chan! jars) true (fn [e] (log/error "Error crawling jars" e)))
@@ -224,7 +89,7 @@
                           (string/ends-with? uri ".cljc")
                           (string/ends-with? uri ".cljs"))))
             (map (juxt identity (fn [uri]
-                                  (safe-find-references uri (slurp uri) false false))))
+                                  (f.references/safe-find-references uri (slurp uri) false false))))
             (remove (comp nil? second)))
         output-chan (async/chan)]
     (async/pipeline-blocking 5 output-chan xf (async/to-chan! dirs) true (fn [e] (log/warn e "hello")))
@@ -278,7 +143,7 @@
         :else :unkown))
 
 (defn determine-dependencies [project-root]
-  (let [root-path (uri->path project-root)
+  (let [root-path (shared/uri->path project-root)
         settings (:settings @db/db)
         source-paths (mapv #(to-file root-path %) (get settings :source-paths))
         dependency-scheme (get settings :dependency-scheme)
@@ -310,10 +175,10 @@
           (log/info "skipping classpath scan due to project hash match")
           (do
             (log/info "starting clj-kondo project classpath scan (this takes awhile)")
-            (let [results (run-kondo-on-paths! classpath)]
+            (let [results (f.diagnostic/run-kondo-on-paths! classpath)]
               (log/info "clj-kondo scanned project classpath in" (str (get-in results [:summary :duration]) "ms")))))
         (merge source-envs file-envs jar-envs))
-      (let [kondo-source-chan (async/go (run-kondo-on-paths! source-paths))
+      (let [kondo-source-chan (async/go (f.diagnostic/run-kondo-on-paths! source-paths))
             crawler-output-chan (async/go (crawl-source-dirs source-paths))]
         (log/info "clj-kondo scanned source paths in"
                   (str (get-in (async/<!! kondo-source-chan) [:summary :duration]) "ms"))
@@ -321,7 +186,7 @@
 
 (defn find-raw-project-settings [project-root]
   (let [config-path (Paths/get ".lsp" (into-array ["config.edn"]))]
-    (loop [dir (uri->path project-root)]
+    (loop [dir (shared/uri->path project-root)]
       (let [full-config-path (.resolve dir config-path)
             file (.toFile full-config-path)
             parent-dir (.getParent dir)]
diff --git a/src/clojure_lsp/feature/code_actions.clj b/src/clojure_lsp/feature/code_actions.clj
new file mode 100644
index 00000000..a5f0bf38
--- /dev/null
+++ b/src/clojure_lsp/feature/code_actions.clj
@@ -0,0 +1,49 @@
+(ns clojure-lsp.feature.code-actions
+  (:require
+    [clojure-lsp.feature.definition :as f.definition]
+    [clojure-lsp.refactor.transform :as r.transform]))
+
+(defn all [zloc uri row col diagnostics client-capabilities]
+  (let [workspace-edit-capability? (get-in client-capabilities [:workspace :workspace-edit])
+        inside-function? (r.transform/inside-function? zloc)
+        [_ {def-uri :uri
+            definition :usage}] (f.definition/definition-usage uri row col)
+        inline-symbol? (r.transform/inline-symbol? def-uri definition)
+        has-unknow-ns? (some #(= (compare "unresolved-namespace" (some-> % .getCode .get)) 0) diagnostics)
+        missing-ns (when has-unknow-ns?
+                     (r.transform/add-missing-libspec zloc))
+        line (dec row)
+        character (dec col)]
+    (cond-> []
+
+      (and has-unknow-ns? missing-ns)
+      (conj {:title          "Add missing namespace"
+             :kind           :quick-fix
+             :preferred?     true
+             :workspace-edit missing-ns})
+
+      inline-symbol?
+      (conj {:title   "Inline symbol"
+             :kind    :refactor-inline
+             :command {:title     "Inline symbol"
+                       :command   "inline-symbol"
+                       :arguments [uri line character]}})
+
+      inside-function?
+      (conj {:title   "Cycle privacy"
+             :kind    :refactor-rewrite
+             :command {:title     "Cycle privacy"
+                       :command   "cycle-privacy"
+                       :arguments [uri line character]}}
+            {:title   "Extract function"
+             :kind    :refactor-extract
+             :command {:title     "Extract function"
+                       :command   "extract-function"
+                       :arguments [uri line character "new-function"]}})
+
+      workspace-edit-capability?
+      (conj {:title   "Clean namespace"
+             :kind    :source-organize-imports
+             :command {:title     "Clean namespace"
+                       :command   "clean-ns"
+                       :arguments [uri line character]}}))))
diff --git a/src/clojure_lsp/feature/completion.clj b/src/clojure_lsp/feature/completion.clj
new file mode 100644
index 00000000..619df19f
--- /dev/null
+++ b/src/clojure_lsp/feature/completion.clj
@@ -0,0 +1,128 @@
+(ns clojure-lsp.feature.completion
+  (:require
+    [clojure-lsp.clojure-core :as cc]
+    [clojure-lsp.feature.documentation :as f.documentation]
+    [clojure-lsp.refactor.transform :as r.transform]
+    [clojure-lsp.shared :as shared]
+    [clojure.set :as set]
+    [clojure.string :as string]))
+
+(defn ^:private matches-cursor? [cursor-value s]
+  (when (and s (string/starts-with? s cursor-value))
+    s))
+
+(defn completion [uri line column file-envs remote-envs cursor-loc cursor-usage]
+  (let [local-env (get file-envs uri)
+        {cursor-value :str cursor-file-type :file-type} cursor-usage
+        [cursor-ns _] (if-let [idx (some-> cursor-value (string/index-of "/"))]
+                        [(subs cursor-value 0 idx) (subs cursor-value (inc idx))]
+                        [cursor-value nil])
+        matches? (partial matches-cursor? cursor-value)
+        namespaces-and-aliases (->> file-envs
+                                    (mapcat val)
+                                    (filter (fn [{:keys [file-type tags] :as _usage}]
+                                              (and
+                                                (= cursor-file-type file-type)
+                                                (or
+                                                  (set/subset? #{:public :ns} tags)
+                                                  (:alias tags)))))
+                                    (mapv (fn [{:keys [sym] alias-str :str alias-ns :ns :as _usage}]
+                                            [alias-str {:label (name sym)
+                                                        :detail (if alias-ns
+                                                                  (str alias-ns)
+                                                                  (name sym))
+                                                        :alias-str alias-str
+                                                        :alias-ns alias-ns}]))
+                                    (distinct)
+                                    (reduce (fn [m [k v]]
+                                              (update m k (fnil conj []) v))
+                                            {}))
+        remotes-by-ns (->> (for [[_ usages] remote-envs
+                                 usage usages
+                                 :when (and (set/subset? #{:ns :public} (:tags usage))
+                                            (= cursor-file-type (:file-type usage)))]
+                             [(:sym usage) usages])
+                           (into {}))]
+    (when cursor-value
+      (if (contains? (:tags cursor-usage) :refer)
+        ;; If the cursor is within a :refer then the sole
+        ;; completions should be symbols within the namespace
+        ;; that's being referred
+        (->> (get remotes-by-ns (:ns cursor-usage))
+             (filter #(every? (:tags %) [:declare :public]))
+             (filter (comp matches? :str))
+             (map (fn [candidate]
+                    {:label (:str candidate)
+                     :detail (str (:sym candidate))
+                     :data (str (:sym candidate))})))
+        ;; Otherwise, completions could come from various sources
+        (concat
+          (->> local-env
+               (filter (comp :declare :tags))
+               (filter (comp matches? :str))
+               (remove (fn [usage]
+                         (when-let [scope-bounds (:scope-bounds usage)]
+                           (not= :within (shared/check-bounds line column scope-bounds)))))
+               (mapv (fn [{:keys [sym kind]}]
+                       (cond-> {:label (name sym)
+                                :data (str sym)}
+                         kind (assoc :kind kind))))
+               (sort-by :label))
+          (->> namespaces-and-aliases
+               (filter (comp matches? key))
+               (mapcat val)
+               (mapv (fn [{:keys [alias-str alias-ns] :as info}]
+                       (let [require-edit (some-> cursor-loc
+                                                  (r.transform/add-known-libspec (symbol alias-str) alias-ns)
+                                                  (r.transform/result))]
+                         (cond-> (dissoc info :alias-ns :alias-str)
+                           require-edit (assoc :additional-text-edits (mapv #(update % :range shared/->range) require-edit))))))
+               (sort-by :label))
+          (->> (for [[alias-str matches] namespaces-and-aliases
+                     :when (= alias-str cursor-ns)
+                     {:keys [alias-ns]} matches
+                     :let [usages (get remotes-by-ns alias-ns)]
+                     usage usages
+                     :when (and (get-in usage [:tags :public])
+                                (not (get-in usage [:tags :ns]))
+                                (= cursor-file-type (:file-type usage)))
+                     :let [require-edit (some-> cursor-loc
+                                                (r.transform/add-known-libspec (symbol alias-str) alias-ns)
+                                                (r.transform/result))]]
+                 (cond-> {:label (str alias-str "/" (name (:sym usage)))
+                          :detail (name alias-ns)
+                          :data (str (:sym usage))}
+                   require-edit (assoc :additional-text-edits (mapv #(update % :range shared/->range) require-edit))))
+               (sort-by :label))
+          (->> cc/core-syms
+               (filter (comp matches? str))
+               (map (fn [sym] {:label (str sym)
+                               :data (str "clojure.core/" sym)}))
+               (sort-by :label))
+          (when (or (contains? cursor-file-type :cljc)
+                    (contains? cursor-file-type :cljs))
+            (->> cc/cljs-syms
+                 (filter (comp matches? str))
+                 (map (fn [sym] {:label (str sym)
+                                 :data (str "cljs.core/" sym)}))
+                 (sort-by :label)))
+          (when (or (contains? cursor-file-type :cljc)
+                    (contains? cursor-file-type :clj))
+            (->> cc/java-lang-syms
+                 (filter (comp matches? str))
+                 (map (fn [sym] {:label (str sym)
+                                 :data (str "java.lang." sym)}))
+                 (sort-by :label))))))))
+
+(defn resolve-item [label sym-wanted file-envs client-capabilities settings]
+  (let [usage (first
+                (for [[_ usages] file-envs
+                      {:keys [sym tags] :as usage} usages
+                      :when (and (= (str sym) sym-wanted)
+                                 (:declare tags))]
+                  usage))
+        [content-format] (get-in client-capabilities [:text-document :completion :completion-item :documentation-format])
+        show-docs-arity-on-same-line? (get-in settings [:show-docs-arity-on-same-line?])]
+    {:label label
+     :data sym-wanted
+     :documentation (f.documentation/generate content-format usage show-docs-arity-on-same-line?)}))
diff --git a/src/clojure_lsp/feature/definition.clj b/src/clojure_lsp/feature/definition.clj
new file mode 100644
index 00000000..cf533f28
--- /dev/null
+++ b/src/clojure_lsp/feature/definition.clj
@@ -0,0 +1,19 @@
+(ns clojure-lsp.feature.definition
+  (:require [clojure-lsp.db :as db]
+            [clojure-lsp.feature.references :as f.references]
+            [clojure-lsp.shared :as shared]))
+
+(defn definition-usage [doc-id line column]
+  (let [file-envs (:file-envs @db/db)
+        local-env (get file-envs doc-id)
+        file-type (shared/uri->file-type doc-id)
+        cursor (f.references/find-under-cursor line column local-env file-type)
+        cursor-sym (:sym cursor)]
+    [cursor
+     (first
+       (for [[env-doc-id usages] file-envs
+             {:keys [sym tags] :as usage} usages
+             :when (= sym cursor-sym)
+             :when (and (or (= doc-id env-doc-id) (:public tags))
+                        (:declare tags))]
+         {:uri env-doc-id :usage usage}))]))
diff --git a/src/clojure_lsp/feature/diagnostics.clj b/src/clojure_lsp/feature/diagnostics.clj
new file mode 100644
index 00000000..28cccb85
--- /dev/null
+++ b/src/clojure_lsp/feature/diagnostics.clj
@@ -0,0 +1,118 @@
+(ns clojure-lsp.feature.diagnostics
+  (:require
+    [clojure-lsp.shared :as shared]
+    [clj-kondo.core :as kondo]
+    [clojure.string :as string]
+    [clojure-lsp.db :as db]
+    [clojure.set :as set]))
+
+(defn ^:private diagnose-unknown-forward-declarations [usages]
+  (let [forward-usages (seq (filter (fn [usage] (contains? (:tags usage) :forward))
+                                    usages))
+        forward-syms (set (map :sym forward-usages))
+        found-declarations (set (keep (fn [usage]
+                                        (when (and (contains? forward-syms (:sym usage))
+                                                   (contains? (:tags usage) :declare))
+                                          (:sym usage)))
+                                      usages))
+        unknown-forwards (filter (fn [usage]
+                                   (not (contains? found-declarations (:sym usage))))
+                                 forward-usages)]
+    (for [usage unknown-forwards]
+      {:range (shared/->range usage)
+       :code :unknown
+       :source "clojure-lsp"
+       :message (str "Unknown forward declaration: " (:str usage))
+       :severity 1})))
+
+(defn ^:private diagnose-unused-references [uri declared-references all-envs excluded-unused-ns-declarations]
+  (let [references (->> all-envs
+                        (mapcat (comp val))
+                        (remove (comp #(contains? % :declare) :tags))
+                        (remove (comp #(contains? % :forward) :tags))
+                        (map :sym)
+                        set)
+        unused-syms (set/difference (set (map :sym declared-references)) references)]
+    (for [usage (filter (comp unused-syms :sym) declared-references)
+          :let [code (condp set/subset? (:tags usage)
+                       #{:param} :unused-param
+                       #{:ns} :unused-ns
+                       #{:public} :unused-public
+                       :unused)]
+          :when (and (not= code :unused-param)
+                     (or (not= :unused-ns code)
+                         (not-any? #(string/index-of uri %)
+                                   (set/union #{"test/"} excluded-unused-ns-declarations))))]
+      {:range (shared/->range usage)
+       :code code
+       :source "clojure-lsp"
+       :message (case code
+                  :unused-ns (str "Unused namespace: " (:str usage))
+                  (str "Unused declaration: " (:str usage)))
+       :severity 2})))
+
+(defn usages->declarations [usages]
+  (->> usages
+       (filter (comp #(and (or (contains? % :declare)
+                               (contains? % :refer))
+                           (not (contains? % :factory))
+                           (not (contains? % :unused))) :tags))
+       (remove (comp #(string/starts-with? % "_") name :sym))))
+
+(defn ^:private diagnose-unused [uri usages excluded-unused-ns-declarations]
+  (let [all-envs (assoc (:file-envs @db/db) uri usages)
+        declarations (usages->declarations usages)
+        declared-references (remove (comp #(contains? % :alias) :tags) declarations)]
+    (concat (diagnose-unused-references uri declared-references all-envs excluded-unused-ns-declarations))))
+
+(defn ^:private kondo-finding->diagnostic [{:keys [type message level row col] :as finding}]
+  (let [expression? (not= row (:end-row finding))
+        finding (cond-> (merge {:end-row row :end-col col} finding)
+                  expression? (assoc :end-row row :end-col col))]
+    {:range (shared/->range finding)
+     :message message
+     :code (name type)
+     :severity (case level
+                 :error   1
+                 :warning 2
+                 :info    3)
+     :source "clj-kondo"}))
+
+(defn ^:private kondo-args [extra]
+  (let [root-path (shared/uri->path (:project-root @db/db))
+        user-config (get-in @db/db [:settings :clj-kondo])
+        kondo-dir (.resolve root-path ".clj-kondo")]
+    (cond-> {:cache true
+             :cache-dir ".clj-kondo/cache"}
+      (.exists (.toFile kondo-dir))
+      (assoc :cache-dir (str (.resolve kondo-dir ".cache")) :config-dir (str kondo-dir))
+
+      :always
+      (merge extra)
+
+      user-config
+      (update-in [:config] merge user-config)
+
+      ;; TODO Duplicated linter. Remove after using clj-kondo for all linters
+      :always
+      (update-in [:config :linters] merge {:unused-private-var {:level :off}}))))
+
+(defn run-kondo-on-paths! [paths]
+  (kondo/run! (kondo-args {:lint [(string/join (System/getProperty "path.separator") paths)]})))
+
+(defn ^:private run-kondo-on-text! [text lang]
+  (with-in-str text (kondo/run! (kondo-args {:lint ["-"] :lang lang}))))
+
+(defn ^:private kondo-find-diagnostics [uri text]
+  (let [file-type (shared/uri->file-type uri)
+        {:keys [findings]} (run-kondo-on-text! text file-type)]
+    (->> findings
+         (filter #(= "<stdin>" (:filename %)))
+         (map kondo-finding->diagnostic))))
+
+(defn find-diagnostics [uri text usages excluded-unused-ns-declarations]
+  (let [kondo-diagnostics (kondo-find-diagnostics uri text)
+        unused (diagnose-unused uri usages excluded-unused-ns-declarations)
+        unknown-forwards (diagnose-unknown-forward-declarations usages)
+        result (concat unused unknown-forwards kondo-diagnostics)]
+    result))
diff --git a/src/clojure_lsp/feature/documentation.clj b/src/clojure_lsp/feature/documentation.clj
new file mode 100644
index 00000000..90ffb8c6
--- /dev/null
+++ b/src/clojure_lsp/feature/documentation.clj
@@ -0,0 +1,52 @@
+(ns clojure-lsp.feature.documentation
+  (:require
+    [clojure.string :as string]))
+
+(def line-break "\n----\n")
+(def opening-code "```clojure\n")
+(def closing-code "\n```\n")
+
+(defn ^:private drop-whitespace [n s]
+  (if (> n (count s))
+    s
+    (let [fully-trimmed (string/triml s)
+          dropped (subs s n)]
+      (last (sort-by count [fully-trimmed dropped])))))
+
+(defn ^:private count-whitespace [s]
+  (- (count s) (count (string/triml s))))
+
+
+(defn ^:private docstring->formatted-markdown [doc]
+  (let [lines       (string/split-lines doc)
+        other-lines (filter (comp not string/blank?) (rest lines))
+        multi-line? (> (count other-lines) 0)]
+    (str opening-code
+         (if-not multi-line?
+           doc
+           (let [indentation      (apply min (map count-whitespace other-lines))
+                 unindented-lines (cons (first lines)
+                                        (map #(drop-whitespace indentation %) (rest lines)))]
+             (string/join "\n" unindented-lines)))
+         closing-code)))
+
+
+(defn generate [content-format usage show-docs-arity-on-same-line?]
+  (let [{:keys [sym signatures doc]} usage
+        signatures (some->> signatures
+                            (:strings)
+                            (string/join "\n"))
+        signatures (if (and show-docs-arity-on-same-line? signatures)
+                     (-> signatures
+                         (clojure.string/replace #"\n" ",")
+                         (clojure.string/replace #"  +" " "))
+                     signatures)]
+    (case content-format
+      "markdown" {:kind "markdown"
+                  :value (cond-> (str opening-code sym " " (when show-docs-arity-on-same-line? signatures) closing-code)
+                           (and (not show-docs-arity-on-same-line?) signatures) (str opening-code signatures closing-code)
+                           (seq doc) (str line-break (docstring->formatted-markdown doc)))}
+      ;; Default to plaintext
+      (cond-> (str sym " " (when show-docs-arity-on-same-line? signatures) "\n")
+        (and (not show-docs-arity-on-same-line?) signatures) (str signatures "\n")
+        (seq doc) (str line-break doc "\n")))))
diff --git a/src/clojure_lsp/feature/refactor.clj b/src/clojure_lsp/feature/refactor.clj
new file mode 100644
index 00000000..b982eff5
--- /dev/null
+++ b/src/clojure_lsp/feature/refactor.clj
@@ -0,0 +1,65 @@
+(ns clojure-lsp.feature.refactor
+  (:require
+   [clojure-lsp.db :as db]
+   [clojure-lsp.parser :as parser]
+   [clojure-lsp.refactor.transform :as r.transform]
+   [clojure-lsp.feature.references :as f.references]
+   [clojure-lsp.feature.definition :as f.definition]))
+
+(defmulti refactor (comp :refactoring))
+
+(defmethod refactor :add-missing-libspec [{:keys [loc]}]
+  (r.transform/add-missing-libspec loc))
+
+(defmethod refactor :clean-ns [{:keys [loc uri]}]
+  (r.transform/clean-ns loc uri))
+
+(defmethod refactor :cycle-coll [{:keys [loc]}]
+  (r.transform/cycle-coll loc))
+
+(defmethod refactor :cycle-privacy [{:keys [loc]}]
+  (r.transform/cycle-privacy loc))
+
+(defmethod refactor :expand-let [{:keys [loc]}]
+  (r.transform/expand-let loc))
+
+(defmethod refactor :extract-function [{:keys [loc uri args]}]
+  (let [usages (get-in @db/db [:file-envs uri])
+        usages-in-form (parser/usages-in-form loc usages)]
+    (apply r.transform/extract-function loc (conj (vec args) usages-in-form))))
+
+(defmethod refactor :inline-symbol [{:keys [uri row col]}]
+  (let [usage (f.definition/definition-usage uri row col)
+        references (f.references/reference-usages uri row col)]
+    (r.transform/inline-symbol usage references)))
+
+(defmethod refactor :introduce-let [{:keys [loc args]}]
+  (apply r.transform/introduce-let loc (vec args)))
+
+(defmethod refactor :move-to-let [{:keys [loc args]}]
+  (apply r.transform/move-to-let loc (vec args)))
+
+(defmethod refactor :thread-first [{:keys [loc]}]
+  (r.transform/thread-first loc))
+
+(defmethod refactor :thread-first-all [{:keys [loc]}]
+  (r.transform/thread-first-all loc))
+
+(defmethod refactor :thread-last [{:keys [loc]}]
+  (r.transform/thread-last loc))
+
+(defmethod refactor :thread-last-all [{:keys [loc]}]
+  (r.transform/thread-last-all loc))
+
+(defmethod refactor :unwind-all [{:keys [loc]}]
+  (r.transform/unwind-all loc))
+
+(defmethod refactor :unwind-thread [{:keys [loc]}]
+  (r.transform/unwind-thread loc))
+
+(def available-refactors
+  (->> refactor
+       methods
+       keys
+       (map name)
+       vec))
diff --git a/src/clojure_lsp/feature/references.clj b/src/clojure_lsp/feature/references.clj
new file mode 100644
index 00000000..5582b153
--- /dev/null
+++ b/src/clojure_lsp/feature/references.clj
@@ -0,0 +1,61 @@
+(ns clojure-lsp.feature.references
+  (:require [clojure-lsp.shared :as shared]
+            [clojure.set :as set]
+            [clojure-lsp.db :as db]
+            [clojure-lsp.parser :as parser]
+            [clojure-lsp.feature.diagnostics :as f.diagnostic]
+            [clojure.core.async :as async]
+            [clojure.tools.logging :as log]))
+
+(defn find-after-cursor [line column env file-type]
+  (let [file-types (if (= :cljc file-type)
+                     #{:clj :cljs}
+                     #{file-type})]
+    (->> env
+         (filter (comp #(set/subset? % file-types) :file-type))
+         (partition-all 3 1)
+         (filter (comp #{:within :before} (partial shared/check-bounds line column) first))
+         first)))
+
+(defn find-under-cursor [line column env file-type]
+  (let [file-types (if (= :cljc file-type)
+                     #{:clj :cljs}
+                     #{file-type})]
+    (->> env
+         (filter (comp #(set/subset? % file-types) :file-type))
+         (filter (comp #{:within} (partial shared/check-bounds line column)))
+         ;; Pushes keywords last
+         (sort-by (comp keyword? :sym))
+         (first))))
+
+(defn reference-usages [doc-id line column]
+  (let [file-envs (:file-envs @db/db)
+        local-env (get file-envs doc-id)
+        {cursor-sym :sym} (find-under-cursor line column local-env (shared/uri->file-type doc-id))]
+    (into []
+          (for [[uri usages] (:file-envs @db/db)
+                {:keys [sym tags] :as usage} usages
+                :when (and (= sym cursor-sym)
+                           (not (contains? tags :declare)))]
+            {:uri uri
+             :usage usage}))))
+
+(defn safe-find-references
+  ([uri text]
+   (safe-find-references uri text true false))
+  ([uri text diagnose? remove-private?]
+   (try
+     (let [file-type (shared/uri->file-type uri)
+           macro-defs (get-in @db/db [:settings :macro-defs])
+           excluded-unused-ns-declarations (get-in @db/db [:settings :linters :unused-namespace-declarations] #{})
+           references (cond->> (parser/find-usages uri text file-type macro-defs)
+                        remove-private? (filter (fn [{:keys [tags]}] (and (:public tags) (:declare tags)))))]
+       (when diagnose?
+         (async/put! db/diagnostics-chan
+                     {:uri uri
+                      :diagnostics (f.diagnostic/find-diagnostics uri text references excluded-unused-ns-declarations)}))
+       references)
+     (catch Throwable e
+       (log/warn e "Cannot parse: " uri (.getMessage e))
+       ;; On purpose
+       nil))))
diff --git a/src/clojure_lsp/semantic_tokens.clj b/src/clojure_lsp/feature/semantic_tokens.clj
similarity index 97%
rename from src/clojure_lsp/semantic_tokens.clj
rename to src/clojure_lsp/feature/semantic_tokens.clj
index 5bd42227..c307ccab 100644
--- a/src/clojure_lsp/semantic_tokens.clj
+++ b/src/clojure_lsp/feature/semantic_tokens.clj
@@ -1,4 +1,4 @@
-(ns clojure-lsp.semantic-tokens)
+(ns clojure-lsp.feature.semantic-tokens)
 
 (def token-types
   [:function
diff --git a/src/clojure_lsp/handlers.clj b/src/clojure_lsp/handlers.clj
index 324f7445..a9fb9ae4 100644
--- a/src/clojure_lsp/handlers.clj
+++ b/src/clojure_lsp/handlers.clj
@@ -2,53 +2,28 @@
   (:require
     [cljfmt.core :as cljfmt]
     [cljfmt.main :as cljfmt.main]
-    [clojure-lsp.clojure-core :as cc]
     [clojure-lsp.crawler :as crawler]
     [clojure-lsp.db :as db]
+    [clojure-lsp.feature.code-actions :as f.code-actions]
+    [clojure-lsp.feature.completion :as f.completion]
+    [clojure-lsp.feature.definition :as f.definition]
+    [clojure-lsp.feature.documentation :as f.documentation]
+    [clojure-lsp.feature.refactor :as f.refactor]
+    [clojure-lsp.feature.references :as f.references]
+    [clojure-lsp.feature.semantic-tokens :as f.semantic-tokens]
     [clojure-lsp.interop :as interop]
     [clojure-lsp.parser :as parser]
-    [clojure-lsp.refactor.transform :as refactor]
+    [clojure-lsp.refactor.transform :as r.transform]
     [clojure-lsp.shared :as shared]
     [clojure.core.async :as async]
     [clojure.pprint :as pprint]
-    [clojure.set :as set]
     [clojure.string :as string]
     [clojure.tools.logging :as log]
     [rewrite-clj.node :as n]
-    [rewrite-clj.zip :as z]
-    [clojure-lsp.semantic-tokens :as semantic-tokens])
+    [rewrite-clj.zip :as z])
   (:import
     [java.net URL JarURLConnection]))
 
-(defn check-bounds [line column {:keys [row end-row col end-col] :as _usage}]
-  (cond
-    (< line row) :before
-    (and (= line row) (< column col)) :before
-    (< line end-row) :within
-    (and (= end-row line) (>= end-col column)) :within
-    :else :after))
-
-(defn- find-references-after-cursor [line column env file-type]
-  (let [file-types (if (= :cljc file-type)
-                     #{:clj :cljs}
-                     #{file-type})]
-    (->> env
-         (filter (comp #(set/subset? % file-types) :file-type))
-         (partition-all 3 1)
-         (filter (comp #{:within :before} (partial check-bounds line column) first))
-         first)))
-
-(defn find-reference-under-cursor [line column env file-type]
-  (let [file-types (if (= :cljc file-type)
-                     #{:clj :cljs}
-                     #{file-type})]
-    (->> env
-         (filter (comp #(set/subset? % file-types) :file-type))
-         (filter (comp #{:within} (partial check-bounds line column)))
-         ;; Pushes keywords last
-         (sort-by (comp keyword? :sym))
-         (first))))
-
 (defn ^:private uri->namespace [uri]
   (let [project-root (:project-root @db/db)
         source-paths (get-in @db/db [:settings :source-paths])
@@ -84,60 +59,13 @@
        "."
        (name file-type)))
 
-(defn- client-changes [changes]
+(defn ^:private client-changes [changes]
   (if (get-in @db/db [:client-capabilities :workspace :workspace-edit :document-changes])
     {:document-changes changes}
     {:changes (into {} (map (fn [{:keys [text-document edits]}]
                               [(:uri text-document) edits])
                             changes))}))
 
-(defn- drop-whitespace [n s]
-  (if (> n (count s))
-    s
-    (let [fully-trimmed (string/triml s)
-          dropped (subs s n)]
-      (last (sort-by count [fully-trimmed dropped])))))
-
-(defn- count-whitespace [s]
-  (- (count s) (count (string/triml s))))
-
-(def line-break "\n----\n")
-(def opening-code "```clojure\n")
-(def closing-code "\n```\n")
-
-(defn- docstring->formatted-markdown [doc]
-  (let [lines       (string/split-lines doc)
-        other-lines (filter (comp not string/blank?) (rest lines))
-        multi-line? (> (count other-lines) 0)]
-    (str opening-code
-         (if-not multi-line?
-           doc
-           (let [indentation      (apply min (map count-whitespace other-lines))
-                 unindented-lines (cons (first lines)
-                                        (map #(drop-whitespace indentation %) (rest lines)))]
-             (string/join "\n" unindented-lines)))
-         closing-code)))
-
-(defn- generate-docs [content-format usage show-docs-arity-on-same-line?]
-  (let [{:keys [sym signatures doc]} usage
-        signatures (some->> signatures
-                            (:strings)
-                            (string/join "\n"))
-        signatures (if (and show-docs-arity-on-same-line? signatures)
-                     (-> signatures
-                         (clojure.string/replace #"\n" ",")
-                         (clojure.string/replace #"  +" " "))
-                     signatures)]
-    (case content-format
-      "markdown" {:kind "markdown"
-                  :value (cond-> (str opening-code sym " " (when show-docs-arity-on-same-line? signatures) closing-code)
-                           (and (not show-docs-arity-on-same-line?) signatures) (str opening-code signatures closing-code)
-                           (seq doc) (str line-break (docstring->formatted-markdown doc)))}
-      ;; Default to plaintext
-      (cond-> (str sym " " (when show-docs-arity-on-same-line? signatures) "\n")
-        (and (not show-docs-arity-on-same-line?) signatures) (str signatures "\n")
-        (seq doc) (str line-break doc "\n")))))
-
 (defn did-open [uri text]
   (when-let [new-ns (and (string/blank? text)
                          (uri->namespace uri))]
@@ -148,7 +76,7 @@
                                :new-text new-text}]}]]
         (async/put! db/edits-chan (client-changes changes)))))
 
-  (when-let [references (crawler/safe-find-references uri text)]
+  (when-let [references (f.references/safe-find-references uri text)]
     (swap! db/db (fn [state-db]
                    (-> state-db
                        (assoc-in [:documents uri] {:v 0 :text text})
@@ -170,10 +98,6 @@
              :file-envs file-envs
              :project-aliases (apply merge (map (comp :aliases val) file-envs))))))
 
-(defn- matches-cursor? [cursor-value s]
-  (when (and s (string/starts-with? s cursor-value))
-    s))
-
 (defn completion [doc-id line column]
   (let [{:keys [text]} (get-in @db/db [:documents doc-id])
         file-envs (:file-envs @db/db)
@@ -184,141 +108,22 @@
                      (catch Exception e
                        (log/error (.getMessage e))))
         cursor-usage (loop [try-column column]
-                       (if-let [usage (find-reference-under-cursor line try-column local-env (shared/uri->file-type doc-id))]
+                       (if-let [usage (f.references/find-under-cursor line try-column local-env (shared/uri->file-type doc-id))]
                          usage
                          (when (pos? try-column)
-                           (recur (dec try-column)))))
-        {cursor-value :str cursor-file-type :file-type} cursor-usage
-        [cursor-ns _] (if-let [idx (some-> cursor-value (string/index-of "/"))]
-                        [(subs cursor-value 0 idx) (subs cursor-value (inc idx))]
-                        [cursor-value nil])
-        matches? (partial matches-cursor? cursor-value)
-        namespaces-and-aliases (->> file-envs
-                                    (mapcat val)
-                                    (filter (fn [{:keys [file-type tags] :as _usage}]
-                                              (and
-                                                (= cursor-file-type file-type)
-                                                (or
-                                                  (set/subset? #{:public :ns} tags)
-                                                  (:alias tags)))))
-                                    (mapv (fn [{:keys [sym] alias-str :str alias-ns :ns :as _usage}]
-                                            [alias-str {:label (name sym)
-                                                        :detail (if alias-ns
-                                                                  (str alias-ns)
-                                                                  (name sym))
-                                                        :alias-str alias-str
-                                                        :alias-ns alias-ns}]))
-                                    (distinct)
-                                    (reduce (fn [m [k v]]
-                                              (update m k (fnil conj []) v))
-                                            {}))
-        remotes-by-ns (->> (for [[_ usages] remote-envs
-                                 usage usages
-                                 :when (and (set/subset? #{:ns :public} (:tags usage))
-                                            (= cursor-file-type (:file-type usage)))]
-                             [(:sym usage) usages])
-                           (into {}))]
-    (when cursor-value
-      (if (contains? (:tags cursor-usage) :refer)
-        ;; If the cursor is within a :refer then the sole
-        ;; completions should be symbols within the namespace
-        ;; that's being referred
-        (->> (get remotes-by-ns (:ns cursor-usage))
-             (filter #(every? (:tags %) [:declare :public]))
-             (filter (comp matches? :str))
-             (map (fn [candidate]
-                    {:label (:str candidate)
-                     :detail (str (:sym candidate))
-                     :data (str (:sym candidate))})))
-        ;; Otherwise, completions could come from various sources
-        (concat
-          (->> local-env
-               (filter (comp :declare :tags))
-               (filter (comp matches? :str))
-               (remove (fn [usage]
-                         (when-let [scope-bounds (:scope-bounds usage)]
-                           (not= :within (check-bounds line column scope-bounds)))))
-               (mapv (fn [{:keys [sym kind]}]
-                       (cond-> {:label (name sym)
-                                :data (str sym)}
-                         kind (assoc :kind kind))))
-               (sort-by :label))
-          (->> namespaces-and-aliases
-               (filter (comp matches? key))
-               (mapcat val)
-               (mapv (fn [{:keys [alias-str alias-ns] :as info}]
-                       (let [require-edit (some-> cursor-loc
-                                                  (refactor/add-known-libspec (symbol alias-str) alias-ns)
-                                                  (refactor/result))]
-                         (cond-> (dissoc info :alias-ns :alias-str)
-                           require-edit (assoc :additional-text-edits (mapv #(update % :range shared/->range) require-edit))))))
-               (sort-by :label))
-          (->> (for [[alias-str matches] namespaces-and-aliases
-                     :when (= alias-str cursor-ns)
-                     {:keys [alias-ns]} matches
-                     :let [usages (get remotes-by-ns alias-ns)]
-                     usage usages
-                     :when (and (get-in usage [:tags :public])
-                                (not (get-in usage [:tags :ns]))
-                                (= cursor-file-type (:file-type usage)))
-                     :let [require-edit (some-> cursor-loc
-                                                (refactor/add-known-libspec (symbol alias-str) alias-ns)
-                                                (refactor/result))]]
-                 (cond-> {:label (str alias-str "/" (name (:sym usage)))
-                          :detail (name alias-ns)
-                          :data (str (:sym usage))}
-                   require-edit (assoc :additional-text-edits (mapv #(update % :range shared/->range) require-edit))))
-               (sort-by :label))
-          (->> cc/core-syms
-               (filter (comp matches? str))
-               (map (fn [sym] {:label (str sym)
-                               :data (str "clojure.core/" sym)}))
-               (sort-by :label))
-          (when (or (contains? cursor-file-type :cljc)
-                    (contains? cursor-file-type :cljs))
-            (->> cc/cljs-syms
-                 (filter (comp matches? str))
-                 (map (fn [sym] {:label (str sym)
-                                 :data (str "cljs.core/" sym)}))
-                 (sort-by :label)))
-          (when (or (contains? cursor-file-type :cljc)
-                    (contains? cursor-file-type :clj))
-            (->> cc/java-lang-syms
-                 (filter (comp matches? str))
-                 (map (fn [sym] {:label (str sym)
-                                 :data (str "java.lang." sym)}))
-                 (sort-by :label))))))))
+                           (recur (dec try-column)))))]
+    (f.completion/completion doc-id line column file-envs remote-envs cursor-loc cursor-usage)))
 
 (defn resolve-completion-item [label sym-wanted]
   (let [file-envs (:file-envs @db/db)
-        usage (first
-                (for [[_ usages] file-envs
-                      {:keys [sym tags] :as usage} usages
-                      :when (and (= (str sym) sym-wanted)
-                                 (:declare tags))]
-                  usage))
-        [content-format] (get-in @db/db [:client-capabilities :text-document :completion :completion-item :documentation-format])
-        show-docs-arity-on-same-line? (get-in @db/db [:settings :show-docs-arity-on-same-line?])]
-    {:label label
-     :data sym-wanted
-     :documentation (generate-docs content-format usage show-docs-arity-on-same-line?)}))
-
-(defn reference-usages [doc-id line column]
-  (let [file-envs (:file-envs @db/db)
-        local-env (get file-envs doc-id)
-        {cursor-sym :sym} (find-reference-under-cursor line column local-env (shared/uri->file-type doc-id))]
-    (into []
-          (for [[uri usages] (:file-envs @db/db)
-                {:keys [sym tags] :as usage} usages
-                :when (and (= sym cursor-sym)
-                           (not (contains? tags :declare)))]
-            {:uri uri
-             :usage usage}))))
+        client-capabilities (get @db/db :client-capabilities)
+        settings (get @db/db :settings)]
+    (f.completion/resolve-item label sym-wanted file-envs client-capabilities settings)))
 
 (defn references [doc-id line column]
   (mapv (fn [{:keys [uri usage]}]
           {:uri uri :range (shared/->range usage)})
-        (reference-usages doc-id line column)))
+        (f.references/reference-usages doc-id line column)))
 
 (defn did-close [uri]
   (swap! db/db #(-> %
@@ -328,13 +133,13 @@
   ;; Ensure we are only accepting newer changes
   (loop [state-db @db/db]
     (when (> version (get-in state-db [:documents uri :v] -1))
-      (when-let [references (crawler/safe-find-references uri text)]
+      (when-let [references (f.references/safe-find-references uri text)]
         (when-not (compare-and-set! db/db state-db (-> state-db
                                                        (assoc-in [:documents uri] {:v version :text text})
                                                        (assoc-in [:file-envs uri] references)))
           (recur @db/db))))))
 
-(defn- rename-alias [doc-id local-env cursor-usage cursor-name replacement]
+(defn ^:private rename-alias [doc-id local-env cursor-usage cursor-name replacement]
   (for [{u-str :str :as usage} local-env
         :let [version (get-in @db/db [:documents doc-id :v] 0)
               [u-prefix u-ns u-name] (parser/ident-split u-str)
@@ -345,7 +150,7 @@
      :new-text (if alias? replacement (str u-prefix replacement "/" u-name))
      :text-document {:version version :uri doc-id}}))
 
-(defn- rename-name [file-envs cursor-sym replacement]
+(defn ^:private rename-name [file-envs cursor-sym replacement]
   (for [[doc-id usages] file-envs
         :let [version (get-in @db/db [:documents doc-id :v] 0)]
         {u-sym :sym u-str :str :as usage} usages
@@ -361,7 +166,7 @@
         local-env (get file-envs doc-id)
         file-type (shared/uri->file-type doc-id)
         source-path (source-path-from-uri doc-id)
-        {cursor-sym :sym cursor-str :str tags :tags :as cursor-usage} (find-reference-under-cursor line column local-env file-type)]
+        {cursor-sym :sym cursor-str :str tags :tags :as cursor-usage} (f.references/find-under-cursor line column local-env file-type)]
     (when (and cursor-usage (not (simple-keyword? cursor-sym)) (not (contains? tags :norename)))
       (let [[_ cursor-ns cursor-name] (parser/ident-split cursor-str)
             replacement (if cursor-ns
@@ -389,23 +194,8 @@
                                       :new-uri new-uri}])))
           (client-changes doc-changes))))))
 
-(defn definition-usage [doc-id line column]
-  (let [file-envs (:file-envs @db/db)
-        local-env (get file-envs doc-id)
-        file-type (shared/uri->file-type doc-id)
-        cursor (find-reference-under-cursor line column local-env file-type)
-        cursor-sym (:sym cursor)]
-    [cursor
-     (first
-       (for [[env-doc-id usages] file-envs
-             {:keys [sym tags] :as usage} usages
-             :when (= sym cursor-sym)
-             :when (and (or (= doc-id env-doc-id) (:public tags))
-                        (:declare tags))]
-         {:uri env-doc-id :usage usage}))]))
-
 (defn definition [doc-id line column]
-  (let [[cursor {:keys [uri usage]}] (definition-usage doc-id line column)]
+  (let [[cursor {:keys [uri usage]}] (f.definition/definition-usage doc-id line column)]
     (log/info "Finding definition" doc-id "row" line "col" column "cursor" (:sym cursor))
     (if (:sym cursor)
       (if usage
@@ -414,7 +204,7 @@
       (let [file-envs (:file-envs @db/db)
             local-env (get file-envs doc-id)
             file-type (shared/uri->file-type doc-id)]
-        (if-let [next-stuff (find-references-after-cursor line column local-env file-type)]
+        (if-let [next-stuff (f.references/find-after-cursor line column local-env file-type)]
           (log/warn "Could not find element under cursor, next three known elements are:" (string/join ", " (map (comp pr-str :str) next-stuff)))
           (log/warn "Could not find element under cursor, there are no known elements after this position."))))))
 
@@ -464,7 +254,7 @@
 (defn document-highlight [doc-id line column]
   (let [file-envs (:file-envs @db/db)
         local-env (get file-envs doc-id)
-        cursor (find-reference-under-cursor line column local-env (shared/uri->file-type doc-id))
+        cursor (f.references/find-under-cursor line column local-env (shared/uri->file-type doc-id))
         sym (:sym cursor)]
     (into [] (comp (filter #(= (:sym %) sym))
                    (map file-env-entry->document-highlight)) local-env)))
@@ -492,45 +282,28 @@
            (sort-by :name)))
     []))
 
-(def refactorings
-  {"cycle-coll" #'refactor/cycle-coll
-   "thread-first" #'refactor/thread-first
-   "thread-first-all" #'refactor/thread-first-all
-   "thread-last" #'refactor/thread-last
-   "thread-last-all" #'refactor/thread-last-all
-   "unwind-thread" #'refactor/unwind-thread
-   "unwind-all" #'refactor/unwind-all
-   "inline-symbol" #'refactor/inline-symbol
-   "move-to-let" #'refactor/move-to-let
-   "introduce-let" #'refactor/introduce-let
-   "expand-let" #'refactor/expand-let
-   "clean-ns" #'refactor/clean-ns
-   "add-missing-libspec" #'refactor/add-missing-libspec
-   "extract-function" #'refactor/extract-function
-   "cycle-privacy" #'refactor/cycle-privacy})
-
- ;;TODO Refactor this function to a better agnostic refactor
 (defn refactor [doc-id line column refactoring args]
   (let [;; TODO Instead of v=0 should I send a change AND a document change
         {:keys [v text] :or {v 0}} (get-in @db/db [:documents doc-id])
         loc (parser/loc-at-pos text line column)
-        usages (get-in @db/db [:file-envs doc-id])
-        result (apply (get refactorings refactoring) loc doc-id
-                      (cond-> (vec args)
-                        (= "inline-symbol" refactoring) (conj (definition-usage doc-id line column) (reference-usages doc-id line column))
-                        (= "extract-function" refactoring) (conj (parser/usages-in-form loc usages))))]
+        result (f.refactor/refactor {:refactoring (keyword refactoring)
+                                       :loc loc
+                                       :uri doc-id
+                                       :row line
+                                       :col column
+                                       :args args})]
     (if (or loc
             (= "clean-ns" refactoring))
       (cond
         (map? result)
         (let [changes (vec (for [[doc-id sub-results] result]
                              {:text-document {:uri doc-id :version v}
-                              :edits (mapv #(update % :range shared/->range) (refactor/result sub-results))}))]
+                              :edits (mapv #(update % :range shared/->range) (r.transform/result sub-results))}))]
           (client-changes changes))
 
         (seq result)
         (let [changes [{:text-document {:uri doc-id :version v}
-                        :edits (mapv #(update % :range shared/->range) (refactor/result result))}]]
+                        :edits (mapv #(update % :range shared/->range) (r.transform/result result))}]]
           (client-changes changes))
 
         (empty? result)
@@ -551,7 +324,7 @@
 (defn hover [doc-id line column]
   (let [file-envs (:file-envs @db/db)
         local-env (get file-envs doc-id)
-        cursor (find-reference-under-cursor line column local-env (shared/uri->file-type doc-id))
+        cursor (f.references/find-under-cursor line column local-env (shared/uri->file-type doc-id))
         usage (first
                 (for [[_ usages] file-envs
                       {:keys [sym tags] :as usage} usages
@@ -560,7 +333,7 @@
                   usage))
         [content-format] (get-in @db/db [:client-capabilities :text-document :hover :content-format])
         show-docs-arity-on-same-line? (get-in @db/db [:settings :show-docs-arity-on-same-line?])
-        docs (generate-docs content-format usage show-docs-arity-on-same-line?)]
+        docs (f.documentation/generate content-format usage show-docs-arity-on-same-line?)]
     (if cursor
       {:range (shared/->range cursor)
        :contents (if (= content-format "markdown")
@@ -613,51 +386,13 @@
   (let [db @db/db
         row (inc (int line))
         col (inc (int character))
-        has-unknow-ns? (some #(= (compare "unresolved-namespace" (some-> % .getCode .get)) 0) diagnostics)
-        missing-ns (when has-unknow-ns?
-                     (refactor doc-id row col "add-missing-libspec" []))
         zloc (-> db
                  (get-in [:documents doc-id])
                  :text
                  (parser/loc-at-pos row col))
-        inside-function? (refactor/inside-function? zloc)
-        [_ {def-uri :uri
-            definition :usage}] (definition-usage doc-id row col)
-        inline-symbol? (refactor/inline-symbol? def-uri definition)
-        workspace-edit-capability? (get-in db [:client-capabilities :workspace :workspace-edit])]
-    (cond-> []
-
-      (and has-unknow-ns? missing-ns)
-      (conj {:title          "Add missing namespace"
-             :kind           :quick-fix
-             :preferred?     true
-             :workspace-edit missing-ns})
-
-      inline-symbol?
-      (conj {:title   "Inline symbol"
-             :kind    :refactor-inline
-             :command {:title     "Inline symbol"
-                       :command   "inline-symbol"
-                       :arguments [doc-id line character]}})
-
-      inside-function?
-      (conj {:title   "Cycle privacy"
-             :kind    :refactor-rewrite
-             :command {:title     "Cycle privacy"
-                       :command   "cycle-privacy"
-                       :arguments [doc-id line character]}}
-            {:title   "Extract function"
-             :kind    :refactor-extract
-             :command {:title     "Extract function"
-                       :command   "extract-function"
-                       :arguments [doc-id line character "new-function"]}})
-
-      workspace-edit-capability?
-      (conj {:title   "Clean namespace"
-             :kind    :source-organize-imports
-             :command {:title     "Clean namespace"
-                       :command   "clean-ns"
-                       :arguments [doc-id line character]}}))))
+        client-capabilities (get db :client-capabilities)]
+    (f.code-actions/all zloc doc-id row col diagnostics client-capabilities)))
+
 (defn code-lens
   [doc-id]
   (let [db     @db/db
@@ -675,7 +410,7 @@
   [range [doc-id row col]]
   {:range   range
    :command {:title   (-> doc-id
-                          (reference-usages row col)
+                          (f.references/reference-usages row col)
                           count
                           (str " references"))
              :command "code-lens-references"
@@ -685,5 +420,5 @@
   [doc-id]
   (let [db @db/db
         usages (get-in db [:file-envs doc-id])
-        data (semantic-tokens/full usages)]
+        data (f.semantic-tokens/full usages)]
     {:data data}))
diff --git a/src/clojure_lsp/main.clj b/src/clojure_lsp/main.clj
index 530f0200..36d06fb6 100644
--- a/src/clojure_lsp/main.clj
+++ b/src/clojure_lsp/main.clj
@@ -1,9 +1,10 @@
 (ns clojure-lsp.main
   (:require
    [clojure-lsp.db :as db]
+   [clojure-lsp.feature.refactor :as f.refactor]
    [clojure-lsp.handlers :as handlers]
    [clojure-lsp.interop :as interop]
-   [clojure-lsp.semantic-tokens :as semantic-tokens]
+   [clojure-lsp.feature.semantic-tokens :as semantic-tokens]
    [clojure-lsp.shared :as shared]
    [clojure.core.async :as async]
    [clojure.tools.logging :as log]
@@ -337,19 +338,19 @@
   WorkspaceService
 
   (^CompletableFuture executeCommand [_ ^ExecuteCommandParams params]
-   (go :executeCommand
-       (future
-         (end
-           (let [command (.getCommand params)
-                 args (.getArguments params)]
-             (log/info "Executing command" command "with args" args)
-             (cond
-               (= command "server-info")
-               (execute-server-info)
-
-               (contains? handlers/refactorings command)
-               (execute-refactor command args))))))
-   (CompletableFuture/completedFuture 0))
+    (go :executeCommand
+        (future
+          (end
+            (let [command (.getCommand params)
+                  args (.getArguments params)]
+              (log/info "Executing command" command "with args" args)
+              (cond
+                (= command "server-info")
+                (execute-server-info)
+
+                (some #(= % command) f.refactor/available-refactors)
+                (execute-refactor command args))))))
+    (CompletableFuture/completedFuture 0))
 
   (^void didChangeConfiguration [_ ^DidChangeConfigurationParams params]
     (log/warn params))
@@ -426,7 +427,7 @@
                                                                        (.setRange false)
                                                                        (.setFull true))))
                                        (.setExecuteCommandProvider (doto (ExecuteCommandOptions.)
-                                                                     (.setCommands (keys handlers/refactorings))))
+                                                                     (.setCommands f.refactor/available-refactors)))
                                        (.setTextDocumentSync (doto (TextDocumentSyncOptions.)
                                                                (.setOpenClose true)
                                                                (.setChange TextDocumentSyncKind/Full)
diff --git a/src/clojure_lsp/refactor/transform.clj b/src/clojure_lsp/refactor/transform.clj
index e6826697..ecd978d3 100644
--- a/src/clojure_lsp/refactor/transform.clj
+++ b/src/clojure_lsp/refactor/transform.clj
@@ -22,7 +22,7 @@
 
 (defn cycle-coll
   "Cycles collection between vector, list, map and set"
-  [zloc _uri]
+  [zloc]
   (let [sexpr (z/sexpr zloc)]
     (if (coll? sexpr)
       (let [node (z/node zloc)
@@ -78,12 +78,12 @@
   (= (z/tag zloc) :list))
 
 (defn thread-first
-  [zloc _uri]
+  [zloc]
   (when (can-thread? zloc)
     (thread-sym zloc '-> (meta (z/node zloc)))))
 
 (defn thread-last
-  [zloc _uri]
+  [zloc]
   (when (can-thread? zloc)
     (thread-sym zloc '->> (meta (z/node zloc)))))
 
@@ -99,15 +99,15 @@
             (assoc-in result [0 :range] top-range)))))))
 
 (defn thread-first-all
-  [zloc _uri]
+  [zloc]
   (thread-all zloc '->))
 
 (defn thread-last-all
-  [zloc _uri]
+  [zloc]
   (thread-all zloc '->>))
 
 (defn unwind-thread
-  [zloc _uri]
+  [zloc]
   (let [thread-loc (if (= (z/tag zloc) :list)
                      (z/next zloc)
                      zloc)
@@ -138,11 +138,11 @@
               :loc result-loc}]))))))
 
 (defn unwind-all
-  [zloc uri]
-  (loop [current (unwind-thread zloc uri)
+  [zloc]
+  (loop [current (unwind-thread zloc)
          result nil]
     (if current
-      (recur (unwind-thread (:loc (first current)) uri) current)
+      (recur (unwind-thread (:loc (first current))) current)
       result)))
 
 (defn find-within [zloc p?]
@@ -169,7 +169,7 @@
 
 (defn move-to-let
   "Adds form and symbol to a let further up the tree"
-  [zloc _uri binding-name]
+  [zloc binding-name]
   (when-let [let-top-loc (some-> zloc
                                  (edit/find-ops-up 'let)
                                  z/up)]
@@ -208,7 +208,7 @@
 
 (defn introduce-let
   "Adds a let around the current form."
-  [zloc _uri binding-name]
+  [zloc binding-name]
   (let [sym (symbol binding-name)
         {:keys [col]} (meta (z/node zloc))
         loc (-> zloc
@@ -228,7 +228,7 @@
 
 (defn expand-let
   "Expand the scope of the next let up the tree."
-  [zloc _uri]
+  [zloc]
   (let [let-loc (some-> zloc
                         (edit/find-ops-up 'let)
                         z/up)]
@@ -346,7 +346,7 @@
           :loc result-loc}]))))
 
 (defn add-missing-libspec
-  [zloc _uri]
+  [zloc]
   (let [ns-str-to-add (some-> zloc z/sexpr namespace)
         ns-to-add (some-> ns-str-to-add symbol)
         alias->info (->> (:file-envs @db/db)
@@ -373,7 +373,7 @@
     (add-known-libspec zloc ns-to-add qualified-ns-to-add)))
 
 (defn extract-function
-  [zloc _uri fn-name usages]
+  [zloc fn-name usages]
   (let [expr-loc (if (not= :token (z/tag zloc))
                    zloc
                    (z/up (edit/find-op zloc)))
@@ -409,7 +409,7 @@
   (edit/find-ops-up zloc 'defn 'defn- 'def 'defonce 'defmacro 'defmulti 's/defn 's/def))
 
 (defn cycle-privacy
-  [zloc _]
+  [zloc]
   (when-let [oploc (inside-function? zloc)]
     (let [op (z/sexpr oploc)
           switch-defn-? (and (= 'defn op)
@@ -438,7 +438,7 @@
             #{'let 'def})))
 
 (defn inline-symbol
-  [_zloc _uri [_ {def-uri :uri definition :usage}] references]
+  [[_ {def-uri :uri definition :usage}] references]
   (let [{:keys [text]} (get-in @db/db [:documents def-uri])
         def-loc (parser/loc-at-pos text (:row definition) (:col definition))
         op (inline-symbol? def-uri definition)]
diff --git a/src/clojure_lsp/shared.clj b/src/clojure_lsp/shared.clj
index 88859ab5..612ef954 100644
--- a/src/clojure_lsp/shared.clj
+++ b/src/clojure_lsp/shared.clj
@@ -2,8 +2,9 @@
   (:require
    [clojure.string :as string])
   (:import
-   (org.eclipse.lsp4j
-     Range)))
+   [java.net URI]
+   [java.nio.file Paths]
+   [org.eclipse.lsp4j Range]))
 
 (defn uri->file-type [uri]
   (cond
@@ -13,6 +14,9 @@
     (string/ends-with? uri ".edn") :edn
     :else :unknown))
 
+(defn uri->path [uri]
+  (Paths/get (URI. uri)))
+
 (defn ->range [{:keys [row end-row col end-col]}]
   {:start {:line (max 0 (dec row)) :character (max 0 (dec col))}
    :end {:line (max 0 (dec end-row)) :character (max 0 (dec end-col))}})
@@ -28,3 +32,12 @@
   (into {}
         (for [[k v] m]
           [(keyword k) v])))
+
+(defn check-bounds
+  [line column {:keys [row end-row col end-col] :as _usage}]
+  (cond
+    (< line row) :before
+    (and (= line row) (< column col)) :before
+    (< line end-row) :within
+    (and (= end-row line) (>= end-col column)) :within
+    :else :after))


Test patch:
diff --git a/test/clojure_lsp/semantic_tokens_test.clj b/test/clojure_lsp/features/semantic_tokens_test.clj
similarity index 96%
rename from test/clojure_lsp/semantic_tokens_test.clj
rename to test/clojure_lsp/features/semantic_tokens_test.clj
index f27a61a0..9913914b 100644
--- a/test/clojure_lsp/semantic_tokens_test.clj
+++ b/test/clojure_lsp/features/semantic_tokens_test.clj
@@ -1,6 +1,6 @@
-(ns clojure-lsp.semantic-tokens-test
+(ns clojure-lsp.features.semantic-tokens-test
   (:require [clojure-lsp.parser :as parser]
-            [clojure-lsp.semantic-tokens :as semantic-tokens]
+            [clojure-lsp.feature.semantic-tokens :as semantic-tokens]
             [clojure.string :as clojure.string]
             [clojure.test :refer [deftest is testing]]))
 
diff --git a/test/clojure_lsp/handlers_test.clj b/test/clojure_lsp/handlers_test.clj
index 28ea0b7c..895ff345 100644
--- a/test/clojure_lsp/handlers_test.clj
+++ b/test/clojure_lsp/handlers_test.clj
@@ -1,17 +1,17 @@
 (ns clojure-lsp.handlers-test
   (:require
-    [clojure-lsp.crawler :as crawler]
-    [clojure-lsp.db :as db]
-    [clojure-lsp.handlers :as handlers]
-    [clojure-lsp.parser :as parser]
-    [clojure.string :as string]
-    [clojure.test :refer :all])
+   [clojure-lsp.db :as db]
+   [clojure-lsp.feature.diagnostics :as f.diagnostic]
+   [clojure-lsp.handlers :as handlers]
+   [clojure-lsp.parser :as parser]
+   [clojure.string :as string]
+   [clojure.test :refer [deftest is testing]])
   (:import
-    (org.eclipse.lsp4j
-      Diagnostic
-      DiagnosticSeverity
-      Range
-      Position)))
+   (org.eclipse.lsp4j
+     Diagnostic
+     DiagnosticSeverity
+     Range
+     Position)))
 
 (deftest did-close
   (reset! db/db {:documents {"file://a.clj" {:text "(ns a)"}
@@ -215,7 +215,7 @@
                   (foo 1 ['a 'b])
                   (foo 1 2 3 {:k 1 :v 2})"]
         (reset! db/db {:file-envs {"file://a.clj" (parser/find-usages code :clj {})} :project-root "file:///"})
-        (let [usages (#'crawler/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
+        (let [usages (f.diagnostic/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
           (is (= ["user/foo is called with 3 args but expects 1 or 2"
                   "user/baz is called with 1 arg but expects 3"
                   "user/bar is called with 0 args but expects 1 or more"
@@ -246,7 +246,7 @@
                     (foo 1)
                     (bar))"]
         (reset! db/db {:file-envs {"file://a.clj" (parser/find-usages code :clj {})} :project-root "file:///"})
-        (let [usages (#'crawler/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
+        (let [usages (f.diagnostic/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
           (is (= ["user/foo is called with 2 args but expects 1 or 3"
                   "user/bar is called with 1 arg but expects 0"
                   "user/bar is called with 1 arg but expects 0"
@@ -262,7 +262,7 @@
                   (bar :a)
                   (bar :a :b)"]
         (reset! db/db {:file-envs {"file://a.clj" (parser/find-usages code :clj {})} :project-root "file:///"})
-        (let [usages (#'crawler/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
+        (let [usages (f.diagnostic/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
           (is (= ["user/foo is called with 2 args but expects 1"]
                  (map :message usages))))))
     ;; Waiting for kondo implement this support: https://github.com/borkdude/clj-kondo/issues/912
@@ -276,7 +276,7 @@
                   (foo)
                   (foo (foo :a :b))"]
         (reset! db/db {:file-envs {"file://a.clj" (parser/find-usages code :clj {})} :project-root "file:///"})
-        (let [usages (#'crawler/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
+        (let [usages (f.diagnostic/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
           (is (= ["No overload for 'foo' with 0 arguments"
                   "No overload for 'foo' with 2 arguments"]
                  (map :message usages))))))
@@ -289,7 +289,7 @@
                   (foo 1 2)
                   (foo 1)"]
         (reset! db/db {:file-envs {"file://a.clj" (parser/find-usages code :clj {})} :project-root "file:///"})
-        (let [usages (#'crawler/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
+        (let [usages (f.diagnostic/find-diagnostics "file://a.clj" code (get-in @db/db [:file-envs "file://a.clj"]) #{})]
           (is (= ["Unused namespace: user"
                   "user/foo is called with 0 args but expects 2"
                   "user/foo is called with 1 arg but expects 2"]
@@ -318,7 +318,7 @@
                      {"file://a.clj" (parser/find-usages "(ns a) (def bar ::bar)" :clj {})
                       "file://b.clj" (parser/find-usages code-b :clj {})}
                     :project-root "file:///"})
-      (let [usages (#'crawler/find-diagnostics "file://b.clj" code-b (get-in @db/db [:file-envs "file://b.clj"]) #{})]
+      (let [usages (f.diagnostic/find-diagnostics "file://b.clj" code-b (get-in @db/db [:file-envs "file://b.clj"]) #{})]
         (is (= ["Unused namespace: b"
                 "Unused declaration: x"
                 "Unused declaration: y"
@@ -330,7 +330,7 @@
     (let [code "(ns foo.bar)"]
       (reset! db/db {:file-envs {"file://foo/bar.clj" (parser/find-usages code :clj {})}
                      :project-root "file:///"})
-      (let [usages (#'crawler/find-diagnostics "file://foo/bar.clj" code (get-in @db/db [:file-envs "file://foo/bar.clj"]) #{"foo"})]
+      (let [usages (f.diagnostic/find-diagnostics "file://foo/bar.clj" code (get-in @db/db [:file-envs "file://foo/bar.clj"]) #{"foo"})]
         (is (empty?
                (map :message usages)))))))
 
diff --git a/test/clojure_lsp/refactor/transform_test.clj b/test/clojure_lsp/refactor/transform_test.clj
index 6cf754cb..70d35d42 100644
--- a/test/clojure_lsp/refactor/transform_test.clj
+++ b/test/clojure_lsp/refactor/transform_test.clj
@@ -1,13 +1,14 @@
 (ns clojure-lsp.refactor.transform-test
   (:require
     [clojure-lsp.db :as db]
+    [clojure-lsp.feature.definition :as f.definition]
+    [clojure-lsp.feature.references :as f.references]
     [clojure-lsp.handlers :as handlers]
     [clojure-lsp.parser :as parser]
     [clojure-lsp.refactor.edit :as edit]
     [clojure-lsp.refactor.transform :as transform]
     [clojure.string :as string]
     [clojure.test :refer :all]
-    [clojure.tools.logging :as log]
     [rewrite-clj.zip :as z]))
 
 (deftest paredit-test
@@ -22,12 +23,12 @@
 
 (deftest thread-test
   (let [zloc (z/of-string "(remove nil? (filter :id (map (comp now doit) xs)))")]
-    (let [[{:keys [loc]}] (transform/thread-last zloc nil)]
+    (let [[{:keys [loc]}] (transform/thread-last zloc)]
       (is (= '->> (z/sexpr (z/down loc))))
       (is (= (string/join "\n" ["(->> (filter :id (map (comp now doit) xs))"
                                 "     (remove nil?))"])
              (z/root-string loc))))
-    (let [[{:keys [loc]}] (transform/thread-last-all zloc nil)]
+    (let [[{:keys [loc]}] (transform/thread-last-all zloc)]
       (is (= '->> (z/sexpr (z/down loc))))
       (is (= (string/join "\n" ["(->> xs"
                                 "     (map (comp now doit))"
@@ -35,12 +36,12 @@
                                 "     (remove nil?))"])
              (z/root-string loc)))))
   (let [zloc (z/of-string "(assoc (dissoc (update m :xs reverse) :bye) :hello :world)")]
-    (let [[{:keys [loc]}] (transform/thread-first zloc nil)]
+    (let [[{:keys [loc]}] (transform/thread-first zloc)]
       (is (= '-> (z/sexpr (z/down loc))))
       (is (= (string/join "\n" ["(-> (dissoc (update m :xs reverse) :bye)"
                                 "    (assoc :hello :world))"])
              (z/root-string loc))))
-    (let [[{:keys [loc]}] (transform/thread-first-all zloc nil)]
+    (let [[{:keys [loc]}] (transform/thread-first-all zloc)]
       (is (= '-> (z/sexpr (z/down loc))))
       (is (= (string/join "\n" ["(-> m"
                                 "    (update :xs reverse)"
@@ -48,76 +49,76 @@
                                 "    (assoc :hello :world))"])
              (z/root-string loc)))))
   (let [zloc (z/of-string "(interpose (spaces) (foo))")
-        [{:keys [loc]} :as _result] (transform/thread-last-all zloc nil)]
+        [{:keys [loc]} :as _result] (transform/thread-last-all zloc)]
     (is (= '->> (z/sexpr (z/down loc))))
     (is (= "(->> (foo)\n     (interpose (spaces)))" (z/root-string loc))))
   (let [zloc (z/of-string "[:a :b]")
-        result (transform/thread-last-all zloc nil)]
+        result (transform/thread-last-all zloc)]
     (is (nil? result)))
   (let [zloc (z/of-string "(get-in foo [:a :b])")
-        [{:keys [loc]} :as _result] (transform/thread-last-all zloc nil)]
+        [{:keys [loc]} :as _result] (transform/thread-last-all zloc)]
     (is (= '->> (z/sexpr (z/down loc))))
     (is (= "(->> [:a :b]\n     (get-in foo))" (z/root-string loc)))))
 
 (deftest move-to-let-test
   (let [zloc (z/rightmost (z/down (z/of-string "(let [a 1] a)")))
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'b)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [a 1" \newline
                 "      b a] b)")
            (z/root-string loc))))
   (let [zloc (z/up (z/find-value (z/of-string "(let [a 1] (inc a))") z/next 'inc))
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'b)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [a 1" \newline
                 "      b (inc a)] b)") (z/root-string loc))))
   (let [zloc (z/up (z/find-value (z/of-string "(let [a 1] (thing (inc a)))") z/next 'inc))
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'b)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [a 1" \newline
                 "      b (inc a)] (thing b))") (z/root-string loc))))
   (let [zloc (z/up (z/find-value (z/of-string "(let [a 1] a) (inc b)") z/next 'inc))
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'b)]
     (is (nil? loc)))
   (let [zloc (z/of-string "(let [as [{:a :a}]] b)")
-        [{:keys [loc]}] (transform/move-to-let (z/rightmost (z/down zloc)) nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let (z/rightmost (z/down zloc)) 'b)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [as [{:a :a}]\n      b b] b)") (z/root-string loc))))
   (let [zloc (z/find-value (z/of-string "(let [a (inc 1)] a (inc 1))") z/next 1)
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'b)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [b 1" \newline
                 "      a (inc b)] a (inc b))") (z/root-string loc))))
   (let [zloc (z/up (z/find-value (z/of-string "(let [a (inc 1)] a (inc 1))") z/next 1))
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'b)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'b)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [b (inc 1)" \newline
                 "      a b] a b)") (z/root-string loc))))
   (let [zloc (z/find-value (z/of-string "(let [x 1] y)") z/next 'y)
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'x)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'x)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [x 1" \newline
                 "      x y] x)")
            (z/root-string loc))))
   (let [zloc (z/find-value (z/of-string "(let [[_] 1 a (x)] a)") z/next 'x)
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'x)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'x)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [[_] 1 x x" \newline
                 "      a (x)] a)")
            (z/root-string loc))))
   (let [zloc (z/find-value (z/of-string "(let [] a)") z/next 'a)
-        [{:keys [loc]}] (transform/move-to-let zloc nil 'x)]
+        [{:keys [loc]}] (transform/move-to-let zloc 'x)]
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [x a] x)")
            (z/root-string loc)))))
 
 (deftest introduce-let-test
   (let [zloc (z/of-string "(inc a)")
-        [{:keys [loc range]}] (transform/introduce-let zloc nil 'b)]
+        [{:keys [loc range]}] (transform/introduce-let zloc 'b)]
     (is (some? range))
     (is (= 'let (z/sexpr (z/down loc))))
     (is (= (str "(let [b (inc a)]\n  b)") (z/root-string loc)))
-    (let [[{:keys [loc range]}] (transform/introduce-let (z/rightmost (z/down  (z/of-string (z/root-string loc)))) nil 'c)]
+    (let [[{:keys [loc range]}] (transform/introduce-let (z/rightmost (z/down (z/of-string (z/root-string loc)))) 'c)]
       (is (some? range))
       (is (= 'let (z/sexpr (z/down loc))))
       (is (= (str "(let [b (inc a)\n c b]\n  c)") (z/root-string loc))))))
@@ -125,26 +126,26 @@
 (deftest expand-let-test
   (testing "simple"
     (let [zloc (-> (z/of-string "(+ 1 (let [a 1] a) 2)") (z/find-value z/next 'let))
-          [{:keys [loc range]}] (transform/expand-let zloc nil)]
+          [{:keys [loc range]}] (transform/expand-let zloc)]
       (is (some? range))
       (is (= 'let (z/sexpr (z/down loc))))
       (is (= (str "(let [a 1]\n (+ 1 a 2))") (z/root-string loc)))))
   (testing "in fn literal"
     (let [code "#(a (b c))"
           zloc (-> (z/of-string code) (z/find-value z/next 'b) (z/up))
-          [{:keys [loc]}] (transform/expand-let zloc nil)]
+          [{:keys [loc]}] (transform/expand-let zloc)]
       (is (nil? (z/root-string loc)))))
   (testing "in fn without args"
     (let [code "(def foo (fn [] (let [a 1] a) 2))"
           zloc (-> code z/of-string (z/find-value z/next 'let))
-          [{:keys [loc range]}] (transform/expand-let zloc nil)]
+          [{:keys [loc range]}] (transform/expand-let zloc)]
       (is (some? range))
       (is (= 'let (z/sexpr (z/down loc))))
       (is (= (str "(def foo (let [a 1]\n          (fn [] a 2)))") (z/root-string loc)))))
   (testing "in fn with args"
     (let [code "(def foo (fn [bar] (let [a 1] a) 2))"
           zloc (-> code z/of-string (z/find-value z/next 'let))
-          [{:keys [loc range]}] (transform/expand-let zloc nil)]
+          [{:keys [loc range]}] (transform/expand-let zloc)]
       (is (some? range))
       (is (= 'let (z/sexpr (z/down loc))))
       (is (= (str "(def foo (let [a 1]\n          (fn [bar] a 2)))") (z/root-string loc))))))
@@ -269,15 +270,15 @@
   (reset! db/db {:file-envs
                  {"file://a.clj" (parser/find-usages "(ns a (:require [foo.s :as s]))" :clj {})}})
   (let [zloc (-> (z/of-string "(ns foo) s/thing") z/rightmost)
-        [{:keys [loc range]}] (transform/add-missing-libspec zloc nil)]
+        [{:keys [loc range]}] (transform/add-missing-libspec zloc)]
     (is (some? range))
     (is (= '(ns foo (:require [foo.s :as s])) (z/sexpr loc)))))
 
 (deftest unwind-thread-test
   (let [zloc (z/of-string "(-> a b (c) d)")
-        [{loc1 :loc :keys [range]}] (transform/unwind-thread zloc nil)
-        [{loc2 :loc}] (transform/unwind-thread loc1 nil)
-        [{loc3 :loc}] (transform/unwind-thread loc2 nil)]
+        [{loc1 :loc :keys [range]}] (transform/unwind-thread zloc)
+        [{loc2 :loc}] (transform/unwind-thread loc1)
+        [{loc3 :loc}] (transform/unwind-thread loc2)]
     (is (some? range))
     (is (= "(-> (b a) (c) d)" (z/string loc1)))
     (is (= "(-> (c (b a)) d)" (z/string loc2)))
@@ -285,7 +286,7 @@
 
 (deftest unwind-all-test
   (let [zloc (z/of-string "(->> (a) b (c x y) d)")
-        [{:keys [loc range]}] (transform/unwind-all zloc nil)]
+        [{:keys [loc range]}] (transform/unwind-all zloc)]
     (is (some? range))
     (is (= "(d (c x y (b (a))))" (z/string loc)))))
 
@@ -295,7 +296,6 @@
         zloc (z/find-value (z/of-string code) z/next 'let)
         results (transform/extract-function
                   zloc
-                  nil
                   "foo"
                   (parser/usages-in-form zloc usages))]
     (is (= (z/string (:loc (first results))) "(defn foo [b]\n  (let [c 1] (b c)))"))
@@ -309,9 +309,9 @@
                      :file-envs {"file://a.clj" usages}})
       (let [zloc (z/find-value (z/of-string code) z/next 'something)
             pos (meta (z/node zloc))
-            definition (handlers/definition-usage "file://a.clj" (:row pos) (:col pos))
-            references (handlers/reference-usages "file://a.clj" (:row pos) (:col pos))
-            results (transform/inline-symbol zloc "file://a.clj" definition references)
+            definition (f.definition/definition-usage "file://a.clj" (:row pos) (:col pos))
+            references (f.references/reference-usages "file://a.clj" (:row pos) (:col pos))
+            results (transform/inline-symbol definition references)
             a-results (get results "file://a.clj")]
         (is (map? results))
         (is (= 1 (count results)))
@@ -328,9 +328,9 @@
                                  "file://b.clj" b-usages}})
       (let [zloc (z/find-value (z/of-string b-code) z/next 'a/something)
             pos (meta (z/node zloc))
-            definition (handlers/definition-usage "file://b.clj" (:row pos) (:col pos))
-            references (handlers/reference-usages "file://b.clj" (:row pos) (:col pos))
-            results (transform/inline-symbol zloc "file://b.clj" definition references)
+            definition (f.definition/definition-usage "file://b.clj" (:row pos) (:col pos))
+            references (f.references/reference-usages "file://b.clj" (:row pos) (:col pos))
+            results (transform/inline-symbol definition references)
             a-results (get results "file://a.clj")
             b-results (get results "file://b.clj")]
         (is (map? results))
@@ -342,22 +342,22 @@
   (testing "without-setting"
     (reset! db/db {:settings {}})
     (let [[{:keys [loc]}] (-> (z/find-value (z/of-string "(defn a [])") z/next 'a)
-                              (transform/cycle-privacy nil))]
+                              transform/cycle-privacy)]
       (is (= (z/string loc) "defn-")))
     (let [[{:keys [loc]}] (-> (z/find-value (z/of-string "(defn- a [])") z/next 'a)
-                              (transform/cycle-privacy nil))]
+                              transform/cycle-privacy)]
       (is (= (z/string loc) "defn")))
     (let [[{:keys [loc]}] (-> (z/find-value (z/of-string "(def a [])") z/next 'a)
-                              (transform/cycle-privacy nil))]
+                              transform/cycle-privacy)]
       (is (= (z/string loc) "^:private a")))
     (let [[{:keys [loc]}] (-> (z/find-value (z/of-string "(def ^:private a [])") z/next 'a)
-                              (transform/cycle-privacy nil))]
+                              transform/cycle-privacy)]
       (is (= (z/string loc) "a"))))
   (testing "with-setting"
     (reset! db/db {:settings {:use-metadata-for-privacy? true}})
     (let [[{:keys [loc]}] (-> (z/find-value (z/of-string "(defn a [])") z/next 'a)
-                              (transform/cycle-privacy nil))]
+                              transform/cycle-privacy)]
       (is (= (z/string loc) "^:private a")))
     (let [[{:keys [loc]}] (-> (z/find-value (z/of-string "(defn ^:private a [])") z/next 'a)
-                              (transform/cycle-privacy nil))]
+                              transform/cycle-privacy)]
       (is (= (z/string loc) "a")))))