using System; using Amphora; using Annulus; using Glass.Editing; using Glass.Typesetting; using UnitTestSharp; using Glass.UnitTests.Editing; namespace Glass.UnitTests.Typesetting { public class TextViewportTests : TestFixture { // Seven hard lines, starting at 0, 4, 11, 14, 19, 25 and 33. public const string SevenLines = "abc\nabcdef\nab\nabcd\nabcde\nabcdefg\nab"; public static readonly TextMaterialStyle Style = new TextMaterialStyle { Font = LineWrapperTests.FontWithTenPixelLines, }; public static TextViewport NewViewport(bool multiline, int width = 100, int? height = null) { var viewport = new TextViewport(); viewport.Configure(Style, multiline, width, height); return viewport; } // Three hard lines starting at 0, 3 and 24, the middle one 200 wide. public const string WideLines = "ab\nabcdefghijklmnopqrst\nabc"; public static TextViewport NewSideways(Alignment.Horizontal alignment = Alignment.Horizontal.Left, int width = 100) { var viewport = new TextViewport(); viewport.Configure(Style with { HorizontalAlignment = alignment }, multiline: true, fitWidth: width, fitHeight: 30, wrap: false, caretWidth: 2); return viewport; } public static BandsMaterial NewBands() { return new BandsMaterial(new SolidColorMaterial()); } // Hard lines of several shapes among short ones, each past the long line length of NewChunked. public static readonly string[] LongLineTexts = { VisualLineIndexTests.Repeat("abcd efgh ", 36), "ab\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20) + "\ncd\n" + VisualLineIndexTests.Repeat("ij kl mn ", 24) + "\nef", VisualLineIndexTests.Repeat("abcdefghij", 30), VisualLineIndexTests.Repeat("ab cd ", 24), VisualLineIndexTests.Repeat("abcd-efgh ", 30), "x\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20) + "\n\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20), VisualLineIndexTests.Repeat("abcd efgh ", 8) + "\nab\ncd\nef\ngh\n" + VisualLineIndexTests.Repeat("abcd efgh ", 8) + "\nij", }; // Lays long lines out a few chars at a time, to be checked against NewViewport, which lays out the whole line. // A limit small enough that a line of a few thousand chars is left partly uncounted. public const int UncountedSynchronousChars = 3000; public static TextViewport NewChunked(int width, int height, int synchronousChars = VisualLineIndex.DefaultSynchronousChars) { var viewport = new TextViewport(blockChars: 16, longLineChars: 48, stretchChars: 24, synchronousChars: synchronousChars); viewport.Configure(Style, multiline: true, width, height); return viewport; } // Letters a million pixels wide, so a line of a few thousand is wider than an int. public static readonly TextMaterialStyle WideStyle = new TextMaterialStyle { Font = VisualLineIndexTests.WidthChunkedLinesTests.WideFont, }; // Lays a wide line out around the view only, to be checked against NewSideways, which lays the whole line out. public static TextViewport NewSidewaysChunked(int width, TextMaterialStyle style = null, int synchronousChars = VisualLineIndex.DefaultSynchronousChars) { var viewport = new TextViewport(blockChars: 16, longLineChars: 48, stretchChars: 24, synchronousChars: synchronousChars); viewport.Configure(style ?? Style, multiline: true, fitWidth: width, fitHeight: 30, wrap: false, caretWidth: 2); return viewport; } public static TextViewport NewAlignedChunked(Alignment.Horizontal alignment, int width, TextMaterialStyle style = null, int synchronousChars = VisualLineIndex.DefaultSynchronousChars) { return NewSidewaysChunked(width, (style ?? Style) with { HorizontalAlignment = alignment }, synchronousChars); } // What a sideways window shows, as the view sees it whether or not the window places rows by origin. public static string SidewaysViewOf(TextViewport viewport, ITextSource text, int caret, bool withWidth = true) { CaretPosition at = viewport.CaretAt(caret, atLineEnd: false); long shift = viewport.HasRowOrigins ? 0 : viewport.ScrollX; var returnMe = new System.Text.StringBuilder(); returnMe.Append($"first {viewport.FirstVisible}, scroll {viewport.ScrollX}, "); if (withWidth) { returnMe.Append($"width {viewport.ContentWidth(text)}, "); } // A caret outside the view is not drawn, and a window lays out only a little past the view. long across = at.X - shift; string where = across < 0 || across > viewport.VisibleWidth ? "off" : across.ToString(); returnMe.Append($"top {viewport.TopLine(text)}, caret line {at.Line} at {where},{at.Y}, "); returnMe.Append($"in {viewport.IsInWindow(caret, false)}, "); foreach (int x in new[] { -5, 0, 20, 60, 99, 120 }) { int index = viewport.IndexAt(x, 10, out bool atLineEnd, out int pastEdge); returnMe.Append($"at {x}: {index} {pastEdge}, "); } BandsMaterial bands = NewBands(); viewport.AddBands(bands, caret, Math.Min(text.Length, caret + 40)); // A band reaches only as far as the part of the line laid out, so only the view's part is compared. for (int i = 0; i < bands.Count; ++i) { Rect band = bands[i]; long left = Math.Max(0, (int)band.Left - shift); long right = Math.Min(viewport.VisibleWidth, (int)band.Right - shift); if (right > left) { returnMe.Append($"band {left}..{right} at {(int)band.Top}, "); } } return returnMe.ToString(); } // Compares what a sideways window shows when it lays out a wide line around the view with what it shows laying // out the whole line, after each of several scrolls to a caret. public static string SidewaysScrollToCaretDifferences(string text, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewAlignedChunked(alignment, width); TextViewport whole = NewSideways(alignment, width); CountAll(chunked, paged); for (int step = 0; step <= 2 * text.Length; step += 13) { int caret = step; if (step > text.Length) { caret = 2 * text.Length - step; } chunked.ScrollToCaret(paged, caret, caretAtLineEnd: false); whole.ScrollToCaret(paged, caret, caretAtLineEnd: false); string expected = SidewaysViewOf(whole, paged, caret); string actual = SidewaysViewOf(chunked, paged, caret); if (expected != actual) { return $"caret {caret}: {actual} not {expected}"; } } return ""; } // The same comparison after scrolling to a series of offsets across the text. public static string SidewaysScrollToXDifferences(string text, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewAlignedChunked(alignment, width); TextViewport whole = NewSideways(alignment, width); CountAll(chunked, paged); long widest = whole.ContentWidth(paged); for (long offset = 0; offset <= widest + 50; offset += 41) { chunked.ScrollToX(paged, offset); whole.ScrollToX(paged, offset); int caret = Math.Min(text.Length, (int)(offset / 9)); string expected = SidewaysViewOf(whole, paged, caret); string actual = SidewaysViewOf(chunked, paged, caret); if (expected != actual) { return $"offset {offset}: {actual} not {expected}"; } } return ""; } // The same comparison for moves of the caret by lines, pages and to the edges of a line. public static string SidewaysMoveDifferences(string text, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewAlignedChunked(alignment, width); TextViewport whole = NewSideways(alignment, width); CountAll(chunked, paged); for (int caret = 0; caret <= text.Length; caret += 17) { foreach (bool forward in new[] { false, true }) { foreach (int lines in new[] { 1, 2 }) { int expected = whole.LineTarget(paged, caret, false, lines, forward, version: 1, out bool ea); int actual = chunked.LineTarget(paged, caret, false, lines, forward, version: 1, out bool aa); if (expected != actual || ea != aa) { return $"line target {caret} {forward} {lines}: {actual} {aa} not {expected} {ea}"; } } int expectedPage = whole.PageTarget(paged, caret, false, forward, version: 1, out bool epa); int actualPage = chunked.PageTarget(paged, caret, false, forward, version: 1, out bool apa); if (expectedPage != actualPage || epa != apa) { return $"page target {caret} {forward}: {actualPage} {apa} not {expectedPage} {epa}"; } string expectedView = SidewaysViewOf(whole, paged, expectedPage); string actualView = SidewaysViewOf(chunked, paged, actualPage); if (expectedView != actualView) { return $"after the page target {caret} {forward}: {actualView} not {expectedView}"; } int expectedEdge = whole.LineEdge(paged, caret, false, forward, out bool eea); int actualEdge = chunked.LineEdge(paged, caret, false, forward, out bool aea); if (expectedEdge != actualEdge || eea != aea) { return $"line edge {caret} {forward}: {actualEdge} {aea} not {expectedEdge} {eea}"; } } } return ""; } // The same comparison after scrolling by lines and to lines, and keeping the window valid. public static string SidewaysLineScrollDifferences(string text, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewAlignedChunked(alignment, width); TextViewport whole = NewSideways(alignment, width); CountAll(chunked, paged); int lines = chunked.TotalLines(paged); for (int step = 0; step < 3 * lines; ++step) { string what; if (step % 3 == 0) { what = $"to line {step / 3}"; chunked.ScrollToLine(paged, step / 3); whole.ScrollToLine(paged, step / 3); } else if (step % 3 == 1) { what = "by one line"; chunked.ScrollByLines(paged, 1); whole.ScrollByLines(paged, 1); } else { what = "revalidated"; chunked.ScrollToX(paged, 60 * step); whole.ScrollToX(paged, 60 * step); chunked.Revalidate(paged); whole.Revalidate(paged); } string expected = SidewaysViewOf(whole, paged, whole.FirstVisible); string actual = SidewaysViewOf(chunked, paged, chunked.FirstVisible); if (expected != actual) { return $"{what}: {actual} not {expected}"; } } return ""; } // Compares a sideways window that lays a wide line out around the view, under a composition spliced into the // text, with one that lays out the whole text with the composition in it. public static string SidewaysComposedDifferences(string text, int start, int end, string replacement, int width = 100, Alignment.Horizontal alignment = Alignment.Horizontal.Left, TextMaterialStyle style = null, bool referenceIsChunked = false) { PagedText paged = PagedTextTests.Paged(text); var spliced = new SplicedText(); spliced.Set(paged, start, end, replacement); string composed = text.Remove(start, end - start).Insert(start, replacement); PagedText reference = PagedTextTests.Paged(composed); TextMaterialStyle aligned = (style ?? Style) with { HorizontalAlignment = alignment }; TextViewport chunked = NewSidewaysChunked(width, aligned); TextViewport whole; if (referenceIsChunked) { whole = NewSidewaysChunked(width, aligned); CountAll(whole, reference); } else { whole = new TextViewport(); whole.Configure(aligned, multiline: true, width, 30, wrap: false, caretWidth: 2); } CountAll(chunked, spliced); for (int caret = 0; caret <= composed.Length; caret += Math.Max(11, composed.Length / 40)) { chunked.ScrollToCaret(spliced, caret, caretAtLineEnd: false); whole.ScrollToCaret(reference, caret, caretAtLineEnd: false); string expected = SidewaysViewOf(whole, reference, caret); string actual = SidewaysViewOf(chunked, spliced, caret); if (expected != actual) { return $"caret {caret}: {actual} not {expected}"; } } long widest = whole.ContentWidth(reference); for (long offset = 0; offset <= widest + 50; offset += Math.Max(53, widest / 25)) { chunked.ScrollToX(spliced, offset); whole.ScrollToX(reference, offset); int caret = (int)Math.Min(composed.Length, offset * composed.Length / Math.Max(1, widest)); string expected = SidewaysViewOf(whole, reference, caret); string actual = SidewaysViewOf(chunked, spliced, caret); if (expected != actual) { return $"offset {offset}: {actual} not {expected}"; } } for (int caret = 0; caret <= composed.Length; caret += Math.Max(17, composed.Length / 30)) { foreach (bool forward in new[] { false, true }) { int expectedMove = whole.LineTarget(reference, caret, false, 1, forward, 1, out bool ea); int actualMove = chunked.LineTarget(spliced, caret, false, 1, forward, 1, out bool aa); int expectedPage = whole.PageTarget(reference, caret, false, forward, 1, out bool epa); int actualPage = chunked.PageTarget(spliced, caret, false, forward, 1, out bool apa); int expectedEdge = whole.LineEdge(reference, caret, false, forward, out bool eea); int actualEdge = chunked.LineEdge(spliced, caret, false, forward, out bool aea); if (expectedMove != actualMove || ea != aa || expectedPage != actualPage || epa != apa || expectedEdge != actualEdge || eea != aea) { return $"moves from {caret} {forward}: {actualMove} {actualPage} {actualEdge} " + $"not {expectedMove} {expectedPage} {expectedEdge}"; } } } return ""; } public static string ViewOf(TextViewport viewport, ITextSource text, int caret, bool atLineEnd) { int index = viewport.IndexAt(20, 10, out bool indexAtLineEnd, out int pastEdge); return $"first {viewport.FirstVisible}, top {viewport.TopLine(text)}, " + $"caret {viewport.CaretAt(caret, atLineEnd)}, in window {viewport.IsInWindow(caret, atLineEnd)}, " + $"index at {index} {indexAtLineEnd} {pastEdge}"; } public static string ScrollToCaretDifferences(string text, int width, int height = 30) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height); TextViewport whole = NewViewport(multiline: true, width, height); for (int step = 0; step <= 2 * text.Length; step += 9) { int caret = step; if (step > text.Length) { caret = 2 * text.Length - step; } foreach (bool atLineEnd in new[] { false, true }) { chunked.ScrollToCaret(paged, caret, atLineEnd); whole.ScrollToCaret(paged, caret, atLineEnd); string expected = ViewOf(whole, paged, caret, atLineEnd); string actual = ViewOf(chunked, paged, caret, atLineEnd); if (expected != actual) { return $"caret {caret} {atLineEnd}: {actual} not {expected}"; } } } return ""; } public static string LineEdgeDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); for (int caret = 0; caret <= text.Length; caret += 5) { foreach (bool atLineEnd in new[] { false, true }) { foreach (bool end in new[] { false, true }) { int expected = whole.LineEdge(paged, caret, atLineEnd, end, out bool expectedAtLineEnd); int actual = chunked.LineEdge(paged, caret, atLineEnd, end, out bool actualAtLineEnd); if (expected != actual || expectedAtLineEnd != actualAtLineEnd) { return $"caret {caret} {atLineEnd} end {end}: {actual} {actualAtLineEnd} " + $"not {expected} {expectedAtLineEnd}"; } } } } return ""; } public static string LineTargetDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); for (int caret = 0; caret <= text.Length; caret += 13) { foreach (int lines in new[] { 1, 3, 9, 1000 }) { foreach (bool forward in new[] { false, true }) { int expected = whole.LineTarget(paged, caret, false, lines, forward, 1, out bool expectedAt); int actual = chunked.LineTarget(paged, caret, false, lines, forward, 1, out bool actualAt); if (expected != actual || expectedAt != actualAt) { return $"caret {caret} lines {lines} {forward}: {actual} {actualAt} " + $"not {expected} {expectedAt}"; } } } } return ""; } public static string LineTargetRunDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); foreach (bool forward in new[] { true, false }) { int expectedCaret = 0; int actualCaret = 0; bool expectedAt = false; bool actualAt = false; if (!forward) { expectedCaret = text.Length; actualCaret = text.Length; } for (int move = 0; move < 40; ++move) { expectedCaret = whole.LineTarget(paged, expectedCaret, expectedAt, 1, forward, 1, out expectedAt); actualCaret = chunked.LineTarget(paged, actualCaret, actualAt, 1, forward, 1, out actualAt); whole.KeepGoalColumn(1); chunked.KeepGoalColumn(1); if (expectedCaret != actualCaret || expectedAt != actualAt) { return $"move {move} {forward}: {actualCaret} {actualAt} not {expectedCaret} {expectedAt}"; } } } return ""; } public static string PageTargetDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); chunked.ScrollToCaret(paged, 0, false); whole.ScrollToCaret(paged, 0, false); int caret = 0; bool atLineEnd = false; for (int move = 0; move < 24; ++move) { bool forward = move < 12; int expected = whole.PageTarget(paged, caret, atLineEnd, forward, 1, out bool expectedAt); int actual = chunked.PageTarget(paged, caret, atLineEnd, forward, 1, out bool actualAt); string expectedView = $"{expected} {expectedAt} first {whole.FirstVisible} top {whole.TopLine(paged)}"; string actualView = $"{actual} {actualAt} first {chunked.FirstVisible} top {chunked.TopLine(paged)}"; if (expectedView != actualView) { return $"move {move} from {caret}: {actualView} not {expectedView}"; } caret = expected; atLineEnd = expectedAt; whole.KeepGoalColumn(1); chunked.KeepGoalColumn(1); } return ""; } public static string ScrollToLineDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); if (whole.TotalLines(paged) != chunked.TotalLines(paged)) { return $"total {chunked.TotalLines(paged)} not {whole.TotalLines(paged)}"; } for (int step = 0; step <= 2 * whole.TotalLines(paged) + 4; ++step) { int line = step; if (step > whole.TotalLines(paged) + 2) { line = 2 * whole.TotalLines(paged) + 4 - step; } chunked.ScrollToLine(paged, line); whole.ScrollToLine(paged, line); string expected = $"first {whole.FirstVisible}, top {whole.TopLine(paged)}"; string actual = $"first {chunked.FirstVisible}, top {chunked.TopLine(paged)}"; if (expected != actual) { return $"line {line}: {actual} not {expected}"; } } return ""; } public static string RevalidateDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); chunked.ScrollToCaret(paged, text.Length, false); whole.ScrollToCaret(paged, text.Length, false); int removed = text.Length / 2; paged.Replace(text.Length - removed, removed, ""); chunked.NoteEdit(paged, text.Length - removed, removed, 0); whole.NoteEdit(paged, text.Length - removed, removed, 0); chunked.Revalidate(paged); whole.Revalidate(paged); string expected = $"first {whole.FirstVisible}, top {whole.TopLine(paged)}"; string actual = $"first {chunked.FirstVisible}, top {chunked.TopLine(paged)}"; return expected == actual ? "" : $"{actual} not {expected}"; } public class CtorTests : TestFixture { public void AStretchOfNoChars_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new TextViewport(stretchChars: 0); } } public class CharsMeasuredTests : TestFixture { public static readonly string HugeLine = VisualLineIndexTests.Repeat("abcd efgh ", 15000); public void ScrollingAboutInABoxOfOneLine_MeasuresAWindowNotTheText() { PagedText paged = PagedTextTests.Paged(HugeLine); TextViewport viewport = NewOneLine(Alignment.Horizontal.Left, width: 100, reach: 128); foreach (int caret in new[] { 0, HugeLine.Length, HugeLine.Length / 2, 5, HugeLine.Length - 3 }) { viewport.ScrollToCaret(paged, caret, caretAtLineEnd: false); } CheckTrue(viewport.CharsMeasured < 6000); } public void TypingInABoxOfOneLine_MeasuresAWindowNotTheText() { PagedText paged = PagedTextTests.Paged(HugeLine); TextViewport viewport = NewOneLine(Alignment.Horizontal.Left, width: 100, reach: 128); viewport.ScrollToCaret(paged, HugeLine.Length / 2, caretAtLineEnd: false); long before = viewport.CharsMeasured; paged.Replace(HugeLine.Length / 2, 0, "x"); viewport.ScrollToCaret(paged, HugeLine.Length / 2 + 1, caretAtLineEnd: false); CheckTrue(viewport.CharsMeasured - before < 1500); } public const int Bound = 1500; // Long enough that a viewport with UncountedSynchronousChars leaves the end of it uncounted. public static readonly string UncountedLine = VisualLineIndexTests.Repeat("abcd efgh ", 2000); public static readonly string OneLine = VisualLineIndexTests.Repeat("abcd efgh ", 4000); public static TextViewport NewCounted(PagedText paged) { TextViewport returnMe = NewChunked(width: 100, height: 30); CountAll(returnMe, paged); return returnMe; } public void StartsAtNothing() { CheckEqual(0L, new TextViewport().CharsMeasured); } public void ScrollingToACaretFarIntoALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); viewport.ScrollToCaret(paged, 30000, caretAtLineEnd: false); CheckTrue(viewport.CharsMeasured < Bound); } public void ScrollingToACaretNearAComposition_InALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); var spliced = new SplicedText(); spliced.Set(paged, 20000, 20003, "xyz"); foreach (int caret in new[] { 20003, 20050, 5, 30000, 20000 }) { viewport.ScrollToCaret(spliced, caret, caretAtLineEnd: false); } CheckTrue(viewport.CharsMeasured < 5 * Bound); } public void MovingByLinesNearAComposition_InALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); var spliced = new SplicedText(); spliced.Set(paged, 20000, 20003, "xyz"); long before = viewport.CharsMeasured; viewport.LineTarget(spliced, 20003, false, lines: 5, forward: true, version: 1, out _); viewport.LineTarget(spliced, 20003, false, lines: 5, forward: false, version: 1, out _); CheckTrue(viewport.CharsMeasured - before < Bound); } public void MovingByLinesFarIntoALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); long before = viewport.CharsMeasured; viewport.LineTarget(paged, 30000, false, lines: 5, forward: true, version: 1, out _); viewport.LineTarget(paged, 30000, false, lines: 5, forward: false, version: 1, out _); CheckTrue(viewport.CharsMeasured - before < Bound); } public void MovingByPagesFarIntoALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); viewport.ScrollToCaret(paged, 30000, caretAtLineEnd: false); long before = viewport.CharsMeasured; viewport.PageTarget(paged, 30000, false, forward: true, version: 1, out _); viewport.PageTarget(paged, 30000, false, forward: false, version: 1, out _); CheckTrue(viewport.CharsMeasured - before < Bound); } public void FindingTheEdgesOfALineFarIntoALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); long before = viewport.CharsMeasured; viewport.LineEdge(paged, 30000, false, end: false, out _); viewport.LineEdge(paged, 30000, false, end: true, out _); CheckTrue(viewport.CharsMeasured - before < Bound); } public void ScrollingToALineFarIntoALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); long before = viewport.CharsMeasured; viewport.ScrollToLine(paged, viewport.TotalLines(paged) / 2); CheckTrue(viewport.CharsMeasured - before < Bound); } public static TextViewport NewWide() { return NewSidewaysChunked(100); } public void ScrollingAboutAWideLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewWide(); CountAll(viewport, paged); long before = viewport.CharsMeasured; foreach (int caret in new[] { 30000, 3, OneLine.Length, OneLine.Length / 2 }) { viewport.ScrollToCaret(paged, caret, caretAtLineEnd: false); } viewport.ScrollToX(paged, 140000); viewport.ScrollToX(paged, 5); CheckTrue(viewport.CharsMeasured - before < Bound); } public void ScrollingAboutAWideLineUnderComposition_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewWide(); CountAll(viewport, paged); var spliced = new SplicedText(); spliced.Set(paged, 20000, 20003, "xyz"); long before = viewport.CharsMeasured; foreach (int caret in new[] { 20003, 30000, 3, OneLine.Length, 20000 }) { viewport.ScrollToCaret(spliced, caret, caretAtLineEnd: false); } viewport.ScrollToX(spliced, 140000); viewport.ScrollToX(spliced, 5); CheckTrue(viewport.CharsMeasured - before < 2 * Bound); } public void MovingAboutAWideLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine + "\n" + OneLine); TextViewport viewport = NewWide(); CountAll(viewport, paged); viewport.ScrollToCaret(paged, 30000, caretAtLineEnd: false); long before = viewport.CharsMeasured; int target = viewport.LineTarget(paged, 30000, false, lines: 1, forward: true, version: 1, out _); viewport.LineEdge(paged, target, false, end: true, out _); viewport.PageTarget(paged, target, false, forward: false, version: 1, out _); CheckTrue(viewport.CharsMeasured - before < Bound); } public void TypingInAWideLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewWide(); CountAll(viewport, paged); viewport.ScrollToCaret(paged, 20000, caretAtLineEnd: false); long before = viewport.CharsMeasured; paged.Replace(20000, 0, "x"); viewport.NoteEdit(paged, 20000, 0, 1); viewport.ScrollToCaret(paged, 20001, caretAtLineEnd: false); CheckTrue(viewport.CharsMeasured - before < Bound); } public void TypingInALongLine_MeasuresAWindowNotTheLine() { PagedText paged = PagedTextTests.Paged(OneLine); TextViewport viewport = NewCounted(paged); viewport.ScrollToCaret(paged, 20000, caretAtLineEnd: false); long before = viewport.CharsMeasured; paged.Replace(20000, 0, "x"); viewport.NoteEdit(paged, 20000, 0, 1); viewport.ScrollToCaret(paged, 20001, caretAtLineEnd: false); CheckTrue(viewport.CharsMeasured - before < Bound); } } public static string ScrollByLinesDifferences(string text, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewChunked(width, height: 30); TextViewport whole = NewViewport(multiline: true, width, height: 30); foreach (int lines in new[] { 1, 3, 7, -2, 20, -9, 1, 60, -1, 400, -33, -1000, 5, 1000 }) { chunked.ScrollByLines(paged, lines); whole.ScrollByLines(paged, lines); string expected = $"first {whole.FirstVisible}, top {whole.TopLine(paged)}"; string actual = $"first {chunked.FirstVisible}, top {chunked.TopLine(paged)}"; if (expected != actual) { return $"by {lines}: {actual} not {expected}"; } } return ""; } public class ScrollByLinesTests : TestFixture { public void MovesDownByThatManyLines() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, 2); CheckEqual(11, viewport.FirstVisible); CheckEqual(2, viewport.TopLine(SevenLines)); } public void MovesUpByThatManyLines() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, 3); viewport.ScrollByLines(SevenLines, -2); CheckEqual(1, viewport.TopLine(SevenLines)); } public void ByNothing_StaysPut() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, 2); viewport.ScrollByLines(SevenLines, 0); CheckEqual(2, viewport.TopLine(SevenLines)); } public void PastTheEnd_StopsWhereTheLastLinesFillTheBox() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, 100); CheckEqual(4, viewport.TopLine(SevenLines)); } public void PastTheStart_StopsAtTheFirstLine() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, 3); viewport.ScrollByLines(SevenLines, -100); CheckEqual(0, viewport.FirstVisible); } public void ByTheWholeRangeOfAnInt_Stops() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, int.MaxValue); CheckEqual(4, viewport.TopLine(SevenLines)); viewport.ScrollByLines(SevenLines, int.MinValue); CheckEqual(0, viewport.FirstVisible); } public void MeasuresTheWindowItEndsIn() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollByLines(SevenLines, 1); CheckEqual(4, viewport.FirstVisible); CheckEqual(18, viewport.WindowEnd); } public void AWindowThatShowsAllLines_DoesNothing() { TextViewport viewport = NewViewport(multiline: true); viewport.ScrollByLines(SevenLines, 3); CheckEqual(0, viewport.FirstVisible); } public void AWindowOfOneLine_DoesNothing() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollByLines(SevenLines, 3); CheckEqual(0, viewport.FirstVisible); } public void WithoutAFont_DoesNothing() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); viewport.ScrollByLines(SevenLines, 3); CheckEqual(0, viewport.FirstVisible); } [Thorough] public void OnLongLines_MovesAsLayingOutTheWholeLineDoes() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollByLinesDifferences(text, width: 100)); CheckEqual("", ScrollByLinesDifferences(text, width: 35)); } } public void WhileTheLongLineIsNotCounted_MovesByExactlyThatManyLines() { PagedText paged = PagedTextTests.Paged(CharsMeasuredTests.UncountedLine); VisualLineIndex index = VisualLineIndexTests.NewIndex(blockChars: 16, longLineChars: 48); VisualLineIndexTests.CountAll(index, paged); TextViewport viewport = NewChunked(width: 100, height: 30, UncountedSynchronousChars); viewport.ScrollByLines(paged, 5); int afterFive = viewport.FirstVisible; viewport.ScrollByLines(paged, 1000); CheckEqual(index.IndexOfRow(paged, 5), afterFive); CheckEqual(index.IndexOfRow(paged, 1005), viewport.FirstVisible); CheckFalse(viewport.IsCounted(paged)); } public void OnALongLine_MeasuresRowsNotTheLine() { PagedText paged = PagedTextTests.Paged(CharsMeasuredTests.OneLine); TextViewport viewport = CharsMeasuredTests.NewCounted(paged); viewport.ScrollToLine(paged, viewport.TotalLines(paged) / 2); long before = viewport.CharsMeasured; viewport.ScrollByLines(paged, 3); viewport.ScrollByLines(paged, -3); CheckTrue(viewport.CharsMeasured - before < CharsMeasuredTests.Bound); } } // Lines of one hard line in several shapes, each far wider than the view. public static readonly string[] SingleLineTexts = { VisualLineIndexTests.Repeat("abcd efgh ", 40), VisualLineIndexTests.Repeat("abcdefghij", 40), VisualLineIndexTests.Repeat("ab cd ", 30), VisualLineIndexTests.Repeat("iiiWWWii mm ", 30), VisualLineIndexTests.Repeat("ab" + char.ConvertFromUtf32(0x20BB7) + " ", 40), VisualLineIndexTests.Repeat("a" + (char)0x301 + "bc d" + (char)0x301 + " ", 40), VisualLineIndexTests.Repeat("" + (char)0x4F60 + (char)0x597D, 80), }; public static TextViewport NewOneLine(Alignment.Horizontal alignment, int width, int reach) { var viewport = new TextViewport(stretchChars: reach); viewport.Configure(Style with { HorizontalAlignment = alignment }, multiline: false, width, fitHeight: null); return viewport; } public static string OneLineViewOf(TextViewport viewport, int caret, bool atLineEnd) { var returnMe = new System.Text.StringBuilder(); returnMe.Append($"first {viewport.FirstVisible}, caret {viewport.CaretAt(caret, atLineEnd)}"); foreach (int x in new[] { 0, 20, 50, 99 }) { int index = viewport.IndexAt(x, 0, out bool indexAtLineEnd, out int pastEdge); returnMe.Append($", {index} {indexAtLineEnd} {pastEdge}"); } return returnMe.ToString(); } public static string OneLineDifferences(string text, Alignment.Horizontal alignment, int width) { PagedText paged = PagedTextTests.Paged(text); TextViewport bounded = NewOneLine(alignment, width, reach: 8); TextViewport whole = NewOneLine(alignment, width, reach: TextViewport.DefaultStretchChars); for (int step = 0; step <= 2 * text.Length; step += 7) { int caret = step; if (step > text.Length) { caret = 2 * text.Length - step; } if (caret > 0 && caret < text.Length && char.IsLowSurrogate(text[caret])) { continue; } foreach (bool atLineEnd in new[] { false, true }) { bounded.ScrollToCaret(paged, caret, atLineEnd); whole.ScrollToCaret(paged, caret, atLineEnd); string expected = OneLineViewOf(whole, caret, atLineEnd); string actual = OneLineViewOf(bounded, caret, atLineEnd); if (expected != actual) { return $"caret {caret} {atLineEnd}: {actual} not {expected}"; } } } return ""; } public class ConfigureTests : TestFixture { public void RejectsNoStyle() { CheckThrow(typeof(System.ArgumentNullException)); new TextViewport().Configure(null, multiline: true, fitWidth: 100, fitHeight: null); } public void WithoutAFont_HasNoLineHeightAndOneVisibleLine() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 100); CheckEqual(0, viewport.LineHeight); CheckEqual(1, viewport.VisibleLines); } public void TakesLineHeightFromTheFont() { CheckEqual(10, NewViewport(multiline: true).LineHeight); } public void SingleLine_ShowsOneLineHoweverTallTheBox() { CheckEqual(1, NewViewport(multiline: false, height: 100).VisibleLines); } public void Multiline_ShowsTheLinesThatFit() { CheckEqual(3, NewViewport(multiline: true, height: 35).VisibleLines); } public void Multiline_ShowsOneLineInABoxShorterThanOne() { CheckEqual(1, NewViewport(multiline: true, height: 4).VisibleLines); } public void Multiline_WithNoHeightLimit_ShowsEveryLine() { CheckEqual(int.MaxValue, NewViewport(multiline: true).VisibleLines); } } public class ResetTests : TestFixture { public void ScrollsBackToTheStart() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.Reset(); CheckEqual(0, viewport.FirstVisible); } } public class LimitToTests : TestFixture { public void ScrollsBackToAnIndexBeforeTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.LimitTo(index: 6); CheckEqual(6, viewport.FirstVisible); } public void LeavesAWindowThatStartsBeforeTheIndex() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.LimitTo(index: 30); CheckEqual(19, viewport.FirstVisible); } } public class ScrollToCaretTests : TestFixture { [Thorough] public void InABoxOfOneLine_ShowsTheSameAsMeasuringTheWholeText() { foreach (string text in SingleLineTexts) { CheckEqual("", OneLineDifferences(text, Alignment.Horizontal.Left, width: 100)); } } [Thorough] public void InABoxOfOneLine_ShowsTheSameAsMeasuringTheWholeText_AtNarrowWidth() { foreach (string text in SingleLineTexts) { CheckEqual("", OneLineDifferences(text, Alignment.Horizontal.Left, width: 35)); } } public void InABoxOfOneLine_ShowsTheSameAsMeasuringTheWholeText_Centred() { foreach (string text in SingleLineTexts) { CheckEqual("", OneLineDifferences(text, Alignment.Horizontal.Center, width: 100)); } } [Thorough] public void InABoxOfOneLine_ShowsTheSameAsMeasuringTheWholeText_RightAligned() { foreach (string text in SingleLineTexts) { CheckEqual("", OneLineDifferences(text, Alignment.Horizontal.Right, width: 100)); } } public void OnLongLines_ShowTheSameLinesAsLayingOutTheWholeLine_WhenARowIsLongerThanAStretch() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToCaretDifferences(text, width: 600)); } } [Thorough] public void OnLongLines_ShowTheSameLinesAsLayingOutTheWholeLine_InATallBox() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToCaretDifferences(text, width: 100, height: 90)); } } public void OnLongLines_ShowTheSameLinesAsLayingOutTheWholeLine_InATallBoxAtNarrowWidth() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToCaretDifferences(text, width: 35, height: 150)); } } public static readonly string ComposedLine = WideLineText; public static readonly TextMaterialStyle KernedStyle = new TextMaterialStyle { Font = VisualLineIndexTests.WidthChunkedLinesTests.Kerned, }; [Thorough] public void SidewaysOnAWideLineUnderComposition_InsertingInTheMiddle_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 100, 100, "xy")); } public void SidewaysOnAWideLineUnderComposition_ReplacingSomeChars_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 100, 104, "wxyz!")); } public void SidewaysOnAWideLineUnderComposition_ReplacingManyChars_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 50, 150, "q")); } public void SidewaysOnAWideLineUnderComposition_AtTheStartOfTheLine_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 3, 3, "xy")); } public static long MeasuredOnScrollingToTheStart(string text, int height) { TextViewport viewport = NewChunked(width: 100, height); viewport.ScrollToCaret(PagedTextTests.Paged(text), caret: 0, caretAtLineEnd: false); return viewport.CharsMeasured; } public void ALongLineThatLaysOutInOneStretch_UsesUpAsManyRowsOfTheBoxAsItHas() { string sixRows = VisualLineIndexTests.Repeat("abcd efgh ", 6) + "\n"; CheckEqual(MeasuredOnScrollingToTheStart(sixRows + VisualLineIndexTests.Repeat("abcdefghij\n", 18), height: 240), MeasuredOnScrollingToTheStart(sixRows + VisualLineIndexTests.Repeat("abcdefghij\n", 100), height: 240)); } public void SidewaysOnAWideLineUnderComposition_AtTheEndOfTheLine_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 203, 203, "xy")); } public void SidewaysOnAWideLineUnderComposition_AddingSpacesAtTheEnd_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 203, 203, " ")); } [Thorough] public void SidewaysOnAWideLineUnderComposition_WhenTheLineEndsInSpaces_ShowsWhatLayingOutTheWholeTextDoes() { string text = "ab\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20) + " \ncd"; CheckEqual("", SidewaysComposedDifferences(text, 100, 100, "xy")); CheckEqual("", SidewaysComposedDifferences(text, 203, 205, "q")); } [Thorough] public void SidewaysOnAShortLineUnderComposition_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 1, 1, "xy")); CheckEqual("", SidewaysComposedDifferences(ComposedLine, 206, 206, "xy")); } public void SidewaysOnAWideLineUnderComposition_WithABreakInIt_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 100, 100, "x\ny")); } public void SidewaysOnAWideLineUnderComposition_ReplacingAcrossLines_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 1, 206, "zz")); } public void SidewaysOnAWideLineUnderComposition_WhenCentred_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 100, 100, "xy", alignment: Alignment.Horizontal.Center)); } public void SidewaysOnAWideLineUnderComposition_WhenRightAligned_ShowsWhatLayingOutTheWholeTextDoes() { CheckEqual("", SidewaysComposedDifferences(ComposedLine, 100, 103, "xy", alignment: Alignment.Horizontal.Right)); } public void SidewaysOnAWideLineUnderComposition_WithKerning_ShowsWhatLayingOutTheWholeTextDoes() { string text = "ab\n" + VisualLineIndexTests.Repeat("AVAWTa ", 40) + "\ncd"; foreach (int start in new[] { 3, 40, 41, 100, 283 }) { CheckEqual("", SidewaysComposedDifferences(text, start, start, "AVT", style: KernedStyle)); CheckEqual("", SidewaysComposedDifferences(text, start, start + 2, "WA", style: KernedStyle)); } } public void SidewaysOnAWideLineUnderComposition_WiderThanAnInt_ShowsWhatCountingTheWholeTextDoes() { string text = "ab\n" + VisualLineIndexTests.Repeat("abcd efgh ", 250) + "\ncd"; CheckEqual("", SidewaysComposedDifferences(text, 1500, 1500, "xy", style: WideStyle, referenceIsChunked: true)); CheckEqual("", SidewaysComposedDifferences(text, 1500, 1520, "x", style: WideStyle, referenceIsChunked: true)); } public void OnALongLineUnderComposition_ShowTheSameLinesAsLayingOutTheWholeLine() { string text = LongLineTexts[1]; PagedText paged = PagedTextTests.Paged(text); var spliced = new SplicedText(); spliced.Set(paged, 20, 20, "xyz"); TextViewport chunked = NewChunked(width: 100, height: 30); TextViewport whole = NewViewport(multiline: true, width: 100, height: 30); for (int caret = 0; caret <= spliced.Length; caret += 17) { chunked.ScrollToCaret(spliced, caret, caretAtLineEnd: false); whole.ScrollToCaret(spliced, caret, caretAtLineEnd: false); CheckEqual(ViewOf(whole, spliced, caret, atLineEnd: false), ViewOf(chunked, spliced, caret, atLineEnd: false)); } } public void OnALongLineUnderComposition_ShowTheSameLinesAsLayingOutTheWholeLine_WhereTheSpliceIs() { string text = LongLineTexts[1]; PagedText paged = PagedTextTests.Paged(text); var spliced = new SplicedText(); spliced.Set(paged, 200, 203, "xyz"); TextViewport chunked = NewChunked(width: 100, height: 30); TextViewport whole = NewViewport(multiline: true, width: 100, height: 30); for (int caret = 0; caret <= 203; caret += 7) { chunked.ScrollToCaret(spliced, caret, caretAtLineEnd: false); whole.ScrollToCaret(spliced, caret, caretAtLineEnd: false); CheckEqual(ViewOf(whole, spliced, caret, atLineEnd: false), ViewOf(chunked, spliced, caret, atLineEnd: false)); } } public void PastTheCountedPartOfALongLine_CountsUpToTheCaretAndShowsWhatCountedTextDoes() { PagedText paged = PagedTextTests.Paged(CharsMeasuredTests.UncountedLine); TextViewport counted = NewChunked(width: 100, height: 30); CountAll(counted, paged); TextViewport uncounted = NewChunked(width: 100, height: 30, UncountedSynchronousChars); counted.ScrollToCaret(paged, 10000, caretAtLineEnd: false); uncounted.ScrollToCaret(paged, 10000, caretAtLineEnd: false); CheckEqual(ViewOf(counted, paged, 10000, atLineEnd: false), ViewOf(uncounted, paged, 10000, atLineEnd: false)); CheckFalse(uncounted.IsCounted(paged)); } [Thorough] public void OnLongLines_ShowTheSameLinesAsLayingOutTheWholeLine() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToCaretDifferences(text, width: 100)); } } [Thorough] public void OnLongLines_ShowTheSameLinesAsLayingOutTheWholeLine_AtNarrowWidth() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToCaretDifferences(text, width: 35)); } } public void WithoutAFont_StaysAtTheStart() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: false, fitWidth: 100, fitHeight: null); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); CheckEqual(0, viewport.FirstVisible); } public static void CheckWideTexts(int first, int last, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { for (int i = first; i <= last; ++i) { string difference = SidewaysScrollToCaretDifferences(LongLineTexts[i], width, alignment); CheckStatic(difference.Length == 0, $"Text {i}: {difference}"); } } public static void CheckStatic(bool condition, string message) { if (!condition) { throw new InvalidOperationException(message); } } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_RightAligned_1() { CheckWideTexts(0, 2, width: 100, Alignment.Horizontal.Right); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_RightAligned_2() { CheckWideTexts(3, 6, width: 100, Alignment.Horizontal.Right); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_Centred_1() { CheckWideTexts(0, 2, width: 100, Alignment.Horizontal.Center); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_Centred_2() { CheckWideTexts(3, 6, width: 100, Alignment.Horizontal.Center); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_First() { CheckWideTexts(0, 2, width: 100); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_Second() { CheckWideTexts(3, 4, width: 100); } public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_Third() { CheckWideTexts(5, 6, width: 100); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_InANarrowView() { CheckWideTexts(0, 1, width: 33); } [Thorough] public void WideLines_ShowWhatALayoutOfTheWholeLineDoes_InAWideView() { CheckWideTexts(0, 1, width: 237); } public void InAComposedText_ScrollsSidewaysToTheCaret() { TextViewport viewport = NewSideways(); var spliced = new SplicedText(); spliced.Set(PagedTextTests.Paged(WideLines), 5, 5, "xy"); viewport.ScrollToCaret(spliced, 20, caretAtLineEnd: false); CheckGreater(viewport.ScrollX, 0L); CheckTrue(viewport.IsInWindow(20, atLineEnd: false)); } public void ALineWiderThanAnInt_ScrollsToAndShowsACaretFarAlong() { string text = VisualLineIndexTests.Repeat("ab", 3000); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewSidewaysChunked(100, WideStyle); CountAll(viewport, paged); viewport.ScrollToCaret(paged, 5000, caretAtLineEnd: false); CheckEqual(5000L * 1000000 + 2 - 100, viewport.ScrollX); CheckTrue(viewport.HasRowOrigins); CheckEqual(98, viewport.CaretAt(5000, atLineEnd: false).X); CheckTrue(viewport.IsInWindow(5000, atLineEnd: false)); CheckFalse(viewport.IsInWindow(4990, atLineEnd: false)); CheckEqual(5000, viewport.IndexAt(50, 5, out _, out int pastEdge)); CheckEqual(0, pastEdge); } public void ALineWiderThanAnInt_ScrollsBackToTheStart() { string text = VisualLineIndexTests.Repeat("ab", 3000); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewSidewaysChunked(100, WideStyle); viewport.ScrollToCaret(paged, 5000, caretAtLineEnd: false); viewport.ScrollToCaret(paged, 0, caretAtLineEnd: false); CheckEqual(0L, viewport.ScrollX); CheckEqual(0, viewport.CaretAt(0, atLineEnd: false).X); } public void SingleLine_ScrollsSidewaysToShowACaretPastTheEnd() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); CheckEqual(6, viewport.FirstVisible); } public void SingleLine_ScrollsBackToShowACaretBeforeTheWindow() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 2, caretAtLineEnd: false); CheckEqual(2, viewport.FirstVisible); } public void SingleLine_ScrollsBackToFillTheBoxWhenTheTextShrinks() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); viewport.ScrollToCaret("abcdefghijklm", caret: 13, caretAtLineEnd: false); CheckEqual(3, viewport.FirstVisible); } public void Multiline_ScrollsDownToShowACaretBelowTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); CheckEqual(19, viewport.FirstVisible); } public void Multiline_ScrollsUpToTheStartOfTheLineOfACaretAboveTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.ScrollToCaret(SevenLines, caret: 16, caretAtLineEnd: false); CheckEqual(14, viewport.FirstVisible); } public void Multiline_LeavesTheWindowWhenTheCaretIsInIt() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.ScrollToCaret(SevenLines, caret: 25, caretAtLineEnd: false); CheckEqual(19, viewport.FirstVisible); } public void Multiline_ScrollsBackToFillTheWindowWhenTheTextShrinks() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.ScrollToCaret(SevenLines.Substring(0, 24), caret: 24, caretAtLineEnd: false); CheckEqual(11, viewport.FirstVisible); } public void Sideways_ScrollsToShowACaretPastTheRightEdge() { TextViewport viewport = NewSideways(); viewport.ScrollToCaret(WideLines, caret: 23, caretAtLineEnd: false); CheckEqual(102, viewport.ScrollX); } public void Sideways_ScrollsOnlyFarEnoughToShowACaretInTheMiddle() { TextViewport viewport = NewSideways(); viewport.ScrollToCaret(WideLines, caret: 15, caretAtLineEnd: false); CheckEqual(22, viewport.ScrollX); } public void Sideways_ScrollsBackToShowACaretLeftOfTheView() { TextViewport viewport = NewSideways(); viewport.ScrollToCaret(WideLines, caret: 23, caretAtLineEnd: false); viewport.ScrollToCaret(WideLines, caret: 5, caretAtLineEnd: false); CheckEqual(20, viewport.ScrollX); } public void Sideways_LeavesTheOffsetWhenTheCaretIsInView() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 40); viewport.ScrollToCaret(WideLines, caret: 9, caretAtLineEnd: false); CheckEqual(40, viewport.ScrollX); } public void Sideways_KeepsTheLinesUnwrapped() { TextViewport viewport = NewSideways(); CheckEqual(3, viewport.TotalLines(WideLines)); } public void Multiline_ShowsTheStartOfAWrappedLineWhoseEndHoldsTheCaret() { TextViewport viewport = NewViewport(multiline: true, height: 10); viewport.ScrollToCaret("aaaa bbbb cccc dddd", caret: 19, caretAtLineEnd: false); CheckEqual(10, viewport.FirstVisible); } } public class CaretAtTests : TestFixture { public void SingleLine_FindsTheCaretRelativeToTheScrolledWindow() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); CaretPosition caret = viewport.CaretAt(16, atLineEnd: false); CheckEqual(new CaretPosition(Line: 0, X: 100, Y: 0), caret); } public void Multiline_FindsTheLineWithinTheScrolledWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); CaretPosition caret = viewport.CaretAt(SevenLines.Length, atLineEnd: false); CheckEqual(new CaretPosition(Line: 2, X: 20, Y: 20), caret); } public void AnIndexBeforeTheWindow_FindsTheWindowsStart() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); CaretPosition caret = viewport.CaretAt(0, atLineEnd: false); CheckEqual(new CaretPosition(Line: 0, X: 0, Y: 0), caret); } } public class AddBandsTests : TestFixture { public void AddsABandForEachLineTheRangeCrosses() { TextViewport viewport = NewViewport(multiline: true, height: 100); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); BandsMaterial bands = NewBands(); viewport.AddBands(bands, start: 2, end: 12); CheckEqual(3, bands.Count); CheckEqual(10, bands[1].TopLeft.Y); } public void AnEmptyRange_AddsNoBands() { TextViewport viewport = NewViewport(multiline: true, height: 100); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); BandsMaterial bands = NewBands(); viewport.AddBands(bands, start: 5, end: 5); CheckEqual(0, bands.Count); } } public class IndexAtTests : TestFixture { public void SingleLine_AddsTheWindowsStartToTheIndexInIt() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); int index = viewport.IndexAt(x: 31, y: 0, out _, out int pastEdge); CheckEqual(9, index); CheckEqual(0, pastEdge); } public void SingleLine_ReportsHowFarLeftOfTheWindowThePointIs() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); int index = viewport.IndexAt(x: -5, y: 0, out _, out int pastEdge); CheckEqual(6, index); CheckEqual(-5, pastEdge); } public void SingleLine_ReportsHowFarRightOfTheWindowThePointIs() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); int index = viewport.IndexAt(x: 130, y: 0, out _, out int pastEdge); CheckEqual(16, index); CheckEqual(30, pastEdge); } public void Multiline_ReportsNoDistancePastAnEdge() { TextViewport viewport = NewViewport(multiline: true, height: 100); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); viewport.IndexAt(x: -5, y: 0, out _, out int pastLeft); viewport.IndexAt(x: 130, y: 0, out _, out int pastRight); CheckEqual(0, pastLeft); CheckEqual(0, pastRight); } public void Multiline_FindsTheIndexOnTheLineAtY() { TextViewport viewport = NewViewport(multiline: true, height: 100); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); CheckEqual(13, viewport.IndexAt(x: 25, y: 22, out _, out _)); } public void Sideways_AddsTheOffsetToThePoint() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); CheckEqual(9, viewport.IndexAt(x: 10, y: 10, out _, out int pastEdge)); CheckEqual(0, pastEdge); } public void Sideways_ReportsHowFarOutsideTheViewThePointIs() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); viewport.IndexAt(x: -5, y: 10, out _, out int pastLeft); viewport.IndexAt(x: 150, y: 10, out _, out int pastRight); CheckEqual(-5, pastLeft); CheckEqual(50, pastRight); } public void WithoutAFont_FindsIndexZeroAndStillReportsThePastEdge() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: false, fitWidth: 100, fitHeight: null); int index = viewport.IndexAt(x: 130, y: 0, out bool atLineEnd, out int pastEdge); CheckEqual(0, index); CheckFalse(atLineEnd); CheckEqual(30, pastEdge); } } public class ScrollsSidewaysTests : TestFixture { public void MultilineThatDoesNotWrap_Is() { Check(NewSideways().ScrollsSideways); } public void MultilineThatWraps_IsNot() { CheckFalse(NewViewport(multiline: true).ScrollsSideways); } public void SingleLine_IsNot() { var viewport = new TextViewport(); viewport.Configure(Style, multiline: false, fitWidth: 100, fitHeight: null, wrap: false); CheckFalse(viewport.ScrollsSideways); } } public class ColumnTests : TestFixture { public void IsHalfTheFontsEm() { CheckEqual(5, NewViewport(multiline: true).Column); } public void WithoutAFont_IsOne() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: null); CheckEqual(1, viewport.Column); } } public class LayoutWidthTests : TestFixture { public void WhenWrapping_IsNull() { TextViewport viewport = NewViewport(multiline: true); viewport.TotalLines(WideLines); CheckNull(viewport.LayoutWidth); } public void WhenNotWrapping_IsTheWidestLine() { TextViewport viewport = NewSideways(); viewport.TotalLines(WideLines); CheckEqual(200, viewport.LayoutWidth); } public void WhenNotWrapping_IsAtLeastTheViewsWidth() { TextViewport viewport = NewSideways(); viewport.TotalLines("ab"); CheckEqual(100, viewport.LayoutWidth); } public void FollowsTheText() { TextViewport viewport = NewSideways(); viewport.TotalLines(WideLines); viewport.TotalLines("abcdefghijklmnopqrstuvwxyz"); CheckEqual(260, viewport.LayoutWidth); } } public class ContentWidthTests : TestFixture { public void LeavesRoomForTheCaretAfterTheWidestLine() { CheckEqual(202, NewSideways().ContentWidth(WideLines)); } public static readonly string LongLine = VisualLineIndexTests.Repeat("abcd efgh ", 2000); // Each ten chars are 88 wide, and the last space hangs. public const long LongLineWidth = 2000L * 88 - 4 + 2; public void OfAWideLineNotYetCounted_IsReckonedFromWhatIsCounted() { PagedText paged = PagedTextTests.Paged(LongLine); TextViewport viewport = NewSidewaysChunked(100, synchronousChars: UncountedSynchronousChars); viewport.CountMore(paged, 7000); long reckoned = viewport.ContentWidth(paged); CheckFalse(viewport.IsCounted(paged)); CheckGreater(reckoned, LongLineWidth / 10 * 9); CheckGreater(LongLineWidth / 10 * 11, reckoned); } public void OfAWideLineNotYetCounted_IsReckonedFromItsFirstBlock() { PagedText paged = PagedTextTests.Paged(LongLine); TextViewport viewport = NewSidewaysChunked(100, synchronousChars: UncountedSynchronousChars); long reckoned = viewport.ContentWidth(paged); CheckGreater(reckoned, LongLineWidth / 10 * 9); CheckGreater(LongLineWidth / 10 * 11, reckoned); } public void OfAWideLine_IsExactOnceCounted() { PagedText paged = PagedTextTests.Paged(LongLine); TextViewport viewport = NewSidewaysChunked(100); CountAll(viewport, paged); CheckEqual(LongLineWidth, viewport.ContentWidth(paged)); } public void ReckoningMore_LeavesTheViewWhereItIs() { PagedText paged = PagedTextTests.Paged(LongLine); TextViewport viewport = NewSidewaysChunked(100, synchronousChars: UncountedSynchronousChars); viewport.ScrollToX(paged, 12000); for (int step = 0; step < 6; ++step) { viewport.CountMore(paged, 4000); viewport.ContentWidth(paged); CheckEqual(12000L, viewport.ScrollX); } } public void OfALineWiderThanAnInt_IsALong() { PagedText paged = PagedTextTests.Paged(VisualLineIndexTests.Repeat("ab", 3000)); TextViewport viewport = NewSidewaysChunked(100, WideStyle); CountAll(viewport, paged); CheckEqual(6000L * 1000000 + 2, viewport.ContentWidth(paged)); } public void TextNarrowerThanTheView_IsTheViewsWidth() { CheckEqual(100, NewSideways().ContentWidth("ab")); } public void WhenWrapping_IsTheViewsWidth() { CheckEqual(100, NewViewport(multiline: true).ContentWidth(WideLines)); } } public class WindowEndTests : TestFixture { public void InABoxOfOneLine_ReachesPastTheViewButNotToTheEnd() { string text = VisualLineIndexTests.Repeat("abcd efgh ", 400); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewOneLine(Alignment.Horizontal.Left, width: 100, reach: 8); viewport.ScrollToCaret(paged, 0, caretAtLineEnd: false); CheckTrue(viewport.WindowEnd > 10 && viewport.WindowEnd < 200); } public void InABoxOfOneLine_ThatFits_IsTheEndOfTheText() { PagedText paged = PagedTextTests.Paged("abc"); TextViewport viewport = NewOneLine(Alignment.Horizontal.Left, width: 100, reach: 8); viewport.ScrollToCaret(paged, 0, caretAtLineEnd: false); CheckEqual(3, viewport.WindowEnd); } public void OnALongLine_IsInsideIt() { string text = VisualLineIndexTests.Repeat("abcd efgh ", 60); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewChunked(width: 100, height: 30); viewport.ScrollToCaret(paged, 0, caretAtLineEnd: false); CheckTrue(viewport.WindowEnd > 0 && viewport.WindowEnd < text.Length); } public void OnALongLineThatEndsInTheBox_ReachesTheLinesAfterIt() { PagedText paged = PagedTextTests.Paged(LongLineTexts[6]); TextViewport viewport = NewChunked(width: 100, height: 400); viewport.ScrollToCaret(paged, 0, caretAtLineEnd: false); CheckTrue(viewport.WindowEnd > paged.EndOfLine(0) + 1); } public void HoldsAsManyHardLinesAsTheBoxShows() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); CheckEqual(13, viewport.WindowEnd); } public void FollowsTheScroll() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 2); CheckEqual(24, viewport.WindowEnd); } public void NearTheEndOfTheText_IsTheEndOfTheText() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 4); CheckEqual(SevenLines.Length, viewport.WindowEnd); } public void AHardLineThatWraps_StaysWhole() { TextViewport viewport = NewViewport(multiline: true, width: 100, height: 10); viewport.ScrollToCaret("aaaa bbbb cccc dddd\nxx\nyy\nzz", caret: 0, caretAtLineEnd: false); CheckEqual(19, viewport.WindowEnd); } public void WithNoLimitOnTheHeight_IsTheEndOfTheText() { TextViewport viewport = NewViewport(multiline: true); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); CheckEqual(SevenLines.Length, viewport.WindowEnd); } public void SingleLine_IsTheEndOfTheText() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); CheckEqual(16, viewport.WindowEnd); } public void WithoutAFont_IsTheEndOfTheText() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); CheckEqual(SevenLines.Length, viewport.WindowEnd); } public void CaretAtClampsToTheEndOfTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); CheckEqual(new CaretPosition(Line: 2, X: 20, Y: 20), viewport.CaretAt(30, atLineEnd: false)); } public void AddBandsClampsARangePastTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); BandsMaterial bands = NewBands(); viewport.AddBands(bands, start: 2, end: 30); CheckEqual(3, bands.Count); } public void ACaretPastTheWindow_IsOutside() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); Check(viewport.IsInWindow(13, atLineEnd: false)); Check(!viewport.IsInWindow(14, atLineEnd: false)); } public void AFarJump_PutsTheCaretsLineAtTheBottomThroughWrappedLines() { const string wrapped = "aaaa bbbb cccc dddd eeee\nxx\nyy\nzz"; TextViewport viewport = NewViewport(multiline: true, width: 100, height: 50); viewport.ScrollToCaret(wrapped, caret: wrapped.Length, caretAtLineEnd: false); CheckEqual(10, viewport.FirstVisible); } public void AFarJump_PutsTheCaretsLineAtTheBottomThroughHardLines() { const string wrapped = "aaaa bbbb cccc dddd eeee\nxx\nyy\nzz"; TextViewport viewport = NewViewport(multiline: true, width: 100, height: 20); viewport.ScrollToCaret(wrapped, caret: wrapped.Length, caretAtLineEnd: false); CheckEqual(wrapped.IndexOf("yy"), viewport.FirstVisible); } public void AFarJump_CountsEmptyLines() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret("a\n\n\n\nb", caret: 6, caretAtLineEnd: false); CheckEqual(3, viewport.FirstVisible); } public void AFarJump_IntoALongTextLaysOutOnlyTheLinesItNeeds() { var longText = new System.Text.StringBuilder(); for (int i = 0; i < 20000; ++i) { longText.Append("ab\n"); } longText.Append("end"); TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(longText.ToString(), caret: longText.Length, caretAtLineEnd: false); CheckEqual(longText.Length - 9, viewport.FirstVisible); } public void AFarJump_ToTheStartOfTheTextFromTheEnd() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); CheckEqual(0, viewport.FirstVisible); } } public class ScrollToXTests : TestFixture { public void SetsTheOffset() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); CheckEqual(50, viewport.ScrollX); } public void PastTheEnd_StopsWhereTheCaretFitsAfterTheWidestLine() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 500); CheckEqual(102, viewport.ScrollX); } public void Negative_ScrollsToTheStart() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); viewport.ScrollToX(WideLines, -5); CheckEqual(0, viewport.ScrollX); } public void WhenWrapping_DoesNothing() { TextViewport viewport = NewViewport(multiline: true); viewport.ScrollToX(WideLines, 50); CheckEqual(0, viewport.ScrollX); } public void CaretAtStaysInTheTextsOwnUnits() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); CheckEqual(120, viewport.CaretAt(15, atLineEnd: false).X); } public static void CheckWideTexts(int first, int last, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { for (int i = first; i <= last; ++i) { string difference = SidewaysScrollToXDifferences(LongLineTexts[i], width, alignment); if (difference.Length > 0) { throw new InvalidOperationException($"Text {i}: {difference}"); } } } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_RightAligned_1() { CheckWideTexts(0, 2, width: 100, Alignment.Horizontal.Right); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_RightAligned_2() { CheckWideTexts(3, 6, width: 100, Alignment.Horizontal.Right); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_Centred_1() { CheckWideTexts(0, 2, width: 100, Alignment.Horizontal.Center); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_Centred_2() { CheckWideTexts(3, 6, width: 100, Alignment.Horizontal.Center); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_First() { CheckWideTexts(0, 2, width: 100); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_Second() { CheckWideTexts(3, 6, width: 100); } public static readonly string[] SurrogateTexts = { VisualLineIndexTests.Repeat("ab" + VisualLineIndexTests.WidthChunkedLinesTests.Pair + "cd ", 30), VisualLineIndexTests.Repeat("a" + VisualLineIndexTests.WidthChunkedLinesTests.Pair, 70), VisualLineIndexTests.Repeat("ab" + (char)0xD800, 50), VisualLineIndexTests.Repeat("ab" + (char)0xDC00 + "c", 50), VisualLineIndexTests.Repeat(char.ConvertFromUtf32(0x1F1E6), 120), VisualLineIndexTests.Repeat("a" + char.ConvertFromUtf32(0x1F1E6), 70), }; public void OnWideLinesOfSurrogates_ShowWhatALayoutOfTheWholeLineDoes() { foreach (string text in SurrogateTexts) { string difference = SidewaysScrollToXDifferences(text, width: 100); if (difference.Length > 0) { throw new InvalidOperationException(difference); } } } public void OnAWideLine_PlacesTheRowsByTheirOrigins() { TextViewport viewport = NewSidewaysChunked(100); string text = "ab\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20) + "\ncd"; viewport.ScrollToX(text, 500); CheckTrue(viewport.HasRowOrigins); CheckEqual(500L, viewport.ScrollX); CheckEqual(-500L, viewport.RowOrigin(0)); } public void FarIntoAnUncountedLine_CountsOnToTheOffset() { string text = VisualLineIndexTests.Repeat("abcd efgh ", 2000); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewSidewaysChunked(100, synchronousChars: UncountedSynchronousChars); var words = new MeasuredWords(Style.Font, new StringView(text)); int expected = CaretLocator.IndexAt(words, 15000 + 33, y: 5, int.MaxValue, Alignment.Horizontal.Left, out _); viewport.ScrollToX(paged, 15000); CheckFalse(viewport.IsCounted(paged)); CheckEqual(15000L, viewport.ScrollX); CheckEqual(expected, viewport.IndexAt(33, 5, out _, out _)); } public void PastTheEndOfAnUncountedLine_StopsWhereTheCaretFitsAfterIt() { string text = VisualLineIndexTests.Repeat("abcd efgh ", 2000); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewSidewaysChunked(100, synchronousChars: UncountedSynchronousChars); TextViewport counted = NewSidewaysChunked(100, synchronousChars: UncountedSynchronousChars); CountAll(counted, paged); viewport.ScrollToX(paged, long.MaxValue); CheckEqual(counted.ContentWidth(paged) - 100, viewport.ScrollX); } public void OnALineWiderThanAnInt_ScrollsBeyondIt() { string text = VisualLineIndexTests.Repeat("ab", 3000); PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewSidewaysChunked(100, WideStyle); CountAll(viewport, paged); viewport.ScrollToX(paged, 3000000000L); CheckEqual(3000000000L, viewport.ScrollX); CheckEqual(3000, viewport.IndexAt(0, 5, out _, out _)); viewport.ScrollToX(paged, 6000000000L - 5); CheckEqual(5999999902L, viewport.ScrollX); } } public class ScrollXTests : TestFixture { public const string Longer = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; public void LeftAlignedText_StartsAtTheLeft() { TextViewport viewport = NewSideways(Alignment.Horizontal.Left); viewport.ContentWidth(WideLines); CheckEqual(0, viewport.ScrollX); } public void RightAlignedText_StartsAtTheRightEnd() { TextViewport viewport = NewSideways(Alignment.Horizontal.Right); viewport.ContentWidth(WideLines); CheckEqual(102, viewport.ScrollX); } public void CenteredText_StartsHalfWayAcross() { TextViewport viewport = NewSideways(Alignment.Horizontal.Center); viewport.ContentWidth(WideLines); CheckEqual(51, viewport.ScrollX); } public void RightAlignedText_KeepsItsPlaceAtTheRightEdgeAsItGrows() { TextViewport viewport = NewSideways(Alignment.Horizontal.Right); viewport.ContentWidth(WideLines); viewport.ContentWidth(Longer); CheckEqual(422, viewport.ScrollX); } public void LeftAlignedText_KeepsItsOffsetAsItGrows() { TextViewport viewport = NewSideways(Alignment.Horizontal.Left); viewport.ScrollToX(WideLines, 40); viewport.ContentWidth(Longer); CheckEqual(40, viewport.ScrollX); } public void ATextThatShrinks_ClampsTheOffset() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 102); viewport.Revalidate("ab"); CheckEqual(0, viewport.ScrollX); } public void RightAlignedText_ThatFitsTheView_DoesNotScroll() { TextViewport viewport = NewSideways(Alignment.Horizontal.Right); viewport.ContentWidth("ab"); CheckEqual(0, viewport.ScrollX); } } public class PastVerticalTests : TestFixture { public void InsideTheLinesShown_IsZero() { TextViewport viewport = NewViewport(multiline: true, height: 30); CheckEqual(0, viewport.PastVertical(0)); CheckEqual(0, viewport.PastVertical(29)); } public void AboveTheFirstLine_IsHowFarAbove() { CheckEqual(-5, NewViewport(multiline: true, height: 30).PastVertical(-5)); } public void BelowTheLastLine_IsHowFarBelow() { TextViewport viewport = NewViewport(multiline: true, height: 30); CheckEqual(1, viewport.PastVertical(30)); CheckEqual(16, viewport.PastVertical(45)); } public void ASliverUnderTheLastWholeLine_CountsAsBelow() { CheckEqual(1, NewViewport(multiline: true, height: 35).PastVertical(30)); } public void SingleLine_IsZero() { CheckEqual(0, NewViewport(multiline: false, height: 30).PastVertical(100)); } public void WithNoHeightLimit_IsZero() { CheckEqual(0, NewViewport(multiline: true).PastVertical(-100)); } public void WithoutAFont_IsZero() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); CheckEqual(0, viewport.PastVertical(100)); } } // A wrapped hard line on the second line of the text, wrapping at 10 chars into rows starting at 3 and 13. public const string WrappedBetween = "ab\naaaa bbbb cccc dddd\ncd"; /// /// A text that counts the chars it is asked for, to show how much of a long text a move reads. /// public class CountingSource : ITextSource { private readonly StringSource text; public int CharsRead; public CountingSource(string text) { this.text = new StringSource(text); } public int Length => text.Length; public int HardLineCount => text.HardLineCount; public int Version => text.Version; public char this[int index] { get { ++CharsRead; return text[index]; } } public void CopyTo(int start, int count, char[] destination, int destinationIndex) { CharsRead += count; text.CopyTo(start, count, destination, destinationIndex); } public StringView Slice(int start, int count, ref char[] scratch) { CharsRead += count; return text.Slice(start, count, ref scratch); } public int StartOfLine(int index) { return text.StartOfLine(index); } public int EndOfLine(int index) { return text.EndOfLine(index); } public int LengthAt(int index) { return text.LengthAt(index); } public int LengthBefore(int index) { return text.LengthBefore(index); } public int LineNumberOf(int index) { return text.LineNumberOf(index); } public int StartOfLineNumber(int line) { return text.StartOfLineNumber(line); } } public static string TwentyThousandShortLines() { return string.Concat(System.Linq.Enumerable.Repeat("ab\n", 20000)) + "end"; } public class LineEdgeTests : TestFixture { public void OnLongLines_AreWhereLayingOutTheWholeLineFindsThem() { foreach (string text in LongLineTexts) { CheckEqual("", LineEdgeDifferences(text, width: 100)); } } [Thorough] public void OnLongLines_AreWhereLayingOutTheWholeLineFindsThem_AtNarrowWidth() { foreach (string text in LongLineTexts) { CheckEqual("", LineEdgeDifferences(text, width: 35)); } } public void Start_OfALaterWrappedRow_LeadsToTheRowsStart() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge(WrappedBetween, caret: 14, caretAtLineEnd: false, end: false, out bool atLineEnd); CheckEqual(13, index); CheckFalse(atLineEnd); } public void Start_OfTheFirstRowOfALaterHardLine_LeadsToTheHardLinesStart() { TextViewport viewport = NewViewport(multiline: true); CheckEqual(3, viewport.LineEdge(WrappedBetween, caret: 5, caretAtLineEnd: false, end: false, out _)); } public void End_OfAnEarlierRowOfALaterHardLine_LeadsToTheWrapPoint() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge(WrappedBetween, caret: 5, caretAtLineEnd: false, end: true, out bool atLineEnd); CheckEqual(13, index); Check(atLineEnd); } public void End_OfTheLastRowOfALaterHardLine_LeadsToBeforeItsBreak() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge(WrappedBetween, caret: 14, caretAtLineEnd: false, end: true, out bool atLineEnd); CheckEqual(22, index); CheckFalse(atLineEnd); } public void ACaretShownAtTheEndOfAWrappedRow_IsOnThatRow() { TextViewport viewport = NewViewport(multiline: true); CheckEqual(3, viewport.LineEdge(WrappedBetween, caret: 13, caretAtLineEnd: true, end: false, out _)); CheckEqual(13, viewport.LineEdge(WrappedBetween, caret: 13, caretAtLineEnd: false, end: false, out _)); } public void AnEmptyLastLine_LeadsToTheEndOfTheText() { TextViewport viewport = NewViewport(multiline: true); CheckEqual(3, viewport.LineEdge("ab\n", caret: 3, caretAtLineEnd: false, end: true, out _)); CheckEqual(3, viewport.LineEdge("ab\n", caret: 3, caretAtLineEnd: false, end: false, out _)); } public void InALongText_ReadsOnlyTheLineTheCaretIsOn() { var source = new CountingSource(TwentyThousandShortLines()); TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge(source, caret: 30001, caretAtLineEnd: false, end: true, out _); CheckEqual(30002, index); Check(source.CharsRead < 100); } public void End_LeadsToTheEndOfTheHardLineBeforeItsBreak() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge(SevenLines, caret: 6, caretAtLineEnd: false, end: true, out bool atLineEnd); CheckEqual(10, index); CheckFalse(atLineEnd); } public void Start_LeadsToTheStartOfTheHardLine() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge(SevenLines, caret: 6, caretAtLineEnd: false, end: false, out bool atLineEnd); CheckEqual(4, index); CheckFalse(atLineEnd); } public void End_OfAWrappedLine_LeadsToTheWrapPointOnTheLineSide() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineEdge("aaaa bbbb cccc", caret: 0, caretAtLineEnd: false, end: true, out bool atLineEnd); CheckEqual(10, index); Check(atLineEnd); } } public class LineTargetTests : TestFixture { public static void CheckWideTexts(int first, int last, int width, Alignment.Horizontal alignment = Alignment.Horizontal.Left) { for (int i = first; i <= last; ++i) { string difference = SidewaysMoveDifferences(LongLineTexts[i], width, alignment); if (difference.Length > 0) { throw new InvalidOperationException($"Text {i}: {difference}"); } } } public void OnWideLines_KeepTheColumnTheRunBeganIn() { string line = VisualLineIndexTests.Repeat("abcd efgh ", 20); string text = line + "\nab\n" + line; PagedText paged = PagedTextTests.Paged(text); TextViewport chunked = NewSidewaysChunked(100); TextViewport whole = NewSideways(); CountAll(chunked, paged); var results = new int[2, 3]; int viewport = 0; foreach (TextViewport run in new[] { chunked, whole }) { int caret = 100; for (int step = 0; step < 3; ++step) { caret = run.LineTarget(paged, caret, false, lines: 1, forward: step < 2, version: 7, out _); run.KeepGoalColumn(7); results[viewport, step] = caret; } ++viewport; } for (int step = 0; step < 3; ++step) { CheckEqual(results[1, step], results[0, step]); } } public void OnWideLines_LeadWhereALayoutOfTheWholeLineDoes_RightAligned_1() { CheckWideTexts(0, 2, width: 100, Alignment.Horizontal.Right); } public void OnWideLines_LeadWhereALayoutOfTheWholeLineDoes_RightAligned_2() { CheckWideTexts(3, 6, width: 100, Alignment.Horizontal.Right); } public void OnWideLines_LeadWhereALayoutOfTheWholeLineDoes_Centred_1() { CheckWideTexts(0, 2, width: 100, Alignment.Horizontal.Center); } public void OnWideLines_LeadWhereALayoutOfTheWholeLineDoes_Centred_2() { CheckWideTexts(3, 6, width: 100, Alignment.Horizontal.Center); } public void OnWideLines_LeadWhereALayoutOfTheWholeLineDoes_First() { CheckWideTexts(0, 2, width: 100); } public void OnWideLines_LeadWhereALayoutOfTheWholeLineDoes_Second() { CheckWideTexts(3, 6, width: 100); } public void OnALongLine_MoreRowsThanAStretchHoldsAway_LeadToThatRow() { PagedText paged = PagedTextTests.Paged(CharsMeasuredTests.OneLine); TextViewport viewport = CharsMeasuredTests.NewCounted(paged); VisualLineIndex index = VisualLineIndexTests.NewIndex(blockChars: 16, longLineChars: 48); VisualLineIndexTests.CountAll(index, paged); int down = viewport.LineTarget(paged, 0, false, lines: 3000, forward: true, version: 1, out bool downAtLineEnd); int up = viewport.LineTarget(paged, paged.Length, false, lines: 3000, forward: false, version: 1, out bool upAtLineEnd); CheckEqual(3000, index.RowOf(paged, down, downAtLineEnd)); CheckEqual(index.TotalRows(paged) - 1 - 3000, index.RowOf(paged, up, upAtLineEnd)); } [Thorough] public void OnLongLines_LeadWhereLayingOutTheWholeLineLeads() { foreach (string text in LongLineTexts) { CheckEqual("", LineTargetDifferences(text, width: 100)); } } public void OnLongLines_LeadWhereLayingOutTheWholeLineLeads_AtNarrowWidth() { foreach (string text in LongLineTexts) { CheckEqual("", LineTargetDifferences(text, width: 35)); } } [Thorough] public void OnLongLines_KeepToTheGoalColumnOverARunOfMoves() { foreach (string text in LongLineTexts) { CheckEqual("", LineTargetRunDifferences(text, width: 100)); } } public void Forward_IntoAWrappedHardLine_LeadsToItsFirstRow() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(WrappedBetween, caret: 1, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); CheckEqual(4, index); } public void Forward_CountsTheRowsOfAWrappedHardLine() { TextViewport viewport = NewViewport(multiline: true); int second = viewport.LineTarget(WrappedBetween, caret: 1, caretAtLineEnd: false, lines: 2, forward: true, version: 1, out _); int past = viewport.LineTarget(WrappedBetween, caret: 1, caretAtLineEnd: false, lines: 3, forward: true, version: 1, out _); CheckEqual(14, second); CheckEqual(24, past); } public void Forward_FromALaterRow_LeadsToTheNextHardLine() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(WrappedBetween, caret: 14, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); CheckEqual(24, index); } public void Backward_IntoAWrappedHardLine_LeadsToItsLastRow() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(WrappedBetween, caret: 24, caretAtLineEnd: false, lines: 1, forward: false, version: 1, out _); CheckEqual(14, index); } public void Backward_CountsTheRowsOfAWrappedHardLine() { TextViewport viewport = NewViewport(multiline: true); int first = viewport.LineTarget(WrappedBetween, caret: 24, caretAtLineEnd: false, lines: 2, forward: false, version: 1, out _); int before = viewport.LineTarget(WrappedBetween, caret: 24, caretAtLineEnd: false, lines: 3, forward: false, version: 1, out _); CheckEqual(4, first); CheckEqual(1, before); } public void Backward_FromALaterRow_StaysInTheHardLine() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(WrappedBetween, caret: 14, caretAtLineEnd: false, lines: 1, forward: false, version: 1, out _); CheckEqual(4, index); } public void ACaretShownAtTheEndOfAWrappedRow_MovesFromThatRow() { TextViewport viewport = NewViewport(multiline: true); int up = viewport.LineTarget(WrappedBetween, caret: 13, caretAtLineEnd: true, lines: 1, forward: false, version: 1, out _); int down = viewport.LineTarget(WrappedBetween, caret: 13, caretAtLineEnd: false, lines: 1, forward: false, version: 2, out _); CheckEqual(2, up); CheckEqual(3, down); } public void ReportsWhenItLandsAtTheEndOfAWrappedRow() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget("abcdefghij\naaaa bbbb cccc", caret: 10, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out bool atLineEnd); CheckEqual(21, index); Check(atLineEnd); } public void CountsEmptyLines() { TextViewport viewport = NewViewport(multiline: true); int down = viewport.LineTarget("a\n\n\nb", caret: 0, caretAtLineEnd: false, lines: 3, forward: true, version: 1, out _); int up = viewport.LineTarget("a\n\n\nb", caret: 5, caretAtLineEnd: false, lines: 3, forward: false, version: 1, out _); CheckEqual(4, down); CheckEqual(1, up); } public void CountsACrLfAsOneBreak() { TextViewport viewport = NewViewport(multiline: true); int down = viewport.LineTarget("ab\r\ncd", caret: 1, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); int up = viewport.LineTarget("ab\r\ncd", caret: 5, caretAtLineEnd: false, lines: 1, forward: false, version: 1, out _); CheckEqual(5, down); CheckEqual(1, up); } public void ALineAfterATrailingBreak_IsReached() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget("ab\n", caret: 1, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); CheckEqual(3, index); } public void InALongText_ReadsOnlyTheLinesItCrosses() { var source = new CountingSource(TwentyThousandShortLines()); TextViewport viewport = NewViewport(multiline: true); int down = viewport.LineTarget(source, caret: 30001, caretAtLineEnd: false, lines: 5, forward: true, version: 1, out _); int up = viewport.LineTarget(source, caret: 30001, caretAtLineEnd: false, lines: 5, forward: false, version: 1, out _); CheckEqual(30016, down); CheckEqual(29986, up); Check(source.CharsRead < 100); } public void Forward_LeadsToTheSameColumnOnTheNextLine() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(SevenLines, caret: 6, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out bool atLineEnd); CheckEqual(13, index); CheckFalse(atLineEnd); } public void Backward_LeadsToTheSameColumnOnThePreviousLine() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(SevenLines, caret: 16, caretAtLineEnd: false, lines: 1, forward: false, version: 1, out _); CheckEqual(13, index); } public void PastTheFirstLine_LeadsToTheStartOfTheText() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(SevenLines, caret: 6, caretAtLineEnd: false, lines: 2, forward: false, version: 1, out _); CheckEqual(0, index); } public void PastTheLastLine_LeadsToTheEndOfTheText() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(SevenLines, caret: 6, caretAtLineEnd: false, lines: 10, forward: true, version: 1, out _); CheckEqual(SevenLines.Length, index); } public void WithAHugeLineCount_DoesNotOverflow() { TextViewport viewport = NewViewport(multiline: true); int index = viewport.LineTarget(SevenLines, caret: 6, caretAtLineEnd: false, lines: int.MaxValue, forward: true, version: 1, out _); CheckEqual(SevenLines.Length, index); } public void StartsANewRunWhenNoRunWasKept() { TextViewport viewport = NewViewport(multiline: true); int shorter = viewport.LineTarget(SevenLines, caret: 10, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); int index = viewport.LineTarget(SevenLines, shorter, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); CheckEqual(16, index); } } public class PageTargetTests : TestFixture { [Thorough] public void OnLongLines_LeadWhereLayingOutTheWholeLineLeads() { foreach (string text in LongLineTexts) { CheckEqual("", PageTargetDifferences(text, width: 100)); } } [Thorough] public void OnLongLines_LeadWhereLayingOutTheWholeLineLeads_AtNarrowWidth() { foreach (string text in LongLineTexts) { CheckEqual("", PageTargetDifferences(text, width: 35)); } } public void CountsTheRowsOfWrappedLinesAsLines() { const string text = "aaaa bbbb cccc dddd\nxx\nyy\nzz\nww"; TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(text, caret: 0, caretAtLineEnd: false); int first = viewport.PageTarget(text, caret: 0, caretAtLineEnd: false, forward: true, version: 1, out _); int firstTop = viewport.FirstVisible; int second = viewport.PageTarget(text, caret: first, caretAtLineEnd: false, forward: true, version: 1, out _); CheckEqual(23, first); CheckEqual(23, firstTop); CheckEqual(text.Length, second); CheckEqual(23, viewport.FirstVisible); } public void Forward_FromAWindowStartingOnAWrappedRow_CountsFromThatRow() { const string text = "aaaa bbbb cccc dddd\nxx\nyy\nzz\nww"; TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(text, 1); int index = viewport.PageTarget(text, caret: 10, caretAtLineEnd: false, forward: true, version: 1, out _); CheckEqual(26, index); CheckEqual(23, viewport.FirstVisible); } public void Backward_FromAWindowStartingOnAWrappedRow_CountsBackFromThatRow() { TextViewport viewport = NewViewport(multiline: true, height: 20); viewport.ScrollToLine(WrappedBetween, line: 2); CheckEqual(13, viewport.FirstVisible); int index = viewport.PageTarget(WrappedBetween, caret: 15, caretAtLineEnd: false, forward: false, version: 1, out _); CheckEqual(0, viewport.FirstVisible); CheckEqual(2, index); } public void TextShorterThanAPage_DoesNotScroll() { TextViewport viewport = NewViewport(multiline: true, height: 100); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); int down = viewport.PageTarget(SevenLines, caret: 6, caretAtLineEnd: false, forward: true, version: 1, out _); int up = viewport.PageTarget(SevenLines, caret: 6, caretAtLineEnd: false, forward: false, version: 1, out _); CheckEqual(SevenLines.Length, down); CheckEqual(0, up); CheckEqual(0, viewport.FirstVisible); } public void InALongText_ReadsOnlyThePagesItCrosses() { var source = new CountingSource(TwentyThousandShortLines()); TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(source, caret: 30000, caretAtLineEnd: false); source.CharsRead = 0; int down = viewport.PageTarget(source, caret: 30000, caretAtLineEnd: false, forward: true, version: 1, out _); int up = viewport.PageTarget(source, caret: 30000, caretAtLineEnd: false, forward: false, version: 1, out _); CheckEqual(30009, down); CheckEqual(29991, up); Check(source.CharsRead < 500); } public void Forward_ScrollsAPageAndMovesTheCaretTheSameDistance() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); int index = viewport.PageTarget(SevenLines, caret: 4, caretAtLineEnd: false, forward: true, version: 1, out _); CheckEqual(14, viewport.FirstVisible); CheckEqual(19, index); } public void Forward_StopsScrollingWhereTheLastLineFillsTheBottom() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); viewport.PageTarget(SevenLines, caret: 4, caretAtLineEnd: false, forward: true, version: 1, out _); int index = viewport.PageTarget(SevenLines, caret: 19, caretAtLineEnd: false, forward: true, version: 1, out _); CheckEqual(19, viewport.FirstVisible); CheckEqual(SevenLines.Length, index); } public void Backward_ScrollsAPageUpAndMovesTheCaretTheSameDistance() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); int index = viewport.PageTarget(SevenLines, caret: 25, caretAtLineEnd: false, forward: false, version: 1, out _); CheckEqual(4, viewport.FirstVisible); CheckEqual(11, index); } public void Backward_StopsScrollingAtTheStart() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: 4, caretAtLineEnd: false); int index = viewport.PageTarget(SevenLines, caret: 6, caretAtLineEnd: false, forward: false, version: 1, out _); CheckEqual(0, viewport.FirstVisible); CheckEqual(0, index); } public void WithNoHeightLimit_MovesAPageOfEveryLineWithoutScrolling() { TextViewport viewport = NewViewport(multiline: true); viewport.ScrollToCaret(SevenLines, caret: 0, caretAtLineEnd: false); int index = viewport.PageTarget(SevenLines, caret: 6, caretAtLineEnd: false, forward: true, version: 1, out _); CheckEqual(0, viewport.FirstVisible); CheckEqual(SevenLines.Length, index); } } public class TotalLinesTests : TestFixture { public void CountsEachHardLine() { CheckEqual(7, NewViewport(multiline: true).TotalLines(SevenLines)); } public void CountsTheLinesTextWrapsOnto() { CheckEqual(2, NewViewport(multiline: true, width: 100).TotalLines("aaaa bbbb cccc")); } public void EmptyText_IsOneLine() { CheckEqual(1, NewViewport(multiline: true).TotalLines("")); } public void CountsTheLineAfterATrailingBreak() { CheckEqual(2, NewViewport(multiline: true).TotalLines("ab\n")); } public void SingleLine_IsOneLine() { CheckEqual(1, NewViewport(multiline: false).TotalLines(SevenLines)); } public void WithoutAFont_IsOneLine() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: null); CheckEqual(1, viewport.TotalLines(SevenLines)); } public void CountsDifferentTextAgain() { TextViewport viewport = NewViewport(multiline: true); viewport.TotalLines("a\nb"); CheckEqual(3, viewport.TotalLines("a\nb\nc")); } public void CountsAgainWhenTheWidthChanges() { TextViewport viewport = NewViewport(multiline: true, width: 400); CheckEqual(1, viewport.TotalLines("aaaa bbbb cccc")); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: null); CheckEqual(2, viewport.TotalLines("aaaa bbbb cccc")); } } public class LinesForTests : TestFixture { public void CountsEachHardLine() { CheckEqual(3, NewViewport(multiline: true).LinesFor("a\nb\nc")); } public void EmptyText_IsOneLine() { CheckEqual(1, NewViewport(multiline: true).LinesFor("")); } public void CountsTheLineAfterATrailingBreak() { CheckEqual(2, NewViewport(multiline: true).LinesFor("ab\n")); } public void CountsTheLinesTextWrapsOntoAtTheViewsWidth() { CheckEqual(2, NewViewport(multiline: true, width: 100).LinesFor("aaaa bbbb cccc")); CheckEqual(1, NewViewport(multiline: true, width: 400).LinesFor("aaaa bbbb cccc")); } public void TextThatDoesNotWrap_CountsOnlyHardLines() { CheckEqual(2, NewSideways().LinesFor("abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstuvwxyz\nab")); } public void SingleLine_IsOne() { CheckEqual(1, NewViewport(multiline: false).LinesFor("a\nb")); } public void WithoutAFont_IsOne() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); CheckEqual(1, viewport.LinesFor("a\nb")); } public void LeavesWhatTheViewportHoldsLaidOutAlone() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 2); viewport.LinesFor("x\ny"); CheckEqual(7, viewport.TotalLines(SevenLines)); CheckEqual(2, viewport.TopLine(SevenLines)); } } public class LinesUpToTests : TestFixture { public void ATextWithinTheCap_IsCountedExactly() { CheckEqual(7, NewViewport(multiline: true).LinesUpTo(new StringSource(SevenLines), cap: 10)); } public void ATextAtTheCap_IsCountedExactly() { CheckEqual(7, NewViewport(multiline: true).LinesUpTo(new StringSource(SevenLines), cap: 7)); } public void AWrappedTextWithinTheCap_CountsItsRows() { TextViewport viewport = NewViewport(multiline: true, width: 50); CheckEqual(viewport.LinesFor(TwoRowLines(3)), viewport.LinesUpTo(LongPaged(3), cap: 100)); } public void ALongTextWithinTheCap_IsCountedExactly() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CheckEqual(24001, viewport.LinesUpTo(paged, cap: 30000)); CheckTrue(viewport.IsCounted(paged)); } public void ALongTextPastTheCap_StopsCountingOnceItPassesIt() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CheckGreater(viewport.LinesUpTo(paged, cap: 5), 5); CheckFalse(viewport.IsCounted(paged)); } public void ATextJustPastTheCap_IsAboveIt() { CheckGreater(NewViewport(multiline: true).LinesUpTo(new StringSource(SevenLines), cap: 6), 6); } public void SingleLine_IsOne() { CheckEqual(1, NewViewport(multiline: false).LinesUpTo(new StringSource(SevenLines), cap: 3)); } public void WithoutAFont_IsOne() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); CheckEqual(1, viewport.LinesUpTo(new StringSource(SevenLines), cap: 3)); } } public class LinesAddedTests : TestFixture { public static int ByLayingOutTheWholeText(TextViewport viewport, string text, int start, int length, string replacement) { string edited = text.Substring(0, start) + replacement + text.Substring(start + length); return viewport.LinesFor(edited) - viewport.LinesFor(text); } public void InsertingABreak_AddsALine() { CheckEqual(1, NewViewport(multiline: true).LinesAdded(new StringSource("ab\ncd"), 1, 0, "\n")); } public void DeletingABreak_TakesALineAway() { CheckEqual(-1, NewViewport(multiline: true).LinesAdded(new StringSource("ab\ncd"), 2, 1, "")); } public void ACharThatFitsTheLine_AddsNothing() { CheckEqual(0, NewViewport(multiline: true).LinesAdded(new StringSource("ab\ncd"), 1, 0, "x")); } public void ACharThatWrapsTheLine_AddsARow() { CheckEqual(1, NewViewport(multiline: true).LinesAdded(new StringSource("aaaa bbbb"), 9, 0, "cc")); } public void ReplacingTheSameChars_AddsNothing() { CheckEqual(0, NewViewport(multiline: true).LinesAdded(new StringSource("ab\ncd"), 0, 5, "ab\ncd")); } public void ReplacingSeveralLinesWithOne_TakesTheRestAway() { CheckEqual(-4, NewViewport(multiline: true).LinesAdded(new StringSource(SevenLines), 2, 20, "x")); } public void MatchesLayingOutTheWholeText() { string[] texts = { SevenLines, "aaaa bbbb cccc dddd eeee ffff", "ab\r\ncd\r\nef", "ab\ncd\n\n\nef", "", }; string[] replacements = { "", "x", "\n", "\r", "\r\n", "aaaa bbbb ", "q\nr\ns", " " }; TextViewport viewport = NewViewport(multiline: true); var mismatches = new System.Text.StringBuilder(); foreach (string text in texts) { var source = new StringSource(text); for (int start = 0; start <= text.Length; ++start) { foreach (int length in new[] { 0, 1, 5 }) { if (start + length > text.Length) { continue; } foreach (string replacement in replacements) { int expected = ByLayingOutTheWholeText(viewport, text, start, length, replacement); int actual = viewport.LinesAdded(source, start, length, replacement); if (expected != actual) { mismatches.AppendLine($"{text.Length}:{start}+{length}->{replacement.Length}:" + $" {expected} vs {actual} in '{text}' with '{replacement}'"); } } } } } CheckEqual("", mismatches.ToString()); } // Words of eight lengths, so the rows of a line cut at different places along it and an edit's effect on // the wrapping dies out within a few rows. public static string Prose(int cycles) { var returnMe = new System.Text.StringBuilder(); for (int cycle = 0; cycle < cycles; ++cycle) { foreach (int length in new[] { 3, 5, 2, 7, 4, 6, 1, 8 }) { returnMe.Append((char)('a' + (cycle + length) % 26), length).Append(' '); } } return returnMe.ToString(); } public static string Mismatches(TextViewport viewport, TextViewport reference, string[] texts, int step) { string[] replacements = { "", "x", "\n", " ", "abcd efgh ", "q\nr" }; var returnMe = new System.Text.StringBuilder(); foreach (string text in texts) { var source = new StringSource(text); for (int start = 0; start <= text.Length; start += step) { foreach (int length in new[] { 0, 1, 40 }) { if (start + length > text.Length) { continue; } foreach (string replacement in replacements) { int expected = ByLayingOutTheWholeText(reference, text, start, length, replacement); int actual = viewport.LinesAdded(source, start, length, replacement); if (expected != actual) { returnMe.Append($"{text.Length}:{start}+{length}->{replacement.Length}:" + $" {expected} vs {actual}; "); } } } } } return returnMe.ToString(); } public void OnProseOfSeveralChunks_MatchesLayingOutTheWholeText() { var viewport = new TextViewport(blockChars: 1024, longLineChars: 2048, stretchChars: 1024); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: 30); string[] texts = { Prose(55), "ab\n" + Prose(50) + "\ncd\n" + Prose(50) }; CheckEqual("", Mismatches(viewport, NewViewport(multiline: true), texts, step: 512)); } public void OnLongLinesThatDoNotWrap_MatchesLayingOutTheWholeText() { CheckEqual("", Mismatches(NewSidewaysChunked(width: 100), NewSideways(), LongLineTexts, step: 13)); } public void InTextThatIsNotCountedYet_IsExact() { PagedText paged = LongPaged(12000); string text = TwoRowLines(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); var mismatches = new System.Text.StringBuilder(); foreach (int start in new[] { 0, 7000, 35003, 35006, 83999 }) { foreach (string replacement in new[] { "", "\n", "x", "abc de\nabc de\n" }) { int expected = ByLayingOutTheWholeText(viewport, text, start, 0, replacement); int actual = viewport.LinesAdded(paged, start, 0, replacement); if (expected != actual) { mismatches.AppendLine($"{start} '{replacement}': {expected} vs {actual}"); } } } CheckEqual("", mismatches.ToString()); CheckFalse(viewport.IsCounted(paged)); } public void InTextThatIsNotCountedYet_RemovingARangeIsExact() { PagedText paged = LongPaged(12000); string text = TwoRowLines(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CheckEqual(ByLayingOutTheWholeText(viewport, text, 35006, 60, ""), viewport.LinesAdded(paged, 35006, 60, "")); } public void LeavesWhatTheViewportHoldsLaidOutAlone() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 2); viewport.LinesAdded(new StringSource(SevenLines), 0, 0, "x\ny"); CheckEqual(7, viewport.TotalLines(SevenLines)); CheckEqual(2, viewport.TopLine(SevenLines)); } public void SingleLine_AddsNothing() { CheckEqual(0, NewViewport(multiline: false).LinesAdded(new StringSource("ab"), 0, 0, "x\ny")); } public void WithoutAFont_AddsNothing() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); CheckEqual(0, viewport.LinesAdded(new StringSource("ab"), 0, 0, "x\ny")); } } public class TopLineTests : TestFixture { public void AtTheBottomOfManyLines_ReachesTheTotalWhileTheTextIsNotCounted() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); viewport.ScrollByLines(paged, int.MaxValue); CheckEqual(viewport.TotalLines(paged), viewport.TopLine(paged) + viewport.VisibleLines); CheckFalse(viewport.IsCounted(paged)); } public void AtTheBottomOfCountedText_IsTheTotalLessTheLinesShown() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, SevenLines.Length, caretAtLineEnd: false); CheckEqual(4, viewport.TopLine(SevenLines)); } public void StartsAtZero() { CheckEqual(0, NewViewport(multiline: true, height: 30).TopLine(SevenLines)); } public void CountsTheLinesAboveTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(SevenLines, caret: SevenLines.Length, caretAtLineEnd: false); CheckEqual(4, viewport.TopLine(SevenLines)); } public void CountsWrappedLinesAboveTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 10); viewport.ScrollToCaret("aaaa bbbb cccc dddd", caret: 19, caretAtLineEnd: false); CheckEqual(1, viewport.TopLine("aaaa bbbb cccc dddd")); } public void SingleLine_IsZeroHoweverFarItScrolls() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); CheckEqual(0, viewport.TopLine("abcdefghijklmnop")); } public void WithoutAFont_IsZero() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); CheckEqual(0, viewport.TopLine(SevenLines)); } } public class ScrollToLineTests : TestFixture { public static void CheckWideTexts(Alignment.Horizontal alignment) { foreach (int i in new[] { 1, 5, 6 }) { string difference = SidewaysLineScrollDifferences(LongLineTexts[i], width: 100, alignment); if (difference.Length > 0) { throw new InvalidOperationException($"Text {i}: {difference}"); } } } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes() { CheckWideTexts(Alignment.Horizontal.Left); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_RightAligned() { CheckWideTexts(Alignment.Horizontal.Right); } public void OnWideLines_ShowWhatALayoutOfTheWholeLineDoes_Centred() { CheckWideTexts(Alignment.Horizontal.Center); } public void OnLongLines_StartTheWindowWhereLayingOutTheWholeLineDoes() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToLineDifferences(text, width: 100)); } } [Thorough] public void OnLongLines_StartTheWindowWhereLayingOutTheWholeLineDoes_AtNarrowWidth() { foreach (string text in LongLineTexts) { CheckEqual("", ScrollToLineDifferences(text, width: 35)); } } public void StartsTheWindowAtThatLine() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 2); CheckEqual(11, viewport.FirstVisible); CheckEqual(2, viewport.TopLine(SevenLines)); } public void MeasuresTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 2); CheckEqual(new CaretPosition(Line: 1, X: 20, Y: 10), viewport.CaretAt(16, atLineEnd: false)); } public void PastTheLastLineThatFillsTheWindow_StopsThere() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 100); CheckEqual(4, viewport.TopLine(SevenLines)); } public void NegativeLine_ScrollsToTheStart() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 3); viewport.ScrollToLine(SevenLines, line: -3); CheckEqual(0, viewport.FirstVisible); } public void TextShorterThanTheWindow_StaysAtTheStart() { TextViewport viewport = NewViewport(multiline: true, height: 100); viewport.ScrollToLine(SevenLines, line: 3); CheckEqual(0, viewport.FirstVisible); } public void WithNoHeightLimit_StaysAtTheStart() { TextViewport viewport = NewViewport(multiline: true); viewport.ScrollToLine(SevenLines, line: int.MaxValue); CheckEqual(0, viewport.FirstVisible); } public void SingleLine_DoesNothing() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); viewport.ScrollToLine("abcdefghijklmnop", line: 0); CheckEqual(6, viewport.FirstVisible); } public void WithoutAFont_DoesNothing() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); viewport.ScrollToLine(SevenLines, line: 3); CheckEqual(0, viewport.FirstVisible); } } public class RevalidateTests : TestFixture { [Thorough] public void OnLongLines_FillTheWindowAsLayingOutTheWholeLineDoes() { foreach (string text in LongLineTexts) { CheckEqual("", RevalidateDifferences(text, width: 100)); CheckEqual("", RevalidateDifferences(text, width: 35)); } } public void ScrollsBackToFillAWindowThatGrew() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 4); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: 50); viewport.Revalidate(SevenLines); CheckEqual(11, viewport.FirstVisible); } public void AWindowWithNoHeightLimitAnymore_ScrollsBackToTheStart() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 4); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: null); viewport.Revalidate(SevenLines); CheckEqual(0, viewport.FirstVisible); CheckEqual(SevenLines.Length, viewport.WindowEnd); } public void KeepsAWindowThatStillFills() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 3); viewport.Revalidate(SevenLines); CheckEqual(14, viewport.FirstVisible); } public void MovesAWindowStartingMidLineToTheLinesStart() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 4); viewport.LimitTo(index: 6); viewport.Revalidate(SevenLines); CheckEqual(4, viewport.FirstVisible); } public void TextThatShrankPastTheWindow_ScrollsBackToFillIt() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 4); viewport.Revalidate(SevenLines.Substring(0, 12)); CheckEqual(0, viewport.FirstVisible); } public void MeasuresTheWindow() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 4); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: 50); viewport.Revalidate(SevenLines); CheckEqual(new CaretPosition(Line: 1, X: 20, Y: 10), viewport.CaretAt(16, atLineEnd: false)); } public void SingleLine_KeepsTheWindow() { TextViewport viewport = NewViewport(multiline: false); viewport.ScrollToCaret("abcdefghijklmnop", caret: 16, caretAtLineEnd: false); viewport.Revalidate("abcdefghijklmnop"); CheckEqual(6, viewport.FirstVisible); CheckEqual(100, viewport.CaretAt(16, atLineEnd: false).X); } public void WithoutAFont_ScrollsToTheStart() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); viewport.LimitTo(index: 0); viewport.Revalidate(SevenLines); CheckEqual(0, viewport.FirstVisible); } } public class IsInWindowTests : TestFixture { public static TextViewport ScrolledToLineTwo() { TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToLine(SevenLines, line: 2); return viewport; } public void AnIndexBeforeTheWindow_IsOutside() { Check(!ScrolledToLineTwo().IsInWindow(4, atLineEnd: false)); } public void TheWindowsFirstIndex_IsInside() { Check(ScrolledToLineTwo().IsInWindow(11, atLineEnd: false)); } public void TheEndOfTheLastLineShown_IsInside() { Check(ScrolledToLineTwo().IsInWindow(24, atLineEnd: false)); } public void AnIndexBelowTheLastLineShown_IsOutside() { Check(!ScrolledToLineTwo().IsInWindow(25, atLineEnd: false)); } public void AnIndexOnAWrappedRowBelowTheLastLineShown_IsOutside() { TextViewport viewport = NewViewport(multiline: true, height: 20); viewport.ScrollToCaret(WrappedBetween, caret: 0, caretAtLineEnd: false); Check(viewport.IsInWindow(5, atLineEnd: false)); Check(!viewport.IsInWindow(15, atLineEnd: false)); } public void Sideways_ACaretLeftOfTheViewIsOutside() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); Check(!viewport.IsInWindow(3, atLineEnd: false)); } public void Sideways_ACaretInTheViewIsInside() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); Check(viewport.IsInWindow(9, atLineEnd: false)); } public void Sideways_ACaretRightOfTheViewIsOutside() { TextViewport viewport = NewSideways(); viewport.ScrollToX(WideLines, 50); Check(!viewport.IsInWindow(23, atLineEnd: false)); } public void SingleLine_IsAlwaysInside() { Check(NewViewport(multiline: false).IsInWindow(5000, atLineEnd: false)); } public void WithoutAFont_IsAlwaysInside() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 100, fitHeight: 30); Check(viewport.IsInWindow(5000, atLineEnd: false)); } } /// /// Runs the same thing over a string source, a paged text and a text with a range spliced into it, which hold /// the same chars, and checks all of them give the same answer. /// public static void SameOnEverySource(string text, Func run) { string expected = run(new StringSource(text)); CheckEqualStatic(expected, run(PagedTextTests.Paged(text))); int start = text.Length / 3; int end = text.Length * 2 / 3; var spliced = new SplicedText(); spliced.Set(PagedTextTests.Paged(text.Substring(0, start) + "XYZ" + text.Substring(end)), start, start + 3, text.Substring(start, end - start)); CheckEqualStatic(expected, run(spliced)); } private static void CheckEqualStatic(string expected, string actual) { if (expected != actual) { throw new InvalidOperationException($"Expected \"{expected}\" but was \"{actual}\"."); } } // Twenty copies of the seven lines, so the text runs over many of the tiny pages. public static readonly string ManyLines = string.Concat(System.Linq.Enumerable.Repeat(SevenLines + "\n", 20)); public static string WindowAfter(TextViewport viewport) { return $"{viewport.FirstVisible},{viewport.WindowEnd},{viewport.ScrollX}"; } public class ScrollToCaretSourceTests : TestFixture { public void LaysOutTheSameWindowWhereverTheCaretIs() { foreach (int caret in new[] { 0, 6, ManyLines.Length / 2, ManyLines.Length }) { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); viewport.ScrollToCaret(source, caret, caretAtLineEnd: false); return WindowAfter(viewport); }); } } public void ASingleLineWindow_ScrollsTheSame() { SameOnEverySource(new string('a', 40) + "bcdefg", source => { TextViewport viewport = NewViewport(multiline: false, width: 100); viewport.ScrollToCaret(source, source.Length, caretAtLineEnd: false); return WindowAfter(viewport); }); } public void LaysOutAgainWhenThePagedTextChanges() { PagedText paged = PagedTextTests.Paged(SevenLines); TextViewport viewport = NewViewport(multiline: true, height: 30); viewport.ScrollToCaret(paged, SevenLines.Length, caretAtLineEnd: false); CheckEqual(4, viewport.TopLine(paged)); paged.Replace(0, 0, "x\ny\nz\n"); viewport.ScrollToCaret(paged, paged.Length, caretAtLineEnd: false); CheckEqual(7, viewport.TopLine(paged)); } public void LaysOutAgainWhenTheSplicedTextIsSetAgain() { var spliced = new SplicedText(); var text = new StringSource("a\nb"); spliced.Set(text, 0, 1, "q"); TextViewport viewport = NewViewport(multiline: true, height: 30); CheckEqual(2, viewport.TotalLines(spliced)); spliced.Set(text, 0, 1, "q\nr\ns"); CheckEqual(4, viewport.TotalLines(spliced)); } } public class SourceQueriesTests : TestFixture { public void TotalLinesAndTopLine_AreTheSame() { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); viewport.ScrollToCaret(source, source.Length / 2, caretAtLineEnd: false); return viewport.TotalLines(source) + "," + viewport.TopLine(source); }); } public void ScrollToLine_ScrollsTheSame() { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); viewport.ScrollToLine(source, 17); return WindowAfter(viewport); }); } public void Revalidate_KeepsTheSameWindow() { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); viewport.ScrollToLine(source, 17); viewport.Revalidate(source); return WindowAfter(viewport); }); } public void LineEdge_LeadsToTheSameIndex() { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); int caret = source.Length / 2; int home = viewport.LineEdge(source, caret, false, end: false, out bool homeAtEnd); int end = viewport.LineEdge(source, caret, false, end: true, out bool endAtEnd); return $"{home},{homeAtEnd},{end},{endAtEnd}"; }); } public void LineTarget_LeadsToTheSameIndex() { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); int down = viewport.LineTarget(source, 70, false, lines: 3, forward: true, version: 1, out _); int up = viewport.LineTarget(source, 70, false, lines: 3, forward: false, version: 1, out _); return $"{down},{up}"; }); } public void PageTarget_ScrollsAndLeadsToTheSameIndex() { SameOnEverySource(ManyLines, source => { TextViewport viewport = NewViewport(multiline: true, width: 30, height: 30); viewport.ScrollToCaret(source, 0, caretAtLineEnd: false); int index = viewport.PageTarget(source, 4, false, forward: true, version: 1, out _); return $"{index},{viewport.FirstVisible}"; }); } public void ContentWidthAndScrollToX_AreTheSame() { SameOnEverySource(ManyLines + WideLines, source => { TextViewport viewport = NewSideways(); viewport.ScrollToX(source, 50); return viewport.ContentWidth(source) + "," + WindowAfter(viewport); }); } } // Lines of two rows each at 50 wide, enough of them to take longer than a question's worth of counting. public static string TwoRowLines(int lines) { return string.Concat(System.Linq.Enumerable.Repeat("abc de\n", lines)); } public static PagedText LongPaged(int lines) { return PagedTextTests.Paged(TwoRowLines(lines), PagedText.DefaultPageSize); } public static void CountAll(TextViewport viewport, ITextSource text) { int guard = 0; while (viewport.CountMore(text, 20000)) { if (++guard > 1000) { throw new InvalidOperationException("Counting never finished."); } } } public class CountMoreTests : TestFixture { public void FinishesCountingALongText() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CheckEqual(12001, viewport.TotalLines(paged)); CountAll(viewport, paged); CheckEqual(24001, viewport.TotalLines(paged)); } public void ReportsWhetherItCounted() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CheckTrue(viewport.CountMore(paged, 20000)); CountAll(viewport, paged); CheckFalse(viewport.CountMore(paged, 20000)); } public void SingleLine_CountsNothing() { TextViewport viewport = NewViewport(multiline: false); CheckFalse(viewport.CountMore(LongPaged(12000), 20000)); } public void WithoutAFont_CountsNothing() { var viewport = new TextViewport(); viewport.Configure(new TextMaterialStyle(), multiline: true, fitWidth: 50, fitHeight: 30); CheckFalse(viewport.CountMore(LongPaged(12000), 20000)); } public void ACompositionIsCountedThroughTheTextUnderneath() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); var spliced = new SplicedText(); spliced.Set(paged, 35000, 35000, "q"); CountAll(viewport, spliced); CheckEqual(24001, viewport.TotalLines(paged)); } public void NeverMovesTheWindow() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); viewport.ScrollToCaret(paged, caret: 49000, caretAtLineEnd: false); int first = viewport.FirstVisible; int end = viewport.WindowEnd; int topLineEstimated = viewport.TopLine(paged); CountAll(viewport, paged); CheckEqual(first, viewport.FirstVisible); CheckEqual(end, viewport.WindowEnd); CheckEqual(2 * topLineEstimated, viewport.TopLine(paged)); } } public class IsCountedTests : TestFixture { public void ALongText_IsNotUntilCounted() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CheckFalse(viewport.IsCounted(paged)); CountAll(viewport, paged); CheckTrue(viewport.IsCounted(paged)); } public void ASmallText_IsAtOnce() { CheckTrue(NewViewport(multiline: true).IsCounted(new StringSource(SevenLines))); } public void AWindowOfOneLine_HasNothingToCount() { CheckTrue(NewViewport(multiline: false).IsCounted(LongPaged(12000))); } public void ACompositionIsAskedOfTheTextUnderneath() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); var spliced = new SplicedText(); spliced.Set(paged, 35000, 35000, "q"); CheckFalse(viewport.IsCounted(spliced)); CountAll(viewport, paged); CheckTrue(viewport.IsCounted(spliced)); } } public class NoteEditTests : TestFixture { public void KeepsWhatWasCounted() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CountAll(viewport, paged); paged.Replace(35000, 0, "x"); viewport.NoteEdit(paged, 35000, 0, 1); CheckEqual(24001, viewport.TotalLines(paged)); CheckFalse(viewport.CountMore(paged, 20000)); } public void IsHowAnEditAddsLines() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CountAll(viewport, paged); paged.Replace(35000, 0, "\n\n"); viewport.NoteEdit(paged, 35000, 0, 2); CheckEqual(24003, viewport.TotalLines(paged)); CheckFalse(viewport.CountMore(paged, 20000)); } public void WithoutIt_TheTextIsCountedAgain() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CountAll(viewport, paged); paged.Replace(35000, 0, "x"); CheckTrue(viewport.CountMore(paged, 20000)); CountAll(viewport, paged); CheckEqual(24001, viewport.TotalLines(paged)); } } public static readonly string WideLineText = "ab\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20) + "\ncd"; public class HasRowOriginsTests : TestFixture { public void WithAWideLineInTheWindow_Is() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 0); CheckTrue(viewport.HasRowOrigins); } public void WithOnlyShortLinesInTheWindow_Isnt() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX("ab\ncd\nef", 0); CheckFalse(viewport.HasRowOrigins); } public void WithAWideLineBelowTheWindow_Isnt() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX("ab\ncd\nef\n" + VisualLineIndexTests.Repeat("abcd efgh ", 20), 0); CheckFalse(viewport.HasRowOrigins); } public void WhenTheWindowHasNoHeightLimit_Isnt() { var viewport = new TextViewport(blockChars: 16, longLineChars: 48, stretchChars: 24); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: null, wrap: false, caretWidth: 2); viewport.ScrollToX(WideLineText, 0); CheckFalse(viewport.HasRowOrigins); } public void WhenRightAligned_Is() { TextViewport viewport = NewAlignedChunked(Alignment.Horizontal.Right, 100); viewport.ScrollToX(WideLineText, 0); CheckTrue(viewport.HasRowOrigins); } public void WhenCentred_Is() { TextViewport viewport = NewAlignedChunked(Alignment.Horizontal.Center, 100); viewport.ScrollToX(WideLineText, 0); CheckTrue(viewport.HasRowOrigins); } public void WhenWrapping_Isnt() { TextViewport viewport = NewChunked(width: 100, height: 30); viewport.ScrollToX(WideLineText, 0); CheckFalse(viewport.HasRowOrigins); } public void InAComposedText_WithAWideLine_Is() { TextViewport viewport = NewSidewaysChunked(100); var spliced = new SplicedText(); spliced.Set(PagedTextTests.Paged(WideLineText), 5, 5, "xy"); viewport.ScrollToX(spliced, 0); CheckTrue(viewport.HasRowOrigins); } public void ForgetsAWideLineThatIsGone() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 0); viewport.ScrollToX("ab\ncd", 0); CheckFalse(viewport.HasRowOrigins); } } public class RowOriginTests : TestFixture { public void OfAShortRow_IsTheScrollBackwards() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 70); CheckEqual(-70L, viewport.RowOrigin(0)); } public void OfAWideRow_PutsItsFirstCharJustBeforeTheView() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 500); long origin = viewport.RowOrigin(1); CheckTrue(origin <= 0); CheckTrue(origin > -100); CheckEqual(viewport.RowOriginCount, 3); } public void OfShortRowsRightAlignedBesideALineWiderThanAnInt_AreLongs() { string text = VisualLineIndexTests.Repeat("ab", 3000) + "\ncd\nef\ngh"; PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewAlignedChunked(Alignment.Horizontal.Right, 100, WideStyle); CountAll(viewport, paged); viewport.ScrollToLine(paged, 1); CheckTrue(viewport.HasRowOrigins); CheckEqual(6001, viewport.FirstVisible); CheckEqual(6000L * 1000000 + 2 - 100, viewport.ScrollX); CheckEqual(-2000000L + 98, viewport.RowOrigin(0)); CheckEqual(98, viewport.CaretAt(6003, atLineEnd: false).X); } public void OfShortRowsCentredBesideALineWiderThanAnInt_AreLongs() { string text = VisualLineIndexTests.Repeat("ab", 3000) + "\ncd\nef\ngh"; PagedText paged = PagedTextTests.Paged(text); TextViewport viewport = NewAlignedChunked(Alignment.Horizontal.Center, 100, WideStyle); CountAll(viewport, paged); viewport.ScrollToLine(paged, 1); long start = (6000L * 1000000 - 2000000) / 2 - viewport.ScrollX; CheckEqual(start, viewport.RowOrigin(0)); } public void OfTheWidestLine_StaysAtTheStartWhileTheLineIsReckoned() { string text = VisualLineIndexTests.Repeat("abcd efgh ", 2000); PagedText paged = PagedTextTests.Paged(text); foreach (Alignment.Horizontal alignment in new[] { Alignment.Horizontal.Right, Alignment.Horizontal.Center }) { TextViewport viewport = NewAlignedChunked(alignment, 100, synchronousChars: UncountedSynchronousChars); viewport.ScrollToX(paged, 12000); long firstGlyphAt = viewport.RowOrigin(0) + viewport.ScrollX; for (int step = 0; step < 5; ++step) { viewport.CountMore(paged, 4000); viewport.ScrollToX(paged, 12000); CheckEqual(12000L, viewport.ScrollX); CheckEqual(firstGlyphAt, viewport.RowOrigin(0) + viewport.ScrollX); } } } public void OfAWindowOfManyRows_AnswersForEveryRow() { var viewport = new TextViewport(blockChars: 16, longLineChars: 48, stretchChars: 24); viewport.Configure(Style, multiline: true, fitWidth: 100, fitHeight: 400, wrap: false, caretWidth: 2); string text = VisualLineIndexTests.Repeat("ab\n", 20) + VisualLineIndexTests.Repeat("abcd efgh ", 20) + "\ncd"; viewport.ScrollToX(text, 300); CheckEqual(22, viewport.RowOriginCount); CheckEqual(-300L, viewport.RowOrigin(0)); CheckEqual(-300L, viewport.RowOrigin(19)); CheckTrue(viewport.RowOrigin(20) > -400 && viewport.RowOrigin(20) <= 0); CheckEqual(-300L, viewport.RowOrigin(21)); } public void WithoutOrigins_Throws() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX("ab", 0); CheckThrow(typeof(ArgumentOutOfRangeException)); viewport.RowOrigin(0); } public void OfARowTheWindowLacks_Throws() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 0); CheckThrow(typeof(ArgumentOutOfRangeException)); viewport.RowOrigin(3); } public void OfARowBeforeTheFirst_Throws() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 0); CheckThrow(typeof(ArgumentOutOfRangeException)); viewport.RowOrigin(-1); } } public class WindowTextTests : TestFixture { public void BeforeAnythingIsLaidOut_IsEmpty() { CheckTrue(new TextViewport().WindowText.IsDefault); } public void OfAWideLine_IsThePartAroundTheViewJoinedToTheLinesAroundIt() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX(WideLineText, 300); string window = viewport.WindowText.ToString(); CheckTrue(window.StartsWith("ab\n")); CheckTrue(window.EndsWith("\ncd")); CheckGreater(WideLineText.Length - 10, window.Length); CheckTrue(WideLineText.Contains(window.Substring(3, window.Length - 6))); } public void OfShortLines_IsTheLines() { TextViewport viewport = NewSidewaysChunked(100); viewport.ScrollToX("ab\ncd\nef", 0); CheckEqual("ab\ncd\nef", viewport.WindowText.ToString()); } } public class CompositionInALongTextTests : TestFixture { public void AddsTheLinesItBrings() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CountAll(viewport, paged); var spliced = new SplicedText(); spliced.Set(paged, 35000, 35000, "\n\n"); CheckEqual(24003, viewport.TotalLines(spliced)); } public void ReplacesTheLinesItStandsFor() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CountAll(viewport, paged); var spliced = new SplicedText(); spliced.Set(paged, 35000, 35014, "x"); CheckEqual(24001 - 4, viewport.TotalLines(spliced)); } public void ScrollsToALineBelowIt() { PagedText paged = LongPaged(12000); TextViewport viewport = NewViewport(multiline: true, width: 50, height: 30); CountAll(viewport, paged); var spliced = new SplicedText(); spliced.Set(paged, 35000, 35000, "\n\n"); viewport.ScrollToLine(spliced, 10010); CheckEqual(10010, viewport.TopLine(spliced)); } } public class KeepGoalColumnTests : TestFixture { public void IsCheckedAgainstTheVersionOfTheNextMove() { TextViewport viewport = NewViewport(multiline: true); int shorter = viewport.LineTarget(SevenLines, caret: 10, caretAtLineEnd: false, lines: 1, forward: true, version: 1, out _); viewport.KeepGoalColumn(version: 7); CheckEqual(18, viewport.LineTarget(SevenLines, shorter, caretAtLineEnd: false, lines: 1, forward: true, version: 7, out _)); CheckEqual(16, viewport.LineTarget(SevenLines, shorter, caretAtLineEnd: false, lines: 1, forward: true, version: 8, out _)); } } } }