diff options
-rw-r--r-- | scripts/HeaderOrder.txt | 1 | ||||
-rw-r--r-- | src/Selection.cxx | 145 | ||||
-rw-r--r-- | src/Selection.h | 7 | ||||
-rw-r--r-- | test/unit/testSelection.cxx | 112 |
4 files changed, 265 insertions, 0 deletions
diff --git a/scripts/HeaderOrder.txt b/scripts/HeaderOrder.txt index 79cf0fa7a..94516260b 100644 --- a/scripts/HeaderOrder.txt +++ b/scripts/HeaderOrder.txt @@ -52,6 +52,7 @@ #include <memory> #include <numeric> #include <chrono> +#include <charconv> #include <regex> #include <iostream> #include <sstream> diff --git a/src/Selection.cxx b/src/Selection.cxx index ad63afd70..5650d5c7e 100644 --- a/src/Selection.cxx +++ b/src/Selection.cxx @@ -9,11 +9,13 @@ #include <cstdlib> #include <stdexcept> +#include <string> #include <string_view> #include <vector> #include <optional> #include <algorithm> #include <memory> +#include <charconv> #include "Debugging.h" @@ -22,6 +24,30 @@ using namespace Scintilla::Internal; +namespace { + +// Generically convert a string to a integer value throwing if the conversion failed. +// Failures include values that are out of range for the destination variable. +template <typename T> +void ValueFromString(std::string_view sv, T &value) { + const std::from_chars_result res = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (res.ec != std::errc{}) { + if (res.ec == std::errc::result_out_of_range) + throw std::runtime_error("from_chars out of range."); + throw std::runtime_error("from_chars failed."); + } +} + +} + +SelectionPosition::SelectionPosition(std::string_view sv) : position(0) { + if (const size_t v = sv.find('v'); v != std::string_view::npos) { + ValueFromString(sv.substr(v + 1), virtualSpace); + sv = sv.substr(0, v); + } + ValueFromString(sv, position); +} + void SelectionPosition::MoveForInsertDelete(bool insertion, Sci::Position startChange, Sci::Position length, bool moveForEqual) noexcept { if (insertion) { if (position == startChange) { @@ -73,6 +99,26 @@ bool SelectionPosition::operator >=(const SelectionPosition &other) const noexce return *this > other; } +std::string SelectionPosition::ToString() const { + std::string result = std::to_string(position); + if (virtualSpace) { + result += 'v'; + result += std::to_string(virtualSpace); + } + return result; +} + +SelectionRange::SelectionRange(std::string_view sv) { + const size_t dash = sv.find('-'); + if (dash == std::string_view::npos) { + anchor = SelectionPosition(sv); + caret = anchor; + } else { + anchor = SelectionPosition(sv.substr(0, dash)); + caret = SelectionPosition(sv.substr(dash + 1)); + } +} + Sci::Position SelectionRange::Length() const noexcept { if (anchor > caret) { return anchor.Position() - caret.Position(); @@ -190,10 +236,73 @@ void SelectionRange::MinimizeVirtualSpace() noexcept { } } +std::string SelectionRange::ToString() const { + std::string result = anchor.ToString(); + if (!(caret == anchor)) { + result += '-'; + result += caret.ToString(); + } + return result; +} + Selection::Selection() : mainRange(0), moveExtends(false), tentativeMain(false), selType(SelTypes::stream) { AddSelection(SelectionRange(SelectionPosition(0))); } +Selection::Selection(std::string_view sv) : mainRange(0), moveExtends(false), tentativeMain(false), selType(SelTypes::stream) { + if (sv.empty()) { + return; + } + try { + // Decode initial letter prefix if any + switch (sv.front()) { + case 'R': + selType = SelTypes::rectangle; + break; + case 'L': + selType = SelTypes::lines; + break; + case 'T': + selType = SelTypes::thin; + break; + default: + break; + } + if (selType != SelTypes::stream) { + sv.remove_prefix(1); + } + + // Non-zero main index at end after '#' + if (const size_t hash = sv.find('#'); hash != std::string_view::npos) { + ValueFromString(sv.substr(hash + 1), mainRange); + sv = sv.substr(0, hash); + } + + // Remainder is list of ranges + if (selType == SelTypes::rectangle || selType == SelTypes::thin) { + rangeRectangular = SelectionRange(sv); + // Ensure enough ranges exist for mainRange to be in bounds + for (size_t i = 0; i <= mainRange; i++) { + ranges.emplace_back(SelectionPosition(0)); + } + } else { + size_t comma = sv.find(','); + while (comma != std::string_view::npos) { + ranges.emplace_back(sv.substr(0, comma)); + sv.remove_prefix(comma + 1); + comma = sv.find(','); + } + ranges.emplace_back(sv); + if (mainRange >= ranges.size()) { + mainRange = ranges.size() - 1; + } + } + } catch (std::runtime_error &) { + // On failure, produce an empty selection. + Clear(); + } +} + bool Selection::IsRectangular() const noexcept { return (selType == SelTypes::rectangle) || (selType == SelTypes::thin); } @@ -458,3 +567,39 @@ void Selection::RotateMain() noexcept { void Selection::SetRanges(const Ranges &rangesToSet) { ranges = rangesToSet; } + +std::string Selection::ToString() const { + std::string result; + switch (selType) { + case SelTypes::rectangle: + result += 'R'; + break; + case SelTypes::lines: + result += 'L'; + break; + case SelTypes::thin: + result += 'T'; + break; + default: + // No handling of none as not a real value of enumeration, just used for empty arguments + // No prefix. + break; + } + if (selType == SelTypes::rectangle || selType == SelTypes::thin) { + result += rangeRectangular.ToString(); + } else { + for (size_t r = 0; r < ranges.size(); r++) { + if (r > 0) { + result += ','; + } + result += ranges[r].ToString(); + } + } + + if (mainRange > 0) { + result += '#'; + result += std::to_string(mainRange); + } + + return result; +} diff --git a/src/Selection.h b/src/Selection.h index 7f3fd937b..68e7a4e54 100644 --- a/src/Selection.h +++ b/src/Selection.h @@ -20,6 +20,7 @@ public: if (virtualSpace < 0) virtualSpace = 0; } + explicit SelectionPosition(std::string_view sv); void Reset() noexcept { position = 0; virtualSpace = 0; @@ -63,6 +64,7 @@ public: bool IsValid() const noexcept { return position >= 0; } + std::string ToString() const; }; // Ordered range to make drawing simpler @@ -112,6 +114,7 @@ struct SelectionRange { } constexpr SelectionRange(Sci::Position caret_, Sci::Position anchor_) noexcept : caret(caret_), anchor(anchor_) { } + explicit SelectionRange(std::string_view sv); bool Empty() const noexcept { return anchor == caret; } @@ -147,6 +150,7 @@ struct SelectionRange { bool Trim(SelectionRange range) noexcept; // If range is all virtual collapse to start of virtual space void MinimizeVirtualSpace() noexcept; + std::string ToString() const; }; // Deliberately an enum rather than an enum class to allow treating as bool @@ -165,6 +169,8 @@ public: SelTypes selType; Selection(); // Allocates so may throw. + explicit Selection(std::string_view sv); + bool IsRectangular() const noexcept; Sci::Position MainCaret() const noexcept; Sci::Position MainAnchor() const noexcept; @@ -210,6 +216,7 @@ public: return ranges; } void SetRanges(const Ranges &rangesToSet); + std::string ToString() const; }; } diff --git a/test/unit/testSelection.cxx b/test/unit/testSelection.cxx index 8c27b42e8..64148bf61 100644 --- a/test/unit/testSelection.cxx +++ b/test/unit/testSelection.cxx @@ -110,6 +110,26 @@ TEST_CASE("SelectionPosition") { REQUIRE(sel == SelectionPosition(2, 0)); } + SECTION("Serialization") { + // Conversion to/from string form + + const std::string invalidString(invalid.ToString()); + REQUIRE(invalidString == "-1"); + const SelectionPosition invalidReturned(invalidString); + REQUIRE(invalidReturned == invalid); + + const std::string zeroString(zero.ToString()); + REQUIRE(zeroString == "0"); + const SelectionPosition zeroReturned(zeroString); + REQUIRE(zeroReturned == zero); + + const SelectionPosition virtue(2, 3); + const std::string virtueString(virtue.ToString()); + REQUIRE(virtueString == "2v3"); + const SelectionPosition virtueReturned(virtueString); + REQUIRE(virtueReturned == virtue); + } + } TEST_CASE("SelectionSegment") { @@ -130,6 +150,18 @@ TEST_CASE("SelectionRange") { REQUIRE(sr.caret == invalid); } + SECTION("Serialization") { + // Conversion to/from string form + + // Range from 1 to 2 with 3 virtual spaces + const SelectionRange range123(SelectionPosition(2, 3), SelectionPosition(1)); + const std::string range123String(range123.ToString()); + // Opposite order to constructor: from anchor to caret + REQUIRE(range123String == "1-2v3"); + const SelectionRange range123Returned(range123String); + REQUIRE(range123Returned == range123); + } + } TEST_CASE("Selection") { @@ -148,4 +180,84 @@ TEST_CASE("Selection") { REQUIRE(sel.Empty()); } + SECTION("Serialization") { + // Conversion to/from string form + + // Range from 5 with 3 virtual spaces to 2 + const SelectionRange range532(SelectionPosition(2), SelectionPosition(5, 3)); + Selection selection; + selection.SetSelection(range532); + const std::string selectionString(selection.ToString()); + // Opposite order to constructor: from anchor to caret + REQUIRE(selectionString == "5v3-2"); + const SelectionRange selectionReturned(selectionString); + + REQUIRE(selection.selType == Selection::SelTypes::stream); + REQUIRE(!selection.IsRectangular()); + REQUIRE(selection.Count() == 1); + REQUIRE(selection.Main() == 0); + + REQUIRE(selection.Range(0) == range532); + REQUIRE(selection.RangeMain() == range532); + REQUIRE(selection.Rectangular() == rangeInvalid); + REQUIRE(!selection.Empty()); + } + + SECTION("SerializationMultiple") { + // Conversion to/from string form + + // Range from 5 with 3 virtual spaces to 2 + const SelectionRange range532(SelectionPosition(2), SelectionPosition(5, 3)); + const SelectionRange range1(SelectionPosition(1)); + Selection selection; + selection.SetSelection(range532); + selection.AddSelection(range1); + selection.SetMain(1); + const std::string selectionString(selection.ToString()); + REQUIRE(selectionString == "5v3-2,1#1"); + const SelectionRange selectionReturned(selectionString); + + REQUIRE(selection.selType == Selection::SelTypes::stream); + REQUIRE(!selection.IsRectangular()); + REQUIRE(selection.Count() == 2); + REQUIRE(selection.Main() == 1); + + REQUIRE(selection.Range(0) == range532); + REQUIRE(selection.Range(1) == range1); + REQUIRE(selection.RangeMain() == range1); + REQUIRE(selection.Rectangular() == rangeInvalid); + REQUIRE(!selection.Empty()); + } + + SECTION("SerializationRectangular") { + // Conversion to/from string form + + // Range from 5 with 3 virtual spaces to 2 + const SelectionRange range532(SelectionPosition(2), SelectionPosition(5, 3)); + + // Create a single-line rectangular selection + Selection selection; + selection.selType = Selection::SelTypes::rectangle; + selection.Rectangular() = range532; + // Set arbitrary realized range - inside editor ranges would be calculated from line layout + selection.SetSelection(rangeZero); + + const std::string selectionString(selection.ToString()); + REQUIRE(selectionString == "R5v3-2"); + const Selection selectionReturned(selectionString); + + REQUIRE(selection.selType == Selection::SelTypes::rectangle); + REQUIRE(selection.IsRectangular()); + REQUIRE(selection.Count() == 1); + REQUIRE(selection.Main() == 0); + + REQUIRE(selection.Range(0) == rangeZero); + REQUIRE(selection.RangeMain() == rangeZero); + REQUIRE(selection.Rectangular() == range532); + + selection.selType = Selection::SelTypes::thin; + const std::string thinString(selection.ToString()); + REQUIRE(thinString == "T5v3-2"); + } + } |