How to write a VS2010 Extension using Statement Lambdas

Here's how to write a VS2010 extension (".vsix") in Visual Basic. The code is made a lot easier thanks to one of the major new features in Visual Basic 10, "statement lambdas".

  • This extension renders comments in a new typeface, Comic Sans Bold Italic.
  • You can write extensions that put any kind of WPF graphical effect in the buffer.
  • [I've made a channel9 video to accompany this blogpost, and it's in the middle of processing....]
  • Source code: ComicComments-src.zip [9k]
  • Prebuilt binary: ComicComments.vsix [26k]

 

0. PRELIMINARIES 

Install the VS2010 SDK from here: https://www.microsoft.com/downloads/details.aspx?FamilyID=47305cf4-2bea-43c0-91cd-1b853602dcc5&displaylang=en

MSDN documentation about it is here: https://msdn.microsoft.com/en-us/library/bb166441(VS.80).aspx

 

1. CREATE THE PROJECT

File > New > Project > Extensibility > EditorClassifier

This creates a new project. It created it with three source files:
  - EditorClassifier1.vb
  - EditorClassifier1Format.vb
  - EditorClassifier1Type.vb

This was a bit more complexity than we need. I deleted the last two from the project, and replaced the source code of EditorClassifier1.vb with just the following:

Imports System.ComponentModel.Composition

Imports System.Windows

Imports System.Windows.Media

Imports Microsoft.VisualStudio.Text.Classification

Imports Microsoft.VisualStudio.Text.Editor

Imports Microsoft.VisualStudio.Utilities

<Export(GetType(IWpfTextViewCreationListener)), ContentType("code"), TextViewRole(PredefinedTextViewRoles.Document)>

Class ViewCreationListener : Implements IWpfTextViewCreationListener

    <Import()> Dim formatMapService As IClassificationFormatMapService

    <Import()> Dim typeRegistry As IClassificationTypeRegistryService

    Public Sub TextViewCreated(ByVal view As IWpfTextView) Implements IWpfTextViewCreationListener.TextViewCreated

    End Sub

End Class

The <Export> and <Import> attributes show you that the extension is a MEF component. The long and short of it is that our code TextViewCreated will be invoked once for each new code-editor buffer.

 

2. CHANGE THE FORMAT MAP: FIRST ATTEMPT

Here's the first attempt at implementing TextViewCreated:

Public Sub TextViewCreated(ByVal view As IWpfTextView) Implements IWpfTextViewCreationListener.TextViewCreated

    Dim formatMap = formatMapService.GetClassificationFormatMap(view)

    Dim textClassification = typeRegistry.GetClassificationType("text")

    Dim textProperties = formatMap.GetTextProperties(textClassification)

    Dim commentClassification = typeRegistry.GetClassificationType("comment")

    Dim commentProperties = formatMap.GetTextProperties(commentClassification)

    Dim commentTypeface2 = New Typeface(New FontFamily("Comic Sans"), FontStyles.Italic, FontWeights.Bold, commentProperties.Typeface.Stretch)

    Dim commentEmSize2 = textProperties.FontRenderingEmSize + 1

    Dim commentProperties2 = commentProperties.SetTypeface(commentTypeface2).SetFontRenderingEmSize(commentEmSize2)

    formatMap.SetTextProperties(commentClassification, commentProperties2)

End Sub

 

Each language classifies the text in the buffer, as "comment" or "text" or "typename". The formatMap is a map from classification to typeface. The line in yellow is where we update the map.

 

3. CHANGE THE FORMAT MAP: FINAL ATTEMPT

There are some problems with the above code. The first problem is that TextViewChanged can be called more than once, but the highlighted line of code is quite costly -- it causes an entire UI refresh. We can avoid this cost by only changing the map if necessary:

If Not commentProperties.Equals(commentProperties2) Then

    formatMap.SetTextProperties(commentClassification, commentProperties2)

End If

The second problem is that sometimes TextViewChanged isn't called at the right time. It might be called too early, for instance. To make it more robust we should also update the formatMap the first time the window gets focus, in response to the view.GotAggregateFocus event.

The third problem is that, if some other plugin made a change to the formatMap, or if the user changed font size for instance, then we need to redo our work on the formatMap. The right place to do this is in response to the formatMap.ClassificationFormatMappingChanged event.

The final problem is that if we respond to the formatMap.ClassificationFormatMappingChanged event by calling formatMap.SetTextProperties, then that might itself trigger the formatMap.ClassificationFormatMappingChanged event again! So we need to protect against being recursively called by ourselves.

 

All these problems can be elegantly solved with a multi-line statement lambda. This is how:

Public Sub TextViewCreated(ByVal view As IWpfTextView) Implements IWpfTextViewCreationListener.TextViewCreated

    Dim formatMap = formatMapService.GetClassificationFormatMap(view)

    Dim inUpdate = False

    Dim FixComments =

        Sub()

            If inUpdate Then Return Else inUpdate = True

            Dim textClassification = typeRegistry.GetClassificationType("text")

            Dim textProperties = formatMap.GetTextProperties(textClassification)

            Dim commentClassification = typeRegistry.GetClassificationType("comment")

            Dim commentProperties = formatMap.GetTextProperties(commentClassification)

            Dim commentTypeface2 = New Typeface(New FontFamily("Comic Sans"), FontStyles.Italic, FontWeights.Bold, commentProperties.Typeface.Stretch)

            Dim commentEmSize2 = textProperties.FontRenderingEmSize + 1

            Dim commentProperties2 = commentProperties.SetTypeface(commentTypeface2).SetFontRenderingEmSize(commentEmSize2)

            If Not commentProperties.Equals(commentProperties2) Then

                formatMap.SetTextProperties(commentClassification, commentProperties2)

            End If

            inUpdate = False

        End Sub

    AddHandler view.GotAggregateFocus, FixComments

    AddHandler formatMap.ClassificationFormatMappingChanged, FixComments

    FixComments()

End Sub

 

The yellow highlights show the key changes.

What's good about using a lambda for this is that we can keep everything local to the place where it's needed. That's because the FixComments lambda is able to use the various fields (view, formatMap, inUpdate) directly. If we tried to turn FixComments into a normal named method, then we'd have to create a class which had those three things as members, and we'd have to write a constructor for it, and the code would start to look messy.

 

4. DEBUGGING AND DEPLOYING

source.extensions.vsixmanifest -- this file contains name and other information about your plugin. You must supply some additional files:
  - some kind of "license.txt" of your chosing
  - an icon 32x32 pixels big (I use PNG format)
  - another icon 200x200 pixels big (again I use PNG)

To debug, just press F5 as normal. This will launch a version of VS2010 with your extension installed. However, it installs it into an "Experimental" folder
   C:\Users\lwischik\AppData\Local\Microsoft\VisualStudio\10.0Exp
rather than the more usual
   C:\Users\lwischik\AppData\Local\Microsoft\VisualStudio\10.0
All new editor-classification projects have been set up to debug by launching VS with the command-line argument "/rootsuffix Exp", which is why it looked in that experimental folder. This is so that you can debug without compromising the real VS.

Once it's finished, you can deploy. The extension is packaged up in your EditorClassifier1\bin\Debug folder in a file called
  - EditorClassifier1.vsix

Any user can double-click on this .vsix to install the extension. Internally, it's really just a .zip file that contains the necessary files.

 

ComicComments-src.zip