File: TextStructureNavigation\CSharpTextStructureNavigatorProvider.cs
Web Access
Project: src\src\EditorFeatures\CSharp\Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj (Microsoft.CodeAnalysis.CSharp.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.
 
using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.Editor.Implementation.TextStructureNavigation;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.TextStructureNavigation;
 
[Export(typeof(ITextStructureNavigatorProvider))]
[ContentType(ContentTypeNames.CSharpContentType)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CSharpTextStructureNavigatorProvider(
    ITextStructureNavigatorSelectorService selectorService,
    IContentTypeRegistryService contentTypeService,
    IUIThreadOperationExecutor uIThreadOperationExecutor) : AbstractTextStructureNavigatorProvider(selectorService, contentTypeService, uIThreadOperationExecutor)
{
    protected override bool ShouldSelectEntireTriviaFromStart(SyntaxTrivia trivia)
        => trivia.IsRegularOrDocComment();
 
    protected override TextExtent GetExtentOfWordFromToken(ITextStructureNavigator naturalLanguageNavigator, SyntaxToken token, SnapshotPoint position)
    {
        var snapshot = position.Snapshot;
 
        // Legacy behavior.  We let the editor handle these.  Note: this can be revisited if we think we would do a better
        // job handling these.
        if (token.Kind() is SyntaxKind.InterpolatedStringTextToken or SyntaxKind.XmlTextLiteralToken)
            return naturalLanguageNavigator.GetExtentOfWord(position);
 
        // Legacy behavior.  If we're on the start of a char literal, we select the entire thing.  For anything else, we
        // defer to the editor. Note: this can be revisited if we think we would do a better
        if (token.Kind() is SyntaxKind.CharacterLiteralToken)
        {
            if (token.SpanStart == position)
                return GetTokenExtent(token, snapshot);
 
            return naturalLanguageNavigator.GetExtentOfWord(position);
        }
 
        // For string literals, if we're on the starting quote, we want to select the entire string.
        //
        // If we're on the closing quote, we want to treat it as separate token.  This allows the cursor to stop during
        // word navigation (Ctrl+LeftArrow, etc.) immediately before AND after the closing quote, just like it did in
        // VS2013 and like it currently does for interpolated strings.
        //
        // If we're in the middle of the string, we want to let the editor take over.  but if it selects a span outside
        // of the string, we'll clamp the result back to within the string.
 
        var isNormalStringLiteral = token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken;
        var isRawStringLiteral = token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.Utf8SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken;
 
        if (!isNormalStringLiteral && !isRawStringLiteral)
        {
            // Not a string literal. Just select the entire token.
            return GetTokenExtent(token, snapshot);
        }
 
        // At the start of the string, select the start span.
        var (startSpan, contentSpan, endSpan) = GetStringLiteralParts();
        if (startSpan.Contains(position))
            return new TextExtent(startSpan.ToSnapshotSpan(snapshot), isSignificant: true);
 
        // If at the end, select the end piece only.
        if (endSpan.Contains(position))
            return new TextExtent(endSpan.ToSnapshotSpan(snapshot), isSignificant: true);
 
        // We're in the middle.  Defer to the editor.  But make sure we don't go outside of the middle section.
        var naturalExtent = naturalLanguageNavigator.GetExtentOfWord(position);
 
        var intersection = naturalExtent.Span.Intersection(contentSpan.ToSpan());
        return intersection is null ? naturalExtent : new TextExtent(intersection.Value, isSignificant: naturalExtent.IsSignificant);
 
        (TextSpan startSpan, TextSpan contentSpan, TextSpan endSpan) GetStringLiteralParts()
        {
            var start = token.Span.Start;
            var contentStart = start;
 
            if (CharAt(contentStart) == '@')
                contentStart++;
 
            if (CharAt(contentStart) == '"')
                contentStart++;
 
            if (isRawStringLiteral)
            {
                while (CharAt(contentStart) == '"')
                    contentStart++;
            }
 
            var end = token.Span.End;
            var contentEnd = end;
 
            if (CharAt(contentEnd - 1) == '8')
                contentEnd--;
 
            if (CharAt(contentEnd - 1) is 'u' or 'U')
                contentEnd--;
 
            if (CharAt(contentEnd - 1) == '"')
                contentEnd--;
 
            if (isRawStringLiteral)
            {
                while (CharAt(contentEnd - 1) == '"')
                    contentEnd--;
            }
 
            return (TextSpan.FromBounds(start, contentStart), TextSpan.FromBounds(contentStart, contentEnd), TextSpan.FromBounds(contentEnd, end));
        }
 
        char CharAt(int position)
            => position >= 0 && position < snapshot.Length ? snapshot[position] : '\0';
    }
}