File: TextStructureNavigation\AbstractTextStructureNavigatorProvider.TextStructureNavigator.cs
Web Access
Project: ..\..\..\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.TextStructureNavigation
{
    internal partial class AbstractTextStructureNavigatorProvider
    {
        private class TextStructureNavigator : ITextStructureNavigator
        {
            private readonly ITextBuffer _subjectBuffer;
            private readonly ITextStructureNavigator _naturalLanguageNavigator;
            private readonly AbstractTextStructureNavigatorProvider _provider;
            private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
 
            internal TextStructureNavigator(
                ITextBuffer subjectBuffer,
                ITextStructureNavigator naturalLanguageNavigator,
                AbstractTextStructureNavigatorProvider provider,
                IUIThreadOperationExecutor uIThreadOperationExecutor)
            {
                Contract.ThrowIfNull(subjectBuffer);
                Contract.ThrowIfNull(naturalLanguageNavigator);
                Contract.ThrowIfNull(provider);
 
                _subjectBuffer = subjectBuffer;
                _naturalLanguageNavigator = naturalLanguageNavigator;
                _provider = provider;
                _uiThreadOperationExecutor = uIThreadOperationExecutor;
            }
 
            public IContentType ContentType => _subjectBuffer.ContentType;
 
            public TextExtent GetExtentOfWord(SnapshotPoint currentPosition)
            {
                using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetExtentOfWord, CancellationToken.None))
                {
                    var result = default(TextExtent);
                    _uiThreadOperationExecutor.Execute(
                        title: EditorFeaturesResources.Text_Navigation,
                        defaultDescription: EditorFeaturesResources.Finding_word_extent,
                        allowCancellation: true,
                        showProgress: false,
                        action: context =>
                    {
                        result = GetExtentOfWordWorker(currentPosition, context.UserCancellationToken);
                    });
 
                    return result;
                }
            }
 
            private TextExtent GetExtentOfWordWorker(SnapshotPoint position, CancellationToken cancellationToken)
            {
                var textLength = position.Snapshot.Length;
                if (textLength == 0)
                {
                    return _naturalLanguageNavigator.GetExtentOfWord(position);
                }
 
                // If at the end of the file, go back one character so stuff works
                if (position == textLength && position > 0)
                {
                    position -= 1;
                }
 
                // If we're at the EOL position, return the line break's extent
                var line = position.Snapshot.GetLineFromPosition(position);
                if (position >= line.End && position < line.EndIncludingLineBreak)
                {
                    return new TextExtent(new SnapshotSpan(line.End, line.EndIncludingLineBreak - line.End), isSignificant: false);
                }
 
                var document = GetDocument(position);
                if (document != null)
                {
                    var root = document.GetSyntaxRootSynchronously(cancellationToken);
                    var trivia = root.FindTrivia(position, findInsideTrivia: true);
 
                    if (trivia != default)
                    {
                        if (trivia.Span.Start == position && _provider.ShouldSelectEntireTriviaFromStart(trivia))
                        {
                            // We want to select the entire comment
                            return new TextExtent(trivia.Span.ToSnapshotSpan(position.Snapshot), isSignificant: true);
                        }
                    }
 
                    var token = root.FindToken(position, findInsideTrivia: true);
 
                    // If end of file, go back a token
                    if (token.Span.Length == 0 && token.Span.Start == textLength)
                    {
                        token = token.GetPreviousToken();
                    }
 
                    if (token.Span.Length > 0 && token.Span.Contains(position) && !_provider.IsWithinNaturalLanguage(token, position))
                    {
                        // Cursor position is in our domain - handle it.
                        return _provider.GetExtentOfWordFromToken(token, position);
                    }
                }
 
                // Fall back to natural language navigator do its thing.
                return _naturalLanguageNavigator.GetExtentOfWord(position);
            }
 
            public SnapshotSpan GetSpanOfEnclosing(SnapshotSpan activeSpan)
            {
                using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfEnclosing, CancellationToken.None))
                {
                    var span = default(SnapshotSpan);
                    var result = _uiThreadOperationExecutor.Execute(
                        title: EditorFeaturesResources.Text_Navigation,
                        defaultDescription: EditorFeaturesResources.Finding_enclosing_span,
                        allowCancellation: true,
                        showProgress: false,
                        action: context =>
                    {
                        span = GetSpanOfEnclosingWorker(activeSpan, context.UserCancellationToken);
                    });
 
                    return result == UIThreadOperationStatus.Completed ? span : activeSpan;
                }
            }
 
            private static SnapshotSpan GetSpanOfEnclosingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken)
            {
                // Find node that covers the entire span.
                var node = FindLeafNode(activeSpan, cancellationToken);
                if (node != null && activeSpan.Length == node.Value.Span.Length)
                {
                    // Go one level up so the span widens.
                    node = GetEnclosingNode(node.Value);
                }
 
                return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
            }
 
            public SnapshotSpan GetSpanOfFirstChild(SnapshotSpan activeSpan)
            {
                using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfFirstChild, CancellationToken.None))
                {
                    var span = default(SnapshotSpan);
                    var result = _uiThreadOperationExecutor.Execute(
                        title: EditorFeaturesResources.Text_Navigation,
                        defaultDescription: EditorFeaturesResources.Finding_enclosing_span,
                        allowCancellation: true,
                        showProgress: false,
                        action: context =>
                    {
                        span = GetSpanOfFirstChildWorker(activeSpan, context.UserCancellationToken);
                    });
 
                    return result == UIThreadOperationStatus.Completed ? span : activeSpan;
                }
            }
 
            private static SnapshotSpan GetSpanOfFirstChildWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken)
            {
                // Find node that covers the entire span.
                var node = FindLeafNode(activeSpan, cancellationToken);
                if (node != null)
                {
                    // Take first child if possible, otherwise default to node itself.
                    var firstChild = node.Value.ChildNodesAndTokens().FirstOrNull();
                    if (firstChild.HasValue)
                    {
                        node = firstChild.Value;
                    }
                }
 
                return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
            }
 
            public SnapshotSpan GetSpanOfNextSibling(SnapshotSpan activeSpan)
            {
                using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfNextSibling, CancellationToken.None))
                {
                    var span = default(SnapshotSpan);
                    var result = _uiThreadOperationExecutor.Execute(
                        title: EditorFeaturesResources.Text_Navigation,
                        defaultDescription: EditorFeaturesResources.Finding_span_of_next_sibling,
                        allowCancellation: true,
                        showProgress: false,
                        action: context =>
                    {
                        span = GetSpanOfNextSiblingWorker(activeSpan, context.UserCancellationToken);
                    });
 
                    return result == UIThreadOperationStatus.Completed ? span : activeSpan;
                }
            }
 
            private static SnapshotSpan GetSpanOfNextSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken)
            {
                // Find node that covers the entire span.
                var node = FindLeafNode(activeSpan, cancellationToken);
                if (node != null)
                {
                    // Get ancestor with a wider span.
                    var parent = GetEnclosingNode(node.Value);
                    if (parent != null)
                    {
                        // Find node immediately after the current in the children collection.
                        var nodeOrToken = parent.Value
                            .ChildNodesAndTokens()
                            .SkipWhile(child => child != node)
                            .Skip(1)
                            .FirstOrNull();
 
                        if (nodeOrToken.HasValue)
                        {
                            node = nodeOrToken.Value;
                        }
                        else
                        {
                            // If this is the last node, move to the parent so that the user can continue 
                            // navigation at the higher level.
                            node = parent.Value;
                        }
                    }
                }
 
                return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
            }
 
            public SnapshotSpan GetSpanOfPreviousSibling(SnapshotSpan activeSpan)
            {
                using (Logger.LogBlock(FunctionId.TextStructureNavigator_GetSpanOfPreviousSibling, CancellationToken.None))
                {
                    var span = default(SnapshotSpan);
                    var result = _uiThreadOperationExecutor.Execute(
                        title: EditorFeaturesResources.Text_Navigation,
                        defaultDescription: EditorFeaturesResources.Finding_span_of_previous_sibling,
                        allowCancellation: true,
                        showProgress: false,
                        action: context =>
                    {
                        span = GetSpanOfPreviousSiblingWorker(activeSpan, context.UserCancellationToken);
                    });
 
                    return result == UIThreadOperationStatus.Completed ? span : activeSpan;
                }
            }
 
            private static SnapshotSpan GetSpanOfPreviousSiblingWorker(SnapshotSpan activeSpan, CancellationToken cancellationToken)
            {
                // Find node that covers the entire span.
                var node = FindLeafNode(activeSpan, cancellationToken);
                if (node != null)
                {
                    // Get ancestor with a wider span.
                    var parent = GetEnclosingNode(node.Value);
                    if (parent != null)
                    {
                        // Find node immediately before the current in the children collection.
                        var nodeOrToken = parent.Value
                            .ChildNodesAndTokens()
                            .Reverse()
                            .SkipWhile(child => child != node)
                            .Skip(1)
                            .FirstOrNull();
 
                        if (nodeOrToken.HasValue)
                        {
                            node = nodeOrToken.Value;
                        }
                        else
                        {
                            // If this is the first node, move to the parent so that the user can continue 
                            // navigation at the higher level.
                            node = parent.Value;
                        }
                    }
                }
 
                return node == null ? activeSpan : node.Value.Span.ToSnapshotSpan(activeSpan.Snapshot);
            }
 
            private static Document GetDocument(SnapshotPoint point)
            {
                var textLength = point.Snapshot.Length;
                if (textLength == 0)
                {
                    return null;
                }
 
                return point.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
            }
 
            /// <summary>
            /// Finds deepest node that covers given <see cref="SnapshotSpan"/>.
            /// </summary>
            private static SyntaxNodeOrToken? FindLeafNode(SnapshotSpan span, CancellationToken cancellationToken)
            {
                if (!TryFindLeafToken(span.Start, out var token, cancellationToken))
                {
                    return null;
                }
 
                SyntaxNodeOrToken? node = token;
                while (node != null && (span.End.Position > node.Value.Span.End))
                {
                    node = GetEnclosingNode(node.Value);
                }
 
                return node;
            }
 
            /// <summary>
            /// Given position in a text buffer returns the leaf syntax node it belongs to.
            /// </summary>
            private static bool TryFindLeafToken(SnapshotPoint point, out SyntaxToken token, CancellationToken cancellationToken)
            {
                var syntaxTree = GetDocument(point).GetSyntaxTreeSynchronously(cancellationToken);
                if (syntaxTree != null)
                {
                    token = syntaxTree.GetRoot(cancellationToken).FindToken(point, true);
                    return true;
                }
 
                token = default;
                return false;
            }
 
            /// <summary>
            /// Returns first ancestor of the node which has a span wider than node's span.
            /// If none exist, returns the last available ancestor.
            /// </summary>
            private static SyntaxNodeOrToken SkipSameSpanParents(SyntaxNodeOrToken node)
            {
                while (node.Parent != null && node.Parent.Span == node.Span)
                {
                    node = node.Parent;
                }
 
                return node;
            }
 
            /// <summary>
            /// Finds node enclosing current from navigation point of view (that is, some immediate ancestors
            /// may be skipped during this process).
            /// </summary>
            private static SyntaxNodeOrToken? GetEnclosingNode(SyntaxNodeOrToken node)
            {
                var parent = SkipSameSpanParents(node).Parent;
                if (parent != null)
                {
                    return parent;
                }
                else
                {
                    return null;
                }
            }
        }
    }
}