joyride

pez 2022-05-26T09:17:56.085099Z

Before I settle for returning a string from the Calva evaluateCode API. What are the trade-offs between returning something potentially structured and a string? The consumer of this API might not be a ClojureScript program. It makes some sense to return a JS object, but it can't represent reader tags, for instance. And then also we might have a non-EDN result, even if right now, I'm not sure when that would be?

borkdude 2022-05-26T09:24:45.306009Z

A string is the only viable option imo

πŸ™ 1
borkdude 2022-05-26T09:25:04.332709Z

You can of course return a map with a string and add more things later.

borkdude 2022-05-26T09:25:15.682199Z

But the evaluation result should be a string

pez 2022-05-26T09:26:46.829359Z

If you could elaborate a bit on this, that would be swell. I trust your judgment here, of course, but still would like to know about why it is the only viable option.

borkdude 2022-05-26T09:27:22.099919Z

There is a reason why all of these tools use a string. The io.prepl also uses a a string:

$ clj
Clojure 1.11.0
user=> (clojure.core.server/io-prepl)
(+ 1 2 3)
{:tag :ret, :val "6", :ns "user", :ms 13, :form "(+ 1 2 3)"}
I'm sure if EDN would be possible, they would have done this. But you just can't represent non-data things (objects, JS functions, Clojure functions, etc) as data.

borkdude 2022-05-26T09:28:41.187339Z

Maybe returning a format similar to io-prepl could be nice, for extensibility later

borkdude 2022-05-26T09:30:43.688039Z

excellent

pez 2022-05-26T16:35:09.692669Z

Here's a pull request on Calva adding the evaluateCode API: https://github.com/BetterThanTomorrow/calva/pull/1740 Install the PR VSIX build once it is built to test this. Docs here: https://github.com/BetterThanTomorrow/calva/blob/5b155d58035a7779aedeb95f8664ba6f74a29abb/docs/site/api.md (Slightly weirdly rendered, since it is made for MkDocs Material.)

pez 2022-05-26T19:51:56.881779Z

The evaluateCode Calva API now released. https://calva.io/api/ cc @orestis @seancorfield Thanks to @alpox for helping me with it! I had a lot of fun in that pairing session.

πŸŽ‰ 1
seancorfield 2022-05-26T20:02:13.388179Z

Nice! I think I know what I'm going to be doing this afternoon (as soon as I get off this call with our vendor πŸ™‚ )...

2
pez 2022-05-26T20:14:28.850579Z

Looking forward to see what you do with this! And I am also curious what your next need for a Calva API will be.

seancorfield 2022-05-26T21:25:46.630609Z

@pez What is [z-joylib.editor-utils :as eu]? I tried following the ignore_form example but I get Could not find namespace: z-joylib.editor-utils

seancorfield 2022-05-26T21:26:42.675699Z

Ah, it's in a subfolder! Sorry...

borkdude 2022-05-26T21:31:29.971979Z

@pez What if you are connected to multiple REPLs, what part of the API controls which REPL you are sending a form to?

borkdude 2022-05-26T21:33:02.959869Z

Shouldn't you be able to query the currently open sessions and get a unique id from those to indicate where to send?

borkdude 2022-05-26T21:34:34.243119Z

E.g. if you are simultaneously connected to a joyride, JVM and nbb REPL

alpox 2022-05-26T21:45:18.019249Z

From what I gathered the current design only allows for one clj repl and one cljs repl connection at one time cutting down the selection (for the moment) to "clj" and "cljs" which can be passed to evaluateCode as session key. Although im not quite sure if the joyride connection is treated as a normal cljs repl session πŸ€”

seancorfield 2022-05-26T21:55:41.565789Z

Yeah, currently, if you have the Joyride nREPL connected and try to invoke Calva's evaluate API, it'll execute it via Joyride.

seancorfield 2022-05-26T21:56:36.766749Z

I developed most of my script interactively via the REPL, but then had to switch back to my Clojure nREPL connection to run my new script completely, since it relies on executing in a Clojure JVM environment.

orestis 2022-05-27T03:36:11.815919Z

Nice!! I'll give it a spin today. I also wonder how should I detect which session key to use? Daily development has me connected to a deps.edn + shadow.cljs so I jump between CLJS and CLJ. I could probably use the filename extension for this, but not sure about cljc :)

seancorfield 2022-05-27T03:40:40.046759Z

Yeah, my life is way simpler for only doing Clojure, but I do sort of look forward to a time where I might be doing cljs again (after seven year break at this point).

seancorfield 2022-05-27T03:41:42.669889Z

It sure would be nice if Calva supported a) more than two possible REPL connections and b) automatically figured out the right REPL based on the currently-active editor πŸ™‚

orestis 2022-05-27T04:44:37.559739Z

The other immediate need that comes to mind to do interesting things is having access to constructs like current form, top level form etc etc.

seancorfield 2022-05-27T04:46:36.110479Z

Yeah, I was a bit disappointed that it would not be easy to get "current form" -- executing paredit commands and trying to figure out ranges is way too messy. We need all of the substitutions that customREPLCommandSnippets can use, accessible as API methods.

orestis 2022-05-27T04:58:26.031799Z

My thoughts exactly. I think adding a few more top level APIs like get current session key, get the current/top level form as text etc etc would allow for interesting combinations. Presumably Calva could also use the same API to do what it does today.

seancorfield 2022-05-27T04:59:19.425839Z

Early days. This is just the first API -- and it's pretty powerful -- so I'm sure a lot more will follow πŸ™‚

pez 2022-05-27T05:28:30.635169Z

@borkdude, about the ”which session”. So what @alpox says, Calva only has two at most. See this old issue for a discussion around this: https://github.com/BetterThanTomorrow/calva/issues/76. So the unique id:s are clj, or cljs, (and also cljc, as it happens, which is an alias for either of those, that the user controls).

seancorfield 2022-05-27T05:29:27.352879Z

Sure would be nice if it had more than two and had a different ID for Joyride πŸ™‚

βž• 2
pez 2022-05-27T05:31:53.468719Z

I choose to decouple this API from the ”magic” of automatically figuring out which session should be used. Thinking that an API for querying this makes more sense. Also thinking that in a world where Calva supports more configurable REPL connections, this whole story changes and less magic will help then.

πŸ‘πŸ» 2
seancorfield 2022-05-27T05:33:18.567539Z

I'm all for "less magic".

pez 2022-05-27T05:48:33.958919Z

As for API for getting snippet-variables. That's on my todo. For now to get e.g. the current form text, we can do something like this:

(defn top-level-form-text []
  (p/let [_ (vscode/commands.executeCommand "paredit.rangeForDefun")
          selection vscode/window.activeTextEditor.selection
          document vscode/window.activeTextEditor.document
          code (.getText document selection)
          _ (vscode/commands.executeCommand "cursorUndo")]
    code))
I tuck such things away in editor-utils and try to forget about all the mutation going on. πŸ˜ƒ But, yeah, eventually this will beak in some situation.

seancorfield 2022-05-27T05:50:44.538389Z

That's fine for top-level form. What about "current form"?

seancorfield 2022-05-27T05:52:32.896349Z

For my symbol/expression needs, "current selection" is OK since you really kind of need to specify just how much text (code) should be evaluated, but having all those "custom REPL command snippet" substitutions easily available would be awesome!

pez 2022-05-27T06:01:26.843069Z

As it happens, I think all those calls are synchronous, so we get away without the promise. A more general function:

(defn text-for-selection-command [selection-command-id]
  (let [_ (vscode/commands.executeCommand selection-command-id)
        selection vscode/window.activeTextEditor.selection
        document vscode/window.activeTextEditor.document
        code (.getText document selection)
        _ (vscode/commands.executeCommand "cursorUndo")]
    code))
The two main examples:
(text-for-selection-command "paredit.rangeForDefun")
(text-for-selection-command "calva.selectCurrentForm")

pez 2022-05-27T06:02:32.804909Z

For some reason it doesn't work for "editor.action.selectAll", I don't understand why...

pez 2022-05-27T06:04:41.793079Z

To find the command ids, an easy way is to use the keyboard shortcuts editor:

orestis 2022-05-27T06:05:28.609669Z

Indeed !

pez 2022-05-27T06:06:55.294609Z

With that particular command you can recreate calva's evaluate-thread-to-cursor command. You'll have to close the form, of course. Calva has code for that. I guess that's a candidate for an API function as well.

orestis 2022-05-27T06:10:18.438219Z

So cool. I will spend some time today hacking with all this.

pez 2022-05-27T06:12:50.709389Z

Awesome. When you start playing with the evaluateCode API and thus run into the limitations with Calva's REPL session managament, there's a recipe over at ClojureVerse for how to give yourself some evaluate code in Joyride commands: https://clojureverse.org/t/say-hello-to-joyride-the-clojure-repl-in-vs-code/8957/3?u=pez

seancorfield 2022-05-27T06:18:03.597389Z

Thank you for this:

(text-for-selection-command "calva.selectCurrentForm")
This makes my Joyride scripts work much better!

2
seancorfield 2022-05-27T06:19:54.395959Z

(my vsode-calva-setup has been updated)

seancorfield 2022-05-27T06:20:23.592149Z

Seriously, this is really f'ing amazing πŸ™‚

❀️ 2
pez 2022-05-27T06:27:32.992339Z

An alternative to calva.selectCurrentForm, is paredit.sexpRangeExpansion, which in many cases does the same things, but if you run it an even number of times it can be used for selecting enclosing forms N levels up. You'll then have to shrink the selection back an equal amount of times, of course, (or Undo Selection, which is equivalent for this purpose.)

borkdude 2022-05-27T06:28:36.343669Z

πŸ€“

pez 2022-05-27T06:33:33.409689Z

When exposing these as API function we'll not be using Paredit really, because those commands are very much the same as what you get with the executeCommand function. We'll need to expose the underlying TokenCursor (similar in concept to a rewrite-clj zipper) in some nice way. Will be quite exciting to see what people do with that one. It often gives me a feeling of unlimited powers using it. πŸ˜ƒ

seancorfield 2022-05-27T06:37:19.131119Z

I'd be interested in ways to parameterize my two scripts to reduce duplication.

seancorfield 2022-05-27T06:38:49.730649Z

I know I could refactor to a "library script" in subfolder, like you have with z-joylib in the examples but that seems a bit artificial. Providing that sort of stuff directly inside Joyride itself seems reasonable, in terms of providing a Calva-specific ns that we can use.

seancorfield 2022-05-27T06:40:01.829329Z

Maybe a joyride.calva namespace with some of the common stuff in?

pez 2022-05-27T06:45:08.615919Z

It's not a perfect fit in Joyride, I think. But Calva could maybe provide it somehow... Right now Joyride only has the user and the workspace script folders on its class path, But if we provide a way to add to the classpath, then a calva joyride library could live in the calva extension folder. WDYT, @borkdude?

seancorfield 2022-05-27T06:46:00.348389Z

Ah, so it would be automatically available if you were running Calva? Interesting.

seancorfield 2022-05-27T06:46:36.062399Z

I can imagine several calva.* commands being available as direct functions, to make life easier.

borkdude 2022-05-27T06:46:38.920779Z

Hm yeah, that's interesting

pez 2022-05-27T06:46:51.891149Z

It could be general, Joyride checks all extensions for if they provide a library and adds to the classpath.

orestis 2022-05-27T06:47:26.672269Z

That would be nice also for the portal extension.

seancorfield 2022-05-27T06:47:32.740749Z

This feels like boilerplate, for example:

(def ^:private calva (vscode/extensions.getExtension "betterthantomorrow.calva"))

(def ^:private calvaApi (-> calva
                            .-exports
                            .-v0
                            (js->clj :keywordize-keys true)))

(defn- evaluate [code] ((:evaluateCode calvaApi) "clj" code))

seancorfield 2022-05-27T06:48:22.273589Z

Even this:

(defn- selected-text []
  (vscode/commands.executeCommand "calva.selectCurrentForm")
  (let [editor ^js vscode/window.activeTextEditor
        selection (.-selection editor)]
    (.getText (.-document editor) selection)))
select-current-form would be a good name for this, providing the text of selecting the current form.

borkdude 2022-05-27T06:48:27.891189Z

@pez Is it possible to determine the loading order of extensions? E.g. extensions could also actively register extra joyride namespaces

seancorfield 2022-05-27T06:48:51.600759Z

And maybe a higher level execute function for this:

(-> (p/let [code (clojuredocs-url (selected-text))
            resp (evaluate code)
            url  (edn/read-string (.-result resp))]
      (vscode/commands.executeCommand "simpleBrowser.show" url))
    (p/catch (fn [e] (println (str "Evaluation error: " e)))))

borkdude 2022-05-27T06:50:00.408599Z

There's always a boilerplate/flexibility balance to strike

seancorfield 2022-05-27T06:50:25.502669Z

(joyride.calva.evaluate (clojuredocs-url (joyride.calva.select-current-form)))

pez 2022-05-27T06:50:38.311179Z

@borkdude, so if Calva want's to provide a library it would rather tell Joyride that, than that Joyride checks Calva for it?

borkdude 2022-05-27T06:51:12.172829Z

Yeah, potentially it could call (joyride.api/add-namespace 'foo.bar {'dude (fn [] :hello)})

borkdude 2022-05-27T06:51:33.758129Z

but this would only work if joyride would be loaded before calva I guess

pez 2022-05-27T06:52:20.648959Z

I think it might be. But Calva could also wait for it to be active before it does it.

borkdude 2022-05-27T06:53:14.668789Z

Is there a hook in VSCode to wait for all extensions to have finished loading, or extension("joyride").ready(fn ...) or so

pez 2022-05-27T06:53:35.911129Z

Not that I know of.

pez 2022-05-27T06:53:54.776499Z

Doesn't mean there isn't one, of course.

borkdude 2022-05-27T06:54:53.340549Z

We could also do it another way. Whenever a namespace is loaded, we first search the classpath and then all extensions, lazily.

calva
.-exports
.-joyride_namespaces
.-the_namespace_you_were_looking_for

pez 2022-05-27T06:55:12.381099Z

@seancorfield if Calva can provide a library to Joyride, it could deffo be used to reduce boilderplate.

πŸ‘πŸ» 1
borkdude 2022-05-27T07:21:33.276039Z

Another idea:

(require '["extension-name$namespace" :as calva])
(calva/evaluatecode "...")

borkdude 2022-05-27T07:22:04.211149Z

And then Joyride searches within the extension-name exports for the namespace object (and treats this like any other JS library)

borkdude 2022-05-27T07:23:02.202459Z

extension-name would be calva here for this example

borkdude 2022-05-27T07:26:46.397659Z

So the full example would be:

(require '[""betterthantomorrow.calva$joyride" :as calva])
(the dollar sign is already a convention in CLJS for getting a property out of a JS library)

pez 2022-05-27T07:27:02.385519Z

Yes, I think that is very similar to what I had in mind starting this discussion. Even if I was thinking it would be a joyride/cljs library rather than a JS one.

borkdude 2022-05-27T07:27:59.597729Z

It doesn't really matter, but in the end, the extension exports a JS object, so we can just treat like a JS library. Then other extensions not written in Clojure can also contribute functions we can use in joyride

borkdude 2022-05-27T07:29:10.920659Z

I think as it stands, you'd have to do:

(require '["betterthantomorrow.calva$v0" :as calva])
And then you could immediately do:
(calva/evaluateCode ...)
This would work with the current Calva extension

borkdude 2022-05-27T07:33:00.307279Z

Yeah, I like that

pez 2022-05-27T07:36:21.210769Z

I'm thinking more that it would work very similar to what we do with user and workspace scripts. It would basically be joyride wrappers around the extension JS api.

borkdude 2022-05-27T07:41:28.703749Z

There is no need to wrap, imo. You can just require the functions from the extension and use them

pez 2022-05-27T07:47:47.313119Z

Ah, there could be other reasons why you would want to wrap things though. Some things only make sense to Joyride and the general API is not a good place for that.

borkdude 2022-05-27T07:49:10.333229Z

That is why you can select a certain part from the extension that is the specific joyride API:

(require '["betterthantomorrow.calva$joyride" :as calva])
This also allows you to have those functions pre-compiled (as JS) and thus would avoid interpretation overhead.

borkdude 2022-05-27T07:49:48.475429Z

I think it makes sense to have these functions as compiled JS so any extension can participate, not only those built with ClojureScript

borkdude 2022-05-27T07:50:04.422179Z

E.g. one could make a TypeScript extension and functions from those could be used from joyride as well in this way

pez 2022-05-27T07:54:56.895969Z

I don't think it would limit things to extensions written in ClojureScript. Calva is written mostly in TypeScript. Anyway, I opened up a discussion on the repo for this: https://github.com/BetterThanTomorrow/joyride/discussions/70

borkdude 2022-05-27T07:56:02.792299Z

I already responded to https://github.com/BetterThanTomorrow/joyride/discussions/69 which a couple of points. One point you seem to overlook is that extensions can provide functions directly, not code that should still be interpreted.

borkdude 2022-05-27T07:56:18.291049Z

It seems 70 is a duplicate, let's close that one.

pez 2022-05-27T07:57:01.805489Z

Ah, Github gave me errors so I didn't realize I had opened #69. Will update that one then.

pez 2022-05-27T08:28:58.929279Z

I know I replied to your comment on the #69 discussion, @borkdude, but now Github seems to be hiding them. It says 2 replies in one place, and 0 replies in another, and the latter seems to be what it is using. πŸ˜ƒ

seancorfield 2022-05-26T22:01:35.115849Z

This was the "missing piece" after my switch from Clover to Calva: with a symbol or expression highlighted in your Clojure code, running this Joyride script will: a) evaluate the selection in the context of your Clojure nREPL b) which includes the wrapper to produce a JavaDoc URL based on the type of that evaluated code c) and the result is then used to open SimpleBrowser inside VS Code at that page. I used this a lot with Clover where I could highlight some Java class type (or expression), hit a hot key (I have this bound to ctrl+alt+o j to match my other config which is all custom REPL snippets!), and get the JavaDocs directly in VS Code. Now I'm off to make one for clojuredocs which was the other wrapped up URL renderer I had in Clover!

πŸŽ‰ 2
borkdude 2022-05-26T22:09:08.395159Z

Very nice. Feel free to PR this to the joyride examples directory

seancorfield 2022-05-26T22:10:46.276269Z

Ah, good point! Yup, I'll clean it up and add more comments and send a PR, along with the clojuredocs.cljs that I just wrote.

seancorfield 2022-05-26T22:11:25.336589Z

(they are very similar)

seancorfield 2022-05-26T22:56:56.105849Z

I've updated my VS Code / Calva setup repo with these scripts for the time being -- https://github.com/seancorfield/vscode-calva-setup