Sometimes you make some tooling because you want to use it; sometimes, to experiment; and sometimes to test the waters.
The last tooling in that I did was one of these cases – now there’s a port of Chlorine to Visual Studio Code called Clover!
Now, when I started the project I imagined that VSCode would not have all the features that I have in Atom, nor all the APIs that I want to use – for example I didn’t have any hope of having the inline results in this version.
The thing is, I wasn’t expecting it to be so bad! To begin with, the API: is not really that bad documented, but compared to the Atom, it is incredibly weak. The first thing is that they expect you to use TypeScript so there’s little to no documentation on how to represent objects in pure JavaScript; for example in some cases you can use pure objects, on others you have to instantiate a TypeScript class in the JavaScript code. Also, there are multiple parts of the documentation when they just give you the type signatures and little (or even no) explanation (and let’s make a little detour here: what’s the deal with some people that use static types, that they expect you to understand how any API work just by showing the types of the functions?).
The second part is that VSCode expects you to fit your plug-in infrastructure on what they offer – so, some functionalities will land on the “peek definition” API, others on “Code Lens” and so on. The problem is that they don’t explain what’s a “code lens” for example nor give you any screenshots of the functionality in action – mostly the documentation is some code examples in GitHub repositories so you have to download, install the example extension on your machine and then run it to simply understand how something works.
The second hard part is that you can’t test the API in the devtools – in fact the devtools is almost useless because when you have an error, the stacktrace will point you to a minified JS code in the VSCode internal API. Also, some exception messages are completely obscure and some log errors on the devtools but things work fine on the editor. Well, to summarize: it’s completely useless to depend on the errors.
But the worst part, at least for REPL-Tooling, is that you can’t change the UI of VSCode in any way – and this means no pop-ups, no new elements, no console in the editor, nothing – the only way you have that you can extend the visual components is by implementing a webview – and by webview I am not saying an “electrom webview” when you can access all the Node.JS APIs – I am saying a simple web page when you have to pass your data to and from the editor using JSON. And and that’s all there is – no Date
, no JavaScript classes and, of course no Clojure objects.
So, considering that all the REPL interaction is written in ClojureScript, this means that I had to serialize all my data in EDN, pass all my results and make sure that I wasn’t passing any rich object like r/atom
or any of the protocols that I use to render objects. Also, remembering that the webview is just a page, I can’t connect to a Socket REPL because it would require websockify or some other technique…
So, it was time to discover if I made the right design decisions, and they did pay off! For REPL-Tooling (and Chlorine, and Clematis, and Clover) all connections to Clojure, ClojureScript or any REPL (even nREPL, if on the future me or someone decide to integrate it) are wrapped by a protocol repl-tooling.eval/Evaluator
. So, there are implementations for Clojure, for ClojureScript inside Clojure like Shadow-CLJS, and for Lumo too. So that means that evaluation of any REPL, and the parsing of the result is completely decoupled from the connection, the protocol, in fact, from anything that you imagine.
So, for VSCode, the evaluator in the webview just needs to pick up a command, generate an unique ID with (gensym)
, serialize the command with (str command)
, generate an EDN with the current REPL “flavor” (:clj
or :cljs
) and send this EDN via message-passing to the plug-in. When it got a result, it would call a callback. And that’s all. Wrap this up in a protocol and everything magically works.
Also, the renderer of objects is also implemented inside REPL-Tooling using reagent
, and it have multiple tests too, so it can be used on the webview. What it means is that both Atom and VSCode now have the exactly same renderer, and new versions, bugfixes, and so on will be replicated on both editors. It also means that as soon as more code is sent to REPL-Tooling (like the REBL integration, show var documentation, connecting to ClojureScript) both editors will have the same functionality.
But we still had to write code to connect the editor to the library. Currently, we have to pass some callbacks: what to do when we have some output (stdout, stderr, or an evaluation result), how to get the editor’s contents, notify for errors/warnings/infos, etc. If we register these callbacks (and, because of VSCode API’s limitations, we have to add on package.json
all commands that the editor will have to answer to), we have already have almost everything… and this means connecting to a Socket REPL, then evaluating code, rendering on the webview, all the “datafy/nav” and “lazy render” that Chlorine already have, on just 454 lines of code. Now, consider that 116 lines are just the webview contents that I had to implement (and even that one I’m thinking about moving to REPL-Tooling), the full plug-in is about 340 lines of code…
So, I consider the project a huge success. Now, there are still lots of things to do, of course, and in the process I still found things to migrate, ideas to implement, and limitations I’ll somehow have to bypass. But everything is looking really promising!