VS Extension: TextPoint.GreaterThan / LessThan very slow for large files
Asked Answered
B

2

6

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 and TestPoint.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.

enter image description here

Barling answered 27/7, 2017 at 19:8 Comment(8)
Are you sure TextPoint.GreaterThan is slow, not element.StartPoint?Oconnell
Actually, good point, no I'm not sure. It could be the element.StartPoint and element.EndPoint. I can/will confirm this when I get in front of my computer a little later this evening.Barling
@SergeyVlasov sorry for not getting back to you sooner. It seems it is the 'LessThan' and 'GreaterThan' method calls that are slow (see my update). ThanksBarling
What's the real issue: LessThan/GreaterThan taking too much relative CPU or being really slow?Finegan
Slow - my extension makes VS nearly unusable when trying to navigate a large code file. I have this code being called on textView.Caret.PositionChanged. The ext needs to know the class hierarchy (member, class, maybe nested classes). For example, I tried an autogenerated data schema file with several classes and about 24,000 lines. I can navigate it smoothly prior to my ext being installed, but afterwards it just skips along and chokes constantly. Those lines of code are the bottleneck. So a way to speed up those calls or an alternative is what I'm hoping to find.Barling
EnvDTE.TextPoint is part of the old Visual Studio automation interfaces. Its implementation is native and private to Visual Studio. I don't think Microsoft will ever touch nor change this. Maybe you should take a look at Roslyn wich is supposed to be fast/async/newer and you can even look at its source for this kind of issues. I understand this is not the answer you're looking for :-)Kebab
@SimonMourier No that's exactly the answer I'm looking for! (although, maybe with more examples lol). I was thinking exactly that - that this was an old API. My extension needs to support VS2015+ which should be right up Roslyn's ally. You pointed me in (I think) the right direction. I'll research that a bit and report back. ThanksBarling
One issue with Roslyn is to get (already parsed) Roslyn objects from Visual Studio objects. It's not obvious because it's using composition/DI/etc., and lots of extensions methods. Intellisense on VS doesn't show Roslyn methods unless you know the namespaces to include. I find doc is rather poor. Here is an example from me: github.com/smourier/EnumCaseGenerator and this is a (famous) repository with many Visual Studio extensions source: github.com/madskristensen?tab=repositoriesKebab
B
0

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

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'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):

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);
    ...
}

...
Barling answered 18/8, 2017 at 8:7 Comment(0)
O
2

After you get a TextPoint using GetCursorTextPoint, you can use TextPoint.CodeElement property to find 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);
    }
Oconnell answered 12/8, 2017 at 17:51 Comment(4)
Thx! Great solution, way faster. Huge file is lagging at a couple hundred milliseconds, which is great - compared to before at 2+ secs. It seems to take a few seconds before it can be used on the large file (presumably due to indexing), also ok. Two probs: the hierarchy is out of order (namespace -> property [or member] -> class). Can I count on that order? if so, that's ok, and I can reorder it manually. Other prob: it doesn't seem to account for nested classes. I can probably live without this feature for now, so if there's no solution, that's ok. Thank you!Barling
1. The order in the loop is determined by the EnvDTE.vsCMElement enumeration declaration. It is not related to code in any way. 2. You can probably get to the parent class from the nested class using EnvDTE.CodeClass.Parent msdn.microsoft.com/en-us/library/envdte.codeclass.aspxOconnell
Thank you Sergey, while this answer does give me most of what I need, it's very important for me to have the hierarchy. I'm having a hard time finding any info about recursively building a hierarchy from a CodeElement. I have the codeelement at the caret, how can I find the parent of that? I suppose certain assumptions could be made when I construct the hierarchy, like methods, properties, etc, would have a parent class. But I feel like that sort of code would get very nasty. Ideally this would work with any language not just C#, but C# is the priority right now.Barling
To organize obtained code elements into a hierarchy, you can sort them by StartPoint.AbsoluteCharOffset.Oconnell
B
0

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

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'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):

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);
    ...
}

...
Barling answered 18/8, 2017 at 8:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.