using System; using Amphora; using Glass.Editing; using Glass.Typesetting; using UnitTestSharp; namespace Glass.UnitTests.Editing { public class PagedTextTests : TestFixture { // Pages this small split and join after a handful of chars, so a short text takes every path. public const int Tiny = 8; public static readonly string Pair = char.ConvertFromUtf32(0x20BB7); // Every kind of break, in lines of different lengths. public static readonly string Breaks = "one\ntwo\r\nthree\rfour" + (char)0x85 + "five" + (char)0x0B + "six" + (char)0x0C + "seven" + (char)0x2028 + "eight" + (char)0x2029 + "nine\n\n\r\n\ntail"; public static PagedText Paged(string text, int pageSize = Tiny) { var returnMe = new PagedText(text, pageSize); returnMe.Validate(); return returnMe; } /// /// Replaces a range, then checks the text is what the same replacement makes of a string and the pages are /// sound. /// public static string Replaced(PagedText paged, string expectedBefore, int start, int count, string replacement) { CheckEqualStatic(expectedBefore, paged.ToString()); paged.Replace(start, count, replacement); paged.Validate(); return expectedBefore.Substring(0, start) + replacement + expectedBefore.Substring(start + count); } private static void CheckEqualStatic(string expected, string actual) { if (expected != actual) { throw new InvalidOperationException($"Expected \"{expected}\" but was \"{actual}\"."); } } public class CtorTests : TestFixture { public void DefaultsToAPageSizeOfFourThousandAndNinetySix() { CheckEqual(4096, new PagedText().PageSize); } public void StoresThePageSize() { CheckEqual(32, new PagedText(32).PageSize); } public void PageSizeBelowTheMinimum_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new PagedText(PagedText.MinimumPageSize - 1); } public void StartsEmptyInOnePage() { PagedText paged = Paged(""); CheckEqual(0, paged.Length); CheckEqual(1, paged.HardLineCount); CheckEqual(1, paged.PageCount); } public void HoldsTheInitialText() { CheckEqual("hello, world", Paged("hello, world").ToString()); } public void CutsTheInitialTextIntoPages() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); CheckEqual(26, paged.Length); Check(paged.PageCount > 1); } public void CutsALargeInitialTextIntoNearlyFullPages() { PagedText paged = Paged(new string('x', 100000), PagedText.DefaultPageSize); CheckEqual(25, paged.PageCount); } } public class IndexerTests : TestFixture { public void ReadsEachChar() { const string text = "abcdefghijklmnopqrstuvwxyz"; PagedText paged = Paged(text); for (int i = 0; i < text.Length; ++i) { CheckEqual(text[i], paged[i]); } } public void NegativeIndex_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); _ = Paged("abc")[-1]; } public void IndexAtTheEnd_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); _ = Paged("abc")[3]; } } public class CopyToTests : TestFixture { public void CopiesAcrossPages() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); var destination = new char[10]; paged.CopyTo(5, 10, destination, 0); CheckEqual("fghijklmno", new string(destination)); } public void CopiesIntoTheMiddleOfTheDestination() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); var destination = new char[] { '-', '-', '-', '-', '-' }; paged.CopyTo(2, 3, destination, 1); CheckEqual("-cde-", new string(destination)); } public void CopyingNothing_ChangesNothing() { var destination = new char[] { 'x' }; Paged("abc").CopyTo(1, 0, destination, 0); CheckEqual('x', destination[0]); } public void ARangePastTheEnd_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Paged("abc").CopyTo(2, 2, new char[4], 0); } } public class SliceTests : TestFixture { public void ViewsACopyOfTheRange() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); char[] scratch = new char[0]; StringView view = paged.Slice(3, 12, ref scratch); CheckEqual("defghijklmno", view.ToString()); } public void MakesAScratchThatIsNull() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); char[] scratch = null; StringView view = paged.Slice(3, 4, ref scratch); CheckEqual("defg", view.ToString()); CheckEqual(4, scratch.Length); } public void GrowsAScratchThatIsTooSmall() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); char[] scratch = new char[2]; paged.Slice(0, 20, ref scratch); Check(scratch.Length >= 20); } public void LeavesALargeEnoughScratchAlone() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); char[] scratch = new char[64]; paged.Slice(0, 20, ref scratch); CheckEqual(64, scratch.Length); } public void TheViewSurvivesALaterEditOfTheText() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); char[] scratch = new char[0]; StringView view = paged.Slice(0, 5, ref scratch); paged.Replace(0, 5, "XXXXX"); CheckEqual("abcde", view.ToString()); } } public class VersionTests : TestFixture { public void ChangesWithEachReplace() { PagedText paged = Paged("abc"); int version = paged.Version; paged.Replace(1, 1, "X"); Check(version != paged.Version); } public void ReplacingNothingWithNothing_KeepsIt() { PagedText paged = Paged("abc"); int version = paged.Version; paged.Replace(1, 0, ""); CheckEqual(version, paged.Version); } public void ReadingKeepsIt() { PagedText paged = Paged("abc"); int version = paged.Version; char[] scratch = null; paged.Slice(0, 2, ref scratch); paged.ToString(); CheckEqual(version, paged.Version); } } public class SubstringTests : TestFixture { public void CopiesTheRange() { CheckEqual("jklm", Paged("abcdefghijklmnopqrstuvwxyz").Substring(9, 4)); } public void AnEmptyRange_IsAnEmptyString() { CheckEqual("", Paged("abc").Substring(1, 0)); } } public class ToStringTests : TestFixture { public void IsTheWholeText() { CheckEqual(Breaks, Paged(Breaks).ToString()); } public void IsTheWholeTextOfALargeOne() { string text = new string('x', 100000) + "end"; CheckEqual(text, Paged(text, pageSize: 1024).ToString()); } } public class ReplaceTests : TestFixture { public void InsertingInsideAPage_KeepsThePages() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 2, 0, "X"); CheckEqual(expected, paged.ToString()); CheckEqual(pages, paged.PageCount); } public void SplittingAndJoiningPages_AllocatesNothingOnceWarm() { PagedText paged = Paged(new string('x', 4000), pageSize: 64); string inserted = new string('y', 40); int middle = 2000; int pages = paged.PageCount; paged.Replace(middle, 0, inserted); Check(paged.PageCount > pages); paged.Replace(middle, inserted.Length, ""); CheckEqual(pages, paged.PageCount); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; ++i) { paged.Replace(middle, 0, inserted); paged.Replace(middle, inserted.Length, ""); } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; CheckEqual(0L, allocated); paged.Validate(); } public void InsertingPastThePageSize_SplitsThePage() { PagedText paged = Paged("abcdef"); int pages = paged.PageCount; string expected = Replaced(paged, "abcdef", 3, 0, "123456789"); CheckEqual(expected, paged.ToString()); Check(paged.PageCount > pages); } public void InsertingSeveralPagesWorth_MakesSeveralPages() { PagedText paged = Paged("ab"); string expected = Replaced(paged, "ab", 1, 0, new string('x', 100)); CheckEqual(expected, paged.ToString()); Check(paged.PageCount >= 100 / Tiny); } public void InsertingAPageOrMore_CutsThePagesNearlyFull() { PagedText paged = Paged(new string('x', 100), pageSize: 128); Replaced(paged, new string('x', 100), 50, 0, new string('y', 128)); CheckEqual(2, paged.PageCount); } public void InsertingLessThanAPage_LeavesRoomInThePagesItCuts() { PagedText paged = Paged(new string('x', 100), pageSize: 128); Replaced(paged, new string('x', 100), 50, 0, new string('y', 100)); CheckEqual(3, paged.PageCount); } public void TypingInALoadedPage_SplitsItOnceAndThenKeepsThePages() { string text = new string('x', 180); PagedText paged = Paged(text, pageSize: 64); int pages = paged.PageCount; text = Replaced(paged, text, 30, 0, "12345"); CheckEqual(pages + 1, paged.PageCount); Replaced(paged, text, 31, 0, "6"); CheckEqual(pages + 1, paged.PageCount); } public void DeletingInsideAPage_KeepsThePages() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 1, 1, ""); CheckEqual(expected, paged.ToString()); CheckEqual(pages, paged.PageCount); } public void DeletingAcrossPages_JoinsWhatIsLeft() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 2, 20, ""); CheckEqual("abwxyz", expected); CheckEqual(expected, paged.ToString()); Check(paged.PageCount < pages); } public void DeletingEverything_LeavesOneEmptyPage() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 0, 26, ""); CheckEqual(0, paged.Length); CheckEqual(1, paged.PageCount); } public void ReplacingWithTextOfTheSameLength_KeepsThePages() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 4, 10, "0123456789"); CheckEqual(pages, paged.PageCount); } public void ReplacingARangeWithLongerText_Works() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 4, 3, "0123456789012345678901234567"); CheckEqual(expected, paged.ToString()); } public void AppendingAtTheEnd_Works() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 26, 0, "!!!"); CheckEqual(expected, paged.ToString()); } public void InsertingAtTheStart_Works() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 0, 0, "!!!"); CheckEqual(expected, paged.ToString()); } public void InsertingOnAPageBoundary_Works() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int boundary = paged.PageLength(0); string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", boundary, 0, "!"); CheckEqual(expected, paged.ToString()); } public void NothingForNothing_ChangesNothing() { PagedText paged = Paged("abc"); paged.Replace(1, 0, ""); CheckEqual("abc", paged.ToString()); } public void ARangePastTheEnd_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Paged("abc").Replace(2, 5, "x"); } public void ANegativeStart_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Paged("abc").Replace(-1, 0, "x"); } public void TypingOneCharAtATime_NeverLeavesAPageOverSize() { PagedText paged = Paged(""); string expected = ""; for (int i = 0; i < 100; ++i) { expected = Replaced(paged, expected, expected.Length / 2, 0, ((char)('a' + i % 26)).ToString()); } CheckEqual(expected, paged.ToString()); } public void DeletingOneCharAtATime_KeepsNoTinyPagesNextToEachOther() { string expected = new string('x', 100); PagedText paged = Paged(expected); while (expected.Length > 12) { expected = Replaced(paged, expected, expected.Length / 3, 1, ""); } CheckEqual(expected, paged.ToString()); Check(paged.PageCount <= 4); } public void TypingAndDeletingAtAPageBoundary_DoesNotRebuildForever() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); string expected = "abcdefghijklmnopqrstuvwxyz"; for (int i = 0; i < 20; ++i) { expected = Replaced(paged, expected, 8, 0, "X"); expected = Replaced(paged, expected, 8, 1, ""); } CheckEqual("abcdefghijklmnopqrstuvwxyz", paged.ToString()); } public void DeletingMostOfTheFirstPage_JoinsItToTheNextOne() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; string expected = Replaced(paged, "abcdefghijklmnopqrstuvwxyz", 0, paged.PageLength(0) - 1, ""); CheckEqual(expected, paged.ToString()); CheckEqual(pages - 1, paged.PageCount); } public void ACrInsertedBeforeAnLfThatStartsAPage_JoinsThePair() { int found = 0; for (int before = 1; before < 40; ++before) { for (int after = 1; after < 20; ++after) { string text = new string('a', before) + "\n" + new string('b', after); PagedText paged = Paged(text); int boundary = 0; for (int page = 0; page + 1 < paged.PageCount; ++page) { boundary += paged.PageLength(page); if (paged.PageText(page + 1)[0] == '\n') { ++found; string expected = Replaced(paged, text, boundary, 0, "\r"); CheckEqual(expected, paged.ToString()); CheckEqual(2, paged.HardLineCount); } } } } Check(found > 0); } public void ADeleteThatJoinsACrToAnLf_KeepsThemOnOnePage() { string text = "aaaaaaa\rxyzxyzxyzxyzxyz\nbbbbbbb"; PagedText paged = Paged(text); string expected = Replaced(paged, text, 8, 15, ""); CheckEqual("aaaaaaa\r\nbbbbbbb", expected); CheckEqual(expected, paged.ToString()); CheckEqual(2, paged.HardLineCount); } public void AnInsertOfACrBeforeAnLf_MakesOneBreak() { string text = "aaaaaaaa\nbbbbbbbb"; PagedText paged = Paged(text); string expected = Replaced(paged, text, 8, 0, "\r"); CheckEqual("aaaaaaaa\r\nbbbbbbbb", expected); CheckEqual(2, paged.HardLineCount); } public void CuttingPages_KeepsAMarkOnThePageOfItsLetter() { PagedText paged = Paged(Repeat(ArabicBaWithFatha, 200), pageSize: 64); for (int page = 1; page < paged.PageCount; ++page) { CheckNotEqual(Fatha[0], paged.PageText(page)[0]); } paged.Validate(); } public void CuttingPages_StillCutsInsideARunOfMarksTooLongToAvoid() { PagedText paged = Paged("a" + Repeat(Fatha, 500), pageSize: 64); CheckGreater(paged.PageCount, 1); CheckEqual("a" + Repeat(Fatha, 500), paged.ToString()); paged.Validate(); } public void CuttingFullPages_StillCutsInsideARunOfMarksTooLongToAvoid() { string text = "a" + Repeat(Fatha, 20000); PagedText paged = Paged(text, PagedText.DefaultPageSize); CheckEqual(text, paged.ToString()); CheckGreater(paged.PageCount, 4); } public void ADeleteThatJoinsAHighSurrogateToALowOne_KeepsThemOnOnePage() { string text = "aaaaaaa" + Pair.Substring(0, 1) + "xyzxyzxyzxyzxyz" + Pair.Substring(1, 1) + "bbbbbbb"; PagedText paged = Paged(text); string expected = Replaced(paged, text, 8, 15, ""); CheckEqual("aaaaaaa" + Pair + "bbbbbbb", expected); } public void LoadingTextWithAPairOnAPageBoundary_DoesNotSplitIt() { string text = "aaaaaaa" + Pair + "bbbbbbbbbbbbbbbbbbbb"; PagedText paged = Paged(text); CheckEqual(text, paged.ToString()); } public void LoadingTextWithACrLfOnAPageBoundary_DoesNotSplitIt() { string text = "aaaaa\r\nbbbbbbbbbbbbbbbbbbbbbbbbb\r\nccccc"; PagedText paged = Paged(text); CheckEqual(text, paged.ToString()); CheckEqual(3, paged.HardLineCount); } } public static bool IsPlainPage(PagedText paged, int page) { string text = paged.PageText(page); for (int i = 0; i < text.Length; ++i) { int codePoint = text[i]; bool atStart = i == 0; if (char.IsHighSurrogate(text[i]) && i + 1 < text.Length && char.IsLowSurrogate(text[i + 1])) { codePoint = char.ConvertToUtf32(text[i], text[i + 1]); ++i; } else if (char.IsSurrogate(text[i])) { return false; } switch (UnicodeProperties.Of(codePoint).LineBreakClass()) { case LineBreakClass.AL: case LineBreakClass.HL: case LineBreakClass.NU: break; case LineBreakClass.CM: if (atStart) { return false; } break; default: return false; } } return true; } // Found from the pages' own text: the end of the plain pages from the one holding the index on. public static int ExpectedPlainRunEnd(PagedText paged, int index) { if (index == paged.Length) { return index; } int start = 0; int page = 0; while (start + paged.PageLength(page) <= index) { start += paged.PageLength(page); ++page; } if (!IsPlainPage(paged, page)) { return index; } int end = start; for (; page < paged.PageCount && IsPlainPage(paged, page); ++page) { end += paged.PageLength(page); } return end; } // Found from the pages' own text: the start of the plain pages up to the one holding the char before the index. public static int ExpectedPlainRunStart(PagedText paged, int index) { if (index == 0) { return 0; } int start = 0; int page = 0; while (start + paged.PageLength(page) < index) { start += paged.PageLength(page); ++page; } if (!IsPlainPage(paged, page)) { return index; } for (; page > 0 && IsPlainPage(paged, page - 1); --page) { start -= paged.PageLength(page - 1); } return start; } public static readonly string EAcute = ((char)0xE9).ToString(); public static readonly string Ideograph = ((char)0x4E2D).ToString(); public static readonly string Cyrillic = ((char)0x436).ToString(); public static readonly string Alef = ((char)0x5D0).ToString(); public static readonly string ArabicBaWithFatha = ((char)0x628).ToString() + (char)0x64E; public static readonly string Fatha = ((char)0x64E).ToString(); public static readonly string Deseret = char.ConvertFromUtf32(0x10400); public static string Repeat(string unit, int count) { return new System.Text.StringBuilder().Insert(0, unit, count).ToString(); } public static readonly string[] PlainTexts = { Repeat(Cyrillic, 100) + " " + Repeat(Alef, 100) + "\n" + Repeat(ArabicBaWithFatha, 60) + Repeat(Deseret, 50), Repeat("a" + Fatha, 40) + " " + Fatha + Repeat("b", 30) + Repeat(Deseret, 3) + "-" + Repeat(Deseret, 20), Repeat("a", 7) + Fatha + Repeat("b", 30) + (char)0xD801 + Repeat("c", 30), "", "a", "a b", new string('a', 600), new string('a', 100) + " " + new string('b', 100) + "\n" + new string('1', 120) + EAcute + new string('c', 80) + "_" + new string('d', 90) + "-" + new string('e', 50), new string('Z', 200) + "9" + new string('y', 200) + "/", }; // Every kind of seam in a short text: CR LF, pairs, a mark, and lone halves of a pair. public static readonly string Seams = "aa\r\nbb" + Pair + "cc\rdd\ne" + Fatha + "ff" + Pair + "\r\ngg" + (char)0xD801 + "hh" + (char)0xDC01 + "ii"; public static bool IsBoundary(string text, int index) { if (index == 0 || index == text.Length) { return true; } bool insidePair = char.IsHighSurrogate(text[index - 1]) && char.IsLowSurrogate(text[index]); bool insideCrLf = text[index - 1] == '\r' && text[index] == '\n'; return !insidePair && !insideCrLf; } public class TakeOutTests : TestFixture { public void ReturnsTheRange_AndLeavesTheRest() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(5, 12); CheckEqual("fghijklmnopq", run.ToString()); CheckEqual("abcderstuvwxyz", paged.ToString()); CheckEqual(12, run.Length); paged.Validate(); } public void ARangeOfWholePages_ReturnsThoseExactPages() { const string text = "abcdefghijklmnopqrstuvwxyz"; PagedText paged = Paged(text); int start = paged.PageLength(0); int count = paged.PageLength(1) + paged.PageLength(2); PagedText.PageRun run = paged.TakeOut(start, count); CheckEqual(text.Substring(start, count), run.ToString()); CheckEqual(2, run.Pages.Count); CheckEqual(text.Remove(start, count), paged.ToString()); paged.Validate(); } public void ARangeStartingOnAPageBoundary_CutsOnlyTheLastPage() { const string text = "abcdefghijklmnopqrstuvwxyz"; PagedText paged = Paged(text); int start = paged.PageLength(0); int count = paged.PageLength(1) + 2; PagedText.PageRun run = paged.TakeOut(start, count); CheckEqual(text.Substring(start, count), run.ToString()); CheckEqual(2, run.Pages.Count); CheckEqual(text.Remove(start, count), paged.ToString()); paged.Validate(); } public void ARangeEndingOnAPageBoundary_CutsOnlyTheFirstPage() { const string text = "abcdefghijklmnopqrstuvwxyz"; PagedText paged = Paged(text); int start = 2; int count = paged.PageLength(0) - 2 + paged.PageLength(1); PagedText.PageRun run = paged.TakeOut(start, count); CheckEqual(text.Substring(start, count), run.ToString()); CheckEqual(2, run.Pages.Count); CheckEqual(text.Remove(start, count), paged.ToString()); paged.Validate(); } public void ARangeAcrossOnePageBoundary_CutsBothPages() { const string text = "abcdefghijklmnopqrstuvwxyz"; PagedText paged = Paged(text); int start = paged.PageLength(0) - 2; PagedText.PageRun run = paged.TakeOut(start, 4); CheckEqual(text.Substring(start, 4), run.ToString()); CheckEqual(2, run.Pages.Count); CheckEqual(text.Remove(start, 4), paged.ToString()); paged.Validate(); } public void ACountOfZero_ReturnsAnEmptyRunAndChangesNothing() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int version = paged.Version; PagedText.PageRun run = paged.TakeOut(5, 0); CheckEqual(0, run.Length); CheckEqual(0, run.Pages.Count); CheckEqual(version, paged.Version); CheckEqual("abcdefghijklmnopqrstuvwxyz", paged.ToString()); } public void FromTheStart_LeavesTheEnd() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(0, 20); CheckEqual("abcdefghijklmnopqrst", run.ToString()); CheckEqual("uvwxyz", paged.ToString()); paged.Validate(); } public void ToTheEnd_LeavesTheStart() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(6, 20); CheckEqual("ghijklmnopqrstuvwxyz", run.ToString()); CheckEqual("abcdef", paged.ToString()); paged.Validate(); } public void TheWholeText_LeavesOneEmptyPage() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(0, 26); CheckEqual("abcdefghijklmnopqrstuvwxyz", run.ToString()); CheckEqual(0, paged.Length); CheckEqual(1, paged.PageCount); CheckEqual(1, paged.HardLineCount); paged.Validate(); } public void ARangeInsideOnePage_ReturnsIt() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(1, 2); CheckEqual("bc", run.ToString()); CheckEqual("adefghijklmnopqrstuvwxyz", paged.ToString()); paged.Validate(); } public void CountsTheHardBreaksItTakes() { PagedText paged = Paged(Breaks); int lines = paged.HardLineCount; PagedText.PageRun run = paged.TakeOut(4, 30); CheckEqual(Paged(Breaks.Substring(4, 30)).HardLineCount - 1, run.Breaks); CheckEqual(Paged(Breaks.Remove(4, 30)).HardLineCount, paged.HardLineCount); CheckEqual(lines, paged.HardLineCount + run.Breaks); } public void BumpsTheVersion() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int version = paged.Version; paged.TakeOut(5, 12); CheckNotEqual(version, paged.Version); } public void RemovingTheTextBetweenACrAndAnLf_JoinsThemIntoOnePair() { string text = "aaaaaaa\r" + new string('x', 30) + "\nbbbbbbb"; PagedText paged = Paged(text); paged.TakeOut(8, 30); CheckEqual("aaaaaaa\r\nbbbbbbb", paged.ToString()); CheckEqual(2, paged.HardLineCount); paged.Validate(); } public void RemovingTheTextBetweenTheHalvesOfAPair_JoinsThem() { string text = "aaaaaaa" + Pair.Substring(0, 1) + new string('x', 30) + Pair.Substring(1, 1) + "bbbbbbb"; PagedText paged = Paged(text); paged.TakeOut(8, 30); CheckEqual("aaaaaaa" + Pair + "bbbbbbb", paged.ToString()); paged.Validate(); } public void FragmentsThatFitOnePage_AreJoined() { PagedText paged = Paged("a" + new string('x', 198) + "z"); paged.TakeOut(1, 198); CheckEqual("az", paged.ToString()); CheckEqual(1, paged.PageCount); paged.Validate(); } public void ARangeOutsideTheText_Throws() { PagedText paged = Paged("abcdef"); CheckThrow(typeof(ArgumentOutOfRangeException)); paged.TakeOut(2, 5); } public void EveryRangeBetweenSeams_LeavesWhatCuttingAStringLeaves() { for (int start = 0; start <= Seams.Length; ++start) { for (int count = 0; start + count <= Seams.Length; ++count) { if (!IsBoundary(Seams, start) || !IsBoundary(Seams, start + count)) { continue; } PagedText paged = Paged(Seams); PagedText.PageRun run = paged.TakeOut(start, count); paged.Validate(); CheckEqual(Seams.Remove(start, count), paged.ToString()); CheckEqual(Seams.Substring(start, count), run.ToString()); } } } } public class PutBackTests : TestFixture { public void WhereItCameFrom_RestoresTheText() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(5, 12); paged.PutBack(5, run); CheckEqual("abcdefghijklmnopqrstuvwxyz", paged.ToString()); paged.Validate(); } public void AtAPageBoundary_AddsExactlyTheRunsPages() { PagedText source = Paged(new string('x', 12)); PagedText.PageRun run = source.TakeOut(0, 12); int runPages = run.Pages.Count; PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; int at = paged.PageLength(0) + paged.PageLength(1); paged.PutBack(at, run); CheckEqual(pages + runPages, paged.PageCount); CheckEqual("abcdefghijklmnopqrstuvwxyz".Insert(at, new string('x', 12)), paged.ToString()); paged.Validate(); } public void InsideAPage_CutsThatPageInTwo() { PagedText source = Paged(new string('x', 12)); PagedText.PageRun run = source.TakeOut(0, 12); int runPages = run.Pages.Count; PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); int pages = paged.PageCount; int at = paged.PageLength(0) + 2; paged.PutBack(at, run); CheckEqual(pages + runPages + 1, paged.PageCount); CheckEqual("abcdefghijklmnopqrstuvwxyz".Insert(at, new string('x', 12)), paged.ToString()); paged.Validate(); } public void AtAnotherIndex_InsertsTheRunsText() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(5, 12); paged.PutBack(0, run); CheckEqual("fghijklmnopqabcderstuvwxyz", paged.ToString()); paged.Validate(); } public void AtTheEnd_AppendsTheRunsText() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(0, 10); paged.PutBack(paged.Length, run); CheckEqual("klmnopqrstuvwxyzabcdefghij", paged.ToString()); paged.Validate(); } public void LeavesTheRunEmpty() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(5, 12); paged.PutBack(5, run); CheckEqual(0, run.Length); CheckEqual(0, run.Breaks); CheckEqual(0, run.Pages.Count); } public void AnEmptyRun_ChangesNothing() { PagedText paged = Paged("abcdef"); int version = paged.Version; paged.PutBack(3, new PagedText.PageRun()); CheckEqual(version, paged.Version); CheckEqual("abcdef", paged.ToString()); } public void BumpsTheVersion() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); PagedText.PageRun run = paged.TakeOut(5, 12); int version = paged.Version; paged.PutBack(5, run); CheckNotEqual(version, paged.Version); } public void IntoAnEmptyText_LeavesNoEmptyPage() { PagedText source = Paged(new string('x', 100)); PagedText.PageRun run = source.TakeOut(0, 100); int runPages = run.Pages.Count; PagedText paged = Paged(""); paged.PutBack(0, run); CheckEqual(new string('x', 100), paged.ToString()); CheckEqual(runPages, paged.PageCount); paged.Validate(); } public void CountsTheHardBreaksItBrings() { PagedText paged = Paged(Breaks); PagedText.PageRun run = paged.TakeOut(4, 30); int linesWithout = paged.HardLineCount; paged.PutBack(4, run); CheckEqual(Paged(Breaks).HardLineCount, paged.HardLineCount); Check(paged.HardLineCount > linesWithout); } public void ALfAtTheStartOfTheRun_JoinsACrBeforeIt() { PagedText source = Paged("\n" + new string('x', 30)); PagedText.PageRun run = source.TakeOut(0, 31); PagedText paged = Paged("aaaaaaa\rbbbbbbb"); paged.PutBack(8, run); CheckEqual("aaaaaaa\r\n" + new string('x', 30) + "bbbbbbb", paged.ToString()); CheckEqual(2, paged.HardLineCount); paged.Validate(); } public void ACrAtTheEndOfTheRun_JoinsAnLfAfterIt() { PagedText source = Paged(new string('x', 30) + "\r"); PagedText.PageRun run = source.TakeOut(0, 31); PagedText paged = Paged("aaaaaaa\nbbbbbbb"); paged.PutBack(7, run); CheckEqual("aaaaaaa" + new string('x', 30) + "\r\nbbbbbbb", paged.ToString()); CheckEqual(2, paged.HardLineCount); paged.Validate(); } public void AnLfOnItsOwnAfterACr_JoinsThemAndTheCharsAfterThemInOnePage() { PagedText source = Paged("\n"); PagedText.PageRun run = source.TakeOut(0, 1); PagedText paged = Paged("aaaaaaa\rbbbbbbb"); paged.PutBack(8, run); CheckEqual("aaaaaaa\r\nbbbbbbb", paged.ToString()); CheckEqual(2, paged.HardLineCount); paged.Validate(); } public void ALoneHighHalfAtTheEndOfTheRun_JoinsALowHalfAfterIt() { PagedText source = Paged("xxxxxxxx" + Pair.Substring(0, 1)); PagedText.PageRun run = source.TakeOut(0, 9); PagedText paged = Paged(Pair.Substring(1, 1) + "bbbbbbb"); paged.PutBack(0, run); CheckEqual("xxxxxxxx" + Pair + "bbbbbbb", paged.ToString()); paged.Validate(); } public void ARangeOutsideTheText_Throws() { PagedText paged = Paged("abcdef"); CheckThrow(typeof(ArgumentOutOfRangeException)); paged.PutBack(7, new PagedText.PageRun()); } public void EveryRangeBetweenSeams_PutBackWhereItCameFrom_RestoresTheText() { for (int start = 0; start <= Seams.Length; ++start) { for (int count = 0; start + count <= Seams.Length; ++count) { if (!IsBoundary(Seams, start) || !IsBoundary(Seams, start + count)) { continue; } PagedText paged = Paged(Seams); paged.PutBack(start, paged.TakeOut(start, count)); paged.Validate(); CheckEqual(Seams, paged.ToString()); } } } public void ASmallRunInsideOnePage_LeavesOnePage() { PagedText paged = Paged("abcdef"); paged.PutBack(2, paged.TakeOut(2, 2)); CheckEqual("abcdef", paged.ToString()); CheckEqual(1, paged.PageCount); paged.Validate(); } public void ALongRange_MovesWholePagesAtTheDefaultPageSize() { string text = Repeat("0123456789\n", 10000); PagedText paged = Paged(text, PagedText.DefaultPageSize); int pages = paged.PageCount; PagedText.PageRun run = paged.TakeOut(10000, 60000); CheckGreater(run.Pages.Count, 10); CheckEqual(text.Remove(10000, 60000), paged.ToString()); paged.PutBack(0, run); CheckEqual(text.Substring(10000, 60000) + text.Remove(10000, 60000), paged.ToString()); paged.Validate(); CheckGreater(paged.PageCount, pages - 3); } public void EveryRangeBetweenSeams_PutBackAtEveryIndex_InsertsItThere() { for (int start = 0; start <= Seams.Length; ++start) { for (int count = 1; start + count <= Seams.Length; ++count) { if (!IsBoundary(Seams, start) || !IsBoundary(Seams, start + count)) { continue; } string remaining = Seams.Remove(start, count); string taken = Seams.Substring(start, count); for (int at = 0; at <= remaining.Length; ++at) { PagedText paged = Paged(Seams); PagedText.PageRun run = paged.TakeOut(start, count); paged.PutBack(at, run); paged.Validate(); CheckEqual(remaining.Insert(at, taken), paged.ToString()); } } } } } public class RangeEqualsTests : TestFixture { public void TheWholeText_IsEqualToItself() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); CheckTrue(paged.RangeEquals(0, new StringView("abcdefghijklmnopqrstuvwxyz"))); } public void ARangeAcrossPages_IsEqualToItsChars() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); CheckTrue(paged.RangeEquals(3, new StringView("defghijklmnopqrstu"))); } public void AnEmptyView_IsEqualAnywhere() { PagedText paged = Paged("abcdef"); CheckTrue(paged.RangeEquals(6, new StringView(""))); CheckTrue(paged.RangeEquals(0, new StringView(""))); } public void ACharThatDiffers_IsNotEqual() { PagedText paged = Paged("abcdefghijklmnopqrstuvwxyz"); CheckFalse(paged.RangeEquals(3, new StringView("Xefghijklmnopqrstu"))); CheckFalse(paged.RangeEquals(3, new StringView("defghijklmXopqrstu"))); CheckFalse(paged.RangeEquals(3, new StringView("defghijklmnopqrstX"))); } public void ARangeOutsideTheText_Throws() { PagedText paged = Paged("abcdef"); CheckThrow(typeof(ArgumentOutOfRangeException)); paged.RangeEquals(3, new StringView("defg")); } } public class PlainRunEndTests : TestFixture { public void MatchesTheTextOfThePagesAtEveryIndex() { foreach (int pageSize in new[] { 8, 16 }) { foreach (string text in PlainTexts) { PagedText paged = Paged(text, pageSize); for (int i = 0; i <= text.Length; ++i) { CheckEqual(ExpectedPlainRunEnd(paged, i), paged.PlainRunEnd(i)); } } } } public void LettersAndDigitsOnly_RunToTheEndOfTheText() { CheckEqual(600, Paged(new string('a', 600)).PlainRunEnd(0)); CheckEqual(600, Paged(new string('a', 600)).PlainRunEnd(333)); } public void DigitsAreLetters_ToTheRun() { CheckEqual(300, Paged(new string('7', 300)).PlainRunEnd(5)); } public void LettersOfOtherScripts_RunToTheEndOfTheText() { CheckEqual(600, Paged(Repeat(Cyrillic, 600)).PlainRunEnd(0)); CheckEqual(600, Paged(Repeat(Alef, 600)).PlainRunEnd(333)); CheckEqual(600, Paged(Repeat(Deseret, 300)).PlainRunEnd(0)); } public void MarksAfterLetters_RunToTheEndOfTheText() { CheckEqual(600, Paged(Repeat(ArabicBaWithFatha, 300)).PlainRunEnd(0)); } public void AMarkAtTheStartOfAPage_EndsTheRunBeforeIt() { int pageLength = Paged(Repeat("a", 64), pageSize: 8).PageLength(0); PagedText paged = Paged(Repeat("a", pageLength) + Fatha + Repeat("a", 40), pageSize: 8); CheckEqual(pageLength, paged.PlainRunEnd(0)); } public void ALoneSurrogate_EndsTheRunToo() { PagedText paged = Paged(Repeat("a", 300) + (char)0xD801 + Repeat("b", 300)); CheckTrue(paged.PlainRunEnd(0) <= 300); } public void ASpace_EndsTheRunAtTheStartOfItsPage() { PagedText paged = Paged(new string('a', 300) + " " + new string('b', 300)); int end = paged.PlainRunEnd(0); CheckEqual(ExpectedPlainRunEnd(paged, 0), end); CheckTrue(end <= 300); CheckTrue(end > 300 - 2 * paged.PageSize); } public void APunctuationMarkOrUnderscoreOrNonAsciiChar_EndsTheRunToo() { foreach (string odd in new[] { "-", " ", "/", Ideograph }) { PagedText paged = Paged(new string('a', 300) + odd + new string('b', 300)); CheckTrue(paged.PlainRunEnd(0) <= 300); } } public void ACharNotInAPlainPage_IsItsOwnIndex() { PagedText paged = Paged("a b c d e f g h"); CheckEqual(5, paged.PlainRunEnd(5)); } public void AtTheEndOfTheText_IsTheEnd() { CheckEqual(600, Paged(new string('a', 600)).PlainRunEnd(600)); } public void FollowsEdits() { PagedText paged = Paged(new string('a', 200)); CheckEqual(200, paged.PlainRunEnd(0)); paged.Replace(100, 0, " "); for (int i = 0; i <= paged.Length; ++i) { CheckEqual(ExpectedPlainRunEnd(paged, i), paged.PlainRunEnd(i)); } CheckTrue(paged.PlainRunEnd(0) <= 100); paged.Replace(100, 1, ""); CheckEqual(200, paged.PlainRunEnd(0)); paged.Validate(); } public void IndexBeyondTheText_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Paged("abc").PlainRunEnd(4); } } public class PlainRunStartTests : TestFixture { public void MatchesTheTextOfThePagesAtEveryIndex() { foreach (int pageSize in new[] { 8, 16 }) { foreach (string text in PlainTexts) { PagedText paged = Paged(text, pageSize); for (int i = 0; i <= text.Length; ++i) { CheckEqual(ExpectedPlainRunStart(paged, i), paged.PlainRunStart(i)); } } } } public void LettersAndDigitsOnly_RunBackToTheStartOfTheText() { CheckEqual(0, Paged(new string('a', 600)).PlainRunStart(600)); CheckEqual(0, Paged(new string('a', 600)).PlainRunStart(333)); } public void ASpace_StartsTheRunAtTheEndOfItsPage() { PagedText paged = Paged(new string('a', 300) + " " + new string('b', 300)); int start = paged.PlainRunStart(601); CheckTrue(start >= 301); CheckTrue(start < 301 + 2 * paged.PageSize); } public void AtTheStartOfTheText_IsTheStart() { CheckEqual(0, Paged(new string('a', 600)).PlainRunStart(0)); } public void LettersOfOtherScripts_RunBackToTheStartOfTheText() { CheckEqual(0, Paged(Repeat(Cyrillic, 600)).PlainRunStart(600)); CheckEqual(0, Paged(Repeat(Alef, 600)).PlainRunStart(333)); CheckEqual(0, Paged(Repeat(Deseret, 300)).PlainRunStart(600)); } public void MarksAfterLetters_RunBackToTheStartOfTheText() { CheckEqual(0, Paged(Repeat(ArabicBaWithFatha, 300)).PlainRunStart(600)); } public void AMarkAtTheStartOfAPage_StartsTheRunAfterIt() { int pageLength = Paged(Repeat("a", 64), pageSize: 8).PageLength(0); PagedText paged = Paged(Repeat("a", pageLength) + Fatha + Repeat("a", 40), pageSize: 8); CheckEqual(pageLength + paged.PageLength(1), paged.PlainRunStart(paged.Length)); } public void AfterACharNotInAPlainPage_IsItsOwnIndex() { PagedText paged = Paged("a b c d e f g h"); CheckEqual(6, paged.PlainRunStart(6)); } public void FollowsEdits() { PagedText paged = Paged(new string('a', 200)); paged.Replace(100, 0, " "); for (int i = 0; i <= paged.Length; ++i) { CheckEqual(ExpectedPlainRunStart(paged, i), paged.PlainRunStart(i)); } paged.Replace(100, 1, ""); CheckEqual(0, paged.PlainRunStart(200)); paged.Validate(); } public void IndexBeyondTheText_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Paged("abc").PlainRunStart(4); } } public class HardLineCountTests : TestFixture { public void IsOneMoreThanTheBreaks() { CheckEqual(13, Paged(Breaks).HardLineCount); } public void ACrLfCountsOnce() { CheckEqual(2, Paged("a\r\nb").HardLineCount); } public void ACrAloneAndAnLfAlone_CountSeparately() { CheckEqual(3, Paged("a\rb\nc").HardLineCount); } public void FollowsEdits() { PagedText paged = Paged("a\nb"); paged.Replace(1, 0, "\n\n"); CheckEqual(4, paged.HardLineCount); } public void FollowsEditsAcrossPages() { PagedText paged = Paged("ab\ncd\nef\ngh\nij\nkl\nmn\nop"); paged.Replace(2, 15, ""); CheckEqual(3, paged.HardLineCount); paged.Validate(); } } public class LineFunctionTests : TestFixture { public static readonly int[] PageSizes = { 8, 16, 4096 }; public void StartOfLine_MatchesHardBreaksAtEveryIndex() { foreach (int pageSize in PageSizes) { PagedText paged = Paged(Breaks, pageSize); for (int i = 0; i <= Breaks.Length; ++i) { CheckEqual(HardBreaks.StartOfLine(Breaks, i), paged.StartOfLine(i)); } } } public void EndOfLine_MatchesHardBreaksAtEveryIndex() { foreach (int pageSize in PageSizes) { PagedText paged = Paged(Breaks, pageSize); for (int i = 0; i <= Breaks.Length; ++i) { CheckEqual(HardBreaks.EndOfLine(Breaks, i), paged.EndOfLine(i)); } } } public void LengthAt_MatchesHardBreaksAtEveryIndex() { foreach (int pageSize in PageSizes) { PagedText paged = Paged(Breaks, pageSize); for (int i = 0; i <= Breaks.Length; ++i) { CheckEqual(HardBreaks.LengthAt(Breaks, i), paged.LengthAt(i)); } } } public void LengthBefore_MatchesHardBreaksAtEveryIndex() { foreach (int pageSize in PageSizes) { PagedText paged = Paged(Breaks, pageSize); for (int i = 0; i <= Breaks.Length; ++i) { CheckEqual(HardBreaks.LengthBefore(Breaks, i), paged.LengthBefore(i)); } } } public void ALineLongerThanSeveralPages_IsFoundWhole() { string text = "ab\n" + new string('x', 100) + "\ncd"; PagedText paged = Paged(text); CheckEqual(3, paged.StartOfLine(60)); CheckEqual(103, paged.EndOfLine(60)); } public void LinesWithLongStretchesBetweenBreaks_AreFoundAtEveryIndex() { string text = new string('a', 30) + "\n" + new string('b', 200) + "\r\n" + "cc" + "\r" + new string('d', 100) + "\n\n" + new string('e', 60); foreach (int pageSize in new[] { 8, 16 }) { PagedText paged = Paged(text, pageSize); for (int i = 0; i <= text.Length; ++i) { CheckEqual(HardBreaks.StartOfLine(text, i), paged.StartOfLine(i)); CheckEqual(HardBreaks.EndOfLine(text, i), paged.EndOfLine(i)); } } } public void ALineOfManyPages_IsFoundWholeFromTheMiddleOfAPageAndFromAPageStart() { string text = "x\n" + new string('y', 500) + "\nz"; PagedText paged = Paged(text); CheckEqual(2, paged.StartOfLine(502)); CheckEqual(502, paged.EndOfLine(2)); CheckEqual(502, paged.EndOfLine(250)); CheckEqual(2, paged.StartOfLine(251)); } public void ALineTheTextEndsIn_IsFoundWhole() { PagedText paged = Paged("x\n" + new string('y', 500)); CheckEqual(502, paged.EndOfLine(100)); CheckEqual(2, paged.StartOfLine(502)); } public void ATextWithNoBreaksAtAll_IsOneLine() { PagedText paged = Paged(new string('y', 500)); CheckEqual(0, paged.StartOfLine(499)); CheckEqual(500, paged.EndOfLine(1)); } public void IndexBeyondTheText_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Paged("abc").StartOfLine(4); } public void LineNumberOf_CountsTheBreaksBeforeAnIndex() { foreach (int pageSize in PageSizes) { PagedText paged = Paged(Breaks, pageSize); for (int i = 0; i <= Breaks.Length; ++i) { CheckEqual(ExpectedLineNumber(Breaks, i), paged.LineNumberOf(i)); } } } public void BetweenACrAndAnLf_IsOnTheLineThePairEnds() { CheckEqual(0, Paged("a\r\nb").LineNumberOf(2)); CheckEqual(1, Paged("a\r\nb").LineNumberOf(3)); } public void StartOfLineNumber_FindsEachLinesStart() { foreach (int pageSize in PageSizes) { PagedText paged = Paged(Breaks, pageSize); for (int line = 0; line < paged.HardLineCount; ++line) { CheckEqual(ExpectedStartOfLine(Breaks, line), paged.StartOfLineNumber(line)); } } } public void StartOfLineNumber_AgreesWithLineNumberOf() { PagedText paged = Paged(Breaks); for (int line = 0; line < paged.HardLineCount; ++line) { CheckEqual(line, paged.LineNumberOf(paged.StartOfLineNumber(line))); } } public void StartOfLineNumber_PastTheLastLine_Throws() { PagedText paged = Paged("a\nb"); CheckThrow(typeof(ArgumentOutOfRangeException)); paged.StartOfLineNumber(2); } public void LinesFollowEditsAcrossPages() { PagedText paged = Paged("ab\ncd\nef\ngh\nij\nkl\nmn\nop"); paged.Replace(3, 6, "X\nY\nZ\n"); CheckEqual(9, paged.StartOfLineNumber(4)); CheckEqual(4, paged.LineNumberOf(9)); paged.Validate(); } // Counted straight from the string: the breaks that end at or before the index. public static int ExpectedLineNumber(string text, int index) { int returnMe = 0; int i = 0; while (i < index) { int breakLength = HardBreaks.LengthAt(text, i); if (breakLength == 0) { ++i; continue; } if (i + breakLength <= index) { ++returnMe; } i += breakLength; } return returnMe; } public static int ExpectedStartOfLine(string text, int line) { int index = 0; for (int seen = 0; seen < line; ++seen) { while (HardBreaks.LengthAt(text, index) == 0) { ++index; } index += HardBreaks.LengthAt(text, index); } return index; } } } }