VSBlockJumper Postmortem Part 2: Code and Publishing

As I mentioned in Part 1, I wrote almost all of the important code for the extension in the first afternoon. The difficulty came in wrapping my head around the Visual Studio Extension framework and environment. The full source code is available on GitHub should you want to check out the implementation. I’m writing this out here in the hope that it could help others who face the same difficulties I did.

Let’s jump in 😁

VSBlockJumper

Adding Commands

The task seemed simple enough. Add a few new commands to the command-well and respond to them being invoked by the user. The Custom Command item template in Visual Studio Extensibility section was the right place to start. Adding one of these to the project sets up a VsPackage which adds a custom command to the Tools menu. Building off of this I added another Custom Command so that I had one called JumpUp and another called JumpDown. I then altered the generated .vsct file (presumably Visual Studio Command Table) to add my commands to the Edit menu since this also namespaces the command in the command-well (i.e. Edit.JumpUp vs. Tools.JumpUp). The list of menus can be found here.

I got these commands working by filling out the callback stubs provided by the item template. The next step was to remove them from the menu and add keyboard shortcuts. To achieve the former, I added <CommandFlag>CommandWellOnly</CommandFlag> to each of the Button elements in the .vsct xml file. Command flags are listed here.

I then tried to invoke the command via a newly assigned keybinding. However, the callback stubs provided are for the menu interaction only, so even though the command appeared in the command-well, assigning and executing a keyboard shortcut did nothing!

Responding to Keybindings

So the problem I’m facing at this point is how do I listen and respond to command-well events being invoked by keybinding. A quick search yielded this walkthrough on MSDN detailing how to add a shortcut key for an extension.

It details steps to add a Command Filter to a Text View, and operate on the Text View when a character is entered. What it doesn’t tell you is how to respond to a command of your own. Here’s how I did it in my Exec function.

public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, 
                IntPtr pvaIn, IntPtr pvaOut)
{
    if (pguidCmdGroup == IDs.PackageCommandSetGUID)
    {
        if (nCmdID == IDs.JumpUpCommandID)
        {
            Jump(JumpDirection.Up);
        }
        else if (nCmdID == IDs.JumpDownCommandID)
        {
            Jump(JumpDirection.Down);
        }
        else if (nCmdID == IDs.JumpSelectUpCommandID)
        {
            JumpSelect(JumpDirection.Up);
        }
        else if (nCmdID == IDs.JumpSelectDownCommandID)
        {
            JumpSelect(JumpDirection.Down);
        }

        return VSConstants.S_OK;
    }

    return Next.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, 
                     pvaOut);
}

The key is to check that the pguidCmdGroup parameter is the same as your VsPackage Command Set GUID. If it is then you can compare the nCmdID parameter against your VsPackage command IDs. Both the GUID and Command IDs can be found in the .vsct file, I threw them into a static IDs class to be accessible from anywhere.

I now needed to add a Text View creation listener in order to add my command filter to any Text View the user could interact with. This step would cause me no end of trouble.

Adding the Command Filter to a Text View

As part of the walkthrough it asks you to add an Editor Text Adornment item template to your project, and then delete all of the files it generates. As far as I can tell (MSDN doesn’t spectify) the sole purpose of this is to set up your Extension project as an MEF Component by adding all of the required dependencies/references, and altering the assets specified in the manifest file. This is required for the view creation listener to work.

That means that the extension was no longer just a VsPackage, but it was now also an MEF Component. I strongly suspect this was the source of my woes, but I can’t be sure.

What happened, was that after I added the view creation listener it didn’t work. At all. I put breakpoints in the listener and there was never a callback when a new document was instantiated. So, I restarted the project, only this time I skipped adding the Custom Commands and went straight to adding the Text View creation listener. Voila! I was getting callbacks. Time to re-add the Custom Commands! Uh oh. Callbacks are dead again.

I ended up repeating this process numerous times because even after I did get these two things working, it was somehow so fragile that small code changes could break the Text View creation listener callback.

I tried this on multiple machines, I asked in the ExtendVS gitter channel, I checked the MSDN documentation. There seemed to be no solution listed anywhere. It turned out, that it was a problem with the experimental instance.

The experimental instance is an isolated instance of visual studio that is set up solely for debugging extensions. Apparently things get corrupted very easily. I kind of intuitively figured this out and after a bit of googling found that there is a shortcut in the Visual Studio folder to reset the experimental instance:

C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2017\Microsoft Visual Studio SDK\Tools\Reset the Visual Studio 2017 Experimental Instance.lnk

I ran this and everything started working again without changing the code whatsoever. If there is one thing that you can take away about creating extensions for Visual Studio, it should be this. I ended up running it 9/10 before launching the Experimental Instance.

Backwards Compatibility

Now that I had all of the base functionality working, something I wanted to do was support Visual Studio 2015 (since I use this all day at work and desperately wanted this feature). Now that I know what to do, it seems quite simple but it did take a lot of tinkering on my part to arrive at the solution.

Basically what I did was to downgrade all of the references I was using to v14 (v15 is for Visual Studio 2017). That got it working for Visual Studio 2015, but broke it for 2017. The trick is to downgrade everything EXCEPT for the build tools (which remain at v15).

You also need to update the manifest to include v14 as an installation target and to use v14 dependencies. My mistake was that I was also updated the Prerequisites for the core editor to include v14. The problem with this, is that versions of Visual Studio older than 2017 don’t support this element. They use manifest version 2. Visual Studio 2017, while it says that the manifest version is 2.0, it’s actually 3.0.

Carlos Quintero explains (and thank God I found his blog):

You need to convert the extension.vsixmanifest to version 3, which is the only version that Visual Studio 2017 accepts (and the Visual Studio Gallery for extensions targeting Visual Studio 2017). Version 3 is backwards compatible with version 2, which means that all fields are the same (including the Version=”2.0.0″!). The only difference is that version 3.0 adds a mandatory section for the prerequisites. At the very least you need to specify the Visual Studio Core Editor

After returning the Prerequisites value to default the extension finally worked in both VS 2015 and 2017.

Icon Shenanigans

Finally I was done with version 1.0 and ready to upload to the marketpace. However it wouldn’t accept my .vsix. It gave a very vague error which I posted about here. As a workaround I linked my release on GitHub but I wasn’t satisfied with that because it forces you to download and install the extension outside of Visual Studio instead of the one-click install inside when you find an extension you want.

I did a bunch of reading regarding the error and there was nobody who had the same problem I did. A few posts I found were quite similar though and it turned out in their case there was something wrong with their package icon. I shrunk mine down a bit and sure enough it worked.

I wanted to use the correct size for these image files so I looked up how big they should be. I found this MSDN article about the manifest designer, but the dimensions they specify here are not good viewing dimensions for the marketplace or the in editor browser. I ended up checking out a bunch of popular packages to see what they did and followed their standard instead, which was:

Icon: 128x128
Preview: 200x200

This looked good while still allowing me to upload the images.

Settings Shenanigans

I decided post-launch that I wanted to add a couple of settings in case users wanted a bit more control over the way the cursor jumped. Adding the settings page and options was quite straight forward as detailed here.

The challenge was in getting the data from the serialized settings into the MEF Component from the VsPackage. Everything about the package is lazy load, even the settings don’t load until they need to be displayed on the dialog page. To achieve this I had to employ a bit of hackery and crackery.

First, I needed to make sure the VsPackage would load at the same time the MEF Component would be loaded (so, when the first text view is created). Again, a VsPackage doesn’t load until you DO SOMETHING with it, and since the MEF Component is doing everything, the Package itself doesn’t get loaded at all unless we intervene.

Luckily I discovered you can add an attribute to your package to force a load by specifying a UI Context GUID under which it should load. The CodeWindow context seemed like it’d be exactly what I wanted, but it didn’t actually do anything. I found a few sources saying to use NoSolution, but I wasn’t happy with that either. I figured there must exist a GUID for the basic text editor and so I persisted until I managed to find this tweet from Justin Clareburt (Senior Program Manager for Visual Studio):

This was exactly what I was looking for! The VsPackage now loaded whenever a text view was created. However, the damn DialogPage (which serializes and deserializes my settings) was still not loaded unless I specifically went into Tools>Options>VsBlockJumper. Now here is where things start to get yucky.

To force the load, I added this code into my VsPackage.Initialize() function:

OptionPageGrid page = (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid));

Since the damn thing is so lazy, this actually forces our VsPackage to instantiate our DialogPage class which loads up our settings.

To get the data into the MEF Component (my Command Filters) I had to interface with the MEF Component Model and Get/Set my settings via the IEditorOptionsFactoryService. The code on GitHub should be fairly self explanatory.

I skimmed over a lot of the details in this post, but I genuinely hope it helps. I know that I wish all of this information was collated somewhere when I wrote the extension, maybe it would’ve saved me a bunch of time. Feel free to ask any questions in the comments and I’ll do my best to answer them!

Leave a Reply