|
// 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.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste;
using static StringCopyPasteHelpers;
/// <summary>
/// Implementation of <see cref="AbstractPasteProcessor"/> used when we know the original string literal expression
/// we were copying text out of. Because we know the original literal expression, we can determine what the
/// characters being pasted meant in the original context and we can attempt to preserve that as closely as
/// possible.
/// </summary>
internal class KnownSourcePasteProcessor(
string newLine,
string indentationWhitespace,
ITextSnapshot snapshotBeforePaste,
ITextSnapshot snapshotAfterPaste,
Document documentBeforePaste,
Document documentAfterPaste,
ExpressionSyntax stringExpressionBeforePaste,
TextSpan selectionSpanBeforePaste,
StringCopyPasteData copyPasteData,
ITextBufferFactoryService2 textBufferFactoryService) : AbstractPasteProcessor(newLine, indentationWhitespace, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste)
{
/// <summary>
/// The selection in the document prior to the paste happening.
/// </summary>
private readonly TextSpan _selectionSpanBeforePaste = selectionSpanBeforePaste;
/// <summary>
/// Information stored to the clipboard about the original cut/copy.
/// </summary>
private readonly StringCopyPasteData _copyPasteData = copyPasteData;
private readonly ITextBufferFactoryService2 _textBufferFactoryService = textBufferFactoryService;
public override ImmutableArray<TextChange> GetEdits()
{
// For pastes into non-raw strings, we can just determine how the change should be escaped in-line at that
// same location the paste originally happened at. For raw-strings things get more complex as we have to
// deal with things like indentation and potentially adding newlines to make things legal.
// Smart Pasting into raw string not supported yet.
return IsAnyRawStringExpression(StringExpressionBeforePaste)
? GetEditsForRawString()
: GetEditsForNonRawString();
}
private string EscapeForNonRawStringLiteral(string value)
=> EscapeForNonRawStringLiteral_DoNotCallDirectly(
IsVerbatimStringExpression(StringExpressionBeforePaste),
StringExpressionBeforePaste is InterpolatedStringExpressionSyntax,
// We do not want to try skipping escapes in the 'value'. We know exactly what 'value' means and don't
// want it touched.
trySkipExistingEscapes: false,
value);
private ImmutableArray<TextChange> GetEditsForNonRawString()
{
using var _ = PooledStringBuilder.GetInstance(out var builder);
var isLiteral = StringExpressionBeforePaste is LiteralExpressionSyntax;
foreach (var content in _copyPasteData.Contents)
{
if (content.IsText)
{
builder.Append(EscapeForNonRawStringLiteral(content.TextValue));
}
else if (content.IsInterpolation)
{
builder.Append('{');
if (isLiteral)
{
// we're copying an interpolation from an interpolated string to a string literal. For example,
// we're pasting `{x + y}` into the middle of `"goobar"`. One thing we could potentially do in
// the future is split the literal into `"goo" + $"{x + y}" + "bar"`, or just making the
// containing literal into an interpolation itself. However, for now, we do the simple thing
// and just treat the interpolation as raw text that should just be escaped as appropriate into
// the destination.
builder.Append(EscapeForNonRawStringLiteral(content.InterpolationExpression));
}
else
{
// we're moving an interpolation from one interpolation to another. This can just be copied
// wholesale *except* for the format literal portion (e.g. `{...:XXXX}` which may have to be
// updated for the destination type.
builder.Append(content.InterpolationExpression);
}
builder.Append(content.InterpolationAlignmentClause);
if (content.InterpolationFormatClause != null)
{
builder.Append(':');
builder.Append(EscapeForNonRawStringLiteral(content.InterpolationFormatClause));
}
builder.Append('}');
}
else
{
throw ExceptionUtilities.UnexpectedValue(content.Kind);
}
}
return [new TextChange(_selectionSpanBeforePaste, builder.ToString())];
}
private ImmutableArray<TextChange> GetEditsForRawString()
{
// To make a change to a raw string we have to go through several passes to determine what to do.
//
// First, just take the copied text and determine the most basic edit that would insert it into the
// destination string. Importantly, do not insert interpolations in this step (instead just replace them
// with a dummy character).
//
// Second, after this text is inserted, look at the content regions of the string after the paste and look
// at the sequences of `"` and `{` in them to see if we need to update the delimiters of the raw string.
// Note: this is why it is critical that any interpolations are not inserted. We don't want the content of
// the interpolation to affect the delimiters. e.g. a interpolation containing `""""` *inside* of it
// doesn't require updating the delimiters of the outer raw-string expression.
//
// Also, after the text is inserted, look to see if we need to convert a single-line raw expression to
// multi-line.
//
// At this point, we will now have the information necessary to actually insert the content and do things
// like give interpolations the proper number of braces for the final string we're making.
var (quotesToAdd, dollarSignsToAdd, convertToMultiLine) = DetermineTopLevelChangesToMakeToRawString();
return DetermineTotalEditsToMakeToRawString(quotesToAdd, dollarSignsToAdd, convertToMultiLine);
}
private (string? quotesToAdd, string? dollarSignsToAdd, bool convertToMultiLine) DetermineTopLevelChangesToMakeToRawString()
{
PerformInitialBasicPasteInRawString(out var textAfterBasicPaste, out var contentSpansAfterBasicPaste);
var convertToMultiLine = !IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste) && RawContentMustBeMultiLine(textAfterBasicPaste, contentSpansAfterBasicPaste);
return (GetQuotesToAddToRawString(textAfterBasicPaste, contentSpansAfterBasicPaste),
GetDollarSignsToAddToRawString(textAfterBasicPaste, contentSpansAfterBasicPaste),
convertToMultiLine);
}
private void PerformInitialBasicPasteInRawString(
out SourceText textAfterBasicPaste, out ImmutableArray<TextSpan> contentSpansAfterBasicPaste)
{
var trivialContentEdit = GetContentEditForRawString(insertInterpolations: false, dollarSignCount: -1);
// We want to map spans forward (which requires tracking spans), but we don't want to modify the original
// text buffer. So clone the text buffer to a new one where we can then make the change without touching
// the original.
var clonedBuffer = _textBufferFactoryService.CreateTextBuffer(
new SnapshotSpan(SnapshotBeforePaste, 0, SnapshotBeforePaste.Length), SnapshotBeforePaste.ContentType);
var snapshotBeforeTrivialEdit = clonedBuffer.CurrentSnapshot;
var edit = clonedBuffer.CreateEdit();
edit.Replace(_selectionSpanBeforePaste.ToSpan(), trivialContentEdit.NewText);
var snapshotAfterTrivialEdit = edit.Apply();
textAfterBasicPaste = snapshotAfterTrivialEdit.AsText();
contentSpansAfterBasicPaste = StringExpressionBeforePasteInfo.ContentSpans.SelectAsArray(
ts => MapSpan(ts, snapshotBeforeTrivialEdit, snapshotAfterTrivialEdit));
}
private ImmutableArray<TextChange> DetermineTotalEditsToMakeToRawString(
string? quotesToAdd, string? dollarSignsToAdd, bool convertToMultiLine)
{
var finalDollarSignCount = StringExpressionBeforePasteInfo.DelimiterDollarCount +
(dollarSignsToAdd == null ? 0 : dollarSignsToAdd.Length);
using var _ = ArrayBuilder<TextChange>.GetInstance(out var edits);
// First, add any extra dollar signs needed.
if (dollarSignsToAdd != null)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePaste.Span.Start, 0), dollarSignsToAdd));
// Then any quotes to the start delimiter.
if (quotesToAdd != null)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.ContentSpans.First().Start, 0), quotesToAdd));
// A newline and the indentation to start with. Note: adding the indentation here means that existing
// content will start at the right location, as will any content we are pasting in.
if (convertToMultiLine)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace));
// If we need to add braces to existing interpolations, do so now for the interpolations after the selection.
if (dollarSignsToAdd != null)
UpdateExistingInterpolationBraces(edits, beforeSelection: true, dollarSignsToAdd.Length);
// Now determine the actual content to add again, this time properly emitting it with
// indentation/interpolations correctly.
edits.Add(GetContentEditForRawString(insertInterpolations: true, finalDollarSignCount));
// If we need to add braces to existing interpolations, do so now for the interpolations before the selection.
if (dollarSignsToAdd != null)
UpdateExistingInterpolationBraces(edits, beforeSelection: false, dollarSignsToAdd.Length);
// A final new-line and indentation before the end delimiter.
if (convertToMultiLine)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + IndentationWhitespace));
// Then any extra quotes to the end delimiter.
if (quotesToAdd != null)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpanWithoutSuffix.End, 0), quotesToAdd));
return edits.ToImmutableAndClear();
}
private void UpdateExistingInterpolationBraces(
ArrayBuilder<TextChange> edits, bool beforeSelection, int dollarSignsToAdd)
{
var interpolatedStringExpression = (InterpolatedStringExpressionSyntax)StringExpressionBeforePaste;
foreach (var content in interpolatedStringExpression.Contents)
{
if (content is InterpolationSyntax interpolation)
{
if (beforeSelection && interpolation.Span.End > _selectionSpanBeforePaste.Start)
continue;
if (!beforeSelection && interpolation.Span.Start < _selectionSpanBeforePaste.End)
continue;
edits.Add(new TextChange(new TextSpan(interpolation.OpenBraceToken.Span.End, 0), new string('{', dollarSignsToAdd)));
edits.Add(new TextChange(new TextSpan(interpolation.CloseBraceToken.Span.Start, 0), new string('}', dollarSignsToAdd)));
}
}
}
private TextChange GetContentEditForRawString(
bool insertInterpolations, int dollarSignCount)
{
dollarSignCount = Math.Max(1, dollarSignCount);
using var _ = PooledStringBuilder.GetInstance(out var builder);
var isLiteral = StringExpressionBeforePaste is LiteralExpressionSyntax;
var isMultiLine = IsAnyMultiLineRawStringExpression(StringExpressionBeforePaste);
for (var contentIndex = 0; contentIndex < _copyPasteData.Contents.Length; contentIndex++)
{
// Special handling for the first thing being pasted if we are pasting into a multi-line expression.
if (isMultiLine && contentIndex == 0)
{
TextBeforePaste.GetLineAndOffset(_selectionSpanBeforePaste.Start, out var line, out var offset);
if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.SpanStart).LineNumber)
{
// the user selection starts on the line containing the leading delimiter. e.g.
//
// var v = """ [|
// content|]
// """
//
// In this case, ensure we add a new-line + indentation so that the copied
// text will actually start in the right location.
builder.Append(NewLine);
builder.Append(IndentationWhitespace);
}
else if (offset < IndentationWhitespace.Length)
{
// if the line they're pasting into doesn't have enough indentation whitespace, then
// add enough whitespace to make the text insertion point level. e.g.:
//
// var v = """
// [| content|]
// """
builder.Append(IndentationWhitespace[offset..]);
}
}
var content = _copyPasteData.Contents[contentIndex];
SourceText? lastContentSourceText = null;
if (content.IsText)
{
// Convert the string to a source-text instance so we can easily process it one line at a time.
var sourceText = SourceText.From(content.TextValue);
lastContentSourceText = sourceText;
for (var i = 0; i < sourceText.Lines.Count; i++)
{
if (i != 0)
builder.Append(IndentationWhitespace);
builder.Append(sourceText.ToString(sourceText.Lines[i].SpanIncludingLineBreak));
}
}
else if (content.IsInterpolation)
{
if (!insertInterpolations)
{
// Just insert a basic string that represents the interpolation, but doesn't actually insert any
// potential " or { characters that might screw up later computations.
builder.Append('0');
}
else
{
builder.Append(new string('{', dollarSignCount));
builder.Append(content.InterpolationExpression);
builder.Append(content.InterpolationAlignmentClause);
if (content.InterpolationFormatClause != null)
{
builder.Append(':');
builder.Append(content.InterpolationFormatClause);
}
builder.Append(new string('}', dollarSignCount));
}
}
else
{
throw ExceptionUtilities.UnexpectedValue(content.Kind);
}
if (isMultiLine && contentIndex == _copyPasteData.Contents.Length - 1)
{
// Similar to the check we do for the first-change, if the last change was pasted into the space
// before the last `"""` then we need potentially insert a newline, then enough indentation
// whitespace to keep delimiter in the right location.
TextBeforePaste.GetLineAndOffset(_selectionSpanBeforePaste.End, out var line, out var offset);
if (line == TextBeforePaste.Lines.GetLineFromPosition(StringExpressionBeforePaste.Span.End).LineNumber)
{
var hasNewLine = content.IsText && HasNewLine(lastContentSourceText!.Lines.Last());
if (!hasNewLine)
builder.Append(NewLine);
builder.Append(TextBeforePaste.ToString(new TextSpan(TextBeforePaste.Lines[line].Start, offset)));
}
}
}
return new TextChange(_selectionSpanBeforePaste, builder.ToString());
}
}
|