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

Improve inspector #24

Merged
merged 6 commits into from
Mar 21, 2018
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## master (unreleased)

* Inspector: separately clickable keys and values when inspecting maps
* Inspector: remember page numbers for each nesting level

### New features

* Initial extraction of nREPL-agnostic functionality from `cider-nrepl`.
122 changes: 66 additions & 56 deletions src/orchard/inspect.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,52 +24,47 @@
(defn push-item-to-path
"Takes the current inspector index, the `idx` of the value in it to be navigated
to, and the path so far, and returns the updated path to the selected value."
[index idx path]
[index idx path current-page page-size]
(if (>= idx (count index))
(conj path '<unknown>)
(if (= idx 0)
(conj path 'class)
(let [klass (first index)]
(cond
;; If value's class is a map, jumping into its value means finding a
;; MapEntry object for the key.
;; If value's class is a map, going down means jumping into either key
;; or value.
((supers klass) clojure.lang.IPersistentMap)
(conj path (list 'find (first (nth index idx))))

;; For a MapEntry, clicking on the first item means getting the key of
;; the MapEntry, second - means getting the value by the key.
(= klass clojure.lang.MapEntry)
(if (= idx 1)
(conj path 'first)
(let [[_ key] (peek path)]
(conj (pop path)
(if (keyword? key)
key
(list 'get key)))))
(if (even? idx)
;; Even index means jumping into the value by the key.
(let [key (nth index (dec idx))]
(conj path (if (keyword? key)
key
(list 'get key))))
;; Odd index means finding the map entry and taking its key
(conj path (list 'find (nth index idx)) 'key))

;; For sequential things going down means getting the nth value.
((supers klass) clojure.lang.Sequential)
(conj path (list 'nth (dec idx)))
(let [coll-idx (+ (* (or current-page 0) page-size)
(dec idx))]
(conj path (list 'nth coll-idx)))

:else (conj path '<unknown>))))))

(defn pop-item-from-path
"Takes the current inspector path, and returns an updated path one level up."
[path]
(let [last-node (peek path)]
(if (or (keyword? last-node)
(and (list? last-node) (= (first last-node) 'get)))
(let [key (if (keyword? last-node)
last-node
(second last-node))]
(conj (pop path) (list 'find key)))
(if (= last-node 'key)
(pop (pop path)) ; pop twice to remove <(find :some-key) key>
(pop path))))

(defn clear
"Clear an inspector's state."
[inspector]
(merge (reset-index inspector)
{:value nil :stack [] :path [] :rendered '() :current-page 0}))
{:value nil, :stack [], :path [], :pages-stack [],
:current-page 0, :rendered '()}))

(defn fresh
"Return an empty inspector."
Expand All @@ -85,11 +80,13 @@
(defn up
"Pop the stack and re-render an earlier value."
[inspector]
(let [stack (:stack inspector)]
(let [{:keys [stack pages-stack]} inspector]
(if (empty? stack)
(inspect-render inspector)
(-> inspector
(update-in [:path] pop-item-from-path)
(assoc :current-page (peek pages-stack))
(update-in [:pages-stack] pop)
(inspect-render (last stack))
(update-in [:stack] pop)))))

Expand All @@ -98,11 +95,13 @@
rendered value."
[inspector idx]
{:pre [(integer? idx)]}
(let [{:keys [index path]} inspector
(let [{:keys [index path current-page page-size]} inspector
new (get index idx)
val (:value inspector)
new-path (push-item-to-path index idx path)]
new-path (push-item-to-path index idx path current-page page-size)]
(-> (update-in inspector [:stack] conj val)
(update-in [:pages-stack] conj current-page)
(assoc :current-page 0)
(assoc :path new-path)
(inspect-render new))))

Expand Down Expand Up @@ -176,8 +175,8 @@
(format "{ %s }")))

(defmethod inspect-value :map-long [value]
(str "{ " (ffirst value) " "
(inspect-value (second (first value))) ", ... }"))
(let [[k v] (first value)]
(str "{ " (inspect-value k) " " (inspect-value v) ", ... }")))

(defmethod inspect-value :vector [value]
(safe-pr-seq value "[ %s ]"))
Expand Down Expand Up @@ -232,7 +231,32 @@
(render-value value)
(render-ln)))

(defn render-indexed-values [inspector obj]
(defn render-map-values [inspector mappable]
(reduce (fn [ins [key val]]
(-> ins
(render " ")
(render-value key)
(render " = ")
(render-value val)
(render '(:newline))))
inspector
mappable))

(defn render-indexed-values
([inspector obj] (render-indexed-values inspector obj 0))
([inspector obj idx-starts-from]
(loop [ins inspector, [c & more] obj, idx idx-starts-from]
(if c
(recur (-> ins
(render " " (str idx) ". ")
(render-value c)
(render '(:newline)))
more (inc idx))
ins))))

(defn render-collection-paged
"Render a single page of either an indexed or associative collection."
[inspector obj]
(let [{:keys [current-page page-size]} inspector
last-page (if (or (instance? clojure.lang.Counted obj)
;; if there are no more items after the current page,
Expand All @@ -245,8 +269,9 @@
current-page (cond (< current-page 0) 0
(> current-page last-page) last-page
:else current-page)
chunk-to-display (->> (map-indexed list obj)
(drop (* current-page page-size))
start-idx (* current-page page-size)
chunk-to-display (->> obj
(drop start-idx)
(take page-size))
paginate? (not= last-page 0)]
(as-> inspector ins
Expand All @@ -256,13 +281,9 @@
(render '(:newline)))
ins)

(reduce (fn [ins [idx val]]
(-> ins
(render " " (str idx) ". ")
(render-value val)
(render '(:newline))))
ins
chunk-to-display)
(if (map? obj)
(render-map-values ins chunk-to-display)
(render-indexed-values ins chunk-to-display start-idx))

(if (< current-page last-page)
(render ins " ...")
Expand All @@ -278,17 +299,6 @@
(assoc :current-page current-page))
ins))))

(defn render-map-values [inspector mappable]
(reduce (fn [ins [key val]]
(-> ins
(render " ")
(render-value key)
(render " = ")
(render-value val)
(render '(:newline))))
inspector
mappable))

(defn render-meta-information [inspector obj]
(if (seq (meta obj))
(-> inspector
Expand All @@ -300,10 +310,10 @@
(defn known-types [ins obj]
(cond
(nil? obj) :nil
(map? obj) :seq
(vector? obj) :seq
(seq? obj) :seq
(set? obj) :seq
(map? obj) :coll
(vector? obj) :coll
(seq? obj) :coll
(set? obj) :coll
(var? obj) :var
(string? obj) :string
(instance? Class obj) :class
Expand All @@ -319,20 +329,20 @@
(-> inspector
(render-ln "nil")))

(defmethod inspect :seq [inspector obj]
(defmethod inspect :coll [inspector obj]
(-> inspector
(render-labeled-value "Class" (class obj))
(render-meta-information obj)
(render-ln "Contents: ")
(render-indexed-values obj)))
(render-collection-paged obj)))

(defmethod inspect :array [inspector obj]
(-> inspector
(render-labeled-value "Class" (class obj))
(render-labeled-value "Count" (alength obj))
(render-labeled-value "Component Type" (.getComponentType (class obj)))
(render-ln "Contents: ")
(render-indexed-values obj)))
(render-collection-paged obj)))

(defmethod inspect :var [inspector ^clojure.lang.Var obj]
(let [header-added
Expand Down
68 changes: 39 additions & 29 deletions test/orchard/inspect_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@

(def eval-result (eval (read-string code)))

(def inspect-result ["(\"Class\" \": \" (:value \"clojure.lang.PersistentTreeMap\" 0) (:newline) \"Contents: \" (:newline) \" \" \"0\" \". \" (:value \"[ :a { :b 1 } ]\" 1) (:newline) \" \" \"1\" \". \" (:value \"[ :c \\\"a\\\" ]\" 2) (:newline) \" \" \"2\" \". \" (:value \"[ :d e ]\" 3) (:newline) \" \" \"3\" \". \" (:value \"[ :f [ 2 3 ] ]\" 4) (:newline))"])
(def inspect-result ["(\"Class\" \": \" (:value \"clojure.lang.PersistentTreeMap\" 0) (:newline) \"Contents: \" (:newline) \" \" (:value \":a\" 1) \" = \" (:value \"{ :b 1 }\" 2) (:newline) \" \" (:value \":c\" 3) \" = \" (:value \"\\\"a\\\"\" 4) (:newline) \" \" (:value \":d\" 5) \" = \" (:value \"e\" 6) (:newline) \" \" (:value \":f\" 7) \" = \" (:value \"[ 2 3 ]\" 8) (:newline))"])

(def push-result ["(\"Class\" \": \" (:value \"clojure.lang.PersistentTreeMap$BlackVal\" 0) (:newline) \"Contents: \" (:newline) \" \" \"0\" \". \" (:value \":a\" 1) (:newline) \" \" \"1\" \". \" (:value \"{ :b 1 }\" 2) (:newline) (:newline) \" Path: (find :a)\")"])
(def push-result ["(\"Class\" \": \" (:value \"clojure.lang.PersistentArrayMap\" 0) (:newline) \"Contents: \" (:newline) \" \" (:value \":b\" 1) \" = \" (:value \"1\" 2) (:newline) (:newline) \" Path: :a\")"])

(def long-sequence (range 70))
(def long-vector (vec (range 70)))
(def long-map (zipmap (range 70) (range 70)))
(def long-nested-coll (vec (map #(range (* % 10) (+ (* % 10) 80)) (range 200))))

(defn inspect
[value]
Expand Down Expand Up @@ -82,15 +83,15 @@
(is (= push-result
(-> eval-result
inspect
(inspect/down 1)
(inspect/down 2)
render)))))

(deftest pop-test
(testing "popping a rendered expr inspector"
(is (= inspect-result
(-> eval-result
inspect
(inspect/down 1)
(inspect/down 2)
inspect/up
render)))))

Expand All @@ -99,7 +100,8 @@
(is (= 33 (-> long-sequence
inspect
:counter)))
(is (= 33 (-> long-map
;; Twice more for maps
(is (= 65 (-> long-map
inspect
:counter)))
(is (.startsWith (-> long-vector
Expand Down Expand Up @@ -154,39 +156,44 @@
inspect/prev-page
inspect/next-page
inspect/prev-page
:current-page)))))
:current-page))))
(testing "page numbers are tracked per nesting level"
(let [ins (-> long-nested-coll
inspect
inspect/next-page
inspect/next-page
inspect/next-page
inspect/next-page)]
(is (= 4 (:current-page ins)))
(let [ins (-> ins
(inspect/down 1)
inspect/next-page
inspect/next-page)]
(is (= 2 (:current-page ins)))
(is (= 4 (:current-page (inspect/up ins))))))))

(deftest path-test
(testing "inspector tracks the path in the data structure"
(is (.endsWith (first (-> long-map
inspect
(inspect/down 20)
(inspect/down 39)
render))
"\" Path: (find 50)\")"))
"\" Path: (find 50) key\")"))
(is (.endsWith (first (-> long-map
inspect
(inspect/down 20)
(inspect/down 1)
render))
"\" Path: (find 50) first\")"))
(is (.endsWith (first (-> long-map
inspect
(inspect/down 20)
(inspect/down 2)
(inspect/down 40)
render))
"\" Path: (get 50)\")"))
(is (.endsWith (first (-> long-map
inspect
(inspect/down 20)
(inspect/down 2)
(inspect/down 40)
(inspect/down 0)
render))
"\" Path: (get 50) class\")")))
(testing "doesn't show path if unknown navigation has happened"
(is (.endsWith (first (-> long-map
inspect
(inspect/down 20)
(inspect/down 2)
(inspect/down 40)
(inspect/down 0)
(inspect/down 1)
render))
Expand Down Expand Up @@ -220,6 +227,7 @@
"#{ :a }" #{:a}
"( 1 1 1 1 1 ... )" (repeat 1)
"[ ( 1 1 1 1 1 ... ) ]" [(repeat 1)]
"{ :a { ( 0 1 2 3 4 ... ) 1, ... } }" {:a {(range 10) 1, 2 3, 4 5, 6 7, 8 9}}
"( 1 2 3 )" (lazy-seq '(1 2 3))
"#<MyTestType test1>" (MyTestType. "test1"))))

Expand All @@ -228,18 +236,20 @@
(let [t {:a (list 1 2 {:b {:c (vec (map (fn [x] {:foo (* x 10)}) (range 100)))}})
:z 42}
inspector (-> (inspect/start (inspect/fresh) t)
(inspect/down 1) (inspect/down 2)
(inspect/up) (inspect/up)
(inspect/down 1) (inspect/down 2)
(inspect/down 1)
(inspect/up)
(inspect/down 2)
(inspect/down 2)
(inspect/up)
(inspect/down 3)
(inspect/down 1) (inspect/down 2)
(inspect/down 1) (inspect/down 2)
(inspect/down 2)
(inspect/down 2)
inspect/next-page
inspect/next-page
(inspect/down 10)
(inspect/down 1) (inspect/down 1))]
(is (= '[:a (nth 2) :b :c (nth 9) (find :foo) first] (:path inspector)))
(is (= '[:a (nth 2) :b :c (nth 9) (find :foo) first class]
(inspect/down 1))]
(is (= '[:a (nth 2) :b :c (nth 73) (find :foo) key] (:path inspector)))
(is (= '[:a (nth 2) :b :c (nth 73) (find :foo) key class]
(:path (-> inspector (inspect/down 0)))))
(is (= '[:a (nth 2) :b :c (nth 9) (find :foo) first class <unknown>]
(is (= '[:a (nth 2) :b :c (nth 73) (find :foo) key class <unknown>]
(:path (-> inspector (inspect/down 0) (inspect/down 1))))))))