aboutsummaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
authorNeil <nyamatongwe@gmail.com>2022-07-31 15:51:53 +1000
committerNeil <nyamatongwe@gmail.com>2022-07-31 15:51:53 +1000
commit926cb6f7d228b347db16a45e1f2632da475da1f0 (patch)
tree6263662cb8c023502d61b61eed34baa93ed35843 /test
parent535e20189d5a2dd9b43a6ea0a74749a50678d631 (diff)
downloadscintilla-mirror-926cb6f7d228b347db16a45e1f2632da475da1f0.tar.gz
Added change history which can display document changes (modified, saved, ...)
in the margin or in the text.
Diffstat (limited to 'test')
-rw-r--r--test/unit/Sci.natvis17
-rw-r--r--test/unit/UnitTester.vcxproj13
-rw-r--r--test/unit/makefile1
-rw-r--r--test/unit/test.mak1
-rw-r--r--test/unit/testCellBuffer.cxx593
5 files changed, 612 insertions, 13 deletions
diff --git a/test/unit/Sci.natvis b/test/unit/Sci.natvis
index d6ac2b96c..c0bd9d15d 100644
--- a/test/unit/Sci.natvis
+++ b/test/unit/Sci.natvis
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
- <Type Name="Scintilla::SplitVector&lt;*&gt;">
+ <Type Name="Scintilla::Internal::SplitVector&lt;*&gt;">
<DisplayString>{{size = {lengthBody}}}</DisplayString>
<Expand>
<Item Name="[size]">lengthBody</Item>
@@ -12,7 +12,20 @@
</IndexListItems>
</Expand>
</Type>
- <Type Name="Scintilla::Partitioning&lt;*&gt;">
+ <Type Name="Scintilla::Internal::XSparseVector&lt;*&gt;">
+ <DisplayString>{{size = {values->lengthBody}}}</DisplayString>
+ <Expand>
+ <IndexListItems>
+ <Size>values->lengthBody</Size>
+ <ValueNode>starts->body->body[($i&lt;starts->body->part1Length)?$i:$i+starts->body->gapLength]+($i&gt;starts->stepPartition?starts->stepLength:0)</ValueNode>
+ </IndexListItems>
+ <IndexListItems>
+ <Size>values->lengthBody</Size>
+ <ValueNode>values->body->body[($i&lt;values->body->part1Length)?$i:$i+values->body->gapLength]+($i&gt;values->stepPartition?values->stepLength:0)</ValueNode>
+ </IndexListItems>
+ </Expand>
+ </Type>
+ <Type Name="Scintilla::Internal::Partitioning&lt;*&gt;">
<DisplayString>{{size = {body->lengthBody}}}</DisplayString>
<Expand>
<IndexListItems>
diff --git a/test/unit/UnitTester.vcxproj b/test/unit/UnitTester.vcxproj
index 8d41cf383..38b0eb7cc 100644
--- a/test/unit/UnitTester.vcxproj
+++ b/test/unit/UnitTester.vcxproj
@@ -22,32 +22,32 @@
<ProjectGuid>{35688A27-D91B-453A-8A05-65A7F28DEFBF}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>UnitTester</RootNamespace>
- <WindowsTargetPlatformVersion>10.0.16299.0</WindowsTargetPlatformVersion>
+ <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
- <PlatformToolset>v141</PlatformToolset>
+ <PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
- <PlatformToolset>v141</PlatformToolset>
+ <PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
- <PlatformToolset>v141</PlatformToolset>
+ <PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
- <PlatformToolset>v141</PlatformToolset>
+ <PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
@@ -74,6 +74,8 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
+ <EnableClangTidyCodeAnalysis>true</EnableClangTidyCodeAnalysis>
+ <CodeAnalysisRuleSet>..\..\..\..\..\Users\Neil\SensibleRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
@@ -153,6 +155,7 @@
<ClCompile Include="..\..\src\CaseConvert.cxx" />
<ClCompile Include="..\..\src\CaseFolder.cxx" />
<ClCompile Include="..\..\src\CellBuffer.cxx" />
+ <ClCompile Include="..\..\src\ChangeHistory.cxx" />
<ClCompile Include="..\..\src\CharacterCategoryMap.cxx" />
<ClCompile Include="..\..\src\CharClassify.cxx" />
<ClCompile Include="..\..\src\ContractionState.cxx" />
diff --git a/test/unit/makefile b/test/unit/makefile
index 89766826f..46747931f 100644
--- a/test/unit/makefile
+++ b/test/unit/makefile
@@ -56,6 +56,7 @@ TESTEDSRC=\
../../src/CaseConvert.cxx \
../../src/CaseFolder.cxx \
../../src/CellBuffer.cxx \
+ ../../src/ChangeHistory.cxx \
../../src/CharacterCategoryMap.cxx \
../../src/CharClassify.cxx \
../../src/ContractionState.cxx \
diff --git a/test/unit/test.mak b/test/unit/test.mak
index 99e4882c2..10a6ebac7 100644
--- a/test/unit/test.mak
+++ b/test/unit/test.mak
@@ -15,6 +15,7 @@ TESTEDSRC=\
../../src/CaseConvert.cxx \
../../src/CaseFolder.cxx \
../../src/CellBuffer.cxx \
+ ../../src/ChangeHistory.cxx \
../../src/CharacterCategoryMap.cxx \
../../src/CharClassify.cxx \
../../src/ContractionState.cxx \
diff --git a/test/unit/testCellBuffer.cxx b/test/unit/testCellBuffer.cxx
index ef7db099b..b6735339a 100644
--- a/test/unit/testCellBuffer.cxx
+++ b/test/unit/testCellBuffer.cxx
@@ -3,6 +3,7 @@
**/
#include <cstddef>
+#include <cassert>
#include <cstring>
#include <stdexcept>
#include <string_view>
@@ -19,6 +20,8 @@
#include "SplitVector.h"
#include "Partitioning.h"
#include "RunStyles.h"
+#include "SparseVector.h"
+#include "ChangeHistory.h"
#include "CellBuffer.h"
#include "catch.hpp"
@@ -37,7 +40,7 @@ TEST_CASE("CellBuffer") {
SECTION("InsertOneLine") {
bool startSequence = false;
- const char *cpChange = cb.InsertString(0, sText, static_cast<int>(sLength), startSequence);
+ const char *cpChange = cb.InsertString(0, sText, sLength, startSequence);
REQUIRE(startSequence);
REQUIRE(sLength == cb.Length());
REQUIRE(memcmp(cpChange, sText, sLength) == 0);
@@ -54,7 +57,7 @@ TEST_CASE("CellBuffer") {
const char sText2[] = "Two\nLines";
const Sci::Position sLength2 = static_cast<Sci::Position>(strlen(sText2));
bool startSequence = false;
- const char *cpChange = cb.InsertString(0, sText2, static_cast<int>(sLength), startSequence);
+ const char *cpChange = cb.InsertString(0, sText2, sLength2, startSequence);
REQUIRE(startSequence);
REQUIRE(sLength2 == cb.Length());
REQUIRE(memcmp(cpChange, sText2, sLength2) == 0);
@@ -64,7 +67,7 @@ TEST_CASE("CellBuffer") {
REQUIRE(4 == cb.LineStart(1));
REQUIRE(1 == cb.LineFromPosition(5));
REQUIRE(sLength2 == cb.LineStart(2));
- REQUIRE(1 == cb.LineFromPosition(static_cast<int>(sLength)));
+ REQUIRE(1 == cb.LineFromPosition(sLength2));
REQUIRE(cb.CanUndo());
REQUIRE(!cb.CanRedo());
}
@@ -74,7 +77,7 @@ TEST_CASE("CellBuffer") {
cb.SetUndoCollection(false);
REQUIRE(!cb.IsCollectingUndo());
bool startSequence = false;
- const char *cpChange = cb.InsertString(0, sText, static_cast<int>(sLength), startSequence);
+ const char *cpChange = cb.InsertString(0, sText, sLength, startSequence);
REQUIRE(!startSequence);
REQUIRE(sLength == cb.Length());
REQUIRE(memcmp(cpChange, sText, sLength) == 0);
@@ -86,7 +89,7 @@ TEST_CASE("CellBuffer") {
const char sTextDeleted[] = "ci";
const char sTextAfterDeletion[] = "Sntilla";
bool startSequence = false;
- const char *cpChange = cb.InsertString(0, sText, static_cast<int>(sLength), startSequence);
+ const char *cpChange = cb.InsertString(0, sText, sLength, startSequence);
REQUIRE(startSequence);
REQUIRE(sLength == cb.Length());
REQUIRE(memcmp(cpChange, sText, sLength) == 0);
@@ -146,7 +149,7 @@ TEST_CASE("CellBuffer") {
cb.SetReadOnly(true);
REQUIRE(cb.IsReadOnly());
bool startSequence = false;
- cb.InsertString(0, sText, static_cast<int>(sLength), startSequence);
+ cb.InsertString(0, sText, sLength, startSequence);
REQUIRE(cb.Length() == 0);
}
@@ -438,3 +441,581 @@ TEST_CASE("CharacterIndex") {
REQUIRE(cb.IndexLineStart(3, LineCharacterIndexType::Utf16) == 5);
}
}
+
+TEST_CASE("ChangeHistory") {
+
+ ChangeHistory il;
+ const EditionSet empty;
+ struct Spanner {
+ Sci::Position position = 0;
+ Sci::Position length = 0;
+ };
+
+ SECTION("Start") {
+ REQUIRE(il.Length() == 0);
+ REQUIRE(il.DeletionCount(0,0) == 0);
+ REQUIRE(il.EditionAt(0) == 0);
+ REQUIRE(il.EditionEndRun(0) == 0);
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ REQUIRE(il.EditionNextDelete(0) == 1);
+ }
+
+ SECTION("Some Space") {
+ il.Insert(0, 10, false, true);
+ REQUIRE(il.Length() == 10);
+ REQUIRE(il.DeletionCount(0,10) == 0);
+ REQUIRE(il.EditionAt(0) == 0);
+ REQUIRE(il.EditionEndRun(0) == 10);
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ REQUIRE(il.EditionNextDelete(0) == 10);
+ REQUIRE(il.EditionDeletesAt(10) == 0);
+ REQUIRE(il.EditionNextDelete(10) == 11);
+ }
+
+ SECTION("An insert") {
+ il.Insert(0, 7, false, true);
+ il.SetSavePoint();
+ il.Insert(2, 3, true, true);
+ REQUIRE(il.Length() == 10);
+ REQUIRE(il.DeletionCount(0,10) == 0);
+
+ REQUIRE(il.EditionAt(0) == 0);
+ REQUIRE(il.EditionEndRun(0) == 2);
+ REQUIRE(il.EditionAt(2) == 2);
+ REQUIRE(il.EditionEndRun(2) == 5);
+ REQUIRE(il.EditionAt(5) == 0);
+ REQUIRE(il.EditionEndRun(5) == 10);
+ REQUIRE(il.EditionAt(10) == 0);
+
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ REQUIRE(il.EditionNextDelete(0) == 10);
+ REQUIRE(il.EditionDeletesAt(10) == 0);
+ }
+
+ SECTION("A delete") {
+ il.Insert(0, 10, false, true);
+ il.SetSavePoint();
+ il.DeleteRangeSavingHistory(2, 3, true, false);
+ REQUIRE(il.Length() == 7);
+ REQUIRE(il.DeletionCount(0,7) == 1);
+
+ REQUIRE(il.EditionAt(0) == 0);
+ REQUIRE(il.EditionEndRun(0) == 7);
+ REQUIRE(il.EditionAt(7) == 0);
+
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ const EditionSet one{ 2 };
+ REQUIRE(il.EditionNextDelete(0) == 2);
+ REQUIRE(il.EditionDeletesAt(2) == 2);
+ REQUIRE(il.EditionNextDelete(2) == 7);
+ REQUIRE(il.EditionDeletesAt(7) == 0);
+ }
+
+ SECTION("Insert, delete, and undo") {
+ il.Insert(0, 9, false, true);
+ il.SetSavePoint();
+ il.Insert(3, 1, true, true);
+ REQUIRE(il.EditionEndRun(0) == 3);
+ REQUIRE(il.EditionAt(3) == 2);
+ REQUIRE(il.EditionEndRun(3) == 4);
+ REQUIRE(il.EditionAt(4) == 0);
+
+ il.DeleteRangeSavingHistory(2, 3, true, false);
+ REQUIRE(il.Length() == 7);
+ REQUIRE(il.DeletionCount(0,7) == 1);
+
+ REQUIRE(il.EditionAt(0) == 0);
+ REQUIRE(il.EditionEndRun(0) == 7);
+ REQUIRE(il.EditionAt(7) == 0);
+
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ const EditionSet one{ 2 };
+ REQUIRE(il.EditionNextDelete(0) == 2);
+ REQUIRE(il.EditionDeletesAt(2) == 2);
+ REQUIRE(il.EditionNextDelete(2) == 7);
+ REQUIRE(il.EditionDeletesAt(7) == 0);
+
+ // Undo in detail (normally inside CellBuffer::PerformUndoStep)
+ il.UndoDeleteStep(2, 3, false);
+ REQUIRE(il.Length() == 10);
+ REQUIRE(il.DeletionCount(0, 10) == 0);
+ // The insertion has reappeared
+ REQUIRE(il.EditionEndRun(0) == 3);
+ REQUIRE(il.EditionAt(3) == 2);
+ REQUIRE(il.EditionEndRun(3) == 4);
+ REQUIRE(il.EditionAt(4) == 0);
+ }
+
+ SECTION("Deletes") {
+ il.Insert(0, 10, false, true);
+ il.SetSavePoint();
+ il.DeleteRangeSavingHistory(2, 3, true, false);
+ REQUIRE(il.Length() == 7);
+ REQUIRE(il.DeletionCount(0,7) == 1);
+
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ REQUIRE(il.EditionNextDelete(0) == 2);
+ REQUIRE(il.EditionDeletesAt(2) == 2);
+ REQUIRE(il.EditionNextDelete(2) == 7);
+ REQUIRE(il.EditionDeletesAt(7) == 0);
+
+ il.DeleteRangeSavingHistory(2, 1, true, false);
+ REQUIRE(il.Length() == 6);
+ REQUIRE(il.DeletionCount(0,6) == 2);
+
+ REQUIRE(il.EditionDeletesAt(0) == 0);
+ REQUIRE(il.EditionNextDelete(0) == 2);
+ REQUIRE(il.EditionDeletesAt(2) == 2);
+ REQUIRE(il.EditionNextDelete(2) == 6);
+ REQUIRE(il.EditionDeletesAt(6) == 0);
+
+ // Undo in detail (normally inside CellBuffer::PerformUndoStep)
+ il.UndoDeleteStep(2, 1, false);
+ REQUIRE(il.Length() == 7);
+ REQUIRE(il.DeletionCount(0, 7) == 1);
+
+ // Undo in detail (normally inside CellBuffer::PerformUndoStep)
+ il.UndoDeleteStep(2, 3, false);
+ REQUIRE(il.Length() == 10);
+ REQUIRE(il.DeletionCount(0, 10) == 0);
+ }
+
+ SECTION("Deletes 101") {
+ // Deletes that hit the start and end permanent positions
+ il.Insert(0, 3, false, true);
+ il.SetSavePoint();
+ REQUIRE(il.DeletionCount(0, 2) == 0);
+ il.DeleteRangeSavingHistory(1, 1, true, false);
+ REQUIRE(il.DeletionCount(0,2) == 1);
+ const EditionSet at1 = {2};
+ REQUIRE(il.DeletionsAt(1) == at1);
+ il.DeleteRangeSavingHistory(1, 1, false, false);
+ REQUIRE(il.DeletionCount(0,1) == 2);
+ const EditionSet at2 = { 2, 3 };
+ REQUIRE(il.DeletionsAt(1) == at2);
+ il.DeleteRangeSavingHistory(0, 1, false, false);
+ const EditionSet at3 = { 2, 3, 3 };
+ REQUIRE(il.DeletionsAt(0) == at3);
+ REQUIRE(il.DeletionCount(0,0) == 3);
+
+ // Undo them
+ il.UndoDeleteStep(0, 1, false);
+ REQUIRE(il.DeletionCount(0, 1) == 2);
+ REQUIRE(il.DeletionsAt(1) == at2);
+ il.UndoDeleteStep(1, 1, false);
+ REQUIRE(il.DeletionCount(0, 2) == 1);
+ REQUIRE(il.DeletionsAt(1) == at1);
+ il.UndoDeleteStep(1, 1, false);
+ REQUIRE(il.DeletionCount(0, 3) == 0);
+ }
+
+ SECTION("Deletes Stack") {
+ std::vector<Spanner> spans = {
+ {5, 1},
+ {4, 3},
+ {1, 1},
+ {1, 1},
+ {0, 1},
+ {0, 3},
+ };
+
+ // Deletes that hit the start and end permanent positions
+ il.Insert(0, 10, false, true);
+ REQUIRE(il.Length() == 10);
+ il.SetSavePoint();
+ REQUIRE(il.DeletionCount(0, 10) == 0);
+ for (size_t i = 0; i < std::size(spans); i++) {
+ il.DeleteRangeSavingHistory(spans[i].position, spans[i].length, false, false);
+ }
+ REQUIRE(il.Length() == 0);
+ for (size_t j = 0; j < std::size(spans); j++) {
+ const size_t i = std::size(spans) - j - 1;
+ il.UndoDeleteStep(spans[i].position, spans[i].length, false);
+ }
+ REQUIRE(il.DeletionCount(0, 10) == 0);
+ REQUIRE(il.Length() == 10);
+ }
+}
+
+struct InsertionResult {
+ Sci::Position position;
+ Sci::Position length;
+ int state;
+ bool operator==(const InsertionResult &other) const noexcept {
+ return position == other.position &&
+ length == other.length &&
+ state == other.state;
+ }
+};
+
+std::ostream &operator << (std::ostream &os, InsertionResult const &value) {
+ os << value.position << " " << value.length << " " << value.state;
+ return os;
+}
+
+using Insertions = std::vector<InsertionResult>;
+
+std::ostream &operator << (std::ostream &os, Insertions const &value) {
+ os << "(";
+ for (const InsertionResult &el : value) {
+ os << "(" << el << ") ";
+ }
+ os << ")";
+ return os;
+}
+
+Insertions HistoryInsertions(const CellBuffer &cb) {
+ Insertions result;
+ Sci::Position startPos = 0;
+ while (startPos < cb.Length()) {
+ const Sci::Position endPos = cb.EditionEndRun(startPos);
+ const int ed = cb.EditionAt(startPos);
+ if (ed) {
+ result.push_back({ startPos, endPos - startPos, ed });
+ }
+ startPos = endPos;
+ }
+ return result;
+}
+
+struct DeletionResult {
+ Sci::Position position;
+ int state;
+ bool operator==(const DeletionResult &other) const noexcept {
+ return position == other.position &&
+ state == other.state;
+ }
+};
+
+std::ostream &operator << (std::ostream &os, DeletionResult const &value) {
+ os << value.position << " " << value.state;
+ return os;
+}
+
+using Deletions = std::vector<DeletionResult>;
+
+std::ostream &operator << (std::ostream &os, Deletions const &value) {
+ os << "(";
+ for (const DeletionResult &el : value) {
+ os << "(" << el << ") ";
+ }
+ os << ")";
+ return os;
+}
+
+Deletions HistoryDeletions(const CellBuffer &cb) {
+ Deletions result;
+ Sci::Position positionDeletion = 0;
+ while (positionDeletion <= cb.Length()) {
+ const unsigned int editions = cb.EditionDeletesAt(positionDeletion);
+ if (editions & 1) {
+ result.push_back({ positionDeletion, 1 });
+ }
+ if (editions & 2) {
+ result.push_back({ positionDeletion, 2 });
+ }
+ if (editions & 4) {
+ result.push_back({ positionDeletion, 3 });
+ }
+ if (editions & 8) {
+ result.push_back({ positionDeletion, 4 });
+ }
+ positionDeletion = cb.EditionNextDelete(positionDeletion);
+ }
+ return result;
+}
+
+struct History {
+ Insertions insertions;
+ Deletions deletions;
+ bool operator==(const History &other) const {
+ return insertions == other.insertions &&
+ deletions == other.deletions;
+ }
+};
+
+std::ostream &operator << (std::ostream &os, History const &value) {
+ os << value.insertions << " " << value.deletions;
+ return os;
+}
+
+History HistoryOf(const CellBuffer &cb) {
+ return { HistoryInsertions(cb), HistoryDeletions(cb) };
+}
+
+void UndoBlock(CellBuffer &cb) {
+ const int steps = cb.StartUndo();
+ for (int step = 0; step < steps; step++) {
+ cb.PerformUndoStep();
+ }
+}
+
+void RedoBlock(CellBuffer &cb) {
+ const int steps = cb.StartRedo();
+ for (int step = 0; step < steps; step++) {
+ cb.PerformRedoStep();
+ }
+}
+
+TEST_CASE("CellBufferWithChangeHistory") {
+
+ SECTION("StraightUndoRedoSaveRevertRedo") {
+ CellBuffer cb(true, false);
+ cb.SetUndoCollection(false);
+ std::string sInsert = "abcdefghijklmnopqrstuvwxyz";
+ bool startSequence = false;
+ cb.InsertString(0, sInsert.c_str(), sInsert.length(), startSequence);
+ cb.SetUndoCollection(true);
+ cb.SetSavePoint();
+ cb.ChangeHistorySet(true);
+
+ const History history0 { {}, {} };
+ REQUIRE(HistoryOf(cb) == history0);
+
+ // 1
+ cb.InsertString(4, "_", 1, startSequence);
+ const History history1{ {{4, 1, 3}}, {} };
+ REQUIRE(HistoryOf(cb) == history1);
+
+ // 2
+ cb.DeleteChars(2, 1, startSequence);
+ const History history2{ {{3, 1, 3}},
+ {{2, 3}} };
+ REQUIRE(HistoryOf(cb) == history2);
+
+ // 3
+ cb.InsertString(1, "[!]", 3, startSequence);
+ const History history3{ { {1, 3, 3}, {6, 1, 3} },
+ { {5, 3} } };
+ REQUIRE(HistoryOf(cb) == history3);
+
+ // 4
+ cb.DeleteChars(2, 1, startSequence); // Inside an insertion
+ const History history4{ { {1, 2, 3}, {5, 1, 3} },
+ { {2, 3}, {4, 3} }};
+ REQUIRE(HistoryOf(cb) == history4);
+
+ // 5 Delete all the insertions and deletions
+ cb.DeleteChars(1, 6, startSequence); // Inside an insertion
+ const History history5{ { },
+ { {1, 3} } };
+ REQUIRE(HistoryOf(cb) == history5);
+
+ // Undo all
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history4);
+
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history3);
+
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history2);
+
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history1);
+
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history0);
+
+ // Redo all
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history1);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history2);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history3);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history4);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history5);
+
+ cb.SetSavePoint();
+ const History history5s{ { },
+ { {1, 2} } };
+ REQUIRE(HistoryOf(cb) == history5s);
+
+ // Change past save point
+ cb.InsertString(4, "123", 3, startSequence);
+ const History history6{ { {4, 3, 3} },
+ { {1, 2} } };
+ REQUIRE(HistoryOf(cb) == history6);
+
+ // Undo to save point: same as 5 but with save state instead of unsaved
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history5s);
+
+ // Reverting past save point, similar to 4 but with most saved and
+ // reverted delete at 1
+ UndoBlock(cb); // Reinsert most of original changes
+ const History history4s{ { {1, 2, 4}, {3, 2, 1}, {5, 1, 4}, {6, 1, 1} },
+ { {2, 2}, {4, 2} } };
+ REQUIRE(HistoryOf(cb) == history4s);
+
+ UndoBlock(cb); // Reinsert "!",
+ const History history3s{ { {1, 3, 4}, {4, 2, 1}, {6, 1, 4}, {7, 1, 1} },
+ { {5, 2} } };
+ REQUIRE(HistoryOf(cb) == history3s);
+
+ UndoBlock(cb); // Revert insertion of [!]
+ const History history2s{ { {1, 2, 1}, {3, 1, 4}, {4, 1, 1} },
+ { {1, 1}, {2, 2} } };
+ REQUIRE(HistoryOf(cb) == history2s);
+
+ UndoBlock(cb); // Revert deletion, inserts at 2
+ const History history1s{ { {1, 3, 1}, {4, 1, 4}, {5, 1, 1} },
+ { {1, 1} } };
+ REQUIRE(HistoryOf(cb) == history1s);
+
+ UndoBlock(cb); // Revert insertion of _ at 4, drops middle insertion run
+ // So merges down to 1 insertion
+ const History history0s{ { {1, 4, 1} },
+ { {1, 1}, {4, 1} } };
+ REQUIRE(HistoryOf(cb) == history0s);
+
+ // At origin but with changes from disk
+ // Now redo the steps
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history1s);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history2s);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history3s);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history4s);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history5s);
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == history6);
+ }
+
+ SECTION("Detached") {
+ CellBuffer cb(true, false);
+ cb.SetUndoCollection(false);
+ std::string sInsert = "abcdefghijklmnopqrstuvwxyz";
+ bool startSequence = false;
+ cb.InsertString(0, sInsert.c_str(), sInsert.length(), startSequence);
+ cb.SetUndoCollection(true);
+ cb.SetSavePoint();
+ cb.ChangeHistorySet(true);
+
+ const History history0{ {}, {} };
+ REQUIRE(HistoryOf(cb) == history0);
+
+ // 1
+ cb.InsertString(4, "_", 1, startSequence);
+ const History history1{ {{4, 1, 3}}, {} };
+ REQUIRE(HistoryOf(cb) == history1);
+
+ // 2
+ cb.DeleteChars(2, 1, startSequence);
+ const History history2{ {{3, 1, 3}},
+ {{2, 3}} };
+ REQUIRE(HistoryOf(cb) == history2);
+
+ cb.SetSavePoint();
+
+ UndoBlock(cb);
+ const History history1s{ {{2, 1, 1}, {4, 1, 2}}, {} };
+ REQUIRE(HistoryOf(cb) == history1s);
+
+ cb.InsertString(6, "()", 2, startSequence);
+ const History detached2{ {{2, 1, 1}, {4, 1, 2}, {6, 2, 3}}, {} };
+ REQUIRE(HistoryOf(cb) == detached2);
+
+ cb.DeleteChars(9, 3, startSequence);
+ const History detached3{ {{2, 1, 1}, {4, 1, 2}, {6, 2, 3}}, {{9,3}} };
+ REQUIRE(HistoryOf(cb) == detached3);
+
+ UndoBlock(cb);
+ REQUIRE(HistoryOf(cb) == detached2);
+ UndoBlock(cb);
+ const History detached1{ {{2, 1, 1}, {4, 1, 2}}, {} };
+ REQUIRE(HistoryOf(cb) == detached1);
+ UndoBlock(cb);
+ const History detached0{ {{2, 1, 1}}, {{4,1}} };
+ REQUIRE(HistoryOf(cb) == detached0);
+ REQUIRE(!cb.CanUndo());
+
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == detached1);
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == detached2);
+ RedoBlock(cb);
+ REQUIRE(HistoryOf(cb) == detached3);
+ }
+}
+
+namespace {
+
+// Implement low quality reproducible pseudo-random numbers.
+// Pseudo-random algorithm based on R. G. Dromey "How to Solve it by Computer" page 122.
+
+class RandomSequence {
+ static constexpr int mult = 109;
+ static constexpr int incr = 853;
+ static constexpr int modulus = 4096;
+ int randomValue = 127;
+public:
+ int Next() noexcept {
+ randomValue = (mult * randomValue + incr) % modulus;
+ return randomValue;
+ }
+};
+
+}
+
+#if 1
+TEST_CASE("CellBufferLong") {
+
+ // Call methods on CellBuffer pseudo-randomly trying to trigger assertion failures
+
+ CellBuffer cb(true, false);
+
+ SECTION("Random") {
+ RandomSequence rseq;
+ for (size_t i = 0l; i < 20000; i++) {
+ const int r = rseq.Next() % 10;
+ if (r <= 2) { // 30%
+ // Insert text
+ const int pos = rseq.Next() % (cb.Length() + 1);
+ const int len = rseq.Next() % 10 + 1;
+ std::string sInsert;
+ for (int j = 0; j < len; j++) {
+ sInsert.push_back(static_cast<char>('a' + j));
+ }
+ bool startSequence = false;
+ cb.InsertString(pos, sInsert.c_str(), len, startSequence);
+ } else if (r <= 5) { // 30%
+ // Delete Text
+ const Sci::Position pos = rseq.Next() % (cb.Length() + 1);
+ const int len = rseq.Next() % 10 + 1;
+ if (pos + len <= cb.Length()) {
+ bool startSequence = false;
+ cb.DeleteChars(pos, len, startSequence);
+ }
+ } else if (r <= 8) { // 30%
+ // Undo or redo
+ const bool undo = rseq.Next() % 2 == 1;
+ if (undo) {
+ UndoBlock(cb);
+ } else {
+ RedoBlock(cb);
+ }
+ } else { // 10%
+ // Save
+ cb.SetSavePoint();
+ }
+ }
+ }
+}
+#endif \ No newline at end of file