File: EditAndContinue\SourceMarkers.cs
Web Access
Project: src\src\Features\TestUtilities\Microsoft.CodeAnalysis.Features.Test.Utilities.csproj (Microsoft.CodeAnalysis.Features.Test.Utilities)
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests;
 
internal static class SourceMarkers
{
    private static readonly Regex s_tags = new(
        "[<]  (?<IsEnd>/?)  (?<Name>(AS|ER|N|S|TS))[:]  (?<Id>[.0-9,]+)  (?<IsStartAndEnd>/?)  [>]", RegexOptions.IgnorePatternWhitespace);
 
    public static readonly Regex ExceptionRegionPattern = new(
        @"[<]ER[:]      (?<Id>(?:[0-9]+[.][0-9]+[,]?)+)   [>]
            (?<ExceptionRegion>.*)
          [<][/]ER[:]   (\k<Id>)                          [>]",
        RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
    private static readonly Regex s_trackingStatementPattern = new(
        @"[<]TS[:]    (?<Id>[0-9,]+) [>]
            (?<TrackingStatement>.*)
          [<][/]TS[:] (\k<Id>)       [>]",
        RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
    internal static string Clear(string source)
        => s_tags.Replace(source, m => new string(' ', m.Length));
 
    internal static string[] Clear(string[] sources)
        => sources.Select(Clear).ToArray();
 
    private static IEnumerable<(int, int)> ParseIds(Match match)
        => from ids in match.Groups["Id"].Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
           let parts = ids.Split('.')
           select (int.Parse(parts[0]), (parts.Length > 1) ? int.Parse(parts[1]) : -1);
 
    private static IEnumerable<((int major, int minor) id, TextSpan span)> ParseSpans(string markedSource, string tagName)
    {
        // id -> content start index
        var tagMap = new Dictionary<(int major, int minor), (int start, int end)>();
 
        foreach (var match in s_tags.Matches(markedSource).ToEnumerable())
        {
            if (match.Groups["Name"].Value != tagName)
            {
                continue;
            }
 
            var isStartingTag = match.Groups["IsEnd"].Value == "" || match.Groups["IsStartAndEnd"].Value != "";
            var isEndingTag = match.Groups["IsEnd"].Value != "" || match.Groups["IsStartAndEnd"].Value != "";
            Contract.ThrowIfFalse(isStartingTag || isEndingTag);
 
            foreach (var id in ParseIds(match))
            {
                if (isStartingTag && isEndingTag)
                {
                    tagMap.Add(id, (match.Index, match.Index));
                }
                else if (isStartingTag)
                {
                    tagMap.Add(id, (match.Index + match.Length, -1));
                }
                else
                {
                    tagMap[id] = (tagMap[id].start, match.Index);
                }
            }
        }
 
        foreach (var (id, (start, end)) in tagMap.OrderBy(k => k.Key))
        {
            Contract.ThrowIfFalse(end >= 0, $"Missing ending tag for {id}");
            yield return (id, TextSpan.FromBounds(start, end));
        }
    }
 
    public static IEnumerable<(TextSpan Span, int Id)> GetActiveSpans(string markedSource)
        => ParseSpans(markedSource, tagName: "AS").Select(s => (s.span, s.id.major));
 
    public static (int id, TextSpan span)[] GetTrackingSpans(string src)
    {
        var matches = s_trackingStatementPattern.Matches(src);
        if (matches.Count == 0)
        {
            return [];
        }
 
        var result = new List<(int id, TextSpan span)>();
 
        for (var i = 0; i < matches.Count; i++)
        {
            var span = matches[i].Groups["TrackingStatement"];
            foreach (var (id, _) in ParseIds(matches[i]))
            {
                result.Add((id, new TextSpan(span.Index, span.Length)));
            }
        }
 
        Contract.ThrowIfTrue(result.Any(span => span == default));
 
        return [.. result];
    }
 
    public static ImmutableArray<ImmutableArray<TextSpan>> GetExceptionRegions(string markedSource)
    {
        var matches = ExceptionRegionPattern.Matches(markedSource);
        var plainSource = Clear(markedSource);
 
        var result = new List<List<TextSpan>>();
 
        for (var i = 0; i < matches.Count; i++)
        {
            var exceptionRegion = matches[i].Groups["ExceptionRegion"];
 
            foreach (var (activeStatementId, exceptionRegionId) in ParseIds(matches[i]))
            {
                EnsureSlot(result, activeStatementId);
                result[activeStatementId] ??= [];
                EnsureSlot(result[activeStatementId], exceptionRegionId);
 
                var regionText = plainSource.AsSpan().Slice(exceptionRegion.Index, exceptionRegion.Length);
                var start = IndexOfDifferent(regionText, ' ');
                var length = LastIndexOfDifferent(regionText, ' ') - start + 1;
 
                result[activeStatementId][exceptionRegionId] = new TextSpan(exceptionRegion.Index + start, length);
            }
        }
 
        return result.Select(r => r.AsImmutableOrEmpty()).ToImmutableArray();
    }
 
    public static ImmutableArray<ImmutableArray<TextSpan>> GetNodeSpans(string markedSource)
    {
        var result = new List<List<TextSpan>>();
 
        foreach (var ((major, minor), span) in ParseSpans(markedSource, tagName: "N"))
        {
            var (i, j) = (minor >= 0) ? (major, minor) : (0, major);
 
            EnsureSlot(result, i);
            result[i] ??= [];
            EnsureSlot(result[i], j);
            result[i][j] = span;
        }
 
        return result.Select(r => r.AsImmutableOrEmpty()).ToImmutableArray();
    }
 
    public static ImmutableArray<TextSpan> GetSpans(string markedSource, string tagName)
    {
        var result = new List<TextSpan>();
 
        foreach (var ((major, minor), span) in ParseSpans(markedSource, tagName))
        {
            Debug.Assert(minor == -1);
            EnsureSlot(result, major);
            result[major] = span;
        }
 
        return result.ToImmutableArray();
    }
 
    public static int IndexOfDifferent(ReadOnlySpan<char> span, char c)
    {
        for (var i = 0; i < span.Length; i++)
        {
            if (span[i] != c)
            {
                return i;
            }
        }
 
        return -1;
    }
 
    public static int LastIndexOfDifferent(ReadOnlySpan<char> span, char c)
    {
        for (var i = span.Length - 1; i >= 0; i--)
        {
            if (span[i] != c)
            {
                return i;
            }
        }
 
        return -1;
    }
 
    public static List<T> SetListItem<T>(List<T> list, int i, T item)
    {
        EnsureSlot(list, i);
        list[i] = item;
        return list;
    }
 
    public static void EnsureSlot<T>(List<T> list, int i)
    {
        while (i >= list.Count)
        {
            list.Add(default!);
        }
    }
}