VS Extension: TextPoint.GreaterThan / LessThan is very slow for large files

I am working on a VS extension that should know which member of the class is currently in the text course (methods, properties, etc.). He also needs parental awareness (e.g. class, nested classes, etc.). He must know the type, name and line number of the member or class. When I say "Type", I mean "method" or "property", not necessarily ".NET Type".

I am currently 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 what currently works, if I call GetCodeElementAtCursor, I will get the member and its parents. (This is a kind of old code, but I suppose I originally hooked it from Carlos's blog and ported it from VB).

My problem is that when my extension is used for very large code, for example, automatically generated files with several thousand lines, it leads to VS crawl. Almost unusable. The launch of the profiler shows that the hotlines

 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 is a recursive call to oneself, therefore everything that affects it will affect the call to oneself. However, the first two, I'm not sure how to fix it.

  • Is there an alternative method that I could use to get the type of member on which my cursor is on (class, method, support, etc.), name, line # and parents?
  • Is there something I could do to make the TextPoint.GreaterThan and TestPoint.LessThan methods work better?
  • Or am I SOL?

Regardless of the method, it just needs to support VS2015 or later.

Thanks!

UPDATE: To respond to Sergey's comment - it really looks like .GreaterThan / .LessThan() . I have separated the code, and the slowdown occurs on these method calls, and not on the property accessory for element.StartPoint and element.EndPoint .

enter image description here

+6
c # visual-studio visual-studio-extensions envdte vsix
source share
2 answers

I ended up using some of the new roslyn stuff. The code below does (to a large extent) all the same stuff as my code above in the question, with the addition of Moniker return.

I mark this as an 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, it definitely deserves points :).

The code

 public static (string, ImageMoniker)[] GetSyntaxHierarchyAtCaret(IWpfTextView textView) { var caretPosition = textView.Caret.Position.BufferPosition; var document = caretPosition.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); var syntaxRoot = document.GetSyntaxRootAsync().Result; var caretParent = syntaxRoot.FindToken(caretPosition).Parent; var returnValue = new List<(string, ImageMoniker)>(); while (caretParent != null) { var kind = caretParent.Kind(); switch (kind) { case SyntaxKind.ClassDeclaration: { var dec = caretParent as ClassDeclarationSyntax; returnValue.Add((dec.Identifier.ToString(),KnownMonikers.Class)); break; } case SyntaxKind.MethodDeclaration: { var dec = caretParent as MethodDeclarationSyntax; returnValue.Add((dec.Identifier.ToString(),KnownMonikers.Method)); break; } case SyntaxKind.PropertyDeclaration: { var dec = caretParent as PropertyDeclarationSyntax; returnValue.Add((dec.Identifier.ToString(), KnownMonikers.Property)); break; } } caretParent = caretParent.Parent; } return returnValue.ToArray(); } 

Dependencies

Since I am returning Tuple, you will need System.ValueTuple , and Roslyn material requires Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.CSharp , as well as all the dependencies.

Indicative versions of VS2015 / 2017 and the required version of .NET

CodeAnalysis builds require target (I think) .NET 4.6.1 or higher. The CodeAnalysis build version is also directly related to the version of VS that it can support. I have not seen any official documentation about this (which, I think, should be placed in big bold red letters at the top of every msdn page about it!), But here is the SO answer with versions for use for different purposes by VS. The earliest you can target seems to be VS2015 (RTM). I personally use v1.3.2, which should support VS2015 update or higher.

Performance

I did not run this through the profiler, but it works much more smoothly. Firstly, on large files, there are a couple of seconds that it does not work (I assume that the file is indexed), but if you look carefully, many functions in VS do not work as long as it is indexed (or whatever it is ) You hardly notice this. In a small file this is immaterial.

(slightly unrelated to the question, but may help someone ...)

One tip for anyone who uses the CaretChanged event to manage a feature that faces performance issues: I would recommend using a dispatcher and reducing the number of calls. In the code below, a delay of 200 ms for the call will be added and more than one call every 200 ms is not allowed. Well, 200 ms by measure. This is unpredictable, but it will work when it can - with low priority (DispatcherPriority.ApplicationIdle):

 private readonly IWpfTextView _textView; private readonly DispatcherTimer _throttleCursorMove; ... // constructor { _textView.Caret.PositionChanged += HandleCaretPositionChanged; _throttleCursorMove = new DispatcherTimer(DispatcherPriority.ApplicationIdle); _throttleCursorMove.Tick += (sender, args) => CaretPositionChanged(); _throttleCursorMove.Interval = new TimeSpan(0, 0, 0, 0, 200); } private void HandleCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) { if (!_throttleCursorMove.IsEnabled) _throttleCursorMove.Start(); } private void CaretPositionChanged() { _throttleCursorMove.Stop(); ... var hierarchy = CodeHierarchyHelper.GetSyntaxHierarchyAtCaret(_textView); ... } ... 
0
source share

After retrieving a TextPoint using GetCursorTextPoint, you can use the TextPoint.CodeElement property to search for the current code elements:

  EnvDTE.TextPoint p = GetCursorTextPoint(DTE); foreach (EnvDTE.vsCMElement i in Enum.GetValues(typeof(EnvDTE.vsCMElement))) { EnvDTE.CodeElement e = p.CodeElement[i]; if (e != null) System.Windows.MessageBox.Show(i.ToString() + " " + e.FullName); } 
+2
source share

All Articles