diff --git a/src/Core/Reflection/ReflectionUtility.cs b/src/Core/Reflection/ReflectionUtility.cs index e64f1cd..dfb7f8d 100644 --- a/src/Core/Reflection/ReflectionUtility.cs +++ b/src/Core/Reflection/ReflectionUtility.cs @@ -37,7 +37,23 @@ namespace UnityExplorer /// Key: Type.FullName public static readonly SortedDictionary AllTypes = new SortedDictionary(StringComparer.OrdinalIgnoreCase); - private static readonly SortedSet allTypeNames = new SortedSet(StringComparer.OrdinalIgnoreCase); + //private static readonly SortedSet allTypeNames = new SortedSet(StringComparer.OrdinalIgnoreCase); + + private static string[] allTypesArray; + private static string[] GetTypeNameArray() + { + if (allTypesArray == null || allTypesArray.Length != AllTypes.Count) + { + allTypesArray = new string[AllTypes.Count]; + int i = 0; + foreach (var name in AllTypes.Keys) + { + allTypesArray[i] = name; + i++; + } + } + return allTypesArray; + } private static void SetupTypeCache() { @@ -64,7 +80,7 @@ namespace UnityExplorer else { AllTypes.Add(type.FullName, type); - allTypeNames.Add(type.FullName); + //allTypeNames.Add(type.FullName); } OnTypeLoaded?.Invoke(type); @@ -211,14 +227,24 @@ namespace UnityExplorer /// /// The base type, which can optionally be abstract / interface. /// All implementations of the type in the current AppDomain. - public static HashSet GetImplementationsOf(Type baseType, bool allowAbstract, bool allowGeneric) + public static HashSet GetImplementationsOf(Type baseType, bool allowAbstract, bool allowGeneric, bool allowRecursive = true) { - var key = GetImplementationKey(baseType); //baseType.FullName; + var key = GetImplementationKey(baseType); + int count = AllTypes.Count; + HashSet ret; if (!baseType.IsGenericParameter) - return GetImplementations(key, baseType, allowAbstract, allowGeneric); + ret = GetImplementations(key, baseType, allowAbstract, allowGeneric); else - return GetGenericParameterImplementations(key, baseType, allowAbstract, allowGeneric); + ret = GetGenericParameterImplementations(key, baseType, allowAbstract, allowGeneric); + + // types were resolved during the parse, do it again if we're not already rebuilding. + if (allowRecursive && AllTypes.Count != count) + { + ret = GetImplementationsOf(baseType, allowAbstract, allowGeneric, false); + } + + return ret; } private static HashSet GetImplementations(string key, Type baseType, bool allowAbstract, bool allowGeneric) @@ -226,8 +252,10 @@ namespace UnityExplorer if (!typeInheritance.ContainsKey(key)) { var set = new HashSet(); - foreach (var name in allTypeNames) + var names = GetTypeNameArray(); + for (int i = 0; i < names.Length; i++) { + var name = names[i]; try { var type = AllTypes[name]; @@ -263,8 +291,10 @@ namespace UnityExplorer { var set = new HashSet(); - foreach (var name in allTypeNames) + var names = GetTypeNameArray(); + for (int i = 0; i < names.Length; i++) { + var name = names[i]; try { var type = AllTypes[name]; diff --git a/src/Core/Runtime/Mono/MonoProvider.cs b/src/Core/Runtime/Mono/MonoProvider.cs index 9c415a0..d6ace86 100644 --- a/src/Core/Runtime/Mono/MonoProvider.cs +++ b/src/Core/Runtime/Mono/MonoProvider.cs @@ -10,9 +10,6 @@ using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; -using UnityExplorer.Core; -using UnityExplorer.Core.CSharp; -using UnityExplorer.Core.Input; namespace UnityExplorer.Core.Runtime.Mono { diff --git a/src/Core/Runtime/Mono/SortedSet.cs b/src/Core/Runtime/Mono/SortedSet.cs deleted file mode 100644 index 0b0f396..0000000 --- a/src/Core/Runtime/Mono/SortedSet.cs +++ /dev/null @@ -1,3107 +0,0 @@ -#if MONO -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; -using System.Runtime.CompilerServices; - -// Reimplementation of SortedSet and required supplementary classes for .NET 3.5 -// https://referencesource.microsoft.com/#System/compmod/system/collections/generic/sortedset.cs - -namespace System.Collections.Generic -{ - #region ISet - - /// - /// Generic collection that guarantees the uniqueness of its elements, as defined - /// by some comparer. It also supports basic set operations such as Union, Intersection, - /// Complement and Exclusive Complement. - /// - public interface ISet : ICollection - { - //Add ITEM to the set, return true if added, false if duplicate - new bool Add(T item); - - //Transform this set into its union with the IEnumerable other - void UnionWith(IEnumerable other); - - //Transform this set into its intersection with the IEnumberable other - void IntersectWith(IEnumerable other); - - //Transform this set so it contains no elements that are also in other - void ExceptWith(IEnumerable other); - - //Transform this set so it contains elements initially in this or in other, but not both - void SymmetricExceptWith(IEnumerable other); - - //Check if this set is a subset of other - bool IsSubsetOf(IEnumerable other); - - //Check if this set is a superset of other - bool IsSupersetOf(IEnumerable other); - - //Check if this set is a subset of other, but not the same as it - bool IsProperSupersetOf(IEnumerable other); - - //Check if this set is a superset of other, but not the same as it - bool IsProperSubsetOf(IEnumerable other); - - //Check if this set has any elements in common with other - bool Overlaps(IEnumerable other); - - //Check if this set contains the same and only the same elements as other - bool SetEquals(IEnumerable other); - } - - #endregion - - - #region Bit Helper - - /// - /// ABOUT: - /// Helps with operations that rely on bit marking to indicate whether an item in the - /// collection should be added, removed, visited already, etc. - /// - /// BitHelper doesn't allocate the array; you must pass in an array or ints allocated on the - /// stack or heap. ToIntArrayLength() tells you the int array size you must allocate. - /// - /// USAGE: - /// Suppose you need to represent a bit array of length (i.e. logical bit array length) - /// BIT_ARRAY_LENGTH. Then this is the suggested way to instantiate BitHelper: - /// *************************************************************************** - /// int intArrayLength = BitHelper.ToIntArrayLength(BIT_ARRAY_LENGTH); - /// BitHelper bitHelper; - /// if (intArrayLength less than stack alloc threshold) - /// int* m_arrayPtr = stackalloc int[intArrayLength]; - /// bitHelper = new BitHelper(m_arrayPtr, intArrayLength); - /// else - /// int[] m_arrayPtr = new int[intArrayLength]; - /// bitHelper = new BitHelper(m_arrayPtr, intArrayLength); - /// *************************************************************************** - /// - /// IMPORTANT: - /// The second ctor args, length, should be specified as the length of the int array, not - /// the logical bit array. Because length is used for bounds checking into the int array, - /// it's especially important to get this correct for the stackalloc version. See the code - /// samples above; this is the value gotten from ToIntArrayLength(). - /// - /// The length ctor argument is the only exception; for other methods -- MarkBit and - /// IsMarked -- pass in values as indices into the logical bit array, and it will be mapped - /// to the position within the array of ints. - /// - /// - - unsafe internal class BitHelper - { // should not be serialized - - private const byte MarkedBitFlag = 1; - private const byte IntSize = 32; - - // m_length of underlying int array (not logical bit array) - private int m_length; - - // ptr to stack alloc'd array of ints - [System.Security.SecurityCritical] - private int* m_arrayPtr; - - // array of ints - private int[] m_array; - - // whether to operate on stack alloc'd or heap alloc'd array - private bool useStackAlloc; - - /// - /// Instantiates a BitHelper with a heap alloc'd array of ints - /// - /// int array to hold bits - /// length of int array - // - // - // - // - [System.Security.SecurityCritical] - internal BitHelper(int* bitArrayPtr, int length) - { - this.m_arrayPtr = bitArrayPtr; - this.m_length = length; - useStackAlloc = true; - } - - /// - /// Instantiates a BitHelper with a heap alloc'd array of ints - /// - /// int array to hold bits - /// length of int array - internal BitHelper(int[] bitArray, int length) - { - this.m_array = bitArray; - this.m_length = length; - } - - /// - /// Mark bit at specified position - /// - /// - // - // - // - [System.Security.SecurityCritical] - internal unsafe void MarkBit(int bitPosition) - { - if (useStackAlloc) - { - int bitArrayIndex = bitPosition / IntSize; - if (bitArrayIndex < m_length && bitArrayIndex >= 0) - { - m_arrayPtr[bitArrayIndex] |= (MarkedBitFlag << (bitPosition % IntSize)); - } - } - else - { - int bitArrayIndex = bitPosition / IntSize; - if (bitArrayIndex < m_length && bitArrayIndex >= 0) - { - m_array[bitArrayIndex] |= (MarkedBitFlag << (bitPosition % IntSize)); - } - } - } - - /// - /// Is bit at specified position marked? - /// - /// - /// - // - // - // - [System.Security.SecurityCritical] - internal unsafe bool IsMarked(int bitPosition) - { - if (useStackAlloc) - { - int bitArrayIndex = bitPosition / IntSize; - if (bitArrayIndex < m_length && bitArrayIndex >= 0) - { - return ((m_arrayPtr[bitArrayIndex] & (MarkedBitFlag << (bitPosition % IntSize))) != 0); - } - return false; - } - else - { - int bitArrayIndex = bitPosition / IntSize; - if (bitArrayIndex < m_length && bitArrayIndex >= 0) - { - return ((m_array[bitArrayIndex] & (MarkedBitFlag << (bitPosition % IntSize))) != 0); - } - return false; - } - } - - /// - /// How many ints must be allocated to represent n bits. Returns (n+31)/32, but - /// avoids overflow - /// - /// - /// - internal static int ToIntArrayLength(int n) - { - return n > 0 ? ((n - 1) / IntSize + 1) : 0; - } - - } - - #endregion - - - #region SortedSet - - /*============================================================ - ** - ** Class: SortedSet - ** - ** Purpose: A generic sorted set. - ** - ** Date: August 15, 2008 - ** - ===========================================================*/ - - // - // A binary search tree is a red-black tree if it satisfies the following red-black properties: - // 1. Every node is either red or black - // 2. Every leaf (nil node) is black - // 3. If a node is red, then both its children are black - // 4. Every simple path from a node to a descendant leaf contains the same number of black nodes - // - // The basic idea of red-black tree is to represent 2-3-4 trees as standard BSTs but to add one extra bit of information - // per node to encode 3-nodes and 4-nodes. - // 4-nodes will be represented as: B - // R R - // 3 -node will be represented as: B or B - // R B B R - // - // For a detailed description of the algorithm, take a look at "Algorithms" by Robert Sedgewick. - // - - internal delegate bool TreeWalkPredicate(SortedSet.Node node); - - internal enum TreeRotation - { - LeftRotation = 1, - RightRotation = 2, - RightLeftRotation = 3, - LeftRightRotation = 4, - } - - //[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "by design name choice")] - [DebuggerDisplay("Count = {Count}")] -#if !FEATURE_NETCORE - [Serializable] - public class SortedSet : ISet, ICollection, ICollection, ISerializable, IDeserializationCallback - { -#else - public class SortedSet : ISet, ICollection, ICollection, IReadOnlyCollection { -#endif //!FEATURE_NETCORE - #region local variables/constants - Node root; - IComparer comparer; - int count; - int version; - private Object _syncRoot; - - private const String ComparerName = "Comparer"; - private const String CountName = "Count"; - private const String ItemsName = "Items"; - private const String VersionName = "Version"; - //needed for enumerator - private const String TreeName = "Tree"; - private const String NodeValueName = "Item"; - private const String EnumStartName = "EnumStarted"; - private const String ReverseName = "Reverse"; - private const String EnumVersionName = "EnumVersion"; - -#if !FEATURE_NETCORE - //needed for TreeSubset - private const String minName = "Min"; - private const String maxName = "Max"; - private const String lBoundActiveName = "lBoundActive"; - private const String uBoundActiveName = "uBoundActive"; - - private SerializationInfo siInfo; //A temporary variable which we need during deserialization. -#endif - internal const int StackAllocThreshold = 100; - - #endregion - - #region Constructors - public SortedSet() - { - this.comparer = Comparer.Default; - - } - - public SortedSet(IComparer comparer) - { - if (comparer == null) - { - this.comparer = Comparer.Default; - } - else - { - this.comparer = comparer; - } - } - - - public SortedSet(IEnumerable collection) : this(collection, Comparer.Default) { } - - public SortedSet(IEnumerable collection, IComparer comparer) - : this(comparer) - { - - if (collection == null) - { - throw new ArgumentNullException("collection"); - } - - // these are explicit type checks in the mould of HashSet. It would have worked better - // with something like an ISorted (we could make this work for SortedList.Keys etc) - SortedSet baseSortedSet = collection as SortedSet; - SortedSet baseTreeSubSet = collection as TreeSubSet; - if (baseSortedSet != null && baseTreeSubSet == null && AreComparersEqual(this, baseSortedSet)) - { - //breadth first traversal to recreate nodes - if (baseSortedSet.Count == 0) - { - count = 0; - version = 0; - root = null; - return; - } - - - //pre order way to replicate nodes - Stack theirStack = new Stack.Node>(2 * log2(baseSortedSet.Count) + 2); - Stack myStack = new Stack.Node>(2 * log2(baseSortedSet.Count) + 2); - Node theirCurrent = baseSortedSet.root; - Node myCurrent = (theirCurrent != null ? new SortedSet.Node(theirCurrent.Item, theirCurrent.IsRed) : null); - root = myCurrent; - while (theirCurrent != null) - { - theirStack.Push(theirCurrent); - myStack.Push(myCurrent); - myCurrent.Left = (theirCurrent.Left != null ? new SortedSet.Node(theirCurrent.Left.Item, theirCurrent.Left.IsRed) : null); - theirCurrent = theirCurrent.Left; - myCurrent = myCurrent.Left; - } - while (theirStack.Count != 0) - { - theirCurrent = theirStack.Pop(); - myCurrent = myStack.Pop(); - Node theirRight = theirCurrent.Right; - Node myRight = null; - if (theirRight != null) - { - myRight = new SortedSet.Node(theirRight.Item, theirRight.IsRed); - } - myCurrent.Right = myRight; - - while (theirRight != null) - { - theirStack.Push(theirRight); - myStack.Push(myRight); - myRight.Left = (theirRight.Left != null ? new SortedSet.Node(theirRight.Left.Item, theirRight.Left.IsRed) : null); - theirRight = theirRight.Left; - myRight = myRight.Left; - } - } - count = baseSortedSet.count; - version = 0; - } - else - { //As it stands, you're doing an NlogN sort of the collection - - List els = new List(collection); - els.Sort(this.comparer); - for (int i = 1; i < els.Count; i++) - { - if (comparer.Compare(els[i], els[i - 1]) == 0) - { - els.RemoveAt(i); - i--; - } - } - root = ConstructRootFromSortedArray(els.ToArray(), 0, els.Count - 1, null); - count = els.Count; - version = 0; - } - } - - -#if !FEATURE_NETCORE - protected SortedSet(SerializationInfo info, StreamingContext context) - { - siInfo = info; - } -#endif - #endregion - - #region Bulk Operation Helpers - private void AddAllElements(IEnumerable collection) - { - - foreach (T item in collection) - { - if (!this.Contains(item)) - Add(item); - } - } - - private void RemoveAllElements(IEnumerable collection) - { - T min = this.Min; - T max = this.Max; - foreach (T item in collection) - { - if (!(comparer.Compare(item, min) < 0 || comparer.Compare(item, max) > 0) && this.Contains(item)) - this.Remove(item); - } - } - - private bool ContainsAllElements(IEnumerable collection) - { - foreach (T item in collection) - { - if (!this.Contains(item)) - { - return false; - } - } - return true; - } - - // - // Do a in order walk on tree and calls the delegate for each node. - // If the action delegate returns false, stop the walk. - // - // Return true if the entire tree has been walked. - // Otherwise returns false. - // - internal bool InOrderTreeWalk(TreeWalkPredicate action) - { - return InOrderTreeWalk(action, false); - } - - // Allows for the change in traversal direction. Reverse visits nodes in descending order - internal virtual bool InOrderTreeWalk(TreeWalkPredicate action, bool reverse) - { - if (root == null) - { - return true; - } - - // The maximum height of a red-black tree is 2*lg(n+1). - // See page 264 of "Introduction to algorithms" by Thomas H. Cormen - // note: this should be logbase2, but since the stack grows itself, we - // don't want the extra cost - Stack stack = new Stack(2 * (int)(SortedSet.log2(Count + 1))); - Node current = root; - while (current != null) - { - stack.Push(current); - current = (reverse ? current.Right : current.Left); - } - while (stack.Count != 0) - { - current = stack.Pop(); - if (!action(current)) - { - return false; - } - - Node node = (reverse ? current.Left : current.Right); - while (node != null) - { - stack.Push(node); - node = (reverse ? node.Right : node.Left); - } - } - return true; - } - - // - // Do a left to right breadth first walk on tree and - // calls the delegate for each node. - // If the action delegate returns false, stop the walk. - // - // Return true if the entire tree has been walked. - // Otherwise returns false. - // - internal virtual bool BreadthFirstTreeWalk(TreeWalkPredicate action) - { - if (root == null) - { - return true; - } - - List processQueue = new List(); - processQueue.Add(root); - Node current; - - while (processQueue.Count != 0) - { - current = processQueue[0]; - processQueue.RemoveAt(0); - if (!action(current)) - { - return false; - } - if (current.Left != null) - { - processQueue.Add(current.Left); - } - if (current.Right != null) - { - processQueue.Add(current.Right); - } - } - return true; - } - #endregion - - #region Properties - public int Count - { - get - { - VersionCheck(); - return count; - } - } - - public IComparer Comparer - { - get - { - return comparer; - } - } - - bool ICollection.IsReadOnly - { - get - { - return false; - } - } - - bool ICollection.IsSynchronized - { - get - { - return false; - } - } - - object ICollection.SyncRoot - { - get - { - if (_syncRoot == null) - { - System.Threading.Interlocked.CompareExchange(ref _syncRoot, new Object(), null); - } - return _syncRoot; - } - } - #endregion - - #region Subclass helpers - - //virtual function for subclass that needs to update count - internal virtual void VersionCheck() { } - - - //virtual function for subclass that needs to do range checks - internal virtual bool IsWithinRange(T item) - { - return true; - - } - #endregion - - #region ICollection Members - /// - /// Add the value ITEM to the tree, returns true if added, false if duplicate - /// - /// item to be added - public bool Add(T item) - { - return AddIfNotPresent(item); - } - - void ICollection.Add(T item) - { - AddIfNotPresent(item); - } - - - /// - /// Adds ITEM to the tree if not already present. Returns TRUE if value was successfully added - /// or FALSE if it is a duplicate - /// - internal virtual bool AddIfNotPresent(T item) - { - if (root == null) - { // empty tree - root = new Node(item, false); - count = 1; - version++; - return true; - } - - // - // Search for a node at bottom to insert the new node. - // If we can guanratee the node we found is not a 4-node, it would be easy to do insertion. - // We split 4-nodes along the search path. - // - Node current = root; - Node parent = null; - Node grandParent = null; - Node greatGrandParent = null; - - //even if we don't actually add to the set, we may be altering its structure (by doing rotations - //and such). so update version to disable any enumerators/subsets working on it - version++; - - - int order = 0; - while (current != null) - { - order = comparer.Compare(item, current.Item); - if (order == 0) - { - // We could have changed root node to red during the search process. - // We need to set it to black before we return. - root.IsRed = false; - return false; - } - - // split a 4-node into two 2-nodes - if (Is4Node(current)) - { - Split4Node(current); - // We could have introduced two consecutive red nodes after split. Fix that by rotation. - if (IsRed(parent)) - { - InsertionBalance(current, ref parent, grandParent, greatGrandParent); - } - } - greatGrandParent = grandParent; - grandParent = parent; - parent = current; - current = (order < 0) ? current.Left : current.Right; - } - - Debug.Assert(parent != null, "Parent node cannot be null here!"); - // ready to insert the new node - Node node = new Node(item); - if (order > 0) - { - parent.Right = node; - } - else - { - parent.Left = node; - } - - // the new node will be red, so we will need to adjust the colors if parent node is also red - if (parent.IsRed) - { - InsertionBalance(node, ref parent, grandParent, greatGrandParent); - } - - // Root node is always black - root.IsRed = false; - ++count; - return true; - } - - /// - /// Remove the T ITEM from this SortedSet. Returns true if successfully removed. - /// - /// - /// - public bool Remove(T item) - { - return this.DoRemove(item); // hack so it can be made non-virtual - } - - internal virtual bool DoRemove(T item) - { - - if (root == null) - { - return false; - } - - - // Search for a node and then find its succesor. - // Then copy the item from the succesor to the matching node and delete the successor. - // If a node doesn't have a successor, we can replace it with its left child (if not empty.) - // or delete the matching node. - // - // In top-down implementation, it is important to make sure the node to be deleted is not a 2-node. - // Following code will make sure the node on the path is not a 2 Node. - - //even if we don't actually remove from the set, we may be altering its structure (by doing rotations - //and such). so update version to disable any enumerators/subsets working on it - version++; - - Node current = root; - Node parent = null; - Node grandParent = null; - Node match = null; - Node parentOfMatch = null; - bool foundMatch = false; - while (current != null) - { - if (Is2Node(current)) - { // fix up 2-Node - if (parent == null) - { // current is root. Mark it as red - current.IsRed = true; - } - else - { - Node sibling = GetSibling(current, parent); - if (sibling.IsRed) - { - // If parent is a 3-node, flip the orientation of the red link. - // We can acheive this by a single rotation - // This case is converted to one of other cased below. - Debug.Assert(!parent.IsRed, "parent must be a black node!"); - if (parent.Right == sibling) - { - RotateLeft(parent); - } - else - { - RotateRight(parent); - } - - parent.IsRed = true; - sibling.IsRed = false; // parent's color - // sibling becomes child of grandParent or root after rotation. Update link from grandParent or root - ReplaceChildOfNodeOrRoot(grandParent, parent, sibling); - // sibling will become grandParent of current node - grandParent = sibling; - if (parent == match) - { - parentOfMatch = sibling; - } - - // update sibling, this is necessary for following processing - sibling = (parent.Left == current) ? parent.Right : parent.Left; - } - Debug.Assert(sibling != null || sibling.IsRed == false, "sibling must not be null and it must be black!"); - - if (Is2Node(sibling)) - { - Merge2Nodes(parent, current, sibling); - } - else - { - // current is a 2-node and sibling is either a 3-node or a 4-node. - // We can change the color of current to red by some rotation. - TreeRotation rotation = RotationNeeded(parent, current, sibling); - Node newGrandParent = null; - switch (rotation) - { - case TreeRotation.RightRotation: - Debug.Assert(parent.Left == sibling, "sibling must be left child of parent!"); - Debug.Assert(sibling.Left.IsRed, "Left child of sibling must be red!"); - sibling.Left.IsRed = false; - newGrandParent = RotateRight(parent); - break; - case TreeRotation.LeftRotation: - Debug.Assert(parent.Right == sibling, "sibling must be left child of parent!"); - Debug.Assert(sibling.Right.IsRed, "Right child of sibling must be red!"); - sibling.Right.IsRed = false; - newGrandParent = RotateLeft(parent); - break; - - case TreeRotation.RightLeftRotation: - Debug.Assert(parent.Right == sibling, "sibling must be left child of parent!"); - Debug.Assert(sibling.Left.IsRed, "Left child of sibling must be red!"); - newGrandParent = RotateRightLeft(parent); - break; - - case TreeRotation.LeftRightRotation: - Debug.Assert(parent.Left == sibling, "sibling must be left child of parent!"); - Debug.Assert(sibling.Right.IsRed, "Right child of sibling must be red!"); - newGrandParent = RotateLeftRight(parent); - break; - } - - newGrandParent.IsRed = parent.IsRed; - parent.IsRed = false; - current.IsRed = true; - ReplaceChildOfNodeOrRoot(grandParent, parent, newGrandParent); - if (parent == match) - { - parentOfMatch = newGrandParent; - } - grandParent = newGrandParent; - } - } - } - - // we don't need to compare any more once we found the match - int order = foundMatch ? -1 : comparer.Compare(item, current.Item); - if (order == 0) - { - // save the matching node - foundMatch = true; - match = current; - parentOfMatch = parent; - } - - grandParent = parent; - parent = current; - - if (order < 0) - { - current = current.Left; - } - else - { - current = current.Right; // continue the search in right sub tree after we find a match - } - } - - // move successor to the matching node position and replace links - if (match != null) - { - ReplaceNode(match, parentOfMatch, parent, grandParent); - --count; - } - - if (root != null) - { - root.IsRed = false; - } - return foundMatch; - } - - public virtual void Clear() - { - root = null; - count = 0; - ++version; - } - - - public virtual bool Contains(T item) - { - - return FindNode(item) != null; - } - - - - - public void CopyTo(T[] array) { CopyTo(array, 0, Count); } - - public void CopyTo(T[] array, int index) { CopyTo(array, index, Count); } - - public void CopyTo(T[] array, int index, int count) - { - if (array == null) - throw new ArgumentNullException("array"); - - if (index < 0) - throw new ArgumentOutOfRangeException("index"); - - if (count < 0) - throw new ArgumentOutOfRangeException("count"); - - // will array, starting at arrayIndex, be able to hold elements? Note: not - // checking arrayIndex >= array.Length (consistency with list of allowing - // count of 0; subsequent check takes care of the rest) - if (index > array.Length || count > array.Length - index) - throw new ArgumentException("count and index"); - - //upper bound - count += index; - - InOrderTreeWalk(delegate (Node node) - { - if (index >= count) - { - return false; - } - else - { - array[index++] = node.Item; - return true; - } - }); - } - - void ICollection.CopyTo(Array array, int index) - { - // TODO THROWS - - //if (array == null) - //{ - // ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); - //} - // - //if (array.Rank != 1) - //{ - // ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); - //} - // - //if (array.GetLowerBound(0) != 0) - //{ - // ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound); - //} - // - //if (index < 0) - //{ - // ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); - //} - // - //if (array.Length - index < Count) - //{ - // ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); - //} - - T[] tarray = array as T[]; - if (tarray != null) - { - CopyTo(tarray, index); - } - else - { - object[] objects = array as object[]; - //if (objects == null) - //{ - // ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArrayType); - //} - - try - { - InOrderTreeWalk(delegate (Node node) { objects[index++] = node.Item; return true; }); - } - catch (ArrayTypeMismatchException) - { - //ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidArrayType); - // TODO THROW - } - } - } - - #endregion - - #region IEnumerable members - public Enumerator GetEnumerator() - { - return new Enumerator(this); - } - - - - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new Enumerator(this); - } - #endregion - - #region Tree Specific Operations - - private static Node GetSibling(Node node, Node parent) - { - if (parent.Left == node) - { - return parent.Right; - } - return parent.Left; - } - - // After calling InsertionBalance, we need to make sure current and parent up-to-date. - // It doesn't matter if we keep grandParent and greatGrantParent up-to-date - // because we won't need to split again in the next node. - // By the time we need to split again, everything will be correctly set. - // - private void InsertionBalance(Node current, ref Node parent, Node grandParent, Node greatGrandParent) - { - Debug.Assert(grandParent != null, "Grand parent cannot be null here!"); - bool parentIsOnRight = (grandParent.Right == parent); - bool currentIsOnRight = (parent.Right == current); - - Node newChildOfGreatGrandParent; - if (parentIsOnRight == currentIsOnRight) - { // same orientation, single rotation - newChildOfGreatGrandParent = currentIsOnRight ? RotateLeft(grandParent) : RotateRight(grandParent); - } - else - { // different orientaton, double rotation - newChildOfGreatGrandParent = currentIsOnRight ? RotateLeftRight(grandParent) : RotateRightLeft(grandParent); - // current node now becomes the child of greatgrandparent - parent = greatGrandParent; - } - // grand parent will become a child of either parent of current. - grandParent.IsRed = true; - newChildOfGreatGrandParent.IsRed = false; - - ReplaceChildOfNodeOrRoot(greatGrandParent, grandParent, newChildOfGreatGrandParent); - } - - private static bool Is2Node(Node node) - { - Debug.Assert(node != null, "node cannot be null!"); - return IsBlack(node) && IsNullOrBlack(node.Left) && IsNullOrBlack(node.Right); - } - - private static bool Is4Node(Node node) - { - return IsRed(node.Left) && IsRed(node.Right); - } - - private static bool IsBlack(Node node) - { - return (node != null && !node.IsRed); - } - - private static bool IsNullOrBlack(Node node) - { - return (node == null || !node.IsRed); - } - - private static bool IsRed(Node node) - { - return (node != null && node.IsRed); - } - - private static void Merge2Nodes(Node parent, Node child1, Node child2) - { - Debug.Assert(IsRed(parent), "parent must be be red"); - // combing two 2-nodes into a 4-node - parent.IsRed = false; - child1.IsRed = true; - child2.IsRed = true; - } - - // Replace the child of a parent node. - // If the parent node is null, replace the root. - private void ReplaceChildOfNodeOrRoot(Node parent, Node child, Node newChild) - { - if (parent != null) - { - if (parent.Left == child) - { - parent.Left = newChild; - } - else - { - parent.Right = newChild; - } - } - else - { - root = newChild; - } - } - - // Replace the matching node with its succesor. - private void ReplaceNode(Node match, Node parentOfMatch, Node succesor, Node parentOfSuccesor) - { - if (succesor == match) - { // this node has no successor, should only happen if right child of matching node is null. - Debug.Assert(match.Right == null, "Right child must be null!"); - succesor = match.Left; - } - else - { - Debug.Assert(parentOfSuccesor != null, "parent of successor cannot be null!"); - Debug.Assert(succesor.Left == null, "Left child of succesor must be null!"); - Debug.Assert((succesor.Right == null && succesor.IsRed) || (succesor.Right.IsRed && !succesor.IsRed), "Succesor must be in valid state"); - if (succesor.Right != null) - { - succesor.Right.IsRed = false; - } - - if (parentOfSuccesor != match) - { // detach succesor from its parent and set its right child - parentOfSuccesor.Left = succesor.Right; - succesor.Right = match.Right; - } - - succesor.Left = match.Left; - } - - if (succesor != null) - { - succesor.IsRed = match.IsRed; - } - - ReplaceChildOfNodeOrRoot(parentOfMatch, match, succesor); - - } - - internal virtual Node FindNode(T item) - { - Node current = root; - while (current != null) - { - int order = comparer.Compare(item, current.Item); - if (order == 0) - { - return current; - } - else - { - current = (order < 0) ? current.Left : current.Right; - } - } - - return null; - } - - //used for bithelpers. Note that this implementation is completely different - //from the Subset's. The two should not be mixed. This indexes as if the tree were an array. - //http://en.wikipedia.org/wiki/Binary_Tree#Methods_for_storing_binary_trees - internal virtual int InternalIndexOf(T item) - { - Node current = root; - int count = 0; - while (current != null) - { - int order = comparer.Compare(item, current.Item); - if (order == 0) - { - return count; - } - else - { - current = (order < 0) ? current.Left : current.Right; - count = (order < 0) ? (2 * count + 1) : (2 * count + 2); - } - } - return -1; - } - - - - internal Node FindRange(T from, T to) - { - return FindRange(from, to, true, true); - } - internal Node FindRange(T from, T to, bool lowerBoundActive, bool upperBoundActive) - { - Node current = root; - while (current != null) - { - if (lowerBoundActive && comparer.Compare(from, current.Item) > 0) - { - current = current.Right; - } - else - { - if (upperBoundActive && comparer.Compare(to, current.Item) < 0) - { - current = current.Left; - } - else - { - return current; - } - } - } - - return null; - } - - internal void UpdateVersion() - { - ++version; - } - - - private static Node RotateLeft(Node node) - { - Node x = node.Right; - node.Right = x.Left; - x.Left = node; - return x; - } - - private static Node RotateLeftRight(Node node) - { - Node child = node.Left; - Node grandChild = child.Right; - - node.Left = grandChild.Right; - grandChild.Right = node; - child.Right = grandChild.Left; - grandChild.Left = child; - return grandChild; - } - - private static Node RotateRight(Node node) - { - Node x = node.Left; - node.Left = x.Right; - x.Right = node; - return x; - } - - private static Node RotateRightLeft(Node node) - { - Node child = node.Right; - Node grandChild = child.Left; - - node.Right = grandChild.Left; - grandChild.Left = node; - child.Left = grandChild.Right; - grandChild.Right = child; - return grandChild; - } - /// - /// Testing counter that can track rotations - /// - - - private static TreeRotation RotationNeeded(Node parent, Node current, Node sibling) - { - Debug.Assert(IsRed(sibling.Left) || IsRed(sibling.Right), "sibling must have at least one red child"); - if (IsRed(sibling.Left)) - { - if (parent.Left == current) - { - return TreeRotation.RightLeftRotation; - } - return TreeRotation.RightRotation; - } - else - { - if (parent.Left == current) - { - return TreeRotation.LeftRotation; - } - return TreeRotation.LeftRightRotation; - } - } - - /// - /// Used for deep equality of SortedSet testing - /// - /// - public static IEqualityComparer> CreateSetComparer() - { - return new SortedSetEqualityComparer(); - } - - /// - /// Create a new set comparer for this set, where this set's members' equality is defined by the - /// memberEqualityComparer. Note that this equality comparer's definition of equality must be the - /// same as this set's Comparer's definition of equality - /// - public static IEqualityComparer> CreateSetComparer(IEqualityComparer memberEqualityComparer) - { - return new SortedSetEqualityComparer(memberEqualityComparer); - } - - - /// - /// Decides whether these sets are the same, given the comparer. If the EC's are the same, we can - /// just use SetEquals, but if they aren't then we have to manually check with the given comparer - /// - internal static bool SortedSetEquals(SortedSet set1, SortedSet set2, IComparer comparer) - { - // handle null cases first - if (set1 == null) - { - return (set2 == null); - } - else if (set2 == null) - { - // set1 != null - return false; - } - - if (AreComparersEqual(set1, set2)) - { - if (set1.Count != set2.Count) - return false; - - return set1.SetEquals(set2); - } - else - { - bool found = false; - foreach (T item1 in set1) - { - found = false; - foreach (T item2 in set2) - { - if (comparer.Compare(item1, item2) == 0) - { - found = true; - break; - } - } - if (!found) - return false; - } - return true; - } - - } - - - //This is a little frustrating because we can't support more sorted structures - private static bool AreComparersEqual(SortedSet set1, SortedSet set2) - { - return set1.Comparer.Equals(set2.Comparer); - } - - - private static void Split4Node(Node node) - { - node.IsRed = true; - node.Left.IsRed = false; - node.Right.IsRed = false; - } - - /// - /// Copies this to an array. Used for DebugView - /// - /// - internal T[] ToArray() - { - T[] newArray = new T[Count]; - CopyTo(newArray); - return newArray; - } - - - #endregion - - #region ISet Members - - /// - /// Transform this set into its union with the IEnumerable OTHER - ///Attempts to insert each element and rejects it if it exists. - /// NOTE: The caller object is important as UnionWith uses the Comparator - ///associated with THIS to check equality - /// Throws ArgumentNullException if OTHER is null - /// - /// - public void UnionWith(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - SortedSet s = other as SortedSet; - TreeSubSet t = this as TreeSubSet; - - if (t != null) - VersionCheck(); - - if (s != null && t == null && this.count == 0) - { - SortedSet dummy = new SortedSet(s, this.comparer); - this.root = dummy.root; - this.count = dummy.count; - this.version++; - return; - } - - - if (s != null && t == null && AreComparersEqual(this, s) && (s.Count > this.Count / 2)) - { //this actually hurts if N is much greater than M the /2 is arbitrary - //first do a merge sort to an array. - T[] merged = new T[s.Count + this.Count]; - int c = 0; - Enumerator mine = this.GetEnumerator(); - Enumerator theirs = s.GetEnumerator(); - bool mineEnded = !mine.MoveNext(), theirsEnded = !theirs.MoveNext(); - while (!mineEnded && !theirsEnded) - { - int comp = Comparer.Compare(mine.Current, theirs.Current); - if (comp < 0) - { - merged[c++] = mine.Current; - mineEnded = !mine.MoveNext(); - } - else if (comp == 0) - { - merged[c++] = theirs.Current; - mineEnded = !mine.MoveNext(); - theirsEnded = !theirs.MoveNext(); - } - else - { - merged[c++] = theirs.Current; - theirsEnded = !theirs.MoveNext(); - } - } - - if (!mineEnded || !theirsEnded) - { - Enumerator remaining = (mineEnded ? theirs : mine); - do - { - merged[c++] = remaining.Current; - } while (remaining.MoveNext()); - } - - //now merged has all c elements - - //safe to gc the root, we have all the elements - root = null; - - - root = SortedSet.ConstructRootFromSortedArray(merged, 0, c - 1, null); - count = c; - version++; - } - else - { - AddAllElements(other); - } - } - - - private static Node ConstructRootFromSortedArray(T[] arr, int startIndex, int endIndex, Node redNode) - { - - - - //what does this do? - //you're given a sorted array... say 1 2 3 4 5 6 - //2 cases: - // If there are odd # of elements, pick the middle element (in this case 4), and compute - // its left and right branches - // If there are even # of elements, pick the left middle element, save the right middle element - // and call the function on the rest - // 1 2 3 4 5 6 -> pick 3, save 4 and call the fn on 1,2 and 5,6 - // now add 4 as a red node to the lowest element on the right branch - // 3 3 - // 1 5 -> 1 5 - // 2 6 2 4 6 - // As we're adding to the leftmost of the right branch, nesting will not hurt the red-black properties - // Leaf nodes are red if they have no sibling (if there are 2 nodes or if a node trickles - // down to the bottom - - - //the iterative way to do this ends up wasting more space than it saves in stack frames (at - //least in what i tried) - //so we're doing this recursively - //base cases are described below - int size = endIndex - startIndex + 1; - if (size == 0) - { - return null; - } - Node root = null; - if (size == 1) - { - root = new Node(arr[startIndex], false); - if (redNode != null) - { - root.Left = redNode; - } - } - else if (size == 2) - { - root = new Node(arr[startIndex], false); - root.Right = new Node(arr[endIndex], false); - root.Right.IsRed = true; - if (redNode != null) - { - root.Left = redNode; - } - } - else if (size == 3) - { - root = new Node(arr[startIndex + 1], false); - root.Left = new Node(arr[startIndex], false); - root.Right = new Node(arr[endIndex], false); - if (redNode != null) - { - root.Left.Left = redNode; - - } - } - else - { - int midpt = ((startIndex + endIndex) / 2); - root = new Node(arr[midpt], false); - root.Left = ConstructRootFromSortedArray(arr, startIndex, midpt - 1, redNode); - if (size % 2 == 0) - { - root.Right = ConstructRootFromSortedArray(arr, midpt + 2, endIndex, new Node(arr[midpt + 1], true)); - } - else - { - root.Right = ConstructRootFromSortedArray(arr, midpt + 1, endIndex, null); - } - } - return root; - - } - - - /// - /// Transform this set into its intersection with the IEnumerable OTHER - /// NOTE: The caller object is important as IntersectionWith uses the - /// comparator associated with THIS to check equality - /// Throws ArgumentNullException if OTHER is null - /// - /// - public virtual void IntersectWith(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if (Count == 0) - return; - - //HashSet optimizations can't be done until equality comparers and comparers are related - - //Technically, this would work as well with an ISorted - SortedSet s = other as SortedSet; - TreeSubSet t = this as TreeSubSet; - if (t != null) - VersionCheck(); - //only let this happen if i am also a SortedSet, not a SubSet - if (s != null && t == null && AreComparersEqual(this, s)) - { - - - //first do a merge sort to an array. - T[] merged = new T[this.Count]; - int c = 0; - Enumerator mine = this.GetEnumerator(); - Enumerator theirs = s.GetEnumerator(); - bool mineEnded = !mine.MoveNext(), theirsEnded = !theirs.MoveNext(); - T max = Max; - T min = Min; - - while (!mineEnded && !theirsEnded && Comparer.Compare(theirs.Current, max) <= 0) - { - int comp = Comparer.Compare(mine.Current, theirs.Current); - if (comp < 0) - { - mineEnded = !mine.MoveNext(); - } - else if (comp == 0) - { - merged[c++] = theirs.Current; - mineEnded = !mine.MoveNext(); - theirsEnded = !theirs.MoveNext(); - } - else - { - theirsEnded = !theirs.MoveNext(); - } - } - - //now merged has all c elements - - //safe to gc the root, we have all the elements - root = null; - - root = SortedSet.ConstructRootFromSortedArray(merged, 0, c - 1, null); - count = c; - version++; - } - else - { - IntersectWithEnumerable(other); - } - } - - internal virtual void IntersectWithEnumerable(IEnumerable other) - { - // - List toSave = new List(this.Count); - foreach (T item in other) - { - if (this.Contains(item)) - { - toSave.Add(item); - this.Remove(item); - } - } - this.Clear(); - AddAllElements(toSave); - - } - - - - /// - /// Transform this set into its complement with the IEnumerable OTHER - /// NOTE: The caller object is important as ExceptWith uses the - /// comparator associated with THIS to check equality - /// Throws ArgumentNullException if OTHER is null - /// - /// - public void ExceptWith(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if (count == 0) - return; - - if (other == this) - { - this.Clear(); - return; - } - - SortedSet asSorted = other as SortedSet; - - if (asSorted != null && AreComparersEqual(this, asSorted)) - { - //outside range, no point doing anything - if (!(comparer.Compare(asSorted.Max, this.Min) < 0 || comparer.Compare(asSorted.Min, this.Max) > 0)) - { - T min = this.Min; - T max = this.Max; - foreach (T item in other) - { - if (comparer.Compare(item, min) < 0) - continue; - if (comparer.Compare(item, max) > 0) - break; - Remove(item); - } - } - - } - else - { - RemoveAllElements(other); - } - } - - /// - /// Transform this set so it contains elements in THIS or OTHER but not both - /// NOTE: The caller object is important as SymmetricExceptWith uses the - /// comparator associated with THIS to check equality - /// Throws ArgumentNullException if OTHER is null - /// - /// - public void SymmetricExceptWith(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if (this.Count == 0) - { - this.UnionWith(other); - return; - } - - if (other == this) - { - this.Clear(); - return; - } - - - SortedSet asSorted = other as SortedSet; - -#if USING_HASH_SET - HashSet asHash = other as HashSet; -#endif - if (asSorted != null && AreComparersEqual(this, asSorted)) - { - SymmetricExceptWithSameEC(asSorted); - } -#if USING_HASH_SET - else if (asHash != null && this.comparer.Equals(Comparer.Default) && asHash.Comparer.Equals(EqualityComparer.Default)) { - SymmetricExceptWithSameEC(asHash); - } -#endif - else - { - //need perf improvement on this - T[] elements = (new List(other)).ToArray(); - Array.Sort(elements, this.Comparer); - SymmetricExceptWithSameEC(elements); - } - } - - //OTHER must be a set - internal void SymmetricExceptWithSameEC(ISet other) - { - foreach (T item in other) - { - //yes, it is classier to say - //if (!this.Remove(item))this.Add(item); - //but this ends up saving on rotations - if (this.Contains(item)) - { - this.Remove(item); - } - else - { - this.Add(item); - } - } - } - - //OTHER must be a sorted array - internal void SymmetricExceptWithSameEC(T[] other) - { - if (other.Length == 0) - { - return; - } - T last = other[0]; - for (int i = 0; i < other.Length; i++) - { - while (i < other.Length && i != 0 && comparer.Compare(other[i], last) == 0) - i++; - if (i >= other.Length) - break; - if (this.Contains(other[i])) - { - this.Remove(other[i]); - } - else - { - this.Add(other[i]); - } - last = other[i]; - } - } - - - /// - /// Checks whether this Tree is a subset of the IEnumerable other - /// - /// - /// - [System.Security.SecuritySafeCritical] - public bool IsSubsetOf(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if (Count == 0) - return true; - - - SortedSet asSorted = other as SortedSet; - if (asSorted != null && AreComparersEqual(this, asSorted)) - { - if (this.Count > asSorted.Count) - return false; - return IsSubsetOfSortedSetWithSameEC(asSorted); - } - else - { - //worst case: mark every element in my set and see if i've counted all - //O(MlogN) - - ElementCount result = CheckUniqueAndUnfoundElements(other, false); - return (result.uniqueCount == Count && result.unfoundCount >= 0); - } - } - - private bool IsSubsetOfSortedSetWithSameEC(SortedSet asSorted) - { - SortedSet prunedOther = asSorted.GetViewBetween(this.Min, this.Max); - foreach (T item in this) - { - if (!prunedOther.Contains(item)) - return false; - } - return true; - - } - - - /// - /// Checks whether this Tree is a proper subset of the IEnumerable other - /// - /// - /// - [System.Security.SecuritySafeCritical] - public bool IsProperSubsetOf(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if ((other as ICollection) != null) - { - if (Count == 0) - return (other as ICollection).Count > 0; - } - - -#if USING_HASH_SET - //do it one way for HashSets - HashSet asHash = other as HashSet; - if (asHash != null && comparer.Equals(Comparer.Default) && asHash.Comparer.Equals(EqualityComparer.Default)) { - return asHash.IsProperSupersetOf(this); - } -#endif - //another for sorted sets with the same comparer - SortedSet asSorted = other as SortedSet; - if (asSorted != null && AreComparersEqual(this, asSorted)) - { - if (this.Count >= asSorted.Count) - return false; - return IsSubsetOfSortedSetWithSameEC(asSorted); - } - - - //worst case: mark every element in my set and see if i've counted all - //O(MlogN). - ElementCount result = CheckUniqueAndUnfoundElements(other, false); - return (result.uniqueCount == Count && result.unfoundCount > 0); - } - - - /// - /// Checks whether this Tree is a super set of the IEnumerable other - /// - /// - /// - public bool IsSupersetOf(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if ((other as ICollection) != null && (other as ICollection).Count == 0) - return true; - - //do it one way for HashSets -#if USING_HASH_SET - HashSet asHash = other as HashSet; - if (asHash != null && comparer.Equals(Comparer.Default) && asHash.Comparer.Equals(EqualityComparer.Default)) { - return asHash.IsSubsetOf(this); - } -#endif - //another for sorted sets with the same comparer - SortedSet asSorted = other as SortedSet; - if (asSorted != null && AreComparersEqual(this, asSorted)) - { - if (this.Count < asSorted.Count) - return false; - SortedSet pruned = GetViewBetween(asSorted.Min, asSorted.Max); - foreach (T item in asSorted) - { - if (!pruned.Contains(item)) - return false; - } - return true; - } - //and a third for everything else - return ContainsAllElements(other); - } - - /// - /// Checks whether this Tree is a proper super set of the IEnumerable other - /// - /// - /// - [System.Security.SecuritySafeCritical] - public bool IsProperSupersetOf(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if (Count == 0) - return false; - - if ((other as ICollection) != null && (other as ICollection).Count == 0) - return true; - -#if USING_HASH_SET - //do it one way for HashSets - - HashSet asHash = other as HashSet; - if (asHash != null && comparer.Equals(Comparer.Default) && asHash.Comparer.Equals(EqualityComparer.Default)) { - return asHash.IsProperSubsetOf(this); - } -#endif - //another way for sorted sets - SortedSet asSorted = other as SortedSet; - if (asSorted != null && AreComparersEqual(asSorted, this)) - { - if (asSorted.Count >= this.Count) - return false; - SortedSet pruned = GetViewBetween(asSorted.Min, asSorted.Max); - foreach (T item in asSorted) - { - if (!pruned.Contains(item)) - return false; - } - return true; - } - - - //worst case: mark every element in my set and see if i've counted all - //O(MlogN) - //slight optimization, put it into a HashSet and then check can do it in O(N+M) - //but slower in better cases + wastes space - ElementCount result = CheckUniqueAndUnfoundElements(other, true); - return (result.uniqueCount < Count && result.unfoundCount == 0); - } - - - - /// - /// Checks whether this Tree has all elements in common with IEnumerable other - /// - /// - /// - [System.Security.SecuritySafeCritical] - public bool SetEquals(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - -#if USING_HASH_SET - HashSet asHash = other as HashSet; - if (asHash != null && comparer.Equals(Comparer.Default) && asHash.Comparer.Equals(EqualityComparer.Default)) { - return asHash.SetEquals(this); - } -#endif - SortedSet asSorted = other as SortedSet; - if (asSorted != null && AreComparersEqual(this, asSorted)) - { - IEnumerator mine = this.GetEnumerator(); - IEnumerator theirs = asSorted.GetEnumerator(); - bool mineEnded = !mine.MoveNext(); - bool theirsEnded = !theirs.MoveNext(); - while (!mineEnded && !theirsEnded) - { - if (Comparer.Compare(mine.Current, theirs.Current) != 0) - { - return false; - } - mineEnded = !mine.MoveNext(); - theirsEnded = !theirs.MoveNext(); - } - return mineEnded && theirsEnded; - } - - //worst case: mark every element in my set and see if i've counted all - //O(N) by size of other - ElementCount result = CheckUniqueAndUnfoundElements(other, true); - return (result.uniqueCount == Count && result.unfoundCount == 0); - } - - - - /// - /// Checks whether this Tree has any elements in common with IEnumerable other - /// - /// - /// - public bool Overlaps(IEnumerable other) - { - if (other == null) - { - throw new ArgumentNullException("other"); - } - - if (this.Count == 0) - return false; - - if ((other as ICollection != null) && (other as ICollection).Count == 0) - return false; - - SortedSet asSorted = other as SortedSet; - if (asSorted != null && AreComparersEqual(this, asSorted) && (comparer.Compare(Min, asSorted.Max) > 0 || comparer.Compare(Max, asSorted.Min) < 0)) - { - return false; - } -#if USING_HASH_SET - HashSet asHash = other as HashSet; - if (asHash != null && comparer.Equals(Comparer.Default) && asHash.Comparer.Equals(EqualityComparer.Default)) { - return asHash.Overlaps(this); - } -#endif - foreach (T item in other) - { - if (this.Contains(item)) - { - return true; - } - } - return false; - } - - /// - /// This works similar to HashSet's CheckUniqueAndUnfound (description below), except that the bit - /// array maps differently than in the HashSet. We can only use this for the bulk boolean checks. - /// - /// Determines counts that can be used to determine equality, subset, and superset. This - /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet - /// these properties can be checked faster without use of marking because we can assume - /// other has no duplicates. - /// - /// The following count checks are performed by callers: - /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = Count; i.e. everything - /// in other is in this and everything in this is in other - /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = Count; i.e. other may - /// have elements not in this and everything in this is in other - /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = Count; i.e - /// other must have at least one element not in this and everything in this is in other - /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less - /// than Count; i.e. everything in other was in this and this had at least one element - /// not contained in other. - /// - /// An earlier implementation used delegates to perform these checks rather than returning - /// an ElementCount struct; however this was changed due to the perf overhead of delegates. - /// - /// - /// Allows us to finish faster for equals and proper superset - /// because unfoundCount must be 0. - /// - // - // - // - // - // - // - [System.Security.SecurityCritical] - private unsafe ElementCount CheckUniqueAndUnfoundElements(IEnumerable other, bool returnIfUnfound) - { - ElementCount result; - - // need special case in case this has no elements. - if (Count == 0) - { - int numElementsInOther = 0; - foreach (T item in other) - { - numElementsInOther++; - // break right away, all we want to know is whether other has 0 or 1 elements - break; - } - result.uniqueCount = 0; - result.unfoundCount = numElementsInOther; - return result; - } - - - int originalLastIndex = Count; - int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); - - BitHelper bitHelper; - if (intArrayLength <= StackAllocThreshold) - { - int* bitArrayPtr = stackalloc int[intArrayLength]; - bitHelper = new BitHelper(bitArrayPtr, intArrayLength); - } - else - { - int[] bitArray = new int[intArrayLength]; - bitHelper = new BitHelper(bitArray, intArrayLength); - } - - // count of items in other not found in this - int unfoundCount = 0; - // count of unique items in other found in this - int uniqueFoundCount = 0; - - foreach (T item in other) - { - int index = InternalIndexOf(item); - if (index >= 0) - { - if (!bitHelper.IsMarked(index)) - { - // item hasn't been seen yet - bitHelper.MarkBit(index); - uniqueFoundCount++; - } - } - else - { - unfoundCount++; - if (returnIfUnfound) - { - break; - } - } - } - - result.uniqueCount = uniqueFoundCount; - result.unfoundCount = unfoundCount; - return result; - } - public int RemoveWhere(Predicate match) - { - if (match == null) - { - throw new ArgumentNullException("match"); - } - List matches = new List(this.Count); - - BreadthFirstTreeWalk(delegate (Node n) - { - if (match(n.Item)) - { - matches.Add(n.Item); - } - return true; - }); - // reverse breadth first to (try to) incur low cost - int actuallyRemoved = 0; - for (int i = matches.Count - 1; i >= 0; i--) - { - if (this.Remove(matches[i])) - { - actuallyRemoved++; - } - } - - return actuallyRemoved; - - } - - - #endregion - - #region ISorted Members - - - public T Min - { - get - { - T ret = default(T); - InOrderTreeWalk(delegate (SortedSet.Node n) { ret = n.Item; return false; }); - return ret; - } - } - - public T Max - { - get - { - T ret = default(T); - InOrderTreeWalk(delegate (SortedSet.Node n) { ret = n.Item; return false; }, true); - return ret; - } - } - - public IEnumerable Reverse() - { - Enumerator e = new Enumerator(this, true); - while (e.MoveNext()) - { - yield return e.Current; - } - } - - - /// - /// Returns a subset of this tree ranging from values lBound to uBound - /// Any changes made to the subset reflect in the actual tree - /// - /// Lowest Value allowed in the subset - /// Highest Value allowed in the subset - public virtual SortedSet GetViewBetween(T lowerValue, T upperValue) - { - if (Comparer.Compare(lowerValue, upperValue) > 0) - { - throw new ArgumentException("lowerBound is greater than upperBound"); - } - return new TreeSubSet(this, lowerValue, upperValue, true, true); - } - -#if DEBUG - - /// - /// debug status to be checked whenever any operation is called - /// - /// - internal virtual bool versionUpToDate() { - return true; - } -#endif - - - /// - /// This class represents a subset view into the tree. Any changes to this view - /// are reflected in the actual tree. Uses the Comparator of the underlying tree. - /// - /// -#if !FEATURE_NETCORE - [Serializable] - internal sealed class TreeSubSet : SortedSet, ISerializable, IDeserializationCallback - { -#else - internal sealed class TreeSubSet : SortedSet { -#endif - SortedSet underlying; - T min, max; - //these exist for unbounded collections - //for instance, you could allow this subset to be defined for i>10. The set will throw if - //anything <=10 is added, but there is no upperbound. These features Head(), Tail(), were punted - //in the spec, and are not available, but the framework is there to make them available at some point. - bool lBoundActive, uBoundActive; - //used to see if the count is out of date - - -#if DEBUG - internal override bool versionUpToDate() { - return (this.version == underlying.version); - } -#endif - - public TreeSubSet(SortedSet Underlying, T Min, T Max, bool lowerBoundActive, bool upperBoundActive) - : base(Underlying.Comparer) - { - underlying = Underlying; - min = Min; - max = Max; - lBoundActive = lowerBoundActive; - uBoundActive = upperBoundActive; - root = underlying.FindRange(min, max, lBoundActive, uBoundActive); // root is first element within range - count = 0; - version = -1; - VersionCheckImpl(); - } - -#if !FEATURE_NETCORE - /// - /// For serialization and deserialization - /// - private TreeSubSet() - { - comparer = null; - } - - - [SuppressMessage("Microsoft.Usage", "CA2236:CallBaseClassMethodsOnISerializableTypes", Justification = "special case TreeSubSet serialization")] - private TreeSubSet(SerializationInfo info, StreamingContext context) - { - siInfo = info; - OnDeserializationImpl(info); - } -#endif // !FEATURE_NETCORE - - /// - /// Additions to this tree need to be added to the underlying tree as well - /// - - internal override bool AddIfNotPresent(T item) - { - - if (!IsWithinRange(item)) - { - throw new ArgumentOutOfRangeException("collection"); - } - - bool ret = underlying.AddIfNotPresent(item); - VersionCheck(); -#if DEBUG - Debug.Assert(this.versionUpToDate() && this.root == this.underlying.FindRange(min, max)); -#endif - - return ret; - } - - - public override bool Contains(T item) - { - VersionCheck(); -#if DEBUG - Debug.Assert(this.versionUpToDate() && this.root == this.underlying.FindRange(min, max)); -#endif - return base.Contains(item); - } - - internal override bool DoRemove(T item) - { // todo: uppercase this and others - - if (!IsWithinRange(item)) - { - return false; - } - - bool ret = underlying.Remove(item); - VersionCheck(); -#if DEBUG - Debug.Assert(this.versionUpToDate() && this.root == this.underlying.FindRange(min, max)); -#endif - return ret; - } - - public override void Clear() - { - - - if (count == 0) - { - return; - } - - List toRemove = new List(); - BreadthFirstTreeWalk(delegate (Node n) { toRemove.Add(n.Item); return true; }); - while (toRemove.Count != 0) - { - underlying.Remove(toRemove[toRemove.Count - 1]); - toRemove.RemoveAt(toRemove.Count - 1); - } - root = null; - count = 0; - version = underlying.version; - } - - - internal override bool IsWithinRange(T item) - { - - int comp = (lBoundActive ? Comparer.Compare(min, item) : -1); - if (comp > 0) - { - return false; - } - comp = (uBoundActive ? Comparer.Compare(max, item) : 1); - if (comp < 0) - { - return false; - } - return true; - } - - internal override bool InOrderTreeWalk(TreeWalkPredicate action, Boolean reverse) - { - VersionCheck(); - - if (root == null) - { - return true; - } - - // The maximum height of a red-black tree is 2*lg(n+1). - // See page 264 of "Introduction to algorithms" by Thomas H. Cormen - Stack stack = new Stack(2 * (int)SortedSet.log2(count + 1)); //this is not exactly right if count is out of date, but the stack can grow - Node current = root; - while (current != null) - { - if (IsWithinRange(current.Item)) - { - stack.Push(current); - current = (reverse ? current.Right : current.Left); - } - else if (lBoundActive && Comparer.Compare(min, current.Item) > 0) - { - current = current.Right; - } - else - { - current = current.Left; - } - } - - while (stack.Count != 0) - { - current = stack.Pop(); - if (!action(current)) - { - return false; - } - - Node node = (reverse ? current.Left : current.Right); - while (node != null) - { - if (IsWithinRange(node.Item)) - { - stack.Push(node); - node = (reverse ? node.Right : node.Left); - } - else if (lBoundActive && Comparer.Compare(min, node.Item) > 0) - { - node = node.Right; - } - else - { - node = node.Left; - } - } - } - return true; - } - - internal override bool BreadthFirstTreeWalk(TreeWalkPredicate action) - { - VersionCheck(); - - if (root == null) - { - return true; - } - - List processQueue = new List(); - processQueue.Add(root); - Node current; - - while (processQueue.Count != 0) - { - current = processQueue[0]; - processQueue.RemoveAt(0); - if (IsWithinRange(current.Item) && !action(current)) - { - return false; - } - if (current.Left != null && (!lBoundActive || Comparer.Compare(min, current.Item) < 0)) - { - processQueue.Add(current.Left); - } - if (current.Right != null && (!uBoundActive || Comparer.Compare(max, current.Item) > 0)) - { - processQueue.Add(current.Right); - } - - } - return true; - } - - internal override SortedSet.Node FindNode(T item) - { - - if (!IsWithinRange(item)) - { - return null; - } - VersionCheck(); -#if DEBUG - Debug.Assert(this.versionUpToDate() && this.root == this.underlying.FindRange(min, max)); -#endif - return base.FindNode(item); - } - - //this does indexing in an inefficient way compared to the actual sortedset, but it saves a - //lot of space - internal override int InternalIndexOf(T item) - { - int count = -1; - foreach (T i in this) - { - count++; - if (Comparer.Compare(item, i) == 0) - return count; - } -#if DEBUG - Debug.Assert(this.versionUpToDate() && this.root == this.underlying.FindRange(min, max)); -#endif - return -1; - } - /// - /// checks whether this subset is out of date. updates if necessary. - /// - internal override void VersionCheck() - { - VersionCheckImpl(); - } - - private void VersionCheckImpl() - { - Debug.Assert(underlying != null, "Underlying set no longer exists"); - if (this.version != underlying.version) - { - this.root = underlying.FindRange(min, max, lBoundActive, uBoundActive); - this.version = underlying.version; - count = 0; - InOrderTreeWalk(delegate (Node n) { count++; return true; }); - } - } - - - - //This passes functionality down to the underlying tree, clipping edges if necessary - //There's nothing gained by having a nested subset. May as well draw it from the base - //Cannot increase the bounds of the subset, can only decrease it - public override SortedSet GetViewBetween(T lowerValue, T upperValue) - { - - if (lBoundActive && Comparer.Compare(min, lowerValue) > 0) - { - //lBound = min; - throw new ArgumentOutOfRangeException("lowerValue"); - } - if (uBoundActive && Comparer.Compare(max, upperValue) < 0) - { - //uBound = max; - throw new ArgumentOutOfRangeException("upperValue"); - } - TreeSubSet ret = (TreeSubSet)underlying.GetViewBetween(lowerValue, upperValue); - return ret; - } - - internal override void IntersectWithEnumerable(IEnumerable other) - { - - List toSave = new List(this.Count); - foreach (T item in other) - { - if (this.Contains(item)) - { - toSave.Add(item); - this.Remove(item); - } - } - this.Clear(); - this.AddAllElements(toSave); -#if DEBUG - Debug.Assert(this.versionUpToDate() && this.root == this.underlying.FindRange(min, max)); -#endif - } - -#if !FEATURE_NETCORE - void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) - { - GetObjectData(info, context); - } - - protected override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException("info"); - } - info.AddValue(maxName, max, typeof(T)); - info.AddValue(minName, min, typeof(T)); - info.AddValue(lBoundActiveName, lBoundActive); - info.AddValue(uBoundActiveName, uBoundActive); - base.GetObjectData(info, context); - } - - void IDeserializationCallback.OnDeserialization(Object sender) - { - //don't do anything here as its already been done by the constructor - //OnDeserialization(sender); - - } - - protected override void OnDeserialization(Object sender) - { - OnDeserializationImpl(sender); - } - - private void OnDeserializationImpl(Object sender) - { - if (siInfo == null) - { - throw new SerializationException("Invalid deserialization"); - } - - comparer = (IComparer)siInfo.GetValue(ComparerName, typeof(IComparer)); - int savedCount = siInfo.GetInt32(CountName); - max = (T)siInfo.GetValue(maxName, typeof(T)); - min = (T)siInfo.GetValue(minName, typeof(T)); - lBoundActive = siInfo.GetBoolean(lBoundActiveName); - uBoundActive = siInfo.GetBoolean(uBoundActiveName); - underlying = new SortedSet(); - - if (savedCount != 0) - { - T[] items = (T[])siInfo.GetValue(ItemsName, typeof(T[])); - - if (items == null) - { - throw new SerializationException("Missing values"); - } - - for (int i = 0; i < items.Length; i++) - { - underlying.Add(items[i]); - } - } - underlying.version = siInfo.GetInt32(VersionName); - count = underlying.count; - version = underlying.version - 1; - VersionCheck(); //this should update the count to be right and update root to be right - - if (count != savedCount) - { - throw new SerializationException("Mismatched count"); - } - siInfo = null; - - } -#endif // !FEATURE_NETCORE - } - - - #endregion - - #region Serialization methods - -#if !FEATURE_NETCORE - // LinkDemand here is unnecessary as this is a methodimpl and linkdemand from the interface should suffice - void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) - { - GetObjectData(info, context); - } - - protected virtual void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentException("info"); - } - - info.AddValue(CountName, count); //This is the length of the bucket array. - info.AddValue(ComparerName, comparer, typeof(IComparer)); - info.AddValue(VersionName, version); - - if (root != null) - { - T[] items = new T[Count]; - CopyTo(items, 0); - info.AddValue(ItemsName, items, typeof(T[])); - } - } - - void IDeserializationCallback.OnDeserialization(Object sender) - { - OnDeserialization(sender); - } - - protected virtual void OnDeserialization(Object sender) - { - if (comparer != null) - { - return; //Somebody had a dependency on this class and fixed us up before the ObjectManager got to it. - } - - if (siInfo == null) - { - throw new SerializationException("invalid on deserialize"); - } - - comparer = (IComparer)siInfo.GetValue(ComparerName, typeof(IComparer)); - int savedCount = siInfo.GetInt32(CountName); - - if (savedCount != 0) - { - T[] items = (T[])siInfo.GetValue(ItemsName, typeof(T[])); - - if (items == null) - { - throw new SerializationException("Missing values"); - - } - - for (int i = 0; i < items.Length; i++) - { - Add(items[i]); - } - } - - version = siInfo.GetInt32(VersionName); - if (count != savedCount) - { - throw new SerializationException("Mismatched count"); - } - siInfo = null; - } -#endif //!FEATURE_NETCORE - #endregion - - #region Helper Classes - internal class Node - { - public bool IsRed; - public T Item; - public Node Left; - public Node Right; - - public Node(T item) - { - // The default color will be red, we never need to create a black node directly. - this.Item = item; - IsRed = true; - } - - public Node(T item, bool isRed) - { - // The default color will be red, we never need to create a black node directly. - this.Item = item; - this.IsRed = isRed; - } - } - - [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] -#if !FEATURE_NETCORE - [Serializable] - public struct Enumerator : IEnumerator, IEnumerator, ISerializable, IDeserializationCallback - { -#else - public struct Enumerator : IEnumerator, IEnumerator { -#endif - private SortedSet tree; - private int version; - - - private Stack.Node> stack; - private SortedSet.Node current; - static SortedSet.Node dummyNode = new SortedSet.Node(default(T)); - - private bool reverse; - -#if !FEATURE_NETCORE - private SerializationInfo siInfo; -#endif - internal Enumerator(SortedSet set) - { - tree = set; - //this is a hack to make sure that the underlying subset has not been changed since - // - tree.VersionCheck(); - - version = tree.version; - - // 2lg(n + 1) is the maximum height - stack = new Stack.Node>(2 * (int)SortedSet.log2(set.Count + 1)); - current = null; - reverse = false; -#if !FEATURE_NETCORE - siInfo = null; -#endif - Intialize(); - } - - internal Enumerator(SortedSet set, bool reverse) - { - tree = set; - //this is a hack to make sure that the underlying subset has not been changed since - // - tree.VersionCheck(); - version = tree.version; - - // 2lg(n + 1) is the maximum height - stack = new Stack.Node>(2 * (int)SortedSet.log2(set.Count + 1)); - current = null; - this.reverse = reverse; -#if !FEATURE_NETCORE - siInfo = null; -#endif - Intialize(); - - } - -#if !FEATURE_NETCORE - private Enumerator(SerializationInfo info, StreamingContext context) - { - tree = null; - version = -1; - current = null; - reverse = false; - stack = null; - this.siInfo = info; - } - - void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) - { - GetObjectData(info, context); - } - - private void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException("info"); - - } - info.AddValue(TreeName, tree, typeof(SortedSet)); - info.AddValue(EnumVersionName, version); - info.AddValue(ReverseName, reverse); - info.AddValue(EnumStartName, !NotStartedOrEnded); - info.AddValue(NodeValueName, (current == null ? dummyNode.Item : current.Item), typeof(T)); - } - - void IDeserializationCallback.OnDeserialization(Object sender) - { - OnDeserialization(sender); - } - - private void OnDeserialization(Object sender) - { - if (siInfo == null) - { - throw new SerializationException("invalid on deserialize"); - } - - tree = (SortedSet)siInfo.GetValue(TreeName, typeof(SortedSet)); - version = siInfo.GetInt32(EnumVersionName); - reverse = siInfo.GetBoolean(ReverseName); - bool EnumStarted = siInfo.GetBoolean(EnumStartName); - stack = new Stack.Node>(2 * (int)SortedSet.log2(tree.Count + 1)); - current = null; - if (EnumStarted) - { - T item = (T)siInfo.GetValue(NodeValueName, typeof(T)); - Intialize(); - //go until it reaches the value we want - while (this.MoveNext()) - { - if (tree.Comparer.Compare(this.Current, item) == 0) - break; - } - } - - - } -#endif //!FEATURE_NETCORE - - - private void Intialize() - { - - current = null; - SortedSet.Node node = tree.root; - Node next = null, other = null; - while (node != null) - { - next = (reverse ? node.Right : node.Left); - other = (reverse ? node.Left : node.Right); - if (tree.IsWithinRange(node.Item)) - { - stack.Push(node); - node = next; - } - else if (next == null || !tree.IsWithinRange(next.Item)) - { - node = other; - } - else - { - node = next; - } - } - } - - public bool MoveNext() - { - - //this is a hack to make sure that the underlying subset has not been changed since - // - tree.VersionCheck(); - - if (version != tree.version) - { - throw new Exception("Collection has been modified during enumeration"); - } - - if (stack.Count == 0) - { - current = null; - return false; - } - - current = stack.Pop(); - SortedSet.Node node = (reverse ? current.Left : current.Right); - Node next = null, other = null; - while (node != null) - { - next = (reverse ? node.Right : node.Left); - other = (reverse ? node.Left : node.Right); - if (tree.IsWithinRange(node.Item)) - { - stack.Push(node); - node = next; - } - else if (other == null || !tree.IsWithinRange(other.Item)) - { - node = next; - } - else - { - node = other; - } - } - return true; - } - - public void Dispose() - { - } - - public T Current - { - get - { - if (current != null) - { - return current.Item; - } - return default(T); - } - } - - object IEnumerator.Current - { - get - { - if (current == null) - { - //ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen); - throw new InvalidOperationException("Current is null"); - } - - return current.Item; - } - } - - internal bool NotStartedOrEnded - { - get - { - return current == null; - } - } - - internal void Reset() - { - if (version != tree.version) - { - throw new Exception("Collection has been modified during enumeration"); - } - - stack.Clear(); - Intialize(); - } - - void IEnumerator.Reset() - { - Reset(); - } - - - - - } - - - - internal struct ElementCount - { - internal int uniqueCount; - internal int unfoundCount; - } - #endregion - - #region misc - - /// - /// Searches the set for a given value and returns the equal value it finds, if any. - /// - /// The value to search for. - /// The value from the set that the search found, or the default value of when the search yielded no match. - /// A value indicating whether the search was successful. - /// - /// This can be useful when you want to reuse a previously stored reference instead of - /// a newly constructed one (so that more sharing of references can occur) or to look up - /// a value that has more complete data than the value you currently have, although their - /// comparer functions indicate they are equal. - /// - public bool TryGetValue(T equalValue, out T actualValue) - { - Node node = FindNode(equalValue); - if (node != null) - { - actualValue = node.Item; - return true; - } - actualValue = default(T); - return false; - } - - // used for set checking operations (using enumerables) that rely on counting - private static int log2(int value) - { - //Contract.Requires(value>0) - int c = 0; - while (value > 0) - { - c++; - value >>= 1; - } - return c; - } - #endregion - - - } - - /// - /// A class that generates an IEqualityComparer for this SortedSet. Requires that the definition of - /// equality defined by the IComparer for this SortedSet be consistent with the default IEqualityComparer - /// for the type T. If not, such an IEqualityComparer should be provided through the constructor. - /// - internal class SortedSetEqualityComparer : IEqualityComparer> - { - private IComparer comparer; - private IEqualityComparer e_comparer; - - public SortedSetEqualityComparer() : this(null, null) { } - - public SortedSetEqualityComparer(IComparer comparer) : this(comparer, null) { } - - public SortedSetEqualityComparer(IEqualityComparer memberEqualityComparer) : this(null, memberEqualityComparer) { } - - /// - /// Create a new SetEqualityComparer, given a comparer for member order and another for member equality (these - /// must be consistent in their definition of equality) - /// - public SortedSetEqualityComparer(IComparer comparer, IEqualityComparer memberEqualityComparer) - { - if (comparer == null) - this.comparer = Comparer.Default; - else - this.comparer = comparer; - if (memberEqualityComparer == null) - e_comparer = EqualityComparer.Default; - else - e_comparer = memberEqualityComparer; - } - - - // using comparer to keep equals properties in tact; don't want to choose one of the comparers - public bool Equals(SortedSet x, SortedSet y) - { - return SortedSet.SortedSetEquals(x, y, comparer); - } - //IMPORTANT: this part uses the fact that GetHashCode() is consistent with the notion of equality in - //the set - public int GetHashCode(SortedSet obj) - { - int hashCode = 0; - if (obj != null) - { - foreach (T t in obj) - { - hashCode = hashCode ^ (e_comparer.GetHashCode(t) & 0x7FFFFFFF); - } - } // else returns hashcode of 0 for null HashSets - return hashCode; - } - - // Equals method for the comparer itself. - public override bool Equals(Object obj) - { - SortedSetEqualityComparer comparer = obj as SortedSetEqualityComparer; - if (comparer == null) - { - return false; - } - return (this.comparer == comparer.comparer); - } - - public override int GetHashCode() - { - return comparer.GetHashCode() ^ e_comparer.GetHashCode(); - } - - - } - - #endregion -} - -#endif \ No newline at end of file diff --git a/src/Core/Utility/SignatureHighlighter.cs b/src/Core/Utility/SignatureHighlighter.cs index 03d22d3..8ae4c75 100644 --- a/src/Core/Utility/SignatureHighlighter.cs +++ b/src/Core/Utility/SignatureHighlighter.cs @@ -44,6 +44,7 @@ namespace UnityExplorer public static readonly Color StringOrange = new Color(0.83f, 0.61f, 0.52f); public static readonly Color EnumGreen = new Color(0.57f, 0.76f, 0.43f); public static readonly Color KeywordBlue = new Color(0.3f, 0.61f, 0.83f); + public static readonly string keywordBlueHex = KeywordBlue.ToHex(); public static readonly Color NumberGreen = new Color(0.71f, 0.8f, 0.65f); internal static string GetClassColor(Type type) diff --git a/src/UI/CSConsole/CSAutoCompleter.cs b/src/UI/CSConsole/CSAutoCompleter.cs index 11368f0..dbf0ac7 100644 --- a/src/UI/CSConsole/CSAutoCompleter.cs +++ b/src/UI/CSConsole/CSAutoCompleter.cs @@ -3,70 +3,95 @@ using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; -using UnityExplorer.UI.CSharpConsole.Lexers; +using UnityExplorer.UI.CSConsole.Lexers; using UnityExplorer.UI.Widgets.AutoComplete; -namespace UnityExplorer.UI.CSharpConsole +namespace UnityExplorer.UI.CSConsole { public class CSAutoCompleter : ISuggestionProvider { - public InputFieldRef InputField => CSConsole.Input; + public InputFieldRef InputField => ConsoleController.Input; public bool AnchorToCaretPosition => true; public void OnSuggestionClicked(Suggestion suggestion) { - CSConsole.InsertSuggestionAtCaret(suggestion.UnderlyingValue); + ConsoleController.InsertSuggestionAtCaret(suggestion.UnderlyingValue); + AutoCompleteModal.Instance.ReleaseOwnership(this); } + // Delimiters for completions, notably does not include '.' private readonly HashSet delimiters = new HashSet { '{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?' }; - //private readonly HashSet expressions = new HashSet - //{ - // "new", - // "is", "as", - // "return", "yield", "throw", "in", - // "do", "for", "foreach", - // "else", - //}; + private readonly List suggestions = new List(); public void CheckAutocompletes() { - if (string.IsNullOrEmpty(InputField.Text)) { AutoCompleteModal.Instance.ReleaseOwnership(this); return; } - int caret = Math.Max(0, Math.Min(InputField.Text.Length - 1, InputField.Component.caretPosition - 1)); - int i = caret; + suggestions.Clear(); - while (i > 0) + int caret = Math.Max(0, Math.Min(InputField.Text.Length - 1, InputField.Component.caretPosition - 1)); + int start = caret; + + // If the character at the caret index is whitespace or delimiter, + // or if the next character (if it exists) is not whitespace, + // then we don't want to provide suggestions. + if (char.IsWhiteSpace(InputField.Text[caret]) + || delimiters.Contains(InputField.Text[caret]) + || (InputField.Text.Length > caret + 1 && !char.IsWhiteSpace(InputField.Text[caret + 1]))) { - i--; - char c = InputField.Text[i]; + AutoCompleteModal.Instance.ReleaseOwnership(this); + return; + } + + // get the current composition string (from caret back to last delimiter or whitespace) + while (start > 0) + { + start--; + char c = InputField.Text[start]; if (char.IsWhiteSpace(c) || delimiters.Contains(c)) { - i++; + start++; break; } } + string input = InputField.Text.Substring(start, caret - start + 1); - i = Math.Max(0, i); + // Get MCS completions - string input = InputField.Text.Substring(i, (caret - i + 1)); + string[] evaluatorCompletions = ConsoleController.Evaluator.GetCompletions(input, out string prefix); - string[] evaluatorCompletions = CSConsole.Evaluator.GetCompletions(input, out string prefix); - - if (evaluatorCompletions != null && evaluatorCompletions.Any()) + if (!string.IsNullOrEmpty(prefix) && evaluatorCompletions != null && evaluatorCompletions.Any()) { - var suggestions = from completion in evaluatorCompletions - select new Suggestion($"{prefix}{completion}", completion); + suggestions.AddRange(from completion in evaluatorCompletions + select new Suggestion($"{prefix}{completion}", completion)); + } + // Get manual keyword completions + + foreach (var kw in KeywordLexer.keywords) + { + if (kw.StartsWith(input)) + { + string completion = kw.Substring(input.Length, kw.Length - input.Length); + + suggestions.Add(new Suggestion( + $"{input}" + + $"{completion}", + completion)); + } + } + + if (suggestions.Any()) + { AutoCompleteModal.Instance.TakeOwnership(this); AutoCompleteModal.Instance.SetSuggestions(suggestions); } diff --git a/src/UI/CSConsole/CSConsole.cs b/src/UI/CSConsole/CSConsole.cs deleted file mode 100644 index 9066afa..0000000 --- a/src/UI/CSConsole/CSConsole.cs +++ /dev/null @@ -1,506 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.EventSystems; -using UnityEngine.UI; -using UnityExplorer.Core.CSharp; -using UnityExplorer.Core.Input; -using UnityExplorer.UI.Panels; -using UnityExplorer.UI.Widgets.AutoComplete; - -namespace UnityExplorer.UI.CSharpConsole -{ - public static class CSConsole - { - #region Strings / defaults - - internal const string STARTUP_TEXT = @"Welcome to the UnityExplorer C# Console. - -The following helper methods are available: - -* Log(""message"") logs a message to the debug console - -* StartCoroutine(IEnumerator routine) start the IEnumerator as a UnityEngine.Coroutine - -* CurrentTarget() returns the target of the active Inspector tab as System.Object - -* AllTargets() returns a System.Object[] array containing the targets of all active tabs - -* Inspect(someObject) to inspect an instance, eg. Inspect(Camera.main); - -* Inspect(typeof(SomeClass)) to inspect a Class with static reflection - -* AddUsing(""SomeNamespace"") adds a using directive to the C# console - -* GetUsing() logs the current using directives to the debug console - -* Reset() resets all using directives and variables -"; - - internal static readonly string[] DefaultUsing = new string[] - { - "System", - "System.Linq", - "System.Collections", - "System.Collections.Generic", - "System.Reflection", - "UnityEngine", -#if CPP - "UnhollowerBaseLib", - "UnhollowerRuntimeLib", -#endif - }; - - #endregion - - public static ScriptEvaluator Evaluator; - public static LexerBuilder Lexer; - public static CSAutoCompleter Completer; - - private static HashSet usingDirectives; - private static StringBuilder evaluatorOutput; - - public static CSConsolePanel Panel => UIManager.CSharpConsole; - public static InputFieldRef Input => Panel.Input; - - public static int LastCaretPosition { get; private set; } - internal static float defaultInputFieldAlpha; - - // Todo save as config? - public static bool EnableCtrlRShortcut { get; private set; } = true; - public static bool EnableAutoIndent { get; private set; } = true; - public static bool EnableSuggestions { get; private set; } = true; - - public static void Init() - { - try - { - ResetConsole(false); - Evaluator.Compile("0 == 0"); - } - catch - { - ExplorerCore.LogWarning("C# Console probably not supported, todo"); - return; - } - - Lexer = new LexerBuilder(); - Completer = new CSAutoCompleter(); - - Panel.OnInputChanged += OnConsoleInputChanged; - Panel.InputScroll.OnScroll += OnInputScrolled; - Panel.OnCompileClicked += Evaluate; - Panel.OnResetClicked += ResetConsole; - Panel.OnAutoIndentToggled += OnToggleAutoIndent; - Panel.OnCtrlRToggled += OnToggleCtrlRShortcut; - Panel.OnSuggestionsToggled += OnToggleSuggestions; - - } - - // Updating and event listeners - - private static void OnInputScrolled() => HighlightVisibleInput(Input.Text); - - // Invoked at most once per frame - private static void OnConsoleInputChanged(string value) - { - LastCaretPosition = Input.Component.caretPosition; - - if (EnableSuggestions) - Completer.CheckAutocompletes(); - - HighlightVisibleInput(value); - } - - public static void Update() - { - int lastCaretPos = LastCaretPosition; - UpdateCaret(); - bool caretMoved = lastCaretPos != LastCaretPosition; - - if (EnableSuggestions && caretMoved) - { - Completer.CheckAutocompletes(); - } - - //if (EnableAutoIndent && caretMoved) - // DoAutoIndent(); - - if (EnableCtrlRShortcut - && (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl)) - && InputManager.GetKeyDown(KeyCode.R)) - { - Evaluate(Panel.Input.Text); - } - } - - private static void UpdateCaret() - { - LastCaretPosition = Input.Component.caretPosition; - - // todo check if out of bounds, move content if so - } - - - #region Evaluating - - public static void ResetConsole() => ResetConsole(true); - - public static void ResetConsole(bool logSuccess = true) - { - if (Evaluator != null) - Evaluator.Dispose(); - - evaluatorOutput = new StringBuilder(); - Evaluator = new ScriptEvaluator(new StringWriter(evaluatorOutput)) - { - InteractiveBaseClass = typeof(ScriptInteraction) - }; - - usingDirectives = new HashSet(); - foreach (var use in DefaultUsing) - AddUsing(use); - - if (logSuccess) - ExplorerCore.Log($"C# Console reset. Using directives:\r\n{Evaluator.GetUsing()}"); - } - - public static void AddUsing(string assemblyName) - { - if (!usingDirectives.Contains(assemblyName)) - { - Evaluate($"using {assemblyName};", true); - usingDirectives.Add(assemblyName); - } - } - - public static void Evaluate() - { - Evaluate(Input.Text); - } - - public static void Evaluate(string input, bool supressLog = false) - { - try - { - Evaluator.Run(input); - - string output = ScriptEvaluator._textWriter.ToString(); - var outputSplit = output.Split('\n'); - if (outputSplit.Length >= 2) - output = outputSplit[outputSplit.Length - 2]; - evaluatorOutput.Clear(); - - if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) - throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); - - if (!supressLog) - ExplorerCore.Log("Code executed successfully."); - } - catch (FormatException fex) - { - if (!supressLog) - ExplorerCore.LogWarning(fex.Message); - } - catch (Exception ex) - { - if (!supressLog) - ExplorerCore.LogWarning(ex); - } - } - - #endregion - - - #region Lexer Highlighting - - private static void HighlightVisibleInput(string value) - { - int startIdx = 0; - int endIdx = value.Length - 1; - int topLine = 0; - - // Calculate visible text if necessary - if (Input.Rect.rect.height > Panel.InputScroll.ViewportRect.rect.height) - { - topLine = -1; - int bottomLine = -1; - - // the top and bottom position of the viewport in relation to the text height - // they need the half-height adjustment to normalize against the 'line.topY' value. - var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); - var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height - (Input.Rect.rect.height * 0.5f); - - for (int i = 0; i < Input.TextGenerator.lineCount; i++) - { - var line = Input.TextGenerator.lines[i]; - // if not set the top line yet, and top of line is below the viewport top - if (topLine == -1 && line.topY <= viewportMin) - topLine = i; - // if bottom of line is below the viewport bottom - if ((line.topY - line.height) >= viewportMax) - bottomLine = i; - } - // make sure lines are valid - topLine = Math.Max(0, topLine - 1); - bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1); - - startIdx = Input.TextGenerator.lines[topLine].startCharIdx; - endIdx = bottomLine == Input.TextGenerator.lineCount - ? value.Length - 1 - : (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1); - } - - - // Highlight the visible text with the LexerBuilder - Panel.HighlightText.text = Lexer.BuildHighlightedString(value, startIdx, endIdx, topLine); - } - - #endregion - - - #region Autocompletes - - public static void InsertSuggestionAtCaret(string suggestion) - { - string input = Input.Text; - input = input.Insert(LastCaretPosition, suggestion); - Input.Text = input; - - RuntimeProvider.Instance.StartCoroutine(SetAutocompleteCaret(LastCaretPosition += suggestion.Length)); - } - - private static IEnumerator SetAutocompleteCaret(int caretPosition) - { - var color = Input.Component.selectionColor; - color.a = 0f; - Input.Component.selectionColor = color; - yield return null; - - EventSystem.current.SetSelectedGameObject(Panel.Input.UIRoot, null); - yield return null; - - Input.Component.caretPosition = caretPosition; - Input.Component.selectionFocusPosition = caretPosition; - color.a = defaultInputFieldAlpha; - Input.Component.selectionColor = color; - } - - - #endregion - - - // TODO indenting - #region AUTO INDENT TODO - - //private static void DoAutoIndent() - //{ - // int caret = Panel.LastCaretPosition; - // Panel.InputField.Text = Lexer.AutoIndentOnEnter(InputField.text, ref caret); - // InputField.caretPosition = caret; - // - // Panel.InputText.Rebuild(CanvasUpdate.Prelayout); - // InputField.ForceLabelUpdate(); - // InputField.Rebuild(CanvasUpdate.Prelayout); - // - // OnConsoleInputChanged(InputField.text); - //} - - - // Auto-indenting - - //public int GetIndentLevel(string input, int toIndex) - //{ - // bool stringState = false; - // int indent = 0; - // - // for (int i = 0; i < toIndex && i < input.Length; i++) - // { - // char character = input[i]; - // - // if (character == '"') - // stringState = !stringState; - // else if (!stringState && character == INDENT_OPEN) - // indent++; - // else if (!stringState && character == INDENT_CLOSE) - // indent--; - // } - // - // if (indent < 0) - // indent = 0; - // - // return indent; - //} - - //// TODO not quite correct, but almost there. - // - //public string AutoIndentOnEnter(string input, ref int caretPos) - //{ - // var sb = new StringBuilder(input); - // - // bool inString = false; - // bool inChar = false; - // int currentIndent = 0; - // int curLineIndent = 0; - // bool prevWasNewLine = true; - // - // // process before caret position - // for (int i = 0; i < caretPos; i++) - // { - // char c = sb[i]; - // - // ExplorerCore.Log(i + ": " + c); - // - // // update string/char state - // if (!inChar && c == '\"') - // inString = !inString; - // else if (!inString && c == '\'') - // inChar = !inChar; - // - // // continue if inside string or char - // if (inString || inChar) - // continue; - // - // // check for new line - // if (c == '\n') - // { - // ExplorerCore.Log("new line, resetting line counts"); - // curLineIndent = 0; - // prevWasNewLine = true; - // } - // // check for indent - // else if (c == '\t' && prevWasNewLine) - // { - // ExplorerCore.Log("its a tab"); - // if (curLineIndent > currentIndent) - // { - // ExplorerCore.Log("too many tabs, removing"); - // // already reached the indent we should have - // sb.Remove(i, 1); - // i--; - // caretPos--; - // curLineIndent--; - // } - // else - // curLineIndent++; - // } - // // remove spaces on new lines - // else if (c == ' ' && prevWasNewLine) - // { - // ExplorerCore.Log("removing newline-space"); - // sb.Remove(i, 1); - // i--; - // caretPos--; - // } - // else - // { - // if (c == INDENT_CLOSE) - // currentIndent--; - // - // if (prevWasNewLine && curLineIndent < currentIndent) - // { - // ExplorerCore.Log("line is not indented enough"); - // // line is not indented enough - // int diff = currentIndent - curLineIndent; - // sb.Insert(i, new string('\t', diff)); - // caretPos += diff; - // i += diff; - // } - // - // // check for brackets - // if ((c == INDENT_CLOSE || c == INDENT_OPEN) && !prevWasNewLine) - // { - // ExplorerCore.Log("bracket needs new line"); - // - // // need to put it on a new line - // sb.Insert(i, $"\n{new string('\t', currentIndent)}"); - // caretPos += 1 + currentIndent; - // i += 1 + currentIndent; - // } - // - // if (c == INDENT_OPEN) - // currentIndent++; - // - // prevWasNewLine = false; - // } - // } - // - // // todo put caret on new line after previous bracket if needed - // // indent caret to current indent - // - // // process after caret position, make sure there are equal opened/closed brackets - // ExplorerCore.Log("-- after caret --"); - // for (int i = caretPos; i < sb.Length; i++) - // { - // char c = sb[i]; - // ExplorerCore.Log(i + ": " + c); - // - // // update string/char state - // if (!inChar && c == '\"') - // inString = !inString; - // else if (!inString && c == '\'') - // inChar = !inChar; - // - // if (inString || inChar) - // continue; - // - // if (c == INDENT_OPEN) - // currentIndent++; - // else if (c == INDENT_CLOSE) - // currentIndent--; - // } - // - // if (currentIndent > 0) - // { - // ExplorerCore.Log("there are not enough closing brackets, curIndent is " + currentIndent); - // // There are not enough close brackets - // - // // TODO this should append in reverse indent order (small indents inserted first, then biggest). - // while (currentIndent > 0) - // { - // ExplorerCore.Log("Inserting closing bracket with " + currentIndent + " indent"); - // // append the indented '}' on a new line - // sb.Insert(caretPos, $"\n{new string('\t', currentIndent - 1)}}}"); - // - // currentIndent--; - // } - // - // } - // //else if (currentIndent < 0) - // //{ - // // // There are too many close brackets - // // - // // // todo? - // //} - // - // return sb.ToString(); - //} - - #endregion - - - #region UI Listeners and options - - private static void OnToggleAutoIndent(bool value) - { - // TODO - } - - private static void OnToggleCtrlRShortcut(bool value) - { - // TODO - } - - private static void OnToggleSuggestions(bool value) - { - // TODO - } - - #endregion - - } -} diff --git a/src/UI/CSConsole/ConsoleController.cs b/src/UI/CSConsole/ConsoleController.cs new file mode 100644 index 0000000..57c5973 --- /dev/null +++ b/src/UI/CSConsole/ConsoleController.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using UnityExplorer.UI.CSConsole; +using UnityExplorer.Core.Input; +using UnityExplorer.UI.Panels; +using UnityExplorer.UI.Widgets.AutoComplete; + +namespace UnityExplorer.UI.CSConsole +{ + public static class ConsoleController + { + #region Strings / defaults + + internal const string STARTUP_TEXT = @"Welcome to the UnityExplorer C# Console. + +The following helper methods are available: + +* Log(""message"") logs a message to the debug console + +* StartCoroutine(IEnumerator routine) start the IEnumerator as a UnityEngine.Coroutine + +* CurrentTarget() returns the target of the active Inspector tab as System.Object + +* AllTargets() returns a System.Object[] array containing the targets of all active tabs + +* Inspect(someObject) to inspect an instance, eg. Inspect(Camera.main); + +* Inspect(typeof(SomeClass)) to inspect a Class with static reflection + +* AddUsing(""SomeNamespace"") adds a using directive to the C# console + +* GetUsing() logs the current using directives to the debug console + +* Reset() resets all using directives and variables +"; + + internal static readonly string[] DefaultUsing = new string[] + { + "System", + "System.Linq", + "System.Collections", + "System.Collections.Generic", + "System.Reflection", + "UnityEngine", +#if CPP + "UnhollowerBaseLib", + "UnhollowerRuntimeLib", +#endif + }; + + #endregion + + public static ScriptEvaluator Evaluator; + public static LexerBuilder Lexer; + public static CSAutoCompleter Completer; + + private static HashSet usingDirectives; + private static StringBuilder evaluatorOutput; + + public static CSConsolePanel Panel => UIManager.CSharpConsole; + public static InputFieldRef Input => Panel.Input; + + public static int LastCaretPosition { get; private set; } + public static int PreviousCaretPosition { get; private set; } + internal static float defaultInputFieldAlpha; + + // Todo save as config? + public static bool EnableCtrlRShortcut { get; private set; } = true; + public static bool EnableAutoIndent { get; private set; } = true; + public static bool EnableSuggestions { get; private set; } = true; + + public static void Init() + { + try + { + ResetConsole(false); + Evaluator.Compile("0 == 0"); + } + catch + { + ExplorerCore.LogWarning("C# Console probably not supported, todo"); + return; + } + + Lexer = new LexerBuilder(); + Completer = new CSAutoCompleter(); + + Panel.OnInputChanged += OnInputChanged; + Panel.InputScroll.OnScroll += OnInputScrolled; + Panel.OnCompileClicked += Evaluate; + Panel.OnResetClicked += ResetConsole; + Panel.OnAutoIndentToggled += OnToggleAutoIndent; + Panel.OnCtrlRToggled += OnToggleCtrlRShortcut; + Panel.OnSuggestionsToggled += OnToggleSuggestions; + + } + + #region UI Listeners and options + + // TODO save + + private static void OnToggleAutoIndent(bool value) + { + EnableAutoIndent = value; + } + + private static void OnToggleCtrlRShortcut(bool value) + { + EnableCtrlRShortcut = value; + } + + private static void OnToggleSuggestions(bool value) + { + EnableSuggestions = value; + } + + #endregion + + // Updating and event listeners + + private static bool settingAutoCompletion; + + private static void OnInputScrolled() => HighlightVisibleInput(); + + // Invoked at most once per frame + private static void OnInputChanged(string value) + { + if (!settingAutoCompletion && EnableSuggestions) + Completer.CheckAutocompletes(); + + if (!settingAutoCompletion && EnableAutoIndent) + DoAutoIndent(); + + HighlightVisibleInput(); + } + + public static void Update() + { + int lastCaretPos = LastCaretPosition; + UpdateCaret(); + bool caretMoved = lastCaretPos != LastCaretPosition; + + if (!settingAutoCompletion && EnableSuggestions && caretMoved) + { + Completer.CheckAutocompletes(); + } + + if (EnableCtrlRShortcut + && (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl)) + && InputManager.GetKeyDown(KeyCode.R)) + { + Evaluate(Panel.Input.Text); + } + } + + private const int CSCONSOLE_LINEHEIGHT = 18; + + private static void UpdateCaret() + { + if (Input.Component.isFocused) + { + PreviousCaretPosition = LastCaretPosition; + LastCaretPosition = Input.Component.caretPosition; + } + + if (Input.Text.Length == 0) + return; + + var charInfo = Input.TextGenerator.characters[LastCaretPosition]; + var charTop = charInfo.cursorPos.y; + var charBot = charTop - CSCONSOLE_LINEHEIGHT; + + var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); + var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; + + float diff = 0f; + if (charTop > viewportMin) + diff = charTop - viewportMin; + else if (charBot < viewportMax) + diff = charBot - viewportMax; + + if (Math.Abs(diff) > 1) + { + var rect = Input.Rect; + rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, rect.anchoredPosition.y - diff); + } + } + + + #region Evaluating + + public static void ResetConsole() => ResetConsole(true); + + public static void ResetConsole(bool logSuccess = true) + { + if (Evaluator != null) + Evaluator.Dispose(); + + evaluatorOutput = new StringBuilder(); + Evaluator = new ScriptEvaluator(new StringWriter(evaluatorOutput)) + { + InteractiveBaseClass = typeof(ScriptInteraction) + }; + + usingDirectives = new HashSet(); + foreach (var use in DefaultUsing) + AddUsing(use); + + if (logSuccess) + ExplorerCore.Log($"C# Console reset. Using directives:\r\n{Evaluator.GetUsing()}"); + } + + public static void AddUsing(string assemblyName) + { + if (!usingDirectives.Contains(assemblyName)) + { + Evaluate($"using {assemblyName};", true); + usingDirectives.Add(assemblyName); + } + } + + public static void Evaluate() + { + Evaluate(Input.Text); + } + + public static void Evaluate(string input, bool supressLog = false) + { + try + { + Evaluator.Run(input); + + string output = ScriptEvaluator._textWriter.ToString(); + var outputSplit = output.Split('\n'); + if (outputSplit.Length >= 2) + output = outputSplit[outputSplit.Length - 2]; + evaluatorOutput.Clear(); + + if (ScriptEvaluator._reportPrinter.ErrorsCount > 0) + throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}"); + + if (!supressLog) + ExplorerCore.Log("Code executed successfully."); + } + catch (FormatException fex) + { + if (!supressLog) + ExplorerCore.LogWarning(fex.Message); + } + catch (Exception ex) + { + if (!supressLog) + ExplorerCore.LogWarning(ex); + } + } + + #endregion + + + #region Lexer Highlighting + + private static void HighlightVisibleInput() + { + int startIdx = 0; + int endIdx = Input.Text.Length - 1; + int topLine = 0; + + try + { + // Calculate visible text if necessary + if (Input.Rect.rect.height > Panel.InputScroll.ViewportRect.rect.height) + { + topLine = -1; + int bottomLine = -1; + + // the top and bottom position of the viewport in relation to the text height + // they need the half-height adjustment to normalize against the 'line.topY' value. + var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); + var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; + + for (int i = 0; i < Input.TextGenerator.lineCount; i++) + { + var line = Input.TextGenerator.lines[i]; + // if not set the top line yet, and top of line is below the viewport top + if (topLine == -1 && line.topY <= viewportMin) + topLine = i; + // if bottom of line is below the viewport bottom + if ((line.topY - line.height) >= viewportMax) + bottomLine = i; + } + + topLine = Math.Max(0, topLine - 1); + bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1); + + startIdx = Input.TextGenerator.lines[topLine].startCharIdx; + endIdx = (bottomLine >= Input.TextGenerator.lineCount - 1) + ? Input.Text.Length - 1 + : (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1); + } + } + catch (Exception ex) + { + ExplorerCore.Log("Exception on HighlightVisibleText: " + ex.GetType().Name + ", " + ex.Message); + + } + + + // Highlight the visible text with the LexerBuilder + Panel.HighlightText.text = Lexer.BuildHighlightedString(Input.Text, startIdx, endIdx, topLine); + } + + #endregion + + + #region Autocompletes + + public static void InsertSuggestionAtCaret(string suggestion) + { + settingAutoCompletion = true; + Input.Text = Input.Text.Insert(LastCaretPosition, suggestion); + + RuntimeProvider.Instance.StartCoroutine(SetAutocompleteCaret(LastCaretPosition + suggestion.Length)); + LastCaretPosition = Input.Component.caretPosition; + } + + private static IEnumerator SetAutocompleteCaret(int caretPosition) + { + var color = Input.Component.selectionColor; + color.a = 0f; + Input.Component.selectionColor = color; + yield return null; + + EventSystem.current.SetSelectedGameObject(Panel.Input.UIRoot, null); + yield return null; + + Input.Component.caretPosition = caretPosition; + Input.Component.selectionFocusPosition = caretPosition; + LastCaretPosition = Input.Component.caretPosition; + + color.a = defaultInputFieldAlpha; + Input.Component.selectionColor = color; + + settingAutoCompletion = false; + } + + + #endregion + + + #region Auto indenting + + private static int prevContentLen = 0; + + private static void DoAutoIndent() + { + if (Input.Text.Length > prevContentLen) + { + int inc = Input.Text.Length - prevContentLen; + + if (inc == 1) + { + int caret = Input.Component.caretPosition; + Input.Text = Lexer.IndentCharacter(Input.Text, ref caret); + Input.Component.caretPosition = caret; + LastCaretPosition = caret; + } + else + { + // todo indenting for copy+pasted content + + //ExplorerCore.Log("Content increased by " + inc); + //var comp = Input.Text.Substring(PreviousCaretPosition, inc); + //ExplorerCore.Log("composition string: " + comp); + } + } + + prevContentLen = Input.Text.Length; + } + + #endregion + + + + } +} diff --git a/src/UI/CSConsole/LexerBuilder.cs b/src/UI/CSConsole/LexerBuilder.cs index 376cbae..8e76f89 100644 --- a/src/UI/CSConsole/LexerBuilder.cs +++ b/src/UI/CSConsole/LexerBuilder.cs @@ -1,12 +1,13 @@ -using System; +using Mono.CSharp; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; using UnityEngine.UI; -using UnityExplorer.UI.CSharpConsole.Lexers; +using UnityExplorer.UI.CSConsole.Lexers; -namespace UnityExplorer.UI.CSharpConsole +namespace UnityExplorer.UI.CSConsole { public struct MatchInfo { @@ -20,19 +21,22 @@ namespace UnityExplorer.UI.CSharpConsole #region Core and initialization public const char WHITESPACE = ' '; - public const char INDENT_OPEN = '{'; - public const char INDENT_CLOSE = '}'; + public readonly HashSet IndentOpenChars = new HashSet { '{', '(' }; + public readonly HashSet IndentCloseChars = new HashSet { '}', ')' }; private readonly Lexer[] lexers; private readonly HashSet delimiters = new HashSet(); + private readonly StringLexer stringLexer = new StringLexer(); + private readonly CommentLexer commentLexer = new CommentLexer(); + public LexerBuilder() { lexers = new Lexer[] { - new CommentLexer(), + commentLexer, + stringLexer, new SymbolLexer(), - new StringLexer(), new NumberLexer(), new KeywordLexer(), }; @@ -49,14 +53,22 @@ namespace UnityExplorer.UI.CSharpConsole #endregion - public int LastCommittedIndex { get; private set; } - public int LookaheadIndex { get; private set; } + /// The last committed index for a match or no-match. Starts at -1 for a new parse. + public int CommittedIndex { get; private set; } + /// The index of the character we are currently parsing, at minimum it will be CommittedIndex + 1. + public int CurrentIndex { get; private set; } - public char Current => !EndOfInput ? currentInput[LookaheadIndex] : WHITESPACE; - public char Previous => LookaheadIndex >= 1 ? currentInput[LookaheadIndex - 1] : WHITESPACE; + /// The current character we are parsing, determined by CurrentIndex. + public char Current => !EndOfInput ? currentInput[CurrentIndex] : WHITESPACE; + /// The previous character (CurrentIndex - 1), or whitespace if no previous character. + public char Previous => CurrentIndex >= 1 ? currentInput[CurrentIndex - 1] : WHITESPACE; - public bool EndOfInput => LookaheadIndex > currentEndIdx; - public bool EndOrNewLine => EndOfInput || Current == '\n' || Current == '\r'; + /// Returns true if CurrentIndex is >= the current input length. + public bool EndOfInput => CurrentIndex > currentEndIdx; + /// Returns true if EndOfInput or current character is a new line. + public bool EndOrNewLine => EndOfInput || IsNewLine(Current); + + public static bool IsNewLine(char c) => c == '\n' || c == '\r'; private string currentInput; private int currentStartIdx; @@ -72,6 +84,9 @@ namespace UnityExplorer.UI.CSharpConsole /// A string which contains the amount of leading lines specified, as well as the rich-text highlighted section. public string BuildHighlightedString(string input, int startIdx, int endIdx, int leadingLines) { + + + if (string.IsNullOrEmpty(input) || endIdx <= startIdx) return input; @@ -109,16 +124,16 @@ namespace UnityExplorer.UI.CSharpConsole // Match builder, iterates through each Lexer and returns all matches found. - private IEnumerable GetMatches() + public IEnumerable GetMatches() { - LastCommittedIndex = currentStartIdx - 1; + CommittedIndex = currentStartIdx - 1; Rollback(); while (!EndOfInput) { SkipWhitespace(); bool anyMatch = false; - int startIndex = LastCommittedIndex + 1; + int startIndex = CommittedIndex + 1; foreach (var lexer in lexers) { @@ -129,7 +144,7 @@ namespace UnityExplorer.UI.CSharpConsole yield return new MatchInfo { startIndex = startIndex, - endIndex = LastCommittedIndex, + endIndex = CommittedIndex, htmlColorTag = lexer.ColorTag, }; break; @@ -140,7 +155,7 @@ namespace UnityExplorer.UI.CSharpConsole if (!anyMatch) { - LookaheadIndex = LastCommittedIndex + 1; + CurrentIndex = CommittedIndex + 1; Commit(); } } @@ -150,23 +165,23 @@ namespace UnityExplorer.UI.CSharpConsole public char PeekNext(int amount = 1) { - LookaheadIndex += amount; + CurrentIndex += amount; return Current; } public void Commit() { - LastCommittedIndex = Math.Min(currentEndIdx, LookaheadIndex); + CommittedIndex = Math.Min(currentEndIdx, CurrentIndex); } public void Rollback() { - LookaheadIndex = LastCommittedIndex + 1; + CurrentIndex = CommittedIndex + 1; } public void RollbackBy(int amount) { - LookaheadIndex = Math.Max(LastCommittedIndex + 1, LookaheadIndex - amount); + CurrentIndex = Math.Max(CommittedIndex + 1, CurrentIndex - amount); } public bool IsDelimiter(char character, bool orWhitespace = false, bool orLetterOrDigit = false) @@ -185,8 +200,139 @@ namespace UnityExplorer.UI.CSharpConsole PeekNext(); } - // revert the last PeekNext which would have returned false - Rollback(); + if (!char.IsWhiteSpace(Current)) + Rollback(); } + + #region Auto Indenting + + // Using the Lexer for indenting as it already has what we need to tokenize strings and comments. + // At the moment this only handles when a single newline or close-delimiter is composed. + // Does not handle copy+paste or any other characters yet. + + public string IndentCharacter(string input, ref int caretIndex) + { + int lastCharIndex = caretIndex - 1; + char c = input[lastCharIndex]; + + // we only want to indent for new lines and close indents + if (!IsNewLine(c) && !IndentCloseChars.Contains(c)) + return input; + + // perform a light parse up to the caret to determine indent level + currentInput = input; + currentStartIdx = 0; + currentEndIdx = lastCharIndex; + CommittedIndex = -1; + Rollback(); + + int indent = 0; + + while (!EndOfInput) + { + if (CurrentIndex >= lastCharIndex) + { + // reached the caret index + if (indent <= 0) + break; + + if (IsNewLine(c)) + input = IndentNewLine(input, indent, ref caretIndex); + else // closing indent + input = IndentCloseDelimiter(input, indent, lastCharIndex, ref caretIndex); + + break; + } + + // Try match strings and comments (Lexer will commit to the end of the match) + if (stringLexer.TryMatchCurrent(this) || commentLexer.TryMatchCurrent(this)) + { + PeekNext(); + continue; + } + + // Still parsing, check indent + + if (IndentOpenChars.Contains(Current)) + indent++; + else if (IndentCloseChars.Contains(Current)) + indent--; + + Commit(); + PeekNext(); + } + + return input; + } + + private string IndentNewLine(string input, int indent, ref int caretIndex) + { + // continue until the end of line or next non-whitespace character. + // if there's a close-indent on this line, reduce the indent level. + while (CurrentIndex < input.Length - 1) + { + CurrentIndex++; + char next = input[CurrentIndex]; + if (IsNewLine(next)) + break; + if (char.IsWhiteSpace(next)) + continue; + else if (IndentCloseChars.Contains(next)) + indent--; + + break; + } + + if (indent > 0) + { + input = input.Insert(caretIndex, new string('\t', indent)); + caretIndex += indent; + } + + return input; + } + + private string IndentCloseDelimiter(string input, int indent, int lastCharIndex, ref int caretIndex) + { + if (CurrentIndex > lastCharIndex) + { + return input; + } + + // lower the indent level by one as we would not have accounted for this closing symbol + indent--; + + while (CurrentIndex > 0) + { + CurrentIndex--; + char prev = input[CurrentIndex]; + if (IsNewLine(prev)) + break; + if (!char.IsWhiteSpace(prev)) + { + // the line containing the closing bracket has non-whitespace characters before it. do not indent. + indent = 0; + break; + } + else if (prev == '\t') + indent--; + } + + if (indent > 0) + { + input = input.Insert(caretIndex, new string('\t', indent)); + caretIndex += indent; + } + else if (indent < 0) + { + // line is overly indented + input = input.Remove(lastCharIndex - 1, -indent); + caretIndex += indent; + } + + return input; + } + + #endregion } } diff --git a/src/UI/CSConsole/Lexers/CommentLexer.cs b/src/UI/CSConsole/Lexers/CommentLexer.cs index bd903e7..e4c541e 100644 --- a/src/UI/CSConsole/Lexers/CommentLexer.cs +++ b/src/UI/CSConsole/Lexers/CommentLexer.cs @@ -2,7 +2,7 @@ using System.Linq; using UnityEngine; -namespace UnityExplorer.UI.CSharpConsole.Lexers +namespace UnityExplorer.UI.CSConsole.Lexers { public class CommentLexer : Lexer { diff --git a/src/UI/CSConsole/Lexers/KeywordLexer.cs b/src/UI/CSConsole/Lexers/KeywordLexer.cs index 1de9fd8..376c793 100644 --- a/src/UI/CSConsole/Lexers/KeywordLexer.cs +++ b/src/UI/CSConsole/Lexers/KeywordLexer.cs @@ -2,14 +2,14 @@ using System.Text; using UnityEngine; -namespace UnityExplorer.UI.CSharpConsole.Lexers +namespace UnityExplorer.UI.CSConsole.Lexers { public class KeywordLexer : Lexer { // system blue protected override Color HighlightColor => new Color(0.33f, 0.61f, 0.83f, 1.0f); - private readonly HashSet keywords = new HashSet + public static readonly HashSet keywords = new HashSet { // reserved keywords "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", diff --git a/src/UI/CSConsole/Lexers/Lexer.cs b/src/UI/CSConsole/Lexers/Lexer.cs index c4f5c06..a7adf58 100644 --- a/src/UI/CSConsole/Lexers/Lexer.cs +++ b/src/UI/CSConsole/Lexers/Lexer.cs @@ -2,7 +2,7 @@ using UnityEngine; using System.Linq; -namespace UnityExplorer.UI.CSharpConsole.Lexers +namespace UnityExplorer.UI.CSConsole.Lexers { public abstract class Lexer { diff --git a/src/UI/CSConsole/Lexers/NumberLexer.cs b/src/UI/CSConsole/Lexers/NumberLexer.cs index 542ff30..98c0c17 100644 --- a/src/UI/CSConsole/Lexers/NumberLexer.cs +++ b/src/UI/CSConsole/Lexers/NumberLexer.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace UnityExplorer.UI.CSharpConsole.Lexers +namespace UnityExplorer.UI.CSConsole.Lexers { public class NumberLexer : Lexer { diff --git a/src/UI/CSConsole/Lexers/StringLexer.cs b/src/UI/CSConsole/Lexers/StringLexer.cs index 403f1af..e7c5e02 100644 --- a/src/UI/CSConsole/Lexers/StringLexer.cs +++ b/src/UI/CSConsole/Lexers/StringLexer.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using UnityEngine; -namespace UnityExplorer.UI.CSharpConsole.Lexers +namespace UnityExplorer.UI.CSConsole.Lexers { public class StringLexer : Lexer { diff --git a/src/UI/CSConsole/Lexers/SymbolLexer.cs b/src/UI/CSConsole/Lexers/SymbolLexer.cs index 95c83f3..2562d0a 100644 --- a/src/UI/CSConsole/Lexers/SymbolLexer.cs +++ b/src/UI/CSConsole/Lexers/SymbolLexer.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Text; using UnityEngine; -namespace UnityExplorer.UI.CSharpConsole.Lexers +namespace UnityExplorer.UI.CSConsole.Lexers { public class SymbolLexer : Lexer { diff --git a/src/UI/CSConsole/ScriptEvaluator.cs b/src/UI/CSConsole/ScriptEvaluator.cs index dc4f376..f5f6b51 100644 --- a/src/UI/CSConsole/ScriptEvaluator.cs +++ b/src/UI/CSConsole/ScriptEvaluator.cs @@ -2,11 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Text; using Mono.CSharp; // Thanks to ManlyMarco for this -namespace UnityExplorer.Core.CSharp +namespace UnityExplorer.UI.CSConsole { public class ScriptEvaluator : Evaluator, IDisposable { diff --git a/src/UI/CSConsole/ScriptInteraction.cs b/src/UI/CSConsole/ScriptInteraction.cs index 6765a43..5abb977 100644 --- a/src/UI/CSConsole/ScriptInteraction.cs +++ b/src/UI/CSConsole/ScriptInteraction.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using UnityExplorer.Core.Runtime; -namespace UnityExplorer.UI.CSharpConsole +namespace UnityExplorer.UI.CSConsole { public class ScriptInteraction : InteractiveBase { @@ -22,17 +22,17 @@ namespace UnityExplorer.UI.CSharpConsole public static void AddUsing(string directive) { - CSConsole.AddUsing(directive); + ConsoleController.AddUsing(directive); } public static void GetUsing() { - ExplorerCore.Log(CSConsole.Evaluator.GetUsing()); + ExplorerCore.Log(ConsoleController.Evaluator.GetUsing()); } public static void Reset() { - CSConsole.ResetConsole(); + ConsoleController.ResetConsole(); } public static object CurrentTarget() diff --git a/src/UI/CacheObject/CacheMember.cs b/src/UI/CacheObject/CacheMember.cs index da4e8a1..600a7ba 100644 --- a/src/UI/CacheObject/CacheMember.cs +++ b/src/UI/CacheObject/CacheMember.cs @@ -338,9 +338,9 @@ namespace UnityExplorer.UI.CacheObject // Blacklists private static readonly HashSet bl_typeAndMember = new HashSet { - // these cause a crash in IL2CPP + // these can cause a crash in IL2CPP #if CPP - //"Type.DeclaringMethod", + "Type.DeclaringMethod", "Rigidbody2D.Cast", "Collider2D.Cast", "Collider2D.Raycast", @@ -363,6 +363,7 @@ namespace UnityExplorer.UI.CacheObject "Component.renderer", "Component.rigidbody", "Component.rigidbody2D", + "Light.flare", }; private static readonly HashSet bl_methodNameStartsWith = new HashSet { diff --git a/src/UI/Panels/CSConsolePanel.cs b/src/UI/Panels/CSConsolePanel.cs index 667cc24..f57bbab 100644 --- a/src/UI/Panels/CSConsolePanel.cs +++ b/src/UI/Panels/CSConsolePanel.cs @@ -6,7 +6,7 @@ using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using UnityExplorer.Core.Config; -using UnityExplorer.UI.CSharpConsole; +using UnityExplorer.UI.CSConsole; using UnityExplorer.UI.Utility; namespace UnityExplorer.UI.Panels @@ -44,7 +44,7 @@ namespace UnityExplorer.UI.Panels { base.Update(); - CSConsole.Update(); + ConsoleController.Update(); } // Saving @@ -111,9 +111,9 @@ namespace UnityExplorer.UI.Panels int fontSize = 16; - var inputObj = UIFactory.CreateSrollInputField(this.content, "ConsoleInput", CSConsole.STARTUP_TEXT, out var inputScroller, fontSize); + var inputObj = UIFactory.CreateSrollInputField(this.content, "ConsoleInput", ConsoleController.STARTUP_TEXT, out var inputScroller, fontSize); InputScroll = inputScroller; - CSConsole.defaultInputFieldAlpha = Input.Component.selectionColor.a; + ConsoleController.defaultInputFieldAlpha = Input.Component.selectionColor.a; Input.OnValueChanged += InvokeOnValueChanged; InputText = Input.Component.textComponent; diff --git a/src/UI/UIManager.cs b/src/UI/UIManager.cs index 18b834d..a27695b 100644 --- a/src/UI/UIManager.cs +++ b/src/UI/UIManager.cs @@ -8,7 +8,7 @@ using UnityEngine.EventSystems; using UnityEngine.UI; using UnityExplorer.Core.Config; using UnityExplorer.Core.Input; -using UnityExplorer.UI.CSharpConsole; +using UnityExplorer.UI.CSConsole; using UnityExplorer.UI.Models; using UnityExplorer.UI.Panels; using UnityExplorer.UI.Utility; @@ -178,7 +178,7 @@ namespace UnityExplorer.UI CSharpConsole = new CSConsolePanel(); CSharpConsole.ConstructUI(); - CSConsole.Init(); + ConsoleController.Init(); ShowMenu = !ConfigManager.Hide_On_Startup.Value; diff --git a/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs b/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs index 2709fb6..7c3e0ec 100644 --- a/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs +++ b/src/UI/Widgets/AutoComplete/AutoCompleteModal.cs @@ -12,14 +12,14 @@ using UnityExplorer.UI.Panels; namespace UnityExplorer.UI.Widgets.AutoComplete { + // Shared modal panel for "AutoComplete" suggestions. + // A data source implements ISuggestionProvider and uses TakeOwnership and ReleaseOwnership + // for control, and SetSuggestions to set the actual suggestion data. + public class AutoCompleteModal : UIPanel { - // Static - public static AutoCompleteModal Instance => UIManager.AutoCompleter; - // Instance - public override string Name => "AutoCompleter"; public override UIManager.Panels PanelType => UIManager.Panels.AutoCompleter; public override int MinWidth => -1; @@ -154,14 +154,14 @@ namespace UnityExplorer.UI.Widgets.AutoComplete if (CurrentHandler.AnchorToCaretPosition) { - var textGen = input.Component.textComponent.cachedTextGeneratorForLayout; + var textGen = input.Component.cachedInputTextGenerator; int caretIdx = Math.Max(0, Math.Min(textGen.characterCount - 1, input.Component.caretPosition)); // normalize the caret horizontal position Vector3 caretPos = textGen.characters[caretIdx].cursorPos; - caretPos += new Vector3(input.Rect.rect.width * 0.5f, 0, 0); // transform to world point caretPos = input.UIRoot.transform.TransformPoint(caretPos); + caretPos += new Vector3(input.Rect.rect.width * 0.5f, -(input.Rect.rect.height * 0.5f), 0); uiRoot.transform.position = new Vector3(caretPos.x + 10, caretPos.y - 30, 0); } diff --git a/src/UnityExplorer.csproj b/src/UnityExplorer.csproj index b0c181a..b1fc840 100644 --- a/src/UnityExplorer.csproj +++ b/src/UnityExplorer.csproj @@ -216,7 +216,6 @@ - @@ -234,7 +233,7 @@ - +