Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Retrieving a CodeElement reliably in VS.NET 2005


:P
On this page:

I’ve been working on updating the Help Builder VS.NET 2005 add-in and there are a number of small things that I’ve run into that behave quite a bit different than in VS2003. Here’s one that pretty much broke all of my existing add-in behavior that deals with the editor integration.

 

In VS2003 I used the following code to figure out what type of code element the cursor is sitting on:

element = DTE.ActiveDocument.ProjectItem.FileCodeModel.CodeElementFromPoint(selPoint,0);

This is an awesome feature – it basically tells the innermost element that you are sitting on in the editor and returns that CodeElement to you. So you can get method or property/field or event info very easily.

Unfortunately it looks like this doesn't work any longer in VS2005 as this code will throw an exception. Instead VS2005 requires that you explicitly provide the exact type of element that you are looking for like this:

element DTE.ActiveDocument.ProjectItem.FileCodeModel.CodeElementFromPoint(selPoint,
                                                                                                     vsCMElement.vsCMElementEvent);

There are two issues. First that VS.NET is no longer returning the current element if no type is specified. It’s possible that there’s vsCMElement flag that provides the old behavior but if there is it’s not documented. There’s a new vsCMElement2 enumeration that contains an Unknown element type, but it also doesn’t work nor does it easily cast to the vsCMElement. Offhand I don't know how to easily duplicate the behavior I had in VS2003 - other than testing each of the types.

The other is that the code is throwing COM exceptions on failure rather than returning null which makes it very inefficient to check.

To that effect I found a sample on MSDN here:

http://msdn2.microsoft.com/en-us/library/envdte.filecodemodel.codeelementfrompoint

which does this:

        foreach (vsCMElement scope in Enum.GetValues(scopes.GetType()))
        {
            CodeElement elem = fcm.CodeElementFromPoint(pnt, scope);

            if (elem != null)
                elems += elem.Name + " (" + scope.ToString() + ")\n";
        }

This seems to indicate the CodeElementFromPoint returns null, rather than throwing an exception as is the actual case which means you need to use slow COM exception handling to figure out your current innermost element. The above code doesn’t do the trick actually because it goes through the list of CodeElements that apply to the current cursor location in the editor. And that could be any number of things. For example, if you are sitting inside of a method, it could be both a method element as well as a class element and if class comes first in the list of the enums above you’ve just found the wrong element.

To work around this I took a brute force approach and explicitly set up an array of Scopes that are in the ‘right’ order from innermost to outermost elements:

    // *** Supported scopes - set up here in the right parsing order

    // *** from lowest level to highest level

    // *** NOTE: Must be adjusted to match any CodeElements supported

    vsCMElement[] Scopes = { vsCMElement.vsCMElementFunction,

                             vsCMElement.vsCMElementProperty,

                             vsCMElement.vsCMElementVariable,

                             vsCMElement.vsCMElementEvent,

                             vsCMElement.vsCMElementClass,

                             vsCMElement.vsCMElementInterface,

                             vsCMElement.vsCMElementStruct,

                             vsCMElement.vsCMElementEnum };

 

    foreach (vsCMElement scope in Scopes )

    {

        try

        {

            element = CodeModel.CodeElementFromPoint(selPoint,scope);

            if (element != null)

                break; // if no exception - breakd

        }

        catch { ; }

    }

This seems to work well for duplicating the behavior in the previous version. If you use this you might want to add additional scope types that apply to your situations. For me, I’m dealing only with classes and members so the above list is about all I need to look at.

 

As you might expect this is not nearly as nice as having VS figure out the innermost element on its own. The above is a maintenance nightmare – trying to remember to catch each member element type etc. But alas it works and gets the job done…

For completeness sake, here’s the generic routine I use to retrieve a CodeElement in my add-ins:

/// <summary>

/// Returns an adjusted CodeElement. Walks comments 'down' to the next real element.

/// Works from XML Comment area above a method.

/// </summary>

/// <returns></returns>

public CodeElement GetCodeElement(CodeLanguages CodeLanguage)

{

      TextSelection selection =

            (TextSelection) this.DTE.ActiveWindow.Selection;

 

      EditPoint selPoint = selection.ActivePoint.CreateEditPoint();

      selPoint.StartOfLine();

 

      while(true)

      {                                  

            string BlockText = selPoint.GetText(selPoint.LineLength).Trim();

           

            // *** Skip over any XML Doc comments and Attributes

            if (CodeLanguage == CodeLanguages.CSharp && BlockText.StartsWith("/// ")  ||

                  CodeLanguage == CodeLanguages.CSharp && BlockText.StartsWith("[") ||

                  CodeLanguage == CodeLanguages.VB && BlockText.StartsWith("''' ") ||

                  CodeLanguage == CodeLanguages.VB && BlockText.StartsWith("<") )

            {

                  selPoint.LineDown(1);

                  selPoint.StartOfLine();

            }

            else

                  break;

      }                                  

 

      // *** Make sure the cursor is placed inside of the definition always

      // *** Especially required for single line methods/fields/events etc.

      selPoint.EndOfLine();

      selPoint.CharLeft(1);  // Force into the text

 

      string xBlockText = selPoint.GetText(selPoint.LineLength).Trim();

 

      // get the element under the cursor

      CodeElement element = null;

    FileCodeModel2 CodeModel = this.DTE.ActiveDocument.ProjectItem.FileCodeModel as FileCodeModel2;

 

    // *** Supported scopes - set up here in the right parsing order

    // *** from lowest level to highest level

    // *** NOTE: Must be adjusted to match any CodeElements supported

    vsCMElement[] Scopes = { vsCMElement.vsCMElementFunction,

                             vsCMElement.vsCMElementProperty,

                             vsCMElement.vsCMElementVariable,

                             vsCMElement.vsCMElementEvent,

                             vsCMElement.vsCMElementClass,

                             vsCMElement.vsCMElementInterface,

                             vsCMElement.vsCMElementStruct,

                             vsCMElement.vsCMElementEnum };

 

    foreach (vsCMElement scope in Scopes )

    {

        try

        {

            element = CodeModel.CodeElementFromPoint(selPoint,scope);

            if (element != null)

                break; // if no exception - break

        }

        catch { ; }

    }

   

    if (element == null)

            return null;

     

    return element;

}


The Voices of Reason


 

Paul
March 07, 2006

# re: Retrieving a CodeElement reliably in VS.NET 2005

Where does the CodeLanguages reference come from.

Thanks
Paul

Bink
September 08, 2006

# re: Retrieving a CodeElement reliably in VS.NET 2005

#region CodeElementContainsPoint
private bool CodeElementContainsPoint(CodeElement ce, TextPoint p)
{
return (p.AbsoluteCharOffset < ce.EndPoint.AbsoluteCharOffset) &&
(p.AbsoluteCharOffset >= ce.StartPoint.AbsoluteCharOffset);
}
#endregion

#region FindInnerMostCodeElement
private CodeElement FindInnerMostCodeElement(CodeElements elements, TextPoint p)
{
foreach (CodeElement ce in elements)
{
if (CodeElementContainsPoint(ce, p))
{
CodeElement ce2 = FindInnerMostCodeElement(ce.Children, p);
if (ce2 == null) return ce;
else return ce2;
}
}
return null;
}
#endregion



and how to use it:

TextSelection sel = (TextSelection)_application.ActiveDocument.Selection;
FileCodeModel2 fcm = (FileCodeModel2)_application.ActiveDocument.ProjectItem.FileCodeModel;

CodeElement ce = FindInnerMostCodeElement(fcm.CodeElements, sel.TopPoint);


# DotNetSlackers: Retrieving a CodeElement reliably in VS.NET 2005


Korben
February 26, 2007

# re: Retrieving a CodeElement reliably in VS.NET 2005

In fact there is a way to get the CodeElement instance avoiding the exceptions. EditPoint, TextPoint and VirtualPoint classes have a property called CodeElement. It still takes a vsCMElement object argument, but returns nothing if the code element does not exist instead of throwing an exception.

Nir
June 01, 2009

# re: Retrieving a CodeElement reliably in VS.NET 2005

I agree with Korben, although your way works, using the get_CodeElement(scope) works just as well without hard coding the elements. Nonetheless this gave me a good direction to accomplish what I was looking for. Thanks

Pradeep
November 07, 2011

# re: Retrieving a CodeElement reliably in VS.NET 2005

EndPoint.GetText() is usefull function to get the source code from the code files.

I am developing ad AddIn for VS 2010. I need to copy method's source code to Word Document for documentation purpose. GetText() is returning source code without formatting (colors).

Is it possible to get the source code with Visual Studio format?

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024