File: Language\Syntax\SyntaxList`1.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// 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.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.AspNetCore.Razor.Language.Syntax;
 
[CollectionBuilder(typeof(SyntaxList), methodName: "Create")]
internal readonly partial struct SyntaxList<TNode>(SyntaxNode? node) : IReadOnlyList<TNode>, IEquatable<SyntaxList<TNode>>
    where TNode : SyntaxNode
{
    public static SyntaxList<TNode> Empty => default;
 
    internal SyntaxNode? Node { get; } = node;
 
    /// <summary>
    /// Creates a singleton list of syntax nodes.
    /// </summary>
    /// <param name="node">The single element node.</param>
    public SyntaxList(TNode? node)
        : this((SyntaxNode?)node)
    {
    }
 
    public SyntaxList(params ReadOnlySpan<TNode> nodes)
        : this(CreateRedListNode(nodes))
    {
    }
 
    public SyntaxList(IEnumerable<TNode> nodes)
        : this(CreateRedListNode(nodes))
    {
    }
 
    private static SyntaxNode? CreateRedListNode(ReadOnlySpan<TNode> nodes)
    {
        if (nodes.Length == 0)
        {
            return null;
        }
 
        using var builder = new PooledArrayBuilder<TNode>(nodes.Length);
        builder.AddRange(nodes);
 
        return builder.ToListNode();
    }
 
    private static SyntaxNode? CreateRedListNode(IEnumerable<TNode> nodes)
    {
        using var builder = new PooledArrayBuilder<TNode>();
        builder.AddRange(nodes);
 
        return builder.ToListNode();
    }
 
    /// <summary>
    /// The number of nodes in the list.
    /// </summary>
    public int Count
        => Node == null ? 0 : (Node.IsList ? Node.SlotCount : 1);
 
    /// <summary>
    /// Gets the node at the specified index.
    /// </summary>
    /// <param name="index">The zero-based index of the node to get or set.</param>
    /// <returns>The node at the specified index.</returns>
    public TNode this[int index]
    {
        get
        {
            if (Node != null)
            {
                if (Node.IsList)
                {
                    if (unchecked((uint)index < (uint)Node.SlotCount))
                    {
                        return (TNode)Node.GetNodeSlot(index).AssumeNotNull();
                    }
                }
                else if (index == 0)
                {
                    return (TNode)Node;
                }
            }
 
            throw new ArgumentOutOfRangeException(nameof(index));
        }
    }
 
    private SyntaxNode? ItemInternal(int index)
    {
        if (Node?.IsList is true)
        {
            return Node.GetNodeSlot(index);
        }
 
        Debug.Assert(index == 0);
        return Node;
    }
 
    /// <summary>
    /// The absolute span of the list elements in characters.
    /// </summary>
    public TextSpan Span
        => Count > 0
            ? TextSpan.FromBounds(this[0].Span.Start, this[Count - 1].Span.End)
            : default;
 
    /// <summary>
    /// Returns the string representation of the nodes in this list.
    /// </summary>
    /// <returns>
    /// The string representation of the nodes in this list.
    /// </returns>
    public override string ToString()
        => Node?.ToString() ?? string.Empty;
 
    /// <summary>
    /// Creates a new list with the specified node added at the end.
    /// </summary>
    /// <param name="node">The node to add.</param>
    public SyntaxList<TNode> Add(TNode node)
        => Insert(Count, node);
 
    /// <summary>
    /// Creates a new list with the specified nodes added at the end.
    /// </summary>
    /// <param name="nodes">The nodes to add.</param>
    public SyntaxList<TNode> AddRange(ReadOnlySpan<TNode> nodes)
        => InsertRange(Count, nodes);
 
    /// <summary>
    /// Creates a new list with the specified nodes added at the end.
    /// </summary>
    /// <param name="nodes">The nodes to add.</param>
    public SyntaxList<TNode> AddRange(IEnumerable<TNode> nodes)
        => InsertRange(Count, nodes);
 
    /// <summary>
    /// Creates a new list with the specified node inserted at the index.
    /// </summary>
    /// <param name="index">The index to insert at.</param>
    /// <param name="node">The node to insert.</param>
    public SyntaxList<TNode> Insert(int index, TNode node)
    {
        ArgHelper.ThrowIfNull(node);
 
        return InsertRange(index, [node]);
    }
 
    /// <summary>
    /// Creates a new list with the specified nodes inserted at the index.
    /// </summary>
    /// <param name="index">The index to insert at.</param>
    /// <param name="nodes">The nodes to insert.</param>
    public SyntaxList<TNode> InsertRange(int index, ReadOnlySpan<TNode> tokens)
    {
        var count = Count;
 
        ArgHelper.ThrowIfNegative(index);
        ArgHelper.ThrowIfGreaterThan(index, count);
 
        if (tokens.Length == 0)
        {
            return this;
        }
 
        using var builder = new PooledArrayBuilder<TNode>(count + tokens.Length);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        // Add new tokens
        builder.AddRange(tokens);
 
        // Add remaining tokens starting from 'index'
        builder.AddRange(this, index, count - index);
 
        Debug.Assert(builder.Count == count + tokens.Length);
 
        return builder.ToList();
    }
 
    /// <summary>
    /// Creates a new list with the specified nodes inserted at the index.
    /// </summary>
    /// <param name="index">The index to insert at.</param>
    /// <param name="nodes">The nodes to insert.</param>
    public SyntaxList<TNode> InsertRange(int index, IEnumerable<TNode> nodes)
    {
        var count = Count;
 
        ArgHelper.ThrowIfNegative(index);
        ArgHelper.ThrowIfGreaterThan(index, count);
        ArgHelper.ThrowIfNull(nodes);
 
        if (nodes.TryGetCount(out var nodeCount))
        {
            return InsertRangeWithCount(index, nodes, nodeCount);
        }
 
        using var builder = new PooledArrayBuilder<TNode>(count);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        var oldCount = builder.Count;
 
        // Add new tokens
        builder.AddRange(nodes);
 
        // If builder.Count == oldCount, there weren't any tokens added.
        // So, there's no need to continue.
        if (builder.Count == oldCount)
        {
            return this;
        }
 
        // Add remaining tokens starting from 'index'
        builder.AddRange(this, index, count - index);
 
        return builder.ToList();
    }
 
    private SyntaxList<TNode> InsertRangeWithCount(int index, IEnumerable<TNode> nodes, int nodeCount)
    {
        if (nodeCount == 0)
        {
            return this;
        }
 
        var count = Count;
 
        using var builder = new PooledArrayBuilder<TNode>(count + nodeCount);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        // Add new tokens
        builder.AddRange(nodes);
 
        // Add remaining tokens starting from 'index'
        builder.AddRange(this, index, count - index);
 
        Debug.Assert(builder.Count == count + nodeCount);
 
        return builder.ToList();
    }
 
    /// <summary>
    /// Creates a new list with the element at specified index removed.
    /// </summary>
    /// <param name="index">The index of the element to remove.</param>
    public SyntaxList<TNode> RemoveAt(int index)
    {
        var count = Count;
 
        ArgHelper.ThrowIfNegative(index);
        ArgHelper.ThrowIfGreaterThanOrEqual(index, count);
 
        // count - 1 because we're removing an item.
        var newCount = count - 1;
 
        using var builder = new PooledArrayBuilder<TNode>(newCount);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        // Add remaining tokens starting *after* 'index'
        builder.AddRange(this, index + 1, newCount - index);
 
        return builder.ToList();
    }
 
    /// <summary>
    /// Creates a new list with the element removed.
    /// </summary>
    /// <param name="node">The element to remove.</param>
    public SyntaxList<TNode> Remove(TNode node)
    {
        ArgHelper.ThrowIfNull(node);
 
        var index = IndexOf(node);
        return index >= 0 ? RemoveAt(index) : this;
    }
 
    /// <summary>
    /// Creates a new list with the specified element replaced with the new node.
    /// </summary>
    /// <param name="nodeInList">The element to replace.</param>
    /// <param name="newNode">The new node.</param>
    public SyntaxList<TNode> Replace(TNode nodeInList, TNode newNode)
    {
        ArgHelper.ThrowIfNull(newNode);
 
        return ReplaceRange(nodeInList, [newNode]);
    }
 
    /// <summary>
    /// Creates a new list with the specified element replaced with new nodes.
    /// </summary>
    /// <param name="nodeInList">The element to replace.</param>
    /// <param name="newNodes">The new nodes.</param>
    public SyntaxList<TNode> ReplaceRange(TNode nodeInList, ReadOnlySpan<TNode> nodes)
    {
        ArgHelper.ThrowIfNull(nodeInList);
 
        var index = IndexOf(nodeInList);
 
        if (index < 0)
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(nameof(nodeInList));
        }
 
        if (nodes.Length == 0)
        {
            return RemoveAt(index);
        }
 
        // Count - 1 because we're removing an item.
        var newCount = Count - 1;
 
        using var builder = new PooledArrayBuilder<TNode>(newCount + nodes.Length);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        // Add new tokens
        builder.AddRange(nodes);
 
        // Add remaining tokens starting *after* 'index'
        builder.AddRange(this, index + 1, newCount - index);
 
        return builder.ToList();
    }
 
    /// <summary>
    /// Creates a new list with the specified element replaced with new nodes.
    /// </summary>
    /// <param name="nodeInList">The element to replace.</param>
    /// <param name="newNodes">The new nodes.</param>
    public SyntaxList<TNode> ReplaceRange(TNode nodeInList, IEnumerable<TNode> newNodes)
    {
        ArgHelper.ThrowIfNull(nodeInList);
        ArgHelper.ThrowIfNull(newNodes);
 
        var index = IndexOf(nodeInList);
 
        if (index < 0)
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(nameof(nodeInList));
        }
 
        if (newNodes.TryGetCount(out var nodeCount))
        {
            return ReplaceRangeWithCount(index, newNodes, nodeCount);
        }
 
        // Count - 1 because we're removing an item.
        var newCount = Count - 1;
 
        using var builder = new PooledArrayBuilder<TNode>(newCount);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        // Add new tokens
        builder.AddRange(newNodes);
 
        // Add remaining tokens starting *after* 'index'
        builder.AddRange(this, index + 1, newCount - index);
 
        return builder.ToList();
    }
 
    private SyntaxList<TNode> ReplaceRangeWithCount(int index, IEnumerable<TNode> nodes, int nodeCount)
    {
        if (nodeCount == 0)
        {
            return RemoveAt(index);
        }
 
        // Count - 1 because we're removing an item.
        var newCount = Count - 1;
 
        using var builder = new PooledArrayBuilder<TNode>(newCount + nodeCount);
 
        // Add current tokens up to 'index'
        builder.AddRange(this, 0, index);
 
        // Add new tokens
        builder.AddRange(nodes);
 
        Debug.Assert(builder.Count == index + nodeCount);
 
        // Add remaining tokens starting *after* 'index'
        builder.AddRange(this, index + 1, newCount - index);
 
        Debug.Assert(builder.Count == newCount + nodeCount);
 
        return builder.ToList();
    }
 
    /// <summary>
    /// The first node in the list.
    /// </summary>
    public TNode First()
        => this[0];
 
    /// <summary>
    /// The first node in the list or default if the list is empty.
    /// </summary>
    public TNode? FirstOrDefault()
        => Any() ? this[0] : null;
 
    /// <summary>
    /// The last node in the list.
    /// </summary>
    public TNode Last()
        => this[^1];
 
    /// <summary>
    /// The last node in the list or default if the list is empty.
    /// </summary>
    public TNode? LastOrDefault()
        => Any() ? this[^1] : null;
 
    /// <summary>
    /// True if the list has at least one node.
    /// </summary>
    public bool Any()
    {
        Debug.Assert(Node == null || Count != 0);
        return Node != null;
    }
 
    public bool Any(Func<TNode, bool> predicate)
    {
        foreach (var node in this)
        {
            if (predicate(node))
            {
                return true;
            }
        }
 
        return false;
    }
 
    public SyntaxList<TNode> Where(Func<TNode, bool> predicate)
    {
        using var builder = new PooledArrayBuilder<TNode>(Count);
 
        foreach (var node in this)
        {
            if (predicate(node))
            {
                builder.Add(node);
            }
        }
 
        return builder.ToList();
    }
 
    // for debugging
#pragma warning disable IDE0051 // Remove unused private members
    private TNode[] Nodes => [.. this];
#pragma warning restore IDE0051 // Remove unused private members
 
    /// <summary>
    /// Get's the enumerator for this list.
    /// </summary>
    public Enumerator GetEnumerator()
        => new(in this);
 
    IEnumerator<TNode> IEnumerable<TNode>.GetEnumerator()
        => Any()
            ? new EnumeratorImpl(this)
            : SpecializedCollections.EmptyEnumerator<TNode>();
 
    IEnumerator IEnumerable.GetEnumerator()
        => Any()
            ? new EnumeratorImpl(this)
            : (IEnumerator)SpecializedCollections.EmptyEnumerator<TNode>();
 
    public static bool operator ==(SyntaxList<TNode> left, SyntaxList<TNode> right)
        => left.Node == right.Node;
 
    public static bool operator !=(SyntaxList<TNode> left, SyntaxList<TNode> right)
        => left.Node != right.Node;
 
    public bool Equals(SyntaxList<TNode> other)
        => Node == other.Node;
 
    public override bool Equals(object? obj)
        => obj is SyntaxList<TNode> list &&
           Equals(list);
 
    public override int GetHashCode()
        => Node?.GetHashCode() ?? 0;
 
    public static implicit operator SyntaxList<TNode>(SyntaxList<SyntaxNode> nodes)
        => new(nodes.Node);
 
    public static implicit operator SyntaxList<SyntaxNode>(SyntaxList<TNode> nodes)
        => new(nodes.Node);
 
    /// <summary>
    /// The index of the node in this list, or -1 if the node is not in the list.
    /// </summary>
    public int IndexOf(TNode node)
    {
        var index = 0;
 
        foreach (var child in this)
        {
            if (Equals(child, node))
            {
                return index;
            }
 
            index++;
        }
 
        return -1;
    }
 
    public int IndexOf(Func<TNode, bool> predicate)
    {
        var index = 0;
 
        foreach (var child in this)
        {
            if (predicate(child))
            {
                return index;
            }
 
            index++;
        }
 
        return -1;
    }
 
    internal int IndexOf(SyntaxKind kind)
    {
        var index = 0;
 
        foreach (var child in this)
        {
            if (child.Kind == kind)
            {
                return index;
            }
 
            index++;
        }
 
        return -1;
    }
 
    public int LastIndexOf(TNode node)
    {
        for (var i = Count - 1; i >= 0; i--)
        {
            if (Equals(this[i], node))
            {
                return i;
            }
        }
 
        return -1;
    }
 
    public int LastIndexOf(Func<TNode, bool> predicate)
    {
        for (var i = Count - 1; i >= 0; i--)
        {
            if (predicate(this[i]))
            {
                return i;
            }
        }
 
        return -1;
    }
}