aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNeil <nyamatongwe@gmail.com>2025-02-01 09:07:57 +1100
committerNeil <nyamatongwe@gmail.com>2025-02-01 09:07:57 +1100
commitbd03d24f8bd44813db8ed1c73c5a46400d94a7cf (patch)
tree74c7984b59a193a54db66bd94d15c595a512ed0b
parent97691047e0c13b92639d340ee9f34dde67941e18 (diff)
downloadscintilla-mirror-bd03d24f8bd44813db8ed1c73c5a46400d94a7cf.tar.gz
Implement serialization of Selection to and from strings.
Requires std::from_chars to be available.
-rw-r--r--scripts/HeaderOrder.txt1
-rw-r--r--src/Selection.cxx145
-rw-r--r--src/Selection.h7
-rw-r--r--test/unit/testSelection.cxx112
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");
+ }
+
}