Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observe cider-doc.edn Java resource files for user-extensible documentation #863

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions doc/modules/ROOT/pages/nrepl-api/ops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -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``.
Expand Down
3 changes: 2 additions & 1 deletion src/cider/nrepl.clj
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,15 @@ 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__\".
For Java interop queries, it helps inferring the precise type of the object the `:sym` or `:member` refers to,
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`."})
Expand Down
79 changes: 72 additions & 7 deletions src/cider/nrepl/middleware/info.clj
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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 []
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO - use a cache for not computing the configs every time...

(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))
Expand Down Expand Up @@ -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]
Expand Down
27 changes: 27 additions & 0 deletions test/clj/cider/nrepl/middleware/info_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions test/resources/cider-doc.edn
Original file line number Diff line number Diff line change
@@ -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?}}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides from a function, :if could be a set of ns-qualified symbols:

:if #{clojure.core/future com.corp/my-macro ,,, }

which would mean: "this rule only applies if the symbol was found within one of these calls".

That would simplify user configs since, I reckon, most people won't want to do parsing themselves.