Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Implementation of mozilla::dom::Selection
*/
#include "Selection.h"
#include "ErrorList.h"
#include "LayoutConstants.h"
#include "mozilla/AccessibleCaretEventHub.h"
#include "mozilla/Assertions.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoCopyListener.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/CaretAssociationHint.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ChildIterator.h"
#include "mozilla/dom/SelectionBinding.h"
#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/dom/StaticRange.h"
#include "mozilla/dom/ShadowIncludingTreeIterator.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/intl/Bidi.h"
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/Logging.h"
#include "mozilla/PresShell.h"
#include "mozilla/RangeBoundary.h"
#include "mozilla/RangeUtils.h"
#include "mozilla/SelectionMovementUtils.h"
#include "mozilla/StackWalk.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Try.h"
#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsDirection.h"
#include "nsString.h"
#include "nsFrameSelection.h"
#include "nsISelectionListener.h"
#include "nsDeviceContext.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsRange.h"
#include "nsITableCellLayout.h"
#include "nsTArray.h"
#include "nsTableWrapperFrame.h"
#include "nsTableCellFrame.h"
#include "nsCCUncollectableMarker.h"
#include "nsIDocumentEncoder.h"
#include "nsTextFragment.h"
#include <algorithm>
#include "nsContentUtils.h"
#include "nsGkAtoms.h"
#include "nsLayoutUtils.h"
#include "nsBidiPresUtils.h"
#include "nsTextFrame.h"
#include "nsThreadUtils.h"
#include "nsPresContext.h"
#include "nsCaret.h"
#include "nsITimer.h"
#include "mozilla/dom/Document.h"
#include "nsINamed.h"
#include "nsISelectionController.h" //for the enums
#include "nsCopySupport.h"
#include "nsIFrameInlines.h"
#include "nsRefreshDriver.h"
#include "nsError.h"
#include "nsViewManager.h"
#include "nsFocusManager.h"
#include "nsPIDOMWindow.h"
namespace mozilla {
// "Selection" logs only the calls of AddRangesForSelectableNodes and
// NotifySelectionListeners in debug level.
static LazyLogModule sSelectionLog("Selection");
// "SelectionAPI" logs all API calls (both internal ones and exposed to script
// ones) of normal selection which may change selection ranges.
// 3. Info: Calls of APIs
// 4. Debug: Call stacks with 7 ancestor callers of APIs
// 5. Verbose: Complete call stacks of APIs.
LazyLogModule sSelectionAPILog("SelectionAPI");
MOZ_ALWAYS_INLINE bool NeedsToLogSelectionAPI(dom::Selection& aSelection) {
return aSelection.Type() == SelectionType::eNormal &&
MOZ_LOG_TEST(sSelectionAPILog, LogLevel::Info);
}
void LogStackForSelectionAPI() {
if (!MOZ_LOG_TEST(sSelectionAPILog, LogLevel::Debug)) {
return;
}
static nsAutoCString* sBufPtr = nullptr;
MOZ_ASSERT(!sBufPtr);
nsAutoCString buf;
sBufPtr = &buf;
auto writer = [](const char* aBuf) { sBufPtr->Append(aBuf); };
const LogLevel logLevel = MOZ_LOG_TEST(sSelectionAPILog, LogLevel::Verbose)
? LogLevel::Verbose
: LogLevel::Debug;
MozWalkTheStackWithWriter(writer, CallerPC(),
logLevel == LogLevel::Verbose
? 0u /* all */
: 8u /* 8 inclusive ancestors */);
MOZ_LOG(sSelectionAPILog, logLevel, ("\n%s", buf.get()));
sBufPtr = nullptr;
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s()", aSelection, aFuncName));
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aArgName,
const nsINode* aNode) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s)", aSelection, aFuncName, aArgName,
aNode ? ToString(*aNode).c_str() : "nullptr"));
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aArgName,
const dom::AbstractRange& aRange) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s)", aSelection, aFuncName, aArgName,
ToString(aRange).c_str()));
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aArgName1,
const nsINode* aNode, const char* aArgName2,
uint32_t aOffset) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s, %s=%u)", aSelection, aFuncName, aArgName1,
aNode ? ToString(*aNode).c_str() : "nullptr", aArgName2, aOffset));
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aArgName,
const RawRangeBoundary& aBoundary) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s)", aSelection, aFuncName, aArgName,
ToString(aBoundary).c_str()));
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aArgName1,
const nsAString& aStr1, const char* aArgName2,
const nsAString& aStr2, const char* aArgName3,
const nsAString& aStr3) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s, %s=%s, %s=%s)", aSelection, aFuncName,
aArgName1, NS_ConvertUTF16toUTF8(aStr1).get(), aArgName2,
NS_ConvertUTF16toUTF8(aStr2).get(), aArgName3,
NS_ConvertUTF16toUTF8(aStr3).get()));
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aNodeArgName1,
const nsINode& aNode1, const char* aOffsetArgName1,
uint32_t aOffset1, const char* aNodeArgName2,
const nsINode& aNode2, const char* aOffsetArgName2,
uint32_t aOffset2) {
if (&aNode1 == &aNode2 && aOffset1 == aOffset2) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s=%s, %s=%s=%u)", aSelection, aFuncName,
aNodeArgName1, aNodeArgName2, ToString(aNode1).c_str(),
aOffsetArgName1, aOffsetArgName2, aOffset1));
} else {
MOZ_LOG(
sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s, %s=%u, %s=%s, %s=%u)", aSelection, aFuncName,
aNodeArgName1, ToString(aNode1).c_str(), aOffsetArgName1, aOffset1,
aNodeArgName2, ToString(aNode2).c_str(), aOffsetArgName2, aOffset2));
}
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aNodeArgName1,
const nsINode& aNode1, const char* aOffsetArgName1,
uint32_t aOffset1, const char* aNodeArgName2,
const nsINode& aNode2, const char* aOffsetArgName2,
uint32_t aOffset2, const char* aDirArgName,
nsDirection aDirection, const char* aReasonArgName,
int16_t aReason) {
if (&aNode1 == &aNode2 && aOffset1 == aOffset2) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s=%s, %s=%s=%u, %s=%s, %s=%d)", aSelection,
aFuncName, aNodeArgName1, aNodeArgName2, ToString(aNode1).c_str(),
aOffsetArgName1, aOffsetArgName2, aOffset1, aDirArgName,
ToString(aDirection).c_str(), aReasonArgName, aReason));
} else {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s, %s=%u, %s=%s, %s=%u, %s=%s, %s=%d)",
aSelection, aFuncName, aNodeArgName1, ToString(aNode1).c_str(),
aOffsetArgName1, aOffset1, aNodeArgName2, ToString(aNode2).c_str(),
aOffsetArgName2, aOffset2, aDirArgName,
ToString(aDirection).c_str(), aReasonArgName, aReason));
}
}
static void LogSelectionAPI(const dom::Selection* aSelection,
const char* aFuncName, const char* aArgName1,
const RawRangeBoundary& aBoundary1,
const char* aArgName2,
const RawRangeBoundary& aBoundary2) {
if (aBoundary1 == aBoundary2) {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s=%s)", aSelection, aFuncName, aArgName1,
aArgName2, ToString(aBoundary1).c_str()));
} else {
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
("%p Selection::%s(%s=%s, %s=%s)", aSelection, aFuncName, aArgName1,
ToString(aBoundary1).c_str(), aArgName2,
ToString(aBoundary2).c_str()));
}
}
} // namespace mozilla
using namespace mozilla;
using namespace mozilla::dom;
// #define DEBUG_TABLE 1
#ifdef PRINT_RANGE
static void printRange(nsRange* aDomRange);
# define DEBUG_OUT_RANGE(x) printRange(x)
#else
# define DEBUG_OUT_RANGE(x)
#endif // PRINT_RANGE
static constexpr nsLiteralCString kNoDocumentTypeNodeError =
"DocumentType nodes are not supported"_ns;
static constexpr nsLiteralCString kNoRangeExistsError =
"No selection range exists"_ns;
namespace mozilla {
/******************************************************************************
* Utility methods defined in nsISelectionListener.idl
******************************************************************************/
nsCString SelectionChangeReasonsToCString(int16_t aReasons) {
nsCString reasons;
if (!aReasons) {
reasons.AssignLiteral("NO_REASON");
return reasons;
}
auto EnsureSeparator = [](nsCString& aString) -> void {
if (!aString.IsEmpty()) {
aString.AppendLiteral(" | ");
}
};
struct ReasonData {
int16_t mReason;
const char* mReasonStr;
ReasonData(int16_t aReason, const char* aReasonStr)
: mReason(aReason), mReasonStr(aReasonStr) {}
};
for (const ReasonData& reason :
{ReasonData(nsISelectionListener::DRAG_REASON, "DRAG_REASON"),
ReasonData(nsISelectionListener::MOUSEDOWN_REASON, "MOUSEDOWN_REASON"),
ReasonData(nsISelectionListener::MOUSEUP_REASON, "MOUSEUP_REASON"),
ReasonData(nsISelectionListener::KEYPRESS_REASON, "KEYPRESS_REASON"),
ReasonData(nsISelectionListener::SELECTALL_REASON, "SELECTALL_REASON"),
ReasonData(nsISelectionListener::COLLAPSETOSTART_REASON,
"COLLAPSETOSTART_REASON"),
ReasonData(nsISelectionListener::COLLAPSETOEND_REASON,
"COLLAPSETOEND_REASON"),
ReasonData(nsISelectionListener::IME_REASON, "IME_REASON"),
ReasonData(nsISelectionListener::JS_REASON, "JS_REASON")}) {
if (aReasons & reason.mReason) {
EnsureSeparator(reasons);
reasons.Append(reason.mReasonStr);
}
}
return reasons;
}
} // namespace mozilla
// #define DEBUG_SELECTION // uncomment for printf describing every collapse and
// extend. #define DEBUG_NAVIGATION
// #define DEBUG_TABLE_SELECTION 1
struct CachedOffsetForFrame {
CachedOffsetForFrame()
: mCachedFrameOffset(0, 0) // nsPoint ctor
,
mLastCaretFrame(nullptr),
mLastContentOffset(0),
mCanCacheFrameOffset(false) {}
nsPoint mCachedFrameOffset; // cached frame offset
nsIFrame* mLastCaretFrame; // store the frame the caret was last drawn in.
int32_t mLastContentOffset; // store last content offset
bool mCanCacheFrameOffset; // cached frame offset is valid?
};
class AutoScroller final : public nsITimerCallback, public nsINamed {
public:
NS_DECL_ISUPPORTS
explicit AutoScroller(nsFrameSelection* aFrameSelection)
: mFrameSelection(aFrameSelection),
mPresContext(0),
mPoint(0, 0),
mDelayInMs(30),
mFurtherScrollingAllowed(FurtherScrollingAllowed::kYes) {
MOZ_ASSERT(mFrameSelection);
}
MOZ_CAN_RUN_SCRIPT nsresult DoAutoScroll(nsIFrame* aFrame, nsPoint aPoint);
private:
// aPoint is relative to aPresContext's root frame
nsresult ScheduleNextDoAutoScroll(nsPresContext* aPresContext,
nsPoint& aPoint) {
if (NS_WARN_IF(mFurtherScrollingAllowed == FurtherScrollingAllowed::kNo)) {
return NS_ERROR_FAILURE;
}
mPoint = aPoint;
// Store the presentation context. The timer will be
// stopped by the selection if the prescontext is destroyed.
mPresContext = aPresContext;
mContent = PresShell::GetCapturingContent();
if (!mTimer) {
mTimer = NS_NewTimer(GetMainThreadSerialEventTarget());
if (!mTimer) {
return NS_ERROR_OUT_OF_MEMORY;
}
}
return mTimer->InitWithCallback(this, mDelayInMs, nsITimer::TYPE_ONE_SHOT);
}
public:
enum class FurtherScrollingAllowed { kYes, kNo };
void Stop(const FurtherScrollingAllowed aFurtherScrollingAllowed) {
MOZ_ASSERT((aFurtherScrollingAllowed == FurtherScrollingAllowed::kNo) ||
(mFurtherScrollingAllowed == FurtherScrollingAllowed::kYes));
if (mTimer) {
mTimer->Cancel();
mTimer = nullptr;
}
mContent = nullptr;
mFurtherScrollingAllowed = aFurtherScrollingAllowed;
}
void SetDelay(uint32_t aDelayInMs) { mDelayInMs = aDelayInMs; }
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Notify(nsITimer* timer) override {
if (mPresContext) {
AutoWeakFrame frame =
mContent ? mPresContext->GetPrimaryFrameFor(mContent) : nullptr;
if (!frame) {
return NS_OK;
}
mContent = nullptr;
nsPoint pt = mPoint - frame->GetOffsetTo(
mPresContext->PresShell()->GetRootFrame());
RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
frameSelection->HandleDrag(frame, pt);
if (!frame.IsAlive()) {
return NS_OK;
}
NS_ASSERTION(frame->PresContext() == mPresContext, "document mismatch?");
DoAutoScroll(frame, pt);
}
return NS_OK;
}
NS_IMETHOD GetName(nsACString& aName) override {
aName.AssignLiteral("AutoScroller");
return NS_OK;
}
protected:
virtual ~AutoScroller() {
if (mTimer) {
mTimer->Cancel();
}
}
private:
nsFrameSelection* const mFrameSelection;
nsPresContext* mPresContext;
// relative to mPresContext's root frame
nsPoint mPoint;
nsCOMPtr<nsITimer> mTimer;
nsCOMPtr<nsIContent> mContent;
uint32_t mDelayInMs;
FurtherScrollingAllowed mFurtherScrollingAllowed;
};
NS_IMPL_ISUPPORTS(AutoScroller, nsITimerCallback, nsINamed)
#ifdef PRINT_RANGE
void printRange(nsRange* aDomRange) {
if (!aDomRange) {
printf("NULL Range\n");
}
nsINode* startNode = aDomRange->GetStartContainer();
nsINode* endNode = aDomRange->GetEndContainer();
int32_t startOffset = aDomRange->StartOffset();
int32_t endOffset = aDomRange->EndOffset();
printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n",
(unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset,
(unsigned long)endNode, (long)endOffset);
}
#endif /* PRINT_RANGE */
void Selection::Stringify(nsAString& aResult, FlushFrames aFlushFrames) {
if (aFlushFrames == FlushFrames::Yes) {
// We need FlushType::Frames here to make sure frames have been created for
// the selected content. Use mFrameSelection->GetPresShell() which returns
// null if the Selection has been disconnected (the shell is Destroyed).
RefPtr<PresShell> presShell =
mFrameSelection ? mFrameSelection->GetPresShell() : nullptr;
if (!presShell) {
aResult.Truncate();
return;
}
presShell->FlushPendingNotifications(FlushType::Frames);
}
IgnoredErrorResult rv;
ToStringWithFormat(u"text/plain"_ns, nsIDocumentEncoder::SkipInvisibleContent,
0, aResult, rv);
if (rv.Failed()) {
aResult.Truncate();
}
}
void Selection::ToStringWithFormat(const nsAString& aFormatType,
uint32_t aFlags, int32_t aWrapCol,
nsAString& aReturn, ErrorResult& aRv) {
nsCOMPtr<nsIDocumentEncoder> encoder =
do_createDocumentEncoder(NS_ConvertUTF16toUTF8(aFormatType).get());
if (!encoder) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
PresShell* presShell = GetPresShell();
if (!presShell) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
Document* doc = presShell->GetDocument();
// Flags should always include OutputSelectionOnly if we're coming from here:
aFlags |= nsIDocumentEncoder::OutputSelectionOnly;
nsAutoString readstring;
readstring.Assign(aFormatType);
nsresult rv = encoder->Init(doc, readstring, aFlags);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}
encoder->SetSelection(this);
if (aWrapCol != 0) encoder->SetWrapColumn(aWrapCol);
rv = encoder->EncodeToString(aReturn);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
}
}
nsresult Selection::SetInterlinePosition(InterlinePosition aInterlinePosition) {
MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
MOZ_ASSERT(aInterlinePosition != InterlinePosition::Undefined);
if (!mFrameSelection) {
return NS_ERROR_NOT_INITIALIZED; // Can't do selection
}
mFrameSelection->SetHint(aInterlinePosition ==
InterlinePosition::StartOfNextLine
? CaretAssociationHint::After
: CaretAssociationHint::Before);
return NS_OK;
}
Selection::InterlinePosition Selection::GetInterlinePosition() const {
MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
if (!mFrameSelection) {
return InterlinePosition::Undefined;
}
return mFrameSelection->GetHint() == CaretAssociationHint::After
? InterlinePosition::StartOfNextLine
: InterlinePosition::EndOfLine;
}
void Selection::SetInterlinePositionJS(bool aHintRight, ErrorResult& aRv) {
MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
aRv = SetInterlinePosition(aHintRight ? InterlinePosition::StartOfNextLine
: InterlinePosition::EndOfLine);
}
bool Selection::GetInterlinePositionJS(ErrorResult& aRv) const {
const InterlinePosition interlinePosition = GetInterlinePosition();
if (interlinePosition == InterlinePosition::Undefined) {
aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection
return false;
}
return interlinePosition == InterlinePosition::StartOfNextLine;
}
static bool IsEditorNode(const nsINode* aNode) {
if (!aNode) {
return false;
}
if (aNode->IsEditable()) {
return true;
}
auto* element = Element::FromNode(aNode);
return element && element->State().HasState(ElementState::READWRITE);
}
bool Selection::IsEditorSelection() const {
return IsEditorNode(GetFocusNode());
}
Nullable<int16_t> Selection::GetCaretBidiLevel(
mozilla::ErrorResult& aRv) const {
MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
if (!mFrameSelection) {
aRv.Throw(NS_ERROR_NOT_INITIALIZED);
return Nullable<int16_t>();
}
mozilla::intl::BidiEmbeddingLevel caretBidiLevel =
static_cast<mozilla::intl::BidiEmbeddingLevel>(
mFrameSelection->GetCaretBidiLevel());
return (caretBidiLevel & BIDI_LEVEL_UNDEFINED)
? Nullable<int16_t>()
: Nullable<int16_t>(caretBidiLevel);
}
void Selection::SetCaretBidiLevel(const Nullable<int16_t>& aCaretBidiLevel,
mozilla::ErrorResult& aRv) {
MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
if (!mFrameSelection) {
aRv.Throw(NS_ERROR_NOT_INITIALIZED);
return;
}
if (aCaretBidiLevel.IsNull()) {
mFrameSelection->UndefineCaretBidiLevel();
} else {
mFrameSelection->SetCaretBidiLevelAndMaybeSchedulePaint(
mozilla::intl::BidiEmbeddingLevel(aCaretBidiLevel.Value()));
}
}
/**
* Test whether the supplied range points to a single table element.
* Result is one of the TableSelectionMode constants. "None" means
* a table element isn't selected.
*/
// TODO: Figure out TableSelectionMode::Column and TableSelectionMode::AllCells
static nsresult GetTableSelectionMode(const nsRange& aRange,
TableSelectionMode* aTableSelectionType) {
if (!aTableSelectionType) {
return NS_ERROR_NULL_POINTER;
}
*aTableSelectionType = TableSelectionMode::None;
nsINode* startNode = aRange.GetStartContainer();
if (!startNode) {
return NS_ERROR_FAILURE;
}
nsINode* endNode = aRange.GetEndContainer();
if (!endNode) {
return NS_ERROR_FAILURE;
}
// Not a single selected node
if (startNode != endNode) {
return NS_OK;
}
nsIContent* child = aRange.GetChildAtStartOffset();
// Not a single selected node
if (!child || child->GetNextSibling() != aRange.GetChildAtEndOffset()) {
return NS_OK;
}
if (!startNode->IsHTMLElement()) {
// Implies a check for being an element; if we ever make this work
// for non-HTML, need to keep checking for elements.
return NS_OK;
}
if (startNode->IsHTMLElement(nsGkAtoms::tr)) {
*aTableSelectionType = TableSelectionMode::Cell;
} else // check to see if we are selecting a table or row (column and all
// cells not done yet)
{
if (child->IsHTMLElement(nsGkAtoms::table)) {
*aTableSelectionType = TableSelectionMode::Table;
} else if (child->IsHTMLElement(nsGkAtoms::tr)) {
*aTableSelectionType = TableSelectionMode::Row;
}
}
return NS_OK;
}
nsresult Selection::MaybeAddTableCellRange(nsRange& aRange,
Maybe<size_t>* aOutIndex) {
if (!aOutIndex) {
return NS_ERROR_NULL_POINTER;
}
MOZ_ASSERT(aOutIndex->isNothing());
if (!mFrameSelection) {
return NS_OK;
}
// Get if we are adding a cell selection and the row, col of cell if we are
TableSelectionMode tableMode;
nsresult result = GetTableSelectionMode(aRange, &tableMode);
if (NS_FAILED(result)) return result;
// If not adding a cell range, we are done here
if (tableMode != TableSelectionMode::Cell) {
mFrameSelection->mTableSelection.mMode = tableMode;
// Don't fail if range isn't a selected cell, aDidAddRange tells caller if
// we didn't proceed
return NS_OK;
}
// Set frame selection mode only if not already set to a table mode
// so we don't lose the select row and column flags (not detected by
// getTableCellLocation)
if (mFrameSelection->mTableSelection.mMode == TableSelectionMode::None) {
mFrameSelection->mTableSelection.mMode = tableMode;
}
return AddRangesForSelectableNodes(&aRange, aOutIndex,
DispatchSelectstartEvent::Maybe);
}
Selection::Selection(SelectionType aSelectionType,
nsFrameSelection* aFrameSelection)
: mFrameSelection(aFrameSelection),
mCachedOffsetForFrame(nullptr),
mDirection(eDirNext),
mSelectionType(aSelectionType),
mCustomColors(nullptr),
mSelectionChangeBlockerCount(0),
mUserInitiated(false),
mCalledByJS(false),
mNotifyAutoCopy(false) {}
Selection::~Selection() { Disconnect(); }
void Selection::Disconnect() {
RemoveAnchorFocusRange();
mStyledRanges.UnregisterSelection();
if (mAutoScroller) {
mAutoScroller->Stop(AutoScroller::FurtherScrollingAllowed::kNo);
mAutoScroller = nullptr;
}
mScrollEvent.Revoke();
if (mCachedOffsetForFrame) {
delete mCachedOffsetForFrame;
mCachedOffsetForFrame = nullptr;
}
}
Document* Selection::GetParentObject() const {
PresShell* presShell = GetPresShell();
return presShell ? presShell->GetDocument() : nullptr;
}
DocGroup* Selection::GetDocGroup() const {
PresShell* presShell = GetPresShell();
if (!presShell) {
return nullptr;
}
Document* doc = presShell->GetDocument();
return doc ? doc->GetDocGroup() : nullptr;
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Selection)
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Selection)
// Unlink the selection listeners *before* we do RemoveAllRangesInternal since
// we don't want to notify the listeners during JS GC (they could be
// in JS!).
tmp->mNotifyAutoCopy = false;
if (tmp->mAccessibleCaretEventHub) {
tmp->StopNotifyingAccessibleCaretEventHub();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionChangeEventDispatcher)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionListeners)
MOZ_KnownLive(tmp)->RemoveAllRangesInternal(IgnoreErrors());
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrameSelection)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mHighlightData.mHighlight)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Selection)
{
uint32_t i, count = tmp->mStyledRanges.Length();
for (i = 0; i < count; ++i) {
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyledRanges.mRanges[i].mRange)
}
count = tmp->mStyledRanges.mInvalidStaticRanges.Length();
for (i = 0; i < count; ++i) {
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
mStyledRanges.mInvalidStaticRanges[i].mRange);
}
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorFocusRange)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameSelection)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHighlightData.mHighlight)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionChangeEventDispatcher)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionListeners)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
// QueryInterface implementation for Selection
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Selection)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(Selection)
NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(Selection, Disconnect())
const RangeBoundary& Selection::AnchorRef(
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const {
if (!mAnchorFocusRange) {
static RangeBoundary sEmpty;
return sEmpty;
}
if (GetDirection() == eDirNext) {
return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
? mAnchorFocusRange->MayCrossShadowBoundaryStartRef()
: mAnchorFocusRange->StartRef();
}
return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
? mAnchorFocusRange->MayCrossShadowBoundaryEndRef()
: mAnchorFocusRange->EndRef();
}
const RangeBoundary& Selection::FocusRef(
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const {
if (!mAnchorFocusRange) {
static RangeBoundary sEmpty;
return sEmpty;
}
if (GetDirection() == eDirNext) {
return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
? mAnchorFocusRange->MayCrossShadowBoundaryEndRef()
: mAnchorFocusRange->EndRef();
}
return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes
? mAnchorFocusRange->MayCrossShadowBoundaryStartRef()
: mAnchorFocusRange->StartRef();
}
void Selection::SetAnchorFocusRange(size_t aIndex) {
if (aIndex >= mStyledRanges.Length()) {
return;
}
// Highlight selections may contain static ranges.
MOZ_ASSERT(mSelectionType != SelectionType::eHighlight);
AbstractRange* anchorFocusRange = mStyledRanges.mRanges[aIndex].mRange;
mAnchorFocusRange = anchorFocusRange->AsDynamicRange();
}
static int32_t CompareToRangeStart(const nsINode& aCompareNode,
uint32_t aCompareOffset,
const AbstractRange& aRange,
nsContentUtils::NodeIndexCache* aCache) {
MOZ_ASSERT(aRange.GetMayCrossShadowBoundaryStartContainer());
nsINode* start = aRange.GetMayCrossShadowBoundaryStartContainer();
// If the nodes that we're comparing are not in the same document, assume that
// aCompareNode will fall at the end of the ranges.
if (aCompareNode.GetComposedDoc() != start->GetComposedDoc() ||
!start->GetComposedDoc()) {
NS_WARNING(
"`CompareToRangeStart` couldn't compare nodes, pretending some order.");
return 1;
}
// The points are in the same subtree, hence there has to be an order.
return *nsContentUtils::ComparePoints(
&aCompareNode, aCompareOffset, start,
aRange.MayCrossShadowBoundaryStartOffset(), aCache);
}
static int32_t CompareToRangeStart(const nsINode& aCompareNode,
uint32_t aCompareOffset,
const AbstractRange& aRange) {
return CompareToRangeStart(aCompareNode, aCompareOffset, aRange, nullptr);
}
static int32_t CompareToRangeEnd(const nsINode& aCompareNode,
uint32_t aCompareOffset,
const AbstractRange& aRange) {
MOZ_ASSERT(aRange.IsPositioned());
nsINode* end = aRange.GetMayCrossShadowBoundaryEndContainer();
// If the nodes that we're comparing are not in the same document or in the
// same subtree, assume that aCompareNode will fall at the end of the ranges.
if (aCompareNode.GetComposedDoc() != end->GetComposedDoc() ||
!end->GetComposedDoc()) {
NS_WARNING(
"`CompareToRangeEnd` couldn't compare nodes, pretending some order.");
return 1;
}
// The points are in the same subtree, hence there has to be an order.
return *nsContentUtils::ComparePoints(
&aCompareNode, aCompareOffset, end,
aRange.MayCrossShadowBoundaryEndOffset());
}
// static
size_t Selection::StyledRanges::FindInsertionPoint(
const nsTArray<StyledRange>* aElementArray, const nsINode& aPointNode,
uint32_t aPointOffset,
int32_t (*aComparator)(const nsINode&, uint32_t, const AbstractRange&)) {
int32_t beginSearch = 0;
int32_t endSearch = aElementArray->Length(); // one beyond what to check
if (endSearch) {
int32_t center = endSearch - 1; // Check last index, then binary search
do {
const AbstractRange* range = (*aElementArray)[center].mRange;
int32_t cmp{aComparator(aPointNode, aPointOffset, *range)};
if (cmp < 0) { // point < cur
endSearch = center;
} else if (cmp > 0) { // point > cur
beginSearch = center + 1;
} else { // found match, done
beginSearch = center;
break;
}
center = (endSearch - beginSearch) / 2 + beginSearch;
} while (endSearch - beginSearch > 0);
}
return AssertedCast<size_t>(beginSearch);
}
// Selection::SubtractRange
//
// A helper function that subtracts aSubtract from aRange, and adds
// 1 or 2 StyledRange objects representing the remaining non-overlapping
// difference to aOutput. It is assumed that the caller has checked that
// aRange and aSubtract do indeed overlap
// static
nsresult Selection::StyledRanges::SubtractRange(
StyledRange& aRange, nsRange& aSubtract, nsTArray<StyledRange>* aOutput) {
AbstractRange* range = aRange.mRange;
if (NS_WARN_IF(!range->IsPositioned())) {
return NS_ERROR_UNEXPECTED;
}
if (range->GetStartContainer()->SubtreeRoot() !=
aSubtract.GetStartContainer()->SubtreeRoot()) {
// These are ranges for different shadow trees, we can't subtract them in
// any sensible way.
aOutput->InsertElementAt(0, aRange);
return NS_OK;
}
// First we want to compare to the range start
int32_t cmp{CompareToRangeStart(*range->GetStartContainer(),
range->StartOffset(), aSubtract)};
// Also, make a comparison to the range end
int32_t cmp2{CompareToRangeEnd(*range->GetEndContainer(), range->EndOffset(),
aSubtract)};
// If the existing range left overlaps the new range (aSubtract) then
// cmp < 0, and cmp2 < 0
// If it right overlaps the new range then cmp > 0 and cmp2 > 0
// If it fully contains the new range, then cmp < 0 and cmp2 > 0
if (cmp2 > 0) {
// We need to add a new StyledRange to the output, running from
// the end of aSubtract to the end of range
ErrorResult error;
RefPtr<nsRange> postOverlap =
nsRange::Create(aSubtract.EndRef(), range->EndRef(), error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
MOZ_ASSERT(postOverlap);
if (!postOverlap->Collapsed()) {
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
aOutput->InsertElementAt(0, StyledRange(postOverlap));
(*aOutput)[0].mTextRangeStyle = aRange.mTextRangeStyle;
}
}
if (cmp < 0) {
// We need to add a new StyledRange to the output, running from
// the start of the range to the start of aSubtract
ErrorResult error;
RefPtr<nsRange> preOverlap =
nsRange::Create(range->StartRef(), aSubtract.StartRef(), error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
MOZ_ASSERT(preOverlap);
if (!preOverlap->Collapsed()) {
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
aOutput->InsertElementAt(0, StyledRange(preOverlap));
(*aOutput)[0].mTextRangeStyle = aRange.mTextRangeStyle;
}
}
return NS_OK;
}
static void UserSelectRangesToAdd(nsRange* aItem,
nsTArray<RefPtr<nsRange>>& aRangesToAdd) {
// We cannot directly call IsEditorSelection() because we may be in an
// inconsistent state during Collapse() (we're cleared already but we haven't
// got a new focus node yet).
if (IsEditorNode(aItem->GetStartContainer()) &&
IsEditorNode(aItem->GetEndContainer())) {
// Don't mess with the selection ranges for editing, editor doesn't really
// deal well with multi-range selections.
aRangesToAdd.AppendElement(aItem);
} else {
aItem->ExcludeNonSelectableNodes(&aRangesToAdd);
}
}
static nsINode* DetermineSelectstartEventTarget(
const bool aSelectionEventsOnTextControlsEnabled, const nsRange& aRange) {
nsINode* target = aRange.GetStartContainer();
if (aSelectionEventsOnTextControlsEnabled) {
// Get the first element which isn't in a native anonymous subtree
while (target && target->IsInNativeAnonymousSubtree()) {
target = target->GetParent();
}
} else {
if (target->IsInNativeAnonymousSubtree()) {
// This is a selection under a text control, so don't dispatch the
// event.
target = nullptr;
}
}
return target;
}
/**
* @return true, iff the default action should be executed.
*/
static bool MaybeDispatchSelectstartEvent(
const nsRange& aRange, const bool aSelectionEventsOnTextControlsEnabled,
Document* aDocument) {
nsCOMPtr<nsINode> selectstartEventTarget = DetermineSelectstartEventTarget(
aSelectionEventsOnTextControlsEnabled, aRange);
bool executeDefaultAction = true;
if (selectstartEventTarget) {
nsContentUtils::DispatchTrustedEvent(
aDocument, selectstartEventTarget, u"selectstart"_ns, CanBubble::eYes,
Cancelable::eYes, &executeDefaultAction);
}
return executeDefaultAction;
}
// static
bool Selection::IsUserSelectionCollapsed(
const nsRange& aRange, nsTArray<RefPtr<nsRange>>& aTempRangesToAdd) {
MOZ_ASSERT(aTempRangesToAdd.IsEmpty());
RefPtr<nsRange> scratchRange = aRange.CloneRange();
UserSelectRangesToAdd(scratchRange, aTempRangesToAdd);
const bool userSelectionCollapsed =
(aTempRangesToAdd.Length() == 0) ||
((aTempRangesToAdd.Length() == 1) && aTempRangesToAdd[0]->Collapsed());
aTempRangesToAdd.ClearAndRetainStorage();
return userSelectionCollapsed;
}
nsresult Selection::AddRangesForUserSelectableNodes(
nsRange* aRange, Maybe<size_t>* aOutIndex,
const DispatchSelectstartEvent aDispatchSelectstartEvent) {
MOZ_ASSERT(mUserInitiated);
MOZ_ASSERT(aOutIndex);
MOZ_ASSERT(aOutIndex->isNothing());
if (!aRange) {
return NS_ERROR_NULL_POINTER;
}
if (!aRange->IsPositioned()) {
return NS_ERROR_UNEXPECTED;
}
AutoTArray<RefPtr<nsRange>, 4> rangesToAdd;
if (mStyledRanges.Length()) {
aOutIndex->emplace(mStyledRanges.Length() - 1);
}
Document* doc = GetDocument();
if (aDispatchSelectstartEvent == DispatchSelectstartEvent::Maybe &&
mSelectionType == SelectionType::eNormal && IsCollapsed() &&
!IsBlockingSelectionChangeEvents()) {
// We consider a selection to be starting if we are currently collapsed,
// and the selection is becoming uncollapsed, and this is caused by a
// user initiated event.
// First, we generate the ranges to add with a scratch range, which is a
// clone of the original range passed in. We do this seperately, because
// the selectstart event could have caused the world to change, and
// required ranges to be re-generated
const bool userSelectionCollapsed =
IsUserSelectionCollapsed(*aRange, rangesToAdd);
MOZ_ASSERT(userSelectionCollapsed || nsContentUtils::IsSafeToRunScript());
if (!userSelectionCollapsed && nsContentUtils::IsSafeToRunScript()) {
// The spec currently doesn't say that we should dispatch this event
// on text controls, so for now we only support doing that under a
// pref, disabled by default.
const bool executeDefaultAction = MaybeDispatchSelectstartEvent(
*aRange,
StaticPrefs::dom_select_events_textcontrols_selectstart_enabled(),
doc);
if (!executeDefaultAction) {
return NS_OK;
}
// As we potentially dispatched an event to the DOM, something could have
// changed under our feet. Re-generate the rangesToAdd array, and
// ensure that the range we are about to add is still valid.
if (!aRange->IsPositioned()) {
return NS_ERROR_UNEXPECTED;
}
}
}
// Generate the ranges to add
UserSelectRangesToAdd(aRange, rangesToAdd);
size_t newAnchorFocusIndex =
GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1;
for (size_t i = 0; i < rangesToAdd.Length(); ++i) {
Maybe<size_t> index;
// `MOZ_KnownLive` needed because of broken static analysis
nsresult rv = mStyledRanges.MaybeAddRangeAndTruncateOverlaps(
MOZ_KnownLive(rangesToAdd[i]), &index);
NS_ENSURE_SUCCESS(rv, rv);
if (i == newAnchorFocusIndex) {
*aOutIndex = index;
rangesToAdd[i]->SetIsGenerated(false);
} else {
rangesToAdd[i]->SetIsGenerated(true);
}
}
return NS_OK;
}
nsresult Selection::AddRangesForSelectableNodes(
nsRange* aRange, Maybe<size_t>* aOutIndex,
const DispatchSelectstartEvent aDispatchSelectstartEvent) {
MOZ_ASSERT(aOutIndex);
MOZ_ASSERT(aOutIndex->isNothing());
if (!aRange) {
return NS_ERROR_NULL_POINTER;
}
if (!aRange->IsPositioned()) {
return NS_ERROR_UNEXPECTED;
}
MOZ_LOG(
sSelectionLog, LogLevel::Debug,
("%s: selection=%p, type=%i, range=(%p, StartOffset=%u, EndOffset=%u)",
__FUNCTION__, this, static_cast<int>(GetType()), aRange,
aRange->StartOffset(), aRange->EndOffset()));
if (mUserInitiated) {
return AddRangesForUserSelectableNodes(aRange, aOutIndex,
aDispatchSelectstartEvent);
}
return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex);
}
nsresult Selection::StyledRanges::AddRangeAndIgnoreOverlaps(
AbstractRange* aRange) {
MOZ_ASSERT(aRange);
MOZ_ASSERT(aRange->IsPositioned());
MOZ_ASSERT(mSelection.mSelectionType == SelectionType::eHighlight);
if (aRange->IsStaticRange() && !aRange->AsStaticRange()->IsValid()) {
mInvalidStaticRanges.AppendElement(StyledRange(aRange));
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
return NS_OK;
}
// a common case is that we have no ranges yet
if (mRanges.Length() == 0) {
mRanges.AppendElement(StyledRange(aRange));
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
return NS_OK;
}
Maybe<size_t> maybeStartIndex, maybeEndIndex;
nsresult rv =
GetIndicesForInterval(aRange->GetStartContainer(), aRange->StartOffset(),
aRange->GetEndContainer(), aRange->EndOffset(),
false, maybeStartIndex, maybeEndIndex);
NS_ENSURE_SUCCESS(rv, rv);
size_t startIndex(0);
if (maybeEndIndex.isNothing()) {
// All ranges start after the given range. We can insert our range at
// position 0.
startIndex = 0;
} else if (maybeStartIndex.isNothing()) {
// All ranges end before the given range. We can insert our range at
// the end of the array.
startIndex = mRanges.Length();
} else {
startIndex = *maybeStartIndex;
}
mRanges.InsertElementAt(startIndex, StyledRange(aRange));
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
return NS_OK;
}
nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
nsRange* aRange, Maybe<size_t>* aOutIndex) {
MOZ_ASSERT(aRange);
MOZ_ASSERT(aRange->IsPositioned());
MOZ_ASSERT(aOutIndex);
MOZ_ASSERT(aOutIndex->isNothing());
// a common case is that we have no ranges yet
if (mRanges.Length() == 0) {
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
mRanges.AppendElement(StyledRange(aRange));
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
aOutIndex->emplace(0u);
return NS_OK;
}
Maybe<size_t> maybeStartIndex, maybeEndIndex;
nsresult rv =
GetIndicesForInterval(aRange->GetStartContainer(), aRange->StartOffset(),
aRange->GetEndContainer(), aRange->EndOffset(),
false, maybeStartIndex, maybeEndIndex);
NS_ENSURE_SUCCESS(rv, rv);
size_t startIndex, endIndex;
if (maybeEndIndex.isNothing()) {
// All ranges start after the given range. We can insert our range at
// position 0, knowing there are no overlaps (handled below)
startIndex = endIndex = 0;
} else if (maybeStartIndex.isNothing()) {
// All ranges end before the given range. We can insert our range at
// the end of the array, knowing there are no overlaps (handled below)
startIndex = endIndex = mRanges.Length();
} else {
startIndex = *maybeStartIndex;
endIndex = *maybeEndIndex;
}
// If the range is already contained in mRanges, silently
// succeed
const bool sameRange = HasEqualRangeBoundariesAt(*aRange, startIndex);
if (sameRange) {
aOutIndex->emplace(startIndex);
return NS_OK;
}
if (startIndex == endIndex) {
// The new range doesn't overlap any existing ranges
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
mRanges.InsertElementAt(startIndex, StyledRange(aRange));
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
aOutIndex->emplace(startIndex);
return NS_OK;
}
// We now know that at least 1 existing range overlaps with the range that
// we are trying to add. In fact, the only ranges of interest are those at
// the two end points, startIndex and endIndex - 1 (which may point to the
// same range) as these may partially overlap the new range. Any ranges
// between these indices are fully overlapped by the new range, and so can be
// removed.
AutoTArray<StyledRange, 2> overlaps;
overlaps.AppendElement(mRanges[startIndex]);
if (endIndex - 1 != startIndex) {
overlaps.AppendElement(mRanges[endIndex - 1]);
}
// Remove all the overlapping ranges
for (size_t i = startIndex; i < endIndex; ++i) {
mRanges[i].mRange->UnregisterSelection(mSelection);
}
mRanges.RemoveElementsAt(startIndex, endIndex - startIndex);
AutoTArray<StyledRange, 3> temp;
for (const size_t i : Reversed(IntegerRange(overlaps.Length()))) {
nsresult rv = SubtractRange(overlaps[i], *aRange, &temp);
NS_ENSURE_SUCCESS(rv, rv);
}
// Insert the new element into our "leftovers" array
// `aRange` is positioned, so it has to have a start container.
size_t insertionPoint{FindInsertionPoint(&temp, *aRange->GetStartContainer(),
aRange->StartOffset(),
CompareToRangeStart)};
temp.InsertElementAt(insertionPoint, StyledRange(aRange));
// Merge the leftovers back in to mRanges
mRanges.InsertElementsAt(startIndex, temp);
for (uint32_t i = 0; i < temp.Length(); ++i) {
if (temp[i].mRange->IsDynamicRange()) {
MOZ_KnownLive(temp[i].mRange->AsDynamicRange())
->RegisterSelection(MOZ_KnownLive(mSelection));
// `MOZ_KnownLive` is required because of
}
}
aOutIndex->emplace(startIndex + insertionPoint);
return NS_OK;
}
nsresult Selection::StyledRanges::RemoveRangeAndUnregisterSelection(
AbstractRange& aRange) {
// Find the range's index & remove it. We could use FindInsertionPoint to
// get O(log n) time, but that requires many expensive DOM comparisons.
// For even several thousand items, this is probably faster because the
// comparisons are so fast.
int32_t idx = -1;
uint32_t i;
for (i = 0; i < mRanges.Length(); i++) {
if (mRanges[i].mRange == &aRange) {
idx = (int32_t)i;
break;
}
}
if (idx < 0) return NS_ERROR_DOM_NOT_FOUND_ERR;
mRanges.RemoveElementAt(idx);
aRange.UnregisterSelection(mSelection);
return NS_OK;
}
nsresult Selection::RemoveCollapsedRanges() {
if (NeedsToLogSelectionAPI(*this)) {
LogSelectionAPI(this, __FUNCTION__);
LogStackForSelectionAPI();
}
return mStyledRanges.RemoveCollapsedRanges();
}
nsresult Selection::StyledRanges::RemoveCollapsedRanges() {
uint32_t i = 0;
while (i < mRanges.Length()) {
const AbstractRange* range = mRanges[i].mRange;
// If nsRange::mCrossShadowBoundaryRange exists, it means
// there's a cross boundary selection, so obviously
// we shouldn't remove this range.
const bool collapsed =
range->Collapsed() && !range->MayCrossShadowBoundary();
// Cross boundary range should always be uncollapsed.
MOZ_ASSERT_IF(
range->MayCrossShadowBoundary(),
!range->AsDynamicRange()->CrossShadowBoundaryRangeCollapsed());
if (collapsed) {
nsresult rv = RemoveRangeAndUnregisterSelection(*mRanges[i].mRange);
NS_ENSURE_SUCCESS(rv, rv);
} else {
++i;
}
}
return NS_OK;
}
void Selection::Clear(nsPresContext* aPresContext) {
RemoveAnchorFocusRange();
mStyledRanges.UnregisterSelection();
for (uint32_t i = 0; i < mStyledRanges.Length(); ++i) {
SelectFrames(aPresContext, *mStyledRanges.mRanges[i].mRange, false);
}
mStyledRanges.Clear();
// Reset direction so for more dependable table selection range handling
SetDirection(eDirNext);
// If this was an ATTENTION selection, change it back to normal now
if (mFrameSelection && mFrameSelection->GetDisplaySelection() ==
nsISelectionController::SELECTION_ATTENTION) {
mFrameSelection->SetDisplaySelection(nsISelectionController::SELECTION_ON);
}
}
bool Selection::StyledRanges::HasEqualRangeBoundariesAt(
const AbstractRange& aRange, size_t aRangeIndex) const {
if (aRangeIndex < mRanges.Length()) {
const AbstractRange* range = mRanges[aRangeIndex].mRange;
return range->HasEqualBoundaries(aRange);
}
return false;
}
void Selection::GetRangesForInterval(nsINode& aBeginNode, uint32_t aBeginOffset,
nsINode& aEndNode, uint32_t aEndOffset,
bool aAllowAdjacent,
nsTArray<RefPtr<nsRange>>& aReturn,
mozilla::ErrorResult& aRv) {
AutoTArray<nsRange*, 2> results;
nsresult rv =
GetDynamicRangesForIntervalArray(&aBeginNode, aBeginOffset, &aEndNode,
aEndOffset, aAllowAdjacent, &results);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}
aReturn.SetLength(results.Length());
for (size_t i = 0; i < results.Length(); ++i) {
aReturn[i] = results[i]; // AddRefs
}
}
nsresult Selection::GetAbstractRangesForIntervalArray(
nsINode* aBeginNode, uint32_t aBeginOffset, nsINode* aEndNode,
uint32_t aEndOffset, bool aAllowAdjacent,
nsTArray<AbstractRange*>* aRanges) {
if (NS_WARN_IF(!aBeginNode)) {
return NS_ERROR_UNEXPECTED;
}
if (NS_WARN_IF(!aEndNode)) {
return NS_ERROR_UNEXPECTED;
}
aRanges->Clear();
Maybe<size_t> maybeStartIndex, maybeEndIndex;
nsresult res = mStyledRanges.GetIndicesForInterval(
aBeginNode, aBeginOffset, aEndNode, aEndOffset, aAllowAdjacent,
maybeStartIndex, maybeEndIndex);
NS_ENSURE_SUCCESS(res, res);
if (maybeStartIndex.isNothing() || maybeEndIndex.isNothing()) {
return NS_OK;
}
for (const size_t i : IntegerRange(*maybeStartIndex, *maybeEndIndex)) {
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
aRanges->AppendElement(mStyledRanges.mRanges[i].mRange);
}
return NS_OK;
}
nsresult Selection::GetDynamicRangesForIntervalArray(
nsINode* aBeginNode, uint32_t aBeginOffset, nsINode* aEndNode,
uint32_t aEndOffset, bool aAllowAdjacent, nsTArray<nsRange*>* aRanges) {
MOZ_ASSERT(mSelectionType != SelectionType::eHighlight);
AutoTArray<AbstractRange*, 2> abstractRanges;
nsresult rv = GetAbstractRangesForIntervalArray(
aBeginNode, aBeginOffset, aEndNode, aEndOffset, aAllowAdjacent,
&abstractRanges);
NS_ENSURE_SUCCESS(rv, rv);
aRanges->Clear();
aRanges->SetCapacity(abstractRanges.Length());
for (auto* abstractRange : abstractRanges) {
aRanges->AppendElement(abstractRange->AsDynamicRange());
}
return NS_OK;
}
void Selection::StyledRanges::ReorderRangesIfNecessary() {
const Document* doc = mSelection.GetDocument();
if (!doc) {
return;
}
if (mRanges.Length() < 2 && mInvalidStaticRanges.IsEmpty()) {
// There is nothing to be reordered.
return;
}
const int32_t currentDocumentGeneration = doc->GetGeneration();
const bool domMutationHasHappened =
currentDocumentGeneration != mDocumentGeneration;
if (domMutationHasHappened) {
// After a DOM mutation, invalid static ranges might have become valid and
// valid static ranges might have become invalid.
StyledRangeArray invalidStaticRanges;
for (StyledRangeArray::const_iterator iter = mRanges.begin();
iter != mRanges.end();) {
const AbstractRange* range = iter->mRange;
if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) {
invalidStaticRanges.AppendElement(*iter);
iter = mRanges.RemoveElementAt(iter);
} else {
++iter;
}
}
for (StyledRangeArray::const_iterator iter = mInvalidStaticRanges.begin();
iter != mInvalidStaticRanges.end();) {
MOZ_ASSERT(iter->mRange->IsStaticRange());
if (iter->mRange->AsStaticRange()->IsValid()) {
mRanges.AppendElement(*iter);
iter = mInvalidStaticRanges.RemoveElementAt(iter);
} else {
++iter;
}
}
mInvalidStaticRanges.AppendElements(std::move(invalidStaticRanges));
}
if (domMutationHasHappened || mRangesMightHaveChanged) {
// This is hot code. Proceed with caution.
// This path uses a cache that keep the last 100 node/index combinations
// in a stack-allocated array to save up on expensive calls to
// nsINode::ComputeIndexOf() (which happen in
// nsContentUtils::ComparePoints()).
// The second expensive call here is the sort() below, which should be
// avoided if possible. Sorting can be avoided if the ranges are still in
// order. Checking the order is cheap compared to sorting (also, it fills up
// the cache, which is reused by the sort call).
nsContentUtils::NodeIndexCache cache;
bool rangeOrderHasChanged = false;
const nsINode* prevStartContainer = nullptr;
uint32_t prevStartOffset = 0;
for (const StyledRange& range : mRanges) {
const nsINode* startContainer = range.mRange->GetStartContainer();
uint32_t startOffset = range.mRange->StartOffset();
if (!prevStartContainer) {
prevStartContainer = startContainer;
prevStartOffset = startOffset;
continue;
}
// Calling ComparePoints here saves one call of
// AbstractRange::StartOffset() per iteration (which is surprisingly
// expensive).
const Maybe<int32_t> compareResult = nsContentUtils::ComparePoints(
startContainer, startOffset, prevStartContainer, prevStartOffset,
&cache);
// If the nodes are in different subtrees, the Maybe is empty.
// Since CompareToRangeStart pretends ranges to be ordered, this aligns
// to that behavior.
if (compareResult.valueOr(1) != 1) {
rangeOrderHasChanged = true;
break;
}
prevStartContainer = startContainer;
prevStartOffset = startOffset;
}
if (rangeOrderHasChanged) {
mRanges.Sort([&cache](const StyledRange& a, const StyledRange& b) -> int {
return CompareToRangeStart(*a.mRange->GetStartContainer(),
a.mRange->StartOffset(), *b.mRange, &cache);
});
}
mDocumentGeneration = currentDocumentGeneration;
mRangesMightHaveChanged = false;
}
}
nsresult Selection::StyledRanges::GetIndicesForInterval(
const nsINode* aBeginNode, uint32_t aBeginOffset, const nsINode* aEndNode,
uint32_t aEndOffset, bool aAllowAdjacent, Maybe<size_t>& aStartIndex,
Maybe<size_t>& aEndIndex) {
MOZ_ASSERT(aStartIndex.isNothing());
MOZ_ASSERT(aEndIndex.isNothing());
if (NS_WARN_IF(!aBeginNode)) {
return NS_ERROR_INVALID_POINTER;
}
if (NS_WARN_IF(!aEndNode)) {
return NS_ERROR_INVALID_POINTER;
}
ReorderRangesIfNecessary();
if (mRanges.Length() == 0) {
return NS_OK;
}
const bool intervalIsCollapsed =
aBeginNode == aEndNode && aBeginOffset == aEndOffset;
// Ranges that end before the given interval and begin after the given
// interval can be discarded
size_t endsBeforeIndex{FindInsertionPoint(&mRanges, *aEndNode, aEndOffset,
&CompareToRangeStart)};
if (endsBeforeIndex == 0) {
const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange;
// If the interval is strictly before the range at index 0, we can optimize
// by returning now - all ranges start after the given interval
if (!endRange->StartRef().Equals(aEndNode, aEndOffset)) {
return NS_OK;
}
// We now know that the start point of mRanges[0].mRange
// equals the end of the interval. Thus, when aAllowadjacent is true, the
// caller is always interested in this range. However, when excluding
// adjacencies, we must remember to include the range when both it and the
// given interval are collapsed to the same point
if (!aAllowAdjacent && !(endRange->Collapsed() && intervalIsCollapsed))
return NS_OK;
}
aEndIndex.emplace(endsBeforeIndex);
size_t beginsAfterIndex{FindInsertionPoint(&mRanges, *aBeginNode,
aBeginOffset, &CompareToRangeEnd)};
if (beginsAfterIndex == mRanges.Length()) {
return NS_OK; // optimization: all ranges are strictly before us
}
if (aAllowAdjacent) {
// At this point, one of the following holds:
// endsBeforeIndex == mRanges.Length(),
// endsBeforeIndex points to a range whose start point does not equal the
// given interval's start point
// endsBeforeIndex points to a range whose start point equals the given
// interval's start point
// In the final case, there can be two such ranges, a collapsed range, and
// an adjacent range (they will appear in mRanges in that
// order). For this final case, we need to increment endsBeforeIndex, until
// one of the first two possibilities hold
while (endsBeforeIndex < mRanges.Length()) {
const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange;
if (!endRange->StartRef().Equals(aEndNode, aEndOffset)) {
break;
}
endsBeforeIndex++;
}
// Likewise, one of the following holds:
// beginsAfterIndex == 0,
// beginsAfterIndex points to a range whose end point does not equal
// the given interval's end point
// beginsOnOrAfter points to a range whose end point equals the given
// interval's end point
// In the final case, there can be two such ranges, an adjacent range, and
// a collapsed range (they will appear in mRanges in that
// order). For this final case, we only need to take action if both those
// ranges exist, and we are pointing to the collapsed range - we need to
// point to the adjacent range
const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange;
if (beginsAfterIndex > 0 && beginRange->Collapsed() &&
beginRange->EndRef().Equals(aBeginNode, aBeginOffset)) {
beginRange = mRanges[beginsAfterIndex - 1].mRange;
if (beginRange->EndRef().Equals(aBeginNode, aBeginOffset)) {
beginsAfterIndex--;
}
}
} else {
// See above for the possibilities at this point. The only case where we
// need to take action is when the range at beginsAfterIndex ends on
// the given interval's start point, but that range isn't collapsed (a
// collapsed range should be included in the returned results).
const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange;
if (beginRange->MayCrossShadowBoundaryEndRef().Equals(aBeginNode,
aBeginOffset) &&
!beginRange->Collapsed()) {
beginsAfterIndex++;
}
// Again, see above for the meaning of endsBeforeIndex at this point.
// In particular, endsBeforeIndex may point to a collaped range which
// represents the point at the end of the interval - this range should be
// included
if (endsBeforeIndex < mRanges.Length()) {
const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange;
if (endRange->MayCrossShadowBoundaryStartRef().Equals(aEndNode,
aEndOffset) &&
endRange->Collapsed()) {
endsBeforeIndex++;
}
}
}
NS_ASSERTION(beginsAfterIndex <= endsBeforeIndex, "Is mRanges not ordered?");
NS_ENSURE_STATE(beginsAfterIndex <= endsBeforeIndex);
aStartIndex.emplace(beginsAfterIndex);
aEndIndex = Some(endsBeforeIndex);
return NS_OK;
}
nsIFrame* Selection::GetPrimaryFrameForAnchorNode() const {
MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
nsCOMPtr<nsIContent> content = do_QueryInterface(GetAnchorNode());
if (content && mFrameSelection) {
return SelectionMovementUtils::GetFrameForNodeOffset(
content, AnchorOffset(), mFrameSelection->GetHint());
}
return nullptr;
}
PrimaryFrameData Selection::GetPrimaryFrameForCaretAtFocusNode(
bool aVisual) const {
nsIContent* content = nsIContent::FromNodeOrNull(GetFocusNode());
if (!content || !mFrameSelection || !mFrameSelection->GetPresShell()) {
return {};
</