File: TextDifferencing\SourceTextDiffer.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Razor.TextDifferencing;
 
internal abstract partial class SourceTextDiffer(SourceText oldText, SourceText newText) : TextDiffer, IDisposable
{
    protected readonly SourceText OldText = oldText;
    protected readonly SourceText NewText = newText;
 
    public abstract void Dispose();
 
    protected abstract int GetEditPosition(DiffEdit edit);
    protected abstract int AppendEdit(DiffEdit edit, StringBuilder builder);
 
    /// <summary>
    /// Rents a char array of at least <paramref name="minimumLength"/> from the shared array pool.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    protected static char[] RentArray(int minimumLength)
        => ArrayPool<char>.Shared.Rent(minimumLength);
 
    /// <summary>
    /// Returns a char array to the shared array pool.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    protected static void ReturnArray(char[] array, bool clearArray = false)
        => ArrayPool<char>.Shared.Return(array, clearArray);
 
    /// <summary>
    /// Ensures that <paramref name="array"/> references a char array of at least <paramref name="minimumLength"/>.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    protected static char[] EnsureBuffer(ref char[] array, int minimumLength)
    {
        return array.Length >= minimumLength
            ? array
            : GetNewBuffer(ref array, minimumLength);
 
        static char[] GetNewBuffer(ref char[] array, int minimumLength)
        {
            // We need a larger buffer. Return this array to the pool
            // and rent a new one.
            ReturnArray(array);
            array = RentArray(minimumLength);
 
            return array;
        }
    }
 
    private ImmutableArray<TextChange> ConsolidateEdits(List<DiffEdit> edits)
    {
        // Scan through the list of edits and collapse them into a minimal set of TextChanges.
        // This method assumes that there are no overlapping changes and the changes are sorted.
 
        using var minimalChanges = new PooledArrayBuilder<TextChange>(capacity: edits.Count);
 
        var start = 0;
        var end = 0;
 
        using var _ = StringBuilderPool.GetPooledObject(out var builder);
 
        foreach (var edit in edits)
        {
            var startPosition = GetEditPosition(edit);
            if (startPosition != end)
            {
                // Previous edit's end doesn't match the new edit's start.
                // Output the text change we were tracking.
                if (start != end || builder.Length > 0)
                {
                    minimalChanges.Add(new TextChange(TextSpan.FromBounds(start, end), builder.ToString()));
                    builder.Clear();
                }
 
                start = startPosition;
            }
 
            end = AppendEdit(edit, builder);
        }
 
        if (start != end || builder.Length > 0)
        {
            minimalChanges.Add(new TextChange(TextSpan.FromBounds(start, end), builder.ToString()));
        }
 
        return minimalChanges.ToImmutableAndClear();
    }
 
    public static ImmutableArray<TextChange> GetMinimalTextChanges(SourceText oldText, SourceText newText, DiffKind kind = DiffKind.Line)
    {
        if (oldText.ContentEquals(newText))
        {
            return [];
        }
        else if (oldText.Length == 0 || newText.Length == 0)
        {
            return newText.GetTextChangesArray(oldText);
        }
 
        using SourceTextDiffer differ = kind switch
        {
            DiffKind.Line => new LineDiffer(oldText, newText),
            DiffKind.Char => new CharDiffer(oldText, newText),
            DiffKind.Word => new WordDiffer(oldText, newText),
            _ => throw new ArgumentOutOfRangeException(nameof(kind)),
        };
 
        var edits = differ.ComputeDiff();
 
        var changes = differ.ConsolidateEdits(edits);
 
        Debug.Assert(oldText.WithChanges(changes).ContentEquals(newText), "Incorrect minimal changes");
 
        return changes;
    }
}