From 0210893ba2c900ea582367602b7988844ec8f9c6 Mon Sep 17 00:00:00 2001 From: vemv Date: Sun, 31 Mar 2024 21:08:42 +0200 Subject: [PATCH] Observe `cider-doc.edn` Java resource files for user-extensible documentation --- CHANGELOG.md | 4 + doc/modules/ROOT/pages/nrepl-api/ops.adoc | 6 +- src/cider/nrepl.clj | 3 +- src/cider/nrepl/middleware/info.clj | 79 +++++++++++++++++-- test/clj/cider/nrepl/middleware/info_test.clj | 27 +++++++ test/resources/cider-doc.edn | 2 + 6 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 test/resources/cider-doc.edn diff --git a/CHANGELOG.md b/CHANGELOG.md index be3aad1a..c82e6e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master (unreleased) +### New features + +* Observe `cider-doc.edn` Java resource files for user-extensible documentation. + ### Changes * Refine `ops-that-can-eval` internals, adapting them to the new `cider.nrepl.middleware.reload` ops. diff --git a/doc/modules/ROOT/pages/nrepl-api/ops.adoc b/doc/modules/ROOT/pages/nrepl-api/ops.adoc index 03fc0089..62c5e278 100644 --- a/doc/modules/ROOT/pages/nrepl-api/ops.adoc +++ b/doc/modules/ROOT/pages/nrepl-api/ops.adoc @@ -316,7 +316,8 @@ For Java interop queries, it helps inferring the precise type of the object the making the results more accurate (and less numerous). * `:member` A Java class member. * `:ns` The current namespace -* `:sym` The symbol to lookup +* `:sym` The symbol to lookup. Must be a string. If it represents a keyword, please express so via the ``:symbol-type`` param. +* `:symbol-type` The type of object of ``:sym`` as seen by the user. One of: "symbol" (default), "keyword", "string". * `:var-meta-allowlist` The metadata keys from vars to be returned. Currently only affects ``:clj``. Defaults to the value of ``orchard.meta/var-meta-allowlist``. If specified, the value will be concatenated to that of ``orchard.meta/var-meta-allowlist``. @@ -453,7 +454,8 @@ For Java interop queries, it helps inferring the precise type of the object the making the results more accurate (and less numerous). * `:member` A Java class member. * `:ns` The current namespace -* `:sym` The symbol to lookup +* `:sym` The symbol to lookup. Must be a string. If it represents a keyword, please express so via the ``:symbol-type`` param. +* `:symbol-type` The type of object of ``:sym`` as seen by the user. One of: "symbol" (default), "keyword", "string". * `:var-meta-allowlist` The metadata keys from vars to be returned. Currently only affects ``:clj``. Defaults to the value of ``orchard.meta/var-meta-allowlist``. If specified, the value will be concatenated to that of ``orchard.meta/var-meta-allowlist``. diff --git a/src/cider/nrepl.clj b/src/cider/nrepl.clj index ebae1a7d..32ff84c7 100644 --- a/src/cider/nrepl.clj +++ b/src/cider/nrepl.clj @@ -272,7 +272,7 @@ Depending on the type of the return value of the evaluation this middleware may "doc-block-tags-fragments" (str "May be absent. Represent the 'param', 'returns' and 'throws' sections a Java doc comment. " fragments-desc)}) (def info-params - {"sym" "The symbol to lookup" + {"sym" "The symbol to lookup. Must be a string. If it represents a keyword, please express so via the `:symbol-type` param." "ns" "The current namespace" "context" "A Compliment completion context, just like the ones already passed for the \"complete\" op, with the difference that the symbol at point should be entirely replaced by \"__prefix__\". @@ -280,6 +280,7 @@ For Java interop queries, it helps inferring the precise type of the object the making the results more accurate (and less numerous)." "class" "A Java class. If `:ns` is passed, it will be used for fully-qualifying the class, if necessary." "member" "A Java class member." + "symbol-type" "The type of object of `:sym` as seen by the user. One of: \"symbol\" (default), \"keyword\", \"string\"." "var-meta-allowlist" "The metadata keys from vars to be returned. Currently only affects `:clj`. Defaults to the value of `orchard.meta/var-meta-allowlist`. If specified, the value will be concatenated to that of `orchard.meta/var-meta-allowlist`."}) diff --git a/src/cider/nrepl/middleware/info.clj b/src/cider/nrepl/middleware/info.clj index a3a669e4..a161793c 100644 --- a/src/cider/nrepl/middleware/info.clj +++ b/src/cider/nrepl/middleware/info.clj @@ -1,11 +1,12 @@ (ns cider.nrepl.middleware.info (:require - [compliment.context] - [compliment.sources.class-members] [cider.nrepl.middleware.util :as util] [cider.nrepl.middleware.util.cljs :as cljs] [cider.nrepl.middleware.util.error-handling :refer [with-safe-transport]] + [clojure.edn :as edn] [clojure.string :as str] + [compliment.context] + [compliment.sources.class-members] [orchard.eldoc :as eldoc] [orchard.info :as info] [orchard.meta :as meta] @@ -78,9 +79,51 @@ (def var-meta-allowlist-set (set meta/var-meta-allowlist)) +(def DSLable? + (some-fn simple-symbol? ;; don't allow ns-qualified things, since the Clojure var system takes precedence over DSLs + simple-keyword? + string?)) + +(defn cider-doc-edn-configs [] + (let [resources (-> (Thread/currentThread) + (.getContextClassLoader) + (.getResources "cider-doc.edn") + (enumeration-seq) + (distinct))] + (into {} + (keep (fn [resource] + (try + (let [m (into {} + (keep (fn [[k v]] + (let [symbols (into #{} + (filter DSLable?) + k) + resolved (into {} + (map (fn [[kk vv]] + [kk (or (misc/require-and-resolve vv) + (throw (ex-info "Discard" {})))])) + v)] + (when (and (contains? resolved :info-provider) + (contains? resolved :if) + (seq symbols)) + [symbols + resolved])))) + (edn/read-string (slurp resource)))] + (when (seq m) + ;; We don't merge all configs into a single object, because that risks data loss + ;; (e.g. if we merge {[foo] ,,,} with {[foo] ,,,}), one [foo] ,,, entry would be lost. + ;; Which is why we use `(str resource)` to keep an extra level of nesting. + [(str resource) + m])) + (catch Exception e ;; discard unparseable/unloadable user input + nil)))) + resources))) + (defn info - [{:keys [ns sym class member context var-meta-allowlist] + [{:keys [ns sym class member context var-meta-allowlist file] + symbol-type :type ;; one of: "symbol", "keyword", "string". Represents whether the queried token is a symbol/keyword/string. legacy-sym :symbol + :or {symbol-type "symbol"} :as msg}] (let [sym (or (not-empty legacy-sym) (not-empty sym)) @@ -116,11 +159,33 @@ (when var-meta-allowlist {:var-meta-allowlist (into meta/var-meta-allowlist (remove var-meta-allowlist-set) - var-meta-allowlist)}))] + var-meta-allowlist)})) + match-from-configs (when (and (not java?) ;; We don't encourage users to create ambiguity over Java interop syntax + ;; We don't encourage users to create ambiguity over Clojure var syntax, + ;; so ns-qualified symbols are disregarded: + (DSLable? sym)) + (some (fn [[_resource config]] + (some (fn [[symbols rules]] + (and (contains? symbols sym) + ((:if rules) context) + ((:info-provider rules) {:symbol (cond + (= symbol-type "symbol") + (symbol sym) + + (= symbol-type "keyword") + (keyword sym) + + :else sym) + :ns ns + :file file + :context context}))) + config)) + (cider-doc-edn-configs)))] (cond - java? (info/info-java class (or member sym)) - (and ns sym) (info/info* info-params) - :else nil))) + java? (info/info-java class (or member sym)) + match-from-configs match-from-configs + (and ns sym) (info/info* info-params) + :else nil))) (defn info-reply [msg] diff --git a/test/clj/cider/nrepl/middleware/info_test.clj b/test/clj/cider/nrepl/middleware/info_test.clj index d1829f6f..50b15ead 100644 --- a/test/clj/cider/nrepl/middleware/info_test.clj +++ b/test/clj/cider/nrepl/middleware/info_test.clj @@ -12,6 +12,27 @@ (cider.nrepl.test AnotherTestClass TestClass YetAnotherTest) (org.apache.commons.lang3 SystemUtils))) +(defn sample-info-provider [{:keys [symbol ns file context]}] + {:ns ns + :name symbol + :doc (format "%s rocks - doc dynamically generated from %s" + symbol + (-> ::_ namespace)) + :file file + :arglists [] + ;; :forms + :macro false + :special-form false + :protocol false + ;; :line + ;; :column + :static false + :added "1.0" + :deprecated false}) + +(defn sample-info-provider? [{:keys [symbol ns file context]}] + true) + (defprotocol FormatResponseTest (proto-foo [this]) (proto-bar [this] "baz")) @@ -370,6 +391,12 @@ (let [response (session/message {:op "info" :sym "xyz"})] (is (nil? (:see-also response)) + (pr-str response)))) + + (testing "cider-doc.edn" + (let [response (session/message {:op "info" :sym "cider-doc-edn-example"})] + (is (= "cider-doc-edn-example rocks - doc dynamically generated from cider.nrepl.middleware.info-test" + (:doc response)) (pr-str response))))) (testing "eldoc op" diff --git a/test/resources/cider-doc.edn b/test/resources/cider-doc.edn new file mode 100644 index 00000000..e6be4c0b --- /dev/null +++ b/test/resources/cider-doc.edn @@ -0,0 +1,2 @@ +{[cider-doc-edn-example] {:info-provider cider.nrepl.middleware.info-test/sample-info-provider + :if cider.nrepl.middleware.info-test/sample-info-provider?}}