// Scintilla source code edit control /** @file Selection.cxx ** Classes maintaining the selection. **/ // Copyright 2009 by Neil Hodgson // The License.txt file describes the conditions under which this software may be distributed. #include #include #include #include #include #include #include #include #include #include #include "Debugging.h" #include "Position.h" #include "Selection.h" 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 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."); } sv.remove_prefix(res.ptr - sv.data()); } } SelectionPosition::SelectionPosition(std::string_view &sv) : position(0) { ValueFromString(sv, position); if (!sv.empty() && sv.front() == 'v') { sv.remove_prefix(1); ValueFromString(sv, virtualSpace); } } void SelectionPosition::MoveForInsertDelete(bool insertion, Sci::Position startChange, Sci::Position length, bool moveForEqual) noexcept { if (insertion) { if (position == startChange) { // Always consume virtual space const Sci::Position virtualLengthRemove = std::min(length, virtualSpace); virtualSpace -= virtualLengthRemove; position += virtualLengthRemove; if (moveForEqual) { const Sci::Position lengthAfterVirtualRemove = length - virtualLengthRemove; position += lengthAfterVirtualRemove; } } else if (position > startChange) { position += length; } } else { if (position == startChange) { virtualSpace = 0; } if (position > startChange) { const Sci::Position endDeletion = startChange + length; if (position > endDeletion) { position -= length; } else { position = startChange; virtualSpace = 0; } } } } bool SelectionPosition::operator >(const SelectionPosition &other) const noexcept { if (position == other.position) return virtualSpace > other.virtualSpace; return position > other.position; } bool SelectionPosition::operator <=(const SelectionPosition &other) const noexcept { if (other == *this) return true; return other > *this; } bool SelectionPosition::operator >=(const SelectionPosition &other) const noexcept { if (other == *this) return true; return *this > other; } double SelectionPosition::VirtualSpaceWidth(double spaceWidth) const noexcept { return static_cast(virtualSpace) * spaceWidth; } 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) { anchor = SelectionPosition(sv); if (sv.empty() || sv.front() != '-') { caret = anchor; } else { sv.remove_prefix(1); caret = SelectionPosition(sv); } } Sci::Position SelectionRange::Length() const noexcept { if (anchor > caret) { return anchor.Position() - caret.Position(); } else { return caret.Position() - anchor.Position(); } } void SelectionRange::MoveForInsertDelete(bool insertion, Sci::Position startChange, Sci::Position length) noexcept { // For insertions that occur at the start of the selection move both the start // and end of the selection to preserve the selected length. // The end will automatically move since it is after the insertion, so determine // which position is the start and pass this into // SelectionPosition::MoveForInsertDelete. // There isn't any reason to move an empty selection so don't move it. const bool caretStart = caret.Position() < anchor.Position(); const bool anchorStart = anchor.Position() < caret.Position(); caret.MoveForInsertDelete(insertion, startChange, length, caretStart); anchor.MoveForInsertDelete(insertion, startChange, length, anchorStart); } bool SelectionRange::Contains(Sci::Position pos) const noexcept { if (anchor > caret) return (pos >= caret.Position()) && (pos <= anchor.Position()); else return (pos >= anchor.Position()) && (pos <= caret.Position()); } bool SelectionRange::Contains(SelectionPosition sp) const noexcept { if (anchor > caret) return (sp >= caret) && (sp <= anchor); else return (sp >= anchor) && (sp <= caret); } bool SelectionRange::ContainsCharacter(Sci::Position posCharacter) const noexcept { if (anchor > caret) return (posCharacter >= caret.Position()) && (posCharacter < anchor.Position()); else return (posCharacter >= anchor.Position()) && (posCharacter < caret.Position()); } bool SelectionRange::ContainsCharacter(SelectionPosition spCharacter) const noexcept { if (anchor > caret) return (spCharacter >= caret) && (spCharacter < anchor); else return (spCharacter >= anchor) && (spCharacter < caret); } SelectionSegment SelectionRange::Intersect(SelectionSegment check) const noexcept { const SelectionSegment inOrder = AsSegment(); if ((inOrder.start > check.end) || (inOrder.end < check.start)) { // Nothing in common, not even touching so return empty *invalid* segment return {}; } return { std::max(check.start, inOrder.start), std::min(check.end, inOrder.end) }; } void SelectionRange::Swap() noexcept { std::swap(caret, anchor); } bool SelectionRange::Trim(SelectionRange range) noexcept { const SelectionPosition startRange = range.Start(); const SelectionPosition endRange = range.End(); SelectionPosition start = Start(); SelectionPosition end = End(); PLATFORM_ASSERT(start <= end); PLATFORM_ASSERT(startRange <= endRange); if ((startRange <= end) && (endRange >= start)) { if ((start > startRange) && (end < endRange)) { // Completely covered by range -> empty at start end = start; } else if ((start < startRange) && (end > endRange)) { // Completely covers range -> empty at start end = start; } else if (start <= startRange) { // Trim end end = startRange; } else { // PLATFORM_ASSERT(end >= endRange); // Trim start start = endRange; } if (anchor > caret) { caret = start; anchor = end; } else { anchor = start; caret = end; } return Empty(); } return false; } void SelectionRange::Truncate(Sci::Position length) noexcept { if (anchor.Position() > length) anchor.SetPosition(length); if (caret.Position() > length) caret.SetPosition(length); } // If range is all virtual collapse to start of virtual space void SelectionRange::MinimizeVirtualSpace() noexcept { if (caret.Position() == anchor.Position()) { Sci::Position virtualSpace = caret.VirtualSpace(); if (virtualSpace > anchor.VirtualSpace()) virtualSpace = anchor.VirtualSpace(); caret.SetVirtualSpace(virtualSpace); anchor.SetVirtualSpace(virtualSpace); } } 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 start after '#' if (!sv.empty() && sv.front() == '#') { sv.remove_prefix(1); ValueFromString(sv, mainRange); if (!sv.empty() && sv.front() == ',') { sv.remove_prefix(1); } } // 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 { while (!sv.empty()) { if (sv.front() == ',') { sv.remove_prefix(1); } 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); } Sci::Position Selection::MainCaret() const noexcept { return ranges[mainRange].caret.Position(); } Sci::Position Selection::MainAnchor() const noexcept { return ranges[mainRange].anchor.Position(); } SelectionRange &Selection::Rectangular() noexcept { return rangeRectangular; } SelectionRange Selection::RectangularCopy() const noexcept { return rangeRectangular; } SelectionSegment Selection::Limits() const noexcept { PLATFORM_ASSERT(!ranges.empty()); SelectionSegment sr = ranges[0].AsSegment(); for (size_t i=1; i 1) { ranges.erase(ranges.begin() + 1, ranges.end()); } ranges[0] = range; mainRange = 0; } void Selection::AddSelection(SelectionRange range) { TrimSelection(range); ranges.push_back(range); mainRange = ranges.size() - 1; } void Selection::AddSelectionWithoutTrim(SelectionRange range) { ranges.push_back(range); mainRange = ranges.size() - 1; } void Selection::DropSelection(size_t r) noexcept { if ((ranges.size() > 1) && (r < ranges.size())) { size_t mainNew = mainRange; if (mainNew >= r) { if (mainNew == 0) { mainNew = ranges.size() - 2; } else { mainNew--; } } ranges.erase(ranges.begin() + r); mainRange = mainNew; } } void Selection::DropAdditionalRanges() noexcept { SetSelection(RangeMain()); } void Selection::TentativeSelection(SelectionRange range) { if (!tentativeMain) { rangesSaved = ranges; } ranges = rangesSaved; AddSelection(range); TrimSelection(ranges[mainRange]); tentativeMain = true; } void Selection::CommitTentative() noexcept { rangesSaved.clear(); tentativeMain = false; } InSelection Selection::RangeType(size_t r) const noexcept { return r == Main() ? InSelection::inMain : InSelection::inAdditional; } InSelection Selection::CharacterInSelection(Sci::Position posCharacter) const noexcept { for (size_t i=0; i ranges[i].Start().Position()) && (pos <= ranges[i].End().Position())) return RangeType(i); } return InSelection::inNone; } Sci::Position Selection::VirtualSpaceFor(Sci::Position pos) const noexcept { Sci::Position virtualSpace = 0; for (const SelectionRange &range : ranges) { if ((range.caret.Position() == pos) && (virtualSpace < range.caret.VirtualSpace())) virtualSpace = range.caret.VirtualSpace(); if ((range.anchor.Position() == pos) && (virtualSpace < range.anchor.VirtualSpace())) virtualSpace = range.anchor.VirtualSpace(); } return virtualSpace; } void Selection::Clear() noexcept { if (ranges.size() > 1) { ranges.erase(ranges.begin() + 1, ranges.end()); } ranges[0].Reset(); rangesSaved.clear(); rangeRectangular.Reset(); mainRange = 0; moveExtends = false; tentativeMain = false; selType = SelTypes::stream; } void Selection::RemoveDuplicates() noexcept { for (size_t i=0; i= j) mainRange--; } else { j++; } } } } } void Selection::RotateMain() noexcept { mainRange = (mainRange + 1) % ranges.size(); } void Selection::SetRanges(const Ranges &rangesToSet) { ranges = rangesToSet; } void Selection::Truncate(Sci::Position length) noexcept { // This may be needed when applying a persisted selection onto a document that has been shortened. for (SelectionRange &range : ranges) { range.Truncate(length); } // Above may have made some non-unique empty ranges. RemoveDuplicates(); rangeRectangular.Truncate(length); } 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 (mainRange > 0) { result += '#'; result += std::to_string(mainRange); result += ','; } 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(); } } return result; }