diff options
Diffstat (limited to 'editor/libeditor/SelectionState.cpp')
-rw-r--r-- | editor/libeditor/SelectionState.cpp | 621 |
1 files changed, 621 insertions, 0 deletions
diff --git a/editor/libeditor/SelectionState.cpp b/editor/libeditor/SelectionState.cpp new file mode 100644 index 0000000000..063b941c67 --- /dev/null +++ b/editor/libeditor/SelectionState.cpp @@ -0,0 +1,621 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "SelectionState.h" + +#include "AutoRangeArray.h" // for AutoRangeArray +#include "EditorUtils.h" // for EditorUtils, AutoRangeArray +#include "ErrorList.h" +#include "JoinSplitNodeDirection.h" // for JoinNodesDirection, SplitNodeDirection + +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. +#include "mozilla/IntegerRange.h" // for IntegerRange +#include "mozilla/Likely.h" // For MOZ_LIKELY and MOZ_UNLIKELY +#include "mozilla/RangeUtils.h" // for RangeUtils +#include "mozilla/dom/RangeBinding.h" +#include "mozilla/dom/Selection.h" // for Selection +#include "nsAString.h" // for nsAString::Length +#include "nsCycleCollectionParticipant.h" +#include "nsDebug.h" // for NS_WARNING, etc. +#include "nsError.h" // for NS_OK, etc. +#include "nsIContent.h" // for nsIContent +#include "nsISupportsImpl.h" // for nsRange::Release +#include "nsRange.h" // for nsRange + +namespace mozilla { + +using namespace dom; + +/***************************************************************************** + * mozilla::RangeItem + *****************************************************************************/ + +nsINode* RangeItem::GetRoot() const { + if (MOZ_UNLIKELY(!IsPositioned())) { + return nullptr; + } + nsINode* rootNode = RangeUtils::ComputeRootNode(mStartContainer); + if (mStartContainer == mEndContainer) { + return rootNode; + } + return MOZ_LIKELY(rootNode == RangeUtils::ComputeRootNode(mEndContainer)) + ? rootNode + : nullptr; +} + +/****************************************************************************** + * mozilla::SelectionState + * + * Class for recording selection info. Stores selection as collection of + * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store + * ranges since dom gravity will possibly change the ranges. + ******************************************************************************/ + +template nsresult RangeUpdater::SelAdjCreateNode(const EditorDOMPoint& aPoint); +template nsresult RangeUpdater::SelAdjCreateNode( + const EditorRawDOMPoint& aPoint); +template nsresult RangeUpdater::SelAdjInsertNode(const EditorDOMPoint& aPoint); +template nsresult RangeUpdater::SelAdjInsertNode( + const EditorRawDOMPoint& aPoint); + +SelectionState::SelectionState(const AutoRangeArray& aRanges) + : mDirection(aRanges.GetDirection()) { + mArray.SetCapacity(aRanges.Ranges().Length()); + for (const OwningNonNull<nsRange>& range : aRanges.Ranges()) { + RefPtr<RangeItem> rangeItem = new RangeItem(); + rangeItem->StoreRange(range); + mArray.AppendElement(std::move(rangeItem)); + } +} + +void SelectionState::SaveSelection(Selection& aSelection) { + // if we need more items in the array, new them + if (mArray.Length() < aSelection.RangeCount()) { + for (uint32_t i = mArray.Length(); i < aSelection.RangeCount(); i++) { + mArray.AppendElement(); + mArray[i] = new RangeItem(); + } + } else if (mArray.Length() > aSelection.RangeCount()) { + // else if we have too many, delete them + mArray.TruncateLength(aSelection.RangeCount()); + } + + // now store the selection ranges + const uint32_t rangeCount = aSelection.RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(aSelection.RangeCount() == rangeCount); + const nsRange* range = aSelection.GetRangeAt(i); + MOZ_ASSERT(range); + if (MOZ_UNLIKELY(NS_WARN_IF(!range))) { + continue; + } + mArray[i]->StoreRange(*range); + } + + mDirection = aSelection.GetDirection(); +} + +nsresult SelectionState::RestoreSelection(Selection& aSelection) { + // clear out selection + IgnoredErrorResult ignoredError; + aSelection.RemoveAllRanges(ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Selection::RemoveAllRanges() failed, but ignored"); + + aSelection.SetDirection(mDirection); + + ErrorResult error; + const CopyableAutoTArray<RefPtr<RangeItem>, 10> rangeItems(mArray); + for (const RefPtr<RangeItem>& rangeItem : rangeItems) { + RefPtr<nsRange> range = rangeItem->GetRange(); + if (!range) { + NS_WARNING("RangeItem::GetRange() failed"); + return NS_ERROR_FAILURE; + } + aSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, error); + if (error.Failed()) { + NS_WARNING( + "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed"); + return error.StealNSResult(); + } + } + return NS_OK; +} + +void SelectionState::ApplyTo(AutoRangeArray& aRanges) { + aRanges.RemoveAllRanges(); + aRanges.SetDirection(mDirection); + for (const RefPtr<RangeItem>& rangeItem : mArray) { + RefPtr<nsRange> range = rangeItem->GetRange(); + if (MOZ_UNLIKELY(!range)) { + continue; + } + aRanges.Ranges().AppendElement(std::move(range)); + } +} + +bool SelectionState::Equals(const SelectionState& aOther) const { + if (mArray.Length() != aOther.mArray.Length()) { + return false; + } + if (mArray.IsEmpty()) { + return false; // XXX Why? + } + if (mDirection != aOther.mDirection) { + return false; + } + + for (uint32_t i : IntegerRange(mArray.Length())) { + if (NS_WARN_IF(!mArray[i]) || NS_WARN_IF(!aOther.mArray[i]) || + !mArray[i]->Equals(*aOther.mArray[i])) { + return false; + } + } + // if we got here, they are equal + return true; +} + +/****************************************************************************** + * mozilla::RangeUpdater + * + * Class for updating nsRanges in response to editor actions. + ******************************************************************************/ + +RangeUpdater::RangeUpdater() : mLocked(false) {} + +void RangeUpdater::RegisterRangeItem(RangeItem& aRangeItem) { + if (mArray.Contains(&aRangeItem)) { + NS_ERROR("tried to register an already registered range"); + return; // don't register it again. It would get doubly adjusted. + } + mArray.AppendElement(&aRangeItem); +} + +void RangeUpdater::DropRangeItem(RangeItem& aRangeItem) { + NS_WARNING_ASSERTION( + mArray.Contains(&aRangeItem), + "aRangeItem is not in the range, but tried to removed from it"); + mArray.RemoveElement(&aRangeItem); +} + +void RangeUpdater::RegisterSelectionState(SelectionState& aSelectionState) { + for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) { + if (NS_WARN_IF(!rangeItem)) { + continue; + } + RegisterRangeItem(*rangeItem); + } +} + +void RangeUpdater::DropSelectionState(SelectionState& aSelectionState) { + for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) { + if (NS_WARN_IF(!rangeItem)) { + continue; + } + DropRangeItem(*rangeItem); + } +} + +// gravity methods: + +template <typename PT, typename CT> +nsresult RangeUpdater::SelAdjCreateNode( + const EditorDOMPointBase<PT, CT>& aPoint) { + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + if (mArray.IsEmpty()) { + return NS_OK; + } + + if (NS_WARN_IF(!aPoint.IsSetAndValid())) { + return NS_ERROR_INVALID_ARG; + } + + for (RefPtr<RangeItem>& rangeItem : mArray) { + if (NS_WARN_IF(!rangeItem)) { + return NS_ERROR_FAILURE; + } + if (rangeItem->mStartContainer == aPoint.GetContainer() && + rangeItem->mStartOffset > aPoint.Offset()) { + rangeItem->mStartOffset++; + } + if (rangeItem->mEndContainer == aPoint.GetContainer() && + rangeItem->mEndOffset > aPoint.Offset()) { + rangeItem->mEndOffset++; + } + } + return NS_OK; +} + +template <typename PT, typename CT> +nsresult RangeUpdater::SelAdjInsertNode( + const EditorDOMPointBase<PT, CT>& aPoint) { + nsresult rv = SelAdjCreateNode(aPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "RangeUpdater::SelAdjCreateNode() failed"); + return rv; +} + +void RangeUpdater::SelAdjDeleteNode(nsINode& aNodeToDelete) { + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return; + } + + if (mArray.IsEmpty()) { + return; + } + + EditorRawDOMPoint atNodeToDelete(&aNodeToDelete); + NS_ASSERTION(atNodeToDelete.IsSetAndValid(), + "aNodeToDelete must be an orphan node or this is called " + "during mutation"); + // check for range endpoints that are after aNodeToDelete and in the same + // parent + for (RefPtr<RangeItem>& rangeItem : mArray) { + MOZ_ASSERT(rangeItem); + + if (rangeItem->mStartContainer == atNodeToDelete.GetContainer() && + rangeItem->mStartOffset > atNodeToDelete.Offset()) { + rangeItem->mStartOffset--; + } + if (rangeItem->mEndContainer == atNodeToDelete.GetContainer() && + rangeItem->mEndOffset > atNodeToDelete.Offset()) { + rangeItem->mEndOffset--; + } + + // check for range endpoints that are in aNodeToDelete + if (rangeItem->mStartContainer == &aNodeToDelete) { + rangeItem->mStartContainer = atNodeToDelete.GetContainer(); + rangeItem->mStartOffset = atNodeToDelete.Offset(); + } + if (rangeItem->mEndContainer == &aNodeToDelete) { + rangeItem->mEndContainer = atNodeToDelete.GetContainer(); + rangeItem->mEndOffset = atNodeToDelete.Offset(); + } + + // check for range endpoints that are in descendants of aNodeToDelete + bool updateEndBoundaryToo = false; + if (EditorUtils::IsDescendantOf(*rangeItem->mStartContainer, + aNodeToDelete)) { + updateEndBoundaryToo = + rangeItem->mStartContainer == rangeItem->mEndContainer; + rangeItem->mStartContainer = atNodeToDelete.GetContainer(); + rangeItem->mStartOffset = atNodeToDelete.Offset(); + } + + // avoid having to call IsDescendantOf() for common case of range startnode + // == range endnode. + if (updateEndBoundaryToo || + EditorUtils::IsDescendantOf(*rangeItem->mEndContainer, aNodeToDelete)) { + rangeItem->mEndContainer = atNodeToDelete.GetContainer(); + rangeItem->mEndOffset = atNodeToDelete.Offset(); + } + } +} + +nsresult RangeUpdater::SelAdjSplitNode(nsIContent& aOriginalContent, + uint32_t aSplitOffset, + nsIContent& aNewContent, + SplitNodeDirection aSplitNodeDirection) { + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + + if (mArray.IsEmpty()) { + return NS_OK; + } + + EditorRawDOMPoint atNewNode(&aNewContent); + if (NS_WARN_IF(!atNewNode.IsSetAndValid())) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + + auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer, + uint32_t& aOffset) -> void { + if (aContainer == atNewNode.GetContainer()) { + if (aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne) { + // When we create a left node, we insert it before the right node. + // In this case, + // - `{}<right/>` should become `{}<left/><right/>` (0 -> 0) + // - `<right/>{}` should become `<left/><right/>{}` (1 -> 2) + // - `{<right/>}` should become `{<left/><right/>}` (0 -> 0, 1 -> 2} + // Therefore, we need to increate the offset only when the offset is + // larger than the offset at the left node. + if (aOffset > atNewNode.Offset()) { + aOffset++; + } + } else { + // When we create a right node, we insert it after the left node. + // In this case, + // - `{}<left/>` should become `{}<left/><right/>` (0 -> 0) + // - `<left/>{}` should become `<left/><right/>{}` (1 -> 2) + // - `{<left/>}` should become `{<left/><right/>}` (0 -> 0, 1 -> 2} + // Therefore, we need to increate the offset only when the offset equals + // or is larger than the offset at the right node. + if (aOffset >= atNewNode.Offset()) { + aOffset++; + } + } + } + // If point is in the range which are moved from aOriginalContent to + // aNewContent, we need to change its container to aNewContent and may need + // to adjust the offset. If point is in the range which are not moved from + // aOriginalContent, we may need to adjust the offset. + if (aContainer != &aOriginalContent) { + return; + } + if (aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne) { + if (aOffset > aSplitOffset) { + aOffset -= aSplitOffset; + } else { + aContainer = &aNewContent; + } + } else if (aOffset >= aSplitOffset) { + aContainer = &aNewContent; + aOffset -= aSplitOffset; + } + }; + + for (RefPtr<RangeItem>& rangeItem : mArray) { + if (NS_WARN_IF(!rangeItem)) { + return NS_ERROR_FAILURE; + } + AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset); + AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset); + } + return NS_OK; +} + +nsresult RangeUpdater::SelAdjJoinNodes( + const EditorRawDOMPoint& aStartOfRightContent, + const nsIContent& aRemovedContent, + const EditorDOMPoint& aOldPointAtRightContent, + JoinNodesDirection aJoinNodesDirection) { + MOZ_ASSERT(aStartOfRightContent.IsSetAndValid()); + MOZ_ASSERT(aOldPointAtRightContent.IsSet()); // Invalid point in most cases + MOZ_ASSERT(aOldPointAtRightContent.HasOffset()); + + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return NS_OK; + } + + if (mArray.IsEmpty()) { + return NS_OK; + } + + auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer, + uint32_t& aOffset) -> void { + // FYI: Typically, containers of aOldPointAtRightContent and + // aStartOfRightContent are same. They are different when one of the + // node was moved to somewhere and they are joined by undoing splitting + // a node. + if (aContainer == &aRemovedContent) { + // If the point is in the removed content, move the point to the new + // point in the joined node. If left node content is moved into + // right node, the offset should be same. Otherwise, we need to advance + // the offset to length of the removed content. + aContainer = aStartOfRightContent.GetContainer(); + if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) { + aOffset += aStartOfRightContent.Offset(); + } + } + // TODO: If aOldPointAtRightContent.GetContainer() was in aRemovedContent, + // we fail to adjust container and offset here because we need to + // make point to where aRemoveContent was. However, collecting all + // ancestors of the right content may be expensive. What's the best + // approach to fix this? + else if (aContainer == aOldPointAtRightContent.GetContainer()) { + // If the point is in common parent of joined content nodes and it + // pointed after the right content node, decrease the offset. + if (aOffset > aOldPointAtRightContent.Offset()) { + aOffset--; + } + // If it pointed the right content node, adjust it to point ex-first + // content of the right node. + else if (aOffset == aOldPointAtRightContent.Offset()) { + aContainer = aStartOfRightContent.GetContainer(); + aOffset = aStartOfRightContent.Offset(); + } + } else if (aContainer == aStartOfRightContent.GetContainer()) { + // If the point is in joined node, and removed content is moved to + // start of the joined node, we need to adjust the offset. + if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) { + aOffset += aStartOfRightContent.Offset(); + } + } + }; + + for (RefPtr<RangeItem>& rangeItem : mArray) { + if (NS_WARN_IF(!rangeItem)) { + return NS_ERROR_FAILURE; + } + AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset); + AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset); + } + + return NS_OK; +} + +void RangeUpdater::SelAdjReplaceText(const Text& aTextNode, uint32_t aOffset, + uint32_t aReplacedLength, + uint32_t aInsertedLength) { + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return; + } + + // First, adjust selection for insertion because when offset is in the + // replaced range, it's adjusted to aOffset and never modified by the + // insertion if we adjust selection for deletion first. + SelAdjInsertText(aTextNode, aOffset, aInsertedLength); + + // Then, adjust selection for deletion. + SelAdjDeleteText(aTextNode, aOffset, aReplacedLength); +} + +void RangeUpdater::SelAdjInsertText(const Text& aTextNode, uint32_t aOffset, + uint32_t aInsertedLength) { + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return; + } + + for (RefPtr<RangeItem>& rangeItem : mArray) { + MOZ_ASSERT(rangeItem); + + if (rangeItem->mStartContainer == &aTextNode && + rangeItem->mStartOffset > aOffset) { + rangeItem->mStartOffset += aInsertedLength; + } + if (rangeItem->mEndContainer == &aTextNode && + rangeItem->mEndOffset > aOffset) { + rangeItem->mEndOffset += aInsertedLength; + } + } +} + +void RangeUpdater::SelAdjDeleteText(const Text& aTextNode, uint32_t aOffset, + uint32_t aDeletedLength) { + if (mLocked) { + // lock set by Will/DidReplaceParent, etc... + return; + } + + for (RefPtr<RangeItem>& rangeItem : mArray) { + MOZ_ASSERT(rangeItem); + + if (rangeItem->mStartContainer == &aTextNode && + rangeItem->mStartOffset > aOffset) { + if (rangeItem->mStartOffset >= aDeletedLength) { + rangeItem->mStartOffset -= aDeletedLength; + } else { + rangeItem->mStartOffset = 0; + } + } + if (rangeItem->mEndContainer == &aTextNode && + rangeItem->mEndOffset > aOffset) { + if (rangeItem->mEndOffset >= aDeletedLength) { + rangeItem->mEndOffset -= aDeletedLength; + } else { + rangeItem->mEndOffset = 0; + } + } + } +} + +void RangeUpdater::DidReplaceContainer(const Element& aRemovedElement, + Element& aInsertedElement) { + if (NS_WARN_IF(!mLocked)) { + return; + } + mLocked = false; + + for (RefPtr<RangeItem>& rangeItem : mArray) { + if (NS_WARN_IF(!rangeItem)) { + return; + } + + if (rangeItem->mStartContainer == &aRemovedElement) { + rangeItem->mStartContainer = &aInsertedElement; + } + if (rangeItem->mEndContainer == &aRemovedElement) { + rangeItem->mEndContainer = &aInsertedElement; + } + } +} + +void RangeUpdater::DidRemoveContainer(const Element& aRemovedElement, + nsINode& aRemovedElementContainerNode, + uint32_t aOldOffsetOfRemovedElement, + uint32_t aOldChildCountOfRemovedElement) { + if (NS_WARN_IF(!mLocked)) { + return; + } + mLocked = false; + + for (RefPtr<RangeItem>& rangeItem : mArray) { + if (NS_WARN_IF(!rangeItem)) { + return; + } + + if (rangeItem->mStartContainer == &aRemovedElement) { + rangeItem->mStartContainer = &aRemovedElementContainerNode; + rangeItem->mStartOffset += aOldOffsetOfRemovedElement; + } else if (rangeItem->mStartContainer == &aRemovedElementContainerNode && + rangeItem->mStartOffset > aOldOffsetOfRemovedElement) { + rangeItem->mStartOffset += aOldChildCountOfRemovedElement - 1; + } + + if (rangeItem->mEndContainer == &aRemovedElement) { + rangeItem->mEndContainer = &aRemovedElementContainerNode; + rangeItem->mEndOffset += aOldOffsetOfRemovedElement; + } else if (rangeItem->mEndContainer == &aRemovedElementContainerNode && + rangeItem->mEndOffset > aOldOffsetOfRemovedElement) { + rangeItem->mEndOffset += aOldChildCountOfRemovedElement - 1; + } + } +} + +void RangeUpdater::DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset, + const nsINode& aNewParent, uint32_t aNewOffset) { + if (mLocked) { + // Do nothing if moving nodes is occurred while changing the container. + return; + } + auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aNode, uint32_t& aOffset) { + if (aNode == &aOldParent) { + // If previously pointed the moved content, it should keep pointing it. + if (aOffset == aOldOffset) { + aNode = const_cast<nsINode*>(&aNewParent); + aOffset = aNewOffset; + } else if (aOffset > aOldOffset) { + aOffset--; + } + return; + } + if (aNode == &aNewParent) { + if (aOffset > aNewOffset) { + aOffset++; + } + } + }; + for (RefPtr<RangeItem>& rangeItem : mArray) { + if (NS_WARN_IF(!rangeItem)) { + return; + } + + AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset); + AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset); + } +} + +/****************************************************************************** + * mozilla::RangeItem + * + * Helper struct for SelectionState. This stores range endpoints. + ******************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION(RangeItem, mStartContainer, mEndContainer) + +void RangeItem::StoreRange(const nsRange& aRange) { + mStartContainer = aRange.GetStartContainer(); + mStartOffset = aRange.StartOffset(); + mEndContainer = aRange.GetEndContainer(); + mEndOffset = aRange.EndOffset(); +} + +already_AddRefed<nsRange> RangeItem::GetRange() const { + RefPtr<nsRange> range = nsRange::Create( + mStartContainer, mStartOffset, mEndContainer, mEndOffset, IgnoreErrors()); + NS_WARNING_ASSERTION(range, "nsRange::Create() failed"); + return range.forget(); +} + +} // namespace mozilla |