From 6cd2b954bd36a4ad41d82cf38dda54b22ef4ecd9 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 15 Jul 2025 10:46:03 -0700 Subject: [PATCH] Fix issue where we weren't properly preserving indentation when pasting into a string literal --- .../StringCopyPaste/StringCopyPasteHelpers.cs | 41 +++++++++++++++---- .../UnknownSourcePasteProcessor.cs | 10 ++++- ...nknownSourceIntoMultiLineRawStringTests.cs | 31 ++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs index f2805572e35af..fdf6ae572836b 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs @@ -7,12 +7,14 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; +using Microsoft.VisualStudio.Debugger.Contracts.EditAndContinue.VsdbgIntegration; using Microsoft.VisualStudio.Text; namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste; @@ -497,19 +499,39 @@ static bool WillEscapeAnyCharacters(bool isInterpolated, string value) string? commonIndentPrefix = null; var first = true; + using var allLines = TemporaryArray.Empty; + foreach (var change in textChanges) { var text = SourceText.From(change.NewText); foreach (var line in text.Lines) { - if (first) - { - first = false; - continue; - } - var nonWhitespaceIndex = GetFirstNonWhitespaceIndex(text, line); - if (nonWhitespaceIndex >= 0) + + // For the first line, we only want to consider its indentation if it *has* any. That's because + // people often copy by avoiding indentation on the first line and starting their selection on the + // first real construct. e.g.: + // + // [|Goo + // Bar + // Baz|] + // + // In this case, we don't want to say that there is no common indentation to trim since there is + // no indentation on the selection's first line. However, we do want to trim if the user selected + // whitespace a-la: + // + // [|Goo + // Bar + // Baz|] + // + // In this case, we really only want to trim the common whitespace of all three lines, not the whitespace + // of the second/third lines. If we do the latter, we'd end up with Goo and Bar being aligned, which + // doesn't match the original intent. + + var minimumStartColumn = first ? 1 : 0; + first = false; + + if (nonWhitespaceIndex >= minimumStartColumn) commonIndentPrefix = GetCommonIndentationPrefix(commonIndentPrefix, text, TextSpan.FromBounds(line.Start, nonWhitespaceIndex)); } } @@ -554,6 +576,11 @@ public static bool RawContentMustBeMultiLine(SourceText text, ImmutableArray spans) + { foreach (var span in spans) { for (var i = span.Start; i < span.End; i++) diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs b/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs index 8e5611ff9494e..e72ba8aeba5a3 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/UnknownSourcePasteProcessor.cs @@ -168,9 +168,15 @@ private void AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(Arra if (mustBeMultiLine) edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace)); - SourceText? textOfCurrentChange = null; - var commonIndentationPrefix = GetCommonIndentationPrefix(Changes) ?? ""; + // Only if we're ending with a multi-line raw string do we want to consider the first line when determining + // the common indentation prefix to trim out. If we don't have a multi-line raw string, then that means we + // pasted a boring single-line string into a single-line raw string, and in that case, we don't want to touch + // the contents at all. + var commonIndentationPrefix = SpansContainsNewLine(TextAfterPaste, TextContentsSpansAfterPaste) + ? GetCommonIndentationPrefix(Changes) ?? "" + : ""; + SourceText? textOfCurrentChange = null; foreach (var change in Changes) { // Create a text object around the change text we're making. This is a very simple way to get diff --git a/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoMultiLineRawStringTests.cs b/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoMultiLineRawStringTests.cs index 539eaca72c7bf..ce4e3b40005a1 100644 --- a/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoMultiLineRawStringTests.cs +++ b/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoMultiLineRawStringTests.cs @@ -753,6 +753,37 @@ public void TestNormalTextIntoMultiLineRawStringMultiLine13() """"); } + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/74661")] + public void TestNormalTextIntoMultiLineRawStringMultiLine14() + { + TestPasteUnknownSource( + pasteText: """ + abc + def + ghi + """, + """" + var x = """ + [||] + """ + """", + """" + var x = """ + abc + def + ghi[||] + """ + """", + afterUndo: + """" + var x = """ + abc + def + ghi[||] + """ + """"); + } + [WpfFact] public void TestNormalTextIntoMultiLineRawStringHeader1() {