/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/CSharp/StringCopyPaste/KnownSourcePasteProcessor.cs
348 строк
17 KB
Stuart Lang
Use Append overload for repeating chars
28 июл 2025, 02:05
28 июл 2025, 02:05
dc2b6c4
Код
Авторство
О чём код?
// 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; 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 sealed 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('{', dollarSignCount); builder.Append(content.InterpolationExpression); builder.Append(content.InterpolationAlignmentClause); if (content.InterpolationFormatClause != null) { builder.Append(':'); builder.Append(content.InterpolationFormatClause); } builder.Append('}', 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()); } }