Repository: clojure-lsp/clojure-lsp
Pull request #202 (2020-12-08 21:21:03)

Support text-completion within a `:refer`
Initial shot at implementing support for text-completion for `:refer` blocks

For example:
![image](https://user-images.githubusercontent.com/15933322/101542744-56bf1200-39f7-11eb-9814-5f9dcdb4bab3.png)

I'm not convinced that my implementation is the cleanest it can be, so feel free to suggest edits and whatnot


Patch:
diff --git a/src/clojure_lsp/handlers.clj b/src/clojure_lsp/handlers.clj
index 6a3799e4..8064acb1 100644
--- a/src/clojure_lsp/handlers.clj
+++ b/src/clojure_lsp/handlers.clj
@@ -1,24 +1,24 @@
 (ns clojure-lsp.handlers
   (: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.interop :as interop]
-   [clojure-lsp.parser :as parser]
-   [clojure-lsp.refactor.transform :as refactor]
-   [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])
+    [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.interop :as interop]
+    [clojure-lsp.parser :as parser]
+    [clojure-lsp.refactor.transform :as refactor]
+    [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])
   (:import
-   [java.net URL JarURLConnection]))
+    [java.net URL JarURLConnection]))
 
 (defn check-bounds [line column {:keys [row end-row col end-col] :as _usage}]
   (cond
@@ -219,63 +219,75 @@
                              [(:sym usage) usages])
                            (into {}))]
     (when cursor-value
-      (concat
-        (->> local-env
-             (filter (comp :declare :tags))
+      (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))
-             (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
+             (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-> (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
+                                                (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 "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)))))))
+                               :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-completion-item [label sym-wanted]
   (let [file-envs (:file-envs @db/db)


Test patch:
diff --git a/test/clojure_lsp/handlers_test.clj b/test/clojure_lsp/handlers_test.clj
index fbf3a559..66cf8a0a 100644
--- a/test/clojure_lsp/handlers_test.clj
+++ b/test/clojure_lsp/handlers_test.clj
@@ -351,6 +351,10 @@
                    "file://d.clj" (parser/find-usages
                                     (str "(ns d (:require [alpaca.ns :as alpaca]))")
                                     :clj
+                                    {})
+                   "file://e.clj" (parser/find-usages
+                                    (str "(ns e (:require [alpaca.ns :refer [ba]]))")
+                                    :clj
                                     {})}}]
     (testing "complete-a"
       (reset! db/db db-state)
@@ -375,6 +379,11 @@
               {:label "alpaca/barr" :detail "alpaca.ns" :data "alpaca.ns/barr"}
               {:label "alpaca/bazz" :detail "alpaca.ns" :data "alpaca.ns/bazz"}]
              (handlers/completion "file://b.clj" 3 18))))
+    (testing "complete-within-refering"
+      (reset! db/db db-state)
+      (is (= [{:label "barr" :detail "alpaca.ns/barr" :data "alpaca.ns/barr"}
+              {:label "bazz" :detail "alpaca.ns/bazz" :data "alpaca.ns/bazz"}]
+             (handlers/completion "file://e.clj" 1 38))))
     (testing "complete-core-stuff"
       (reset! db/db (update-in db-state [:file-envs "file://b.clj" 4] merge {:sym 'freq :str "freq"}))
       (is (= [{:label "frequencies" :data "clojure.core/frequencies"}]