editor/libeditor/SelectionState.h
author criss <ccozmuta@mozilla.com>
Sat, 16 Oct 2021 12:46:38 +0300
changeset 596077 e4581e1d6e4b7bc1e647350c0fb67fd2b57e90c9
parent 584674 71670cedaf26214f3bcb8562a445cf1ef24f4db6
permissions -rw-r--r--
Backed out changeset ce929d9e000a (bug 1732674) for causing nsLineIterator::FindLineContaining crashes (bug 1733047) . a=aryx

/* -*- 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/. */

#ifndef mozilla_SelectionState_h
#define mozilla_SelectionState_h

#include "mozilla/EditorDOMPoint.h"
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "nsCOMPtr.h"
#include "nsDirection.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsTArray.h"
#include "nscore.h"

class nsCycleCollectionTraversalCallback;
class nsRange;
namespace mozilla {
class RangeUpdater;
namespace dom {
class Element;
class Selection;
class Text;
}  // namespace dom

/**
 * A helper struct for saving/setting ranges.
 */
struct RangeItem final {
  RangeItem() : mStartOffset(0), mEndOffset(0) {}

 private:
  // Private destructor, to discourage deletion outside of Release():
  ~RangeItem() = default;

 public:
  void StoreRange(const nsRange& aRange);
  void StoreRange(const EditorRawDOMPoint& aStartPoint,
                  const EditorRawDOMPoint& aEndPoint) {
    MOZ_ASSERT(aStartPoint.IsSet());
    MOZ_ASSERT(aEndPoint.IsSet());
    mStartContainer = aStartPoint.GetContainer();
    mStartOffset = aStartPoint.Offset();
    mEndContainer = aEndPoint.GetContainer();
    mEndOffset = aEndPoint.Offset();
  }
  void Clear() {
    mStartContainer = mEndContainer = nullptr;
    mStartOffset = mEndOffset = 0;
  }
  already_AddRefed<nsRange> GetRange();
  bool IsCollapsed() const {
    return mStartContainer == mEndContainer && mStartOffset == mEndOffset;
  }
  bool IsSet() const { return mStartContainer && mEndContainer; }
  EditorDOMPoint StartPoint() const {
    return EditorDOMPoint(mStartContainer, mStartOffset);
  }
  EditorDOMPoint EndPoint() const {
    return EditorDOMPoint(mEndContainer, mEndOffset);
  }
  EditorRawDOMPoint StartRawPoint() const {
    return EditorRawDOMPoint(mStartContainer, mStartOffset);
  }
  EditorRawDOMPoint EndRawPoint() const {
    return EditorRawDOMPoint(mEndContainer, mEndOffset);
  }

  NS_INLINE_DECL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
  NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)

  nsCOMPtr<nsINode> mStartContainer;
  nsCOMPtr<nsINode> mEndContainer;
  uint32_t mStartOffset;
  uint32_t mEndOffset;
};

/**
 * 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.
 */

class SelectionState final {
 public:
  SelectionState();
  ~SelectionState() { Clear(); }

  void SaveSelection(dom::Selection& aSelection);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
  RestoreSelection(dom::Selection& aSelection);
  bool IsCollapsed() const;
  bool Equals(SelectionState& aOther) const;
  void Clear();
  bool IsEmpty() const;

 private:
  CopyableAutoTArray<RefPtr<RangeItem>, 1> mArray;
  nsDirection mDirection;

  friend class RangeUpdater;
  friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
                                          SelectionState&, const char*,
                                          uint32_t);
  friend void ImplCycleCollectionUnlink(SelectionState&);
};

inline void ImplCycleCollectionTraverse(
    nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField,
    const char* aName, uint32_t aFlags = 0) {
  ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
}

inline void ImplCycleCollectionUnlink(SelectionState& aField) {
  ImplCycleCollectionUnlink(aField.mArray);
}

class MOZ_STACK_CLASS RangeUpdater final {
 public:
  RangeUpdater();

  void RegisterRangeItem(RangeItem& aRangeItem);
  void DropRangeItem(RangeItem& aRangeItem);
  void RegisterSelectionState(SelectionState& aSelectionState);
  void DropSelectionState(SelectionState& aSelectionState);

  // editor selection gravity routines.  Note that we can't always depend on
  // DOM Range gravity to do what we want to the "real" selection.  For
  // instance, if you move a node, that corresponds to deleting it and
  // reinserting it. DOM Range gravity will promote the selection out of the
  // node on deletion, which is not what you want if you know you are
  // reinserting it.
  template <typename PT, typename CT>
  nsresult SelAdjCreateNode(const EditorDOMPointBase<PT, CT>& aPoint);
  template <typename PT, typename CT>
  nsresult SelAdjInsertNode(const EditorDOMPointBase<PT, CT>& aPoint);
  void SelAdjDeleteNode(nsINode& aNode);
  nsresult SelAdjSplitNode(nsIContent& aRightNode, nsIContent& aNewLeftNode);
  nsresult SelAdjJoinNodes(nsINode& aLeftNode, nsINode& aRightNode,
                           nsINode& aParent, uint32_t aOffset,
                           uint32_t aOldLeftNodeLength);
  void SelAdjInsertText(const dom::Text& aTextNode, uint32_t aOffset,
                        uint32_t aInsertedLength);
  void SelAdjDeleteText(const dom::Text& aTextNode, uint32_t aOffset,
                        uint32_t aDeletedLength);
  void SelAdjReplaceText(const dom::Text& aTextNode, uint32_t aOffset,
                         uint32_t aReplacedLength, uint32_t aInsertedLength);
  // the following gravity routines need will/did sandwiches, because the other
  // gravity routines will be called inside of these sandwiches, but should be
  // ignored.
  void WillReplaceContainer() {
    // XXX Isn't this possible with mutation event listener?
    NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
    mLocked = true;
  }
  void DidReplaceContainer(const dom::Element& aRemovedElement,
                           dom::Element& aInsertedElement);
  void WillRemoveContainer() {
    // XXX Isn't this possible with mutation event listener?
    NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
    mLocked = true;
  }
  void DidRemoveContainer(const dom::Element& aRemovedElement,
                          nsINode& aRemovedElementContainerNode,
                          uint32_t aOldOffsetOfRemovedElement,
                          uint32_t aOldChildCountOfRemovedElement);
  void WillInsertContainer() {
    // XXX Isn't this possible with mutation event listener?
    NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
    mLocked = true;
  }
  void DidInsertContainer() {
    NS_WARNING_ASSERTION(mLocked, "Not locked");
    mLocked = false;
  }
  void WillMoveNode() { mLocked = true; }
  void DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
                   const nsINode& aNewParent, uint32_t aNewOffset);

 private:
  // TODO: A lot of loop in these methods check whether each item `nullptr` or
  //       not. We should make it not nullable later.
  nsTArray<RefPtr<RangeItem>> mArray;
  bool mLocked;
};

/**
 * Helper class for using SelectionState.  Stack based class for doing
 * preservation of dom points across editor actions.
 */

class MOZ_STACK_CLASS AutoTrackDOMPoint final {
 public:
  AutoTrackDOMPoint() = delete;
  AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr<nsINode>* aNode,
                    uint32_t* aOffset)
      : mRangeUpdater(aRangeUpdater),
        mNode(aNode),
        mOffset(aOffset),
        mPoint(nullptr),
        mRangeItem(do_AddRef(new RangeItem())) {
    mRangeItem->mStartContainer = *mNode;
    mRangeItem->mEndContainer = *mNode;
    mRangeItem->mStartOffset = *mOffset;
    mRangeItem->mEndOffset = *mOffset;
    mRangeUpdater.RegisterRangeItem(mRangeItem);
  }

  AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint)
      : mRangeUpdater(aRangeUpdater),
        mNode(nullptr),
        mOffset(nullptr),
        mPoint(aPoint),
        mRangeItem(do_AddRef(new RangeItem())) {
    mRangeItem->mStartContainer = mPoint->GetContainer();
    mRangeItem->mEndContainer = mPoint->GetContainer();
    mRangeItem->mStartOffset = mPoint->Offset();
    mRangeItem->mEndOffset = mPoint->Offset();
    mRangeUpdater.RegisterRangeItem(mRangeItem);
  }

  ~AutoTrackDOMPoint() {
    mRangeUpdater.DropRangeItem(mRangeItem);
    if (mPoint) {
      // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
      // and the number of times may be too many.  (E.g., 1533913.html hits
      // over 700 times!)  We should just put warning instead.
      if (NS_WARN_IF(!mRangeItem->mStartContainer)) {
        mPoint->Clear();
        return;
      }
      if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
                     mRangeItem->mStartOffset)) {
        mPoint->SetToEndOf(mRangeItem->mStartContainer);
        return;
      }
      mPoint->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
      return;
    }
    *mNode = mRangeItem->mStartContainer;
    *mOffset = mRangeItem->mStartOffset;
  }

 private:
  RangeUpdater& mRangeUpdater;
  // Allow tracking nsINode until nsNode is gone
  nsCOMPtr<nsINode>* mNode;
  uint32_t* mOffset;
  EditorDOMPoint* mPoint;
  OwningNonNull<RangeItem> mRangeItem;
};

class MOZ_STACK_CLASS AutoTrackDOMRange final {
 public:
  AutoTrackDOMRange() = delete;
  AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMPoint* aStartPoint,
                    EditorDOMPoint* aEndPoint)
      : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
    mStartPointTracker.emplace(aRangeUpdater, aStartPoint);
    mEndPointTracker.emplace(aRangeUpdater, aEndPoint);
  }
  AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMRange* aRange)
      : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
    mStartPointTracker.emplace(
        aRangeUpdater, const_cast<EditorDOMPoint*>(&aRange->StartRef()));
    mEndPointTracker.emplace(aRangeUpdater,
                             const_cast<EditorDOMPoint*>(&aRange->EndRef()));
  }
  AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
      : mStartPoint((*aRange)->StartRef()),
        mEndPoint((*aRange)->EndRef()),
        mRangeRefPtr(aRange),
        mRangeOwningNonNull(nullptr) {
    mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
    mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
  }
  AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
      : mStartPoint((*aRange)->StartRef()),
        mEndPoint((*aRange)->EndRef()),
        mRangeRefPtr(nullptr),
        mRangeOwningNonNull(aRange) {
    mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
    mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
  }
  ~AutoTrackDOMRange() {
    if (!mRangeRefPtr && !mRangeOwningNonNull) {
      // The destructor of the trackers will update automatically.
      return;
    }
    // Otherwise, destroy them now.
    mStartPointTracker.reset();
    mEndPointTracker.reset();
    if (mRangeRefPtr) {
      (*mRangeRefPtr)
          ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
                           mEndPoint.ToRawRangeBoundary());
      return;
    }
    if (mRangeOwningNonNull) {
      (*mRangeOwningNonNull)
          ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
                           mEndPoint.ToRawRangeBoundary());
      return;
    }
  }

 private:
  Maybe<AutoTrackDOMPoint> mStartPointTracker;
  Maybe<AutoTrackDOMPoint> mEndPointTracker;
  EditorDOMPoint mStartPoint;
  EditorDOMPoint mEndPoint;
  RefPtr<nsRange>* mRangeRefPtr;
  OwningNonNull<nsRange>* mRangeOwningNonNull;
};

/**
 * Another helper class for SelectionState.  Stack based class for doing
 * Will/DidReplaceContainer()
 */

class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final {
 public:
  AutoReplaceContainerSelNotify() = delete;
  // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
  //      for the members.
  MOZ_CAN_RUN_SCRIPT
  AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
                                dom::Element& aOriginalElement,
                                dom::Element& aNewElement)
      : mRangeUpdater(aRangeUpdater),
        mOriginalElement(aOriginalElement),
        mNewElement(aNewElement) {
    mRangeUpdater.WillReplaceContainer();
  }

  ~AutoReplaceContainerSelNotify() {
    mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
  }

 private:
  RangeUpdater& mRangeUpdater;
  dom::Element& mOriginalElement;
  dom::Element& mNewElement;
};

/**
 * Another helper class for SelectionState.  Stack based class for doing
 * Will/DidRemoveContainer()
 */

class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final {
 public:
  AutoRemoveContainerSelNotify() = delete;
  AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
                               const EditorDOMPoint& aAtRemovingElement)
      : mRangeUpdater(aRangeUpdater),
        mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()),
        mParentNode(*aAtRemovingElement.GetContainer()),
        mOffsetInParent(aAtRemovingElement.Offset()),
        mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) {
    MOZ_ASSERT(aAtRemovingElement.IsSet());
    mRangeUpdater.WillRemoveContainer();
  }

  ~AutoRemoveContainerSelNotify() {
    mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode,
                                     mOffsetInParent,
                                     mChildCountOfRemovingElement);
  }

 private:
  RangeUpdater& mRangeUpdater;
  OwningNonNull<dom::Element> mRemovingElement;
  OwningNonNull<nsINode> mParentNode;
  uint32_t mOffsetInParent;
  uint32_t mChildCountOfRemovingElement;
};

/**
 * Another helper class for SelectionState.  Stack based class for doing
 * Will/DidInsertContainer()
 * XXX The lock state isn't useful if the edit action is triggered from
 *     a mutation event listener so that looks like that we can remove
 *     this class.
 */

class MOZ_STACK_CLASS AutoInsertContainerSelNotify final {
 private:
  RangeUpdater& mRangeUpdater;

 public:
  AutoInsertContainerSelNotify() = delete;
  explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
      : mRangeUpdater(aRangeUpdater) {
    mRangeUpdater.WillInsertContainer();
  }

  ~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); }
};

/**
 * Another helper class for SelectionState.  Stack based class for doing
 * Will/DidMoveNode()
 */

class MOZ_STACK_CLASS AutoMoveNodeSelNotify final {
 public:
  AutoMoveNodeSelNotify() = delete;
  AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
                        const EditorDOMPoint& aOldPoint,
                        const EditorDOMPoint& aNewPoint)
      : mRangeUpdater(aRangeUpdater),
        mOldParent(*aOldPoint.GetContainer()),
        mNewParent(*aNewPoint.GetContainer()),
        mOldOffset(aOldPoint.Offset()),
        mNewOffset(aNewPoint.Offset()) {
    MOZ_ASSERT(aOldPoint.IsSet());
    MOZ_ASSERT(aNewPoint.IsSet());
    mRangeUpdater.WillMoveNode();
  }

  ~AutoMoveNodeSelNotify() {
    mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
  }

  EditorRawDOMPoint ComputeInsertionPoint() const {
    if (&mOldParent == &mNewParent && mOldOffset < mNewOffset) {
      return EditorRawDOMPoint(&mNewParent, mNewOffset - 1);
    }
    return EditorRawDOMPoint(&mNewParent, mNewOffset);
  }

 private:
  RangeUpdater& mRangeUpdater;
  nsINode& mOldParent;
  nsINode& mNewParent;
  uint32_t mOldOffset;
  uint32_t mNewOffset;
};

}  // namespace mozilla

#endif  // #ifndef mozilla_SelectionState_h