提交 db55807b 编写于 作者: C Cyrus Najmabadi

Support fix-all for resolving conflict markers.

上级 146b3d74
......@@ -8,6 +8,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ConflictMarkerResolution
......@@ -448,6 +449,147 @@ static void Main2(string[] args)
}
}
}", index: 2);
}
[WorkItem(21107, "https://github.com/dotnet/roslyn/issues/21107")]
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsResolveConflictMarker)]
public async Task TestFixAll1()
{
await TestInRegularAndScript1Async(
@"
using System;
namespace N
{
{|FixAllInDocument:<<<<<<<|} This is mine!
class Program
{
}
=======
class Program2
{
}
>>>>>>> This is theirs!
<<<<<<< This is mine!
class Program3
{
}
=======
class Program4
{
}
>>>>>>> This is theirs!
}",
@"
using System;
namespace N
{
class Program
{
}
class Program3
{
}
}", index: 0);
}
[WorkItem(21107, "https://github.com/dotnet/roslyn/issues/21107")]
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsResolveConflictMarker)]
public async Task TestFixAll2()
{
await TestInRegularAndScript1Async(
@"
using System;
namespace N
{
{|FixAllInDocument:<<<<<<<|} This is mine!
class Program
{
}
=======
class Program2
{
}
>>>>>>> This is theirs!
<<<<<<< This is mine!
class Program3
{
}
=======
class Program4
{
}
>>>>>>> This is theirs!
}",
@"
using System;
namespace N
{
class Program2
{
}
class Program4
{
}
}", index: 1);
}
[WorkItem(21107, "https://github.com/dotnet/roslyn/issues/21107")]
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsResolveConflictMarker)]
public async Task TestFixAll3()
{
await TestInRegularAndScript1Async(
@"
using System;
namespace N
{
{|FixAllInDocument:<<<<<<<|} This is mine!
class Program
{
}
=======
class Program2
{
}
>>>>>>> This is theirs!
<<<<<<< This is mine!
class Program3
{
}
=======
class Program4
{
}
>>>>>>> This is theirs!
}",
@"
using System;
namespace N
{
class Program
{
}
class Program2
{
}
class Program3
{
}
class Program4
{
}
}", index: 2);
}
}
......
......@@ -6,18 +6,26 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.ConflictMarkerResolution
{
internal abstract class AbstractResolveConflictMarkerCodeFixProvider : CodeFixProvider
internal abstract class AbstractResolveConflictMarkerCodeFixProvider : SyntaxEditorBasedCodeFixProvider
{
private const string TakeTopEquivalenceKey = nameof(TakeTopEquivalenceKey);
private const string TakeBottomEquivalenceKey = nameof(TakeBottomEquivalenceKey);
private const string TakeBothEquivalenceKey = nameof(TakeBothEquivalenceKey);
private static readonly int s_mergeConflictLength = "<<<<<<<".Length;
private readonly ISyntaxKinds _syntaxKinds;
......@@ -29,15 +37,10 @@ internal abstract class AbstractResolveConflictMarkerCodeFixProvider : CodeFixPr
_syntaxKinds = syntaxKinds;
}
public override FixAllProvider? GetFixAllProvider()
{
// Fix All is not currently supported for this code fix
// https://github.com/dotnet/roslyn/issues/34461
return null;
}
public override ImmutableArray<string> FixableDiagnosticIds { get; }
internal override CodeFixCategory CodeFixCategory => CodeFixCategory.Compile;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var cancellationToken = context.CancellationToken;
......@@ -46,17 +49,25 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var startTrivia = root.FindTrivia(context.Span.Start);
if (!IsConflictMarker(text, startTrivia, '<'))
{
return;
}
var conflictTrivia = TryGetConflictTrivia(text, startTrivia);
if (conflictTrivia == null)
return;
var (equalsTrivia, endTrivia) = conflictTrivia.Value;
RegisterCodeFixes(context, startTrivia, equalsTrivia, endTrivia);
}
private (SyntaxTrivia equalsTrivia, SyntaxTrivia endTrivia)? TryGetConflictTrivia(
SourceText text, SyntaxTrivia startTrivia)
{
var token = startTrivia.Token;
while (true)
{
var index = GetEqualsConflictMarkerIndex(text, token);
var index = GetEqualsConflictMarkerIndex(text, token, afterPosition: startTrivia.SpanStart);
if (index >= 0)
{
var leadingTrivia = token.LeadingTrivia;
......@@ -74,8 +85,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
_syntaxKinds.DisabledTextTrivia == disabledTrivia.RawKind &&
IsConflictMarker(text, endTrivia, '>'))
{
RegisterCodeFixes(context, startTrivia, equalsTrivia, endTrivia);
return;
return (equalsTrivia, endTrivia);
}
}
......@@ -90,8 +100,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
if (_syntaxKinds.EndOfLineTrivia == endOfLineTrivia.RawKind &&
IsConflictMarker(text, endTrivia, '>'))
{
RegisterCodeFixes(context, startTrivia, equalsTrivia, endTrivia);
return;
return (equalsTrivia, endTrivia);
}
}
}
......@@ -99,7 +108,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
token = token.GetNextToken(includeZeroWidth: true);
if (token.RawKind == 0)
{
return;
return default;
}
}
}
......@@ -125,77 +134,99 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
context.RegisterCodeFix(
new MyCodeAction(takeTopText,
c => TakeTopAsync(document, startPos, equalsPos, endPos, c)),
c => TakeTopAsync(document, startPos, equalsPos, endPos, c),
TakeTopEquivalenceKey),
context.Diagnostics);
context.RegisterCodeFix(
new MyCodeAction(takeBottomText,
c => TakeBottomAsync(document, startPos, equalsPos, endPos, c)),
c => TakeBottomAsync(document, startPos, equalsPos, endPos, c),
TakeBottomEquivalenceKey),
context.Diagnostics);
context.RegisterCodeFix(
new MyCodeAction(FeaturesResources.Take_both,
c => TakeBothAsync(document, startPos, equalsPos, endPos, c)),
c => TakeBothAsync(document, startPos, equalsPos, endPos, c),
TakeBothEquivalenceKey),
context.Diagnostics);
}
private async Task<Document> TakeTopAsync(
private async Task<Document> AddEditsAsync(
Document document, int startPos, int equalsPos, int endPos,
Action<SourceText, ArrayBuilder<TextChange>, int, int, int> addEdits,
CancellationToken cancellationToken)
{
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var bottomEnd = GetEndIncludingLineBreak(text, endPos);
var newText = text.Replace(TextSpan.FromBounds(equalsPos, bottomEnd), "");
var startEnd = GetEndIncludingLineBreak(text, startPos);
var finaltext = newText.Replace(TextSpan.FromBounds(startPos, startEnd), "");
using var _ = ArrayBuilder<TextChange>.GetInstance(out var edits);
addEdits(text, edits, startPos, equalsPos, endPos);
return document.WithText(finaltext);
var finalText = text.WithChanges(edits);
return document.WithText(finalText);
}
private async Task<Document> TakeBottomAsync(
Document document, int startPos, int equalsPos, int endPos,
CancellationToken cancellationToken)
private static void AddTopEdits(
SourceText text, ArrayBuilder<TextChange> edits,
int startPos, int equalsPos, int endPos)
{
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
// Delete the line containing <<<<<<<
var startEnd = GetEndIncludingLineBreak(text, startPos);
edits.Add(new TextChange(TextSpan.FromBounds(startPos, startEnd), ""));
// Remove the chunk of text (inclusive) from ======= through >>>>>>>
var bottomEnd = GetEndIncludingLineBreak(text, endPos);
var newText = text.Replace(TextSpan.FromBounds(endPos, bottomEnd), "");
edits.Add(new TextChange(TextSpan.FromBounds(equalsPos, bottomEnd), ""));
}
private static void AddBottomEdits(
SourceText text, ArrayBuilder<TextChange> edits,
int startPos, int equalsPos, int endPos)
{
// Remove the chunk of text (inclusive) from <<<<<<< through =======
var equalsEnd = GetEndIncludingLineBreak(text, equalsPos);
var finaltext = newText.Replace(TextSpan.FromBounds(startPos, equalsEnd), "");
edits.Add(new TextChange(TextSpan.FromBounds(startPos, equalsEnd), ""));
return document.WithText(finaltext);
// Delete the line containing >>>>>>>
var bottomEnd = GetEndIncludingLineBreak(text, endPos);
edits.Add(new TextChange(TextSpan.FromBounds(endPos, bottomEnd), ""));
}
private async Task<Document> TakeBothAsync(
Document document, int startPos, int equalsPos, int endPos,
CancellationToken cancellationToken)
private static void AddBothEdits(
SourceText text, ArrayBuilder<TextChange> edits,
int startPos, int equalsPos, int endPos)
{
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
// Delete the line containing <<<<<<<
var startEnd = GetEndIncludingLineBreak(text, startPos);
edits.Add(new TextChange(TextSpan.FromBounds(startPos, startEnd), ""));
// Delete the line containing =======
var equalsEnd = GetEndIncludingLineBreak(text, equalsPos);
edits.Add(new TextChange(TextSpan.FromBounds(equalsPos, equalsEnd), ""));
// Delete the line containing >>>>>>>
var bottomEnd = GetEndIncludingLineBreak(text, endPos);
var newText = text.Replace(TextSpan.FromBounds(endPos, bottomEnd), "");
edits.Add(new TextChange(TextSpan.FromBounds(endPos, bottomEnd), ""));
}
var equalsEnd = GetEndIncludingLineBreak(text, equalsPos);
newText = newText.Replace(TextSpan.FromBounds(equalsPos, equalsEnd), "");
private Task<Document> TakeTopAsync(Document document, int startPos, int equalsPos, int endPos, CancellationToken cancellationToken)
=> AddEditsAsync(document, startPos, equalsPos, endPos, AddTopEdits, cancellationToken);
var startEnd = GetEndIncludingLineBreak(text, startPos);
var finaltext = newText.Replace(TextSpan.FromBounds(startPos, startEnd), "");
private Task<Document> TakeBottomAsync(Document document, int startPos, int equalsPos, int endPos, CancellationToken cancellationToken)
=> AddEditsAsync(document, startPos, equalsPos, endPos, AddBottomEdits, cancellationToken);
return document.WithText(finaltext);
}
private Task<Document> TakeBothAsync(Document document, int startPos, int equalsPos, int endPos, CancellationToken cancellationToken)
=> AddEditsAsync(document, startPos, equalsPos, endPos, AddBothEdits, cancellationToken);
private int GetEndIncludingLineBreak(SourceText text, int position)
private static int GetEndIncludingLineBreak(SourceText text, int position)
=> text.Lines.GetLineFromPosition(position).SpanIncludingLineBreak.End;
private int GetEqualsConflictMarkerIndex(SourceText text, SyntaxToken token)
private int GetEqualsConflictMarkerIndex(SourceText text, SyntaxToken token, int afterPosition)
{
if (token.HasLeadingTrivia)
{
var i = 0;
foreach (var trivia in token.LeadingTrivia)
{
if (IsConflictMarker(text, trivia, '='))
if (trivia.SpanStart >= afterPosition &&
IsConflictMarker(text, trivia, '='))
{
return i;
}
......@@ -207,11 +238,6 @@ private int GetEqualsConflictMarkerIndex(SourceText text, SyntaxToken token)
return -1;
}
private SyntaxTrivia GetEqualsConflictMarker(SyntaxToken token)
{
throw new NotImplementedException();
}
private bool IsConflictMarker(SourceText text, SyntaxTrivia trivia, char ch)
{
return
......@@ -220,10 +246,74 @@ private bool IsConflictMarker(SourceText text, SyntaxTrivia trivia, char ch)
text[trivia.SpanStart] == ch;
}
protected override async Task FixAllAsync(
Document document, ImmutableArray<Diagnostic> diagnostics,
SyntaxEditor editor, string equivalenceKey,
CancellationToken cancellationToken)
{
Debug.Assert(
equivalenceKey == TakeTopEquivalenceKey ||
equivalenceKey == TakeBottomEquivalenceKey ||
equivalenceKey == TakeBothEquivalenceKey);
// Process diagnostics in order so we produce edits in the right order.
var orderedDiagnostics = diagnostics.OrderBy(
(d1, d2) => d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start).ToImmutableArray();
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
using var _ = ArrayBuilder<TextChange>.GetInstance(out var edits);
foreach (var diagnostic in diagnostics)
{
var startTrivia = root.FindTrivia(diagnostic.Location.SourceSpan.Start);
// We'll be called on all the conflict marker diagnostics (i.e. for <<<<<<< =======
// and >>>>>>>). We only care about the <<<<<<< ones as that controls which chunks
// we'll be processing.
if (!IsConflictMarker(text, startTrivia, '<'))
continue;
var conflictTrivia = TryGetConflictTrivia(text, startTrivia);
if (conflictTrivia == null)
continue;
var startPos = startTrivia.SpanStart;
var equalsPos = conflictTrivia.Value.equalsTrivia.SpanStart;
var endPos = conflictTrivia.Value.endTrivia.SpanStart;
switch (equivalenceKey)
{
case TakeTopEquivalenceKey:
AddTopEdits(text, edits, startPos, equalsPos, endPos);
continue;
case TakeBottomEquivalenceKey:
AddBottomEdits(text, edits, startPos, equalsPos, endPos);
continue;
case TakeBothEquivalenceKey:
AddBothEdits(text, edits, startPos, equalsPos, endPos);
continue;
default:
throw ExceptionUtilities.UnexpectedValue(equivalenceKey);
}
}
var finalText = text.WithChanges(edits);
var finalTree = tree.WithChangedText(finalText);
var finalRoot = finalTree.GetRoot(cancellationToken);
editor.ReplaceNode(root, finalRoot);
}
private class MyCodeAction : CodeAction.DocumentChangeAction
{
public MyCodeAction(string title, Func<CancellationToken, Task<Document>> createChangedDocument)
: base(title, createChangedDocument)
public MyCodeAction(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string equivalenceKey)
: base(title, createChangedDocument, equivalenceKey)
{
}
}
......
......@@ -2,15 +2,11 @@
// 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 System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.GeneratedCodeRecognition;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CodeFixes
{
......@@ -81,13 +77,16 @@ public sealed override async Task<CodeAction> GetFixAsync(FixAllContext fixAllCo
private async Task<Document> FixDocumentAsync(
Document document, ImmutableArray<Diagnostic> diagnostics, FixAllContext fixAllContext)
{
var cancellationToken = fixAllContext.CancellationToken;
var equivalenceKey = fixAllContext.CodeActionEquivalenceKey;
// Ensure that diagnostics for this document are always in document location
// order. This provides a consistent and deterministic order for fixers
// that want to update a document.
// Also ensure that we do not pass in duplicates by invoking Distinct.
// See https://github.com/dotnet/roslyn/issues/31381, that seems to be causing duplicate diagnostics.
var filteredDiagnostics = diagnostics.Distinct()
.WhereAsArray(d => _codeFixProvider.IncludeDiagnosticDuringFixAll(d, fixAllContext.Document, fixAllContext.CodeActionEquivalenceKey, fixAllContext.CancellationToken))
.WhereAsArray(d => _codeFixProvider.IncludeDiagnosticDuringFixAll(d, fixAllContext.Document, equivalenceKey, cancellationToken))
.Sort((d1, d2) => d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start);
// PERF: Do not invoke FixAllAsync on the code fix provider if there are no diagnostics to be fixed.
......@@ -96,7 +95,8 @@ public sealed override async Task<CodeAction> GetFixAsync(FixAllContext fixAllCo
return document;
}
return await _codeFixProvider.FixAllAsync(document, filteredDiagnostics, fixAllContext.CancellationToken).ConfigureAwait(false);
return await _codeFixProvider.FixAllAsync(
document, filteredDiagnostics, equivalenceKey, cancellationToken).ConfigureAwait(false);
}
}
}
......
......@@ -22,17 +22,15 @@ protected SyntaxEditorBasedCodeFixProvider(bool supportsFixAll = true)
public sealed override FixAllProvider GetFixAllProvider()
=> _supportsFixAll ? new SyntaxEditorBasedFixAllProvider(this) : null;
protected Task<Document> FixAsync(
Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
return FixAllAsync(document, ImmutableArray.Create(diagnostic), cancellationToken);
}
protected Task<Document> FixAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
=> FixAllAsync(document, ImmutableArray.Create(diagnostic), equivalenceKey: "", cancellationToken);
private Task<Document> FixAllAsync(
Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
Document document, ImmutableArray<Diagnostic> diagnostics,
string equivalenceKey, CancellationToken cancellationToken)
{
return FixAllWithEditorAsync(document,
editor => FixAllAsync(document, diagnostics, editor, cancellationToken),
editor => FixAllAsync(document, diagnostics, editor, equivalenceKey, cancellationToken),
cancellationToken);
}
......@@ -52,8 +50,31 @@ public sealed override FixAllProvider GetFixAllProvider()
internal abstract CodeFixCategory CodeFixCategory { get; }
protected abstract Task FixAllAsync(
Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, CancellationToken cancellationToken);
/// <summary>
/// Method to override in subclasses to actually implement fix-all behavior for the provided
/// <paramref name="document"/> given the selected set of <paramref name="diagnostics"/>.
/// <para/>
/// This method should be overridden when the subclass does not care about the fix-all
/// equivalence-key when determining what to do.
/// <para/>
/// One of <see cref="FixAllAsync(Document, ImmutableArray{Diagnostic}, SyntaxEditor, CancellationToken)"/> or
/// <see cref="FixAllAsync(Document, ImmutableArray{Diagnostic}, SyntaxEditor, string, CancellationToken)"/> must be overridden.
/// </summary>
protected virtual Task FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, CancellationToken cancellationToken)
=> Task.CompletedTask;
/// <summary>
/// Method to override in subclasses to actually implement fix-all behavior for the provided
/// <paramref name="document"/> given the selected set of <paramref name="diagnostics"/>.
/// <para/>
/// This method should be overridden when the subclass does care about the fix-all
/// equivalence-key when determining what to do.
/// <para/>
/// One of <see cref="FixAllAsync(Document, ImmutableArray{Diagnostic}, SyntaxEditor, CancellationToken)"/> or
/// <see cref="FixAllAsync(Document, ImmutableArray{Diagnostic}, SyntaxEditor, string, CancellationToken)"/> must be overridden.
/// </summary>
protected virtual Task FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, string equivalenceKey, CancellationToken cancellationToken)
=> FixAllAsync(document, diagnostics, editor, cancellationToken);
/// <summary>
/// Whether or not this diagnostic should be included when performing a FixAll. This is
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册