Sam's Blog

25 Jan

Creating a WPF Tool Window for Visual Studio 2010

I’ve been working on Visual Studio 2010 extensibility for some time now, and I must say that creating a tool window was not the easiest task in the world.

First Attempt: Create an MEF-friendly IToolWindowService

The original goal of this project was creating an MEF service that allowed exporting classes implementing IToolWindowProvider, and have the service manage the creation of the tool windows and their entries on the View > Other Windows menu. This worked, but had several drawbacks that eventually led to the conclusion that this was the wrong approach. First and foremost, this method forced the assemblies providing tool windows to load even when the tool windows weren’t visible. The performance implications of this rule out using MEF as a general solution to this problem. That said, here are some other “little things” that I didn’t have worked out in the MEF solution:

  • Tool windows would not save their visible state; they were always hidden the next time Visual Studio was opened.
  • The menu commands to show the tool windows were not available until a source file of {insert some hard-coded type} was opened. This is because the service loaded itself by exporting IVsTextViewCreationListener. When I tried to use the “any” content type, I found that the service would cause an InvalidOperationException when the environment loaded because the tool windows were created while the environment was using an IEnumerator to traverse the current windows.

Overview of the Improved Process

Here is an outline of the general process of creating a new tool window. Following the outline, I’ll explain in detail what each step requires.

  • Create a VSIX project
  • Configure the project to offer a VSPackage and a command table
  • Create the WPF control your tool window will be displaying
  • Include my helper class WpfToolWindowPane in your project
  • Derive a class from WpfToolWindowPane for your tool window
  • Set up menu commands

Create a VSIX project

I’m basing this off of the VSIX project, because it’s a nice clean project template. It’s also beneficial because users that already have a VSIX project can easily add a tool window to it.

  1. Create a project using the “VSIX Project” template (Visual C# > Extensibility). Choose a different solution name than project name or you’ll run into issues later.
  2. Delete the file VSIXProject.cs.
  3. Right click the file “source.extension.manifest” and select View Code. Update the Name, Author, and Description fields with information about your extension. Save and close the file.
  4. Right click the project in Solution Explorer and select Unload Project. Then right click it again and select “Edit MyProject.csproj”
  5. Locate the line that says <IncludeAssemblyInVSIXContainer>false</IncludeAssemblyInVSIXContainer> and change the value to true. Save and close the file.
  6. Right click the project in Solution Explorer and select Reload Project. If Visual Studio gives an error about a project with the same name already being open, restart Visual Studio and try again.

Configure the project to offer a VSPackage and a command table

  1. Add an XML file to the project named “MyProject.vsct”. In the properties pane for it, change the Build Action to VSCTCompile.
  2. Add a resources file to the project named “VSPackage.resx” (use exactly that name). Open the file in the resources editor, and at the top set the Access Modifier to No code generation. Save and close the file.
  3. Unload the project again and open it for editing.
  4. Set GeneratePkgDefFile to true.
  5. Below the line with GeneratePkgDefFile, add <RegisterWithCodebase>true</RegisterWithCodebase>.
  6. Set CopyBuildOutputToOutputDirectory to true.
  7. Locate the line containing <VSCTCompile Include="MyProject.vsct"/> and change it to <VSCTCompile Include="MyProject.vsct"><ResourceName>1000</ResourceName><SubType>Designer</SubType></VSCTCompile> .
  8. Find the line containing <EmbeddedResource Include="VSPackage.resx">, and add both <MergeWithCTO>true</MergeWithCTO> and <LogicalName>VSPackage.resources</LogicalName> as children.
  9. Save, close, and reload the project file.
  10. Add a new class to the project named MyProjectPackage. Derive the class from Microsoft.VisualStudio.Shell.Package. Set the class GUID by adding the attribute [Guid("00000000-0000-0000-0000-000000000000")] (use the Create GUID tool to create the actual value you’ll use here). Add the attribute [PackageRegistration(UseManagedResourcesOnly = true)] to tell Visual Studio to register the class as a VSPackage. Provide the command table (vsct) by adding the attribute [ProvideMenuResource(1000, 1)].

Create the WPF control your tool window will be displaying

For this part, you can create any control derived directly or indirectly from System.Windows.Control.

Include the helper class WpfToolWindowPane in your project

This helper class handles most of the code required for providing a tool window. You can download WpfToolWindowPane.cs here.

Derive a class from WpfToolWindowPane for your tool window

  1. Add a new class named ToolNameToolWindowPane to your project and derive it from WpfToolWindowPane. Use the Create GUID tool to create a guid for ToolNameToolWindowPane, and add a GuidAttribute to it.
  2. In the constructor, set the Caption of your tool window with base.Caption = "Tool Window Name"; .
  3. Override the CreateToolWindowControl() method to return a new instance of the control you created earlier. The method can be as simple as return new ToolNameControl(base.GlobalServiceProvider); .

Set up menu commands

The last step is adding a command to the View > Other Windows menu so your tool window can be opened.

Setting up the command table

Open the MyProject.vsct file and fill it with the following. Use the same Guid for guidMyProjectPackage as you used for the MyProjectPackage class. For the other two placeholders, create new Guids.

< ?xml version="1.0" encoding="utf-8" ?>
<commandtable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable">
  <extern href="vsshlids.h"/>
  <commands package="guidMyProjectPackage">
    <buttons>
      <button guid="guidMyProjectPackageCmdSet" id="cmdidShowToolName" priority="0x100" type="Button">
        <!--<Icon guid="guidShowToolNameCmdBmp" id="bmpShowToolName"/>-->
        <strings>
          <buttontext>Tool Name</buttontext>
        </strings>
      </button>
    </buttons>
 
    <!--<Bitmaps>
      <bitmap guid="guidShowToolNameCmdBmp" href="Resources\ToolIcon.png" usedList="bmpShowToolName"/>
    -->
  </commands>
 
  <commandplacements>
    <commandplacement guid="guidMyProjectPackageCmdSet" id="cmdidShowToolName" priority="0x100">
      <parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1" />
    </commandplacement>
  </commandplacements>
 
  <symbols>
    <guidsymbol name="guidMyProjectPackage" value="{00000000-0000-0000-0000-000000000000}" />
 
    <guidsymbol name="guidMyProjectPackageCmdSet" value="{00000000-0000-0000-0000-000000000000}">
      <idsymbol name="cmdidShowToolName" value="0x2001" />
    </guidsymbol>
 
    <!--<GuidSymbol name="guidShowToolNameCmdBmp" value="{00000000-0000-0000-0000-000000000000}">
      <idsymbol name="bmpShowToolName" value="1" />
    -->
  </symbols>
</commandtable>

Add a Constants class to your project

Add a class named Constants to your project to make the Guids you used in the command table accessible to your code.

internal static class Constants
{
  public const int ToolWindowCommandId = 0x2001;
  public const string MyProjectPackageCmdSet = "{00000000-0000-0000-0000-000000000000}";
  public static readonly Guid GuidMyProjectPackageCmdSet = new Guid(MyProjectPackageCmdSet);
}

Handling the menu command

Open the MyProjectPackage class. Add a new attribute to provide your tool window: [ProvideToolWindow(typeof(ToolNameToolWindowPane))]. Add the following code to the body of the class:

protected override void Initialize()
{
  base.Initialize();
  WpfToolWindowPane.ProvideToolWindowCommand<ToolNameToolWindowPane>(this, Constants.GuidMyProjectPackageCmdSet, Constants.ToolWindowCommandId);
}
22 Jan

Commentary on Parsing Languages for IDEs

I originally sent this to the ANTLR interest group, but figured it would make for a decent post here as well.

I’ll start with a couple fundamental concepts. First, there are three key things that get parsed in an IDE:

  1. The syntax highlighter lexes the current document.
  2. Files that are not opened in the editor.
  3. Files that are open (for editing) in the editor.

Second, the very nature of writing code results in semantically incorrect (invalid) code *almost always*, and syntactically incorrect code most of the time. Very often, the code is in a state that it can’t even be lexed by the language’s lexer. It is critical for each of the above cases to absolutely minimize the impact on the ability to provide meaningful coding assistance. This point is the fundamental reason I believe incremental lexers and parsers are significantly less valuable to an IDE than is often believed.

Here are several things to remember about each of the three "parsables".

Syntax Highlighting

Syntax highlighting is the only item above that requires a real-time component. The syntax highlighter must be able to perform a standard view update in less than 20ms for any character input. The easiest way to accomplish this is writing a "lightweight" lexer that, given any input that starts at a token boundary can tokenize the remaining text. The backing engine for my current syntax highlighters maintains a list of lines that do not start with a token boundary – for many languages this occurs for the 2nd and following lines of a multi-line comment. I can start lexing at any line in the entire document as long as it isn’t contained in this list, and can stop under a similar condition (the stop condition is slightly more complicated so I’ll leave it out). *The primary syntax highlighter must not perform any form of semantic analysis of the result.* A secondary highlighting component can asynchronously add highlighting to semantic elements such as semantic definitions, references, or other names.

Here are some basic things I do to improve the performance of the syntax highlighter’s lexer:

  1. Do not lex language keywords that look like an identifier. Instead, when I assign colors to tokens I check if the identifier is in a hashtable of language keywords.
  2. Do not restrict escape sequences in strings literals to be valid. The only escape to handle is \" to not close the string. If strings cannot span multiple lines in your language, then treat an unclosed string literal as ending at the end of the current line.
  3. If character literals start and end with, say, an apostrophe (‘) and no other literal in the language uses an apostrophe, then treat character literals like you treat strings (allow any number of characters inside them, and make sure to stop the token at the end of a line if it’s missing a closing apostrophe.
  4. Make sure the lexer is strictly LL(1).
  5. Include the following rule at the end of the grammar: ANY_CHAR : . {Skip();};

Unopened Files In The Project

For any block of language code that does not affect other open documents (such as the body of functions in C), don’t validate the contents of a block. For C, this could mean parsing the body of a method as:

body:block
block:'{' (~('{'|'}') | block)* '}';

Often, all you care about are the declarations and definition headers. This sort of "loose" parsing prevents most syntax errors in the body of methods from impacting the availability of the key information – references to the declarations are usable at other points in code. Further improvements can be made by forcing a block termination when a keyword is found that cannot appear in the block, but since this can cause some unexpected results and offers relatively low "bang for the buck", I recommend holding off on this approach.

Files Open For Editing

One of the most difficult aspects of an "intelligent" IDE is how to handle files while they are being edited. In designing an appropriate attack on the problem, it’s important to identify the types of information you can gather and for each: 1) Categorize it, 2) Give it a difficulty rating, 3) What can you do with it, and 4) Prioritize it. I’ll give several examples along with how you might leverage each to improve the overall usefulness of the IDE. Note that all of the below are performed asynchronously using a parameterized deferred action strategy described in a section below.

Information: Invalid literals (strings, numbers, or characters)

  • Category: Lexical errors
  • Difficulty: Given access to the tokens in the current document, very easy. Incremental by nature due to intentionally implementing it using the syntax highlighter’s lexer and strategy.
  • Action: Underline the token or part of a token that is invalid.
  • Priority: Useful information that is easy and computationally inexpensive to maintain. This might be implemented at a very early stage when, say, learning how to asynchronously "mark" text after it has been highlighted (information that is helpful for a number of more difficult features later).

Information: Language element blocks (perhaps method and field declarations, but not locals).

  • Category: Editor navigation
  • Difficulty: Easy to "first-pass" it by running the unopened files parser. For a high-quality product, you’ll use a different strategy that I’ll describe under the Advanced Strategies section below.
  • Action: Populate the "type and member dropdown bars" and the top of the editor, update a pane that shows a structural outline of the current file, and mark large blocks (say full methods) as collapsible.
  • Priority: This is a fundamental and relatively easy feature that should be implemented in some form soon after syntax highlighting is in place.

Information: Argument type mismatch

  • Category: Language compile-time semantics
  • Difficulty: Requires full type information for the target method and semantic details of the expression(s) passed as arguments. If you just use the language’s compiler for this information, the information will only be available when the compiler can get far enough to observe the problem. If you don’t have access to the compiler’s code, you might also be unable to turn off several compiler features that slow down the operation significantly.
  • Action: Underline the parameter(s) or the call with a tooltip or other description of the problem.
  • Priority: While quite useful in a statically-typed language, this is an extremely difficult feature to fully implement and is therefore relatively low on the priority list.

Auto-completion

I decided to address this separately from the above. Here are the governing factors an auto-complete feature:

  1. It must be fast (sub-50ms *latency*) – the user is actively waiting for the results.
  2. The document is never syntactically correct when this feature is used.

Core strategy: This is not complete, but gives a general idea of the initial approach that gives quite tolerable results.

Start at the cursor and read tokens in reverse "until they no longer affect the current location". For c-like languages, this means reading identifiers, periods, arrows (->), and parenthesis with arbitrary contents and nesting.

The result can generally be parsed as a postfix expression; the parser should be able to built an AST for just that postfix expression without any additional context.

Evaluate the AST against its previously cached context – use the buffer-mapped span of the enclosing language elements to evaluate the visibility of elements as you manually walk the expression’s AST. At each step, generate a list of visible elements, and select the appropriate item before continuing. At the end, you’ll have a list of accessible items at the point auto-complete was triggered – match that against any text the user has already typed and either fill in the single result or present a dropdown.

Advanced Parsing

(only one example here so far)

Robust and fast language element identification in the presence of errors:

Due to the way the unopened file parser skips parsing function bodies, it tends to be extremely fast. When it fails to parse a document, due to say a syntactically incorrect field declaration, you don’t want your opened files to stop showing navigation information. Upon failure, the opened file strategy can fall back to using an ANTLR fragment parser to locate the headers of language elements (class, field, or function definitions), from that information you can infer the scope of each element, and generally identify most or all of the items declared in a document.

After identifying elements by name and their "span" in the document, you’ll want to keep a list of the information around in case future edits render both of the above parsing methods unusable. When the header portion of a language element is deleted as part of a document edit, the element can be immediately removed from the cached list of elements located in the current file. Further, if the previous element had an inferred termination due to mismatched braces, its span can immediately be expanded to include any remaining portion of the original span of the removed language element. The cache relies on [buffer span mapping] to properly track the current location of each language element as edits are applied to the document.

Any time the IDE tries to offer information about a language element in an opened file, the information is checked against that documents cache for potential updates (for example, Go To Definition should always go to the correct location, even if a large block of text was pasted above the definition).

Deferrable Action Strategy

This strategy is basically a modified, asynchronous "dirty" flag based trigger for an arbitrary action. The strategy addresses several goals:

  1. Since the operation is not "free", you don’t want to repeat it too many times.
  2. Since the operation takes more time than the actions that necessitate the action (typing keystrokes takes less time than a re-parse), you want to A) run the action on a different thread and B) try to avoid running the action on old data.

Here’s the basic implementation:

  1. When the action’s target is initially marked dirty, a timer is set so the clean-up action will run on a separate thread after some period of time.
  2. When some operation occurs that *would* mark the target dirty, except the target is already dirty, the timer is deferred – reset to occur at a later time.
  3. Decide on a "cancellation policy":
    • When the action runs, does it mark the target clean for the state before running or the state after running? If it marks the target clean *after* running, then you can simply ignore a request to mark it dirty while running (this is the rare case though).
    • Are the results useful if the target is marked dirty after the action starts but before it finishes? If not, you want to cancel any currently running operation and go back to step 1 (or 2). If so, keep going and go to step 1 after it completes and mark the target dirty (and reset the timer). Normally this is a gray area – the results are partially useful so the decision is balanced in some manner between the speed of the operation, the rate at which things are marking the target dirty, and the usefulness of "stale" results (results that are available only after the target was marked dirty again).
17 Jan

Root Folders v. Window 7 Support

With the addition of Jump Lists to the taskbar in Windows 7, I couldn’t pass up creating a new root folders solution to leverage this organization on the Start Menu. By combining WPF and .NET 4’s new Windows Shell APIs, the result was remarkably simple. I’ve included the source code in the archive below if you want to see the details.

To use the application:

  1. Download the RootFolders7.zip (22kB) archive and extract the contents to some location (C:\Program Files\RootFolders perhaps?)
  2. Right click RootFolders.exe and select “Pin to Start Menu”.
  3. On the start menu, you can hold shift and right click the shortcut to rename it to “Root Folders” (adding a space).
  4. Run the application to configure your root folder targets, then select File > Save Changes.

Here is the configuration application and the result:

Configuration

Presentation

25 Nov

MEF support for the Output Window in Visual Studio 2010

As some people have noticed, the Visual Studio 2010 SDK doesn’t have great support for the Output Window from MEF extensions. This post aims to change that. Like several other components I’m writing about, I hope to provide this one in a “general extensibility helper” that could be installed separately from extensions that use it so its features can be shared among them.

There are a couple goals here:

  • Provide a service for accessing the output window panes
  • Provide a simple method of creating new output window panes

I’ll start with the usage (since that’s the interface people will normally see) and follow it with the implementation.

Defining an output window pane

Now this is easy.

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace JavaLanguageService
{
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Utilities;
    using JavaLanguageService.Panes;
 
    public static class Services
    {
        [Export]
        [Name("ANTLR IntelliSense Engine")]
        internal static OutputWindowDefinition AntlrIntellisenseOutputWindowDefinition;
    }
}

Using an output window

First you import the IOutputWindowService:

1
2
[Import]
internal IOutputWindowService OutputWindowService;

Then you use it to get an output window, which you can write to:

1
2
3
var outputWindow = OutputWindowService.TryGetPane("ANTLR IntelliSense Engine");
if (outputWindow != null)
    outputWindow.WriteLine(message);

If you want to write to one of the standard panes, pass one of the following to TryGetPane:

1
2
3
4
5
6
public static class PredefinedOutputWindowPanes
{
    public static readonly string General;
    public static readonly string Debug;
    public static readonly string Build;
}

The IOutputWindowPane and IOutputWindowService interfaces

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace JavaLanguageService.Panes
{
    using System;
 
    public interface IOutputWindowPane : IDisposable
    {
        string Name
        {
            get;
            set;
        }
 
        void Activate();
        void Hide();
        void Write(string text);
        void WriteLine(string text);
    }
}
1
2
3
4
5
6
7
namespace JavaLanguageService.Panes
{
    public interface IOutputWindowService
    {
        IOutputWindowPane TryGetPane(string name);
    }
}
1
2
3
4
5
6
namespace JavaLanguageService.Panes
{
    public sealed class OutputWindowDefinition
    {
    }
}
1
2
3
4
5
6
7
8
9
namespace JavaLanguageService.Panes
{
    public static class PredefinedOutputWindowPanes
    {
        public static readonly string General = "General";
        public static readonly string Debug = "Debug";
        public static readonly string Build = "Build";
    }
}

The internal implementation

1
2
3
4
5
6
7
8
9
10
namespace JavaLanguageService.Panes
{
    internal interface IOutputWindowDefinitionMetadata
    {
        string Name
        {
            get;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
namespace JavaLanguageService.Panes
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using JavaLanguageService.Extensions;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell.Interop;
    using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
 
    [Export(typeof(IOutputWindowService))]
    internal sealed class OutputWindowService : IOutputWindowService
    {
        [Import]
        public IServiceProvider GlobalServiceProvider;
 
        [ImportMany]
        internal List<Lazy<OutputWindowDefinition, IOutputWindowDefinitionMetadata>> OutputWindowDefinitions;
 
        private readonly Dictionary<string, Guid> _outputWindows =
            new Dictionary<string, Guid>()
            {
                { PredefinedOutputWindowPanes.Build, VSConstants.GUID_BuildOutputWindowPane },
                { PredefinedOutputWindowPanes.Debug, VSConstants.GUID_OutWindowDebugPane },
                { PredefinedOutputWindowPanes.General, VSConstants.GUID_OutWindowGeneralPane },
            };
 
        private readonly Dictionary<string, IOutputWindowPane> _panes = new Dictionary<string, IOutputWindowPane>();
 
        public IOutputWindowPane TryGetPane(string name)
        {
            IOutputWindowPane pane = null;
            if (_panes.TryGetValue(name, out pane))
                return pane;
 
            var olesp = (IOleServiceProvider)GlobalServiceProvider.GetService(typeof(IOleServiceProvider));
            var outputWindow = olesp.TryGetGlobalService<SVsOutputWindow, IVsOutputWindow>();
            if (outputWindow == null)
                return null;
 
            Guid guid;
            if (!_outputWindows.TryGetValue(name, out guid))
            {
                var definition = OutputWindowDefinitions.FirstOrDefault(lazy => lazy.Metadata.Name.Equals(name));
                if (definition == null)
                    return null;
 
                guid = Guid.NewGuid();
                // this controls whether the pane is listed in the output panes dropdown list, *not* whether the pane is initially selected
                bool visible = true;
                bool clearWithSolution = false;
 
                if (ErrorHandler.Failed(ErrorHandler.CallWithCOMConvention(() => outputWindow.CreatePane(ref guid, definition.Metadata.Name, Convert.ToInt32(visible), Convert.ToInt32(clearWithSolution)))))
                    return null;
 
                _outputWindows.Add(definition.Metadata.Name, guid);
            }
 
            IVsOutputWindowPane vspane = null;
            if (ErrorHandler.Failed(ErrorHandler.CallWithCOMConvention(() => outputWindow.GetPane(ref guid, out vspane))))
                return null;
 
            pane = new VsOutputWindowPaneAdapter(vspane);
            _panes[name] = pane;
            return pane;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
namespace JavaLanguageService.Panes
{
    using System;
    using System.Diagnostics.Contracts;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell.Interop;
 
    internal sealed class VsOutputWindowPaneAdapter : IOutputWindowPane, IDisposable
    {
        private IVsOutputWindowPane _pane;
 
        public VsOutputWindowPaneAdapter(IVsOutputWindowPane pane)
        {
            Contract.Requires<ArgumentNullException>(pane != null);
            this._pane = pane;
        }
 
        public string Name
        {
            get
            {
                string name = null;
                ErrorHandler.ThrowOnFailure(this._pane.GetName(ref name));
                return name;
            }
            set
            {
                ErrorHandler.ThrowOnFailure(this._pane.SetName(value));
            }
        }
 
        public void Dispose()
        {
            _pane = null;
        }
 
        public void Activate()
        {
            ErrorHandler.ThrowOnFailure(this._pane.Activate());
        }
 
        public void Hide()
        {
            ErrorHandler.ThrowOnFailure(this._pane.Hide());
        }
 
        public void Write(string text)
        {
            Contract.Requires<ArgumentNullException>(text != null);
            ErrorHandler.ThrowOnFailure(this._pane.OutputStringThreadSafe(text));
        }
 
        public void WriteLine(string text)
        {
            Contract.Requires<ArgumentNullException>(text != null);
            if (!text.EndsWith(Environment.NewLine))
                text += Environment.NewLine;
 
            Write(text);
        }
    }
}

Finally, one extension method that’s used to help with the IOleServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace JavaLanguageService.Extensions
{
    using System;
    using System.Diagnostics.Contracts;
    using System.Runtime.InteropServices;
    using Microsoft.VisualStudio;
    using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
 
    public static class ServiceProviderExtensions
    {
        public static TServiceInterface TryGetGlobalService<TServiceClass, TServiceInterface>(this IOleServiceProvider sp)
            where TServiceInterface : class
        {
            Contract.Requires<NullReferenceException>(sp != null);
 
            Guid guidService = typeof(TServiceInterface).GUID;
            Guid riid = typeof(TServiceInterface).GUID;
            IntPtr obj = IntPtr.Zero;
            int result = ErrorHandler.CallWithCOMConvention(() => sp.QueryService(ref guidService, ref riid, out obj));
            if (ErrorHandler.Failed(result) || obj == IntPtr.Zero)
                return null;
 
            try
            {
                TServiceInterface service = (TServiceInterface)Marshal.GetObjectForIUnknown(obj);
                return service;
            }
            finally
            {
                Marshal.Release(obj);
            }
        }
    }
}
25 Nov

A general brace matching tagger for Visual Studio 2010

I haven’t perfected it yet, but here is a fairly general brace matching tagger that seems to work very well. It relies on the classifier for the content type properly tagging comments and literals with classification types derived from PredefinedClassificationTypeNames.Comment and PredefinedClassificationTypeNames.Literal, which any decent classifier will do.

This example really shows a big improvement in Visual Studio 2010. None of my Visual Studio 2008 language services have brace matching this effective and they’re getting jealous.

Here is an example of providing a brace matcher for a “java” content type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace JavaLanguageService
{
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using CustomLanguageService.Text;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Classification;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
 
    [Export(typeof(IViewTaggerProvider))]
    [ContentType(Constants.JavaContentType)]
    [TagType(typeof(TextMarkerTag))]
    public sealed class JavaBraceMatchingTaggerProvider : IViewTaggerProvider
    {
        [Import]
        public IClassifierAggregatorService AggregatorService;
 
        public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
        {
            if (textView == null)
                return null;
 
            var aggregator = AggregatorService.GetClassifier(buffer);
            var pairs = new KeyValuePair<char, char>[]
                {
                    new KeyValuePair<char, char>('(', ')'),
                    new KeyValuePair<char, char>('{', '}'),
                    new KeyValuePair<char, char>('[', ']')
                };
            return new BraceMatchingTagger(textView, buffer, aggregator, pairs) as ITagger<T>;
        }
    }
}

And here is the brace matching tagger. Depending on the characteristics of your language, you may be able to use this will little to no modification.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
namespace CustomLanguageService.Text
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Microsoft.VisualStudio.Language.StandardClassification;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Classification;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Tagging;
 
    internal sealed class BraceMatchingTagger : ITagger<TextMarkerTag>
    {
        public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
 
        public BraceMatchingTagger(ITextView textView, ITextBuffer sourceBuffer, IClassifier aggregator, IEnumerable<KeyValuePair<char, char>> matchingCharacters)
        {
            Contract.Requires<ArgumentNullException>(textView != null);
            Contract.Requires<ArgumentNullException>(sourceBuffer != null);
            Contract.Requires<ArgumentNullException>(aggregator != null);
            Contract.Requires<ArgumentNullException>(matchingCharacters != null);
 
            this.TextView = textView;
            this.SourceBuffer = sourceBuffer;
            this.Aggregator = aggregator;
            this.MatchingCharacters = matchingCharacters.ToList().AsReadOnly();
 
            this.TextView.Caret.PositionChanged += Caret_PositionChanged;
            this.TextView.LayoutChanged += TextView_LayoutChanged;
        }
 
        public ITextView TextView
        {
            get;
            private set;
        }
 
        public ITextBuffer SourceBuffer
        {
            get;
            private set;
        }
 
        public IClassifier Aggregator
        {
            get;
            private set;
        }
 
        public ReadOnlyCollection<KeyValuePair<char, char>> MatchingCharacters
        {
            get;
            private set;
        }
 
        private SnapshotPoint? CurrentChar
        {
            get;
            set;
        }
 
        private static bool IsInCommentOrLiteral(IClassifier aggregator, SnapshotPoint point, PositionAffinity affinity)
        {
            Contract.Requires(aggregator != null);
 
            // TODO: handle affinity
            SnapshotSpan span = new SnapshotSpan(point, 1);
 
            var classifications = aggregator.GetClassificationSpans(span);
            var relevant = classifications.FirstOrDefault(classificationSpan => classificationSpan.Span.Contains(point));
            if (relevant == null || relevant.ClassificationType == null)
                return false;
 
            return relevant.ClassificationType.IsOfType(PredefinedClassificationTypeNames.Comment)
                || relevant.ClassificationType.IsOfType(PredefinedClassificationTypeNames.Literal);
        }
 
        private bool IsMatchStartCharacter(char c)
        {
            return MatchingCharacters.Any(pair => pair.Key == c);
        }
 
        private bool IsMatchCloseCharacter(char c)
        {
            return MatchingCharacters.Any(pair => pair.Value == c);
        }
 
        private char GetMatchCloseCharacter(char c)
        {
            return MatchingCharacters.First(pair => pair.Key == c).Value;
        }
 
        private char GetMatchOpenCharacter(char c)
        {
            return MatchingCharacters.First(pair => pair.Value == c).Key;
        }
 
        private static bool FindMatchingCloseChar(SnapshotPoint start, IClassifier aggregator, char open, char close, int maxLines, out SnapshotSpan pairSpan)
        {
            pairSpan = new SnapshotSpan(start.Snapshot, 1, 1);
            ITextSnapshotLine line = start.GetContainingLine();
            string lineText = line.GetText();
            int lineNumber = line.LineNumber;
            int offset = start.Position - line.Start.Position + 1;
 
            int stopLineNumber = start.Snapshot.LineCount - 1;
            if (maxLines > 0)
                stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);
 
            int openCount = 0;
            while (true)
            {
                while (offset < line.Length)
                {
                    char currentChar = lineText[offset];
                    // TODO: is this the correct affinity
                    if (currentChar == close && !IsInCommentOrLiteral(aggregator, new SnapshotPoint(start.Snapshot, offset + line.Start.Position), PositionAffinity.Successor))
                    {
                        if (openCount > 0)
                        {
                            openCount--;
                        }
                        else
                        {
                            pairSpan = new SnapshotSpan(start.Snapshot, line.Start + offset, 1);
                            return true;
                        }
                    }
                    // TODO: is this the correct affinity
                    else if (currentChar == open && !IsInCommentOrLiteral(aggregator, new SnapshotPoint(start.Snapshot, offset + line.Start.Position), PositionAffinity.Successor))
                    {
                        openCount++;
                    }
 
                    offset++;
                }
 
                // move on to the next line
                lineNumber++;
                if (lineNumber > stopLineNumber)
                    break;
 
                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                lineText = line.GetText();
                offset = 0;
            }
 
            return false;
        }
 
        private static bool FindMatchingOpenChar(SnapshotPoint start, IClassifier aggregator, char open, char close, int maxLines, out SnapshotSpan pairSpan)
        {
            pairSpan = new SnapshotSpan(start, start);
            ITextSnapshotLine line = start.GetContainingLine();
            int lineNumber = line.LineNumber;
            int offset = start - line.Start - 1;
 
            // if the offset is negative, move to the previous line
            if (offset < 0)
            {
                lineNumber--;
                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                offset = line.Length - 1;
            }
 
            string lineText = line.GetText();
 
            int stopLineNumber = 0;
            if (maxLines > 0)
                stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);
 
            int closeCount = 0;
            while (true)
            {
                while (offset >= 0)
                {
                    char currentChar = lineText[offset];
                    // TODO: is this the correct affinity
                    if (currentChar == open && !IsInCommentOrLiteral(aggregator, new SnapshotPoint(start.Snapshot, offset + line.Start.Position), PositionAffinity.Successor))
                    {
                        if (closeCount > 0)
                        {
                            closeCount--;
                        }
                        else
                        {
                            pairSpan = new SnapshotSpan(line.Start + offset, 1);
                            return true;
                        }
                    }
                    // TODO: is this the correct affinity
                    else if (currentChar == close && !IsInCommentOrLiteral(aggregator, new SnapshotPoint(start.Snapshot, offset + line.Start.Position), PositionAffinity.Successor))
                    {
                        closeCount++;
                    }
 
                    offset--;
                }
 
                // move to the previous line
                lineNumber--;
                if (lineNumber < stopLineNumber)
                    break;
 
                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                lineText = line.GetText();
                offset = line.Length - 1;
            }
 
            return false;
        }
 
        public IEnumerable<ITagSpan<TextMarkerTag>> GetTags(NormalizedSnapshotSpanCollection spans)
        {
            if (spans.Count == 0)
                yield break;
 
            // don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
            if (!CurrentChar.HasValue || CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length)
                yield break;
 
            // hold on to a snapshot of the current character
            var currentChar = CurrentChar.Value;
 
            if (IsInCommentOrLiteral(Aggregator, currentChar, TextView.Caret.Position.Affinity))
                yield break;
 
            // if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
            currentChar = currentChar.TranslateTo(spans[0].Snapshot, PointTrackingMode.Positive);
 
            // get the current char and the previous char
            char currentText = currentChar.GetChar();
            // if current char is 0 (beginning of buffer), don't move it back
            SnapshotPoint lastChar = currentChar == 0 ? currentChar : currentChar - 1;
            char lastText = lastChar.GetChar();
            SnapshotSpan pairSpan = new SnapshotSpan();
 
            if (IsMatchStartCharacter(currentText))
            {
                char closeChar = GetMatchCloseCharacter(currentText);
                /* TODO: Need to improve handling of larger blocks. this won't highlight if the matching brace is more
                 *       than 1 screen's worth of lines away. Changing this to 10 * TextView.TextViewLines.Count seemed
                 *       to improve the situation.
                 */
                if (BraceMatchingTagger.FindMatchingCloseChar(currentChar, Aggregator, currentText, closeChar, TextView.TextViewLines.Count, out pairSpan))
                {
                    yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(currentChar, 1), PredefinedTextMarkerTags.BraceHighlight);
                    yield return new TagSpan<TextMarkerTag>(pairSpan, PredefinedTextMarkerTags.BraceHighlight);
                }
            }
            else if (IsMatchCloseCharacter(lastText))
            {
                var open = GetMatchOpenCharacter(lastText);
                if (BraceMatchingTagger.FindMatchingOpenChar(lastChar, Aggregator, open, lastText, TextView.TextViewLines.Count, out pairSpan))
                {
                    yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(lastChar, 1), PredefinedTextMarkerTags.BraceHighlight);
                    yield return new TagSpan<TextMarkerTag>(pairSpan, PredefinedTextMarkerTags.BraceHighlight);
                }
            }
        }
 
        private void UpdateAtCaretPosition(CaretPosition caretPosition)
        {
            CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
            if (!CurrentChar.HasValue)
                return;
 
            var t = TagsChanged;
            if (t != null)
                t(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)));
        }
 
        private void TextView_LayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
        {
            if (e.NewSnapshot != e.OldSnapshot)
                UpdateAtCaretPosition(TextView.Caret.Position);
        }
 
        private void Caret_PositionChanged(object sender, CaretPositionChangedEventArgs e)
        {
            UpdateAtCaretPosition(e.NewPosition);
        }
    }
}
25 Nov

Tool tips for squiggles and URLs in Visual Studio 2010

I’ve been experimenting with Visual Studio’s new extensibility model, and ran into a problem with squiggles. It seems that despite having an internal IQuickInfoSource for the SquiggleTag and IUrlTag, it’s not hooked up to a IIntellisenseController so it never appears. Here is a general implementation for an SquiggleQuickInfoIntellisenseController and UrlQuickInfoIntellisenseController that will provide QuickInfo for squiggle tags and URLs provided by custom implementations of ITagger<SquiggleTag>. The controller itself is a generic that supports any ITag that comes with a IQuickInfoSource.

This code is very heavily based on an existing internal IntelliSense controller in Visual Studio 2010 that’s used for a different tag type.

First we have the IIntellisenseControllerProvider for squiggles and URLs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
namespace CustomLanguageServices.Text
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
 
    [Export(typeof(IIntellisenseControllerProvider))]
    [ContentType("text")]
    public sealed class SquiggleQuickInfoControllerProvider : IIntellisenseControllerProvider
    {
        [Import]
        public IQuickInfoBroker QuickInfoBroker;
 
        [Import]
        public IViewTagAggregatorFactoryService TagAggregatorFactoryService;
 
        public IIntellisenseController TryCreateIntellisenseController(ITextView textView, IList<ITextBuffer> subjectBuffers)
        {
            Func<TagQuickInfoController<SquiggleTag>> creator =
                () =>
                {
                    var tagAggregator = TagAggregatorFactoryService.CreateTagAggregator<SquiggleTag>(textView);
                    var controller = new TagQuickInfoController<SquiggleTag>(QuickInfoBroker, textView, tagAggregator);
                    return controller;
                };
 
            return textView.Properties.GetOrCreateSingletonProperty(creator);
        }
    }
 
    [Export(typeof(IIntellisenseControllerProvider))]
    [ContentType("text")]
    public sealed class UrlQuickInfoControllerProvider : IIntellisenseControllerProvider
    {
        [Import]
        public IQuickInfoBroker QuickInfoBroker;
 
        [Import]
        public IViewTagAggregatorFactoryService TagAggregatorFactoryService;
 
        public IIntellisenseController TryCreateIntellisenseController(ITextView textView, IList<ITextBuffer> subjectBuffers)
        {
            Func<TagQuickInfoController<IUrlTag>> creator =
                () =>
                {
                    var tagAggregator = TagAggregatorFactoryService.CreateTagAggregator<IUrlTag>(textView);
                    var controller = new TagQuickInfoController<IUrlTag>(QuickInfoBroker, textView, tagAggregator);
                    return controller;
                };
 
            return textView.Properties.GetOrCreateSingletonProperty(creator);
        }
    }
}

And then we have the actual IIntellisenseController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
namespace CustomLanguageService.Text
{
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Tagging;
 
    public class TagQuickInfoController<T> : IIntellisenseController
        where T : ITag
    {
        private IQuickInfoBroker _quickInfoBroker;
        private ITextView _textView;
        private ITagAggregator<T> _tagAggregator;
        private ITextBuffer _surfaceBuffer;
        private IQuickInfoSession _session;
 
        public TagQuickInfoController(IQuickInfoBroker quickInfoBroker, ITextView textView, ITagAggregator<T> tagAggregator)
        {
            this._quickInfoBroker = quickInfoBroker;
            this._textView = textView;
            this._tagAggregator = tagAggregator;
 
            this._surfaceBuffer = textView.TextViewModel.DataBuffer;
            this._surfaceBuffer.Changed += OnSurfaceBuffer_Changed;
            this._textView.MouseHover += OnTextView_MouseHover;
            this._textView.Caret.PositionChanged += OnCaret_PositionChanged;
        }
 
        public void ConnectSubjectBuffer(ITextBuffer subjectBuffer)
        {
        }
 
        public void Detach(ITextView textView)
        {
            this._surfaceBuffer.Changed -= OnSurfaceBuffer_Changed;
            this._textView.MouseHover -= OnTextView_MouseHover;
            this._textView.Caret.PositionChanged -= OnCaret_PositionChanged;
            if (this._tagAggregator != null)
            {
                this._tagAggregator.Dispose();
                this._tagAggregator = null;
            }
        }
 
        public void DisconnectSubjectBuffer(ITextBuffer subjectBuffer)
        {
        }
 
        internal void DismissSession()
        {
            if ((this._session != null) && !this._session.IsDismissed)
            {
                this._session.Dismiss();
                this._session = null;
            }
        }
 
        private bool EnsureSessionStillValid(SnapshotPoint point)
        {
            if (this._session != null)
            {
                if (this._session.IsDismissed)
                {
                    this._session = null;
                    return false;
                }
                if ((this._session.ApplicableToSpan.TextBuffer == point.Snapshot.TextBuffer) && this._session.ApplicableToSpan.GetSpan(point.Snapshot).IntersectsWith(new Span(point.Position, 0)))
                {
                    return true;
                }
                this._session.Dismiss();
                this._session = null;
            }
            return false;
        }
 
        private bool TryExtractQuickInfoFromMarkers(int position)
        {
            IMappingTagSpan<T> mappingTagSpan = null;
 
            foreach (IMappingTagSpan<T> span in this._tagAggregator.GetTags(new SnapshotSpan(this._textView.TextSnapshot, position, 1)))
            {
                if (span.Tag != null)
                    mappingTagSpan = span;
            }
 
            if (mappingTagSpan != null)
            {
                NormalizedSnapshotSpanCollection spans = mappingTagSpan.Span.GetSpans(this._textView.TextBuffer);
                if (spans.Count > 0)
                {
                    this.DismissSession();
                    SnapshotSpan span = spans[0];
                    ITrackingPoint triggerPoint = span.Snapshot.CreateTrackingPoint(span.Start.Position, PointTrackingMode.Positive);
                    this._session = this._quickInfoBroker.CreateQuickInfoSession(this._textView, triggerPoint, true);
                    this._session.Start();
                    return true;
                }
            }
 
            return false;
        }
 
        private void OnSurfaceBuffer_Changed(object sender, TextContentChangedEventArgs e)
        {
            this.DismissSession();
        }
 
        private void OnTextView_MouseHover(object sender, MouseHoverEventArgs e)
        {
            SnapshotPoint? nullable = e.TextPosition.GetPoint(this._surfaceBuffer, PositionAffinity.Successor);
            if (!nullable.HasValue)
                return;
 
            SnapshotPoint point = nullable.Value;
            if (this.EnsureSessionStillValid(point))
                return;
 
            if (this._tagAggregator != null)
            {
                this.TryExtractQuickInfoFromMarkers(e.Position);
            }
        }
 
        private void OnCaret_PositionChanged(object sender, CaretPositionChangedEventArgs e)
        {
            this.DismissSession();
        }
    }
}
25 Mar

Visual Studio language services: Improved brace matching hooks

By default, brace matching is performed in response to the left and right arrow keys. It should be done in response to several additional commands, as shown below.

Continue Reading »

20 Oct

ANTLR integration in Visual Studio

I put up a tech preview of the tool I wrote to help with my language services development. It’s a basic ANTLR v3 language service that integrates into Visual Studio 2005 and/or 2008. While it doesn’t have the feature set of, say, ANTLRworks, it’s still proven extremely useful for my development tasks. If you are following my articles on developing Visual Studio language services built on ANTLR, you may find it helpful. The main page for the project is here:
http://wiki.pixelminegames.com/index.php?title=Tools:nFringe:Antlr

And a list of features with screenshots is here:
http://wiki.pixelminegames.com/index.php?title=Tools:nFringe:Antlr:Features

Syntax highlighting and Quick Info Parser rules dropdown

19 Oct

ManagedMyC: Type and member dropdown bars

This is part 4 of [many?] posts about creating an ANTLR-based language service for Visual Studio.

Now that we have an AST with information about the top-level members in our source files, we can use the tree parser to gather this information and make it available for the dropdown bars. For now, the MyC language doesn’t support user types like structs, enums, or classes, so we’re stuck with listing the members in the file.

Continue Reading »

18 Oct

ManagedMyC: Intro to building an AST

This is part 3 of [many?] posts about creating an ANTLR-based language service for Visual Studio.

Sure a scanner and parser are cool, and syntax highlighting is nice. But the real power in the Visual Studio language services comes in their IntelliSense abilities, and supporting those effectively requires building and processing an AST. In this section, I’ll show how to integrate the ANTLR automatic AST features, including a tree parser, into the ManagedMyC language service. Among other things, I’ll assume the reader is already familiar with ANTLR syntax for parsers, rewrites, and tree parsers.

There are plenty of other places to learn about those separately; my goal is to show how to start incorporating the existing knowledge into a usable language service. This article only discusses adding the tree grammar to your language service and using it to process a bare-bones tree created in the parser. For this article, there are no new UI/language service features supported. In the next article, I’ll show how to use the tree parser output implement a solid TypeAndMemberDropdownBars implementation.

Continue Reading »

© 2010 Sam's Blog | Entries (RSS) and Comments (RSS)

Your Index Web Directorywordpress logo