I'm working on a VS Extension that needs to be aware of which class member the text-cursor is currently located in (methods, properties, etc). It also needs an awareness of the parents (e.g. class, nested classes, etc). It needs to know the type, name, and line number of the member or class. When I say "Type" I mean "method" or "property" not necessarily a ".NET Type".
Currently I have it working with this code here:
public static class CodeElementHelper
{
public static CodeElement[] GetCodeElementAtCursor(DTE2 dte)
{
try
{
var cursorTextPoint = GetCursorTextPoint(dte);
if (cursorTextPoint != null)
{
var activeDocument = dte.ActiveDocument;
var projectItem = activeDocument.ProjectItem;
var codeElements = projectItem.FileCodeModel.CodeElements;
return GetCodeElementAtTextPoint(codeElements, cursorTextPoint).ToArray();
}
}
catch (Exception ex)
{
Debug.WriteLine("[DBG][EXC] - " + ex.Message + " " + ex.StackTrace);
}
return null;
}
private static TextPoint GetCursorTextPoint(DTE2 dte)
{
var cursorTextPoint = default(TextPoint);
try
{
var objTextDocument = (TextDocument)dte.ActiveDocument.Object();
cursorTextPoint = objTextDocument.Selection.ActivePoint;
}
catch (Exception ex)
{
Debug.WriteLine("[DBG][EXC] - " + ex.Message + " " + ex.StackTrace);
}
return cursorTextPoint;
}
private static List<CodeElement> GetCodeElementAtTextPoint(CodeElements codeElements, TextPoint objTextPoint)
{
var returnValue = new List<CodeElement>();
if (codeElements == null)
return null;
int count = 0;
foreach (CodeElement element in codeElements)
{
if (element.StartPoint.GreaterThan(objTextPoint))
{
// The code element starts beyond the point
}
else if (element.EndPoint.LessThan(objTextPoint))
{
// The code element ends before the point
}
else
{
if (element.Kind == vsCMElement.vsCMElementClass ||
element.Kind == vsCMElement.vsCMElementProperty ||
element.Kind == vsCMElement.vsCMElementPropertySetStmt ||
element.Kind == vsCMElement.vsCMElementFunction)
{
returnValue.Add(element);
}
var memberElements = GetCodeElementMembers(element);
var objMemberCodeElement = GetCodeElementAtTextPoint(memberElements, objTextPoint);
if (objMemberCodeElement != null)
{
returnValue.AddRange(objMemberCodeElement);
}
break;
}
}
return returnValue;
}
private static CodeElements GetCodeElementMembers(CodeElement codeElement)
{
CodeElements codeElements = null;
if (codeElement is CodeNamespace)
{
codeElements = (codeElement as CodeNamespace).Members;
}
else if (codeElement is CodeType)
{
codeElements = (codeElement as CodeType).Members;
}
else if (codeElement is CodeFunction)
{
codeElements = (codeElement as CodeFunction).Parameters;
}
return codeElements;
}
}
So that currently works, if I call GetCodeElementAtCursor I will get the member and it's parents back. (This is kinda old code, but I believe I originally snagged it from Carlos' blog and ported it from VB).
My problem is that when my extension is used on code that is very large, like auto-generated files with a couple thousand lines, for example, it brings VS to a crawl. Almost unusable. Running a profiler shows that the hot lines are
private static List<CodeElement> GetCodeElementAtTextPoint(CodeElements codeElements, TextPoint objTextPoint)
{
foreach (CodeElement element in codeElements)
{
...
/*-->*/ if (element.StartPoint.GreaterThan(objTextPoint)) // HERE <---
{
// The code element starts beyond the point
}
/*-->*/ else if (element.EndPoint.LessThan(objTextPoint)) // HERE <----
{
// The code element ends before the point
}
else
{
...
var memberElements = GetCodeElementMembers(element);
/*-->*/ var objMemberCodeElement = GetCodeElementAtTextPoint(memberElements, objTextPoint); // AND, HERE <---
...
}
}
return returnValue;
}
So the third one is obvious, it's a recursive call to itself so whatever is affecting it will affect a call to itself. The first two, however, I'm not sure how to fix.
- Is there an alternative method I could use to retrieve the type of member my cursor is on (class, method, prop, etc), the name, line #, and the parents?
- Is there something that I could do to make the
TextPoint.GreaterThan
andTestPoint.LessThan
methods perform better? - Or, am I S.O.L.?
Whatever the method is, it just needs to support VS2015 or newer.
Thank you!
UPDATE: To answer Sergey's comment - it does indeed seem to be caused by .GreaterThan
/ .LessThan()
. I've separated the code and the slow-down is definitely occurring on those method calls, NOT the property accessor for element.StartPoint
and element.EndPoint
.
After you get a TextPoint using GetCursorTextPoint, you can use TextPoint.CodeElement property to find current code elements:
I ended up going the route of using some of the new'ish roslyn stuff. The code below does (pretty much) all the same stuff as my code above in the question, with the addition of returning a Moniker.
I'm marking this as the answer, but since Sergey was very helpful in his answer, plus the inspiration for my Roslyn code was actually from this SO answer, which was ALSO his answer, he definitely deserves the points :).
The code
Dependencies
Since I'm returning a Tuple, you will need System.ValueTuple and the Roslyn stuff requires Microsoft.CodeAnalysis.EditorFeatures.Text, Microsoft.CodeAnalysis.CSharp, plus all dependencies.
Targeting versions of VS2015/2017 and required .NET version
The CodeAnalysis assemblies require you target (I think) .NET 4.6.1 or higher. The version of the CodeAnalysis assemblies also directly relate to which version of VS it can support. I haven't seen any official documentation on this (which I think should be posted in big bold red letters at the top of each msdn page about this!) but here's a SO answer with the versions to use for different VS targets. Earliest you can target seems to be VS2015 (RTM). I'm personally using v1.3.2 which should support VS2015 Update 3 or above.
Performance
I didn't run this through a profiler, but it runs considerably smoother. There is a couple of seconds at first, on large files, that it doesn't work (I assume the file is being indexed) - but if you look closely a lot of the features in VS don't work until that indexing (or whatever it is) is complete. You hardly notice it. On a small file, it's insignificant.
(slightly unrelated to the question, but may help someone...)
One tip for anyone using the CaretChanged event to drive a feature like this, who are running into performance issues: I would recommend using the dispatcher and throttling the number of calls. The code below will add a 200ms delay to the call, and not allow any more than one call every 200ms. Well, 200ms AT LEAST. It's unpredictable, but it will run when it's able to - at a low priority (DispatcherPriority.ApplicationIdle):