Sunday, 12 October 2008

My first Visual Studio Plugin - CullWindows

okay, that was hell...

In the .net world I usually work with the wonder tool that is Resharper. For this particular story, the main points are the hugely useful navigation shortcuts: Go to symbol, Go to file, Go to type, Navigate to implementation, Navigate to base, you get the picture.

So, the Solution Explorer window usually doesn't get much work, but the tab strip on top is cluttered beyond recognition even after just half an hour or two of work. And while most of the source navigation is done via shortcuts, sometimes it's handy to just click on the respective tab. Just try and find it amidst 20 other files.

Hence, my idea to expand Visual Studio with the simple ability to keep just the top X files I use.

Meet CullWindows. Direct download link here, google code home page here. A simple solution to a simple problem. It was the implementation that was hell.

The Plugin

The plugin's logic is quite simple. Keep track of which files are opened for editing. When the user views a file, see if we've hit the limit. If we have, close documents, from last to first, until we're back on the limit. Ignore unsaved files, and things that are not files.

The limit is configured on the Visual Studio Options panel, under Cull Windows.

The installed is "dumb", meaning it won't give you any feedback, and just install and go away. I'm sorry for that.

For now the plugin is Visual Studio 2008 only, no version for 2005, mostly because I don't use it. If there's any demand, I may give it a try.

The Implementation

Firstly, I created an add-in. Seeing an example that came with the SDK, I managed to get a reference to the Running Documents Table, which contains all currently opened documents, and registered an event sink to listen for changes in the table.

This was all good until I get home and try the same code on another computer. There, the table was nowhere to be found, and found no error. I could've tried to debug it for some time, but it seemed better to just restart it.

Next up, a VSPackage. From what I had gathered, it is a "new" and more powerful way to extend VS, and the API and samples seemed to be a bit more OO. I managed to get it to work, again, but now the debugging is a bit stranger than when it was an add-in, since it now needs to be installed on the registry hive for the IDE.

Most of the interaction is a bit nasty from someone used to clean, object oriented APIs. It seems most of the extensibility points in Visual studio are done via COM, which is a beast I've encountered few times before, and I dont' keep fond memories from those occasions. It may be that I'm too new working with it, but the code that resulted wasn't exactly... pleasant. Feel free to browse the code. It isn't pretty, specially the first iterations, but I'm open to suggestions and criticisms.

Now, the API is HUGE. There are hundreds of interfaces on each of the Visual Studio namespaces, and trying to get from one point to another is somewhat tiring. I have a document cookie from the event handler, now how to get the name of the file. Okay, have the name, now how can I find if it is modified. Where to keep the preferences, and how to access them. There were some concepts which I lacked, but mostly it was just be being a newbie at it and not knowing where to look.

The solution ended up being simply 3 files: the package, the document monitor and the options page.

The package was built mostly by the VSPackage template, and the only things I changed was to remove the menu item, add the option pane and build the document monitor. Some of the attribute values were a matter of faith on the documentation, and I didn't dare touch most of the generated code.

The document monitor does the grunt work of maintaining a document list, ordered by access date, listening to the running document table events, and closing documents when the limit is reached. The first implementation was fetching the window frames from the UI shell and iterating through it to find the one with the file to close. Not optimal by a long shot, but worked. A second (and final) iteration used the IsDocumentOpen method from VsShellUtilities to get the window frame. This was not intuitive. I expected a method starting with Is to just return true or false, and not return more information.

The option page was actually the easier part, with all of the work taken care of by the DialogPage class, including persistence. It's a bit too much magic for my tastes, but it works. I was supposed to access the options via DTE, but it kept throwing an invalid cast exception, and I gave up and just passed the option page object to the monitor.

The Next Steps

The first implementation is done, but I think I can make it a bit smarter if I have the time:

  • Ponder the amount of time spent on a given file when picking which one to remove. This would prevent newer files which were opened by mistake to stay opened when earlier files which had more use go away.
  • Keep a background timer to cull windows after some time with no visits to the document.
  • Have the installed say something instead of just installing and going away.

I guess that's all for now, thanks for tuning in.


Ilya Ryzhenkov said...

And all that just because you don't use "Recent Files" and "Recent Edits"? :) Combine it with "reuse current document window" setting in Tools/Options/Environment/Documentsand you probably don't need such plugin.

Bruno said...

I do use both of those, but my ideia was to remove the tab clutter.
Which may be done just nicely by that setting in particular. Now I feel silly.
On the other hand, the process itself was enlightening, the knowledge will surely be used in the future, and I gained an even deeper respect for your work :)