Repository: clojure-lsp/clojure-lsp
Pull request #256 (2021-01-14 00:49:04)

Add code lens resolve support
- WIth LSP 3.16 released, servers can add support for codeActions/resolve, which should improve server performance since we will only calculate the workspace-edits if client chooses to execute the code action. If client does not support resolve code action yet, we just resolve them on `textDocument/codeActions` as before.
-  Refactor code actions code
- Add support to j/from-java where we can now unwrap a java class -> clojure easily and more realiable


Patch:
diff --git a/deps.edn b/deps.edn
index 2ebcd974..87c1f693 100644
--- a/deps.edn
+++ b/deps.edn
@@ -4,6 +4,7 @@
         org.eclipse.xtend/org.eclipse.xtend.lib {:mvn/version "2.24.0" :exclusions [com.google.guava/guava]}
         com.google.guava/guava {:mvn/version "30.1-jre"}
         rewrite-clj/rewrite-clj {:local/root "lib/rewrite-clj-0.6.2-SNAPSHOT.jar"}
+        org.clojure/data.json {:mvn/version "1.0.0"}
         log4j/log4j {:mvn/version "1.2.17" :exclusions [javax.mail/mail
                                                         javax.jms/jms
                                                         com.sun.jdmk/jmxtools
diff --git a/docs/capabilities.md b/docs/capabilities.md
index c1b2a3f2..73ab30f9 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -47,6 +47,7 @@ Below are all the currently supported LSP capabilities and their implementation
 | textDocument/documentHighlight         | √    |                                               |
 | textDocument/documentSymbol            | √    |                                               |
 | textDocument/codeAction                | √    |                                               |
+| codeAction/resolve                     | √    |                                               |
 | textDocument/codeLens                  | √    |                                               |
 | codeLens/resolve                       | √    |                                               |
 | textDocument/documentLink              |      |                                               |
@@ -109,4 +110,4 @@ See Vim client section for an example.
 
 Emacs provides all those refactorings via [lsp-mode](https://emacs-lsp.github.io/lsp-mode/)  with the `lsp-clojure-` prefix.
 
-Other clients might provide a higher level interface to `workspace/executeCommand` you need to pass the path, line and column numbers.
\ No newline at end of file
+Other clients might provide a higher level interface to `workspace/executeCommand` you need to pass the path, line and column numbers.
diff --git a/src/clojure_lsp/feature/code_actions.clj b/src/clojure_lsp/feature/code_actions.clj
index e39adb95..fcba6c43 100644
--- a/src/clojure_lsp/feature/code_actions.clj
+++ b/src/clojure_lsp/feature/code_actions.clj
@@ -2,67 +2,124 @@
   (:require
     [clojure-lsp.feature.definition :as f.definition]
     [clojure-lsp.feature.refactor :as f.refactor]
-    [clojure-lsp.refactor.transform :as r.transform]
-    [rewrite-clj.zip :as z]))
+    [clojure-lsp.refactor.transform :as r.transform])
+  (:import
+    (org.eclipse.lsp4j
+      CodeActionKind)))
+
+(defn dissoc-if-nil [m k]
+  (if (k m)
+    m
+    (dissoc m k)))
+
+(defn resolve-code-action
+  [{{:keys [id uri line character]} :data :as code-action}
+   zloc]
+  (->
+    (merge code-action
+           (case id
+
+             "add-missing-require"
+             (let [missing-require-edit (f.refactor/call-refactor {:loc         zloc
+                                                                   :refactoring :add-missing-libspec
+                                                                   :uri         uri
+                                                                   :version     0
+                                                                   :row         (inc line)
+                                                                   :col         (inc character)})]
+               {:workspace-edit missing-require-edit})
+
+             "add-missing-import"
+             (let [missing-import-edit (r.transform/add-common-import-to-namespace zloc)]
+               {:workspace-edit missing-import-edit})
+
+             "refactor-inline-symbol"
+             {:command {:title     "Inline symbol"
+                        :command   "inline-symbol"
+                        :arguments [uri line character]}}
+
+             "refactor-cycle-privacy"
+             {:command {:title     "Cycle privacy"
+                        :command   "cycle-privacy"
+                        :arguments [uri line character]}}
+
+             "refactor-extract-function"
+             {:command {:title     "Extract function"
+                        :command   "extract-function"
+                        :arguments [uri line character "new-function"]}}
+
+             "clean-ns"
+             {:command {:title     "Clean namespace"
+                        :command   "clean-ns"
+                        :arguments [uri line character]}}
+             {}))
+    (dissoc-if-nil :data)
+    (dissoc-if-nil :diagnostics)
+    (dissoc-if-nil :command)))
 
 (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)
-        line (dec row)
-        character (dec col)
-        has-unknown-ns? (some #(= (compare "unresolved-namespace" (some-> % .getCode .get)) 0) diagnostics)
-        unresolved-symbol (some #(= (compare "unresolved-symbol" (some-> % .getCode .get)) 0) diagnostics)
-        known-refer? (when unresolved-symbol
-                       (get r.transform/common-refers->info (z/sexpr zloc)))
-        missing-require (when (or has-unknown-ns? known-refer?)
-                          (f.refactor/call-refactor {:loc zloc
-                                                     :refactoring :add-missing-libspec
-                                                     :uri uri
-                                                     :version 0
-                                                     :row row
-                                                     :col col
-                                                     :args {:source :code-action}}))
-        missing-import (when unresolved-symbol
-                          (r.transform/add-common-import-to-namespace zloc))]
+        resolve-support?           (get-in client-capabilities [:text-document :code-action :resolve-support])
+        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)
+        line                       (dec row)
+        character                  (dec col)
+        has-unknown-ns?            (some #(= (compare "unresolved-namespace" (:code %)) 0) diagnostics)
+        unresolved-symbol          (some #(= (compare "unresolved-symbol" (:code %)) 0) diagnostics)
+        missing-require            (when (or has-unknown-ns? unresolved-symbol)
+                                     (r.transform/find-missing-require zloc))
+        missing-import             (when unresolved-symbol
+                                     (r.transform/find-missing-import zloc))]
     (cond-> []
 
-      (:result missing-require)
-      (conj {:title          (str "Add missing '" (-> missing-require :code-action-data :ns-name) "' require")
-             :kind           :quick-fix
-             :preferred?     true
-             :workspace-edit (f.refactor/refactor-client-seq-changes uri 0 (:result missing-require))})
+      missing-require
+      (conj {:title      (str "Add missing '" missing-require "' require")
+             :kind       CodeActionKind/QuickFix
+             :preferred? true
+             :data       {:id        "add-missing-require"
+                          :uri       uri
+                          :line      line
+                          :character character}})
 
-      (:result missing-import)
-      (conj {:title          (str "Add missing '" (-> missing-import :code-action-data :import-name) "' import")
-             :kind           :quick-fix
-             :preferred?     true
-             :workspace-edit (f.refactor/refactor-client-seq-changes uri 0 (:result missing-import))})
+      missing-import
+      (conj {:title      (str "Add missing '" missing-import "' import")
+             :kind       CodeActionKind/QuickFix
+             :preferred? true
+             :data       {:id        "add-missing-import"
+                          :uri       uri
+                          :line      line
+                          :character character}})
 
       inline-symbol?
-      (conj {:title   "Inline symbol"
-             :kind    :refactor-inline
-             :command {:title     "Inline symbol"
-                       :command   "inline-symbol"
-                       :arguments [uri line character]}})
+      (conj {:title "Inline symbol"
+             :kind  CodeActionKind/RefactorInline
+             :data  {:id        "refactor-inline-symbol"
+                     :uri       uri
+                     :line      line
+                     :character 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"]}})
+      (conj {:title "Cycle privacy"
+             :kind  CodeActionKind/RefactorRewrite
+             :data  {:id        "refactor-cycle-privacy"
+                     :uri       uri
+                     :line      line
+                     :character character}}
+            {:title "Extract function"
+             :kind  CodeActionKind/RefactorExtract
+             :data  {:id        "refactor-extract-function"
+                     :uri       uri
+                     :line      line
+                     :character character}})
 
       workspace-edit-capability?
-      (conj {:title   "Clean namespace"
-             :kind    :source-organize-imports
-             :command {:title     "Clean namespace"
-                       :command   "clean-ns"
-                       :arguments [uri line character]}}))))
+      (conj {:title "Clean namespace"
+             :kind  CodeActionKind/SourceOrganizeImports
+             :data  {:id        "clean-ns"
+                     :uri       uri
+                     :line      line
+                     :character character}})
+
+      (not resolve-support?)
+      (->> (map #(resolve-code-action % zloc))))))
diff --git a/src/clojure_lsp/feature/refactor.clj b/src/clojure_lsp/feature/refactor.clj
index bf586459..d7a82b4b 100644
--- a/src/clojure_lsp/feature/refactor.clj
+++ b/src/clojure_lsp/feature/refactor.clj
@@ -19,10 +19,10 @@
 (defmulti refactor (comp :refactoring))
 
 (defmethod refactor :add-import-to-namespace [{:keys [loc args]}]
-  (r.transform/add-import-to-namespace loc (vec args)))
+  (apply r.transform/add-import-to-namespace loc (vec args)))
 
-(defmethod refactor :add-missing-libspec [{:keys [loc args]}]
-  (r.transform/add-missing-libspec loc args))
+(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))
@@ -77,19 +77,11 @@
        (map name)
        vec))
 
-(defn refactor-client-seq-changes [uri version result]
-  (let [changes [{:text-document {:uri uri :version version}
-                  :edits (mapv #(update % :range shared/->range) (r.transform/result result))}]]
-    (client-changes changes)))
-
 (defn call-refactor [{:keys [loc uri refactoring row col version] :as data}]
   (let [result (refactor data)]
     (if (or loc
             (= :clean-ns refactoring))
       (cond
-        (and (map? result)
-             (contains? result :code-action-data))
-        result
 
         (map? result)
         (let [changes (vec (for [[doc-id sub-results] result]
@@ -98,7 +90,9 @@
           (client-changes changes))
 
         (seq result)
-        (refactor-client-seq-changes uri version result)
+        (let [changes [{:text-document {:uri uri :version version}
+                        :edits (mapv #(update % :range shared/->range) (r.transform/result result))}]]
+          (client-changes changes))
 
         (empty? result)
         (log/warn refactoring "made no changes" (z/string loc))
diff --git a/src/clojure_lsp/handlers.clj b/src/clojure_lsp/handlers.clj
index 64da516b..72010909 100644
--- a/src/clojure_lsp/handlers.clj
+++ b/src/clojure_lsp/handlers.clj
@@ -24,7 +24,8 @@
     [rewrite-clj.zip :as z]
     [trptcolin.versioneer.core :as version])
   (:import
-    [java.net URL JarURLConnection]))
+    [java.net URL
+              JarURLConnection]))
 
 (defn ^:private uri->namespace [uri]
   (let [project-root (:project-root @db/db)
@@ -61,6 +62,12 @@
        "."
        (name file-type)))
 
+(defn ^:private cursor-zloc [uri line character]
+  (-> @db/db
+      (get-in [:documents uri])
+      :text
+      (parser/loc-at-pos (inc line) (inc character))))
+
 (defn did-open [uri text]
   (when-let [new-ns (and (string/blank? text)
                          (uri->namespace uri))]
@@ -339,16 +346,20 @@
                        (update :file-envs #(apply dissoc % uris)))))))
 
 (defn code-actions
-  [doc-id diagnostics line character]
+  [{:keys [range context textDocument]}]
   (let [db @db/db
-        row (inc (int line))
-        col (inc (int character))
-        zloc (-> db
-                 (get-in [:documents doc-id])
-                 :text
-                 (parser/loc-at-pos row col))
+        diagnostics (-> context :diagnostics)
+        line (-> range :start :line)
+        character (-> range :start :character)
+        row (inc line)
+        col (inc character)
+        zloc (cursor-zloc textDocument line character)
         client-capabilities (get db :client-capabilities)]
-    (f.code-actions/all zloc doc-id row col diagnostics client-capabilities)))
+    (f.code-actions/all zloc textDocument row col diagnostics client-capabilities)))
+
+(defn resolve-code-action [{{:keys [uri line character]} :data :as action}]
+  (let [zloc (cursor-zloc uri line character)]
+    (f.code-actions/resolve-code-action action zloc)))
 
 (defn code-lens
   [doc-id]
diff --git a/src/clojure_lsp/interop.clj b/src/clojure_lsp/interop.clj
index e1768dd4..de3131d9 100644
--- a/src/clojure_lsp/interop.clj
+++ b/src/clojure_lsp/interop.clj
@@ -1,11 +1,14 @@
 (ns clojure-lsp.interop
   (:require
+    [clojure.data.json :as json]
+    [clojure.java.data :as j]
     [clojure.spec.alpha :as s]
     [clojure.string :as string]
     [clojure.tools.logging :as log]
     [clojure.walk :as walk]
     [medley.core :as medley])
   (:import
+    (com.google.gson JsonObject)
     (org.eclipse.lsp4j
       CallHierarchyIncomingCall
       CallHierarchyItem
@@ -33,12 +36,45 @@
       SymbolKind
       SymbolInformation
       TextDocumentEdit
+      TextDocumentIdentifier
       TextEdit
       VersionedTextDocumentIdentifier
+      WatchKind
       WorkspaceEdit)
     (org.eclipse.lsp4j.jsonrpc.messages Either)
     (java.net URLDecoder)))
 
+(defn ^:private enum->keyword [enum]
+  (-> enum (.name) .toLowerCase keyword))
+
+(defn learn-how-to-parse-enums [enums]
+  (-> #(defmethod j/from-java % [instance] (enum->keyword instance))
+      (map enums)))
+
+(defn document->decoded-uri [document]
+  (-> document
+      .getUri
+      URLDecoder/decode))
+
+(learn-how-to-parse-enums
+  [DiagnosticSeverity
+   MessageType
+   WatchKind
+   CompletionItemKind
+   SymbolKind])
+
+(defmethod j/from-java Either [instance]
+  (j/from-java (.get instance)))
+
+(defmethod j/from-java TextDocumentIdentifier [instance]
+  (document->decoded-uri instance))
+
+(defmethod j/from-java JsonObject [instance]
+  (-> instance
+      .toString
+      json/read-str
+      walk/keywordize-keys))
+
 (s/def ::line (s/and integer? (s/conformer int)))
 (s/def ::character (s/and integer? (s/conformer int)))
 (s/def ::position (s/and (s/keys :req-un [::line ::character])
@@ -55,8 +91,8 @@
    :folder 19 :enummember 20 :constant 21 :struct 22 :event 23 :operator 24 :typeparameter 25})
 
 (s/def :completion-item/kind (s/and keyword?
-                     completion-kind-enum
-                     (s/conformer (fn [v] (CompletionItemKind/forValue (get completion-kind-enum v))))))
+                                    completion-kind-enum
+                                    (s/conformer (fn [v] (CompletionItemKind/forValue (get completion-kind-enum v))))))
 (s/def ::new-text string?)
 (s/def ::text-edit (s/and (s/keys :req-un [::new-text ::range])
                           (s/conformer #(TextEdit. (:range %1) (:new-text %1)))))
@@ -208,28 +244,25 @@
 (s/def :code-action/title string?)
 
 (def code-action-kind
-  {:quick-fix CodeActionKind/QuickFix
-   :refactor CodeActionKind/Refactor
-   :refactor-extract CodeActionKind/RefactorExtract
-   :refactor-inline CodeActionKind/RefactorInline
-   :refactor-rewrite CodeActionKind/RefactorRewrite
-   :source CodeActionKind/Source
-   :source-organize-imports CodeActionKind/SourceOrganizeImports})
-
-(s/def :code-action/kind (s/and keyword?
-                                code-action-kind
-                                (s/conformer (fn [v] (get code-action-kind v)))))
+  [CodeActionKind/QuickFix
+   CodeActionKind/Refactor
+   CodeActionKind/RefactorExtract
+   CodeActionKind/RefactorInline
+   CodeActionKind/RefactorRewrite
+   CodeActionKind/Source
+   CodeActionKind/SourceOrganizeImports])
 
 (s/def :code-action/preferred? boolean?)
 
 (s/def ::code-action (s/and (s/keys :req-un [:code-action/title]
-                                    :opt-un [:code-action/kind ::diagnostics ::workspace-edit ::command :code-action/preferred?])
+                                    :opt-un [::kind ::diagnostics ::workspace-edit ::command :code-action/preferred? ::data])
                             (s/conformer #(doto (CodeAction. (:title %1))
                                             (.setKind (:kind %1))
                                             (.setDiagnostics (:diagnostics %1))
                                             (.setIsPreferred (:preferred? %1))
                                             (.setEdit (:workspace-edit %1))
-                                            (.setCommand (:command %1))))))
+                                            (.setCommand (:command %1))
+                                            (.setData (walk/stringify-keys (:data %1)))))))
 
 (s/def ::code-actions (s/coll-of ::code-action))
 
@@ -271,7 +304,7 @@
 
 (s/def ::call-hierarchy-incoming-calls (s/coll-of ::call-hierarchy-incoming-call))
 
-(defn debeaner [inst]
+(defn ^{:deprecated "use j/from-java instead"} debeaner [inst]
   (when inst
     (->> (dissoc (bean inst) :class)
          (into {})
@@ -424,8 +457,3 @@
         (update-in [:cljfmt :indents] clean-symbol-map)
         (update :document-formatting? (fnil identity true))
         (update :document-range-formatting? (fnil identity true)))))
-
-(defn document->decoded-uri [document]
-  (-> document
-      .getUri
-      URLDecoder/decode))
diff --git a/src/clojure_lsp/main.clj b/src/clojure_lsp/main.clj
index 52639008..f3245a81 100644
--- a/src/clojure_lsp/main.clj
+++ b/src/clojure_lsp/main.clj
@@ -1,70 +1,73 @@
 (ns clojure-lsp.main
   (:require
-   [clojure-lsp.db :as db]
-   [clojure-lsp.feature.refactor :as f.refactor]
-   [clojure-lsp.feature.semantic-tokens :as semantic-tokens]
-   [clojure-lsp.handlers :as handlers]
-   [clojure-lsp.interop :as interop]
-   [clojure-lsp.shared :as shared]
-   [clojure-lsp.window :as window]
-   [clojure.core.async :as async]
-   [clojure.tools.logging :as log]
-   [nrepl.server :as nrepl.server]
-   [trptcolin.versioneer.core :as version])
+    [clojure-lsp.db :as db]
+    [clojure-lsp.feature.refactor :as f.refactor]
+    [clojure-lsp.feature.semantic-tokens :as semantic-tokens]
+    [clojure-lsp.handlers :as handlers]
+    [clojure-lsp.interop :as interop]
+    [clojure-lsp.shared :as shared]
+    [clojure-lsp.window :as window]
+    [clojure.core.async :as async]
+    [clojure.tools.logging :as log]
+    [nrepl.server :as nrepl.server]
+    [trptcolin.versioneer.core :as version]
+    [clojure.java.data :as j])
   (:import
-   (clojure_lsp ClojureExtensions)
-   (org.eclipse.lsp4j.services LanguageServer TextDocumentService WorkspaceService LanguageClient)
-   (org.eclipse.lsp4j
-     ApplyWorkspaceEditParams
-     CallHierarchyIncomingCallsParams
-     CallHierarchyPrepareParams
-     CodeActionParams
-     CodeLens
-     CodeLensParams
-     CodeLensOptions
-     CompletionItem
-     CompletionOptions
-     CompletionParams
-     DefinitionParams
-     DidChangeConfigurationParams
-     DidChangeTextDocumentParams
-     DidChangeWatchedFilesParams
-     DidChangeWatchedFilesRegistrationOptions
-     DidCloseTextDocumentParams
-     DidOpenTextDocumentParams
-     DidSaveTextDocumentParams
-     DocumentFormattingParams
-     DocumentHighlightParams
-     DocumentRangeFormattingParams
-     DocumentSymbolParams
-     ExecuteCommandOptions
-     ExecuteCommandParams
-     FileSystemWatcher
-     HoverParams
-     InitializeParams
-     InitializeResult
-     InitializedParams
-     ParameterInformation
-     ReferenceParams
-     Registration
-     RegistrationParams
-     RenameParams
-     SaveOptions
-     SemanticTokensLegend
-     SemanticTokensParams
-     SemanticTokensRangeParams
-     SemanticTokensWithRegistrationOptions
-     ServerCapabilities
-     SignatureHelp
-     SignatureHelpParams
-     SignatureInformation
-     TextDocumentContentChangeEvent
-     TextDocumentSyncKind
-     TextDocumentSyncOptions
-     WorkspaceSymbolParams)
-   (org.eclipse.lsp4j.launch LSPLauncher)
-   (java.util.concurrent CompletableFuture)
-   (java.util.function Supplier))
+    (clojure_lsp ClojureExtensions)
+    (org.eclipse.lsp4j.services LanguageServer TextDocumentService WorkspaceService LanguageClient)
+    (org.eclipse.lsp4j
+      ApplyWorkspaceEditParams
+      CallHierarchyIncomingCallsParams
+      CallHierarchyPrepareParams
+      CodeActionParams
+      CodeAction
+      CodeActionOptions
+      CodeLens
+      CodeLensParams
+      CodeLensOptions
+      CompletionItem
+      CompletionOptions
+      CompletionParams
+      DefinitionParams
+      DidChangeConfigurationParams
+      DidChangeTextDocumentParams
+      DidChangeWatchedFilesParams
+      DidChangeWatchedFilesRegistrationOptions
+      DidCloseTextDocumentParams
+      DidOpenTextDocumentParams
+      DidSaveTextDocumentParams
+      DocumentFormattingParams
+      DocumentHighlightParams
+      DocumentRangeFormattingParams
+      DocumentSymbolParams
+      ExecuteCommandOptions
+      ExecuteCommandParams
+      FileSystemWatcher
+      HoverParams
+      InitializeParams
+      InitializeResult
+      InitializedParams
+      ParameterInformation
+      ReferenceParams
+      Registration
+      RegistrationParams
+      RenameParams
+      SaveOptions
+      SemanticTokensLegend
+      SemanticTokensParams
+      SemanticTokensRangeParams
+      SemanticTokensWithRegistrationOptions
+      ServerCapabilities
+      SignatureHelp
+      SignatureHelpParams
+      SignatureInformation
+      TextDocumentContentChangeEvent
+      TextDocumentSyncKind
+      TextDocumentSyncOptions
+      WorkspaceSymbolParams)
+    (org.eclipse.lsp4j.launch LSPLauncher)
+    (java.util.concurrent CompletableFuture)
+    (java.util.function Supplier))
   (:gen-class))
 
 (defonce formatting (atom false))
@@ -234,11 +237,11 @@
                                  start (.getStart range)
                                  end (.getEnd range)]
                              (interop/conform-or-log ::interop/edits (#'handlers/range-formatting
-                                                                       doc-id
-                                                                       {:row (inc (.getLine start))
-                                                                        :col (inc (.getCharacter start))
-                                                                        :end-row (inc (.getLine end))
-                                                                        :end-col (inc (.getCharacter end))})))
+                                                                      doc-id
+                                                                      {:row (inc (.getLine start))
+                                                                       :col (inc (.getCharacter start))
+                                                                       :end-row (inc (.getLine end))
+                                                                       :end-col (inc (.getCharacter end))})))
                            (catch Exception e
                              (log/error e))
                            (finally
@@ -247,40 +250,46 @@
               result)))))
 
   (^CompletableFuture codeAction [_ ^CodeActionParams params]
-   (go :codeAction
-       (CompletableFuture/supplyAsync
-         (reify Supplier
-           (get [_this]
-             (end
-               (try
-                 (let [doc-id          (interop/document->decoded-uri (.getTextDocument params))
-                       diagnostics     (.getDiagnostics (.getContext params))
-                       start           (.getStart (.getRange params))
-                       start-line      (.getLine start)
-                       start-character (.getCharacter start)]
-                   (interop/conform-or-log ::interop/code-actions (#'handlers/code-actions doc-id diagnostics start-line start-character)))
-                 (catch Exception e
-                   (log/error e)))))))))
+    (go :codeAction
+        (CompletableFuture/supplyAsync
+          (reify Supplier
+            (get [_this]
+              (end
+                (->> params
+                     j/from-java
+                     handlers/code-actions
+                     (interop/conform-or-log ::interop/code-actions))))))))
+
+  (^CompletableFuture resolveCodeAction [_ ^CodeAction unresolved]
+    (go :resolveCodeAction
+        (CompletableFuture/supplyAsync
+          (reify Supplier
+            (get [_this]
+              (end
+                (->> unresolved
+                     j/from-java
+                     handlers/resolve-code-action
+                     (interop/conform-or-log ::interop/code-action))))))))
 
   (^CompletableFuture codeLens [_ ^CodeLensParams params]
-   (go :codeLens
-       (CompletableFuture/supplyAsync
-         (reify Supplier
-           (get [_this]
-             (end
-               (let [doc-id (interop/document->decoded-uri (.getTextDocument params))]
-                 (interop/conform-or-log ::interop/code-lenses (#'handlers/code-lens doc-id)))))))))
+    (go :codeLens
+        (CompletableFuture/supplyAsync
+          (reify Supplier
+            (get [_this]
+              (end
+                (let [doc-id (interop/document->decoded-uri (.getTextDocument params))]
+                  (interop/conform-or-log ::interop/code-lenses (#'handlers/code-lens doc-id)))))))))
 
   (^CompletableFuture resolveCodeLens [_ ^CodeLens params]
-   (go :resolveCodeLens
-       (CompletableFuture/supplyAsync
-         (reify Supplier
-           (get [_this]
-             (end
-               (->> (.getData params)
-                    interop/json->clj
-                    (handlers/code-lens-resolve (-> params .getRange shared/range->clj))
-                    (interop/conform-or-log ::interop/code-lens))))))))
+    (go :resolveCodeLens
+        (CompletableFuture/supplyAsync
+          (reify Supplier
+            (get [_this]
+              (end
+                (->> (.getData params)
+                     interop/json->clj
+                     (handlers/code-lens-resolve (-> params .getRange shared/range->clj))
+                     (interop/conform-or-log ::interop/code-lens))))))))
 
   (^CompletableFuture definition [this ^DefinitionParams params]
     (go :definition
@@ -460,7 +469,8 @@
                   (InitializeResult. (doto (ServerCapabilities.)
                                        (.setHoverProvider true)
                                        (.setCallHierarchyProvider true)
-                                       (.setCodeActionProvider true)
+                                       (.setCodeActionProvider (doto (CodeActionOptions. interop/code-action-kind)
+                                                                 (.setResolveProvider true)))
                                        (.setCodeLensProvider (CodeLensOptions. true))
                                        (.setReferencesProvider true)
                                        (.setRenameProvider true)
diff --git a/src/clojure_lsp/refactor/transform.clj b/src/clojure_lsp/refactor/transform.clj
index f5beb31b..d9cee5bf 100644
--- a/src/clojure_lsp/refactor/transform.clj
+++ b/src/clojure_lsp/refactor/transform.clj
@@ -1,18 +1,19 @@
 (ns clojure-lsp.refactor.transform
   (:require
-   [clojure-lsp.clojure-core :as cc]
-   [clojure-lsp.crawler :as crawler]
-   [clojure-lsp.db :as db]
-   [clojure-lsp.feature.references :as f.references]
-   [clojure-lsp.parser :as parser]
-   [clojure-lsp.refactor.edit :as edit]
-   [clojure.set :as set]
-   [clojure.string :as string]
-   [medley.core :as medley]
-   [rewrite-clj.custom-zipper.core :as cz]
-   [rewrite-clj.node :as n]
-   [rewrite-clj.zip :as z]
-   [rewrite-clj.zip.subedit :as zsub]))
+    [clojure-lsp.crawler :as crawler]
+    [clojure-lsp.db :as db]
+    [clojure-lsp.feature.references :as f.references]
+    [clojure-lsp.parser :as parser]
+    [clojure-lsp.refactor.edit :as edit]
+    [clojure.set :as set]
+    [clojure.string :as string]
+    [medley.core :as medley]
+    [rewrite-clj.custom-zipper.core :as cz]
+    [rewrite-clj.node :as n]
+    [rewrite-clj.zip :as z]
+    [rewrite-clj.zip.subedit :as zsub]
+    [clojure.tools.logging :as log]
+    [clojure-lsp.clojure-core :as cc]))
 
 (defn result [zip-edits]
   (mapv (fn [zip-edit]
@@ -194,8 +195,8 @@
                              (cz/insert-left (n/spaces col)))
                          (-> bindings-loc
                              (cond->
-                               first-bind (cz/append-child (n/newlines 1))
-                               first-bind (cz/append-child (n/spaces col))) ; insert let and binding backwards
+                              first-bind (cz/append-child (n/newlines 1))
+                              first-bind (cz/append-child (n/spaces col))) ; insert let and binding backwards
                              (z/append-child binding-sym) ; add binding symbol
                              (z/append-child bound-node)
                              (z/down)
@@ -423,64 +424,15 @@
         [{:range (meta (z/node result-loc))
           :loc result-loc}]))))
 
-(defn ^:private add-form-to-namespace [zloc form-to-add form-type form-to-check-exists]
-  (let [ns-loc (edit/find-namespace zloc)
-        ns-zip (zsub/subzip ns-loc)
-        cursor-sym (z/sexpr zloc)
-        need-to-add? (and (not (z/find-value ns-zip z/next cursor-sym))
-                          (not (z/find-value ns-zip z/next form-to-add))
-                          (not (z/find-value ns-zip z/next form-to-check-exists)))]
-    (when (and form-to-add need-to-add?)
-      (let [add-form-type? (not (z/find-value ns-zip z/next form-type))
-            form-type-loc (z/find-value (zsub/subzip ns-loc) z/next form-type)
-            keep-require-at-start? (get-in @db/db [:settings :keep-require-at-start?])
-            col (if form-type-loc
-                  (:col (meta (z/node (z/rightmost form-type-loc))))
-                  (if keep-require-at-start?
-                    2
-                    5))
-            result-loc (z/subedit-> ns-zip
-                                    (cond->
-                                        add-form-type? (z/append-child (n/newlines 1))
-                                        add-form-type? (z/append-child (n/spaces 2))
-                                        add-form-type? (z/append-child (list form-type)))
-                                    (z/find-value z/next form-type)
-                                    (z/up)
-                                    (cond->
-                                        (or (not add-form-type?)
-                                            (not keep-require-at-start?)) (cz/append-child (n/newlines 1)))
-                                    (cz/append-child (n/spaces (dec col)))
-                                    (z/append-child form-to-add))]
-        [{:range (meta (z/node result-loc))
-          :loc result-loc}]))))
-
-(defn add-import-to-namespace [zloc import-name]
-  (add-form-to-namespace zloc (symbol import-name) :import import-name))
-
-(defn add-common-import-to-namespace [zloc]
-  (let [class-with-dot (z/sexpr zloc)
-        class-name (->> class-with-dot str drop-last (string/join "") symbol)]
-    (when-let [import-name (or (get cc/java-util-imports class-name)
-                               (get cc/java-util-imports class-with-dot))]
-      (let [result (add-form-to-namespace zloc (symbol import-name) :import import-name)]
-        {:result result
-         :code-action-data {:import-name import-name}}))))
-
-(defn add-known-libspec
-  [zloc ns-to-add qualified-ns-to-add]
-  (when (and qualified-ns-to-add ns-to-add)
-    (add-form-to-namespace zloc [qualified-ns-to-add :as ns-to-add] :require ns-to-add)))
-
-(def common-alias->info
+(def ^:private common-alias->info
   {:string {:alias-str "string" :label "clojure.string" :detail "clojure.string" :alias-ns 'clojure.string}
    :set    {:alias-str "set" :label "clojure.set" :detail "clojure.set" :alias-ns 'clojure.set}
    :walk   {:alias-str "walk" :label "clojure.walk" :detail "clojure.walk" :alias-ns 'clojure.walk}
    :pprint {:alias-str "pprint" :label "clojure.pprint" :detail "clojure.pprint" :alias-ns 'clojure.pprint}
    :async  {:alias-str "async" :label "clojure.core.async" :detail "clojure.core.async" :alias-ns 'clojure.core.async}})
 
-(defn ^:private add-missing-alias-ns [zloc source]
-  (let [ns-str-to-add (some-> zloc z/sexpr namespace)
-        ns-to-add (some-> ns-str-to-add symbol)
+(defn ^:private find-missing-alias-require [zloc]
+  (let [require-alias (some-> zloc z/sexpr namespace)
         alias->info (->> (:file-envs @db/db)
                          (mapcat val)
                          (filter (fn [usage]
@@ -498,17 +450,12 @@
                                               sym)}))
                          (distinct)
                          (group-by :alias-str))
-        posibilities (or (get alias->info ns-str-to-add)
-                         [(get common-alias->info (keyword ns-str-to-add))])
-        qualified-ns-to-add (when (= 1 (count posibilities))
-                              (-> posibilities first :alias-ns))
-        result (add-known-libspec zloc ns-to-add qualified-ns-to-add)]
-    (if (= source :code-action)
-      {:result result
-       :code-action-data {:ns-name qualified-ns-to-add}}
-      result)))
+        posibilities (or (get alias->info require-alias)
+                         [(get common-alias->info (keyword require-alias))])]
+    (when (= 1 (count posibilities))
+      (-> posibilities first :alias-ns))))
 
-(def common-refers->info
+(def ^:private common-refers->info
   {'deftest      'clojure.test
    'testing      'clojure.test
    'is           'clojure.test
@@ -528,50 +475,111 @@
    'fact         'midje.sweet
    'facts        'midje.sweet})
 
-(defn ^:private add-missing-refer [zloc source]
-  (when-let [qualified-ns-to-add (get common-refers->info (z/sexpr zloc))]
+(defn ^:private find-missing-refer-require [zloc]
+  (let [refer-to-add (-> zloc z/sexpr symbol)
+        ns-loc (edit/find-namespace zloc)
+        ns-zip (zsub/subzip ns-loc)]
+    (when (not (z/find-value ns-zip z/next refer-to-add))
+      (get common-refers->info (z/sexpr zloc)))))
+
+(defn find-missing-require [zloc]
+  (let [ns-str (some-> zloc z/sexpr namespace)]
+    (if ns-str
+      (find-missing-alias-require zloc)
+      (find-missing-refer-require zloc))))
+
+(defn find-missing-import [zloc]
+  (let [class-with-dot (z/sexpr zloc)
+        class-name (->> class-with-dot str drop-last (string/join "") symbol)]
+    (or (get cc/java-util-imports class-name)
+        (get cc/java-util-imports class-with-dot))))
+
+(defn ^:private add-form-to-namespace [zloc form-to-add form-type form-to-check-exists]
+  (let [ns-loc (edit/find-namespace zloc)
+        ns-zip (zsub/subzip ns-loc)
+        cursor-sym (z/sexpr zloc)
+        need-to-add? (and (not (z/find-value ns-zip z/next cursor-sym))
+                          (not (z/find-value ns-zip z/next form-to-add))
+                          (not (z/find-value ns-zip z/next form-to-check-exists)))]
+    (when (and form-to-add need-to-add?)
+      (let [add-form-type? (not (z/find-value ns-zip z/next form-type))
+            form-type-loc (z/find-value (zsub/subzip ns-loc) z/next form-type)
+            keep-require-at-start? (get-in @db/db [:settings :keep-require-at-start?])
+            col (if form-type-loc
+                  (:col (meta (z/node (z/rightmost form-type-loc))))
+                  (if keep-require-at-start?
+                    2
+                    5))
+            result-loc (z/subedit-> ns-zip
+                                    (cond->
+                                     add-form-type? (z/append-child (n/newlines 1))
+                                     add-form-type? (z/append-child (n/spaces 2))
+                                     add-form-type? (z/append-child (list form-type)))
+                                    (z/find-value z/next form-type)
+                                    (z/up)
+                                    (cond->
+                                     (or (not add-form-type?)
+                                         (not keep-require-at-start?)) (cz/append-child (n/newlines 1)))
+                                    (cz/append-child (n/spaces (dec col)))
+                                    (z/append-child form-to-add))]
+        [{:range (meta (z/node result-loc))
+          :loc result-loc}]))))
+
+(defn add-import-to-namespace [zloc import-name]
+  (add-form-to-namespace zloc (symbol import-name) :import import-name))
+
+(defn add-common-import-to-namespace [zloc]
+  (when-let [import-name (find-missing-import zloc)]
+    (add-form-to-namespace zloc (symbol import-name) :import import-name)))
+
+(defn add-known-libspec
+  [zloc ns-to-add qualified-ns-to-add]
+  (when (and qualified-ns-to-add ns-to-add)
+    (add-form-to-namespace zloc [qualified-ns-to-add :as ns-to-add] :require ns-to-add)))
+
+(defn ^:private add-missing-alias-ns [zloc]
+  (let [require-alias (some-> zloc z/sexpr namespace symbol)
+        qualified-ns-to-add (find-missing-alias-require zloc)]
+    (add-known-libspec zloc require-alias qualified-ns-to-add)))
+
+(defn ^:private add-missing-refer [zloc]
+  (when-let [qualified-ns-to-add (find-missing-refer-require zloc)]
     (let [refer-to-add (-> zloc z/sexpr symbol)
           ns-loc (edit/find-namespace zloc)
           ns-zip (zsub/subzip ns-loc)
-          need-to-add? (not (z/find-value ns-zip z/next refer-to-add))]
-      (when need-to-add?
-        (let [existing-ns-require (z/find-value ns-zip z/next qualified-ns-to-add)
-              add-require? (and (not existing-ns-require)
-                                (not (z/find-value ns-zip z/next :require)))
-              require-loc (z/find-value (zsub/subzip ns-loc) z/next :require)
-              col (if require-loc
-                    (-> require-loc z/rightmost z/node meta :col)
-                    5)
-              result-loc (if existing-ns-require
-                           (z/subedit-> ns-loc
-                                        (z/find-value z/next qualified-ns-to-add)
-                                        (z/find-value z/next ':refer)
-                                        z/right
-                                        (cz/append-child (n/spaces 1))
-                                        (z/append-child (z/sexpr zloc)))
-                           (z/subedit-> ns-loc
-                                        (cond->
-                                            add-require? (z/append-child (n/newlines 1))
-                                            add-require? (z/append-child (n/spaces 2))
-                                            add-require? (z/append-child (list :require)))
-                                        (z/find-value z/next :require)
-                                        (z/up)
-                                        (cz/append-child (n/newlines 1))
-                                        (cz/append-child (n/spaces (dec col)))
-                                        (z/append-child [qualified-ns-to-add :refer [refer-to-add]])))
-              result [{:range (meta (z/node result-loc))
-                       :loc result-loc}]]
-          (if (= source :code-action)
-            {:result result
-             :code-action-data {:ns-name qualified-ns-to-add}}
-            result))))))
+          existing-ns-require (z/find-value ns-zip z/next qualified-ns-to-add)
+          add-require? (and (not existing-ns-require)
+                            (not (z/find-value ns-zip z/next :require)))
+          require-loc (z/find-value (zsub/subzip ns-loc) z/next :require)
+          col (if require-loc
+                (-> require-loc z/rightmost z/node meta :col)
+                5)
+          result-loc (if existing-ns-require
+                       (z/subedit-> ns-loc
+                                    (z/find-value z/next qualified-ns-to-add)
+                                    (z/find-value z/next ':refer)
+                                    z/right
+                                    (cz/append-child (n/spaces 1))
+                                    (z/append-child (z/sexpr zloc)))
+                       (z/subedit-> ns-loc
+                                    (cond->
+                                     add-require? (z/append-child (n/newlines 1))
+                                     add-require? (z/append-child (n/spaces 2))
+                                     add-require? (z/append-child (list :require)))
+                                    (z/find-value z/next :require)
+                                    (z/up)
+                                    (cz/append-child (n/newlines 1))
+                                    (cz/append-child (n/spaces (dec col)))
+                                    (z/append-child [qualified-ns-to-add :refer [refer-to-add]])))]
+      [{:range (meta (z/node result-loc))
+        :loc result-loc}])))
 
 (defn add-missing-libspec
-  [zloc {:keys [source]}]
+  [zloc]
   (let [ns-str (some-> zloc z/sexpr namespace)]
     (if ns-str
-      (add-missing-alias-ns zloc source)
-      (add-missing-refer zloc source))))
+      (add-missing-alias-ns zloc)
+      (add-missing-refer zloc))))
 
 (defn extract-function
   [zloc fn-name usages]
@@ -584,9 +592,9 @@
         fn-sym (symbol fn-name)
         {:keys [declared scoped]} (->> usages
                                        (group-by #(condp set/subset? (:tags %)
-                                               #{:declare} :declared
-                                               #{:scoped} :scoped
-                                               nil))
+                                                    #{:declare} :declared
+                                                    #{:scoped} :scoped
+                                                    nil))
                                        (medley/map-vals #(set (map :sym %))))
         used-syms (mapv (comp symbol name) (set/difference scoped declared))
         expr-edit (-> (z/of-string "")


Test patch:
diff --git a/test/clojure_lsp/features/code_actions_test.clj b/test/clojure_lsp/features/code_actions_test.clj
new file mode 100644
index 00000000..7b0cb531
--- /dev/null
+++ b/test/clojure_lsp/features/code_actions_test.clj
@@ -0,0 +1,157 @@
+(ns clojure-lsp.features.code-actions-test
+  (:require
+    [clojure-lsp.db :as db]
+    [clojure-lsp.feature.code-actions :as f.code-actions]
+    [clojure-lsp.parser :as parser]
+    [clojure.string :as string]
+    [clojure.test :refer [deftest testing is]]))
+
+(defn zloc-at [file row col]
+  (-> @db/db
+      (get-in [:documents file])
+      :text
+      (parser/loc-at-pos row col)))
+
+(deftest test-code-actions-without-resolve-support
+  (let [references-code   (str "(ns some-ns)\n"
+                               "(def foo)")
+        b-code   (str "(ns other-ns (:require [some-ns :as sns]))\n"
+                      "(def bar 1)\n"
+                      "(defn baz []\n"
+                      "  bar)")
+        c-code   (str "(ns another-ns)\n"
+                      "(def bar ons/bar)\n"
+                      "(def foo sns/foo)\n"
+                      "(deftest some-test)\n"
+                      "MyClass.\n"
+                      "Date.")
+        db-state {:documents {"file://a.clj" {:text references-code}
+                              "file://b.clj" {:text b-code}
+                              "file://c.clj" {:text c-code}}
+                  :file-envs {"file://a.clj" (parser/find-usages references-code :clj {})
+                              "file://b.clj" (parser/find-usages b-code :clj {})
+                              "file://c.clj" (parser/find-usages c-code :clj {})}}]
+    (reset! db/db db-state)
+    (testing "Add missing namespace"
+      (testing "when it has not unresolved-namespace diagnostic"
+        (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                      (f.code-actions/all (zloc-at "file://c.clj" 2 10)
+                                          "file://c.clj"
+                                          2
+                                          10
+                                          [] {}))))
+
+      (testing "when it has unresolved-namespace but cannot find namespace"
+        (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                      (f.code-actions/all (zloc-at "file://c.clj" 2 11)
+                                          "file://c.clj"
+                                          2
+                                          11
+                                          [{:code "unresolved-namespace"}] {}))))
+
+      (testing "when it has unresolved-namespace and can find namespace"
+        (is (some #(= (:title %) "Add missing 'some-ns' require")
+                  (f.code-actions/all (zloc-at "file://c.clj" 3 11)
+                                      "file://c.clj"
+                                      3
+                                      11
+                                      [{:code "unresolved-namespace"}] {}))))
+      (testing "when it has unresolved-symbol and it's a known refer"
+        (is (some #(= (:title %) "Add missing 'clojure.test' require")
+                  (f.code-actions/all (zloc-at "file://c.clj" 4 2)
+                                      "file://c.clj"
+                                      4
+                                      2
+                                      [{:code "unresolved-namespace"}] {}))))
+      (testing "when it has unresolved-symbol but it's not a known refer"
+        (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                      (f.code-actions/all (zloc-at "file://c.clj" 4 11)
+                                          "file://c.clj"
+                                          4
+                                          11
+                                          [{:code "unresolved-symbol"}] {})))))
+    (testing "Add common missing import"
+      (testing "when it has no unknown-symbol diagnostic"
+        (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                      (f.code-actions/all (zloc-at "file://c.clj" 5 2)
+                                          "file://c.clj"
+                                          5
+                                          2
+                                          [] {}))))
+
+      (testing "when it has unknown-symbol but it's not a common import"
+        (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                      (f.code-actions/all (zloc-at "file://c.clj" 5 2)
+                                          "file://c.clj"
+                                          5
+                                          2
+                                          [{:code "unresolved-symbol"}] {}))))
+      (testing "when it has unknown-symbol but it's not a common import"
+        (is (some #(= (:title %) "Add missing 'java.util.Date' import")
+                  (f.code-actions/all (zloc-at "file://c.clj" 6 2)
+                                      "file://c.clj"
+                                      6
+                                      2
+                                      [{:code "unresolved-symbol"}] {})))))
+    (testing "Inline symbol"
+      (testing "when in not a let/def symbol"
+        (is (not-any? #(= (:title %) "Inline symbol")
+                      (f.code-actions/all (zloc-at "file://b.clj" 4 8)
+                                          "file://b.clj"
+                                          4
+                                          8
+                                          [] {}))))
+      (testing "when in let/def symbol"
+        (is (some #(= (:title %) "Inline symbol")
+                  (f.code-actions/all (zloc-at "file://b.clj" 4 5)
+                                      "file://b.clj"
+                                      4
+                                      5
+                                      [] {})))))
+    (testing "Cycle privacy"
+      (testing "when non function location"
+        (is (not-any? #(= (:title %) "Cycle privacy")
+                      (f.code-actions/all (zloc-at "file://a.clj" 1 5)
+                                          "file://a.clj"
+                                          1
+                                          5
+                                          [] {}))))
+      (testing "when on function location"
+        (is (some #(= (:title %) "Cycle privacy")
+                  (f.code-actions/all (zloc-at "file://a.clj" 2 5)
+                                      "file://a.clj"
+                                      2
+                                      5
+                                      [] {})))))
+    (testing "Extract function"
+      (testing "when non function location"
+        (is (not-any? #(= (:title %) "Extract function")
+                      (f.code-actions/all (zloc-at "file://a.clj" 1 5)
+                                          "file://a.clj"
+                                          1
+                                          5
+                                          [] {}))))
+      (testing "when on function location"
+        (is (some #(= (:title %) "Extract function")
+                  (f.code-actions/all (zloc-at "file://a.clj" 2 5)
+                                      "file://a.clj"
+                                      2
+                                      5
+                                      [] {})))))
+    (testing "clean namespace"
+      (testing "without workspace edit client capability"
+        (is (not-any? #(= (:title %) "Clean namespace")
+                      (f.code-actions/all (zloc-at "file://b.clj" 2 2)
+                                          "file://b.clj"
+                                          2
+                                          2
+                                          [] {}))))
+
+      (testing "with workspace edit client capability"
+        (reset! db/db (assoc-in db-state [:client-capabilities :workspace :workspace-edit] true))
+        (is (some #(= (:title %) "Clean namespace")
+                  (f.code-actions/all (zloc-at "file://b.clj" 2 2)
+                                      "file://b.clj"
+                                      2
+                                      2
+                                      [] {:workspace {:workspace-edit true}})))))))
diff --git a/test/clojure_lsp/handlers_test.clj b/test/clojure_lsp/handlers_test.clj
index b932f700..c4ed8fa9 100644
--- a/test/clojure_lsp/handlers_test.clj
+++ b/test/clojure_lsp/handlers_test.clj
@@ -423,9 +423,9 @@
            :new-text "(a)"}]
          (handlers/range-formatting "file://a.clj" {:row 1 :col 1 :end-row 1 :end-col 4}))))
 
-(deftest test-code-actions
+(deftest test-code-actions-handle
   (let [references-code   (str "(ns some-ns)\n"
-                      "(def foo)")
+                               "(def foo)")
         b-code   (str "(ns other-ns (:require [some-ns :as sns]))\n"
                       "(def bar 1)\n"
                       "(defn baz []\n"
@@ -442,86 +442,28 @@
                   :file-envs {"file://a.clj" (parser/find-usages references-code :clj {})
                               "file://b.clj" (parser/find-usages b-code :clj {})
                               "file://c.clj" (parser/find-usages c-code :clj {})}}]
-    (testing "Add missing namespace"
-      (testing "when it has not unresolved-namespace diagnostic"
-        (reset! db/db db-state)
-        (is (not-any? #(string/starts-with? (:title %) "Add missing")
-                      (handlers/code-actions "file://c.clj" [] 1 9))))
-
-      (testing "when it has unresolved-namespace but cannot find namespace"
-        (reset! db/db db-state)
-        (let [unknown-ns-diagnostic (Diagnostic. (Range. (Position. 1 10) (Position. 1 16)) "Unknown namespace" DiagnosticSeverity/Error "some source" "unresolved-namespace")]
-          (is (not-any? #(string/starts-with? (:title %) "Add missing")
-                        (handlers/code-actions "file://c.clj" [unknown-ns-diagnostic] 1 10)))))
-
-      (testing "when it has unresolved-namespace and can find namespace"
-        (reset! db/db db-state)
-        (let [unknown-ns-diagnostic (Diagnostic. (Range. (Position. 2 10) (Position. 2 16)) "Unknown namespace" DiagnosticSeverity/Error "some source" "unresolved-namespace")]
-          (is (some #(= (:title %) "Add missing 'some-ns' require")
-                    (handlers/code-actions "file://c.clj" [unknown-ns-diagnostic] 2 10)))))
-      (testing "when it has unresolved-symbol and it's a known refer"
-        (reset! db/db db-state)
-        (let [unknown-symbol-diagnostic (Diagnostic. (Range. (Position. 3 1) (Position. 3 8)) "Unresolved symbol deftest" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
-          (is (some #(= (:title %) "Add missing 'clojure.test' require")
-                    (handlers/code-actions "file://c.clj" [unknown-symbol-diagnostic] 3 1)))))
-      (testing "when it has unresolved-symbol but it's not a known refer"
-        (reset! db/db db-state)
-        (let [unknown-symbol-diagnostic (Diagnostic. (Range. (Position. 3 10) (Position. 3 18)) "Unresolved symbol some-test" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
-          (is (not-any? #(string/starts-with? (:title %) "Add missing")
-                        (handlers/code-actions "file://c.clj" [unknown-symbol-diagnostic] 3 10))))))
-    (testing "Add common missing import"
-      (testing "when it has no unknown-symbol diagnostic"
-        (reset! db/db db-state)
-        (is (not-any? #(string/starts-with? (:title %) "Add missing")
-                      (handlers/code-actions "file://c.clj" [] 4 1))))
-
-      (testing "when it has unknown-symbol but it's not a common import"
-        (reset! db/db db-state)
-        (let [unresolved-symbol-diagnostic (Diagnostic. (Range. (Position. 4 1) (Position. 4 5)) "Unresolved symbol" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
-          (is (not-any? #(string/starts-with? (:title %) "Add missing")
-                        (handlers/code-actions "file://c.clj" [unresolved-symbol-diagnostic] 4 1)))))
-      (testing "when it has unknown-symbol but it's not a common import"
-        (reset! db/db db-state)
-        (let [unresolved-symbol-diagnostic (Diagnostic. (Range. (Position. 5 1) (Position. 5 5)) "Unresolved symbol" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
-          (is (some #(= (:title %) "Add missing 'java.util.Date' import")
-                    (handlers/code-actions "file://c.clj" [unresolved-symbol-diagnostic] 5 1))))))
-    (testing "Inline symbol"
-      (testing "when in not a let/def symbol"
-        (reset! db/db db-state)
-        (is (not-any? #(= (:title %) "Inline symbol")
-                      (handlers/code-actions "file://b.clj" [] 3 7))))
-      (testing "when in let/def symbol"
-        (reset! db/db db-state)
-        (is (some #(= (:title %) "Inline symbol")
-                  (handlers/code-actions "file://b.clj" [] 3 4)))))
-    (testing "Cycle privacy"
-      (testing "when non function location"
-        (reset! db/db db-state)
-        (is (not-any? #(= (:title %) "Cycle privacy")
-                      (handlers/code-actions "file://a.clj" [] 0 4))))
-      (testing "when on function location"
-        (reset! db/db db-state)
-        (is (some #(= (:title %) "Cycle privacy")
-                  (handlers/code-actions "file://a.clj" [] 1 4)))))
-    (testing "Extract function"
-      (testing "when non function location"
-        (reset! db/db db-state)
-        (is (not-any? #(= (:title %) "Extract function")
-                      (handlers/code-actions "file://a.clj" [] 0 4))))
-      (testing "when on function location"
-        (reset! db/db db-state)
-        (is (some #(= (:title %) "Extract function")
-                  (handlers/code-actions "file://a.clj" [] 1 4)))))
-    (testing "clean namespace"
-      (testing "without workspace edit client capability"
-        (reset! db/db db-state)
-        (is (not-any? #(= (:title %) "Clean namespace")
-                      (handlers/code-actions "file://b.clj" [] 1 1))))
+    (testing "when it has unresolved-namespace and can find namespace"
+      (reset! db/db db-state)
+      (is (some #(= (:title %) "Add missing 'some-ns' require")
+                (handlers/code-actions
+                  {:textDocument "file://c.clj"
+                   :context {:diagnostics [{:code "unresolved-namespace"}]}
+                   :range {:start {:line 2 :character 10}}}))))
+    (testing "without workspace edit client capability"
+      (reset! db/db db-state)
+      (is (not-any? #(= (:title %) "Clean namespace")
+                    (handlers/code-actions
+                      {:textDocument "file://b.clj"
+                       :context {:diagnostics []}
+                       :range {:start {:line 1 :character 1}}}))))
 
-      (testing "with workspace edit client capability"
-        (reset! db/db (assoc-in db-state [:client-capabilities :workspace :workspace-edit] true))
-        (is (some #(= (:title %) "Clean namespace")
-    (handlers/code-actions "file://b.clj" [] 1 1)))))))
+    (testing "with workspace edit client capability"
+      (reset! db/db (assoc-in db-state [:client-capabilities :workspace :workspace-edit] true))
+      (is (some #(= (:title %) "Clean namespace")
+                (handlers/code-actions
+                  {:textDocument "file://b.clj"
+                   :context {:diagnostics []}
+                   :range {:start {:line 1 :character 1}}}))))))
 
 (deftest test-code-lens
   (let [references-code (str "(ns some-ns)\n"
diff --git a/test/clojure_lsp/refactor/transform_test.clj b/test/clojure_lsp/refactor/transform_test.clj
index 135b13cd..c3f80dde 100644
--- a/test/clojure_lsp/refactor/transform_test.clj
+++ b/test/clojure_lsp/refactor/transform_test.clj
@@ -79,10 +79,8 @@
   (testing "when we known the import"
     (reset! db/db {:file-envs {}})
     (let [zloc (-> (z/of-string "(ns foo.bar) Date.") (z/find-value z/next 'Date.))
-          {:keys [result code-action-data]} (transform/add-common-import-to-namespace zloc)
-          [{:keys [loc range]}] result]
+          [{:keys [loc range]}] (transform/add-common-import-to-namespace zloc)]
       (is (some? range))
-      (is (= 'java.util.Date (:import-name code-action-data)))
       (is (= (code "(ns foo.bar "
                    "  (:import"
                    "    java.util.Date))") (z/root-string loc)))))
@@ -493,13 +491,13 @@
       (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)))))
     (testing "common ns aliases"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo) set/subset?") 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 [clojure.set :as set])) (z/sexpr loc)))))
     (testing "with keep-require-at-start?"
@@ -507,7 +505,7 @@
         (reset! db/db {:file-envs {}
                        :settings {:keep-require-at-start? true}})
         (let [zloc (-> (z/of-string "(ns foo) set/subset?") z/rightmost)
-              [{:keys [loc range]}] (transform/add-missing-libspec zloc nil)]
+              [{:keys [loc range]}] (transform/add-missing-libspec zloc)]
           (is (some? range))
           (is (= (code "(ns foo "
                        "  (:require [clojure.set :as set]))") (z/string loc)))))
@@ -515,7 +513,7 @@
         (reset! db/db {:file-envs {}
                        :settings {:keep-require-at-start? true}})
         (let [zloc (-> (z/of-string "(ns foo \n  (:require [foo :as bar])) set/subset?") z/rightmost)
-              [{:keys [loc range]}] (transform/add-missing-libspec zloc nil)]
+              [{:keys [loc range]}] (transform/add-missing-libspec zloc)]
           (is (some? range))
           (is (= (code "(ns foo "
                        "  (:require [foo :as bar]"
@@ -524,47 +522,43 @@
     (testing "when require doesn't exists"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo) deftest") 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 [clojure.test :refer [deftest]])) (z/sexpr loc)))))
     (testing "when already exists another require"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo (:require [clojure.set :refer [subset?]])) deftest") 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 [clojure.set :refer [subset?]]
                           [clojure.test :refer [deftest]])) (z/sexpr loc)))))
     (testing "when already exists that ns with another refer"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo (:require [clojure.test :refer [deftest]])) testing") 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 [clojure.test :refer [deftest testing]])) (z/sexpr loc)))))
     (testing "we don't add existing refers"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo (:require [clojure.test :refer [testing]])) testing") z/rightmost)]
-        (is (= nil (transform/add-missing-libspec zloc nil)))))
+        (is (= nil (transform/add-missing-libspec zloc)))))
     (testing "we can add multiple refers"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo (:require [clojure.test :refer [deftest testing]])) is") 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 [clojure.test :refer [deftest testing is]])) (z/sexpr loc))))))
   (testing "from code-action source"
     (testing "aliases"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo) set/subset?") z/rightmost)
-            response (transform/add-missing-libspec zloc {:source :code-action})
-            [{:keys [loc range]}] (:result response)]
-        (is (= 'clojure.set (-> response :code-action-data :ns-name)))
+            [{:keys [loc range]}] (transform/add-missing-libspec zloc)]
         (is (some? range))
         (is (= '(ns foo (:require [clojure.set :as set])) (z/sexpr loc)))))
     (testing "refers"
       (reset! db/db {:file-envs {}})
       (let [zloc (-> (z/of-string "(ns foo) deftest") z/rightmost)
-            response (transform/add-missing-libspec zloc {:source :code-action})
-            [{:keys [loc range]}] (:result response)]
-        (is (= 'clojure.test (-> response :code-action-data :ns-name)))
+            [{:keys [loc range]}] (transform/add-missing-libspec zloc)]
         (is (some? range))
         (is (= '(ns foo (:require [clojure.test :refer [deftest]])) (z/sexpr loc)))))))