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

Add Datafy and Nav support to the inspector #161

Merged
merged 1 commit into from
Sep 3, 2022

Conversation

r0man
Copy link
Contributor

@r0man r0man commented Aug 20, 2022

Hello,

this PR adds basic support for Datafy and Nav to Orchard's
Inspector. The PR adds a Datafy section to the inspection view.

A good introduction to Datafy and Nav can be found here:

https://corfield.org/blog/2018/12/03/datafy-nav/

  • The commits are consistent with our contribution guidelines
  • You've added tests to cover your change(s)
  • All tests are passing
  • The new code is not generating reflection warnings
  • You've updated the changelog (if adding/changing user-visible functionality)

User Interface

Here's an overview how the UI looks like at the moment. In a nutshell,
a Datafy section is added to an object if it's datafy representation
is different than the original object.

This is done because datafy is defined on Object. Otherwise we would
show the same data in the datafy section as has been shown in a
previous section.

Class

(class (Object.))

Class: java.lang.Class

--- Interfaces:

--- Constructors:
  public java.lang.Object()

--- Fields:

--- Methods:
  public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
  public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
  public final void java.lang.Object.wait() throws java.lang.InterruptedException
  public boolean java.lang.Object.equals(java.lang.Object)
  public java.lang.String java.lang.Object.toString()
  public native int java.lang.Object.hashCode()
  public final native java.lang.Class java.lang.Object.getClass()
  public final native void java.lang.Object.notify()
  public final native void java.lang.Object.notifyAll()

---
Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :flags = #{ :public }
  :members = { clone [ { :name clone, :return-type java.lang.Object, :declaring-class java.lang.Object, :parameter-types [], :exception-types [ java.lang.CloneNotSupportedException ], ... } ], equals [ { :name equals, :return-type boolean, :declaring-class java.lang.Object, :parameter-types [ java.lang.Object ], :exception-types [], ... } ], finalize [ { :name finalize, :return-type void, :declaring-class java.lang.Object, :parameter-types [], :exception-types [ java.lang.Throwable ], ... } ], getClass [ { :name getClass, :return-type java.lang.Class, :declaring-class java.lang.Object, :parameter-types [], :exception-types [], ... } ], hashCode [ { :name hashCode, :return-type int, :declaring-class java.lang.Object, :parameter-types [], :exception-types [], ... } ], ... }
  :name = java.lang.Object

References

(atom {:a 1})

Class: clojure.lang.Atom
Contains:

Class: clojure.lang.PersistentArrayMap
Contents:
  :a = 1
---
Datafy:
Class: clojure.lang.PersistentVector
Contents:
  0. { :a 1 }

Namespace

(create-ns 'orchard.inspect-test-ns)

Class: clojure.lang.Namespace
Count: 96
---
Refer from:
Imports: { Enum java.lang.Enum, InternalError java.lang.InternalError, NullPointerException java.lang.NullPointerException, InheritableThreadLocal java.lang.InheritableThreadLocal, Class java.lang.Class, ... }
Interns: {}
---
Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :name = orchard.inspect-test-ns
  :publics = {}
  :imports = { AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, ArithmeticException java.lang.ArithmeticException, ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException, ArrayStoreException java.lang.ArrayStoreException, ... }
  :interns = {}

Objects extended with metadata

(with-meta {:name "John Doe"}
  {'clojure.core.protocols/datafy (fn [x] (assoc x :type 'Person))})

Class: clojure.lang.PersistentArrayMap
Meta Information:
  clojure.core.protocols/datafy = orchard.inspect_test$eval70975$fn__70976@24234c2b
Contents:
  :name = "John Doe"
---
Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :name = "John Doe"
  :type = Person

Nav support

(with-meta {:name "John Doe"}
  {'clojure.core.protocols/datafy
   (fn [x] (assoc x :type 'Person))
   'clojure.core.protocols/nav
   (fn [coll k v]
     [k (get coll k v)])})

Class: clojure.lang.PersistentArrayMap
Meta Information:
  clojure.core.protocols/datafy = orchard.inspect_test$eval70979$fn__70980@11c9298e
  clojure.core.protocols/nav = orchard.inspect_test$eval70979$fn__70982@307bd0ce
Contents:
  :name = "John Doe"
---
Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :name = [ :name "John Doe" ]
  :type = [ :type Person ]

Throwable support

(ex-info "BOOM" {})

Class: clojure.lang.ExceptionInfo
Value: "#error {\n :cause \"BOOM\"\n :data {}\n :via\n [{:type clojure.lang.ExceptionInfo\n   :message \"BOOM\"\n   :data {}\n   :at [orchard.inspect_test...
---
Fields:
  "backtrace" = java.lang.Object[] { short[] { 5, 1, 3, 0, 38, ... }, int[] { 1048576, 393216, 917504, 0, 24510464, ... }, java.lang.Object[] { clojure.core$ex_info, clojure.core$ex_info, orchard.inspect_test$eval70985, orchard.inspect_test$eval70985, clojure.lang.Compiler, ... }, long[] { 34363890496, 34363727080, 34363890496, 34363727080, 140652066883904, ... }, nil }
  "cause" = nil
  "data" = {}
  "depth" = 30
  "detailMessage" = "BOOM"
  "stackTrace" = java.lang.StackTraceElement[] { orchard.inspect_test$eval70985.invokeStatic(form-init6026781234703563055.clj:396), orchard.inspect_test$eval70985.invoke(form-init6026781234703563055.clj:396), clojure.lang.Compiler.eval(Compiler.java:7181), clojure.lang.Compiler.eval(Compiler.java:7136), clojure.core$eval.invokeStatic(core.clj:3202), ... }
  "suppressedExceptions" = (  )

Static fields:
  "$assertionsDisabled" = true
  "CAUSE_CAPTION" = "Caused by: "
  "EMPTY_THROWABLE_ARRAY" = java.lang.Throwable[] {  }
  "NULL_CAUSE_MESSAGE" = "Cannot suppress a null exception."
  "SELF_SUPPRESSION_MESSAGE" = "Self-suppression not permitted"
  "SUPPRESSED_CAPTION" = "Suppressed: "
  "SUPPRESSED_SENTINEL" = (  )
  "UNASSIGNED_STACK" = java.lang.StackTraceElement[] {  }
  "serialVersionUID" = -7034897190745766939

---
Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :via = [ { :type clojure.lang.ExceptionInfo, :message "BOOM", :data {}, :at [ orchard.inspect_test$eval70985 invokeStatic "form-init6026781234703563055.clj" 396 ] } ]
  :trace = [ [ orchard.inspect_test$eval70985 invokeStatic "form-init6026781234703563055.clj" 396 ] [ orchard.inspect_test$eval70985 invoke "form-init6026781234703563055.clj" 396 ] [ clojure.lang.Compiler eval "Compiler.java" 7181 ] [ clojure.lang.Compiler eval "Compiler.java" 7136 ] [ clojure.core$eval invokeStatic "core.clj" 3202 ] ... ]
  :cause = "BOOM"
  :data = {}

Datomic Entity Maps

With the following code Datomic entities can be inspected and navigated.

(extend-protocol clojure.core.protocols/Datafiable
  datomic.query.EntityMap
  (datafy [o] (into {} o)))

(extend-protocol clojure.core.protocols/Navigable
  datomic.query.EntityMap
  (nav [coll k v]
    (clojure.datafy/datafy v)))

A list of Datomic entities looks like this in the inspector.

Class: clojure.lang.LazySeq
Contents: 
  0. #:db{:id 17592186045426}
  1. #:db{:id 17592186045464}

Inspecting one of those entities looks like this:

Class: datomic.query.EntityMap
Value: "#:db{:id 17592186045484}"
---
Fields:
  "cache" = { :db/id 17592186045484 }
  "db" = { :id "6301025a-b0f1-4b9f-a9c1-83d64c7933e0", :memidx { :eavt #{ datomic.db.Datum@3aad15de datomic.db.Datum@a576f1d9 datomic.db.Datum@b92dfaa0 datomic.db.Datum@ca34a035 datomic.db.Datum@acb1f819 ... }, :avet #{ datomic.db.Datum@e0b11c05 datomic.db.Datum@f56f0503 datomic.db.Datum@b75722b3 datomic.db.Datum@f00ed8f5 datomic.db.Datum@adf34881 ... }, :aevt #{ datomic.db.Datum@3aad15de datomic.db.Datum@e0b11c05 datomic.db.Datum@d83059bd datomic.db.Datum@b931c078 datomic.db.Datum@b2faa1fb ... }, :raet #{ datomic.db.Datum@a576f1d9 datomic.db.Datum@b92dfaa0 datomic.db.Datum@ca34a035 datomic.db.Datum@953d3c2a datomic.db.Datum@942a2156 ... }, :fulltext { 47 { "_0.tii" com.datomic.lucene.store.RAMFile@4047c27b, "_0.fnm" com.datomic.lucene.store.RAMFile@3975a5ac, "segments.gen" com.datomic.lucene.store.RAMFile@277722e4, "_0.nrm" com.datomic.lucene.store.RAMFile@491e487, "_0.frq" com.datomic.lucene.store.RAMFile@7abbba74, ... }, 62 { "_0.tii" com.datomic.lucene.store.RAMFile@31127fcb, "_1.nrm" com.datomic.lucene.store.RAMFile@73e6af21, "_0.fnm" com.datomic.lucene.store.RAMFile@6de97d30, "segments.gen" com.datomic.lucene.store.RAMFile@32f05563, "_0.nrm" com.datomic.lucene.store.RAMFile@103e88d1, ... }, 75 { "_0.tii" com.datomic.lucene.store.RAMFile@2418e2ba, "_3.fdt" com.datomic.lucene.store.RAMFile@6b6b0202, "_3.prx" com.datomic.lucene.store.RAMFile@59bab00e, "_1.nrm" com.datomic.lucene.store.RAMFile@6b09064e, "_3.frq" com.datomic.lucene.store.RAMFile@8e724a7, ... } } }, :indexing nil, :mid-index nil, :index nil, ... }
  "edits" = nil
  "eid" = 17592186045484

Static fields:
  "const__0" = #'clojure.core/push-thread-bindings
  "const__1" = #'clojure.core/hash-map
  "const__11" = #'clojure.core/hash
  "const__13" = #'clojure.core/seq
  "const__14" = #'datomic.query/emap
  "const__15" = #'clojure.core/keys
  "const__18" = #'datomic.db/resolve-id
  "const__2" = #'clojure.core/*print-length*
  "const__20" = #'datomic.query/touch
  "const__22" = #'clojure.core/chunked-seq?
  "const__23" = #'clojure.core/chunk-first
  "const__24" = #'clojure.core/chunk-rest
  "const__26" = #'clojure.core/first
  "const__27" = #'clojure.core/next
  "const__28" = #'clojure.core/into
  "const__29" = #'clojure.core/map
  "const__3" = 20
  "const__30" = #'clojure.core/comp
  "const__31" = #'clojure.core/str
  "const__32" = #'datomic.query/get-lazy-entity
  "const__33" = #'clojure.core/concat
  "const__34" = #'clojure.core/remove
  "const__35" = #'clojure.core/not
  "const__37" = #'datomic.db/normalize-kw
  "const__39" = #'datomic.query/eav
  "const__4" = #'clojure.core/*print-level*
  "const__41" = #'clojure.core/assoc
  "const__5" = 3
  "const__6" = #'clojure.core/pr-str
  "const__7" = #'clojure.core/pop-thread-bindings

---
Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :kodo.file/sha = "19229ecff0189702c94d12005e6af143f4d6ee5a"
  :kodo.file/commit = { :kodo.commit/message "zdAELbqMzakfiRsnZusHMHXdXyiUJ", :kodo.commit/sha "f8afc26bc0d09b66c80f3596872302d9d993ccff", :kodo.commit/parents #{ #:db{:id 17592186045426} }, :kodo.commit/files #{ #:db{:id 17592186045484} #:db{:id 17592186045467} }, :kodo.commit/committed-at Sat Aug 20 15:48:42 UTC 2022, ... }
  :kodo.file/forms = #{ #:db{:id 17592186045490} #:db{:id 17592186045492} #:db{:id 17592186045488} }
  :kodo.file/path = "src/ydvwghyccsgrkowkjjlbpehwshnvy/ijiavgsniankwmkrp/tgglucwfamui/jbzawpmsgi.clj"
  :kodo.file/path+sha = [ "src/ydvwghyccsgrkowkjjlbpehwshnvy/ijiavgsniankwmkrp/tgglucwfamui/jbzawpmsgi.clj" "19229ecff0189702c94d12005e6af143f4d6ee5a" ]

I haven't opened a PR on cider-nrepl yet, because I was constantly
tweaking the rendering a bit. Once we aggree on a UI I can do that
works as well (if there needs to be done anything).

WDYT?

Copy link
Member

@vemv vemv left a comment

Choose a reason for hiding this comment

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

Thanks much - looking great!

If you haven't gotten to see the results in your local cider-nrepl, you can run make install first here, then in the cider-nrepl project.

Possibly doing so will serve as QAing and might reveal possible necessary tweaks.

src/orchard/inspect.clj Outdated Show resolved Hide resolved
src/orchard/inspect.clj Outdated Show resolved Hide resolved
src/orchard/inspect.clj Outdated Show resolved Hide resolved
test/orchard/inspect_test.clj Outdated Show resolved Hide resolved
@r0man
Copy link
Contributor Author

r0man commented Aug 20, 2022

Oops, going to look at those failing tests soon.

@vemv
Copy link
Member

vemv commented Aug 20, 2022

Thanks for the follow-up commits!

Do you need help with the build, or you got it?

Either way, ideally the tests would fail as clearly as possible.

I'd suggest, please use the following technique (in addition to preferring files to large inline strings):

(or (is (= expected actual))
    (do
      (println (format "Expected: the contents you can find in %s.txt" path))
      (println (format "Actual: %s" actual))
      (println (format "In order to obtain a diff, you can paste the Actual value in %s.txt and leverage `git diff`"))))

This will make future maintenance more pleasant.

@r0man
Copy link
Contributor Author

r0man commented Aug 20, 2022

Hi @vemv , thanks for your review.

I think I addressed most of it. I did not go so far as to move the data to files, but I changed them to compare data instead of the brittle strings. Is that ok for you?

That helped me also to find some CI issues, with method signatures being printed differently across JDKS (some had native in them, some not). Still digging into it ...

@r0man
Copy link
Contributor Author

r0man commented Aug 20, 2022

Oh, I forgot. The text snippets I posted above are actually taken from the Cider Inspect buffer. I got it running locally and was viewing the results in Cider. :) They seem to look and work as I would expect.

I'm actually not a REBL user myself. So, if anyone (especially someone familiar with REBL and the Cider inspector) has suggestions for improvements, feedback welcome!

@r0man
Copy link
Contributor Author

r0man commented Aug 20, 2022

I think I'm fine now. The tests are passing now. Thanks for your help!

@r0man r0man force-pushed the datafy branch 2 times, most recently from dd813c4 to 7e3cdba Compare August 20, 2022 20:20
Copy link
Member

@vemv vemv left a comment

Choose a reason for hiding this comment

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

One (likely) last round

I will check it out again soon for a more domain-centric review. I've used rebl in the past.

src/orchard/inspect.clj Outdated Show resolved Hide resolved
src/orchard/inspect.clj Show resolved Hide resolved
src/orchard/inspect.clj Outdated Show resolved Hide resolved
src/orchard/misc.clj Show resolved Hide resolved
test/orchard/inspect_test.clj Outdated Show resolved Hide resolved
test/orchard/inspect_test.clj Outdated Show resolved Hide resolved
@r0man r0man force-pushed the datafy branch 3 times, most recently from 2923db3 to 8ab4cde Compare August 20, 2022 22:39
inspect
render)))))
(is (= (if datafy?
'("Class"
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you can use (cond-> '["Class" ,,,] datafy? (conj ,,,)) for DRY

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

(testing "doesn't show path if unknown navigation has happened"
(is (-> long-map inspect (inspect/down 40) (inspect/down 0) (inspect/down 1) render ^String (first) (.endsWith "(:newline))"))))
(is (= '(:newline) (-> long-map inspect (inspect/down 40) (inspect/down 0) (inspect/down 1) render last))))
Copy link
Member

Choose a reason for hiding this comment

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

I like these new assertions, they're simpler.

Just in case, why is first now last?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously render returned a vector with a single string. So what the test did, it used first to get at the string, and then matches that large string with endsWith. Since we now return a seq, extracting the first element is not needed (we are interested in the last one). The test diff looks kind of confusing

(.setStackTrace (into-array StackTraceElement [])))
inspect render skip-to-datafy-section)]
;; Differences in JDKs :/
(is (or (= '("Datafy: "
Copy link
Member

Choose a reason for hiding this comment

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

Let's code it approximately like this instead, leaving something with fixed expectations:

(case misc/java-api-version
  8 (is (= ,,,))
  (11 16 17) (is (= ,,,))
  (assert false "Not clause for this JDK"))

Copy link
Member

@vemv vemv left a comment

Choose a reason for hiding this comment

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

I think these make sense, however the PR and changelog should be clear about what "Datafy and Nav" support mean.

In the PR they mean rendering more data at the bottom.

But what would be more commonly understood as "datafy and nav support" would be to:

  • use the nav protocol to navigate objects
    • while cider presumably has its own way to navigate objects (e.g. we already can recursively walk down a nested vector), nav would make even more objects navigable
  • use datafy as the only source of data
    • e.g. replace other fields.

I'd say REBL works like this.

I'm not saying it should be done, but I feel it should be part of the conversation.

@r0man
Copy link
Contributor Author

r0man commented Aug 21, 2022

@vemv I believe the PR makes more objects available to the
Inspector than before. So the Datafy section does not "just" show
the p/datafy data, it shows the result of navigating into the
p/datafy data.

(defn- datafy-nav-data-tx [obj]
  (comp (map (fn [[k v]] (some->> (nav obj k v) datafy (vector k))))
        (remove nil?)))

(defn- datafy-nav-data [obj]
  (let [data (datafy obj)]
    (if-not (map? data)
      data
      (into {} (datafy-nav-data-tx obj) data))))

Notice the call to nav in datafy-nav-data-tx. I believe this
is what REBL does to navigate into things. But I might be wrong.

I previously had 2 sections, one called "Datafy Contents:" and
one called "Nav Contents:". There the "Datafy Contents:" showed
just p/datafy and the "Nav Contents:" was showing what we see
now (the objects reachable by p/nav). Since my initial "Datafy
Contents:" section would just mirror what the inspector showed
anyway I removed that and show now the objects reachable with nav
only.

What you mentioned in "use datafy as the only source of data" is
probably true. We have now 2 ways of navigation, the one the
inspector always used, and now through d/nav in the Datafy
section.

I didn't want to completely change the way the inspector
navigates objects. I'm also not sure if I completely understood
how the inspector and nav works :) But I'm open to anything.

@vemv
Copy link
Member

vemv commented Aug 21, 2022

So the Datafy section does not "just" show
the p/datafy data, it shows the result of navigating into the
p/datafy data.

Yeah, but it does just one level deep down (i.e. it's not fully recursive). Users might want to further navigate. Or not at all!

So rendering text is sort of a best-effort guess of what can be useful to users.

And then users, if interested in this text, cannot navigate it directly, but they have to use the traditional CIDER navigation instead.

This mixing-and-matching of two approaches might create bit of a strange, possibly unintuitive hybrid.

However, as said, I'm not against it - it's displaying information/insights that may not be discoverable at all otherwise.

So I'd say, let's go for your PR, but long-term someone should give a good think to what the UX should be - it has to be created holistically between orchard and cider.el.

We might even end up with two independent features: cider-inspect (as-is) and e.g. cider-navigate (a rebl-like variation that only uses datafy/nav).

And lastly, I'll let @bbatsov chime in before any merging.

Cheers - V

@r0man
Copy link
Contributor Author

r0man commented Aug 21, 2022

@vemv Another option would be to define the datafy like this:

(def ^:private datafy
  (if misc/datafy?
    (misc/call-when-resolved 'clojure.core.protocols/datafy)
    identity))

And use it in all places where we inspect a value. If datafy is
available you see the datafied inspected value, otherwise what
Cider did before. We then could maybe remove the Datafy section
completly.

This would change the current user interface a bit. So, without that
change inspecting the #'*assert* var would be inspected in the
following way:

Class: clojure.lang.Var
Meta Information: 
  :ns = clojure.core
  :name = *assert*
Value: true

After the change I describe it would look like this:

Class: clojure.lang.Var
Meta Information:
  :ns = { :name clojure.core, :publics { * #'clojure.core/*, *' #'clojure.core/*', *1 #'clojure.core/*1, *2 #'clojure.core/*2, *3 #'clojure.core/*3, ... }, :imports { AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, ArithmeticException java.lang.ArithmeticException, Array java.lang.reflect.Array, ArrayChunk clojure.core.ArrayChunk, ... }, :interns { * #'clojure.core/*, *' #'clojure.core/*', *1 #'clojure.core/*1, *2 #'clojure.core/*2, *3 #'clojure.core/*3, ... } }
  :name = *assert*
Value: true

Inspecting a Datomic entitiy seq would then look like this:

  (entities my-db ::commit/sha)
Class: clojure.lang.LazySeq
Contents:
  0. { :kodo.commit/message "lacQnalAY", :kodo.commit/sha "21fb33d56a1a171bd0dff2f66b4209cd11320a34", :kodo.commit/files #{ #:db{:id 17592186045447} #:db{:id 17592186045429} #:db{:id 17592186045464} }, :kodo.commit/committed-at Sun Aug 21 10:07:24 UTC 2022, :kodo.commit/authored-at Sun Aug 21 10:07:24 UTC 2022, ... }
  1. { :kodo.commit/message "qZ", :kodo.commit/sha "75d293da4a2230e81807d379e5f3f854a1d505c8", :kodo.commit/parents #{ #:db{:id 17592186045426} }, :kodo.commit/files #{ #:db{:id 17592186045484} }, :kodo.commit/committed-at Sun Aug 21 10:07:24 UTC 2022, ... }

And navigating into one of the entities would look like this:

Class: clojure.lang.PersistentArrayMap
Contents:
  :kodo.commit/message = "lacQnalAY"
  :kodo.commit/sha = "21fb33d56a1a171bd0dff2f66b4209cd11320a34"
  :kodo.commit/files = #{ #:db{:id 17592186045447} #:db{:id 17592186045429} #:db{:id 17592186045464} }
  :kodo.commit/committed-at = Sun Aug 21 10:07:24 UTC 2022
  :kodo.commit/authored-at = Sun Aug 21 10:07:24 UTC 2022
  :kodo.commit/author = "NprBGLwFXlWFckzyIDiMyjsygjuMD@example.com"
  :kodo.commit/committer = "NprBGLwFXlWFckzyIDiMyjsygjuMD@example.com"

  Path: (nth 0)

Wdyt?

@vemv
Copy link
Member

vemv commented Aug 21, 2022

I see!

I wouldn't be on board with it as-is.

For instance, for:

Class: clojure.lang.Var
Meta Information:
  :ns = { :name clojure.core, :publics { * #'clojure.core/*, *' #'clojure.core/*', *1 #'clojure.core/*1, *2 #'clojure.core/*2, *3 #'clojure.core/*3, ... }, :imports { AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, ArithmeticException java.lang.ArithmeticException, Array java.lang.reflect.Array, ArrayChunk clojure.core.ArrayChunk, ... }, :interns { * #'clojure.core/*, *' #'clojure.core/*', *1 #'clojure.core/*1, *2 #'clojure.core/*2, *3 #'clojure.core/*3, ... } }
  :name = *assert*
Value: true

The formatting isn't particularly great. And clojure.core/* isn't nav-able by the user, it's just text.

Without that optional, yet full recursivity that is triggered by user actions on-demand, we lose all the 'magic' of the rebl.

(if we had said magic, formatting would matter less, but we cannot have the worst of both worlds)

@bbatsov
Copy link
Member

bbatsov commented Aug 22, 2022

My preference would also be to not change the default behavior. On CIDER's front we can expose the Datafy and Nav support as opt-in.

@bbatsov
Copy link
Member

bbatsov commented Aug 22, 2022

Btw, might be a good idea to document the user-interface somewhere in the namespace itself and in the README of Orchard. (you can reuse the description from the PR) We've always been pretty light on docs and we should start changing this at some point.

@vemv
Copy link
Member

vemv commented Aug 22, 2022

My preference would also be to not change the default behavior. On CIDER's front we can expose the Datafy and Nav support as opt-in.

Just in case - this PR just adds extra paragraphs with Datafy/Nav info, but otherwise does not change behavior.

@bbatsov
Copy link
Member

bbatsov commented Aug 22, 2022

I saw this and I meant that we should mention somewhere that if you're using Clojure 1.10 and if the representation is different you'll see different inspector output. Going forward we can add there how to just use Datafy by default if you want to.

Obviously the bigger point here is that the workings of the inspector are not really documented. :( Still, we should start somewhere.

@bbatsov
Copy link
Member

bbatsov commented Aug 22, 2022

(and the API docs, that we point people to, don't really exist :D )

@r0man
Copy link
Contributor Author

r0man commented Aug 22, 2022

@bbatsov Something like this? https://github.com/clojure-emacs/orchard/blob/15fb46de66c46dfa187294ee0084643db3c10bd4/doc/inspector.org

@r0man
Copy link
Contributor Author

r0man commented Aug 22, 2022

@vemv @bbatsov Regarding this fully recursive navigation.

This is how inspecting a namespace looks for example:

(orchard.inspect/inspect-print (create-ns 'orchard.inspect-test-ns))
Class: clojure.lang.Namespace
Count: 96
---
Refer from: 
Imports: { Enum java.lang.Enum, InternalError java.lang.InternalError, NullPointerException java.lang.NullPointerException, InheritableThreadLocal java.lang.InheritableThreadLocal, Class java.lang.Class, ... }
Interns: {}
---
Datafy: 
Class: clojure.lang.PersistentArrayMap
Contents: 
  :name = orchard.inspect-test-ns
  :publics = {}
  :imports = { AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, ArithmeticException java.lang.ArithmeticException, ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException, ArrayStoreException java.lang.ArrayStoreException, ... }
  :interns = {}

If you have point on the map next to "Imports:" or ":imports" in the
Datafy section, and you hit enter you "drill" down into the map point
is at.

If you have point on any text inside the map, on the java.lang.Enum
symbol for example in the "Imports:" map, or the AbstractMethodError
symbold in the ":imports" map, then on RET you will still inspect the
whole map. But as a user you might have expected to drill down into
that symbol instead. Is this what you mean by fully recursive in the
inspector?

The Slime inspector also works how Cider does it at the moment, right?

@r0man
Copy link
Contributor Author

r0man commented Aug 22, 2022

Also, what do you think about some UI tweaks. What about rendering the
Datafy section like this:

Class: clojure.lang.PersistentArrayMap

--- Meta Information: 
  clojure.core.protocols/datafy = orchard.inspect_test$eval10781$fn__10782@595bc66f
  clojure.core.protocols/nav = orchard.inspect_test$eval10781$fn__10784@328a8616

--- Contents: 
  :name = "John Doe"

--- Datafy: 
Class: clojure.lang.PersistentArrayMap
Contents: 
  :name = [ :name "John Doe" ]
  :type = [ :type Person ]

The difference is to place the "---" in front of the section name. To compare take a look at the PR description, there you can see how we render it at the moment.

This would be more consistent with how we do when rendering a class
(if we tweak the datafy section here as well):

Class: java.lang.Class

--- Interfaces: 

--- Constructors: 
  public java.lang.Object()

--- Fields: 

--- Methods: 
  public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
  public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
  public final void java.lang.Object.wait() throws java.lang.InterruptedException
  public boolean java.lang.Object.equals(java.lang.Object)
  public java.lang.String java.lang.Object.toString()
  public native int java.lang.Object.hashCode()
  public final native java.lang.Class java.lang.Object.getClass()
  public final native void java.lang.Object.notify()
  public final native void java.lang.Object.notifyAll()

--- Datafy: 
Class: clojure.lang.PersistentArrayMap
Contents: 
  :flags = #{ :public }
  :members = { clone [ { :name clone, :return-type java.lang.Object, :declaring-class java.lang.Object, :parameter-types [], :exception-types [ java.lang.CloneNotSupportedException ], ... } ], equals [ { :name equals, :return-type boolean, :declaring-class java.lang.Object, :parameter-types [ java.lang.Object ], :exception-types [], ... } ], finalize [ { :name finalize, :return-type void, :declaring-class java.lang.Object, :parameter-types [], :exception-types [ java.lang.Throwable ], ... } ], getClass [ { :name getClass, :return-type java.lang.Class, :declaring-class java.lang.Object, :parameter-types [], :exception-types [], ... } ], hashCode [ { :name hashCode, :return-type int, :declaring-class java.lang.Object, :parameter-types [], :exception-types [], ... } ], ... }
  :name = java.lang.Object

We could align the "Refer from" in the namespace:

Class: clojure.lang.Namespace
Count: 96

--- Refer from:
Imports: { Enum java.lang.Enum, InternalError java.lang.InternalError, NullPointerException java.lang.NullPointerException, InheritableThreadLocal java.lang.InheritableThreadLocal, Class java.lang.Class, ... }
Interns: {}

--- Datafy:
Class: clojure.lang.PersistentArrayMap
Contents:
  :name = orchard.inspect-test-ns
  :publics = {}
  :imports = { AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, ArithmeticException java.lang.ArithmeticException, ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException, ArrayStoreException java.lang.ArrayStoreException, ... }
  :interns = {}

It was not clear to me how to use "---" consistently. Some are in
front of text and some are on a newline, which confused me a bit.

Wdyt?

@bbatsov
Copy link
Member

bbatsov commented Aug 24, 2022

I like the proposal!

It was not clear to me how to use "---" consistently. Some are in
front of text and some are on a newline, which confused me a bit.

There is a lot of legacy here and little design thought I guess. :-) The first version of the inspector mostly followed the original inspector UI from SLIME for Common Lisp and it lived in swank-clojure. Afterwards the code saw mostly cosmetic tweaks and improvements to deal better with large data structures. I'm pretty sure if we start to think about all the weird stuff we copied we can come up with a ton of ideas for improvements. :-)

@bbatsov
Copy link
Member

bbatsov commented Aug 24, 2022

The Slime inspector also works how Cider does it at the moment, right?

More or less. I haven't used SLIME in ages, so I don't remember the details at this point.

@bbatsov
Copy link
Member

bbatsov commented Aug 24, 2022

@bbatsov Something like this? https://github.com/clojure-emacs/orchard/blob/15fb46de66c46dfa187294ee0084643db3c10bd4/doc/inspector.org

Yeah, that's what I had in mind + some "design" documentation explaining at high level the workings of the inspector and why it might be useful to people.

@r0man
Copy link
Contributor Author

r0man commented Aug 26, 2022

Hi @bbatsov and @vemv,

I updated the inspector.org document with the UI tweaks I proposed
above. Apart from rendering the "---" in front of the section I
tweaked the following things:

  • Class interfaces, constructors, fields and methods are sorted by
    calling .getName on them, instead of str. I did this change
    mostly to sort methods by their name, and not by their return type
    (which comes first when just sorting by str) Additionally this
    should help with the testing on different JDKs, since the previous
    behaviour was underterministic.

  • The fields of a class are only shown when not empty.

  • I added indentation and some helper functions. The "Datafy" section
    and the "Contains" section basically render a whole new object with
    the "Class" and other header fields. Having those sections indented
    by 2 spaces makes them a bit more readable in my opinion.

  • The "Refer from", "Interns", and "Imports" sections are pretty
    printed in a similar way as the the class fields, methods, etc.

Here's a link to the new proposal:

https://github.com/r0man/orchard/blob/datafy/doc/inspector.org

And here is how it looked like previously:

https://github.com/clojure-emacs/orchard/blob/15fb46de66c46dfa187294ee0084643db3c10bd4/doc/inspector.org

I haven't updated the tests yet, and haven't committed the code for
those changes yet. Working with those tests is a bit annoying, because
of subtile differences between JDKs or values printed referencing
memory addresses.

I also experimented a bit with @vemv's suggestion about moving the
rendered output into text files, comaring strings again, and showing a
git diff in case on failures to the user. For the majority of cases
this was ok, but I did not have the feeling the workflow got much
better for the edge cases, where you have to "massage" the test data a
bit to get rid of memory addresses for example.

WDYT about those changes? Are you ok with them, do you have any
suggestions for improvemnts? Should I actually do this in a separate
PR?

@bbatsov
Copy link
Member

bbatsov commented Aug 28, 2022

The updated format looks good to me. It seems you've also updated the tests in the mean time.

@r0man
Copy link
Contributor Author

r0man commented Aug 28, 2022

Alright, I'm trying to improve the tests a bit and also do some fine tuning. I also run into an issue the way I rendered namespaces. I might need to tweak that also a bit. I'll ping you when I'm ready.

@bbatsov
Copy link
Member

bbatsov commented Aug 28, 2022

I'd also suggest starting the inspector docs with some general design notes (e.g. rationale behind some decisions, tradeoffs we've made, etc - similar to the explanations from the PR itself).

@vemv
Copy link
Member

vemv commented Aug 28, 2022

As a quick note I don't have much bandwidth these days so I'll unsubscribe from this thread.

Either way, kudos for the great work and driving it towards the finish line!

@r0man
Copy link
Contributor Author

r0man commented Aug 28, 2022

As a quick note I don't have much bandwidth these days so I'll unsubscribe from this thread.

Either way, kudos for the great work and driving it towards the finish line!

Alright, thanks for your help so far!

@r0man
Copy link
Contributor Author

r0man commented Sep 2, 2022

Hi @bbatsov,

I added more information to the inspector docs, updated the changelog and link to the docs from the readme.

Can you have another look please?

@bbatsov bbatsov merged commit 68bfc66 into clojure-emacs:master Sep 3, 2022
@bbatsov
Copy link
Member

bbatsov commented Sep 3, 2022

The PR looks good to me now. Fantastic work! 🙇‍♂️

I guess the next step would be to cut a new release and get the new version in cider-nrepl, right?

@r0man
Copy link
Contributor Author

r0man commented Sep 3, 2022

Hi @bbatsov, yes, that's right. It should work out of the box without any changes to cider-nrepl and cider itself.

@bbatsov
Copy link
Member

bbatsov commented Sep 4, 2022

@r0man The new Orchard is out, but it seems the changes to the format did break 20 unit tests in cider-nrepl https://app.circleci.com/pipelines/github/clojure-emacs/cider-nrepl/608/workflows/0e81dbda-0ca4-405b-b82a-f69ae3f1d432/jobs/6270 Please, look into fixing them when you can.

@r0man
Copy link
Contributor Author

r0man commented Sep 4, 2022

Oops, sorry about that. I'll fix them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants