This is part of the "Markdown mode" series:
Part 2 - Writing a classifier Part 1 - Markdown!
Well, this part ended up being both easier and considerably harder than I expected, depending on how you look at it.
First, the obligatory screenshot:
So the easy thing in this case turned out to be the actual logic of creating the web browser and changing what it shows. WPF comes with a decently handy WebBrowser control, with an equally handy (but funnily-named) NavigateToString. To generate the Markdown text, I just call into Markdown.cs (instead of MarkdownParser.cs; this time, I do just want the HTML output), and dump the string wholesale into the browser.
WebBrowser
NavigateToString
Markdown.cs
MarkdownParser.cs
The updating was also fairly simple. The preview window is updated a) synchronously when a markdown document gains focus and b) asynchronously after 500 milliseconds of contiguous idle (the effect is that it doesn't update if you are typing fast enough, but updates when you stop typing). In this way, you don't really have to wait around for it to update, and it feels like it updates when you've finished a thought. The effect can be slightly annoying at times; the images load asynchronously after the rest of the page has displayed, so any images themselves flicker. Also, the scrolling happens when the LoadCompleted event is sent out, which happens only after all the images have finished downloading, so adding images at all causes the rest of the scrolling to appear jumpy as well. Somewhat disconcerting, but not enough of an issue to reduce the utility of the tool window greatly.
LoadCompleted
The only tricky part was the reload behavior; I wanted the preview window to maintain approximately the same scroll position when refreshed, so you don't have to continue scrolling down to the text you want every time you type. Because the WebBrowser is just a really thin wrapper on top of some mshtml ugliness, the code to do this is equivalently ugly. If you are morbidly curious, you can look at PreviewToolWindow.cs, but don't stare too long. You may lose your eyesight.
mshtml
Ideally, during the reload-from-typing case, the window would scroll to match the editor's scroll position. This hopefully wouldn't be too awful, as I could try to inject an anchor tag of some sort either where the text changed or for something like the middle line visible in the editor, plus some JavaScript for scrolling to that anchor position on load. Maybe I'll play around with that when I finish some of the other work.
Which brings me to the hard part of what I had to do – making a tool window.
Usually, this isn't all that bad. The SDK comes with project/wizard that walks you through a package with a tool window, but the difficulty is in combining a package and an editor extension.
The easy part of this feature (using WebBrowser, figuring out how to update things, and even figuring out the scrolling issue) took maybe 15 minutes. This part (figuring out packages) was about 4 hours.
For the sake of anyone else doing this, I'll describe the hurdles I tripped over, and how to get over them without extensive bodily harm:
First, to note: if you are going to do this, it is probably best to create a Package first and then add the editor extensibility part. If you create the package first, the only thing you need to do (besides adding references for whatever editor assemblies you need) is add a line to your source.extension.vsixmanfiest, in the <Content> section, that reads:
source.extension.vsixmanfiest
<Content>
<MefComponent>|YourProjectName|</MefComponent>
Going in the other direction, as you'll see, is no small feat. As such, this information is more of a get-out-of-trouble-once-you-are-there, and not something you should plan to do.
However, even if you are doing it "the right way", you still may run into issue #5, which will hopefully not be a problem post-Beta 2.
If you don't want to read the complete story, here's a quick list of the issues with links to the fixes/workarounds:
.vsct
No resource file set the resource merger.
.pkgdef
CreatePkgDef
LoaderExceptions
So, the first thing I started with was creating the default Package project, with a tool window, so that I could copy over the pieces I needed into my existing project. Right away I ran into the first problem, which is that this was throwing an exception every time I did it. I've heard from a few other people that I'm not the only one that ran into this, so it may just be an issue with Beta 2.
Issue #1: can't build default VS Package project to use as a reference for your editor extension project transformation. Work around: Look at the Tool Windows SDK samples online. These built just fine on my Beta 2 drop.
Issue #1: can't build default VS Package project to use as a reference for your editor extension project transformation.
Work around: Look at the Tool Windows SDK samples online. These built just fine on my Beta 2 drop.
Ok, now I've found the content I need, and it looks like I'll need to grab a few things from the project:
FooPackage.cs
FooToolWindow.cs
FooCommands.vsct
Resources.resx
bmp
All well and good, until you realize (40 minutes later, for me) that the .vsct file isn't actually being compiled correctly.
Issue #2: just adding a .vsct isn't enough to have it actually compiled as a .vsct. Fix: Make sure the Build Action for the .vsct file is VSCTCompile. You can change this in the Properties tool window (with that file selected).
Issue #2: just adding a .vsct isn't enough to have it actually compiled as a .vsct.
Fix: Make sure the Build Action for the .vsct file is VSCTCompile. You can change this in the Properties tool window (with that file selected).
Build Action
VSCTCompile
We're almost to building, except that now I get some error about "No resource file set the resource merger." This is the only problem that I solved relatively quickly, thanks to Google and someone who filed a Connect bug about it (with the work-around):
Issue #3: build fails, error is "No resource file set the resource merger." Fix: add <MergeWithCTO>true</MergeWithCTO> to the project file under the section for the .resx file.
Issue #3: build fails, error is "No resource file set the resource merger."
Fix: add <MergeWithCTO>true</MergeWithCTO> to the project file under the section for the .resx file.
<MergeWithCTO>true</MergeWithCTO>
.resx
Getting closer now. I ran into issues where it couldn't find one of the .bmp files that I had copied over (even though I made sure the resources file referenced it and the paths were correct and such), so this is when I ended up just removing those files entirely and references to these icons in the .vsct.
.bmp
After another hour or so of trying to figure out why the Package still wasn't getting loaded, I realized that my project wasn't creating a .pkgdef file, which is required for the package to set the correct registry keys for VS to know about it. Of this hour of searching, about 15 minutes was spent trying to find and remove a special little property defined in the .csproj file:
.csproj
Issue #4: Project isn't creating a pkgdef file. Fix: Find and remove the line in the .csproj file that defines the property GeneratePkgDefFile as false.
Issue #4: Project isn't creating a pkgdef file.
Fix: Find and remove the line in the .csproj file that defines the property GeneratePkgDefFile as false.
GeneratePkgDefFile
false
I'm guessing that property is set for all the projects that are editor extensions, since they don't require .pkgdef files (the way we use MEF doesn't require it). I would have expected CreatePkgDef (or the msbuild task) to be smart enough to run anyways but not actually generate a .pkgdef unless you need it, but I guess that isn't how it works. At least now you know what to look for.
At this point, the CreatePkgDef step was running, but it wouldn't complete successfully and my build would fail. It mentioned something about checking the LoaderException property (which generally means that some assemblies weren't found), but I'd have to debug that tool to figure it out, and the build output was thoroughly unhelpful.
LoaderException
To get some more information, I tried running the tool manually (CreatePkgDef.exe comes with the SDK), and discovered that the editor assemblies were all failing to be found. I'm not sure why, but it seemed like CreatePkgDef apparently was using a different assembly search path than the regular build was, and these assemblies were added as references without any explicit path (just Microsoft.VisualStudio.Foo in the .csproj file). Also, at least for Beta 2, these assemblies are not in the GAC, so whatever you are building needs to know to look in the SDK to find them.
CreatePkgDef.exe
Microsoft.VisualStudio.Foo
The workaround for this one felt really hacky, but it got me past this final issue:
Issue #5: CreatePkgDef unable to run for projects that reference editor assemblies without qualifying paths (even though the rest of the build steps complete successfully). Fix: Mark all the editor assemblies as CopyLocal = true in the properties page for each assembly. They won't be included in the .vsix (which would be undesirable), but they will be around for CreatePkgDef to find.
Issue #5: CreatePkgDef unable to run for projects that reference editor assemblies without qualifying paths (even though the rest of the build steps complete successfully).
Fix: Mark all the editor assemblies as CopyLocal = true in the properties page for each assembly. They won't be included in the .vsix (which would be undesirable), but they will be around for CreatePkgDef to find.
CopyLocal
true
.vsix
After that, CreatePkgDef could run and the Package was loaded successfully. The only other somewhat awkward code I had to write was the code that allowed the MEF component (namely, the margin) to access the preview window. It ended up being fairly painless (just using IVsShell to access/load the package), though that was after an initial step of creating an intermediate MEF service that MarkdownPackage would register with on creation. In the end, it all looks deceptively simple.
IVsShell
MarkdownPackage
Issue #0: Started with an editor extension, and tried to migrate VS Package project files over to the editor extension. Fix: Don't do that. Either start with a VS Package project, if you think you will need one, or migrate your editor extensions to the VS Package project when you discover that you do.
Issue #0: Started with an editor extension, and tried to migrate VS Package project files over to the editor extension.
Fix: Don't do that. Either start with a VS Package project, if you think you will need one, or migrate your editor extensions to the VS Package project when you discover that you do.
This was a thoroughly frustrating experience, and a reminder that there is still a lot of work for us to do to make extensibility follow the "pit of success" pattern. This was a common saying of Rico Mariani, which is to say that success should be so automatic that it is like falling into a pit, instead of something you have to strive to accomplish. Hopefully, though, my struggles in getting this work will get at least one person unstuck in the future (fingers are crossed).
Going forward with the list, I've realized that the inline images step just isn't necessary when you can see the image you've linked in the preview pane right next to what you are typing. As such, I'm putting it at the bottom (grayed out), though not necessarily crossing it out yet.
At this point, the only real feature that I'm truly missing (from my vim workflow) is spell checking support, which should hopefully just be a matter of me finding an extension, instead of writing one.
From now on, then, I'll be working on value-added features, relative to my old workflow, instead of just matching features. I also think that the other ideas I've listed are of much smaller value than what I've done so far, so I'll probably complete these more for the value of the examples than for the actual benefit to my workflow.
I've also added a bullet point for the margin work. It needs some cleanup, pretty icons, and various other options could be helpful (such as being able to disable the auto-update, for if you are using remote desktop or you just don't want to see it all the time). I'm hoping to get some help with this, since I'm pretty awful at anything interface related (my color choice is also pretty bad, if you haven't figured out yet from the screenshots).
And, just to add again – if you have any ideas for what you'd like to see added, let me know in the comments.
And here's the list!
ContentType