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

Allow to tap> current value from inside cider inspector #3311

Closed
behrica opened this issue Jan 24, 2023 · 12 comments
Closed

Allow to tap> current value from inside cider inspector #3311

behrica opened this issue Jan 24, 2023 · 12 comments

Comments

@behrica
Copy link
Contributor

behrica commented Jan 24, 2023

Is your feature request related to a problem? Please describe.
I would like to visualize (graphically) parts of a complex data structure using the current value
of the cider-inspector, so combining the simple drilling into the data with a way to render
on key-press the data in a rich, graphical way

Describe the solution you'd like
As "visualizing of Clojure data" has a multitude of different solutions, I think the best
is a generic solution, which allows to easily tap> the current value in cider-inspector.
Then the data renderer could be plugged in as a tap handler
So maybe adding a function such as cider-inspector-tap-current-value to cider inspector could be useful.

Describe alternatives you've considered
Using cider-def-current-value can be used but requires "2 steps", namely define a var and then tap the var.
I could as well write ad-hoc code which drills down and taps, but using the cider-inspector for drill down is more comfortable.

Additional context
There was an initial discussion and some proof of concept code on Slack:
https://app.slack.com/client/T03RZGPFR/C0617A8PQ/thread/C0617A8PQ-1674486881.210099

To fully solve the problem, we would need something additionally.
The "data renderer" available for Clojure (example Clerk) cannot know "how to render arbitrary data".
They expect, that a human user provide them with some "help", how to render. In Clerk this is called "viewer selection" and calling a function giving a it a "viewer"

So ideally we would have a interactive function in Cider which "request" this information from the user and sends it together with the current value to "tap>" , as part of the cider-inspector-tap-current-value
This information could be probably a simple "string", a kind of "viewer id", which options could come from a list, which the user of cider can configure in a variable.

Without this feature, Clojure code would need to "guess" the viewer from the data, which is very likeley not working well.

The code linked above contains as well a proof of concept of such function:
clerk-tap-last-sexp-with-viewer

This does the tapping of the "last sexp", but the same could be applied for the proposed cider-inspector-tap-current-value

@behrica
Copy link
Contributor Author

behrica commented Jan 24, 2023

Just to add to the "viewer selection":
A "static list" is not the "best" solution, even-though being a very good start.

At least in Clerk , viewers can be added and removed at runtime,
so ideally the lisp function should be calling back into Clojure, to ask for the "current list of viewer ids".
To have this generic, the user should be able to configure which is the Clojure call back function to be called for retrieving the "list of viewers".

I have experimented with the viewer selection (with fixed list), and it is very comfortable.
Press-a-key + selecting from a list, and I get my data nicely rendered in Clerk in a web browser with the "right" viewer. ('as html table' for example for a vector-of-maps data structure)
Or as "vega-lite" plot in browser, when the current value of inspector is a map with vega-lite data.

@behrica
Copy link
Contributor Author

behrica commented Jan 24, 2023

Clerk supports this out-of-the box, using it's tap-inspector.
The viewer can be specified via "metadata" on the value tapped.
(my proof-of-concept code does this)
Portal supports something similar, but I have not tried.
https://cljdoc.org/d/djblue/portal/0.35.1/doc/ui-concepts/viewers

So my proposal should allow to use Clerk and Portal "as-are".

It might work as well with Kindly/Clay:
https://scicloj.github.io/kindly/

@vemv
Copy link
Member

vemv commented Jan 25, 2023

The "data renderer" available for Clojure (example Clerk) cannot know "how to render arbitrary data".

The fact that Portal can, and that there's a well-known datafy/nav set of protocols, leave me hesitant.

Ideally we would not let a specific tool's idiosyncracy permeate into the default UX.

I think this might be a middle ground solution:

(defcustom cider-inspector-tap-transformer
  "A ns-qualified, 1-arg function that will be invoked to transform an arbitrary transformation to all values
tapped via `cider-inspector-tap-current-value'.

You can use this to make tapped values easily viewable with certain tooling.

Or for any other arbitrary purpose"
  "clojure.core/identity")

My idea is that the UX would be invoking cider-inspector-tap-current-value and that's it. I think a variety of tools, like REBL and Portal will be happy with it.

But then if you need a little transformation, you can write:

(defn foo [x]
  ;; do whatever is needed to make `x` viewable.
  ;; for example:
  (with-meta x {:viewer :my-viewer}))

cider-nrepl would be responsible to require and invoke this defn.

Does it sound reasonable?

Please do feel free to propose a refinement.

@behrica
Copy link
Contributor Author

behrica commented Jan 26, 2023

I think that a big added value would come if Emacs can be configured (maybe outcide Cider) with "advising" or an other form of "hook" to show in Emacs a Dropdown immidiately before "rendering" for "viewer selection".

To have this in Clojure code seems to be too static to me.

Even things like datify needed to work on the Clojure "type" of the data.
So how could I say that in some case my "map" is vega-lite and should use a "vega-lite viewer", while in on other cases a "map" is just a normal map and should use a "map viewer" ?

Portal and Clerk solve this "in code", by allowing to specify the "map viewer" while calling the (render my-map) function. (render my-map :vega-lite-viewer

@vemv
Copy link
Member

vemv commented Jan 26, 2023

I see!

I think I like a lot having in cider something that maps very closely to tap>, with minimal added concepts. That would both be instantaneous to learn for tap> users, and would honor the Unix "one thing well" philosophy.

To have this in Clojure code seems to be too static to me.

You could build commands on top of this basic tap>-like op.

For instance, would could write this Elisp code as part of your user-specific config:

(defun tap-with-map-viewer
  (let* ((cider-inspector-tap-transformer "my.ns/specify-map-viewer"))
    (cider-inspector-tap-current-value)))

(defun tap-with-vega-lite
  (let* ((cider-inspector-tap-transformer "my.ns/specify-vega-lite"))
    (cider-inspector-tap-current-value)))

;; ...and so on - one defun per viewer

...and then you could M-x tap-with-vega-lite. This seems both usable (one step, not two) and would favor composition.

...you could also build a tiny UI (e.g. with helm, ido etc) for selection, but that wouldn't be CIDER's concern.

Let me know if this would be feasible. Again, I'm open to variations. But I think the general pattern would be "offer one or two low-level building blocks, let users build added value on top".

@behrica
Copy link
Contributor Author

behrica commented Jan 26, 2023

I put here are function which does this for tap-last-sexp

(setq clerk-viewer-list '("default"
                          ":html"
                          ":latex"
                          ":table"
                          "nextjournal.clerk.viewer/html-viewer"
                          "nextjournal.clerk.viewer/vega-lite-viewer"
                          "nextjournal.clerk.viewer/map-viewer"
                          "nextjournal.clerk.viewer/markdown-viewer"
                          "nextjournal.clerk.viewer/katex-viewer"
                          "nextjournal.clerk.viewer/fallback-viewer"
                          "nextjournal.clerk.viewer/string-viewer"))

(defun clerk-tap-last-sexp-with-viewer (viewer)
  (interactive
   (list (completing-read "Choose viewer: " clerk-viewer-list nil t)))

  (let ((tapped-form (concat "(clojure.core/->> "
                             (cider-last-sexp)
                             (if (equal "default" viewer)
                                 (concat " (nextjournal.clerk/with-viewer {:transform-fn identity})")
                               (if (string-prefix-p ":" viewer)
                                   (concat " (nextjournal.clerk/with-viewer " "(keyword \"" (substring viewer 1) "\")" ")")
                                   (concat " (nextjournal.clerk/with-viewer " "(symbol \"" viewer "\")" ")"))
                               )

                             " (clojure.core/tap>))")))
    (cider-interactive-eval tapped-form
                            nil
                            nil
                            (cider--nrepl-pr-request-map))))
                            

This is of course Clerk specific, so should not be in Cider, but it shows what I mean.
I can run this on a expression resulting in a map and then decide which viewer to choose.

@behrica
Copy link
Contributor Author

behrica commented Jan 26, 2023

Your last proposal looks good to me. In principle I agree on the "minimal code around tap>" principle
I think it would allow to do what I am aiming for:
My emacs-lisp is not very advanced, but i would trust you , when you say that it would work.
Maybe look above to my lisp code, to verify that your proposal would allow to do the same with Lisp code being outside cider.

Thanks for your support !

@vemv
Copy link
Member

vemv commented Jan 26, 2023

Yes, I think that my approach and the code you expressed in #3311 (comment) are compatible.

Specifically, you could write clojure code like this:

(defn make-viewer-fn [viewer]
  (fn [value]
    (nextjournal.clerk/with-viewer viewer
      value)))

(def html-viewer (make-viewer-fn nextjournal.clerk.viewer/html-viewer))

(def table-viewer (make-viewer-fn :table))

;; and so on

...then, on the Elisp side, you'd completing-read a value such as "my.ns/table-viewer" to cider-inspector-tap-transformer.

As a refresher, a defcustom is global value, but can be temporarily overriden by using dynamic binding, which is the default behavior in Elisp.

Thanks for your support !

Cheers 🍻 looking forward to your PR!

@behrica
Copy link
Contributor Author

behrica commented Jan 27, 2023

I never wrote any EmacsLisp code...

I think as well this would need a change in the cider middleware as we would need a new operation, if I understood it well.

Looking here:

(defun cider-sync-request:inspect-def-current-val (ns var-name)

My "hack" uses an existing middleware op and defines a var with a random name, but I am sure we don't want this in cider.
So the first thing to do would be to add an operation into teh middleware / orchard, something like:
inspect-tap-current-value

Should not be complicated, though

@behrica
Copy link
Contributor Author

behrica commented Jan 27, 2023

Looking here https://github.com/clojure-emacs/orchard/blob/464b6c8ecc228f7ead91545fefb5e4b65325a3d8/src/orchard/inspect.clj#L167

it would just need a new function tap-current-value which seems to be trivial to implement.

@vemv
Copy link
Member

vemv commented Jan 30, 2023

I think as well this would need a change in the cider middleware as we would need a new operation, if I understood it well.

Most likely yes

I'd be happy to review your PRs or support them with any advice or research.

Cheers - V

@vemv
Copy link
Member

vemv commented Jul 27, 2023

Feel free to pick up the conversation here clojure-emacs/cider-nrepl#770 (comment)

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

No branches or pull requests

2 participants