|
// 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.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.CSharp.TextStructureNavigation
{
[Export(typeof(ITextStructureNavigatorProvider))]
[ContentType(ContentTypeNames.CSharpContentType)]
internal class CSharpTextStructureNavigatorProvider : AbstractTextStructureNavigatorProvider
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpTextStructureNavigatorProvider(
ITextStructureNavigatorSelectorService selectorService,
IContentTypeRegistryService contentTypeService,
IUIThreadOperationExecutor uIThreadOperationExecutor)
: base(selectorService, contentTypeService, uIThreadOperationExecutor)
{
}
protected override bool ShouldSelectEntireTriviaFromStart(SyntaxTrivia trivia)
=> trivia.IsRegularOrDocComment();
protected override bool IsWithinNaturalLanguage(SyntaxToken token, int position)
{
switch (token.Kind())
{
case SyntaxKind.StringLiteralToken:
case SyntaxKind.Utf8StringLiteralToken:
// This, in combination with the override of GetExtentOfWordFromToken() below, treats the closing
// quote as a separate token. This maintains behavior with VS2013.
return !IsAtClosingQuote(token, position);
case SyntaxKind.SingleLineRawStringLiteralToken:
case SyntaxKind.MultiLineRawStringLiteralToken:
case SyntaxKind.Utf8SingleLineRawStringLiteralToken:
case SyntaxKind.Utf8MultiLineRawStringLiteralToken:
{
// Like with normal string literals, treat the closing quotes as as the end of the string so that
// navigation ends there and doesn't go past them.
var end = GetStartOfRawStringLiteralEndDelimiter(token);
return position < end;
}
case SyntaxKind.CharacterLiteralToken:
// Before the ' is considered outside the character
return position != token.SpanStart;
case SyntaxKind.InterpolatedStringTextToken:
case SyntaxKind.XmlTextLiteralToken:
return true;
}
return false;
}
private static int GetStartOfRawStringLiteralEndDelimiter(SyntaxToken token)
{
var text = token.ToString();
var start = 0;
var end = text.Length;
if (token.Kind() is SyntaxKind.Utf8MultiLineRawStringLiteralToken or SyntaxKind.Utf8SingleLineRawStringLiteralToken)
{
// Skip past the u8 suffix
end -= "u8".Length;
}
while (start < end && text[start] == '"')
start++;
while (end > start && text[end - 1] == '"')
end--;
return token.SpanStart + end;
}
private static bool IsAtClosingQuote(SyntaxToken token, int position)
=> token.Kind() switch
{
SyntaxKind.StringLiteralToken => position == token.Span.End - 1 && token.Text[^1] == '"',
SyntaxKind.Utf8StringLiteralToken => position == token.Span.End - 3 && token.Text is [.., '"', 'u' or 'U', '8'],
_ => throw ExceptionUtilities.Unreachable()
};
protected override TextExtent GetExtentOfWordFromToken(SyntaxToken token, SnapshotPoint position)
{
if (token.Kind() is SyntaxKind.StringLiteralToken or SyntaxKind.Utf8StringLiteralToken &&
IsAtClosingQuote(token, position.Position))
{
// Special case to treat the closing quote of a string literal as a 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.
var span = new Span(position.Position, token.Span.End - position.Position);
return new TextExtent(new SnapshotSpan(position.Snapshot, span), isSignificant: true);
}
else if (token.Kind() is
SyntaxKind.SingleLineRawStringLiteralToken or
SyntaxKind.MultiLineRawStringLiteralToken or
SyntaxKind.Utf8SingleLineRawStringLiteralToken or
SyntaxKind.Utf8MultiLineRawStringLiteralToken)
{
var delimiterStart = GetStartOfRawStringLiteralEndDelimiter(token);
return new TextExtent(new SnapshotSpan(position.Snapshot, Span.FromBounds(delimiterStart, token.Span.End)), isSignificant: true);
}
else
{
return base.GetExtentOfWordFromToken(token, position);
}
}
}
}
|