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;
}
Other Posts you might also like