File: Copilot\CopilotUtilities.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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 Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Copilot;
 
internal static class CopilotUtilities
{
    /// <summary>
    /// Returns a new <see cref="SourceText"/> that represents the text after applying the specified changes to
    /// <paramref name="oldText"/>. <c>'newSpans'</c> contains the spans of the <paramref name="changes"/>
    /// mapped to the new text.  The spans are in the same order as the changes, are guaranteed to be non-overlapping.
    /// </summary>
    public static (SourceText newText, ImmutableArray<TextSpan> newSpans) GetNewTextAndChangedSpans(
        SourceText oldText, ImmutableArray<TextChange> changes)
    {
        // Fork the starting document with the changes copilot wants to make.  Keep track of where the edited spans
        // move to in the forked doucment, as that is what we will want to analyze.
        var newText = oldText.WithChanges(changes);
 
        var totalDelta = 0;
 
        var newSpans = new FixedSizeArrayBuilder<TextSpan>(changes.Length);
        foreach (var change in changes)
        {
            var newTextLength = change.NewText!.Length;
 
            newSpans.Add(new TextSpan(change.Span.Start + totalDelta, newTextLength));
            totalDelta += newTextLength - change.Span.Length;
        }
 
        return (newText, newSpans.MoveToImmutable());
    }
 
    /// <summary>
    /// Returns the provided <paramref name="textChanges"/> in normalized form.  Normalized means that the
    /// changes are in order, and no change overlaps with another change.  If changes do overlap, then this
    /// returns <see langword="default"/>.  Note: abutting changes are not merged.  This allows consumers
    /// to maintain a 1:1 mapping between the changes applied to the original document and the spans in the
    /// new document.
    /// </summary>
    public static ImmutableArray<TextChange> TryNormalizeCopilotTextChanges(IEnumerable<TextChange> textChanges)
    {
        using var _ = ArrayBuilder<TextChange>.GetInstance(out var builder);
        foreach (var textChange in textChanges)
            builder.Add(textChange);
 
        // Ensure everything is sorted.
        builder.Sort(static (c1, c2) => c1.Span.Start - c2.Span.Start);
 
        // Now, go through and make sure no edit overlaps another.
        for (int i = 1, n = builder.Count; i < n; i++)
        {
            var lastEdit = builder[i - 1];
            var currentEdit = builder[i];
 
            if (lastEdit.Span.OverlapsWith(currentEdit.Span))
                return default;
        }
 
        // Things look good.  Can process these sorted edits.
        return builder.ToImmutableAndClear();
    }
 
    public static void ThrowIfNotNormalized(ImmutableArray<TextChange> textChanges)
    {
        Contract.ThrowIfTrue(!textChanges.IsSorted(static (c1, c2) => c1.Span.Start - c2.Span.Start), "'changes' was not sorted.");
 
        for (int i = 1, n = textChanges.Length; i < n; i++)
        {
            var lastEdit = textChanges[i - 1];
            var currentEdit = textChanges[i];
            Contract.ThrowIfTrue(lastEdit.Span.OverlapsWith(currentEdit.Span), "'changes' contained overlapping edits.");
        }
    }
}