using System; using Amphora; using Glass.Editing; using UnitTestSharp; namespace Glass.UnitTests.Editing { public class EditHistoryTests : TestFixture { public static bool Record(EditHistory history, EditRun run, int start, string removed, string inserted, int versionBefore, int versionAfter) { var before = new EditState(caret: start, anchor: start, caretAtLineEnd: false, versionBefore); var after = new EditState(caret: start + inserted.Length, anchor: start + inserted.Length, caretAtLineEnd: false, versionAfter); return history.Record(run, start, new StringView(removed), new StringView(inserted), before, after); } public class CtorTests : TestFixture { public void StoresTheBudget() { CheckEqual(50L, new EditHistory(50).Budget); } public void NegativeBudget_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new EditHistory(-1); } public void StartsEmpty() { var history = new EditHistory(50); CheckEqual(0, history.Count); CheckEqual(0L, history.StoredLength); CheckFalse(history.CanUndo); CheckFalse(history.CanRedo); } } public class RoleOfTests : TestFixture { public void ALetter_IsWord() { CheckEqual(CharacterRole.Word, EditHistory.RoleOf('a')); } public void ADigit_IsWord() { CheckEqual(CharacterRole.Word, EditHistory.RoleOf('7')); } public void Punctuation_IsWord() { CheckEqual(CharacterRole.Word, EditHistory.RoleOf(',')); } public void ASpace_IsSpace() { CheckEqual(CharacterRole.Space, EditHistory.RoleOf(' ')); } public void ALineBreak_IsSpace() { CheckEqual(CharacterRole.Space, EditHistory.RoleOf('\n')); CheckEqual(CharacterRole.Space, EditHistory.RoleOf('\r')); } public void AZeroWidthSpace_IsSpace() { CheckEqual(CharacterRole.Space, EditHistory.RoleOf(0x200B)); } public void AHanCharacter_IsIdeograph() { CheckEqual(CharacterRole.Ideograph, EditHistory.RoleOf(0x4F60)); } public void AHiraganaCharacter_IsIdeograph() { CheckEqual(CharacterRole.Ideograph, EditHistory.RoleOf(0x3053)); } public void AnAstralHanCharacter_IsIdeograph() { CheckEqual(CharacterRole.Ideograph, EditHistory.RoleOf(0x20BB7)); } public void AnAstralLetter_IsWord() { CheckEqual(CharacterRole.Word, EditHistory.RoleOf(0x10400)); } public void AHangulSyllable_IsWord() { CheckEqual(CharacterRole.Word, EditHistory.RoleOf(0xD55C)); } public void ANumberPastUnicode_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); EditHistory.RoleOf(0x110000); } } public class BudgetTests : TestFixture { public void SettingIt_ChangesIt() { var history = new EditHistory(50); history.Budget = 80; CheckEqual(80L, history.Budget); } public void Negative_Throws() { var history = new EditHistory(50); CheckThrow(typeof(ArgumentOutOfRangeException)); history.Budget = -1; } public void Zero_ClearsTheHistoryAndKeepsNothingNew() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "abc", versionBefore: 0, versionAfter: 1); history.Budget = 0; Record(history, EditRun.Other, 0, "", "abc", versionBefore: 1, versionAfter: 2); CheckEqual(0, history.Count); CheckEqual(0L, history.StoredLength); } } public static bool RecordSides(EditHistory history, EditRun run, int start, EditSide removed, EditSide inserted, int versionBefore, int versionAfter) { var before = new EditState(caret: start, anchor: start, caretAtLineEnd: false, versionBefore); var after = new EditState(caret: start, anchor: start, caretAtLineEnd: false, versionAfter); return history.Record(run, start, removed, inserted, before, after); } public class PagedRecordTests : TestFixture { public void PagesTakenOut_CountInTheStoredLength() { var history = new EditHistory(500); RecordSides(history, EditRun.Other, 0, EditSide.OfPages(StepTextTests.Pages(30)), EditSide.OfChars(new StringView("ab")), versionBefore: 0, versionAfter: 1); CheckEqual(32L, history.StoredLength); } public void TextInTheDocument_CountsNothing() { var history = new EditHistory(500); RecordSides(history, EditRun.Other, 0, EditSide.OfChars(new StringView("abc")), EditSide.InDocument(900), versionBefore: 0, versionAfter: 1); CheckEqual(3L, history.StoredLength); } public void AStepWithPages_BeginsAStep() { var history = new EditHistory(500); CheckTrue(RecordSides(history, EditRun.Delete, 0, EditSide.OfPages(StepTextTests.Pages(30)), EditSide.OfChars(new StringView("")), versionBefore: 0, versionAfter: 1)); } public void AStepWithPages_IsNotJoinedByTheNextDelete() { var history = new EditHistory(500); RecordSides(history, EditRun.Delete, 0, EditSide.OfPages(StepTextTests.Pages(30)), EditSide.OfChars(new StringView("")), versionBefore: 0, versionAfter: 1); bool began = RecordSides(history, EditRun.Delete, 0, EditSide.OfChars(new StringView("a")), EditSide.OfChars(new StringView("")), versionBefore: 1, versionAfter: 2); CheckTrue(began); CheckEqual(2, history.Count); } public void AStepWithTextInTheDocument_IsNotJoinedByTheNextTyping() { var history = new EditHistory(500); RecordSides(history, EditRun.Typing, 0, EditSide.OfChars(new StringView("")), EditSide.InDocument(900), versionBefore: 0, versionAfter: 1); bool began = RecordSides(history, EditRun.Typing, 900, EditSide.OfChars(new StringView("")), EditSide.OfChars(new StringView("a")), versionBefore: 1, versionAfter: 2); CheckTrue(began); CheckEqual(2, history.Count); } public void ATypedStepBeforeAPagedOne_StaysAsItWas() { var history = new EditHistory(500); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); bool began = RecordSides(history, EditRun.Typing, 1, EditSide.OfChars(new StringView("")), EditSide.InDocument(900), versionBefore: 1, versionAfter: 2); CheckTrue(began); CheckEqual(2, history.Count); CheckEqual(900, history.StepBack().Inserted.Length); CheckEqual("a", history.StepBack().Inserted.Chars.ToString()); } public void ACharStepAfterAPagedOne_CanStillBeJoined() { var history = new EditHistory(500); RecordSides(history, EditRun.Delete, 0, EditSide.OfPages(StepTextTests.Pages(30)), EditSide.OfChars(new StringView("")), versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 1, versionAfter: 2); bool began = Record(history, EditRun.Typing, 1, "", "b", versionBefore: 2, versionAfter: 3); CheckFalse(began); CheckEqual(2, history.Count); } public void WithABudgetOfZero_KeepsNothing() { var history = new EditHistory(0); RecordSides(history, EditRun.Other, 0, EditSide.OfPages(StepTextTests.Pages(30)), EditSide.InDocument(900), versionBefore: 0, versionAfter: 1); CheckEqual(0, history.Count); CheckEqual(0L, history.StoredLength); } public void AStepOverTheBudget_IsKeptWhileItIsTheNewest() { var history = new EditHistory(50); RecordSides(history, EditRun.Other, 0, EditSide.OfPages(StepTextTests.Pages(100)), EditSide.InDocument(10), versionBefore: 0, versionAfter: 1); CheckEqual(1, history.Count); CheckEqual(100L, history.StoredLength); } public void AStepWithPages_IsForgottenWhenANewerStepFillsTheBudget() { var history = new EditHistory(50); RecordSides(history, EditRun.Other, 0, EditSide.OfPages(StepTextTests.Pages(100)), EditSide.InDocument(10), versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 0, "", "xy", versionBefore: 1, versionAfter: 2); CheckEqual(1, history.Count); CheckEqual(2L, history.StoredLength); } public void AForgottenStepWithPages_IsReusedEmpty() { var history = new EditHistory(50); RecordSides(history, EditRun.Other, 0, EditSide.OfPages(StepTextTests.Pages(100)), EditSide.InDocument(10), versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 0, "", "xy", versionBefore: 1, versionAfter: 2); Record(history, EditRun.Other, 0, "q", "r", versionBefore: 2, versionAfter: 3); CheckEqual("q", history.Newest.Removed.Chars.ToString()); CheckEqual("r", history.Newest.Inserted.Chars.ToString()); } } public class ResizedTests : TestFixture { public void AfterTheSidesSwap_CountsWhatTheStepStoresNow() { var history = new EditHistory(500); RecordSides(history, EditRun.Other, 0, EditSide.OfPages(StepTextTests.Pages(100)), EditSide.InDocument(40), versionBefore: 0, versionAfter: 1); EditStep step = history.Newest; int before = step.StoredLength; step.Removed.Set(EditSide.InDocument(100)); step.Inserted.Set(EditSide.OfPages(StepTextTests.Pages(40))); history.Resized(step, before); CheckEqual(40L, history.StoredLength); } public void CountsAStepWhoseChangeGrew() { var history = new EditHistory(500); Record(history, EditRun.Other, 0, "ab", "c", versionBefore: 0, versionAfter: 1); EditStep step = history.Newest; step.Inserted.Chars.Append("defg", 0, 4); history.Resized(step, storedLengthBefore: 3); CheckEqual(7L, history.StoredLength); } } public class NewestTests : TestFixture { public void WithoutSteps_IsNull() { CheckNull(new EditHistory(50).Newest); } public void IsTheLastStepRecorded() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 4, "", "bc", versionBefore: 1, versionAfter: 2); CheckEqual(4, history.Newest.Start); CheckEqual("bc", history.Newest.Inserted.Chars.ToString()); } public void AfterAStepBack_IsTheStepBefore() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 4, "", "bc", versionBefore: 1, versionAfter: 2); history.StepBack(); CheckEqual(0, history.Newest.Start); } public void AfterEveryStepIsUndone_IsNull() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); history.StepBack(); CheckNull(history.Newest); } } public class RecordTests : TestFixture { public void CountsTheTextTakenOutAndPutIn() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "abc", "de", versionBefore: 0, versionAfter: 1); CheckEqual(5L, history.StoredLength); CheckEqual(1, history.Count); } public void AChangeThatBeginsAStep_ReturnsTrue() { var history = new EditHistory(50); CheckTrue(Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1)); CheckTrue(Record(history, EditRun.Other, 5, "", "xyz", versionBefore: 1, versionAfter: 2)); } public void AChangeThatJoinsTheLastStep_ReturnsFalse() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); CheckFalse(Record(history, EditRun.Typing, 1, "", "b", versionBefore: 1, versionAfter: 2)); } public void WithABudgetOfZero_ReturnsTrueAndKeepsNothing() { var history = new EditHistory(0); CheckTrue(Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1)); CheckTrue(Record(history, EditRun.Typing, 1, "", "b", versionBefore: 1, versionAfter: 2)); CheckEqual(0, history.Count); } public void KeepsWhereTheChangeWasAndWhatItDid() { var history = new EditHistory(50); Record(history, EditRun.Other, 3, "abc", "de", versionBefore: 0, versionAfter: 1); EditStep step = history.StepBack(); CheckEqual(3, step.Start); CheckEqual("abc", step.Removed.Chars.ToString()); CheckEqual("de", step.Inserted.Chars.ToString()); CheckEqual(EditRun.Other, step.Run); CheckEqual(0, step.Before.Version); CheckEqual(1, step.After.Version); } public void TypingThatContinues_JoinsTheLastStep() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 1, "", "b", versionBefore: 1, versionAfter: 2); CheckEqual(1, history.Count); CheckEqual(2L, history.StoredLength); CheckEqual("ab", history.StepBack().Inserted.Chars.ToString()); } public void ATypedStepKeepsWhereItBeganAndWhereItEnded() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 1, "", "b", versionBefore: 1, versionAfter: 2); EditStep step = history.StepBack(); CheckEqual(0, step.Before.Version); CheckEqual(2, step.After.Version); CheckEqual(2, step.After.Caret); } public void TypingAfterSomethingElseChangedTheBuffer_StartsANewStep() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 1, "", "b", versionBefore: 5, versionAfter: 6); CheckEqual(2, history.Count); } public void TypingThatDoesNotContinueFromTheEnd_StartsANewStep() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 4, "", "b", versionBefore: 1, versionAfter: 2); CheckEqual(2, history.Count); } public void ABackspaceThatContinues_JoinsAndPrependsWhatItRemoved() { var history = new EditHistory(50); Record(history, EditRun.Backspace, 4, "d", "", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Backspace, 3, "c", "", versionBefore: 1, versionAfter: 2); CheckEqual(1, history.Count); EditStep step = history.StepBack(); CheckEqual("cd", step.Removed.Chars.ToString()); CheckEqual(3, step.Start); } public void ABackspaceThatDoesNotContinue_StartsANewStep() { var history = new EditHistory(50); Record(history, EditRun.Backspace, 4, "d", "", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Backspace, 1, "a", "", versionBefore: 1, versionAfter: 2); CheckEqual(2, history.Count); } public void ADeleteThatContinues_JoinsAndAppendsWhatItRemoved() { var history = new EditHistory(50); Record(history, EditRun.Delete, 3, "c", "", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Delete, 3, "d", "", versionBefore: 1, versionAfter: 2); EditStep step = history.StepBack(); CheckEqual("cd", step.Removed.Chars.ToString()); CheckEqual(3, step.Start); } public void ADeleteThatDoesNotContinue_StartsANewStep() { var history = new EditHistory(50); Record(history, EditRun.Delete, 3, "c", "", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Delete, 5, "e", "", versionBefore: 1, versionAfter: 2); CheckEqual(2, history.Count); } public void ADifferentKindOfEdit_StartsANewStep() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Delete, 1, "b", "", versionBefore: 1, versionAfter: 2); CheckEqual(2, history.Count); } public void AnOtherEdit_NeverJoinsAnything() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "ab", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 2, "", "cd", versionBefore: 1, versionAfter: 2); CheckEqual(2, history.Count); } public void AMultiCharTypedStep_IsNeverJoined() { var history = new EditHistory(50); Record(history, EditRun.Typing, 0, "", "ab", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 2, "", "c", versionBefore: 1, versionAfter: 2); CheckEqual(2, history.Count); } public void RecordingAfterAStepBack_ForgetsTheStepsThatCouldBeRedone() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 1, "", "b", versionBefore: 1, versionAfter: 2); history.StepBack(); Record(history, EditRun.Other, 1, "", "c", versionBefore: 3, versionAfter: 4); CheckEqual(2, history.Count); CheckFalse(history.CanRedo); CheckEqual(2L, history.StoredLength); } public void OverBudget_ForgetsTheOldestStepButNeverTheLast() { var history = new EditHistory(4); Record(history, EditRun.Other, 0, "", "abc", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 3, "", "def", versionBefore: 1, versionAfter: 2); CheckEqual(1, history.Count); CheckEqual(3L, history.StoredLength); CheckEqual("def", history.StepBack().Inserted.Chars.ToString()); } public void AJoinedStepGrowingPastTheBudget_IsKept() { var history = new EditHistory(2); Record(history, EditRun.Typing, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Typing, 1, "", "b", versionBefore: 1, versionAfter: 2); Record(history, EditRun.Typing, 2, "", "c", versionBefore: 2, versionAfter: 3); CheckEqual(1, history.Count); CheckEqual("abc", history.StepBack().Inserted.Chars.ToString()); } public void ManyMoreStepsThanItStartsWithRoomFor_AreAllKept() { var history = new EditHistory(EditBuffer.UnlimitedUndo); for (int i = 0; i < 50; ++i) { Record(history, EditRun.Other, i, "", "x", versionBefore: i, versionAfter: i + 1); } CheckEqual(50, history.Count); CheckEqual(50L, history.StoredLength); } } public class StepBackTests : TestFixture { public void WithNothingRecorded_ReturnsNull() { CheckNull(new EditHistory(50).StepBack()); } public void ReturnsTheNewestStepFirst() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 1, "", "b", versionBefore: 1, versionAfter: 2); CheckEqual("b", history.StepBack().Inserted.Chars.ToString()); CheckEqual("a", history.StepBack().Inserted.Chars.ToString()); CheckNull(history.StepBack()); } public void MakesTheStepRedoable() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); history.StepBack(); Check(history.CanRedo); CheckFalse(history.CanUndo); } } public class StepForwardTests : TestFixture { public void WithNothingToRedo_ReturnsNull() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); CheckNull(history.StepForward()); } public void ReturnsTheStepThatWasSteppedBack() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 1, "", "b", versionBefore: 1, versionAfter: 2); history.StepBack(); history.StepBack(); CheckEqual("a", history.StepForward().Inserted.Chars.ToString()); CheckEqual("b", history.StepForward().Inserted.Chars.ToString()); CheckNull(history.StepForward()); } } public class ClearTests : TestFixture { public void ForgetsEveryStep() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); Record(history, EditRun.Other, 1, "", "b", versionBefore: 1, versionAfter: 2); history.StepBack(); history.Clear(); CheckEqual(0, history.Count); CheckEqual(0L, history.StoredLength); CheckFalse(history.CanUndo); CheckFalse(history.CanRedo); } public void LeavesItUsable() { var history = new EditHistory(50); Record(history, EditRun.Other, 0, "", "a", versionBefore: 0, versionAfter: 1); history.Clear(); Record(history, EditRun.Other, 0, "", "z", versionBefore: 1, versionAfter: 2); CheckEqual("z", history.StepBack().Inserted.Chars.ToString()); } } } }