Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for smart string/copy paste when the destination is a raw string literal. #61177

Merged
merged 32 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
641c027
Simplify
CyrusNajmabadi May 7, 2022
efcd274
in progress
CyrusNajmabadi May 7, 2022
dc9ed2a
in progress
CyrusNajmabadi May 7, 2022
9303de8
Add tests
CyrusNajmabadi May 7, 2022
a71bec9
Add tests
CyrusNajmabadi May 7, 2022
028c6b3
Add tests
CyrusNajmabadi May 7, 2022
97c363c
Add region
CyrusNajmabadi May 7, 2022
1aa1dff
Add tests
CyrusNajmabadi May 8, 2022
327aaa4
Add tests
CyrusNajmabadi May 8, 2022
28eaeca
Add region
CyrusNajmabadi May 8, 2022
79d5f1f
Add tests
CyrusNajmabadi May 8, 2022
a83b312
Add tests
CyrusNajmabadi May 8, 2022
6ab6c26
Add tests
CyrusNajmabadi May 8, 2022
af7f5b4
Add tests
CyrusNajmabadi May 8, 2022
dc3d0ef
Deal with selection at start of literal
CyrusNajmabadi May 8, 2022
6b9d0e0
Add tests
CyrusNajmabadi May 8, 2022
a667674
add tests
CyrusNajmabadi May 8, 2022
8a276a0
Add tests
CyrusNajmabadi May 8, 2022
f1815fe
Refactor
CyrusNajmabadi May 8, 2022
bfb8b27
Share more code
CyrusNajmabadi May 8, 2022
37809ab
Add tewsts
CyrusNajmabadi May 8, 2022
8d08467
Fix indentation when custom indentation is involved
CyrusNajmabadi May 8, 2022
9ec1a8a
Simplify
CyrusNajmabadi May 8, 2022
cceb6bf
add docs
CyrusNajmabadi May 8, 2022
f3ec855
Update src/EditorFeatures/CSharp/StringCopyPaste/KnownSourcePasteProc…
CyrusNajmabadi May 8, 2022
6296d27
add docs
CyrusNajmabadi May 8, 2022
c43d6f9
Merge branch 'stringCopyPaste5' of https://github.com/CyrusNajmabadi/…
CyrusNajmabadi May 8, 2022
a0aa425
add docs
CyrusNajmabadi May 8, 2022
f28f4d7
Docs
CyrusNajmabadi May 9, 2022
612bab7
lint
CyrusNajmabadi May 9, 2022
083c0a6
lint
CyrusNajmabadi May 9, 2022
30bf275
Cleanup
CyrusNajmabadi May 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,9 @@ internal abstract class AbstractPasteProcessor
protected readonly string NewLine;

/// <summary>
/// Users preferred indentation options. Used when dealing with raw strings that we have to convert from
/// single-line to multiline.
/// Amount to indent content in a multi-line raw string literal.
/// </summary>
protected readonly IndentationOptions IndentationOptions;
protected readonly string IndentationWhitespace;

/// <summary>
/// The set of <see cref="ITextChange"/>'s that produced <see cref="SnapshotAfterPaste"/> from <see
Expand All @@ -98,15 +97,15 @@ internal abstract class AbstractPasteProcessor

protected AbstractPasteProcessor(
string newLine,
IndentationOptions indentationOptions,
string indentationWhitespace,
ITextSnapshot snapshotBeforePaste,
ITextSnapshot snapshotAfterPaste,
Document documentBeforePaste,
Document documentAfterPaste,
ExpressionSyntax stringExpressionBeforePaste)
{
NewLine = newLine;
IndentationOptions = indentationOptions;
IndentationWhitespace = indentationWhitespace;

SnapshotBeforePaste = snapshotBeforePaste;
SnapshotAfterPaste = snapshotAfterPaste;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,22 @@ internal partial class StringCopyPasteCommandHandler :
private readonly ITextUndoHistoryRegistry _undoHistoryRegistry;
private readonly IEditorOperationsFactoryService _editorOperationsFactoryService;
private readonly IGlobalOptionService _globalOptions;
private readonly ITextBufferFactoryService2 _textBufferFactoryService;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public StringCopyPasteCommandHandler(
IThreadingContext threadingContext,
ITextUndoHistoryRegistry undoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService,
IGlobalOptionService globalOptions)
IGlobalOptionService globalOptions,
ITextBufferFactoryService2 textBufferFactoryService)
{
_threadingContext = threadingContext;
_undoHistoryRegistry = undoHistoryRegistry;
_editorOperationsFactoryService = editorOperationsFactoryService;
_globalOptions = globalOptions;
_textBufferFactoryService = textBufferFactoryService;
}

public string DisplayName => nameof(StringCopyPasteCommandHandler);
Expand Down Expand Up @@ -126,13 +129,7 @@ public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, Com
if (stringExpressionBeforePaste == null)
return;

var pasteWasSuccessful = PasteWasSuccessful(
snapshotBeforePaste, snapshotAfterPaste, documentAfterPaste, stringExpressionBeforePaste, cancellationToken);

var newLine = textView.Options.GetNewLineCharacter();
var indentationOptions = documentBeforePaste.GetIndentationOptionsAsync(_globalOptions, cancellationToken).WaitAndGetResult(cancellationToken);

var textChanges = GetEdits(newLine, indentationOptions, cancellationToken);
var textChanges = GetEdits(cancellationToken);

// If we didn't get any viable changes back, don't do anything.
if (textChanges.IsDefaultOrEmpty)
Expand Down Expand Up @@ -180,23 +177,31 @@ public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, Com
transaction.Complete();
return;

ImmutableArray<TextChange> GetEdits(string newLine, IndentationOptions indentationOptions, CancellationToken cancellationToken)
ImmutableArray<TextChange> GetEdits(CancellationToken cancellationToken)
{
var newLine = textView.Options.GetNewLineCharacter();
var indentationWhitespace = DetermineIndentationWhitespace(
documentBeforePaste, snapshotBeforePaste.AsText(), stringExpressionBeforePaste, cancellationToken);

// See if this is a paste of the last copy that we heard about.
var edits = TryGetEditsFromKnownCopySource(newLine, indentationOptions, cancellationToken);
var edits = TryGetEditsFromKnownCopySource(newLine, indentationWhitespace, cancellationToken);
if (!edits.IsDefaultOrEmpty)
return edits;

// If not, then just go through teh fallback code path that applies more heuristics.
var pasteWasSuccessful = PasteWasSuccessful(
snapshotBeforePaste, snapshotAfterPaste, documentAfterPaste, stringExpressionBeforePaste, cancellationToken);

// If not, then just go through the fallback code path that applies more heuristics.
var unknownPasteProcessor = new UnknownSourcePasteProcessor(
newLine, indentationOptions,
newLine, indentationWhitespace,
snapshotBeforePaste, snapshotAfterPaste,
documentBeforePaste, documentAfterPaste,
stringExpressionBeforePaste, pasteWasSuccessful);
return unknownPasteProcessor.GetEdits(cancellationToken);
}

ImmutableArray<TextChange> TryGetEditsFromKnownCopySource(string newLine, IndentationOptions indentationOptions, CancellationToken cancellationToken)
ImmutableArray<TextChange> TryGetEditsFromKnownCopySource(
string newLine, string indentationWhitespace, CancellationToken cancellationToken)
{
// For simplicity, we only support smart copy/paste when we are pasting into a single contiguous region.
if (selectionsBeforePaste.Count != 1)
Expand All @@ -212,16 +217,41 @@ ImmutableArray<TextChange> TryGetEditsFromKnownCopySource(string newLine, Indent
return default;

var knownProcessor = new KnownSourcePasteProcessor(
newLine, indentationOptions,
newLine, indentationWhitespace,
snapshotBeforePaste, snapshotAfterPaste,
documentBeforePaste, documentAfterPaste,
stringExpressionBeforePaste,
selectionsBeforePaste[0],
copyPasteData);
selectionsBeforePaste[0].Span.ToTextSpan(),
copyPasteData, _textBufferFactoryService);
return knownProcessor.GetEdits(cancellationToken);
}
}

private string DetermineIndentationWhitespace(
Document documentBeforePaste,
SourceText textBeforePaste,
ExpressionSyntax stringExpressionBeforePaste,
CancellationToken cancellationToken)
{
// Only raw strings care about indentation. Don't bother computing if we don't need it.
if (!IsAnyRawStringExpression(stringExpressionBeforePaste))
return "";

if (IsAnyMultiLineRawStringExpression(stringExpressionBeforePaste))
{
// already have a multi-line raw string. The indentation of it's end delimiter is the indentation all
// lines within it should have.
var lastLine = textBeforePaste.Lines.GetLineFromPosition(stringExpressionBeforePaste.Span.End);
var quotePosition = lastLine.GetFirstNonWhitespacePosition()!.Value;
return textBeforePaste.ToString(TextSpan.FromBounds(lastLine.Span.Start, quotePosition));
}

// Otherwise, we have a single-line raw string. Determine the default indentation desired here.
// We'll use that if we have to convert this single-line raw string to a multi-line one.
var indentationOptions = documentBeforePaste.GetIndentationOptionsAsync(_globalOptions, cancellationToken).WaitAndGetResult(cancellationToken);
return stringExpressionBeforePaste.GetFirstToken().GetPreferredIndentation(documentBeforePaste, indentationOptions, cancellationToken);
}

/// <summary>
/// Returns true if the paste resulted in legal code for the string literal. The string literal is
/// considered legal if it has the same span as the original string (adjusted as per the edit) and that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ internal sealed class UnknownSourcePasteProcessor : AbstractPasteProcessor

public UnknownSourcePasteProcessor(
string newLine,
IndentationOptions indentationOptions,
string indentationWhitespace,
ITextSnapshot snapshotBeforePaste,
ITextSnapshot snapshotAfterPaste,
Document documentBeforePaste,
Document documentAfterPaste,
ExpressionSyntax stringExpressionBeforePaste,
bool pasteWasSuccessful)
: base(newLine, indentationOptions, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste)
: base(newLine, indentationWhitespace, snapshotBeforePaste, snapshotAfterPaste, documentBeforePaste, documentAfterPaste, stringExpressionBeforePaste)
{
_pasteWasSuccessful = pasteWasSuccessful;
}
Expand Down Expand Up @@ -132,7 +132,7 @@ private ImmutableArray<TextChange> GetEditsForRawString(CancellationToken cancel
if (dollarSignsToAdd != null)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePaste.Span.Start, 0), dollarSignsToAdd));

// Then any quotes to your starting delimiter
// Then any quotes to the start delimiter.
if (quotesToAdd != null)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.ContentSpans.First().Start, 0), quotesToAdd));

Expand All @@ -143,7 +143,7 @@ private ImmutableArray<TextChange> GetEditsForRawString(CancellationToken cancel
else
AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(edits, cancellationToken);

// Then add any extra end quotes needed.
// Then any extra quotes to the end delimiter.
if (quotesToAdd != null)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpanWithoutSuffix.End, 0), quotesToAdd));

Expand Down Expand Up @@ -172,13 +172,11 @@ private void AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(

var mustBeMultiLine = RawContentMustBeMultiLine(TextAfterPaste, TextContentsSpansAfterPaste);

var indentationWhitespace = StringExpressionBeforePaste.GetFirstToken().GetPreferredIndentation(DocumentBeforePaste, IndentationOptions, cancellationToken);

using var _ = PooledStringBuilder.GetInstance(out var buffer);

// A newline and the indentation to start with.
if (mustBeMultiLine)
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + indentationWhitespace));
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.StartDelimiterSpan.End, 0), NewLine + IndentationWhitespace));

SourceText? textOfCurrentChange = null;
var commonIndentationPrefix = GetCommonIndentationPrefix(Changes) ?? "";
Expand Down Expand Up @@ -213,7 +211,7 @@ private void AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(

// if we ended with a newline, make sure the next line is indented enough.
if (HasNewLine(currentChangeLine))
buffer.Append(indentationWhitespace);
buffer.Append(IndentationWhitespace);
}

edits.Add(new TextChange(change.OldSpan.ToTextSpan(), buffer.ToString()));
Expand All @@ -222,7 +220,7 @@ private void AdjustWhitespaceAndAddTextChangesForSingleLineRawStringLiteral(
// if the last change ended at the closing delimiter *and* ended with a newline, then we don't need to add a
// final newline-space at the end because we will have already done that.
if (mustBeMultiLine && !LastPastedLineAddedNewLine())
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + indentationWhitespace));
edits.Add(new TextChange(new TextSpan(StringExpressionBeforePasteInfo.EndDelimiterSpan.Start, 0), NewLine + IndentationWhitespace));

return;

Expand Down
Loading