Bug 1810403: Allow `nsRange`s to be in multiple `Selection`s. r=masayuki

The Custom Highlight API allows a use case where a `Range` of a `Highlight`
is also used as `Selection`. Due to the decision to use the `Selection` mechanism
to display `Highlight`s, a `Range` can be part of several `Selection`s.
Since the `Range` has a pointer to its associated `Selection`
to notify about changes, this must be adapted to allow several `Selections`.

As a tradeoff of performance and memory usage, the `Selection`s are stored
as `mozilla::LinkedList`. A helper class `mozilla::SelectionListWrapper`
was implemented to allow `Selection`s to be in multiple of these lists
and without having to be derived from `LinkedListElement<T>`.

To simplify usage of the list, the use case  "does this range belong to Selection x?"
is wrapped into the convenience method`IsInSelection(Selection&)`;
The method previously named like this was renamed to `IsInAnySelection()`
to be named more precisely.

Registering and unregistering of the closest common inclusive ancestor
of the `Range` is done when the first `Selection` is registered and
the last `Selection` is unregistered.

Differential Revision: https://phabricator.services.mozilla.com/D169597
This commit is contained in:
Jan-Niklas Jaeschke 2023-02-21 12:25:28 +00:00
Родитель 5f6910e75e
Коммит 2243494a74
11 изменённых файлов: 210 добавлений и 94 удалений

Просмотреть файл

@ -966,11 +966,10 @@ nsresult Selection::AddRangesForUserSelectableNodes(
GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1; GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1;
for (size_t i = 0; i < rangesToAdd.Length(); ++i) { for (size_t i = 0; i < rangesToAdd.Length(); ++i) {
Maybe<size_t> index; Maybe<size_t> index;
const RefPtr<Selection> selection{this};
// `MOZ_KnownLive` needed because of broken static analysis // `MOZ_KnownLive` needed because of broken static analysis
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253#c1). // (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253#c1).
nsresult rv = mStyledRanges.MaybeAddRangeAndTruncateOverlaps( nsresult rv = mStyledRanges.MaybeAddRangeAndTruncateOverlaps(
MOZ_KnownLive(rangesToAdd[i]), &index, *selection); MOZ_KnownLive(rangesToAdd[i]), &index);
NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_SUCCESS(rv, rv);
if (i == newAnchorFocusIndex) { if (i == newAnchorFocusIndex) {
*aOutIndex = index; *aOutIndex = index;
@ -1007,13 +1006,11 @@ nsresult Selection::AddRangesForSelectableNodes(
aDispatchSelectstartEvent); aDispatchSelectstartEvent);
} }
const RefPtr<Selection> selection{this}; return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex);
return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex,
*selection);
} }
nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps( nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
nsRange* aRange, Maybe<size_t>* aOutIndex, Selection& aSelection) { nsRange* aRange, Maybe<size_t>* aOutIndex) {
MOZ_ASSERT(aRange); MOZ_ASSERT(aRange);
MOZ_ASSERT(aRange->IsPositioned()); MOZ_ASSERT(aRange->IsPositioned());
MOZ_ASSERT(aOutIndex); MOZ_ASSERT(aOutIndex);
@ -1024,7 +1021,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
// XXX(Bug 1631371) Check if this should use a fallible operation as it // XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier. // pretended earlier.
mRanges.AppendElement(StyledRange(aRange)); mRanges.AppendElement(StyledRange(aRange));
aRange->RegisterSelection(aSelection); aRange->RegisterSelection(MOZ_KnownLive(mSelection));
aOutIndex->emplace(0u); aOutIndex->emplace(0u);
return NS_OK; return NS_OK;
@ -1064,7 +1061,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
// XXX(Bug 1631371) Check if this should use a fallible operation as it // XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier. // pretended earlier.
mRanges.InsertElementAt(startIndex, StyledRange(aRange)); mRanges.InsertElementAt(startIndex, StyledRange(aRange));
aRange->RegisterSelection(aSelection); aRange->RegisterSelection(MOZ_KnownLive(mSelection));
aOutIndex->emplace(startIndex); aOutIndex->emplace(startIndex);
return NS_OK; return NS_OK;
} }
@ -1083,7 +1080,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
// Remove all the overlapping ranges // Remove all the overlapping ranges
for (size_t i = startIndex; i < endIndex; ++i) { for (size_t i = startIndex; i < endIndex; ++i) {
mRanges[i].mRange->UnregisterSelection(); mRanges[i].mRange->UnregisterSelection(mSelection);
} }
mRanges.RemoveElementsAt(startIndex, endIndex - startIndex); mRanges.RemoveElementsAt(startIndex, endIndex - startIndex);
@ -1105,7 +1102,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
mRanges.InsertElementsAt(startIndex, temp); mRanges.InsertElementsAt(startIndex, temp);
for (uint32_t i = 0; i < temp.Length(); ++i) { for (uint32_t i = 0; i < temp.Length(); ++i) {
MOZ_KnownLive(temp[i].mRange)->RegisterSelection(aSelection); MOZ_KnownLive(temp[i].mRange)->RegisterSelection(MOZ_KnownLive(mSelection));
// `MOZ_KnownLive` is required because of // `MOZ_KnownLive` is required because of
// https://bugzilla.mozilla.org/show_bug.cgi?id=1622253. // https://bugzilla.mozilla.org/show_bug.cgi?id=1622253.
} }
@ -1131,7 +1128,7 @@ nsresult Selection::StyledRanges::RemoveRangeAndUnregisterSelection(
if (idx < 0) return NS_ERROR_DOM_NOT_FOUND_ERR; if (idx < 0) return NS_ERROR_DOM_NOT_FOUND_ERR;
mRanges.RemoveElementAt(idx); mRanges.RemoveElementAt(idx);
aRange.UnregisterSelection(); aRange.UnregisterSelection(mSelection);
return NS_OK; return NS_OK;
} }
nsresult Selection::RemoveCollapsedRanges() { nsresult Selection::RemoveCollapsedRanges() {
@ -1470,8 +1467,8 @@ nsresult Selection::SelectFramesOfInclusiveDescendantsOfContent(
void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) { void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) {
for (size_t i = 0; i < mStyledRanges.Length(); ++i) { for (size_t i = 0; i < mStyledRanges.Length(); ++i) {
nsRange* range = mStyledRanges.mRanges[i].mRange; nsRange* range = mStyledRanges.mRanges[i].mRange;
MOZ_ASSERT(range->IsInSelection()); MOZ_ASSERT(range->IsInAnySelection());
SelectFrames(aPresContext, range, range->IsInSelection()); SelectFrames(aPresContext, range, range->IsInAnySelection());
} }
} }
@ -1789,7 +1786,7 @@ void Selection::SetAncestorLimiter(nsIContent* aLimiter) {
void Selection::StyledRanges::UnregisterSelection() { void Selection::StyledRanges::UnregisterSelection() {
uint32_t count = mRanges.Length(); uint32_t count = mRanges.Length();
for (uint32_t i = 0; i < count; ++i) { for (uint32_t i = 0; i < count; ++i) {
mRanges[i].mRange->UnregisterSelection(); mRanges[i].mRange->UnregisterSelection(mSelection);
} }
} }
@ -1946,19 +1943,16 @@ void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange,
void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange, void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange,
Document* aDocument, Document* aDocument,
ErrorResult& aRv) { ErrorResult& aRv) {
// If the given range is part of another Selection, we need to clone the RefPtr<nsRange> range = &aRange;
// range first. if (aRange.IsInAnySelection()) {
RefPtr<nsRange> range; if (aRange.IsInSelection(*this)) {
if (aRange.IsInSelection()) {
// If we already have the range, we don't need to handle this. // If we already have the range, we don't need to handle this.
if (aRange.GetSelection() == this) {
return; return;
} }
// Because of performance reason, when there is a cached range, let's use if (mSelectionType != SelectionType::eNormal &&
// it. Otherwise, clone the range. mSelectionType != SelectionType::eHighlight) {
range = aRange.CloneRange(); range = aRange.CloneRange();
} else { }
range = &aRange;
} }
nsINode* rangeRoot = range->GetRoot(); nsINode* rangeRoot = range->GetRoot();

Просмотреть файл

@ -814,6 +814,7 @@ class Selection final : public nsSupportsWeakReference,
void Disconnect(); void Disconnect();
struct StyledRanges { struct StyledRanges {
explicit StyledRanges(Selection& aSelection) : mSelection(aSelection) {}
void Clear(); void Clear();
StyledRange* FindRangeData(nsRange* aRange); StyledRange* FindRangeData(nsRange* aRange);
@ -871,8 +872,8 @@ class Selection final : public nsSupportsWeakReference,
* it. Hence it'll always be in [0, mRanges.Length()). * it. Hence it'll always be in [0, mRanges.Length()).
* This is nothing only when the method returns an error. * This is nothing only when the method returns an error.
*/ */
MOZ_CAN_RUN_SCRIPT nsresult MaybeAddRangeAndTruncateOverlaps( MOZ_CAN_RUN_SCRIPT nsresult
nsRange* aRange, Maybe<size_t>* aOutIndex, Selection& aSelection); MaybeAddRangeAndTruncateOverlaps(nsRange* aRange, Maybe<size_t>* aOutIndex);
/** /**
* GetCommonEditingHost() returns common editing host of all * GetCommonEditingHost() returns common editing host of all
@ -929,9 +930,11 @@ class Selection final : public nsSupportsWeakReference,
// a possible solution, allowing the calculation of the overlap interval in // a possible solution, allowing the calculation of the overlap interval in
// O(log n) time, though this would require rebalancing and other overhead. // O(log n) time, though this would require rebalancing and other overhead.
Elements mRanges; Elements mRanges;
Selection& mSelection;
}; };
StyledRanges mStyledRanges; StyledRanges mStyledRanges{*this};
RefPtr<nsRange> mAnchorFocusRange; RefPtr<nsRange> mAnchorFocusRange;
RefPtr<nsFrameSelection> mFrameSelection; RefPtr<nsFrameSelection> mFrameSelection;

Просмотреть файл

@ -343,7 +343,6 @@ bool nsINode::IsSelected(const uint32_t aStartOffset,
// Collect the selection objects for potential ranges. // Collect the selection objects for potential ranges.
nsTHashSet<Selection*> ancestorSelections; nsTHashSet<Selection*> ancestorSelections;
Selection* prevSelection = nullptr;
for (; n; n = GetClosestCommonInclusiveAncestorForRangeInSelection( for (; n; n = GetClosestCommonInclusiveAncestorForRangeInSelection(
n->GetParentNode())) { n->GetParentNode())) {
const LinkedList<nsRange>* ranges = const LinkedList<nsRange>* ranges =
@ -352,14 +351,12 @@ bool nsINode::IsSelected(const uint32_t aStartOffset,
continue; continue;
} }
for (const nsRange* range : *ranges) { for (const nsRange* range : *ranges) {
MOZ_ASSERT(range->IsInSelection(), MOZ_ASSERT(range->IsInAnySelection(),
"Why is this range registeed with a node?"); "Why is this range registered with a node?");
// Looks like that IsInSelection() assert fails sometimes... // Looks like that IsInSelection() assert fails sometimes...
if (range->IsInSelection()) { if (range->IsInAnySelection()) {
Selection* selection = range->GetSelection(); for (const auto* selectionWrapper : range->GetSelections()) {
if (prevSelection != selection) { ancestorSelections.Insert(selectionWrapper->Get());
prevSelection = selection;
ancestorSelections.Insert(selection);
} }
} }
} }

Просмотреть файл

@ -50,6 +50,11 @@
using namespace mozilla; using namespace mozilla;
using namespace mozilla::dom; using namespace mozilla::dom;
SelectionListWrapper::SelectionListWrapper(Selection* aSelection)
: mSelection(aSelection) {}
NS_IMPL_CYCLE_COLLECTION(SelectionListWrapper)
Selection* SelectionListWrapper::Get() const { return mSelection; }
template already_AddRefed<nsRange> nsRange::Create( template already_AddRefed<nsRange> nsRange::Create(
const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary, const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary,
ErrorResult& aRv); ErrorResult& aRv);
@ -129,7 +134,7 @@ static void InvalidateAllFrames(nsINode* aNode) {
nsTArray<RefPtr<nsRange>>* nsRange::sCachedRanges = nullptr; nsTArray<RefPtr<nsRange>>* nsRange::sCachedRanges = nullptr;
nsRange::~nsRange() { nsRange::~nsRange() {
NS_ASSERTION(!IsInSelection(), "deleting nsRange that is in use"); NS_ASSERTION(!IsInAnySelection(), "deleting nsRange that is in use");
// we want the side effects (releases and list removals) // we want the side effects (releases and list removals)
DoSetRange(RawRangeBoundary(), RawRangeBoundary(), nullptr); DoSetRange(RawRangeBoundary(), RawRangeBoundary(), nullptr);
@ -141,7 +146,8 @@ nsRange::nsRange(nsINode* aNode)
mNextStartRef(nullptr), mNextStartRef(nullptr),
mNextEndRef(nullptr) { mNextEndRef(nullptr) {
// printf("Size of nsRange: %zu\n", sizeof(nsRange)); // printf("Size of nsRange: %zu\n", sizeof(nsRange));
static_assert(sizeof(nsRange) <= 208,
static_assert(sizeof(nsRange) <= 216,
"nsRange size shouldn't be increased as far as possible"); "nsRange size shouldn't be increased as far as possible");
} }
@ -188,6 +194,7 @@ NS_INTERFACE_MAP_END_INHERITING(AbstractRange)
NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange) NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelections);
// We _could_ just rely on Reset() to // We _could_ just rely on Reset() to
// UnregisterClosestCommonInclusiveAncestor(), but it wouldn't know we're // UnregisterClosestCommonInclusiveAncestor(), but it wouldn't know we're
// calling it from Unlink and so would do more work than it really needs to. // calling it from Unlink and so would do more work than it really needs to.
@ -204,6 +211,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelections)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange) NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange)
@ -259,7 +267,8 @@ static void UnmarkDescendants(nsINode* aNode) {
void nsRange::RegisterClosestCommonInclusiveAncestor(nsINode* aNode) { void nsRange::RegisterClosestCommonInclusiveAncestor(nsINode* aNode) {
MOZ_ASSERT(aNode, "bad arg"); MOZ_ASSERT(aNode, "bad arg");
MOZ_DIAGNOSTIC_ASSERT(IsInSelection(), "registering range not in selection"); MOZ_DIAGNOSTIC_ASSERT(IsInAnySelection(),
"registering range not in selection");
mRegisteredClosestCommonInclusiveAncestor = aNode; mRegisteredClosestCommonInclusiveAncestor = aNode;
@ -437,7 +446,7 @@ void nsRange::CharacterDataChanged(nsIContent* aContent,
} }
bool isCommonAncestor = bool isCommonAncestor =
IsInSelection() && mStart.Container() == mEnd.Container(); IsInAnySelection() && mStart.Container() == mEnd.Container();
if (isCommonAncestor) { if (isCommonAncestor) {
UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false); UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false);
RegisterClosestCommonInclusiveAncestor(newStart.Container()); RegisterClosestCommonInclusiveAncestor(newStart.Container());
@ -489,7 +498,7 @@ void nsRange::CharacterDataChanged(nsIContent* aContent,
newEnd = {aInfo.mDetails->mNextSibling, newEndOffset}; newEnd = {aInfo.mDetails->mNextSibling, newEndOffset};
bool isCommonAncestor = bool isCommonAncestor =
IsInSelection() && mStart.Container() == mEnd.Container(); IsInAnySelection() && mStart.Container() == mEnd.Container();
if (isCommonAncestor && !newStart.Container()) { if (isCommonAncestor && !newStart.Container()) {
// The split occurs inside the range. // The split occurs inside the range.
UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false); UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false);
@ -557,7 +566,7 @@ void nsRange::ContentAppended(nsIContent* aFirstNewContent) {
nsINode* container = aFirstNewContent->GetParentNode(); nsINode* container = aFirstNewContent->GetParentNode();
MOZ_ASSERT(container); MOZ_ASSERT(container);
if (container->IsMaybeSelected() && IsInSelection()) { if (container->IsMaybeSelected() && IsInAnySelection()) {
nsINode* child = aFirstNewContent; nsINode* child = aFirstNewContent;
while (child) { while (child) {
if (!child if (!child
@ -840,19 +849,61 @@ bool nsRange::IntersectsNode(nsINode& aNode, ErrorResult& aRv) {
return false; return false;
} }
/**
* @brief Helper class that creates a local copy of `nsRange::mSelections`.
*
* This class uses the RAII principle to create a local copy of
* `nsRange::mSelections`, which is safely iterable while modifications may
* occur on the original.
* When going out of scope, the local copy is being deleted.
*/
class MOZ_RAII SelectionListLocalCopy final {
public:
explicit SelectionListLocalCopy(
mozilla::LinkedList<RefPtr<SelectionListWrapper>>& aSelectionList) {
for (const auto* elem : aSelectionList) {
mSelectionList.insertBack(new SelectionListWrapper(elem->Get()));
}
}
mozilla::LinkedList<RefPtr<SelectionListWrapper>>& Get() {
return mSelectionList;
}
~SelectionListLocalCopy() { mSelectionList.clear(); }
private:
mozilla::LinkedList<RefPtr<SelectionListWrapper>> mSelectionList;
};
void nsRange::NotifySelectionListenersAfterRangeSet() { void nsRange::NotifySelectionListenersAfterRangeSet() {
if (mSelection) { if (!mSelections.isEmpty()) {
// Our internal code should not move focus with using this instance while // Our internal code should not move focus with using this instance while
// it's calling Selection::NotifySelectionListeners() which may move focus // it's calling Selection::NotifySelectionListeners() which may move focus
// or calls selection listeners. So, let's set mCalledByJS to false here // or calls selection listeners. So, let's set mCalledByJS to false here
// since non-*JS() methods don't set it to false. // since non-*JS() methods don't set it to false.
AutoCalledByJSRestore calledByJSRestorer(*this); AutoCalledByJSRestore calledByJSRestorer(*this);
mCalledByJS = false; mCalledByJS = false;
// Be aware, this range may be modified or stop being a range for selection
// after this call. Additionally, the selection instance may have gone. // Notify all Selections. This may modify the range,
RefPtr<Selection> selection = mSelection.get(); // remove it from the selection, or the selection itself may have gone after
// the call. Also, new selections may be added.
// To ensure that listeners are notified for all *current* selections,
// create a copy of the list of selections and use that for iterating. This
// way selections can be added or removed safely during iteration.
// To save allocation cost, the copy is only created if there is more than
// one Selection present (which will barely ever be the case).
if (mSelections.getFirst() != mSelections.getLast()) {
SelectionListLocalCopy copiedSelections{mSelections};
for (const auto* selectionWrapper : copiedSelections.Get()) {
RefPtr<Selection> selection = selectionWrapper->Get();
selection->NotifySelectionListeners(calledByJSRestorer.SavedValue()); selection->NotifySelectionListeners(calledByJSRestorer.SavedValue());
} }
} else {
RefPtr<Selection> selection = mSelections.getFirst()->Get();
selection->NotifySelectionListeners(calledByJSRestorer.SavedValue());
}
}
} }
/****************************************************** /******************************************************
@ -929,7 +980,7 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
bool checkCommonAncestor = bool checkCommonAncestor =
(mStart.Container() != aStartBoundary.Container() || (mStart.Container() != aStartBoundary.Container() ||
mEnd.Container() != aEndBoundary.Container()) && mEnd.Container() != aEndBoundary.Container()) &&
IsInSelection() && !aNotInsertedYet; IsInAnySelection() && !aNotInsertedYet;
// GetClosestCommonInclusiveAncestor is unreliable while we're unlinking // GetClosestCommonInclusiveAncestor is unreliable while we're unlinking
// (could return null if our start/end have already been unlinked), so make // (could return null if our start/end have already been unlinked), so make
@ -948,7 +999,7 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
RegisterClosestCommonInclusiveAncestor(newCommonAncestor); RegisterClosestCommonInclusiveAncestor(newCommonAncestor);
} else { } else {
MOZ_DIAGNOSTIC_ASSERT(!mIsPositioned, "unexpected disconnected nodes"); MOZ_DIAGNOSTIC_ASSERT(!mIsPositioned, "unexpected disconnected nodes");
mSelection = nullptr; mSelections.clear();
MOZ_DIAGNOSTIC_ASSERT( MOZ_DIAGNOSTIC_ASSERT(
!mRegisteredClosestCommonInclusiveAncestor, !mRegisteredClosestCommonInclusiveAncestor,
"How can we have a registered common ancestor when we " "How can we have a registered common ancestor when we "
@ -970,42 +1021,48 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
// the world could be observed by a selection listener while the range was in // the world could be observed by a selection listener while the range was in
// an invalid state. So we run it off of a script runner to ensure it runs // an invalid state. So we run it off of a script runner to ensure it runs
// after the mutation observers have finished running. // after the mutation observers have finished running.
if (mSelection) { if (!mSelections.isEmpty()) {
nsContentUtils::AddScriptRunner( nsContentUtils::AddScriptRunner(
NewRunnableMethod("NotifySelectionListenersAfterRangeSet", this, NewRunnableMethod("NotifySelectionListenersAfterRangeSet", this,
&nsRange::NotifySelectionListenersAfterRangeSet)); &nsRange::NotifySelectionListenersAfterRangeSet));
} }
} }
void nsRange::RegisterSelection(Selection& aSelection) { bool nsRange::IsInSelection(const Selection& aSelection) const {
// A range can belong to at most one Selection instance. for (const auto* selectionWrapper : mSelections) {
MOZ_ASSERT(!mSelection); if (selectionWrapper->Get() == &aSelection) {
return true;
}
}
return false;
}
if (mSelection == &aSelection) { void nsRange::RegisterSelection(Selection& aSelection) {
if (IsInSelection(aSelection)) {
return; return;
} }
bool isFirstSelection = mSelections.isEmpty();
// Extra step in case our parent failed to ensure the above precondition. mSelections.insertBack(new SelectionListWrapper(&aSelection));
if (mSelection) { if (isFirstSelection && !mRegisteredClosestCommonInclusiveAncestor) {
const RefPtr<nsRange> range{this};
const RefPtr<Selection> selection{mSelection};
selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
IgnoreErrors());
}
mSelection = &aSelection;
nsINode* commonAncestor = GetClosestCommonInclusiveAncestor(); nsINode* commonAncestor = GetClosestCommonInclusiveAncestor();
MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes"); MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes");
RegisterClosestCommonInclusiveAncestor(commonAncestor); RegisterClosestCommonInclusiveAncestor(commonAncestor);
} }
}
Selection* nsRange::GetSelection() const { return mSelection; } const mozilla::LinkedList<RefPtr<mozilla::SelectionListWrapper>>&
nsRange::GetSelections() const {
return mSelections;
}
void nsRange::UnregisterSelection() { void nsRange::UnregisterSelection(Selection& aSelection) {
mSelection = nullptr; for (auto* selectionWrapper : mSelections) {
if (selectionWrapper->Get() == &aSelection) {
if (mRegisteredClosestCommonInclusiveAncestor) { selectionWrapper->remove();
break;
}
}
if (mSelections.isEmpty() && mRegisteredClosestCommonInclusiveAncestor) {
UnregisterClosestCommonInclusiveAncestor( UnregisterClosestCommonInclusiveAncestor(
mRegisteredClosestCommonInclusiveAncestor, false); mRegisteredClosestCommonInclusiveAncestor, false);
MOZ_DIAGNOSTIC_ASSERT( MOZ_DIAGNOSTIC_ASSERT(
@ -2948,7 +3005,7 @@ nsresult nsRange::GetUsedFontFaces(nsLayoutUtils::UsedFontFaceList& aResult,
} }
nsINode* nsRange::GetRegisteredClosestCommonInclusiveAncestor() { nsINode* nsRange::GetRegisteredClosestCommonInclusiveAncestor() {
MOZ_ASSERT(IsInSelection(), MOZ_ASSERT(IsInAnySelection(),
"GetRegisteredClosestCommonInclusiveAncestor only valid for range " "GetRegisteredClosestCommonInclusiveAncestor only valid for range "
"in selection"); "in selection");
MOZ_ASSERT(mRegisteredClosestCommonInclusiveAncestor); MOZ_ASSERT(mRegisteredClosestCommonInclusiveAncestor);
@ -2970,7 +3027,7 @@ nsRange::AutoInvalidateSelection::~AutoInvalidateSelection() {
// with selections, ranges, etc. But if it still is, we should check whether // with selections, ranges, etc. But if it still is, we should check whether
// we have a different common ancestor now, and if so invalidate its subtree // we have a different common ancestor now, and if so invalidate its subtree
// so it paints the selection it's in now. // so it paints the selection it's in now.
if (mRange->IsInSelection()) { if (mRange->IsInAnySelection()) {
nsINode* commonAncestor = nsINode* commonAncestor =
mRange->GetRegisteredClosestCommonInclusiveAncestor(); mRange->GetRegisteredClosestCommonInclusiveAncestor();
// XXXbz can commonAncestor really be null here? I wouldn't think so! If // XXXbz can commonAncestor really be null here? I wouldn't think so! If

Просмотреть файл

@ -20,6 +20,7 @@
#include "mozilla/ErrorResult.h" #include "mozilla/ErrorResult.h"
#include "mozilla/LinkedList.h" #include "mozilla/LinkedList.h"
#include "mozilla/RangeBoundary.h" #include "mozilla/RangeBoundary.h"
#include "mozilla/RefPtr.h"
#include "mozilla/WeakPtr.h" #include "mozilla/WeakPtr.h"
namespace mozilla { namespace mozilla {
@ -33,6 +34,26 @@ class DOMRectList;
class InspectorFontFace; class InspectorFontFace;
class Selection; class Selection;
} // namespace dom } // namespace dom
/**
* @brief Wrapper class to allow storing a |Selection| in a |LinkedList|.
*
* This helper allows an |nsRange| to store all |Selection|s associated with it
* in a |mozilla::LinkedList|.
*/
class SelectionListWrapper
: public LinkedListElement<RefPtr<SelectionListWrapper>> {
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(SelectionListWrapper)
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(SelectionListWrapper)
public:
explicit SelectionListWrapper(dom::Selection* aSelection);
/// Returns the stored |Selection|.
dom::Selection* Get() const;
private:
~SelectionListWrapper() = default;
WeakPtr<dom::Selection> mSelection;
};
} // namespace mozilla } // namespace mozilla
class nsRange final : public mozilla::dom::AbstractRange, class nsRange final : public mozilla::dom::AbstractRange,
@ -92,20 +113,26 @@ class nsRange final : public mozilla::dom::AbstractRange,
nsINode* GetRoot() const { return mRoot; } nsINode* GetRoot() const { return mRoot; }
/** /**
* Return true iff this range is part of a Selection object * Return true if this range is part of a Selection object
* and isn't detached. * and isn't detached.
*/ */
bool IsInSelection() const { return !!mSelection; } bool IsInAnySelection() const { return !mSelections.isEmpty(); }
MOZ_CAN_RUN_SCRIPT void RegisterSelection( MOZ_CAN_RUN_SCRIPT void RegisterSelection(
mozilla::dom::Selection& aSelection); mozilla::dom::Selection& aSelection);
void UnregisterSelection(); void UnregisterSelection(mozilla::dom::Selection& aSelection);
/** /**
* Returns pointer to a Selection if the range is associated with a Selection. * Returns a list of all Selections the range is associated with.
*/ */
mozilla::dom::Selection* GetSelection() const; const mozilla::LinkedList<RefPtr<mozilla::SelectionListWrapper>>&
GetSelections() const;
/**
* Return true if this range is in |aSelection|.
*/
bool IsInSelection(const mozilla::dom::Selection& aSelection) const;
/** /**
* Return true if this range was generated. * Return true if this range was generated.
@ -415,7 +442,7 @@ class nsRange final : public mozilla::dom::AbstractRange,
struct MOZ_STACK_CLASS AutoInvalidateSelection { struct MOZ_STACK_CLASS AutoInvalidateSelection {
explicit AutoInvalidateSelection(nsRange* aRange) : mRange(aRange) { explicit AutoInvalidateSelection(nsRange* aRange) : mRange(aRange) {
if (!mRange->IsInSelection() || sIsNested) { if (!mRange->IsInAnySelection() || sIsNested) {
return; return;
} }
sIsNested = true; sIsNested = true;
@ -432,16 +459,18 @@ class nsRange final : public mozilla::dom::AbstractRange,
#ifdef DEBUG #ifdef DEBUG
bool IsCleared() const { bool IsCleared() const {
return !mRoot && !mRegisteredClosestCommonInclusiveAncestor && return !mRoot && !mRegisteredClosestCommonInclusiveAncestor &&
!mSelection && !mNextStartRef && !mNextEndRef; mSelections.isEmpty() && !mNextStartRef && !mNextEndRef;
} }
#endif // #ifdef DEBUG #endif // #ifdef DEBUG
nsCOMPtr<nsINode> mRoot; nsCOMPtr<nsINode> mRoot;
// mRegisteredClosestCommonInclusiveAncestor is only non-null when the range // mRegisteredClosestCommonInclusiveAncestor is only non-null when the range
// IsInSelection(). It's kept alive via mStart/mEnd, // IsInAnySelection(). It's kept alive via mStart/mEnd,
// because we update it any time those could become disconnected from it. // because we update it any time those could become disconnected from it.
nsINode* MOZ_NON_OWNING_REF mRegisteredClosestCommonInclusiveAncestor; nsINode* MOZ_NON_OWNING_REF mRegisteredClosestCommonInclusiveAncestor;
mozilla::WeakPtr<mozilla::dom::Selection> mSelection;
// A Range can be part of multiple |Selection|s. This is a very rare use case.
mozilla::LinkedList<RefPtr<mozilla::SelectionListWrapper>> mSelections;
// These raw pointers are used to remember a child that is about // These raw pointers are used to remember a child that is about
// to be inserted between a CharacterData call and a subsequent // to be inserted between a CharacterData call and a subsequent

Просмотреть файл

@ -429,7 +429,7 @@ AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
bool changed = false; bool changed = false;
for (auto& range : mRanges) { for (auto& range : mRanges) {
MOZ_ASSERT(!range->IsInSelection(), MOZ_ASSERT(!range->IsInAnySelection(),
"Changing range in selection may cause running script"); "Changing range in selection may cause running script");
Result<bool, nsresult> result = Result<bool, nsresult> result =
WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent( WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
@ -939,7 +939,7 @@ AutoRangeArray::SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
// Correct the range. // Correct the range.
// The new end parent becomes the parent node of the text. // The new end parent becomes the parent node of the text.
MOZ_ASSERT(!range->IsInSelection()); MOZ_ASSERT(!range->IsInAnySelection());
range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>() range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
.ToRawRangeBoundary(), .ToRawRangeBoundary(),
ignoredError); ignoredError);

Просмотреть файл

@ -4298,7 +4298,7 @@ WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
const HTMLEditor& aHTMLEditor, nsRange& aRange, const HTMLEditor& aHTMLEditor, nsRange& aRange,
const Element* aEditingHost) { const Element* aEditingHost) {
MOZ_ASSERT(aRange.IsPositioned()); MOZ_ASSERT(aRange.IsPositioned());
MOZ_ASSERT(!aRange.IsInSelection(), MOZ_ASSERT(!aRange.IsInAnySelection(),
"Changing range in selection may cause running script"); "Changing range in selection may cause running script");
if (NS_WARN_IF(!aRange.GetStartContainer()) || if (NS_WARN_IF(!aRange.GetStartContainer()) ||

Просмотреть файл

@ -1834,11 +1834,12 @@ void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf( const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{}); nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex && if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
aOldRangesForSomeWords[indexOfOldRangeToKeep]->GetSelection() == aOldRangesForSomeWords[indexOfOldRangeToKeep]->IsInSelection(
&aSpellCheckerSelection /** TODO: warn in case the old range doesn't aSpellCheckerSelection)) {
/** TODO: warn in case the old range doesn't
belong to the selection. This is not critical, belong to the selection. This is not critical,
because other code can always remove them because other code can always remove them
before the actual spellchecking happens. */) { before the actual spellchecking happens. */
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose, MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
("%s: reusing old range.", __FUNCTION__)); ("%s: reusing old range.", __FUNCTION__));

Просмотреть файл

@ -305,7 +305,7 @@ struct MOZ_RAII AutoPrepareFocusRange {
while (i--) { while (i--) {
nsRange* range = ranges[i].mRange; nsRange* range = ranges[i].mRange;
if (range->IsGenerated()) { if (range->IsGenerated()) {
range->UnregisterSelection(); range->UnregisterSelection(aSelection);
aSelection.SelectFrames(presContext, range, false); aSelection.SelectFrames(presContext, range, false);
ranges.RemoveElementAt(i); ranges.RemoveElementAt(i);
} }

Просмотреть файл

@ -0,0 +1 @@
prefs: [dom.customHighlightAPI.enabled:true]

Просмотреть файл

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
</head>
<body>
<span>One two</span>
<script>
promise_test(async function (t) {
await new Promise(resolve => {
window.onload = resolve;
})
const range = document.createRange();
range.setStart(document.body, 0);
range.setEnd(document.body, 1);
const highlight = new Highlight(range);
CSS.highlights.set("foo", highlight);
document.getSelection().addRange(range);
const highlightRange = highlight.entries().next().value[0];
const selectionRange = document.getSelection().getRangeAt(0);
assert_equals(
highlightRange,
selectionRange,
"The same range must be present in the highlight and the Selection."
);
}, "Range is shared between a custom highlight and the document's Selection.");
</script>
</body>
</html>