From dd208d3efa38bb0273d41779377c27849d6655d9 Mon Sep 17 00:00:00 2001 From: Tianxiang Xiong Date: Fri, 16 Jun 2017 01:09:45 -0700 Subject: [PATCH] Include special forms in apropos Fixes #409. Some special forms (e.g. `if`) are included in apropos if the `search-ns` is nil or `clojure.core`. Included special forms are those those with documentation in `clojure.repl`, as well as `&`, `catch`, and `finally`, the first of which uses the documentation of `fn` and the others that of `try`. --- src/cider/nrepl/middleware/apropos.clj | 32 ++++++++++++----- src/cider/nrepl/middleware/info.clj | 14 ++++---- .../cider/nrepl/middleware/apropos_test.clj | 34 ++++++++++++++++++- test/clj/cider/nrepl/middleware/info_test.clj | 18 +++++++++- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/cider/nrepl/middleware/apropos.clj b/src/cider/nrepl/middleware/apropos.clj index 25969a8de..3f117932b 100644 --- a/src/cider/nrepl/middleware/apropos.clj +++ b/src/cider/nrepl/middleware/apropos.clj @@ -4,6 +4,7 @@ (:require [cider.nrepl.middleware.util.error-handling :refer [with-safe-transport]] [clojure.string :as str] [clojure.tools.nrepl.middleware :refer [set-descriptor!]] + [cider.nrepl.middleware.info :as info] [cider.nrepl.middleware.util.namespace :as ns])) ;;; ## Overview @@ -17,17 +18,26 @@ ;; abbreviated version (i.e. first sentence only) may be returned for ;; symbol-only searches. +(def special-forms + "Special forms that can be apropo'ed." + (concat (keys (var-get #'clojure.repl/special-doc-map)) + '[& catch finally])) + (defn var-name - "Return a var's namespace-qualified name as a string." + "Return a special form's name or var's namespace-qualified name as a string." [v] - (str/join "/" ((juxt (comp ns-name :ns) :name) - (meta v)))) + (if (special-symbol? v) + (str (:name (info/resolve-special v))) + (str/join "/" ((juxt (comp ns-name :ns) :name) (meta v))))) (defn var-doc - "Return a var's docstring, optionally limiting the number of sentences - returned." + "Return a special form or var's docstring, optionally limiting the number of + sentences returned." ([v] - (or (:doc (meta v)) "(not documented)")) + (or (if (special-symbol? v) + (:doc (info/resolve-special v)) + (:doc (meta v))) + "(not documented)")) ([n v] (->> (-> (var-doc v) (str/replace #"\s+" " ") ; normalize whitespace @@ -70,12 +80,16 @@ regex (-> (if case-sensitive? query (format "(?i:%s)" query)) re-pattern)] (->> (namespaces ns search-ns filter-regexps) (mapcat (comp (partial sort-by var-name) vals ns-vars)) + (concat (when (or (empty? search-ns) + (= 'clojure.core (symbol search-ns))) + special-forms)) (filter (comp (partial re-find regex) search-prop)) (map (fn [v] {:name (var-name v) :doc (var-doc* v) - :type (cond (:macro (meta v)) :macro - (fn? (deref v)) :function - :else :variable)}))))) + :type (cond (special-symbol? v) :special-form + (:macro (meta v)) :macro + (fn? (deref v)) :function + :else :variable)}))))) ;;; ## Middleware diff --git a/src/cider/nrepl/middleware/info.clj b/src/cider/nrepl/middleware/info.clj index e63daa0f9..cdf77a49d 100644 --- a/src/cider/nrepl/middleware/info.clj +++ b/src/cider/nrepl/middleware/info.clj @@ -139,15 +139,17 @@ used by `clojure.repl/doc`." [sym] (try - (let [sym (get '{& fn, catch try, finally try} sym sym) + (let [orig-sym sym + sym (get '{& fn, catch try, finally try} sym sym) v (meta (ns-resolve (find-ns 'clojure.core) sym))] (when-let [m (cond (special-symbol? sym) (#'repl/special-doc sym) (:special-form v) v)] - (assoc m - :url (if (contains? m :url) - (when (:url m) - (str "https://clojure.org/" (:url m))) - (str "https://clojure.org/special_forms#" (:name m)))))) + (-> m + (assoc :name orig-sym) + (assoc :url (if (contains? m :url) + (when (:url m) + (str "https://clojure.org/" (:url m))) + (str "https://clojure.org/special_forms#" (:name m))))))) (catch NoClassDefFoundError _) (catch Exception _))) diff --git a/test/clj/cider/nrepl/middleware/apropos_test.clj b/test/clj/cider/nrepl/middleware/apropos_test.clj index 836b057be..013457099 100644 --- a/test/clj/cider/nrepl/middleware/apropos_test.clj +++ b/test/clj/cider/nrepl/middleware/apropos_test.clj @@ -2,11 +2,28 @@ (:require [cider.nrepl.middleware.apropos :refer :all] [clojure.test :refer :all] [cider.nrepl.test-session :as session] + [clojure.repl :as repl] [clojure.string :as str])) (def ^{:doc "Test1. Test2. Test3."} public-var [1 2 3]) (def ^:private ^{:doc "Can't. See. Me"} private-var [:a :b :c]) +(deftest var-name-test + (testing "Returns Var's namespace-qualified name" + (is (= "clojure.core/conj" (var-name #'clojure.core/conj)))) + + (testing "Returns special form's name" + (is (= "if" (var-name 'if))))) + +(deftest var-doc-test + (testing "Returns Var's doc" + (is (= (:doc (meta #'clojure.core/conj)) + (var-doc #'clojure.core/conj)))) + + (testing "Returns special form's doc" + (is (= (:doc (#'repl/special-doc 'if)) + (var-doc 'if))))) + (deftest unit-test-metadata (is (= (var-name #'public-var) "cider.nrepl.middleware.apropos-test/public-var")) (is (= (var-doc #'public-var) "Test1. Test2. Test3.")) @@ -52,7 +69,22 @@ "Symbol search should return an abbreviated docstring.") (is (= (take 20 (:doc x)) (take 20 (:doc y))) - "The abbreviated docstring should be the start of the full docstring.")))) + "The abbreviated docstring should be the start of the full docstring."))) + + (testing "Includes special forms when `search-ns` is nil" + (is (not-empty (filter #(= "if" (:name %)) + (find-symbols nil "if" nil + false false false nil))))) + + (testing "Includes special forms when `search-ns` is \"clojure.core\"" + (is (not-empty (filter #(= "if" (:name %)) + (find-symbols nil "if" "clojure.core" + false false false nil))))) + + (testing "Excludes special forms when `search-ns` is some other ns" + (is (empty? (filter #(= "if" (:name %)) + (find-symbols nil "if" "clojure.set" + false false false nil)))))) (use-fixtures :each session/session-fixture) (deftest integration-test diff --git a/test/clj/cider/nrepl/middleware/info_test.clj b/test/clj/cider/nrepl/middleware/info_test.clj index 69849c6a4..310476741 100644 --- a/test/clj/cider/nrepl/middleware/info_test.clj +++ b/test/clj/cider/nrepl/middleware/info_test.clj @@ -53,7 +53,7 @@ (spit tmp-file-path "test") (testing "when fake.class.path is not set" (is (not (= (class (file tmp-file-name)) - java.net.URL))) + java.net.URL))) (is (= (file tmp-file-name) tmp-file-name))) (testing "when fake.class.path is set" (try @@ -73,6 +73,22 @@ (finally (System/clearProperty "fake.class.path"))))))) +(deftest resolve-special-test + (testing "Resolves all special forms" + (let [specials (keys clojure.lang.Compiler/specials)] + (is (every? (fn [[sym {:keys [name special-form]}]] + (and (= sym name) + (true? special-form))) + (map #(vector % (info/resolve-special %)) specials))))) + + (testing "Names are correct for symbols #{&, catch, finally}" + (is (= '& (:name (info/resolve-special '&)))) + (is (= 'catch (:name (info/resolve-special 'catch)))) + (is (= 'finally (:name (info/resolve-special 'finally))))) + + (testing "Returns nil for unknown symbol" + (is (nil? (info/resolve-special 'unknown))))) + (deftype T []) (deftest info-test