| File: RawStringLiteral\CSharpRawStringLiteralOnAutoInsertService.cs | Web Access |
| Project: src\roslyn\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features) |
// 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.Composition; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.RawStringLiteral; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.RawStringLiteral; [ExportLanguageService(typeof(IRawStringLiteralAutoInsertService), LanguageNames.CSharp), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class CSharpRawStringLiteralOnAutoInsertService() : IRawStringLiteralAutoInsertService { public TextChange? GetTextChangeForQuote(Document document, SourceText text, int caretPosition, CancellationToken cancellationToken) { return TryGenerateInitialEmptyRawString(text, document, caretPosition, cancellationToken) ?? TryGrowInitialEmptyRawString(text, document, caretPosition, cancellationToken) ?? TryGrowRawStringDelimiters(text, document, caretPosition, cancellationToken); } /// <summary> /// When typing <c>"</c> given a normal string like <c>""$$</c>, then update the text to be <c>"""$$"""</c>. /// Note that this puts the user in the position where TryGrowInitialEmptyRawString can now take effect. /// </summary> private static TextChange? TryGenerateInitialEmptyRawString( SourceText text, Document document, int position, CancellationToken cancellationToken) { // if we have ""$$" then typing `"` here should not be handled by this path but by TryGrowInitialEmptyRawString if (position + 1 < text.Length && text[position + 1] == '"') return null; var start = position; while (start - 1 >= 0 && text[start - 1] == '"') start--; // must have exactly `""` if (position - start != 2) return null; while (start - 1 >= 0 && text[start - 1] == '$') start--; // hitting `"` after `@""` shouldn't do anything if (start - 1 >= 0 && text[start - 1] == '@') return null; var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); var token = root.FindToken(start); if (token.SpanStart != start) return null; if (token.Kind() is not (SyntaxKind.StringLiteralToken or SyntaxKind.InterpolatedStringStartToken or SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken)) { return null; } return new TextChange(new TextSpan(position + 1, 0), "\"\"\""); } /// <summary> /// When typing <c>"</c> given a raw string like <c>"""$$"""</c> (or a similar multiline form), then update the /// text to be: <c>""""$$""""</c>. i.e. grow both the start and end delimiters to keep the string properly /// balanced. This differs from TryGrowRawStringDelimiters in that the language will consider that initial /// <c>""""""</c> text to be a single delimiter, while we want to treat it as two. /// </summary> private static TextChange? TryGrowInitialEmptyRawString( SourceText text, Document document, int position, CancellationToken cancellationToken) { var start = position; while (start - 1 >= 0 && text[start - 1] == '"') start--; var end = position; while (end < text.Length && text[end] == '"') end++; // Have to have an even number of quotes. var quoteLength = end - start; if (quoteLength % 2 == 1) return null; // User position must be halfway through the quotes. if (position != (start + quoteLength / 2)) return null; // have to at least have `"""$$"""` if (quoteLength < 6) return null; while (start - 1 >= 0 && text[start - 1] == '$') start--; var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); var token = root.FindToken(start); if (token.SpanStart != start) return null; if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken)) { return null; } return new TextChange(new TextSpan(position + 1, 0), "\""); } /// <summary> /// When typing <c>"</c> given a raw string like <c>"""$$ goo bar """</c> (or a similar multiline form), then /// update the text to be: <c>"""" goo bar """"</c>. i.e. grow both the start and end delimiters to keep the /// string properly balanced. /// </summary> private static TextChange? TryGrowRawStringDelimiters( SourceText text, Document document, int position, CancellationToken cancellationToken) { // if we have """$$" then typing `"` here should not grow the start/end quotes. we only want to grow them // if the user is at the end of the start delimiter. if (position < text.Length && text[position] == '"') return null; var start = position; while (start - 1 >= 0 && text[start - 1] == '"') start--; // must have at least three quotes for this to be a raw string var quoteCount = position - start; if (quoteCount < 3) return null; while (start - 1 >= 0 && text[start - 1] == '$') start--; var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken); var token = root.FindToken(start); if (token.SpanStart != start) return null; if (token.Span.Length < (2 * quoteCount)) return null; if (token.Kind() is SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken) { var interpolatedString = (InterpolatedStringExpressionSyntax)token.GetRequiredParent(); var endToken = interpolatedString.StringEndToken; if (!endToken.Text.EndsWith(new string('"', quoteCount))) return null; } else if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken) { if (!token.Text.EndsWith(new string('"', quoteCount))) return null; } return new TextChange(new TextSpan(token.GetRequiredParent().Span.End, 0), "\""); } }