From 5f0e246b25e9d62b89f65cb2d651e0af4738f679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Pen=CC=83alba?= Date: Tue, 16 Dec 2025 11:06:32 +0100 Subject: [PATCH] Implement IEquatable for easier API usage --- .../Grammar/LineTextTests.cs | 339 ++++++++++++++++++ src/TextMateSharp/Grammar/LineText.cs | 57 ++- 2 files changed, 395 insertions(+), 1 deletion(-) diff --git a/src/TextMateSharp.Tests/Grammar/LineTextTests.cs b/src/TextMateSharp.Tests/Grammar/LineTextTests.cs index 23f2f0e..6f30529 100644 --- a/src/TextMateSharp.Tests/Grammar/LineTextTests.cs +++ b/src/TextMateSharp.Tests/Grammar/LineTextTests.cs @@ -149,5 +149,344 @@ public void CharArrayMemory_ShouldWorkWithLineText() Assert.AreEqual(5, lineText.Length); Assert.AreEqual("abcde", lineText.ToString()); } + + #region Equals Tests + + [Test] + public void Equals_SameContent_ShouldBeEqual() + { + LineText lineText1 = "hello world"; + LineText lineText2 = "hello world"; + + Assert.IsTrue(lineText1.Equals(lineText2)); + Assert.IsTrue(lineText2.Equals(lineText1)); + } + + [Test] + public void Equals_DifferentContent_ShouldNotBeEqual() + { + LineText lineText1 = "hello"; + LineText lineText2 = "world"; + + Assert.IsFalse(lineText1.Equals(lineText2)); + Assert.IsFalse(lineText2.Equals(lineText1)); + } + + [Test] + public void Equals_DifferentLengths_ShouldNotBeEqual() + { + LineText lineText1 = "hello"; + LineText lineText2 = "hello world"; + + Assert.IsFalse(lineText1.Equals(lineText2)); + Assert.IsFalse(lineText2.Equals(lineText1)); + } + + [Test] + public void Equals_BothEmpty_ShouldBeEqual() + { + LineText lineText1 = ""; + LineText lineText2 = ""; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_BothDefault_ShouldBeEqual() + { + LineText lineText1 = default; + LineText lineText2 = default; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_EmptyAndDefault_ShouldBeEqual() + { + LineText lineText1 = ""; + LineText lineText2 = default; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_SameMemoryReference_ShouldBeEqual() + { + char[] buffer = "hello world".ToCharArray(); + ReadOnlyMemory memory = buffer.AsMemory(); + + LineText lineText1 = memory; + LineText lineText2 = memory; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_SameArraySameOffset_ShouldUseReferenceEquality() + { + char[] buffer = "hello world".ToCharArray(); + ReadOnlyMemory slice1 = buffer.AsMemory().Slice(0, 5); + ReadOnlyMemory slice2 = buffer.AsMemory().Slice(0, 5); + + LineText lineText1 = slice1; + LineText lineText2 = slice2; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_SameArrayDifferentOffsetsSameContent_ShouldBeEqual() + { + // Create buffer with repeated content + char[] buffer = "hellohello".ToCharArray(); + ReadOnlyMemory slice1 = buffer.AsMemory().Slice(0, 5); // "hello" + ReadOnlyMemory slice2 = buffer.AsMemory().Slice(5, 5); // "hello" + + LineText lineText1 = slice1; + LineText lineText2 = slice2; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_DifferentArraysSameContent_ShouldBeEqual() + { + string buffer1 = "hello"; + string buffer2 = "hello"; + + LineText lineText1 = buffer1.AsMemory(); + LineText lineText2 = buffer2.AsMemory(); + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_ObjectOverload_WithLineText_ShouldWork() + { + LineText lineText1 = "hello"; + object lineText2 = (LineText)"hello"; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_ObjectOverload_WithNull_ShouldReturnFalse() + { + LineText lineText = "hello"; + + Assert.IsFalse(lineText.Equals(null)); + } + + [Test] + public void Equals_ObjectOverload_WithDifferentType_ShouldReturnFalse() + { + LineText lineText = "hello"; + Assert.IsFalse(lineText.Equals(42)); + } + + [Test] + public void OperatorEquals_SameContent_ShouldReturnTrue() + { + LineText lineText1 = "hello"; + LineText lineText2 = "hello"; + + Assert.IsTrue(lineText1 == lineText2); + } + + [Test] + public void OperatorEquals_DifferentContent_ShouldReturnFalse() + { + LineText lineText1 = "hello"; + LineText lineText2 = "world"; + + Assert.IsFalse(lineText1 == lineText2); + } + + [Test] + public void OperatorNotEquals_SameContent_ShouldReturnFalse() + { + LineText lineText1 = "hello"; + LineText lineText2 = "hello"; + + Assert.IsFalse(lineText1 != lineText2); + } + + [Test] + public void OperatorNotEquals_DifferentContent_ShouldReturnTrue() + { + LineText lineText1 = "hello"; + LineText lineText2 = "world"; + + Assert.IsTrue(lineText1 != lineText2); + } + + [Test] + public void Equals_UnicodeContent_ShouldWork() + { + LineText lineText1 = "안녕하세요"; + LineText lineText2 = "안녕하세요"; + + Assert.IsTrue(lineText1.Equals(lineText2)); + } + + [Test] + public void Equals_CaseSensitive_ShouldNotBeEqual() + { + LineText lineText1 = "Hello"; + LineText lineText2 = "hello"; + + Assert.IsFalse(lineText1.Equals(lineText2)); + } + + #endregion + + #region GetHashCode Tests + + [Test] + public void GetHashCode_SameContent_ShouldReturnSameHash() + { + LineText lineText1 = "hello world"; + LineText lineText2 = "hello world"; + + Assert.AreEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + } + + [Test] + public void GetHashCode_DifferentContent_ShouldReturnDifferentHash() + { + LineText lineText1 = "hello"; + LineText lineText2 = "world"; + + Assert.AreNotEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + } + + [Test] + public void GetHashCode_EmptyLineText_ShouldReturnZero() + { + LineText lineText = ""; + + Assert.AreEqual(0, lineText.GetHashCode()); + } + + [Test] + public void GetHashCode_DefaultLineText_ShouldReturnZero() + { + LineText lineText = default; + + Assert.AreEqual(0, lineText.GetHashCode()); + } + + [Test] + public void GetHashCode_SameInstance_ShouldBeConsistent() + { + LineText lineText = "hello world"; + + int hash1 = lineText.GetHashCode(); + int hash2 = lineText.GetHashCode(); + int hash3 = lineText.GetHashCode(); + + Assert.AreEqual(hash1, hash2); + Assert.AreEqual(hash2, hash3); + } + + [Test] + public void GetHashCode_DifferentArraysSameContent_ShouldReturnSameHash() + { + string buffer1 = "hello"; + string buffer2 = "hello"; + + LineText lineText1 = buffer1.AsMemory(); + LineText lineText2 = buffer2.AsMemory(); + + Assert.AreEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + } + + [Test] + public void GetHashCode_SlicedMemorySameContent_ShouldReturnSameHash() + { + char[] buffer = "hello world".ToCharArray(); + ReadOnlyMemory slice = buffer.AsMemory().Slice(6, 5); // "world" + + LineText lineText1 = slice; + LineText lineText2 = "world"; + + Assert.AreEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + } + + [Test] + public void GetHashCode_UnicodeContent_ShouldWork() + { + LineText lineText1 = "안녕하세요"; + LineText lineText2 = "안녕하세요"; + + Assert.AreEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + } + + [Test] + public void GetHashCode_SimilarStrings_ShouldProduceDifferentHashes() + { + // These are similar but should have different hashes + LineText lineText1 = "abc"; + LineText lineText2 = "abd"; + LineText lineText3 = "bbc"; + + Assert.AreNotEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + Assert.AreNotEqual(lineText1.GetHashCode(), lineText3.GetHashCode()); + Assert.AreNotEqual(lineText2.GetHashCode(), lineText3.GetHashCode()); + } + + [Test] + public void GetHashCode_SingleCharacter_ShouldWork() + { + LineText lineText1 = "a"; + LineText lineText2 = "a"; + LineText lineText3 = "b"; + + Assert.AreEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + Assert.AreNotEqual(lineText1.GetHashCode(), lineText3.GetHashCode()); + } + + #endregion + + #region HashCode and Equals Contract Tests + + [Test] + public void HashCodeEqualsContract_EqualObjects_ShouldHaveSameHashCode() + { + // If two objects are equal, they must have the same hash code + LineText lineText1 = "test string"; + LineText lineText2 = "test string"; + + Assert.IsTrue(lineText1.Equals(lineText2)); + Assert.AreEqual(lineText1.GetHashCode(), lineText2.GetHashCode()); + } + + [Test] + public void HashCodeEqualsContract_WorksWithDictionary() + { + var dictionary = new System.Collections.Generic.Dictionary(); + + LineText key1 = "hello"; + dictionary[key1] = 42; + + LineText key2 = "hello"; // Different instance, same content + Assert.IsTrue(dictionary.ContainsKey(key2)); + Assert.AreEqual(42, dictionary[key2]); + } + + [Test] + public void HashCodeEqualsContract_WorksWithHashSet() + { + var hashSet = new System.Collections.Generic.HashSet(); + + LineText item1 = "hello"; + hashSet.Add(item1); + + LineText item2 = "hello"; // Different instance, same content + Assert.IsTrue(hashSet.Contains(item2)); + Assert.IsFalse(hashSet.Add(item2)); // Should return false as it already exists + } + + #endregion } } diff --git a/src/TextMateSharp/Grammar/LineText.cs b/src/TextMateSharp/Grammar/LineText.cs index e78c1c0..2cb83d6 100644 --- a/src/TextMateSharp/Grammar/LineText.cs +++ b/src/TextMateSharp/Grammar/LineText.cs @@ -1,8 +1,9 @@ using System; +using System.Runtime.InteropServices; namespace TextMateSharp.Grammars { - public readonly struct LineText + public readonly struct LineText : IEquatable { private readonly ReadOnlyMemory _memory; @@ -28,6 +29,60 @@ public LineText(string text) public static implicit operator ReadOnlyMemory(LineText lineText) => lineText._memory; + public bool Equals(LineText other) + { + // Fast path: check length first + if (_memory.Length != other._memory.Length) + return false; + + // Empty memories are equal + if (_memory.Length == 0) + return true; + + // Try to check if they reference the same memory region + if (MemoryMarshal.TryGetArray(_memory, out ArraySegment thisSegment) && + MemoryMarshal.TryGetArray(other._memory, out ArraySegment otherSegment)) + { + // If same array and same offset, they're definitely equal (length already checked) + if (ReferenceEquals(thisSegment.Array, otherSegment.Array) && + thisSegment.Offset == otherSegment.Offset) + { + return true; + } + } + + // Fall back to content comparison + return _memory.Span.SequenceEqual(other._memory.Span); + } + + public override bool Equals(object obj) + { + return obj is LineText other && Equals(other); + } + + public override int GetHashCode() + { + ReadOnlySpan span = _memory.Span; + + if (span.IsEmpty) + return 0; + + // DJB2 hash algorithm - fast and good distribution, no allocations + unchecked + { + int hash = 5381; + for (int i = 0; i < span.Length; i++) + { + hash = ((hash << 5) + hash) ^ span[i]; + } + return hash; + } + } + + public static bool operator ==(LineText left, LineText right) => left.Equals(right); + + public static bool operator !=(LineText left, LineText right) => !left.Equals(right); + public override string ToString() => _memory.Span.ToString(); } }