November 23rd, 2009

What's new for editor extenders in Beta 2?

Noah Richards Noah Richards – Developer, Visual Studio Editor Team
Short Bio:  Noah has been working on the Visual Studio Editor team since he graduated college two and a half years ago.  He maintains a blog, posts sample code for editor extensions, and frequently answers editor-related questions on Twitter.

Editor’s Note:  This post was originally published on Noah’s personal blog, which we’d encourage you to follow for additional content about the new VS 2010 editor and its extensibility.

Now that Visual Studio 2010 Beta 2 has been out for about a month, most of you have probably seen it and tried many of its new features.  For those interested in writing extensions for VS, we wanted to share a couple of updates around editor extensibility, particularly for people who wrote extensions in Beta 1 and want to upgrade to Beta 2.

1 – No More IEnvironment

If you wrote any code that had classifiers or taggers or various other extensions that take an IEnvironment parameter, you’ll be deleting those parameters in Beta 2. IEnvironment had a few historical reasons for being there (back when the editor was a part of a different project and not in Visual Studio yet), but it wasn’t really serving a purpose anymore. In Beta 1, all it really did was confuse people and make you type an extra few characters and/or add an FxCop suppression since you weren’t using it. This is a small change but a simplifying one. (If you are curious about the one place that did use it for taggers, see #5 below).

2 – Mouse and key processors get more metadata

In Beta 2, mouse and key processor providers take the following metadata; note that some of these were required for mouse or key processors, but now the list applies to both:

  1. Export, like all components
  2. ContentType
  3. Name and Order, so that you can position yourself before or after other known handlers. This is important in cases where you want to preempt whatever normally happens in a certain event.
  4. TextViewRole

Also, somewhat tangentially, the ContentType attribute now applies to any buffer in a view’s graph – that’s just a fancy way of saying that if you have something like an ASP.NET page that has embedded C#, your key or mouse processors will get loaded if they have [ContentType("CSharp")].

These fixes were necessary to write the triple click extension, which selects a line on triple click (you can find its code on github and its binaries on the VS Gallery). Without the ordering, I couldn’t put the mouse handler early enough to work correctly. Also, there were a few bugs in some of the default mouse handlers that have since been fixed.

Note that, unfortunately, the general guidance is to not use keyboard providers. The VS command system and the way components like the editor plug in don’t play very well with normal WPF keyboard input, so the short story is that you’ll only get keyboard events for key combinations that aren’t already bound to a command. Which leads me to the next new thing…

3 – Listening for the creation of IVsTextView adapters (IVsTextViewCreationListener)

We added this as an extension to the VS integration piece of the editor to allow people to listen for the creation of IVsTextView (which is what the underlying IWpfTextView /ITextView are wrapped in for the sake of people who still use the existing VS API). It’s effectively identical to IWpfTextViewCreationListener, except that you get an IVsTextView instead of an IWpfTextView when called.

A quick note – if you want to use this extension, you’ll need to add a reference to Microsoft.VisualStudio.Editor.dll. It’s in the SDK, so that’s not a problem, but it isn’t a part of any of the standard editor project templates.

The biggest reason we introduced this was for extensions that want to add command handlers to the editor. You could do this in the past, somewhat painfully, by doing the following:

  1. Write an IWpfTextViewCreationListener
  2. The ugly step – listen for the GotAggregateFocus event on the text view and do the remaining steps in that handler. If you skipped this step, the eventually attempt at getting the IVsTextView would fail, since the two objects weren’t fully hooked up yet.
  3. With an [Import]ed IVsEditorAdaptersFactoryService, call GetViewAdapter to get the IVsTextView.
  4. Call IVsTextView.AddCommandFilter
  5. …and don’t forget to unsubscribe from GotAggregateFocus or use a flag so that you only listen to the first time the event is raised.

With the new event, you just need to:

  1. Write an IVsTextViewCreationListener
  2. Call IVsTextView.AddCommandFilter

If you do want to get the associated IWpfTextView for the IVsTextView adapter, you can do the opposite of #3 above (using GetWpfTextView); the upside, relative to the above list, is that you don’t need to worry about initialization being finished. When you get the call in your creation listener, initialization has completed to the point that GetWpfTextView will always succeed.

Because command filters are still not entirely obvious to get correct, I’m working on a template to do this for you. More in general, I’m working on a set of “New Item” templates for the editor, so that you can add new components (or at least a skeleton of a new component) with a couple clicks in an existing project. I haven’t finished these yet, but you can see the work in progress on CommandFilter.cs.

4 – Custom text marker visual definitions

One of the other things missing in Beta 1 was the ability to set the visualizations for text markers (the markers you can create with an ITagger<TextMarkerTag>). There were some built-in marker types, but they were somewhat limited.

In Beta 2, that’s now changed, and you can export a MarkerFormatDefinition that looks something like this:

Export(typeof(EditorFormatDefinition))]
[Name(“mymarker”)]

internal sealed class MyMarkerDefinition : MarkerFormatDefinition
{
    public MyMarkerDefinition()
    {
        this.ZOrder = 1;
        this.Fill = Brushes.Blue;
        this.Border = new Pen(Brushes.DarkGray, 0.5);
        this.Fill.Freeze();
        this.Border.Freeze();
    }
}

And then create a TextMarkerTag with the name “mymarker”, and you are all set. Note that you’ll probably want some transparency on your fill brush, since the selection is drawn underneath markers.

5 – View tagger provider (IViewTaggerProvider)

In Beta 1, you exported your taggers with an ITaggerProvider. In Beta 2, there is an additional interface you can use called IViewTaggerProvider, which is passed both a buffer and a view in its CreateTagger method. There are a few cases where you may want to use this:

  1. Your tagger needs information specific to a certain view – this may be the case if you are writing an extension that wants to, say, highlight all references in a view that match the reference under the caret. In this case, you need to know the caret position in the view to produce your results.
  2. Your tagger wants to produce different results depending on the view – this is basically the same as #1, but it may help to also think of it in this way. With the highlight references example again, it’s possible to have two different views over the same buffer (split window, for example, or the code definition window), where you want each view to show different results for the highlighted references, even though they are displaying the content of the same text buffer.
  3. Your tagger wants to consume other tag information – this is a more complex case that probably deserves its own blog article. Basically, if you have a tagger for type T that consumes, say, classification tags, you want to avoid accidentally including a classifier that is trying to consume tags of type T, or else you’ll end up with an ugly recursive loop when trying to get tags. You can sidestep this by providing your tagger with an IViewTaggerProvider and consuming taggers (or classifiers) you get from an IBufferTagAggregatorFactoryService (or IClassifierAggregatorService.GetClassifier that takes a buffer). That way, you are guaranteed that any taggers/classifiers you may consume can’t accidentally consume your extension as well.

The downside to #3 is that it is still hairy for other components to safely consume your tagger (that they are using via IViewTagAggregatorFactoryService) to produce tags; if two view-level taggers are each consuming other view-level taggers, you get the same problem as before. As such, any component you make that does this should be created with the understanding that it can’t be safely consumed. For any extensions you write, you should probably steer clear of creating view-level taggers that consume a view-level ITagAggregator<ClassificationTag>, for example. In general, it’s probably safest to follow this rule:

Never create a tag aggregator using an IViewTagAggregatorFactoryService inside a tagger provided with an IViewTaggerProvider.

…and more

This is not an exhaustive list; these are just the biggest ones I can think of off the top of my head. We’ll continue to post more editor and extensibility content on the Visual Studio Blog and my blog, so stay tuned.  And as always, comments are welcome.

– Noah

Author

Visual Studio has evolved from a simple tool bundle into an intelligent, all-in-one development environment. With support for coding in any language on any device, integrated AI to streamline workflows, and seamless cloud scalability, it empowers developers to innovate, deliver faster, and build the future.

0 comments

Discussion are closed.