File: LinkedFileDiffMerging\AbstractLinkedFileMergeConflictCommentAdditionService.cs
Web Access
Project: src\src\roslyn\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis;

internal static class LinkedFileMergeConflictCommentAdditionService
{
    public static ImmutableArray<TextChange> CreateEdits(SourceText originalSourceText, ArrayBuilder<UnmergedDocumentChanges> unmergedChanges)
    {
        using var _ = ArrayBuilder<TextChange>.GetInstance(out var commentChanges);

        foreach (var documentWithChanges in unmergedChanges)
        {
            var partitionedChanges = PartitionChangesForDocument(documentWithChanges.UnmergedChanges, originalSourceText);
            var comments = GetCommentChangesForDocument(partitionedChanges, documentWithChanges.ProjectName, originalSourceText);

            commentChanges.AddRange(comments);
        }

        return commentChanges.ToImmutableAndClear();
    }

    private static List<List<TextChange>> PartitionChangesForDocument(IEnumerable<TextChange> changes, SourceText originalSourceText)
    {
        var partitionedChanges = new List<List<TextChange>>();
        var currentPartition = new List<TextChange>
        {
            changes.First()
        };
        var currentPartitionEndLine = originalSourceText.Lines.GetLineFromPosition(changes.First().Span.End);

        foreach (var change in changes.Skip(1))
        {
            // If changes are on adjacent lines, consider them part of the same change.
            var changeStartLine = originalSourceText.Lines.GetLineFromPosition(change.Span.Start);
            if (changeStartLine.LineNumber >= currentPartitionEndLine.LineNumber + 2)
            {
                partitionedChanges.Add(currentPartition);
                currentPartition = [];
            }

            currentPartition.Add(change);
            currentPartitionEndLine = originalSourceText.Lines.GetLineFromPosition(change.Span.End);
        }

        if (currentPartition.Any())
        {
            partitionedChanges.Add(currentPartition);
        }

        return partitionedChanges;
    }

    private static ImmutableArray<TextChange> GetCommentChangesForDocument(IEnumerable<IEnumerable<TextChange>> partitionedChanges, string projectName, SourceText oldDocumentText)
    {
        using var _ = ArrayBuilder<TextChange>.GetInstance(out var commentChanges);

        foreach (var changePartition in partitionedChanges)
        {
            var startPosition = changePartition.First().Span.Start;
            var endPosition = changePartition.Last().Span.End;

            var startLineStartPosition = oldDocumentText.Lines.GetLineFromPosition(startPosition).Start;
            var endLineEndPosition = oldDocumentText.Lines.GetLineFromPosition(endPosition).End;

            var oldText = oldDocumentText.GetSubText(TextSpan.FromBounds(startLineStartPosition, endLineEndPosition));
            var adjustedChanges = changePartition.Select(c => new TextChange(TextSpan.FromBounds(c.Span.Start - startLineStartPosition, c.Span.End - startLineStartPosition), c.NewText!));
            var newText = oldText.WithChanges(adjustedChanges);

            var warningText = GetConflictCommentText(
                string.Format(WorkspacesResources.TODO_Unmerged_change_from_project_0, projectName),
                TrimBlankLines(oldText),
                TrimBlankLines(newText));

            if (warningText != null)
                commentChanges.Add(new TextChange(TextSpan.FromBounds(startLineStartPosition, startLineStartPosition), warningText));
        }

        return commentChanges.ToImmutableAndClear();
    }

    private static string? GetConflictCommentText(string header, string? beforeString, string? afterString)
        => (beforeString, afterString) switch
        {
            (null, null) => null,
            (not null, not null) =>
                $"""

                <<<<<<< {header}, {WorkspacesResources.Before_colon}
                {beforeString}
                =======
                {afterString}
                >>>>>>> {WorkspacesResources.After}

                """,
            (null, not null) =>
                $"""

                <<<<<<< {header}, {WorkspacesResources.Before_colon}
                =======
                {afterString}
                >>>>>>> {WorkspacesResources.After}

                """,
            (not null, null) =>
                $"""

                <<<<<<< {header}, {WorkspacesResources.Before_colon}
                {beforeString}
                =======
                >>>>>>> {WorkspacesResources.After}

                """,
        };

    private static string? TrimBlankLines(SourceText text)
    {
        int startLine, endLine;
        for (startLine = 0; startLine < text.Lines.Count; startLine++)
        {
            if (!text.Lines[startLine].IsEmptyOrWhitespace())
            {
                break;
            }
        }

        for (endLine = text.Lines.Count - 1; endLine > startLine; endLine--)
        {
            if (!text.Lines[endLine].IsEmptyOrWhitespace())
            {
                break;
            }
        }

        return startLine <= endLine
            ? text.GetSubText(TextSpan.FromBounds(text.Lines[startLine].Start, text.Lines[endLine].End)).ToString()
            : null;
    }
}