A few words from Agical

Extend VS Code in user space, without invented restrictions

Let’s break out of an imaginary jail. Let’s hack Visual Studio Code in all ways it allows for being modified by extensions, but without leaving user space. User space as in “a user of VS Code”, to contrast with “a developer of VS Code extensions”. The imaginary jail being that I thought some extensibility cases are closed to Joyride and need an extension to be implemented. It is not so!

Bliss in a universe of vast possibilities

With Joyride a whole world of possibilities opens for programmers who want to customize their development environment of choice from user space, as long as that choice is VS Code. Joyride scripts allow us to add those tiny features we miss, or large features, or fully automate our specific workflows, or script our use of ChatGPT, or just about anything. If we can imagine it, Joyride makes it so that we can make it happen.

All this without writing a full VS Code extension, with all the boilerplate that entails, and all the restart and reloading of the development environment that comes with it. When we have work we need to get done, we don’t need more restarts. We need less!

Joyride to the rescue! It lets us dynamically modify our work environment, in user space, without any reloading or restarting of anything. Instead we evaluate some code, and BOOM, we can continue to think about the problem we were working on, now boosted with support from the hack of the editor we just did.

Showing myself to jail

Up until today I have been thinking that Joyride can do all these cool things, but that it is limited to the dynamic part of the VS Code Extension API. Since full VS Code extensions rely heavily on the Extension Manifest, this closes off a lot of modifications from the Joyrider that still needs to be done by writing and packaging an extension. Our workflow takes a hit that often makes it a trade-off we might choose not to do. Or, so I thought. Up until today.

VS Code’s limited way of showing me the unsaved files in my workspace has wasted enough of my time. Today it was time to fix this. As a Joyrider it is second nature to me to create a file in my User Scripts directory and start making VS Code behave like I want it to.

Then I paused a bit, thinking about how I want VS Code to show me the unsaved files. Ideally it should sort them in a separate view in the Explorer. Only, that’s not possible with Joyride since Explorer Views need to be declared in an extension manifest. I thought of some other ways I could achieve it, and came up with quite a few, but none that made me excited enough to muster the energy to write the code for keeping tally of unsaved files and all that. I have more important things to do! I could of course write an extension to do the job, but it would be yet another extension solving some micro use case.

I put away the idea of solving this today. Let’s keep scrolling through the open files section in the Explorer. The bad taste of defeat made me grumpy.

Breaking free

Then it dawned on me. What if it doesn’t matter which extension declares the contribution point for the explorer view? I wrote a mini extension manifest which only declared an explorer view contribution. I named the extension Joyride Sidecar:

    ...
    "name": "joyride-sidecar",
    "displayName": "Joyride Sidecar",
    ...
    "contributes": {
        "views": {
            "explorer": [
                {
                    "id": "my-view",
                    "name": "My View"
                }
            ]
        }
    },
    ...

Then I packaged it using `npx vsce package` to a VSIX, and installed it. Yes, an extension manifest is all that is needed for this. The view appeared.

Of course it appeared. The important question here is: Can a Joyride script use this contribution point? To test that I went back to my user script and registered a TreeDataProvider towards the view id I had declared, by evaluating this code:

  (vscode/window.registerTreeDataProvider
   "my-view"
   #js {:getTreeItem
        (fn [element] element)
        :getChildren
        (fn []
          #js [#js {:label "My Item"
                    :command #js {:command "my-command"}}])})

BOOM.

I had the answer to my “what if?”. I never was in the dynamic-extension-api-only jail. I was in another jail, with limitations I only had imagined. But no longer! I have broken free. From today, I know that Joyride scripts can do anything a VS Code extension can do. And it can do it dynamically, keeping restarts at bay.

Prisoner reentry

There is a bit more to this. With registering views and other VS Code resources comes life cycle management. And, we also want to be able to automate the packaging, and the install, and uninstall of the sidecar extension. Of course we should do this with Joyride. We are not savages doing such things manually, right? So, even more lifecycle stuff. But first, let’s try clicking My Item:

Oh, that’s boring. But it is easy to fix! We just need to register “my-command” with an implementation.

  (vscode/commands.registerCommand
   "my-command"
   (fn [& _args]
     (vscode/window.showInformationMessage "Hello World")))

Note that we didn’t need to declare `my-command` in the extension manifest. This is stuff I have been doing before, when still in my imagined dynamic-extension-api-only jail. It is only if we want the command to appear in the command palette, or some other VS Code built-in menu, that it needs to go in the manifest. And we can use Joyride Sidecar for that.

Now, what about automatically packaging, installing and uninstalling Joyride Sidecar? Well, I was already using the command line for it, so it is mostly a matter of shelling out.

Packaging:

  (def sidecar-dir (path/resolve (path/dirname joyride/*file*) "../sidecar"))
  (child-process/exec "npx vsce package"
                      #js {:cwd sidecar-dir :shell true})

Installing:

  (child-process/exec "code --install-extension joyride-sidecar-0.0.1.vsix"
                      #js {:cwd sidecar-dir :shell true})

Uninstalling:

  (child-process/exec "code --uninstall-extension betterthantomorrow.joyride-sidecar"
                      #js {:shell true})

Of course, it gets a bit more involved considering how we need to coordinate these things and tie them into a development workflow where we can hot reload changes and such. It’s a bit of tricky boilerplate that would be a waste if we all needed to create it. Therefore I have shared mine in the Examples section of the Joyride repository:

I would love it if you grabbed this from there to your Joyride user scripts directory, and let us know what you do with it! Please tag @pappapez on Twitter when sharing about your Joyriding.

Meanwhile I will go back to my idea about implementing that unsaved files view. You can take it to the bank that I now have the energy to do so. Breaking out of jail has that effect on me.