views:

335

answers:

1

I'm trying to resolve the fully qualified name of a c# identifier at a certain point (cursor) of a code window, using a Macro (or even an Add-in) in Visual Studio 2008.

For example, if the cursor is in "Rectangle", I would like "System.Drawing.Rectangle" returned.

I've tried FileCodeModel.CodeElements and .CodeElementFromPoint but they only retreive the containing method or class (and others).

If this can't be done using a macro or add-in (even though VS does know the information via intellisense), would it be possible to use Reflection read in the c# file and get the desired info?

+5  A: 

It can be done. Here's one solution (albeit a somewhat hacky one): use F1 Help Context. In order to make F1 help work, Visual Studio pushes the fully-qualified type name of the current selection or insertion point into a bag of name/value pairs called "F1 Help Context". And there are public APIs in the Visual Studio SDK for querying the contents of F1 Help Context.

In order to stay sane, you'll want to enable the debugging registry key for F1 Help Context. This lets you see what's in Help Context at any time via the oft-maligned Dynamic Help window. To do this:

  1. start visual studio and choose Dynamic Help from the Help menu.
  2. set the registry key below (you need step #1 to create the registry tree)
  3. restart Visual Studio to pick up the changes
  4. Now, in the dynamic help window there will be debug output so you can see what's in F1 help context. The rest of this answer describes how to get at that context programmatically so your add-in can use it.

Here's the registry key:

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0\Dynamic Help]
"Display Debug Output in Retail"="YES"

As you'll see from looking at the F1 debug output, Visual Studio doesn't explicitly tell you "this is the identifier's type". Instead, it simply sticks the fully-qualified type name at the head of one or more "Help Keywords" which F1 uses to bring up help. For example, you can have System.String, VS.TextEditor, and VS.Ambient in your help context, and only the first one is related to the current code.

The trick to make this easier is this: Visual Studio can mark keywords as case-sensitive or case-insensitive. AFAIK, the only part of Visual Studio which injects case-sensitive keywords is the code editor of case-sensitive languages (C#, C++) in response to code context. Therefore, if you filter all keywords to case-sensitive keywords, you know you're looking at code.

Unfortunately, the C# editor also pushes language keywords (not just identifiers) into help context if the insertion point is on top of a language keyword. So you'll need to screen out language keywords. There are two ways to do this. You can simply try to look them up in the type system, and since they're not valid type names (especially not the way VS mangles them, e.g. "string_CSharpKeyword" for the string keyword) you can just fail silently. Or you can detect the lack of dots and assume it's not a type name. Or you can detect the _CSharpKeyword suffix and hope the VS team doesn't change it. :-)

Another potential issue is generics. The type name you'll get from VS for a generic type looks like this:

System.Collections.Generic.List`1

and methods look like this:

System.Collections.Generic.List`1.FindAll.

You'll need to be smart about detecting the back-tick and dealing with it.

Also, you might get interesting behavior in cases like ASP.NET MVC .ASPX files where there's both C# code and other case-sensitive code (e.g. javascript) on the page. In that case, you'll need to look at the attributes as well. In addition to keywords, Help Context also has "attributes", which are name/value pairs describing the current context. For example, devlang=csharp is one attribute. The code below can be used to pull out attributes too. You'll need to experiment to figure out the right attributes to look for so you don't end up acting on javascript or other odd code.

Anyway, now that you understand (or at least have been exposed to!) all the caveats, here's some code to pull out the case-sensitive keyword (if it exists) from help context, as well as the rest of the name/value pairs. (keywords are simply name/value pairs whose name is "keyword").

Keep in mind that this code requires the Visual Studio SDK (not just the regular VS install) in order to build, in order to get the Microsoft.VisualStudio.Shell.Interop, Microsoft.VisualStudio.Shell, and Microsoft.VisualStudio.OLE.Interop namespaces (which you'll need to add as references in your addin project).

OK, have fun and good luck!

using System;
using Extensibility;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.CommandBars;
using System.Resources;
using System.Reflection;
using System.Globalization;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.OLE.Interop;
using System.Collections.Generic;

public class HelpAttribute
{
    public string Name;
    public string Value;
    public VSUSERCONTEXTPRIORITY Priority;
    public VSUSERCONTEXTATTRIBUTEUSAGE Usage;
}

public class HelpContext2 : List<HelpAttribute>
{
    public static HelpContext2 GetHelpContext(DTE2 dte)
    {
        // Get a reference to the current active window (presumably a code editor).
        Window activeWindow = dte.ActiveWindow;

        // make a few gnarly COM-interop calls in order to get Help Context 
        Microsoft.VisualStudio.OLE.Interop.IServiceProvider sp = (Microsoft.VisualStudio.OLE.Interop.IServiceProvider)activeWindow.DTE;
        Microsoft.VisualStudio.Shell.ServiceProvider serviceProvider = new Microsoft.VisualStudio.Shell.ServiceProvider(sp);
        IVsMonitorUserContext contextMonitor = (IVsMonitorUserContext)serviceProvider.GetService(typeof(IVsMonitorUserContext));
        IVsUserContext userContext;
        int hresult = contextMonitor.get_ApplicationContext(out userContext);
        HelpContext2 attrs = new HelpContext2(userContext);

        return attrs;
    }
    public HelpContext2(IVsUserContext userContext)
    {
        int count;
        userContext.CountAttributes(null, 1, out count);
        for (int i = 0; i < count; i++)
        {
            string name, value;
            int priority;
            userContext.GetAttributePri(i, null, 1, out priority, out name, out value);
            VSUSERCONTEXTATTRIBUTEUSAGE[] usageArray = new VSUSERCONTEXTATTRIBUTEUSAGE[1];
            userContext.GetAttrUsage(i, 1, usageArray);
            VSUSERCONTEXTATTRIBUTEUSAGE usage = usageArray[0];
            HelpAttribute attr = new HelpAttribute();
            attr.Name = name;
            attr.Value = value;
            attr.Priority = (VSUSERCONTEXTPRIORITY)priority;
            attr.Usage = usage; // name == "keyword" ? VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_Lookup : VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_Filter;
            this.Add(attr);
        }
    }
    public string CaseSensitiveKeyword
    {
        get
        {
            HelpAttribute caseSensitive = Keywords.Find(attr => 
                attr.Usage == VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_LookupF1_CaseSensitive
                || attr.Usage == VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_Lookup_CaseSensitive
                );
            return caseSensitive == null ? null : caseSensitive.Value;
        }
    }
    public List<HelpAttribute> Keywords
    {
        get
        {
            return this.FindAll(attr=> attr.Name == "keyword");
        }
    }
}
Justin Grant
Thanks a lot, this works perfectly!!
ste
excellent! glad to help. luckily I had this code lying around from some previous investigations... figuring out how to do this the first time was a little harder. :-)
Justin Grant
You are officially my personal hero.I was wandering around the wasteland of CodeElement and trying to determine how to derive the help string. You got me back on the right track.Thanks again!
Batgar
There is only one problem with this technique. It only updates the attributes provided via IVsUserContext if the Dynamic Help window was made active at least once since the Visual Studio window appeared.Anyone know of a way to get IVsUserContext to start providing attributes without activating the Dynamic Help window?
Batgar
I found a way to get it to work via IVsMonitorUserContext. I will post up the source code when I get it more solid.
Batgar
Cool! I had figuerd that there was a less hacky solution available.
Justin Grant