using System; using System.Collections.Generic; using System.Text; using Amphora; using Annulus; using Azimuth; using Glass.Editing; using Glass.Typesetting; using Glass.UnitTests.Editing; using UnitTestSharp; using GlyphDefinition = Glass.SpriteFontDefinition.GlyphDefinition; namespace Glass.UnitTests.Typesetting { public class VisualLineIndexTests : TestFixture { public const int Wide = 100; public static readonly SpriteFontDefinition Font = LineWrapperTests.FontWithTenPixelLines; public static readonly int LineSpacing = LineLayout.HeightFor(Font, 1) + Font.LineGap; // Hard lines that wrap, are empty, end in each kind of break and run past a block of 16 chars. public static readonly string[] Corpus = { "", "a", "a\n", "\n", "\n\n\n", "\r\n\r\n", "aaaa bbbb cccc dddd eeee ffff gggg", "ab\ncd ef gh ij kl mn\n\nqrs\r\ntuv wxy zab cde fgh ijk\n", "a\r\nb\rc\nd", "abcdefghijklmnopqrstuvwxyz abc", "aaaa bbbb cccc dddd eeee ffff gggg\nhhhh iiii jjjj kkkk llll\r\nmmmm nnnn\n\n\n" + "oooo pppp qqqq rrrr ssss tttt\nu\nv\nw\nx\ny\nz\n\n\nab cd\r\nef gh ij\nkl", }; public static VisualLineIndex NewIndex(int fitWidth = Wide, bool measureWidths = false, int blockChars = 16, int? longLineChars = null, int synchronousChars = 100000, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang, SpriteFontDefinition font = null) { font = font ?? Font; var returnMe = new VisualLineIndex(blockChars, longLineChars ?? Math.Min(1000, synchronousChars), synchronousChars); returnMe.Configure(font, LineLayout.HeightFor(font, 1) + font.LineGap, fitWidth, trailingSpaces, measureWidths); return returnMe; } public static string Repeat(string text, int times) { return string.Concat(System.Linq.Enumerable.Repeat(text, times)); } /// /// Lays the whole text out in one go, the answer the index has to agree with. /// public class FullLayout { private readonly MeasuredWords words; private readonly string text; private readonly int fitWidth; private readonly TrailingSpaceMode trailingSpaces; private readonly int lineSpacing; public FullLayout(string text, int fitWidth, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang, SpriteFontDefinition font = null) { font = font ?? Font; this.text = text; this.fitWidth = fitWidth; this.trailingSpaces = trailingSpaces; lineSpacing = LineLayout.HeightFor(font, 1) + font.LineGap; words = new MeasuredWords(font, new StringView(text)); } public int TotalRows => RowOf(text.Length, atLineEnd: false) + 1; public int WidestLine { get { var wrapper = new LineWrapper(); wrapper.Initialize(words, int.MaxValue, maxHeight: null, TextOverflow.Clip, maxLines: null, trailingSpaces); int returnMe = 0; while (wrapper.MoveNext()) { returnMe = Math.Max(returnMe, wrapper.Current.VisibleWidth); } return returnMe; } } public int RowOf(int index, bool atLineEnd) { return CaretLocator.Locate(words, index, fitWidth, Alignment.Horizontal.Left, maxHeight: null, TextOverflow.Clip, maxLines: null, trailingSpaces, atLineEnd).Line; } public int IndexOfRow(int row) { return CaretLocator.IndexAt(words, x: 0, row * lineSpacing, fitWidth, Alignment.Horizontal.Left, out _, maxHeight: null, TextOverflow.Clip, maxLines: null, trailingSpaces); } public int XOf(int index) { return CaretLocator.Locate(words, index, fitWidth, Alignment.Horizontal.Left, maxHeight: null, TextOverflow.Clip, maxLines: null, trailingSpaces).X; } public int IndexAtX(int rowIndex, int x) { return CaretLocator.IndexAt(words, x, RowOf(rowIndex, atLineEnd: false) * lineSpacing, fitWidth, Alignment.Horizontal.Left, out _, maxHeight: null, TextOverflow.Clip, maxLines: null, trailingSpaces); } public bool IsInsideCrLf(int index) { return index > 0 && index < text.Length && text[index - 1] == '\r' && text[index] == '\n'; } } /// /// Describes every question the index answers differently from a full layout of the text, or nothing when it /// agrees on all of them. /// public static string Differences(VisualLineIndex index, ITextSource source, string text, int fitWidth, int indexStep = 1, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang) { var layout = new FullLayout(text, fitWidth, trailingSpaces); var returnMe = new StringBuilder(); int total = layout.TotalRows; if (index.TotalRows(source) != total) { returnMe.Append($"TotalRows {index.TotalRows(source)} not {total}; "); } for (int i = 0; i <= text.Length; i += indexStep) { if (layout.IsInsideCrLf(i)) { continue; } foreach (bool atLineEnd in new[] { false, true }) { if (index.RowOf(source, i, atLineEnd) != layout.RowOf(i, atLineEnd)) { returnMe.Append($"RowOf({i},{atLineEnd}) {index.RowOf(source, i, atLineEnd)} not ") .Append($"{layout.RowOf(i, atLineEnd)}; "); } } } for (int row = 0; row < total; ++row) { if (index.IndexOfRow(source, row) != layout.IndexOfRow(row)) { returnMe.Append($"IndexOfRow({row}) {index.IndexOfRow(source, row)} not ") .Append($"{layout.IndexOfRow(row)}; "); } } if (index.IndexOfRow(source, total + 7) != layout.IndexOfRow(total - 1)) { returnMe.Append("IndexOfRow past the end; "); } return returnMe.ToString(); } /// /// Counts the index through to the end of the text, a little at a time. /// public static void CountAll(VisualLineIndex index, ITextSource source, int step = 20) { int guard = 0; while (index.CountMore(source, step)) { CheckStatic(++guard < 100000, "Counting never finished."); } } public static void CheckStatic(bool condition, string message) { if (!condition) { throw new InvalidOperationException(message); } } public class CtorTests : TestFixture { public void ABlockSmallerThanOneChar_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new VisualLineIndex(blockChars: 0); } public void ALongLineShorterThanABlock_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new VisualLineIndex(blockChars: 100, longLineChars: 99); } public void ASynchronousLimitShorterThanALongLine_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new VisualLineIndex(blockChars: 10, longLineChars: 100, synchronousChars: 99); } public void HoldsNoBlocksAtFirst() { CheckEqual(0, new VisualLineIndex().BlockCount); } } public class ConfigureTests : TestFixture { public void RejectsNoFont() { CheckThrow(typeof(ArgumentNullException)); new VisualLineIndex().Configure(null, LineSpacing, Wide, TrailingSpaceMode.Hang, false); } public void RejectsALineSpacingBelowOne() { CheckThrow(typeof(ArgumentOutOfRangeException)); new VisualLineIndex().Configure(Font, 0, Wide, TrailingSpaceMode.Hang, false); } public void QuestionsBeforeItIsCalled_Throw() { CheckThrow(typeof(InvalidOperationException)); new VisualLineIndex().TotalRows(new StringSource("a")); } public void AChangeOfWidth_CountsAgainAtTheNewWidth() { var source = new StringSource(Corpus[6]); VisualLineIndex index = NewIndex(fitWidth: Wide); CheckEqual(4, index.TotalRows(source)); index.Configure(Font, LineSpacing, 40, TrailingSpaceMode.Hang, false); CheckEqual("", Differences(index, source, Corpus[6], 40)); } public void ATrailingSpaceModeChange_CountsAgain() { const string text = "aaaa bbbb cccc dddd"; var source = new StringSource(text); VisualLineIndex index = NewIndex(fitWidth: 90); CheckEqual(new FullLayout(text, 90).TotalRows, index.TotalRows(source)); index.Configure(Font, LineSpacing, 90, TrailingSpaceMode.Wrap, false); CheckEqual(new FullLayout(text, 90, TrailingSpaceMode.Wrap).TotalRows, index.TotalRows(source)); } public void TheSameSettings_KeepWhatWasCounted() { var source = new StringSource(Corpus[10]); VisualLineIndex index = NewIndex(synchronousChars: 100000); index.TotalRows(source); int blocks = index.BlockCount; index.Configure(Font, LineSpacing, Wide, TrailingSpaceMode.Hang, false); CheckEqual(blocks, index.BlockCount); } public void AChangeOfWidth_KeepsTheProportionOfRowsAsAnEstimate() { string text = Repeat("aaaa bbbb cccc dddd\n", 60); var source = new StringSource(text); VisualLineIndex index = NewIndex(fitWidth: 100, synchronousChars: 100); CountAll(index, source); int exactAtWide = index.TotalRows(source); index.Configure(Font, LineSpacing, 60, TrailingSpaceMode.Hang, false); int estimate = index.TotalRows(source); CheckEqual(exactAtWide, estimate); CheckFalse(index.IsExact(source)); } } public class TotalRowsTests : TestFixture { public void MatchesAFullLayout_OfAString() { foreach (string text in Corpus) { var source = new StringSource(text); CheckEqual(new FullLayout(text, Wide).TotalRows, NewIndex().TotalRows(source)); } } public void MatchesAFullLayout_OfAPagedText() { foreach (string text in Corpus) { CheckEqual(new FullLayout(text, Wide).TotalRows, NewIndex().TotalRows(PagedTextTests.Paged(text))); } } public void MatchesAFullLayout_AtOtherWidths() { foreach (int width in new[] { 15, 45, 250, int.MaxValue }) { foreach (string text in Corpus) { CheckEqual(new FullLayout(text, width).TotalRows, NewIndex(width).TotalRows(new StringSource(text))); } } } public void MatchesAFullLayout_WithBlocksOfOneChar() { foreach (string text in Corpus) { var index = NewIndex(blockChars: 1, longLineChars: 48); CheckEqual(new FullLayout(text, Wide).TotalRows, index.TotalRows(new StringSource(text))); } } public void EmptyText_IsOneRow() { CheckEqual(1, NewIndex().TotalRows(new StringSource(""))); } public void CountsTheEmptyLineAfterATrailingBreak() { CheckEqual(3, NewIndex().TotalRows(new StringSource("a\nb\n"))); } public void NeverFallsBelowTheHardLines_WhileCountingIsIncomplete() { string text = Repeat("aaaa bbbb cccc dddd\n", 80); var source = new StringSource(text); VisualLineIndex index = NewIndex(synchronousChars: 100); do { CheckTrue(index.TotalRows(source) >= source.HardLineCount); } while (index.CountMore(source, 40)); } public void EstimatesFromTheLinesCounted() { string text = Repeat("aaaa bbbb cccc dddd\n", 80); var source = new StringSource(text); VisualLineIndex index = NewIndex(synchronousChars: 100); index.CountMore(source, 200); int real = new FullLayout(text, Wide).TotalRows; CheckTrue(Math.Abs(real - index.TotalRows(source)) <= 2); CheckFalse(index.IsExact(source)); } public void ADifferentSource_IsCountedFromScratch() { VisualLineIndex index = NewIndex(); index.TotalRows(new StringSource(Corpus[10])); CheckEqual(new FullLayout(Corpus[6], Wide).TotalRows, index.TotalRows(new StringSource(Corpus[6]))); } } public class RowOfTests : TestFixture { public void MatchesAFullLayoutAtEveryIndex_OfAString() { foreach (string text in Corpus) { CheckEqual("", Differences(NewIndex(), new StringSource(text), text, Wide)); } } public void MatchesAFullLayoutAtEveryIndex_OfAPagedText() { foreach (string text in Corpus) { CheckEqual("", Differences(NewIndex(), PagedTextTests.Paged(text), text, Wide)); } } public void MatchesAFullLayoutAtEveryIndex_AtOtherWidths() { foreach (int width in new[] { 15, 45, 250, int.MaxValue }) { foreach (string text in Corpus) { CheckEqual("", Differences(NewIndex(width), new StringSource(text), text, width)); } } } public void MatchesAFullLayoutAtEveryIndex_WithBlocksOfOneChar() { foreach (string text in Corpus) { CheckEqual("", Differences(NewIndex(blockChars: 1), new StringSource(text), text, Wide)); } } public void IsExactForTheCountedPart_WhileTheRestIsEstimated() { string text = Repeat("aaaa bbbb cccc dddd\nab\n\ncd ef gh ij kl mn\n", 12); var source = new StringSource(text); var layout = new FullLayout(text, Wide); VisualLineIndex index = NewIndex(synchronousChars: 100); index.CountMore(source, 150); int counted = index.CountedChars(source); CheckGreater(counted, 0); CheckGreater(text.Length, counted); for (int i = 0; i < counted; ++i) { CheckEqual(layout.RowOf(i, atLineEnd: false), index.RowOf(source, i, atLineEnd: false)); } } public void AtTheStartOfEachUncountedLine_IsWhereTheEstimateOfItsRowLeadsBack() { string text = Repeat("aaaa bbbb cccc dddd\nab\n\ncd ef gh ij kl mn\n", 12); var source = new StringSource(text); VisualLineIndex index = NewIndex(synchronousChars: 100); index.CountMore(source, 150); for (int start = source.StartOfLineNumber(source.LineNumberOf(index.CountedChars(source))); start < text.Length; start = source.EndOfLine(start) + source.LengthAt(source.EndOfLine(start))) { CheckEqual(start, index.IndexOfRow(source, index.RowOf(source, start, atLineEnd: false))); } } public void InsideAnUncountedLine_IsWithinThatLinesRows() { string text = Repeat("aaaa bbbb cccc dddd eeee ffff gggg\n", 30); var source = new StringSource(text); VisualLineIndex index = NewIndex(synchronousChars: 100); index.CountMore(source, 100); int lineStart = 35 * 20; int atStart = index.RowOf(source, lineStart, atLineEnd: false); int atEnd = index.RowOf(source, lineStart + 30, atLineEnd: false); CheckEqual(atStart + 3, atEnd); } public void AnIndexOutsideTheText_Throws() { VisualLineIndex index = NewIndex(); var source = new StringSource("abc"); CheckThrow(typeof(ArgumentOutOfRangeException)); index.RowOf(source, 4, atLineEnd: false); } public void ANegativeIndex_Throws() { VisualLineIndex index = NewIndex(); var source = new StringSource("abc"); CheckThrow(typeof(ArgumentOutOfRangeException)); index.RowOf(source, -1, atLineEnd: false); } } public class IndexOfRowTests : TestFixture { public void AFarRow_LeadsToTheLastRowsStart() { var source = new StringSource("ab\ncd"); CheckEqual(3, NewIndex().IndexOfRow(source, 500)); } public void ANegativeRow_LeadsToTheStart() { var source = new StringSource("ab\ncd"); CheckEqual(0, NewIndex().IndexOfRow(source, -4)); } public void EmptyText_LeadsToTheStart() { CheckEqual(0, NewIndex().IndexOfRow(new StringSource(""), 3)); } public void InAnUncountedTextWithNothingCountedYet_ReckonsOneRowToEachHardLine() { var source = new StringSource(Repeat("ab\n", 100)); VisualLineIndex index = NewIndex(synchronousChars: 100); CheckEqual(150, index.IndexOfRow(source, 50)); CheckEqual(0, index.CountedChars(source)); } public void ARowInAnUncountedLine_LeadsToTheStartOfThatLine() { string text = Repeat("aaaa bbbb cccc dddd eeee ffff gggg\n", 30); var source = new StringSource(text); VisualLineIndex index = NewIndex(synchronousChars: 100); index.CountMore(source, 100); int row = index.RowOf(source, 35 * 20, atLineEnd: false); CheckEqual(35 * 20, index.IndexOfRow(source, row)); CheckEqual(35 * 20, index.IndexOfRow(source, row + 1)); } public void TheLastRowOfAPartlyCountedText_LeadsToTheLastLine() { string text = Repeat("aaaa bbbb cccc dddd\n", 60); var source = new StringSource(text); VisualLineIndex index = NewIndex(synchronousChars: 100); index.CountMore(source, 100); CheckEqual(text.Length, index.IndexOfRow(source, int.MaxValue)); } } public class NoteEditTests : TestFixture { public static readonly string[] Inserted = { "", "x", "\n", "\r", "\r\n", "ab cd\nef", "\n\n", "\rb\n" }; public static readonly string[] BaseTexts = { "aaaa bbbb cccc dddd eeee ffff gggg\nhhhh iiii jjjj kkkk llll\r\nmmmm nnnn\n\n\n" + "oooo pppp\nq\r\nr\rs\n\nt u v", "ab\ncd\nef\ngh\nij\nkl\nmn\nop\nqr\nst\nuv\nwx\nyz\n", "", "a", }; // Long enough that a synchronous limit of 100 leaves most of it uncounted. public static readonly string PartialBase = Repeat(BaseTexts[0] + "\n", 3); public static string EditAndCompare(string baseText, int start, int removed, string inserted, int synchronousChars = 100000, int countedFirst = 0, int width = Wide) { PagedText paged = PagedTextTests.Paged(baseText); VisualLineIndex index = NewIndex(width, synchronousChars: synchronousChars); index.TotalRows(paged); if (countedFirst > 0) { index.CountMore(paged, countedFirst); } paged.Replace(start, removed, inserted); index.NoteEdit(paged, start, removed, inserted.Length); string edited = baseText.Remove(start, removed).Insert(start, inserted); if (synchronousChars < 100000) { CountAll(index, paged, step: 30); } string differences = Differences(index, paged, edited, width, indexStep: 3); return differences.Length == 0 ? "" : $"edit({start},{removed},{inserted.Length}): {differences}"; } public static string CompareAllEdits(string insertedText, string[] baseTexts = null, int synchronousChars = 100000, int countedFirst = 0, int startStep = 1) { var returnMe = new StringBuilder(); foreach (string baseText in baseTexts ?? BaseTexts) { for (int start = 0; start <= baseText.Length; start += startStep) { foreach (int removed in startStep > 1 ? new[] { 0, 3, 40 } : new[] { 0, 1, 2, 5, 40 }) { if (start + removed > baseText.Length) { continue; } returnMe.Append(EditAndCompare(baseText, start, removed, insertedText, synchronousChars, countedFirst)); } } } return returnMe.ToString(); } [Thorough] public void Deleting_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[0])); } [Thorough] public void InsertingAChar_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[1])); } public void InsertingALineFeed_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[2])); } public void InsertingACarriageReturn_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[3])); } public void InsertingACrLf_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[4])); } [Thorough] public void InsertingTwoLines_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[5])); } [Thorough] public void InsertingTwoBreaks_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[6])); } [Thorough] public void InsertingBreaksAroundAChar_AnywhereInAText_LeavesTheAnswersOfAFullLayout() { CheckEqual("", CompareAllEdits(Inserted[7])); } [Thorough] public void Deleting_InAPartlyCountedText_LeavesTheAnswersOfAFullLayoutOnceCounted() { CheckEqual("", CompareAllEdits("", new[] { PartialBase }, synchronousChars: 100, countedFirst: 120, startStep: 13)); } [Thorough] public void InsertingTwoLines_InAPartlyCountedText_LeavesTheAnswersOfAFullLayoutOnceCounted() { CheckEqual("", CompareAllEdits("x\ny", new[] { PartialBase }, synchronousChars: 100, countedFirst: 120, startStep: 13)); } [Thorough] public void InsertingALineFeed_InAPartlyCountedText_LeavesTheAnswersOfAFullLayoutOnceCounted() { CheckEqual("", CompareAllEdits("\n", new[] { PartialBase }, synchronousChars: 100, countedFirst: 120, startStep: 13)); } [Thorough] public void InsertingACrLf_InAPartlyCountedText_LeavesTheAnswersOfAFullLayoutOnceCounted() { CheckEqual("", CompareAllEdits("\r\n", new[] { PartialBase }, synchronousChars: 100, countedFirst: 120, startStep: 13)); } public void AnEditBeforeAnythingIsCounted_LeavesTheAnswersOfAFullLayoutOnceCounted() { string[] texts = { PartialBase }; CheckEqual("", CompareAllEdits("x\ny", texts, synchronousChars: 100, countedFirst: 0, startStep: 17)); } public void AnEditThatCutsTheCountedPartInTwo_LeavesTheAnswersOfAFullLayoutOnceCounted() { var returnMe = new StringBuilder(); string text = PartialBase; PagedText probeText = PagedTextTests.Paged(text); VisualLineIndex probe = NewIndex(synchronousChars: 100); probe.CountMore(probeText, 120); int frontier = probe.CountedChars(probeText); foreach (int start in new[] { frontier - 3, frontier - 1, frontier, frontier + 1 }) { foreach (int removed in new[] { 1, 4, 50 }) { returnMe.Append(EditAndCompare(text, start, removed, "q\r\n", synchronousChars: 100, countedFirst: 120)); } } CheckEqual("", returnMe.ToString()); } public void AnInsertionOfManyBlocks_GrowsTheIndexAndStaysExact() { PagedText paged = PagedTextTests.Paged("ab\ncd\n"); VisualLineIndex index = NewIndex(); index.TotalRows(paged); string insertion = Repeat("cd ef gh\nij kl\n", 100); paged.Replace(3, 0, insertion); index.NoteEdit(paged, 3, 0, insertion.Length); CheckEqual("", Differences(index, paged, "ab\n" + insertion + "cd\n", Wide, indexStep: 37)); CheckGreater(index.BlockCount, 16); } public void AnInsertionTooBigToCountAtOnce_LeavesTheRestToBeCounted() { string text = Repeat("aaaa bbbb\n", 30); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(synchronousChars: 100); CountAll(index, paged); string insertion = Repeat("cc dd\n", 60); paged.Replace(150, 0, insertion); index.NoteEdit(paged, 150, 0, insertion.Length); CheckGreater(paged.Length, index.CountedChars(paged)); CountAll(index, paged); CheckEqual("", Differences(index, paged, text.Insert(150, insertion), Wide, indexStep: 11)); } public void CuttingTheBreakOffTheCountedPart_OfLinesThatDoNotWrap_CountsTheJoinedLineAgain() { string text = Repeat("ab cd\n", 40); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: false, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(paged, 100); int counted = index.CountedChars(paged); paged.Replace(counted - 1, 1, ""); index.NoteEdit(paged, counted - 1, 1, 0); CountAll(index, paged); CheckEqual("", Differences(index, paged, text.Remove(counted - 1, 1), int.MaxValue, indexStep: 7)); } public void ManyEditsBeforeTheNextQuestion_LeaveTheAnswersOfAFullLayout() { string text = BaseTexts[0]; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(); index.TotalRows(paged); foreach ((int start, int removed, string inserted) in new[] { (10, 0, "\n"), (11, 3, ""), (0, 4, "q\r"), (40, 1, "abc def\r\n"), (paged.Length + 2, 0, "end"), }) { int at = Math.Min(start, paged.Length); int remove = Math.Min(removed, paged.Length - at); paged.Replace(at, remove, inserted); index.NoteEdit(paged, at, remove, inserted.Length); } var whole = new StringBuilder(); for (int i = 0; i < paged.Length; ++i) { whole.Append(paged[i]); } CheckEqual("", Differences(index, paged, whole.ToString(), Wide, indexStep: 3)); } public void ATextEditedWithoutBeingTold_IsCountedAgain() { PagedText paged = PagedTextTests.Paged(BaseTexts[0]); VisualLineIndex index = NewIndex(); index.TotalRows(paged); paged.Replace(5, 40, "\n\n\n"); string edited = BaseTexts[0].Remove(5, 40).Insert(5, "\n\n\n"); CheckEqual("", Differences(index, paged, edited, Wide, indexStep: 3)); } public void AnEditToAnotherText_IsIgnored() { var source = new StringSource(BaseTexts[0]); VisualLineIndex index = NewIndex(); index.TotalRows(source); index.NoteEdit(new StringSource("abc"), 1, 1, 0); CheckEqual("", Differences(index, source, BaseTexts[0], Wide, indexStep: 3)); } public void AnEditBeforeAnythingWasAsked_IsIgnored() { PagedText paged = PagedTextTests.Paged(BaseTexts[0]); VisualLineIndex index = NewIndex(); index.NoteEdit(paged, 0, 0, 1); CheckEqual("", Differences(index, paged, BaseTexts[0], Wide, indexStep: 3)); } public void TypingOneCharAtATimeAtTheEnd_StaysExact() { PagedText paged = PagedTextTests.Paged(""); VisualLineIndex index = NewIndex(); var typed = new StringBuilder(); foreach (char next in "aaaa bbbb\ncccc dddd eeee ffff\n\ngggg hhhh iiii jjjj kkkk llll") { index.TotalRows(paged); paged.Replace(paged.Length, 0, next.ToString()); index.NoteEdit(paged, paged.Length - 1, 0, 1); typed.Append(next); CheckEqual("", Differences(index, paged, typed.ToString(), Wide, indexStep: 5)); } } public void TypingEveryCharOfALineFromTheMiddle_StaysExact() { string text = BaseTexts[0]; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(); string current = text; foreach (char next in "the quick brown fox\njumps") { index.TotalRows(paged); paged.Replace(30, 0, next.ToString()); index.NoteEdit(paged, 30, 0, 1); current = current.Insert(30, next.ToString()); CheckEqual("", Differences(index, paged, current, Wide, indexStep: 7)); } } public void DeletingEverything_LeavesOneEmptyRow() { PagedText paged = PagedTextTests.Paged(BaseTexts[0]); VisualLineIndex index = NewIndex(); index.TotalRows(paged); paged.Replace(0, paged.Length, ""); index.NoteEdit(paged, 0, BaseTexts[0].Length, 0); CheckEqual(1, index.TotalRows(paged)); CheckEqual(1, index.BlockCount); } public void AGrowingBlock_SplitsIntoSeveral() { PagedText paged = PagedTextTests.Paged("ab\n"); VisualLineIndex index = NewIndex(blockChars: 16); index.TotalRows(paged); CheckEqual(1, index.BlockCount); string insertion = Repeat("cd ef gh\n", 20); paged.Replace(1, 0, insertion); index.NoteEdit(paged, 1, 0, insertion.Length); index.TotalRows(paged); CheckGreater(index.BlockCount, 5); } } public class CountMoreTests : TestFixture { public void CountsUntilTheEnd_ThenSaysThereIsNothingLeft() { var source = new StringSource(Repeat("aaaa bbbb cccc dddd\n", 40)); VisualLineIndex index = NewIndex(synchronousChars: 100); CheckFalse(index.IsExact(source)); CheckTrue(index.CountMore(source, 50)); CountAll(index, source); CheckTrue(index.IsExact(source)); CheckFalse(index.CountMore(source, 50)); } public void CountsAtLeastOneBlock_AndStopsOnceTheBudgetIsUsed() { var source = new StringSource(Repeat("aaaa bbbb cccc dddd\n", 40)); VisualLineIndex index = NewIndex(blockChars: 16, synchronousChars: 100); int before = index.CountedChars(source); index.CountMore(source, 1); int after = index.CountedChars(source); CheckGreater(after, before); CheckGreater(60, after - before); } public void ABudgetOfZero_CountsNothing() { var source = new StringSource(Repeat("aaaa bbbb cccc dddd\n", 40)); VisualLineIndex index = NewIndex(synchronousChars: 100); CheckFalse(index.CountMore(source, 0)); CheckEqual(0, index.CountedChars(source)); } public void AnEmptyText_IsCountedByTheFirstQuestion() { var source = new StringSource(""); VisualLineIndex index = NewIndex(); CheckTrue(index.IsExact(source)); CheckFalse(index.CountMore(source, 100)); } public void TheWholeCountingEndsExactlyLikeAFullLayout() { string text = Repeat("aaaa bbbb cccc dddd\nab\n\ncd ef gh ij kl mn\r\n", 25); var source = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(synchronousChars: 100); CountAll(index, source); CheckEqual("", Differences(index, source, text, Wide, indexStep: 3)); } public void ATextThatFitsTheSynchronousLimit_IsCountedWithoutBeingAsked() { var source = new StringSource(Repeat("aaaa bbbb cccc dddd\n", 5)); VisualLineIndex index = NewIndex(synchronousChars: 100); CheckTrue(index.IsExact(source)); } } public class IsExactTests : TestFixture { public void ASmallText_IsExactAtOnce() { CheckTrue(NewIndex().IsExact(new StringSource(Corpus[10]))); } public void ALongLine_IsExact_WhenItWraps() { var source = new StringSource(Repeat("abcd ", 30)); VisualLineIndex index = NewIndex(longLineChars: 48); CountAll(index, source); CheckTrue(index.IsExact(source)); } public void ALongLine_IsNeverExact_WhenItDoesNotWrap() { var source = new StringSource(Repeat("abcd ", 30)); VisualLineIndex index = NewIndex(int.MaxValue, longLineChars: 48); CountAll(index, source); CheckFalse(index.IsExact(source)); } public void ALongLine_IsNeverExact_WhenItsWidthIsKept() { var source = new StringSource(Repeat("abcd ", 30)); VisualLineIndex index = NewIndex(measureWidths: true, longLineChars: 48); CountAll(index, source); CheckFalse(index.IsExact(source)); } } public class CountedCharsTests : TestFixture { public void IsTheWholeTextOnceCounted() { var source = new StringSource(Corpus[10]); CheckEqual(Corpus[10].Length, NewIndex().CountedChars(source)); } public void GrowsWithEachSliceCounted() { var source = new StringSource(Repeat("aaaa bbbb cccc dddd\n", 40)); VisualLineIndex index = NewIndex(synchronousChars: 100); int previous = index.CountedChars(source); while (index.CountMore(source, 30)) { int counted = index.CountedChars(source); CheckGreater(counted, previous); previous = counted; } CheckEqual(source.Length, previous); } } public class CountedRowsTests : TestFixture { public void OfACountedText_IsTheTotal() { var source = new StringSource(Corpus[10]); VisualLineIndex index = NewIndex(); CheckEqual(index.TotalRows(source), index.CountedRows(source)); } public void OfAPartlyCountedText_GrowsToTheTotalWithEachSliceCounted() { var source = new StringSource(Repeat("aaaa bbbb cccc dddd\n", 40)); VisualLineIndex index = NewIndex(synchronousChars: 100); int previous = index.CountedRows(source); CheckGreater(index.TotalRows(source) + 1, previous + 1); while (index.CountMore(source, 30)) { int counted = index.CountedRows(source); CheckGreater(counted, previous); previous = counted; } CheckEqual(index.TotalRows(source), previous); } public void OfAnEmptyText_IsItsOneLine() { CheckEqual(1, NewIndex().CountedRows(new StringSource(""))); } } public class LongLinesTests : TestFixture { public static string LongLine(int words) { return Repeat("abcd efgh ", words); } public void WhenTheirWidthIsKept_AreEstimatedFromASample_NearTheRealCount() { string text = "ab\n" + LongLine(1000) + "\ncd"; var source = new StringSource(text); VisualLineIndex index = NewIndex(measureWidths: true, longLineChars: 48); CountAll(index, source); int real = new FullLayout(text, Wide).TotalRows; CheckGreater(index.TotalRows(source), real - 3); CheckGreater(real + 3, index.TotalRows(source)); CheckFalse(index.IsExact(source)); } public void WhenTheirWidthIsKept_AndCounted_PlaceAnIndexAlongTheirRows() { string text = "ab\n" + LongLine(1000) + "\ncd"; var source = new StringSource(text); var layout = new FullLayout(text, Wide); VisualLineIndex index = NewIndex(measureWidths: true, longLineChars: 48); CountAll(index, source); int middle = 3 + 5000; CheckTrue(Math.Abs(layout.RowOf(middle, false) - index.RowOf(source, middle, false)) <= 2); } public void WhenTheirWidthIsKept_AndCounted_LeadARowToAnIndexAlongTheLine() { string text = "ab\n" + LongLine(1000) + "\ncd"; var source = new StringSource(text); var layout = new FullLayout(text, Wide); VisualLineIndex index = NewIndex(measureWidths: true, longLineChars: 48); CountAll(index, source); int row = layout.RowOf(3 + 5000, false); CheckTrue(Math.Abs(layout.IndexOfRow(row) - index.IndexOfRow(source, row)) <= 40); } public void WhenNotYetCounted_PlaceAnIndexAlongTheirRows() { string text = Repeat("ab\n", 60) + LongLine(1000) + "\ncd"; var source = new StringSource(text); var layout = new FullLayout(text, Wide); VisualLineIndex index = NewIndex(synchronousChars: 100); int middle = 180 + 5000; CheckEqual(0, index.CountedChars(source)); CheckTrue(Math.Abs(layout.RowOf(middle, false) - index.RowOf(source, middle, false)) <= 2); } public void WithoutWrapping_AreOneRowEach() { string text = "ab\n" + LongLine(1000) + "\ncd"; var source = new StringSource(text); VisualLineIndex index = NewIndex(fitWidth: int.MaxValue, longLineChars: 48); CountAll(index, source); CheckEqual(3, index.TotalRows(source)); } public void LeaveTheLinesAroundThemExact() { string text = "ab cd\nef gh ij kl mn op qr st uv wx\n" + LongLine(1000) + "\nyz\naaaa bbbb cccc dddd eeee\n"; var source = new StringSource(text); var layout = new FullLayout(text, Wide); VisualLineIndex index = NewIndex(longLineChars: 48); CountAll(index, source); CheckEqual(layout.RowOf(0, false), index.RowOf(source, 0, false)); CheckEqual(layout.RowOf(10, false), index.RowOf(source, 10, false)); int after = text.IndexOf("yz", StringComparison.Ordinal); CheckGreater(index.RowOf(source, after, false), layout.RowOf(after, false) - 3); CheckGreater(layout.RowOf(after, false) + 3, index.RowOf(source, after, false)); CheckEqual(layout.RowOf(text.Length, false) - layout.RowOf(after, false), index.RowOf(source, text.Length, false) - index.RowOf(source, after, false)); } public void AnEditInside_KeepsTheCountsOfTheLinesAfterIt() { string text = "ab\n" + LongLine(1000) + "\n" + Repeat("aaaa bbbb cccc dddd\n", 40); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(longLineChars: 48, synchronousChars: 100000); CountAll(index, paged); int counted = index.CountedChars(paged); paged.Replace(500, 0, "xyz"); index.NoteEdit(paged, 500, 0, 3); CheckEqual(counted + 3, index.CountedChars(paged)); } public void AnEditInsideOneWhoseWidthIsKept_KeepsTheCountsOfTheLinesAfterIt() { string text = "ab\n" + LongLine(1000) + "\n" + Repeat("aaaa bbbb cccc dddd\n", 40); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(measureWidths: true, longLineChars: 48, synchronousChars: 100000); CountAll(index, paged); int counted = index.CountedChars(paged); paged.Replace(500, 0, "xyz"); index.NoteEdit(paged, 500, 0, 3); CheckEqual(counted + 3, index.CountedChars(paged)); } public void AnEditThatShortensOneWhoseWidthIsKept_CountsItExactly() { string text = "ab\n" + LongLine(1000) + "\ncd"; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(measureWidths: true, longLineChars: 48); CountAll(index, paged); paged.Replace(3, LongLine(1000).Length - 5, ""); index.NoteEdit(paged, 3, LongLine(1000).Length - 5, 0); string edited = text.Remove(3, LongLine(1000).Length - 5); CheckEqual("", Differences(index, paged, edited, Wide)); CheckTrue(index.IsExact(paged)); } public void AnEditThatShortensOne_CountsItExactly() { string text = "ab\n" + LongLine(1000) + "\ncd"; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(longLineChars: 48); CountAll(index, paged); paged.Replace(3, LongLine(1000).Length - 5, ""); index.NoteEdit(paged, 3, LongLine(1000).Length - 5, 0); string edited = text.Remove(3, LongLine(1000).Length - 5); CheckEqual("", Differences(index, paged, edited, Wide)); CheckTrue(index.IsExact(paged)); } public void ABlockStartingWithShortLines_EndsBeforeALongOne() { string text = Repeat("ab\n", 4) + LongLine(30) + "\n"; var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 48); index.CountMore(source, 1); CheckEqual(12, index.CountedChars(source)); } } public class ChunkedLinesTests : TestFixture { public static readonly string Prose = Repeat("abcd efgh ", 14); public static readonly string OneLongWord = Repeat("abcdefghij", 14); public static readonly string[] Texts = { Repeat("abcd efgh ", 30), Repeat("abcdefghij", 30), Repeat("ab cd ", 20), Repeat("a b c d e ", 30), "ab\ncd ef gh\n" + Repeat("abcd efgh ", 20) + "\nij kl\r\nmn\n\n" + Repeat("abcdefghijk ", 12), Repeat("abcd efgh ", 20) + "\n", "\n" + Repeat("abcd efgh ", 20), Repeat("abcd" + (char)0xAD + "efgh ", 20), Repeat("abcd-efgh ", 20), Repeat("" + (char)0x4F60 + (char)0x597D, 60), Repeat("abcd " + char.ConvertFromUtf32(0x20BB7) + "efgh ", 20), Repeat("a" + (char)0x301 + "bcd e" + (char)0x301 + "fgh ", 20), Repeat("1,234.5 abc 6-7 ", 12), }; public static string WholeLineDifferences(string text, int fitWidth, int blockChars, TrailingSpaceMode trailingSpaces) { var source = new StringSource(text); VisualLineIndex index = NewIndex(fitWidth, blockChars: blockChars, longLineChars: blockChars, trailingSpaces: trailingSpaces); CountAll(index, source); return Differences(index, source, text, fitWidth, indexStep: 3, trailingSpaces); } public static void CheckEveryTextAt(int width, TrailingSpaceMode trailingSpaces) { foreach (string text in Texts) { CheckStatic(WholeLineDifferences(text, width, blockChars: 16, trailingSpaces).Length == 0, $"Different at {width}: {WholeLineDifferences(text, width, 16, trailingSpaces)}"); } } [Thorough] public void MatchAFullLayout_AtAWidthOf100() { CheckEveryTextAt(100, TrailingSpaceMode.Hang); } [Thorough] public void MatchAFullLayout_AtAWidthOf70() { CheckEveryTextAt(70, TrailingSpaceMode.Hang); } [Thorough] public void MatchAFullLayout_AtAWidthOf35() { CheckEveryTextAt(35, TrailingSpaceMode.Hang); } [Thorough] public void MatchAFullLayout_AtAWidthOf20() { CheckEveryTextAt(20, TrailingSpaceMode.Hang); } [Thorough] public void MatchAFullLayout_WithTrailingSpacesWrapping_AtAWidthOf100() { CheckEveryTextAt(100, TrailingSpaceMode.Wrap); } [Thorough] public void MatchAFullLayout_WithTrailingSpacesWrapping_AtAWidthOf70() { CheckEveryTextAt(70, TrailingSpaceMode.Wrap); } [Thorough] public void MatchAFullLayout_WithTrailingSpacesWrapping_AtAWidthOf35() { CheckEveryTextAt(35, TrailingSpaceMode.Wrap); } [Thorough] public void MatchAFullLayout_WithTrailingSpacesWrapping_AtAWidthOf20() { CheckEveryTextAt(20, TrailingSpaceMode.Wrap); } [Thorough] public void MatchAFullLayout_WithLargerBlocks() { foreach (string text in Texts) { CheckEqual("", WholeLineDifferences(text, Wide, blockChars: 80, TrailingSpaceMode.Hang)); } } public void AreExact_OnceCounted() { foreach (string text in Texts) { var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 16); CountAll(index, source); CheckTrue(index.IsExact(source)); } } public void ALineOfExactlyTheLongLineLength_IsCountedWhole() { var source = new StringSource(Repeat("abcd efgh ", 5).Substring(0, 48)); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, source); CheckEqual(1, index.BlockCount); } public void ALineOneLongerThanTheLongLineLength_IsCountedInChunks() { var source = new StringSource(Repeat("abcd efgh ", 5).Substring(0, 49)); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, source); CheckGreater(index.BlockCount, 1); } public void AreCountedInChunks() { var source = new StringSource(Repeat("abcd efgh ", 60)); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 16); CountAll(index, source); CheckGreater(index.BlockCount, 20); } public void LeaveTheLinesAroundThemAsTheyAre() { string text = "ab cd\nef gh ij kl mn op qr st uv wx\n" + Repeat("abcd efgh ", 60) + "\nyz\naaaa bbbb cccc dddd eeee\n"; CheckEqual("", WholeLineDifferences(text, Wide, blockChars: 16, TrailingSpaceMode.Hang)); } public void WhileCounting_AnEstimateOfTheRestOfTheLineGrowsTowardTheCount() { string text = Repeat("abcd efgh ", 2000); var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 48); int real = new FullLayout(text, Wide).TotalRows; index.CountMore(source, 200); CheckGreater(index.CountedChars(source), 199); CheckGreater(text.Length, index.CountedChars(source)); CheckGreater(real / 10 * 11, index.TotalRows(source)); CheckGreater(index.TotalRows(source), real / 10 * 9); } public void WhileCounting_ARowInTheUncountedRestOfTheLineIsPlacedAlongIt() { string text = Repeat("abcd efgh ", 2000); var source = new StringSource(text); var layout = new FullLayout(text, Wide); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 48); index.CountMore(source, 200); int middle = text.Length / 2; CheckTrue(Math.Abs(layout.RowOf(middle, false) - index.RowOf(source, middle, false)) <= 3); CheckTrue(Math.Abs(layout.IndexOfRow(layout.RowOf(middle, false)) - index.IndexOfRow(source, layout.RowOf(middle, false))) <= 60); } public void WhileCounting_LinesAfterTheOneBeingCountedAreEstimated() { string text = Repeat("abcd efgh ", 200) + "\n" + Repeat("ab cd\n", 50); var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 48); index.CountMore(source, 200); CheckGreater(index.TotalRows(source), new FullLayout(text, Wide).TotalRows / 2); CheckGreater(index.RowOf(source, text.Length, false), index.RowOf(source, 1000, false)); } public void ARunWithoutFirmRowStarts_ShorterThanALongLine_ReachesTheNextChunk() { string text = Repeat("abcd efgh ", 3) + Repeat("!", 100) + Repeat("abcd efgh ", 20); var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 300); CountAll(index, source); CheckEqual("", Differences(index, source, text, Wide)); CheckTrue(index.IsExact(source)); } public void ARunWithoutFirmRowStarts_ToTheEndOfTheLine_IsTheLastChunk() { string text = Repeat("abcd efgh ", 31) + Repeat("!", 50); var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 300); CountAll(index, source); CheckEqual("", Differences(index, source, text, Wide)); CheckTrue(index.IsExact(source)); } public void WithoutAFirmRowStart_AreEstimated() { string text = Repeat("!!!!", 100); var source = new StringSource(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, source); CheckFalse(index.IsExact(source)); CheckGreater(new FullLayout(text, Wide).TotalRows + 3, index.TotalRows(source)); CheckGreater(index.TotalRows(source), new FullLayout(text, Wide).TotalRows - 3); } public static string EditDifferences(string text, int position, int removed, string inserted, int fitWidth = Wide, int blockChars = 16) { PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(fitWidth, blockChars: blockChars, longLineChars: blockChars); CountAll(index, paged); paged.Replace(position, removed, inserted); index.NoteEdit(paged, position, removed, inserted.Length); return Differences(index, paged, text.Remove(position, removed).Insert(position, inserted), fitWidth, indexStep: 5); } public static string FirstEditDifference(string text, int removed, string inserted, int fitWidth = Wide) { for (int position = 0; position + removed <= text.Length; ++position) { string difference = EditDifferences(text, position, removed, inserted, fitWidth); if (difference.Length > 0) { return $"At {position}: {difference}"; } } return ""; } [Thorough] public void InsertingAWord_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, " xyz")); } [Thorough] public void InsertingALongWord_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, " xyzxyzxyzxyzxyzxyzxyzxyzxyzxyz ")); } [Thorough] public void InsertingAHardBreak_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, "\n")); } [Thorough] public void DeletingSomeChars_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 3, "")); } [Thorough] public void ReplacingAWord_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 4, "wxyz")); } [Thorough] public void EditingALongWord_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(OneLongWord, removed: 1, "xy")); } public void EditingNarrowRows_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, "ab ", fitWidth: 35)); } public void DeletingAHardBreakThatJoinsTwoLines_MatchesAFullLayout() { string text = Prose + "\n" + Prose.Substring(0, 60); int breakAt = text.IndexOf('\n'); CheckEqual("", EditDifferences(text, breakAt, 1, "")); } public void AnEditInside_KeepsTheCountsOfTheChunksAfterIt() { string text = Repeat("abcd efgh ", 400); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 100000); CountAll(index, paged); paged.Replace(2000, 0, "xyz"); index.NoteEdit(paged, 2000, 0, 3); CheckEqual(text.Length + 3, index.CountedChars(paged)); } public void AnEditAtTheEndOfTheCountedPart_CountsOnFromThere() { string text = Repeat("abcd efgh ", 400); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 48); index.CountMore(paged, 300); int counted = index.CountedChars(paged); paged.Replace(counted, 0, "xyz"); index.NoteEdit(paged, counted, 0, 3); CountAll(index, paged); CheckEqual("", Differences(index, paged, text.Insert(counted, "xyz"), Wide, indexStep: 7)); } public void AnEditThatMovesEveryLaterRow_CountsOnToTheEnd() { string text = Repeat("abcd efgh ", 100); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 100000); CountAll(index, paged); paged.Replace(0, 0, "xx"); index.NoteEdit(paged, 0, 0, 2); CheckEqual(text.Length + 2, index.CountedChars(paged)); CheckEqual("", Differences(index, paged, "xx" + text, Wide, indexStep: 13)); } public void AnEditThatMovesEveryLaterRow_DropsTheCountsAfterItOncePastTheLimit() { string text = Repeat("abcd efgh ", 100); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 100); CountAll(index, paged); paged.Replace(0, 0, "xx"); index.NoteEdit(paged, 0, 0, 2); CheckGreater(text.Length, index.CountedChars(paged)); CountAll(index, paged); CheckEqual("", Differences(index, paged, "xx" + text, Wide, indexStep: 13)); } public void AnEditTooBigToRecountAtOnce_DropsTheCountsAfterIt() { string text = Repeat("abcd efgh ", 400); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 100); CountAll(index, paged); paged.Replace(500, 0, Repeat("x", 300)); index.NoteEdit(paged, 500, 0, 300); CheckGreater(text.Length, index.CountedChars(paged)); CountAll(index, paged); CheckEqual("", Differences(index, paged, text.Insert(500, Repeat("x", 300)), Wide, indexStep: 11)); } } public class WidthChunkedLinesTests : TestFixture { public const int WideGlyph = 1000000; public static readonly SpriteFontDefinition Kerned = GlyphPlacerTests.KernedFont; public static readonly SpriteFontDefinition WideFont = BuildWideFont(); public static readonly SpriteFontDefinition TightFont = BuildTightFont(); public static readonly string Pair = char.ConvertFromUtf32(0x20BB7); public static readonly string Tag = char.ConvertFromUtf32(0xE0001); public static readonly string[] SpacedTexts = { Repeat("ab (a)-b. ", 24), Repeat("abababab", 30), Repeat("a(b)", 50), Repeat("ab (b) ", 16), " " + Repeat("ab ", 30) + " ", "ab\n" + Repeat("ab (a) ", 24) + "\n\n" + Repeat("a.b-", 30) + "\r\n" + "xyz", Repeat(" ", 80), "abc" + Repeat(" ", 80), "\n" + Repeat("ab (a)", 30), Repeat("ab (a)", 30) + "\n", }; public static readonly string[] OddTexts = { Repeat("ab" + (char)0xAD + "ab ", 20), Repeat("ab" + (char)0xAD, 40), Repeat("ab ", 20) + "a" + (char)0xAD, Repeat("ab " + Pair + " ba ", 20), Repeat("a" + Pair, 30), Repeat("ab" + Pair, 30), Repeat("ab" + Tag + "ba ", 20), Repeat("a" + (char)0x301 + "b ", 30), Repeat("" + (char)0x4F60 + (char)0x597D, 40), }; public static readonly string Prose = Repeat("ab (a)-b. ", 7); /// /// Text of many different pairs of letters, with a space after every few of them. /// public static string Varied(int length) { var returnMe = new StringBuilder(); for (int i = 0; i < length; ++i) { returnMe.Append(i % 7 == 6 ? ' ' : (char)('a' + (i * 7 + i / 5 * 3 + i * i % 11) % 26)); } return returnMe.ToString(); } public static SpriteFontDefinition BuildTightFont() { var glyphs = new Dictionary(); var kernings = new Dictionary<(short, short), short>(); foreach (char left in "ab ") { glyphs[left] = new GlyphDefinition { Width = 1 }; foreach (char right in "ab ") { kernings[WordMeasurerTests.Pair(left, right)] = (short)((left * 5 + right * 3) % 9 - 2); } } return new SpriteFontDefinition(new SpriteFontDefinition.Data { GlyphDefinitions = glyphs, InvalidGlyph = new GlyphDefinition { Width = WordMeasurerTests.InvalidGlyphWidth }, Kernings = kernings, Baseline = 8, LineHeight = GlyphPlacerTests.LineSpacing, }); } public static SpriteFontDefinition BuildWideFont(int glyphWidth = WideGlyph) { var glyphs = new Dictionary(); for (char letter = 'a'; letter <= 'z'; ++letter) { glyphs[letter] = new GlyphDefinition { Width = glyphWidth }; } glyphs[' '] = new GlyphDefinition { Width = glyphWidth }; return new SpriteFontDefinition(new SpriteFontDefinition.Data { GlyphDefinitions = glyphs, InvalidGlyph = new GlyphDefinition { Width = WordMeasurerTests.InvalidGlyphWidth }, Kernings = new Dictionary<(short, short), short>(), Baseline = 8, LineHeight = GlyphPlacerTests.LineSpacing, }); } public static VisualLineIndex NewWidthIndex(TrailingSpaceMode trailingSpaces, int blockChars = 16, int? longLineChars = null, SpriteFontDefinition font = null, int synchronousChars = 100000) { return NewIndex(int.MaxValue, measureWidths: true, blockChars, longLineChars ?? blockChars, synchronousChars, trailingSpaces, font ?? Kerned); } /// /// Describes every width the index answers differently from a full layout of the text, or nothing when it /// agrees on all of them. /// public static string WidthDifferences(VisualLineIndex index, ITextSource source, string text, TrailingSpaceMode trailingSpaces, int indexStep, SpriteFontDefinition font = null) { var layout = new FullLayout(text, int.MaxValue, trailingSpaces, font ?? Kerned); var returnMe = new StringBuilder(); if (index.WidestLine(source) != layout.WidestLine) { returnMe.Append($"WidestLine {index.WidestLine(source)} not {layout.WidestLine}; "); } for (int i = 0; i <= text.Length && returnMe.Length < 300; i += indexStep) { if (layout.IsInsideCrLf(i)) { continue; } int x = layout.XOf(i); if (index.WidthBefore(source, i) != x) { returnMe.Append($"WidthBefore({i}) {index.WidthBefore(source, i)} not {x}; "); } foreach (int nudge in new[] { -3, -1, 0, 1, 4 }) { // Stops that lie at the same place are equally near, and either may come back. int probe = Math.Max(0, x + nudge); int found = index.IndexAtWidth(source, i, probe); int expected = layout.IndexAtX(i, probe); if (Math.Abs(layout.XOf(found) - probe) != Math.Abs(layout.XOf(expected) - probe)) { returnMe.Append($"IndexAtWidth({i},{probe}) {found} not {expected}; "); } } } return returnMe.ToString(); } public static void CheckEveryText(string[] texts, TrailingSpaceMode trailingSpaces, int blockChars = 16) { foreach (string text in texts) { var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(trailingSpaces, blockChars); CountAll(index, source); string difference = WidthDifferences(index, source, text, trailingSpaces, indexStep: 5); CheckStatic(difference.Length == 0, $"Different for {text.Length} chars: {difference}"); } } [Thorough] public void SpacedTexts_MatchAFullLayout() { CheckEveryText(SpacedTexts, TrailingSpaceMode.Hang); } public void SpacedTexts_MatchAFullLayout_WithTrailingSpacesWrapping() { CheckEveryText(SpacedTexts, TrailingSpaceMode.Wrap); } public void OddTexts_MatchAFullLayout() { CheckEveryText(OddTexts, TrailingSpaceMode.Hang); } [Thorough] public void OddTexts_MatchAFullLayout_WithTrailingSpacesWrapping() { CheckEveryText(OddTexts, TrailingSpaceMode.Wrap); } public void SpacedTexts_MatchAFullLayout_WithLargerChunks() { CheckEveryText(SpacedTexts, TrailingSpaceMode.Hang, blockChars: 80); } [Thorough] public void ShortLines_MatchAFullLayout() { foreach (string text in SpacedTexts) { var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 1000); CheckEqual("", WidthDifferences(index, source, text, TrailingSpaceMode.Hang, indexStep: 5)); } } public static void CheckCutsAtEveryBlockSize(string text) { for (int blockChars = 4; blockChars < 10; ++blockChars) { var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars); CountAll(index, source); string difference = WidthDifferences(index, source, text, TrailingSpaceMode.Hang, indexStep: 1); CheckStatic(difference.Length == 0, $"Different at {blockChars}: {difference}"); } } public void PairsOfSurrogates_MatchAFullLayout_AtEveryBlockSize() { CheckCutsAtEveryBlockSize(Repeat(Pair, 12) + "a"); CheckCutsAtEveryBlockSize("a" + Repeat(Pair, 12)); } public void LoneSurrogates_MatchAFullLayout_AtEveryBlockSize() { CheckCutsAtEveryBlockSize(Repeat("ab", 8) + (char)0xD800); CheckCutsAtEveryBlockSize(Repeat("ab" + (char)0xD800, 6)); CheckCutsAtEveryBlockSize(Repeat("ab" + (char)0xDC00, 6)); } public void KerningWiderThanTheGlyphs_MatchesAFullLayoutNearEveryCut() { foreach (string text in new[] { Repeat("ba", 60), Repeat("ba b", 30), Repeat("aab ", 30) }) { var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8, font: TightFont); CountAll(index, source); string difference = WidthDifferences(index, source, text, TrailingSpaceMode.Hang, indexStep: 1, TightFont); CheckStatic(difference.Length == 0, $"Different for {text.Length} chars: {difference}"); } } public void AreCountedInChunks() { var source = new StringSource(Varied(1000)); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CountAll(index, source); CheckGreater(index.BlockCount, 20); CheckGreater(1000 / 16, index.BlockCount); CheckTrue(index.IsExact(source)); } public void LeaveTheLinesAroundThemAsTheyAre() { string text = "ab (a)\nb.a b.a b.a b.a\n" + Repeat("ab (a)", 60) + "\nyz\na.b-a.b-a.b-a.b-\n"; var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CountAll(index, source); CheckEqual("", WidthDifferences(index, source, text, TrailingSpaceMode.Hang, indexStep: 3)); } public void CountLinesAndRowsLikeAFullLayout() { foreach (string text in SpacedTexts) { var source = new StringSource(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 16); CountAll(index, source); CheckEqual("", Differences(index, source, text, int.MaxValue, indexStep: 5)); } } public void WhileCounting_AWidthIsOnlyWhatIsCounted() { string text = Repeat("ab (a)", 400); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 200); long early = index.WidestLine(source); CountAll(index, source); CheckGreater(early, 0L); CheckGreater(index.WidestLine(source), early); CheckEqual(new FullLayout(text, int.MaxValue, TrailingSpaceMode.Hang, Kerned).WidestLine, index.WidestLine(source)); } public void WhileCounting_TheOpenLineStaysOneRow() { string text = Repeat("ab (a)", 400) + "\nabc\ndef\nghi"; var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 200); CheckGreater(text.IndexOf('\n'), index.CountedChars(source)); CheckEqual(4, index.TotalRows(source)); CheckEqual(0, index.RowOf(source, 1000, false)); CheckEqual(3, index.RowOf(source, text.Length, false)); } public void WidthBefore_CountsOnToTheIndex() { string text = Repeat("ab (a)", 400); var source = new StringSource(text); var layout = new FullLayout(text, int.MaxValue, TrailingSpaceMode.Hang, Kerned); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 200); CheckEqual(layout.XOf(2100), index.WidthBefore(source, 2100)); CheckGreater(index.CountedChars(source), 2100); CheckGreater(text.Length, index.CountedChars(source) + 100); } public void IndexAtWidth_CountsOnToTheWidth() { string text = Repeat("ab (a)", 400); var source = new StringSource(text); var layout = new FullLayout(text, int.MaxValue, TrailingSpaceMode.Hang, Kerned); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 200); int probe = layout.XOf(2100); CheckEqual(layout.IndexAtX(0, probe), index.IndexAtWidth(source, 0, probe)); CheckGreater(index.CountedChars(source), 2100); CheckGreater(text.Length, index.CountedChars(source) + 100); } public void IndexAtWidth_ToTheLeftOfALine_IsItsStart() { string text = "ab\n" + Repeat("ab (a)", 60); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CheckEqual(3, index.IndexAtWidth(source, 100, -5)); CheckEqual(0, index.IndexAtWidth(source, 1, -5)); } public void IndexAtWidth_ToTheRightOfALine_IsItsEnd() { string text = "ab\n" + Repeat("ab (a)", 60) + "\nxy"; var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CheckEqual(text.Length - 3, index.IndexAtWidth(source, 100, 1000000)); CheckEqual(2, index.IndexAtWidth(source, 1, 1000000)); } public void AWidthPastAnInt_IsKept() { string text = Repeat("ab", 3000); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 64, font: WideFont); CountAll(index, source); CheckEqual(6000L * WideGlyph, index.WidestLine(source)); CheckEqual(5000L * WideGlyph, index.WidthBefore(source, 5000)); CheckEqual(4000, index.IndexAtWidth(source, 0, 4000L * WideGlyph + WideGlyph / 4)); } public void ARunOfIgnorablesLongerThanALongLine_StillCounts() { string text = Repeat("ab(a", 8) + Repeat(Tag, 100) + Repeat("ab(a", 8); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, font: Font); CountAll(index, source); CheckTrue(index.IsExact(source)); CheckEqual("", WidthDifferencesWith(Font, index, source, text)); } public void ARunOfIgnorablesThatStartsMidChunk_NeverCutsAPair() { string text = Repeat("ab(a", 8) + "x" + Repeat(Tag, 100) + Repeat("ab(a", 8); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, font: Font); CountAll(index, source); CheckEqual("", WidthDifferencesWith(Font, index, source, text)); } public void ARunOfIgnorablesLongerThanALongLine_IsOffByNoMoreThanAKerning() { string text = Repeat("ab(a", 8) + Repeat(Tag, 100) + Repeat("ab(a", 8); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CountAll(index, source); long real = new FullLayout(text, int.MaxValue, TrailingSpaceMode.Hang, Kerned).WidestLine; int kerningLimit = 2; int fallbackCuts = Repeat(Tag, 100).Length / 16 + 1; CheckTrue(Math.Abs(index.WidestLine(source) - real) <= kerningLimit * fallbackCuts); } private static string WidthDifferencesWith(SpriteFontDefinition font, VisualLineIndex index, ITextSource source, string text) { var layout = new FullLayout(text, int.MaxValue, TrailingSpaceMode.Hang, font); if (index.WidestLine(source) != layout.WidestLine) { return "WidestLine"; } for (int i = 0; i <= text.Length; i += 3) { if (index.WidthBefore(source, i) != layout.XOf(i)) { return $"WidthBefore({i})"; } } return ""; } public static string EditDifferences(string text, int position, int removed, string inserted, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang, int longLineChars = 8) { PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(trailingSpaces, blockChars: 8, longLineChars); CountAll(index, paged); paged.Replace(position, removed, inserted); index.NoteEdit(paged, position, removed, inserted.Length); return WidthDifferences(index, paged, text.Remove(position, removed).Insert(position, inserted), trailingSpaces, indexStep: 9); } public static string FirstEditDifference(string text, int removed, string inserted, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang) { for (int position = 0; position + removed <= text.Length; ++position) { string difference = EditDifferences(text, position, removed, inserted, trailingSpaces); if (difference.Length > 0) { return $"At {position}: {difference}"; } } return ""; } [Thorough] public void InsertingAChar_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, "b")); } [Thorough] public void InsertingAWord_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, " a.b ")); } public void InsertingAHardBreak_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 0, "\n")); } [Thorough] public void DeletingSomeChars_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 3, "")); } [Thorough] public void ReplacingAChar_MatchesAFullLayout() { CheckEqual("", FirstEditDifference(Prose, removed: 1, "b")); } public void ReplacingSomeChars_MatchesAFullLayout_WithTrailingSpacesWrapping() { CheckEqual("", FirstEditDifference(Prose, removed: 4, "-a ", TrailingSpaceMode.Wrap)); } public void DeletingAHardBreakThatJoinsTwoLines_MatchesAFullLayout() { string text = Prose + "\n" + Prose.Substring(0, 40); CheckEqual("", EditDifferences(text, text.IndexOf('\n'), 1, "")); } public void ShorteningALineBelowTheLongLength_MatchesAFullLayout() { CheckEqual("", EditDifferences(Prose, 6, Prose.Length - 12, "", longLineChars: 40)); } public void LengtheningALineAboveTheLongLength_MatchesAFullLayout() { CheckEqual("", EditDifferences("ab (a)\nab", 8, 0, Prose + Prose, longLineChars: 40)); } public void AnEditAtTheStartTooBigToRecountAtOnce_DropsEverythingCounted() { string text = Repeat("ab (a)", 80); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8, longLineChars: 24, synchronousChars: 24); CountAll(index, paged); paged.Replace(0, 0, Repeat("x", 40)); index.NoteEdit(paged, 0, 0, 40); CheckGreater(text.Length, index.CountedChars(paged)); CountAll(index, paged); CheckEqual("", WidthDifferences(index, paged, Repeat("x", 40) + text, TrailingSpaceMode.Hang, indexStep: 11)); } public void TheEmptyLineAfterABreakThatEndsTheText_IsOnItsOwnRow() { string text = Repeat("ab (a)", 60) + "\n"; var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CountAll(index, source); CheckEqual(0, index.RowOf(source, text.Length - 1, false)); CheckEqual(1, index.RowOf(source, text.Length, false)); CheckEqual(text.Length, index.IndexOfRow(source, 1)); CheckEqual(2, index.TotalRows(source)); } public void IndexAtWidth_OnALaterLine_CountsOnToItsStart() { string line = Repeat("ab (a)", 100); string text = line + "\n" + line; var source = new StringSource(text); var layout = new FullLayout(text, int.MaxValue, TrailingSpaceMode.Hang, Kerned); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 100); int inSecondLine = line.Length + 1 + 200; CheckEqual(layout.IndexAtX(inSecondLine, 500), index.IndexAtWidth(source, inSecondLine, 500)); CheckGreater(index.CountedChars(source), line.Length + 1); } public void AnEditInTheMiddleOfALongLine_KeepsTheChunksFarAfterIt() { string text = Varied(1000); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8, longLineChars: 24, synchronousChars: 100); CountAll(index, paged); paged.Replace(500, 0, "x"); index.NoteEdit(paged, 500, 0, 1); CheckEqual(text.Length + 1, index.CountedChars(paged)); CheckEqual("", WidthDifferences(index, paged, text.Insert(500, "x"), TrailingSpaceMode.Hang, indexStep: 19)); } public void AnEditInRepeatedText_CountsTheRestAgain() { string text = Repeat("a", 3000); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 16, longLineChars: 48, synchronousChars: 100); CountAll(index, paged); paged.Replace(1500, 0, "x"); index.NoteEdit(paged, 1500, 0, 1); CountAll(index, paged); CheckEqual("", WidthDifferences(index, paged, text.Insert(1500, "x"), TrailingSpaceMode.Hang, indexStep: 97)); } public void AnEditTooBigToRecountAtOnce_DropsTheCountsAfterIt() { string text = Repeat("ab (a)", 80); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8, longLineChars: 24, synchronousChars: 24); CountAll(index, paged); paged.Replace(100, 0, Repeat("x", 40)); index.NoteEdit(paged, 100, 0, 40); CheckGreater(text.Length, index.CountedChars(paged)); CountAll(index, paged); CheckEqual("", WidthDifferences(index, paged, text.Insert(100, Repeat("x", 40)), TrailingSpaceMode.Hang, indexStep: 8)); } public void DeletingTheWidestLine_FallsToTheNextWidest() { string narrow = Repeat("ab(a", 20); string text = narrow + "\n" + Repeat("ab(a", 40) + "\n" + narrow; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8); long widest = index.WidestLine(paged); int start = narrow.Length + 1; paged.Replace(start, 160, ""); index.NoteEdit(paged, start, 160, 0); CheckGreater(widest, index.WidestLine(paged)); CheckEqual("", WidthDifferences(index, paged, narrow + "\n\n" + narrow, TrailingSpaceMode.Hang, 7)); } public void DeletingAWholeWidestLine_FromBetweenTwoShortOnes_FallsToTheNextWidest() { string wide = Repeat("ab(a", 40); string narrow = Repeat("ab(a", 20); string text = "ab\n" + wide + "\ncd\n" + narrow; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8); long widest = index.WidestLine(paged); int removed = text.IndexOf("cd"); paged.Replace(1, removed, ""); index.NoteEdit(paged, 1, removed, 0); CheckGreater(widest, index.WidestLine(paged)); CheckEqual("", WidthDifferences(index, paged, text.Remove(1, removed), TrailingSpaceMode.Hang, 7)); } public void ShorteningTheOnlyLongLine_NarrowsTheWidest() { string text = Repeat("ab(a", 40); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8); long widest = index.WidestLine(paged); paged.Replace(100, 40, ""); index.NoteEdit(paged, 100, 40, 0); CheckGreater(widest, index.WidestLine(paged)); CheckEqual("", WidthDifferences(index, paged, text.Remove(100, 40), TrailingSpaceMode.Hang, 7)); } public void AnEditToANarrowerLine_LeavesTheWidestAlone() { string narrow = Repeat("ab(a", 20); string text = Repeat("ab(a", 40) + "\n" + narrow; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8); long widest = index.WidestLine(paged); int at = text.Length - 10; paged.Replace(at, 2, ""); index.NoteEdit(paged, at, 2, 0); CheckEqual(widest, index.WidestLine(paged)); } public void AWiderLineTyped_RaisesTheWidest() { string text = Repeat("ab(a", 40) + "\n" + Repeat("ab(a", 20); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang, blockChars: 8); long widest = index.WidestLine(paged); paged.Replace(text.Length, 0, Repeat("ab(a", 40)); index.NoteEdit(paged, text.Length, 0, 160); CheckGreater(index.WidestLine(paged), widest); CheckEqual("", WidthDifferences(index, paged, text + Repeat("ab(a", 40), TrailingSpaceMode.Hang, 11)); } public void CheckpointAtOrBefore_IsTheLineStart() { string text = "ab\n" + Repeat("ab (a)", 60); var source = new StringSource(text); VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CheckEqual(3, index.CheckpointAtOrBefore(source, 300)); } public void WidthBefore_InTheMiddleOfACrLf_IsTheWidthOfTheLine() { var source = new StringSource("ab\r\ncd"); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(20, index.WidthBefore(source, 3)); } public void WidthBefore_RejectsLinesThatWrap() { VisualLineIndex index = NewIndex(Wide, measureWidths: true); CheckThrow(typeof(InvalidOperationException)); index.WidthBefore(new StringSource("ab"), 1); } public void WidthBefore_RejectsAnIndexThatKeepsNoWidths() { VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: false); CheckThrow(typeof(InvalidOperationException)); index.WidthBefore(new StringSource("ab"), 1); } public void WidthBefore_RejectsAnIndexOutsideTheText() { VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CheckThrow(typeof(ArgumentOutOfRangeException)); index.WidthBefore(new StringSource("ab"), 3); } public void IndexAtWidth_RejectsLinesThatWrap() { VisualLineIndex index = NewIndex(Wide, measureWidths: true); CheckThrow(typeof(InvalidOperationException)); index.IndexAtWidth(new StringSource("ab"), 1, 5); } public void IndexAtWidth_RejectsAnIndexOutsideTheText() { VisualLineIndex index = NewWidthIndex(TrailingSpaceMode.Hang); CheckThrow(typeof(ArgumentOutOfRangeException)); index.IndexAtWidth(new StringSource("ab"), -1, 5); } } public class IsWidthChunkedTests : TestFixture { public void ALineOverTheLongLength_ThatDoesNotWrap_Is() { CheckTrue(NewIndex(int.MaxValue, measureWidths: true, longLineChars: 48).IsWidthChunked(0, 49)); } public void ALineAtTheLongLength_Isnt() { CheckFalse(NewIndex(int.MaxValue, measureWidths: true, longLineChars: 48).IsWidthChunked(0, 48)); } public void ALineThatWraps_Isnt() { CheckFalse(NewIndex(Wide, measureWidths: true, longLineChars: 48).IsWidthChunked(0, 5000)); } public void WithoutMeasuringWidths_Isnt() { CheckFalse(NewIndex(int.MaxValue, measureWidths: false, longLineChars: 48).IsWidthChunked(0, 5000)); } public void BeforeConfigure_Throws() { CheckThrow(typeof(InvalidOperationException)); new VisualLineIndex().IsWidthChunked(0, 5000); } } public class IsChunkedTests : TestFixture { public void ALineOverTheLongLength_ThatWraps_Is() { CheckTrue(NewIndex(longLineChars: 48).IsChunked(lineStart: 10, lineEnd: 59)); } public void ALineAtTheLongLength_IsNot() { CheckFalse(NewIndex(longLineChars: 48).IsChunked(lineStart: 10, lineEnd: 58)); } public void ALineThatDoesNotWrap_IsNot() { CheckFalse(NewIndex(fitWidth: int.MaxValue, longLineChars: 48).IsChunked(0, 1000)); } public void ALineWhoseWidthIsKept_IsNot() { CheckFalse(NewIndex(measureWidths: true, longLineChars: 48).IsChunked(0, 1000)); } public void BeforeItIsConfigured_Throws() { CheckThrow(typeof(InvalidOperationException)); new VisualLineIndex().IsChunked(0, 1000); } } public class WidthStretchTests : TestFixture { public static readonly string WideLine = Repeat("abcd efgh ", 20); public static VisualLineIndex NewCountedIndex(ITextSource source, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang) { VisualLineIndex returnMe = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 48, trailingSpaces: trailingSpaces); CountAll(returnMe, source); return returnMe; } // The chunk starts of a line that starts at 0 and has no break. public static System.Collections.Generic.List ChunkStarts(VisualLineIndex index, ITextSource source) { var returnMe = new System.Collections.Generic.List { 0 }; while (true) { int edge = index.ChunkEdgeAtOrAfter(source, returnMe[returnMe.Count - 1] + 1); if (edge >= source.Length) { return returnMe; } returnMe.Add(edge); } } public void ChunkRunWidth_OfTheWholeLine_IsItsWidthPlusItsHangingSpaces() { var source = new StringSource(WideLine + " "); VisualLineIndex index = NewCountedIndex(source); long run = index.ChunkRunWidth(source, 0, source.Length); CheckEqual(index.EstimatedLineWidth(source, 0) + index.HangingWidth(source, 0, source.Length), run); } public void ChunkRunWidth_AddsUpAcrossChunkStarts() { var source = new StringSource(WideLine); VisualLineIndex index = NewCountedIndex(source); List starts = ChunkStarts(index, source); CheckGreater(starts.Count, 3); CheckEqual(index.ChunkRunWidth(source, 0, source.Length), index.ChunkRunWidth(source, 0, starts[2]) + index.ChunkRunWidth(source, starts[2], source.Length)); CheckEqual(0L, index.ChunkRunWidth(source, starts[1], starts[1])); } public void ChunkRunWidth_IncludesTheBreakThatEndsTheLastChunk() { var withBreak = new StringSource(WideLine + "\r\nab"); var alone = new StringSource(WideLine); CheckEqual(NewCountedIndex(alone).ChunkRunWidth(alone, 0, alone.Length), NewCountedIndex(withBreak).ChunkRunWidth(withBreak, 0, WideLine.Length + 2)); } public void StretchWidth_OfTheChunks_AddsUpToTheChunkRun() { var source = new StringSource(WideLine); VisualLineIndex index = NewCountedIndex(source); List starts = ChunkStarts(index, source); starts.Add(source.Length); long sum = 0; for (int i = 0; i + 1 < starts.Count; ++i) { sum += index.StretchWidth(source, starts[i], starts[i + 1], 0, source.Length); } CheckEqual(index.ChunkRunWidth(source, 0, source.Length), sum); } public void StretchWidth_OfAMergedRun_IsTheRunOfItsChunks() { var source = new StringSource(WideLine); VisualLineIndex index = NewCountedIndex(source); List starts = ChunkStarts(index, source); CheckEqual(index.ChunkRunWidth(source, starts[1], starts[3]), index.StretchWidth(source, starts[1], starts[3], 0, source.Length)); } public void WidthWithin_AddsToTheWidthBeforeTheStretch() { var source = new StringSource(WideLine); VisualLineIndex index = NewCountedIndex(source); List starts = ChunkStarts(index, source); for (int i = starts[2]; i < starts[3]; ++i) { CheckEqual(index.WidthBefore(source, i), index.WidthBefore(source, starts[2]) + index.WidthWithin(source, starts[2], starts[3], i)); } } public void StopWithin_IsTheNearestStopInTheStretch() { var source = new StringSource("abcdef"); VisualLineIndex index = NewCountedIndex(source); CheckEqual(2, index.StopWithin(source, 0, 6, 6, along: 24)); CheckEqual(3, index.StopWithin(source, 0, 6, 6, along: 26)); CheckEqual(6, index.StopWithin(source, 0, 6, 6, along: 600)); } public void StopWithin_FromAStretchThatEndsShortOfItsLine_NeverPassesTheEnd() { var source = new StringSource("abcdef"); VisualLineIndex index = NewCountedIndex(source); CheckEqual(3, index.StopWithin(source, 0, 3, 6, along: 600)); } public void HangingWidth_IsTheWidthOfTheSpacesAtTheEndOfALine() { var source = new StringSource("ab "); VisualLineIndex hanging = NewCountedIndex(source); VisualLineIndex wrapping = NewCountedIndex(source, TrailingSpaceMode.Wrap); CheckGreater(hanging.HangingWidth(source, 0, 5), 0L); CheckEqual(wrapping.EstimatedLineWidth(source, 0), hanging.EstimatedLineWidth(source, 0) + hanging.HangingWidth(source, 0, 5)); CheckEqual(0L, wrapping.HangingWidth(source, 0, 5)); CheckEqual(0L, hanging.HangingWidth(source, 0, 2)); } public void WholeLineWidth_IsTheWidthOfTheLineLaidOut() { var source = new StringSource("ab cd\nef ghij"); VisualLineIndex index = NewCountedIndex(source); CheckEqual(index.EstimatedLineWidth(source, 6), index.WholeLineWidth(source, 6, source.Length)); CheckEqual(index.EstimatedLineWidth(source, 0), index.WholeLineWidth(source, 0, 5)); } public void WidestLineExcept_LeavesOutTheChunkedLinesInTheRange() { string wider = Repeat("abcd efgh ", 30); string narrower = Repeat("abcd efgh ", 20); var source = new StringSource(wider + "\nab\n" + narrower); VisualLineIndex index = NewCountedIndex(source); int narrowerStart = wider.Length + 4; CheckEqual(index.EstimatedLineWidth(source, 0), index.WidestLineExcept(source, -1, -1)); CheckEqual(index.EstimatedLineWidth(source, narrowerStart), index.WidestLineExcept(source, 0, 1)); CheckEqual(index.EstimatedLineWidth(source, 0), index.WidestLineExcept(source, narrowerStart, narrowerStart + 1)); CheckEqual(20L, index.WidestLineExcept(source, 0, source.Length)); } public void EstimatedWidestLineExcept_LeavesOutTheLineStillBeingCounted() { string wider = Repeat("abcd efgh ", 30); string narrower = Repeat("abcd efgh ", 20); var source = new StringSource(narrower + "\n" + wider); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 300); long without = index.EstimatedWidestLineExcept(source, narrower.Length + 1, source.Length); CheckGreater(index.EstimatedWidestLine(source), without); CheckEqual(index.EstimatedWidestLineExcept(source, -1, -1), index.EstimatedWidestLine(source)); } } public class WidthChunkStartAtOrBeforeTests : TestFixture { public void OnAChunkedLine_IsTheStartOfTheChunkHoldingTheIndex() { var source = new StringSource("ab\n" + WidthStretchTests.WideLine); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 48); CountAll(index, source); for (int i = 3; i <= source.Length; ++i) { int start = index.WidthChunkStartAtOrBefore(source, i); CheckTrue(start >= 3 && start <= i); CheckTrue(i - start <= CheckpointAtOrBeforeTests.ChunkBound); CheckEqual(start, index.WidthChunkStartAtOrBefore(source, start)); } } public void OnALineThatWraps_IsTheStartOfTheLine() { var source = new StringSource("ab\n" + WidthStretchTests.WideLine); VisualLineIndex index = NewIndex(longLineChars: 48); CheckEqual(3, index.WidthChunkStartAtOrBefore(source, 100)); } public void OnAShortLine_IsTheStartOfTheLine() { var source = new StringSource("ab cd\nef gh"); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, longLineChars: 48); CheckEqual(6, index.WidthChunkStartAtOrBefore(source, 9)); } } public class ChunkEdgeAtOrAfterTests : TestFixture { public static string Differences(string text, int fitWidth = Wide) { PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(fitWidth, blockChars: 16, longLineChars: 48); CountAll(index, paged); for (int i = 0; i < text.Length; ++i) { int edge = index.ChunkEdgeAtOrAfter(paged, i); int lineEnd = paged.EndOfLine(i); if (edge < i || edge > lineEnd + paged.LengthAt(lineEnd)) { return $"Edge({i}) {edge} is outside {i}..the end of the line"; } if (edge < lineEnd + paged.LengthAt(lineEnd) && index.CheckpointAtOrBefore(paged, edge) != edge) { return $"Edge({i}) {edge} is not a chunk start"; } if (edge > i && index.CheckpointAtOrBefore(paged, edge - 1) > i) { return $"Edge({i}) {edge} skips a chunk start"; } } return ""; } public void OnAChunkedLine_IsTheIndexOrTheNextChunkStart() { CheckEqual("", Differences(Repeat("abcd efgh ", 60))); } public void OnAChunkedLine_OfOneLongWord_IsTheIndexOrTheNextChunkStart() { CheckEqual("", Differences(Repeat("abcdefghij", 50))); } public void OnAChunkedLine_AmongShortOnes_IsTheIndexOrTheNextChunkStart() { string text = "ab\n" + Repeat("abcd efgh ", 40) + "\ncd\n" + Repeat("ij kl mn ", 30) + "\nef"; CheckEqual("", Differences(text)); } public void InTheLastChunkOfALine_IsTheEndOfTheLineAndItsBreak() { string text = Repeat("abcd efgh ", 40) + "\r\nxy"; PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, paged); CheckEqual(402, index.ChunkEdgeAtOrAfter(paged, 399)); } public void OnAShortLine_IsTheEndOfTheLineAndItsBreak() { var source = new StringSource("ab cd\nef gh"); VisualLineIndex index = NewIndex(longLineChars: 48); CheckEqual(6, index.ChunkEdgeAtOrAfter(source, 2)); CheckEqual(11, index.ChunkEdgeAtOrAfter(source, 8)); } public void AtTheEndOfTheText_IsTheEnd() { var source = new StringSource(Repeat("abcd efgh ", 40)); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CheckEqual(400, index.ChunkEdgeAtOrAfter(source, 400)); } public void PastTheCountedPart_CountsUpToTheIndex() { string text = Repeat("abcd efgh ", 100); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 100); int edge = index.ChunkEdgeAtOrAfter(paged, 400); CheckTrue(edge >= 400 && edge < 400 + CheckpointAtOrBeforeTests.ChunkBound); CheckGreater(index.CountedChars(paged), 400); } public void InABlockWithoutFirmRowStarts_IsTheEndOfTheLine() { var source = new StringSource("ab\n" + Repeat("!!!!", 100)); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, source); CheckEqual(403, index.ChunkEdgeAtOrAfter(source, 300)); } } public class CheckpointAtOrBeforeTests : TestFixture { public const int ChunkBound = 64; public static string Differences(string text, int fitWidth = Wide) { PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(fitWidth, blockChars: 16, longLineChars: 48); CountAll(index, paged); var layout = new FullLayout(text, fitWidth); for (int i = 0; i <= text.Length; ++i) { int checkpoint = index.CheckpointAtOrBefore(paged, i); int lineStart = paged.StartOfLine(i); if (checkpoint > i || checkpoint < lineStart) { return $"Checkpoint({i}) {checkpoint} is outside {lineStart}..{i}"; } if (layout.IndexOfRow(layout.RowOf(checkpoint, atLineEnd: false)) != checkpoint) { return $"Checkpoint({i}) {checkpoint} is not a row start"; } if (i - checkpoint > ChunkBound) { return $"Checkpoint({i}) {checkpoint} is more than a chunk back"; } } return ""; } public void OnAShortLine_IsTheStartOfTheLine() { var source = new StringSource("ab cd\nef gh"); VisualLineIndex index = NewIndex(longLineChars: 48); CheckEqual(6, index.CheckpointAtOrBefore(source, 9)); } public void OnAChunkedLine_IsARowStartNoFurtherBackThanAChunk() { CheckEqual("", Differences(Repeat("abcd efgh ", 60))); } public static string ChunkStartsOf(string text, int fitWidth = Wide) { PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(fitWidth, blockChars: 16, longLineChars: 48); CountAll(index, paged); var starts = new SortedSet(); for (int i = 0; i <= text.Length; ++i) { starts.Add(index.CheckpointAtOrBefore(paged, i)); } return string.Join(",", starts); } public void OnAChunkedLine_StartsEachChunkAtTheFirstRowStartAtLeastABlockAfterTheLast() { CheckEqual("0,20,40,60,80,100", ChunkStartsOf(Repeat("abcd efgh ", 12))); } public void OnAChunkedLine_KeepsATailOfABlockAndAHalfInTheLastChunk() { CheckEqual("0,20,40,60,80,100", ChunkStartsOf(Repeat("abcd efgh ", 13).Substring(0, 124))); } public void OnAChunkedLine_SplitsATailLongerThanABlockAndAHalf() { CheckEqual("0,20,40,60,80,100,120", ChunkStartsOf(Repeat("abcd efgh ", 13).Substring(0, 125))); } public void OnAChunkedLine_OfOneLongWord_IsARowStartNoFurtherBackThanAChunk() { CheckEqual("", Differences(Repeat("abcdefghij", 50))); } public void OnAChunkedLine_AmongShortOnes_IsARowStartNoFurtherBackThanAChunk() { string text = "ab\n" + Repeat("abcd efgh ", 40) + "\ncd\n" + Repeat("ij kl mn ", 30) + "\nef"; CheckEqual("", Differences(text)); } public void OnAChunkedLine_AtANarrowWidth_IsARowStartNoFurtherBackThanAChunk() { CheckEqual("", Differences(Repeat("abcd efgh ", 60), fitWidth: 35)); } public void OnAChunkedLine_NeverLeavesTheLine() { string text = Repeat("abcd efgh ", 40) + "\n" + Repeat("abcd efgh ", 40); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, paged); CheckEqual(401, index.CheckpointAtOrBefore(paged, 401)); } public void PastTheCountedPart_CountsUpToTheIndex() { string text = Repeat("abcd efgh ", 100); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48, synchronousChars: 100); int checkpoint = index.CheckpointAtOrBefore(paged, 400); CheckTrue(checkpoint <= 400 && checkpoint > 400 - ChunkBound); CheckGreater(index.CountedChars(paged), 400); CheckFalse(index.IsExact(paged)); } public void OnALineThatIsNotChunked_IsTheStartOfTheLine() { var source = new StringSource("ab\n" + Repeat("abcd efgh ", 60)); VisualLineIndex index = NewIndex(measureWidths: true, blockChars: 16, longLineChars: 48); CheckEqual(3, index.CheckpointAtOrBefore(source, 500)); } public void InABlockWithoutFirmRowStarts_IsTheStartOfTheLine() { var source = new StringSource("ab\n" + Repeat("!!!!", 100)); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, source); CheckEqual(3, index.CheckpointAtOrBefore(source, 300)); } public void AtTheEndOfTheText_IsInTheLastChunk() { string text = Repeat("abcd efgh ", 60); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(blockChars: 16, longLineChars: 48); CountAll(index, paged); CheckTrue(index.CheckpointAtOrBefore(paged, text.Length) > text.Length - ChunkBound); } public void OutsideTheText_Throws() { var source = new StringSource("ab"); VisualLineIndex index = NewIndex(); CheckThrow(typeof(ArgumentOutOfRangeException)); index.CheckpointAtOrBefore(source, 3); } } public class WidestLineTests : TestFixture { public void MatchesAFullLayout_WhenLinesDoNotWrap() { foreach (string text in Corpus) { VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(new FullLayout(text, int.MaxValue).WidestLine, index.WidestLine(new StringSource(text))); } } public void MatchesAFullLayout_WhenLinesWrap() { foreach (string text in Corpus) { VisualLineIndex index = NewIndex(Wide, measureWidths: true); CheckEqual(new FullLayout(text, Wide).WidestLine, index.WidestLine(new StringSource(text))); } } public void IsZero_WhenNotAsked() { CheckEqual(0, NewIndex(measureWidths: false).WidestLine(new StringSource(Corpus[6]))); } public void GrowsAsMoreOfThePartlyCountedTextIsCounted() { string text = Repeat("ab\n", 60) + "abcdefghijkl\n" + Repeat("ab\n", 60); var source = new StringSource(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, synchronousChars: 100); index.CountMore(source, 20); long early = index.WidestLine(source); CountAll(index, source); CheckEqual(20, early); CheckEqual(120, index.WidestLine(source)); } public void FallsWhenTheWidestLineIsDeleted() { string text = Repeat("ab\n", 20) + "abcdefghijkl\n" + Repeat("ab\n", 20); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(120, index.WidestLine(paged)); paged.Replace(60, 13, ""); index.NoteEdit(paged, 60, 13, 0); CheckEqual(20, index.WidestLine(paged)); } public void FallsWhenTheWidestLineIsShortened_AmongSeveralBlocks() { string text = "abcdefghijkl\n" + Repeat("ab\n", 30); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 30); CheckEqual(120, index.WidestLine(paged)); paged.Replace(0, 12, "abc"); index.NoteEdit(paged, 0, 12, 3); CheckEqual(30, index.WidestLine(paged)); } public void IsLeftAloneByAnEditToANarrowerBlock() { string text = "abcdefghijkl\n" + Repeat("ab\n", 30); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 30); CheckEqual(120, index.WidestLine(paged)); paged.Replace(60, 0, "cd"); index.NoteEdit(paged, 60, 0, 2); CheckEqual(120, index.WidestLine(paged)); } public void RisesWhenAWiderLineIsTyped() { string text = Repeat("ab\n", 20); PagedText paged = PagedTextTests.Paged(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(20, index.WidestLine(paged)); paged.Replace(3, 0, "abcdefg"); index.NoteEdit(paged, 3, 0, 7); CheckEqual(90, index.WidestLine(paged)); } public void OfALongLine_MatchesAFullLayout() { string text = LongLines(1000); var source = new StringSource(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, longLineChars: 48); CheckEqual(new FullLayout(text, int.MaxValue).WidestLine, index.WidestLine(source)); } private static string LongLines(int words) { return Repeat("abcd efgh ", words); } } public class EstimatedWidestLineTests : TestFixture { public static readonly string Line = Repeat("abcd efgh ", 400); public static VisualLineIndex NewPartlyCounted(string text, out StringSource source) { source = new StringSource(text); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 48, synchronousChars: 100); index.CountMore(source, 500); return index; } public void OfACountedText_IsTheWidestLine() { foreach (string text in Corpus) { VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); var source = new StringSource(text); CheckEqual(index.WidestLine(source), index.EstimatedWidestLine(source)); } } public void OfALineBeingCounted_ScalesTheCountedPartToTheWholeLine() { VisualLineIndex index = NewPartlyCounted(Line, out StringSource source); long real = new FullLayout(Line, int.MaxValue).WidestLine; CheckGreater(Line.Length, index.CountedChars(source)); CheckGreater(index.EstimatedWidestLine(source), real / 10 * 9); CheckGreater(real / 10 * 11, index.EstimatedWidestLine(source)); } public void OfALineBeingCounted_IsMoreThanTheWidthCounted() { VisualLineIndex index = NewPartlyCounted(Line, out StringSource source); CheckGreater(index.EstimatedWidestLine(source), index.WidestLine(source)); } public void OfAnUncountedText_CountsItsFirstBlock() { var source = new StringSource(Line); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 48, synchronousChars: 100); long real = new FullLayout(Line, int.MaxValue).WidestLine; long estimate = index.EstimatedWidestLine(source); CheckGreater(index.CountedChars(source), 0); CheckGreater(estimate, real / 10 * 9); CheckGreater(real / 10 * 11, estimate); } public void OfAnEmptyText_IsZero() { VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(0L, index.EstimatedWidestLine(new StringSource(""))); } public void DoesNotGuessAtLinesNotYetReached() { VisualLineIndex index = NewPartlyCounted(Line + "\n" + Repeat("m", 10000), out StringSource source); CheckGreater(50000L, index.EstimatedWidestLine(source)); } } public class EstimatedLineWidthTests : TestFixture { public void OfAShortLine_IsItsVisibleWidth() { var source = new StringSource("ab \ncdef"); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(20L, index.EstimatedLineWidth(source, 0)); CheckEqual(40L, index.EstimatedLineWidth(source, 4)); } public void OfAShortLine_CountsTheSpacesWhenTheyDoNotHang() { var source = new StringSource("ab \ncdef"); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, trailingSpaces: TrailingSpaceMode.Wrap); CheckEqual(24L, index.EstimatedLineWidth(source, 0)); } public void OfAnEmptyLine_IsZero() { var source = new StringSource("ab\n\ncd"); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckEqual(0L, index.EstimatedLineWidth(source, 3)); } public void OfACountedLongLine_IsExact() { var source = new StringSource(EstimatedWidestLineTests.Line + "\nab"); VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true, blockChars: 16, longLineChars: 48); CheckEqual(new FullLayout(EstimatedWidestLineTests.Line, int.MaxValue).WidestLine, index.EstimatedLineWidth(source, 0)); } public void OfALongLineBeingCounted_ScalesTheCountedPartToTheWholeLine() { VisualLineIndex index = EstimatedWidestLineTests.NewPartlyCounted(EstimatedWidestLineTests.Line, out StringSource source); long real = new FullLayout(EstimatedWidestLineTests.Line, int.MaxValue).WidestLine; CheckGreater(EstimatedWidestLineTests.Line.Length, index.CountedChars(source)); CheckGreater(index.EstimatedLineWidth(source, 0), real / 10 * 9); CheckGreater(real / 10 * 11, index.EstimatedLineWidth(source, 0)); } public void OfALongLineNotYetReached_CountsUpToItAndScalesIt() { string line = EstimatedWidestLineTests.Line; VisualLineIndex index = EstimatedWidestLineTests.NewPartlyCounted(line + "\n" + line, out StringSource source); long real = new FullLayout(line, int.MaxValue).WidestLine; long estimate = index.EstimatedLineWidth(source, line.Length + 1); CheckGreater(index.CountedChars(source), line.Length + 1); CheckGreater(estimate, real / 10 * 9); CheckGreater(real / 10 * 11, estimate); } public void OfTheLineBeingCounted_IsTheEstimatedWidestLine() { VisualLineIndex index = EstimatedWidestLineTests.NewPartlyCounted(EstimatedWidestLineTests.Line, out StringSource source); CheckEqual(index.EstimatedWidestLine(source), index.EstimatedLineWidth(source, 0)); } public void ForLinesThatWrap_Throws() { VisualLineIndex index = NewIndex(Wide, measureWidths: true); CheckThrow(typeof(InvalidOperationException)); index.EstimatedLineWidth(new StringSource("ab"), 0); } public void ForAnIndexOutsideTheText_Throws() { VisualLineIndex index = NewIndex(int.MaxValue, measureWidths: true); CheckThrow(typeof(ArgumentOutOfRangeException)); index.EstimatedLineWidth(new StringSource("ab"), 3); } } public class LayoutVersionTests : TestFixture { public void ChangesWhenTheLayoutDoes() { VisualLineIndex index = NewIndex(); int before = index.LayoutVersion; index.Configure(Font, LineSpacing, 40, TrailingSpaceMode.Hang, false); CheckTrue(index.LayoutVersion != before); } public void StaysWhenConfiguredTheSameAgain() { VisualLineIndex index = NewIndex(); int before = index.LayoutVersion; index.Configure(Font, LineSpacing, Wide, TrailingSpaceMode.Hang, false); CheckEqual(before, index.LayoutVersion); } } public class MeasureStretchTests : TestFixture { public const string Lines = "ab\ncd ef gh ij kl mn\nopstuvwxyz abc def\n\nqr\n"; public void ALongStretch_WrapsLikeItsOwnText() { var source = new StringSource(Lines); int start = Lines.IndexOf("cd", StringComparison.Ordinal); int end = Lines.IndexOf("qr", StringComparison.Ordinal); NewIndex().MeasureStretch(source, start, end, out int rows, out _); string own = Lines.Substring(start, end - start - 1); CheckEqual(new FullLayout(own, Wide).TotalRows, rows); } public void AStretchToTheEnd_KeepsTheLastBreakAndItsEmptyLine() { var source = new StringSource(Lines); int start = Lines.IndexOf("qr", StringComparison.Ordinal); NewIndex().MeasureStretch(source, start, Lines.Length, out int rows, out _); CheckEqual(2, rows); } public void AnEmptyStretchBeforeTheEnd_IsNoRows() { var source = new StringSource(Lines); NewIndex().MeasureStretch(source, 3, 3, out int rows, out int widest); CheckEqual(0, rows); CheckEqual(0, widest); } public void AnEmptyStretchAtTheEnd_IsTheEmptyLastLine() { var source = new StringSource(Lines); NewIndex().MeasureStretch(source, Lines.Length, Lines.Length, out int rows, out _); CheckEqual(1, rows); } public void ReportsTheWidestLineWhenAsked() { var source = new StringSource(Lines); int start = Lines.IndexOf("cd", StringComparison.Ordinal); int end = Lines.IndexOf("qr", StringComparison.Ordinal); NewIndex(measureWidths: true).MeasureStretch(source, start, end, out _, out int widest); string own = Lines.Substring(start, end - start - 1); CheckEqual(new FullLayout(own, Wide).WidestLine, widest); } public void ReportsNoWidthWhenNotAsked() { var source = new StringSource(Lines); NewIndex().MeasureStretch(source, 0, Lines.Length, out _, out int widest); CheckEqual(0, widest); } public void BeforeTheIndexIsConfigured_Throws() { CheckThrow(typeof(InvalidOperationException)); new VisualLineIndex().MeasureStretch(new StringSource(Lines), 0, 3, out _, out _); } } public class RowInStretchTests : TestFixture { public void CountsFromTheStretchsFirstLine() { var source = new StringSource(MeasureStretchTests.Lines); string lines = MeasureStretchTests.Lines; int start = lines.IndexOf("ops", StringComparison.Ordinal); int end = lines.IndexOf("qr", StringComparison.Ordinal); var own = new FullLayout(lines.Substring(start, end - start - 1), Wide); for (int i = start; i < end - 1; ++i) { CheckEqual(own.RowOf(i - start, false), NewIndex().RowInStretch(source, start, end, i, false)); } } public void ACaretShownAtTheEndOfARow_IsOnThatRow() { var source = new StringSource("aaaa bbbb cccc"); var own = new FullLayout("aaaa bbbb cccc", Wide); CheckEqual(own.RowOf(10, atLineEnd: true), NewIndex().RowInStretch(source, 0, source.Length, 10, true)); } public void TwoTextsWithTheSameRange_AreNotMixedUp() { VisualLineIndex index = NewIndex(); var narrow = new StringSource("ab cd"); var long2 = new StringSource("aaaa bbbb cccc dddd"); int first = index.RowInStretch(long2, 0, long2.Length, 19, false); int second = index.RowInStretch(narrow, 0, long2.Length - 14, 5, false); int firstAgain = index.RowInStretch(long2, 0, long2.Length, 19, false); CheckEqual(1, first); CheckEqual(0, second); CheckEqual(first, firstAgain); } public void ATextEditedBetweenQuestions_IsMeasuredAgain() { PagedText paged = PagedTextTests.Paged("aaaa bbbb cccc"); VisualLineIndex index = NewIndex(); CheckEqual(1, index.RowInStretch(paged, 0, paged.Length, 14, false)); paged.Replace(0, 4, "a"); CheckEqual(0, index.RowInStretch(paged, 0, paged.Length, 11, false)); } } public class IndexInStretchTests : TestFixture { public void LeadsToTheStartOfARowCountedFromTheStretchsFirstLine() { const string text = "ab\naaaa bbbb cccc dddd eeee\nqr"; var source = new StringSource(text); int start = 3; int end = text.IndexOf("qr", StringComparison.Ordinal); var own = new FullLayout(text.Substring(start, end - start - 1), Wide); for (int row = 0; row < own.TotalRows; ++row) { CheckEqual(start + own.IndexOfRow(row), NewIndex().IndexInStretch(source, start, end, row)); } } } public class ResetTests : TestFixture { public void ForgetsEverythingCounted() { var source = new StringSource(Corpus[10]); VisualLineIndex index = NewIndex(); index.TotalRows(source); index.Reset(); CheckEqual(0, index.BlockCount); CheckEqual(new FullLayout(Corpus[10], Wide).TotalRows, index.TotalRows(source)); } } } }