Bug 1419745 - part 1: Make EditorDOMPointBase not a sub class of RangeBoundaryBase and duplicate methods of RangeBoundaryBase into EditorDOMPointBase r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 23 Nov 2017 00:15:29 +0900
changeset 704242 e1e59134cbb46628f21a1cd5727f26f4c58b3e9b
parent 704194 5b33b070378ae0806bed0b5e5e34de429a29e7db
child 704243 470a59bdd3a3ecd3be67394676d568b3f071b70a
push id91129
push usermasayuki@d-toybox.com
push dateTue, 28 Nov 2017 13:40:01 +0000
reviewersm_kato
bugs1419745
milestone59.0a1
Bug 1419745 - part 1: Make EditorDOMPointBase not a sub class of RangeBoundaryBase and duplicate methods of RangeBoundaryBase into EditorDOMPointBase r?m_kato EditorDOMPointBase should store child at offset for editor uses. E.g., when editor wants to refer a node as child node of an EditorDOMPoint instance, even if the node is unexpectedly moved to different container, editor wants to keep referring the child node rather than its previous sibling. Therefore this patch makes EditorDOMPointBase not a sub class of RangeBoundaryBase but copying all methods and members of RangeBoundaryBase into EditorDOMPointBase for keeping current behavior completely. MozReview-Commit-ID: LIyPFkCfsZ9
dom/base/RangeBoundary.h
editor/libeditor/EditorDOMPoint.h
--- a/dom/base/RangeBoundary.h
+++ b/dom/base/RangeBoundary.h
@@ -10,16 +10,19 @@
 #include "nsCOMPtr.h"
 #include "nsIContent.h"
 #include "mozilla/Maybe.h"
 
 class nsRange;
 
 namespace mozilla {
 
+template<typename T, typename U>
+class EditorDOMPointBase;
+
 // This class will maintain a reference to the child immediately
 // before the boundary's offset. We try to avoid computing the
 // offset as much as possible and just ensure mRef points to the
 // correct child.
 //
 // mParent
 //    |
 // [child0] [child1] [child2]
@@ -39,16 +42,18 @@ typedef RangeBoundaryBase<nsINode*, nsIC
 // This class has two specializations, one using reference counting
 // pointers and one using raw pointers. This helps us avoid unnecessary
 // AddRef/Release calls.
 template<typename ParentType, typename RefType>
 class RangeBoundaryBase
 {
   template<typename T, typename U>
   friend class RangeBoundaryBase;
+  template<typename T, typename U>
+  friend class EditorDOMPointBase;
 
   // nsRange needs to use InvalidOffset() which requires mRef initialized
   // before it's called.
   friend class ::nsRange;
 
   friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
                                           RangeBoundary&, const char*,
                                           uint32_t);
--- a/editor/libeditor/EditorDOMPoint.h
+++ b/editor/libeditor/EditorDOMPoint.h
@@ -20,109 +20,537 @@ template<typename ParentType, typename R
 class EditorDOMPointBase;
 
 typedef EditorDOMPointBase<nsCOMPtr<nsINode>,
                            nsCOMPtr<nsIContent>> EditorDOMPoint;
 typedef EditorDOMPointBase<nsINode*, nsIContent*> EditorRawDOMPoint;
 
 template<typename ParentType, typename RefType>
 class MOZ_STACK_CLASS EditorDOMPointBase final
-                        : public RangeBoundaryBase<ParentType, RefType>
 {
 public:
   EditorDOMPointBase()
-    : RangeBoundaryBase<ParentType, RefType>()
+    : mParent(nullptr)
+    , mRef(nullptr)
   {
   }
 
-  EditorDOMPointBase(nsINode* aConatiner,
+  EditorDOMPointBase(nsINode* aContainer,
                      int32_t aOffset)
-    : RangeBoundaryBase<ParentType, RefType>(aConatiner,
-                                             aOffset)
+    : mParent(aContainer)
+    , mRef(nullptr)
+    , mOffset(mozilla::Some(aOffset))
   {
+    if (!mParent) {
+      mOffset.reset();
+    }
   }
 
   EditorDOMPointBase(nsIDOMNode* aDOMContainer,
                      int32_t aOffset)
-    : RangeBoundaryBase<ParentType, RefType>()
   {
     nsCOMPtr<nsINode> container = do_QueryInterface(aDOMContainer);
     this->Set(container, aOffset);
   }
 
   /**
    * Different from RangeBoundary, aPointedNode should be a child node
    * which you want to refer.  So, set non-nullptr if offset is
    * 0 - Length() - 1.  Otherwise, set nullptr, i.e., if offset is same as
    * Length().
    */
   explicit EditorDOMPointBase(nsINode* aPointedNode)
-    : RangeBoundaryBase<ParentType, RefType>(
-        aPointedNode && aPointedNode->IsContent() ?
-          aPointedNode->GetParentNode() : nullptr,
-        aPointedNode && aPointedNode->IsContent() ?
-          GetRef(aPointedNode->GetParentNode(),
-                 aPointedNode->AsContent()) : nullptr)
+    : mParent(aPointedNode && aPointedNode->IsContent() ?
+                aPointedNode->GetParentNode() : nullptr)
+    , mRef(aPointedNode && aPointedNode->IsContent() ?
+             GetRef(aPointedNode->GetParentNode(),
+                    aPointedNode->AsContent()) : nullptr)
   {
+    if (!mRef) {
+      mOffset = mozilla::Some(0);
+    } else {
+      NS_WARNING_ASSERTION(mRef->GetParentNode() == mParent,
+                           "Initializing RangeBoundary with invalid value");
+      mOffset.reset();
+    }
   }
 
   EditorDOMPointBase(nsINode* aContainer,
                      nsIContent* aPointedNode,
                      int32_t aOffset)
-    : RangeBoundaryBase<ParentType, RefType>(aContainer,
-                                             GetRef(aContainer, aPointedNode),
-                                             aOffset)
+    : mParent(aContainer)
+    , mRef(GetRef(aContainer, aPointedNode))
+    , mOffset(mozilla::Some(aOffset))
   {
+    MOZ_RELEASE_ASSERT(aContainer,
+      "This constructor shouldn't be used when pointing nowhere");
+    if (!mRef) {
+      MOZ_ASSERT(!mParent->IsContainerNode() || mOffset.value() == 0);
+      return;
+    }
+    MOZ_ASSERT(mOffset.value() > 0);
+    MOZ_ASSERT(mParent == mRef->GetParentNode());
+    MOZ_ASSERT(mParent->GetChildAt(mOffset.value() - 1) == mRef);
   }
 
   template<typename PT, typename RT>
   explicit EditorDOMPointBase(const RangeBoundaryBase<PT, RT>& aOther)
-    : RangeBoundaryBase<ParentType, RefType>(aOther)
+    : mParent(aOther.mParent)
+    , mRef(aOther.mRef)
+    , mOffset(aOther.mOffset)
+  {
+  }
+
+  template<typename PT, typename RT>
+  explicit EditorDOMPointBase(const EditorDOMPointBase<PT, RT>& aOther)
+    : mParent(aOther.mParent)
+    , mRef(aOther.mRef)
+    , mOffset(aOther.mOffset)
   {
   }
 
-  explicit EditorDOMPointBase(const RawRangeBoundary& aRawRangeBoundary)
-    : RangeBoundaryBase<ParentType, RefType>(aRawRangeBoundary)
+  // Following methods are just copy of same methods of RangeBoudnaryBase.
+
+  nsIContent*
+  Ref() const
+  {
+    EnsureRef();
+    return mRef;
+  }
+
+  nsINode*
+  Container() const
+  {
+    return mParent;
+  }
+
+  nsIContent*
+  GetChildAtOffset() const
+  {
+    if (!mParent || !mParent->IsContainerNode()) {
+      return nullptr;
+    }
+    EnsureRef();
+    if (!mRef) {
+      MOZ_ASSERT(Offset() == 0, "invalid RangeBoundary");
+      return mParent->GetFirstChild();
+    }
+    MOZ_ASSERT(mParent->GetChildAt(Offset()) == mRef->GetNextSibling());
+    return mRef->GetNextSibling();
+  }
+
+  /**
+   * GetNextSiblingOfChildOffset() returns next sibling of a child at offset.
+   * If this refers after the last child or the container cannot have children,
+   * this returns nullptr with warning.
+   */
+  nsIContent*
+  GetNextSiblingOfChildAtOffset() const
+  {
+    if (NS_WARN_IF(!mParent) || NS_WARN_IF(!mParent->IsContainerNode())) {
+      return nullptr;
+    }
+    EnsureRef();
+    if (NS_WARN_IF(!mRef->GetNextSibling())) {
+      // Already referring the end of the container.
+      return nullptr;
+    }
+    return mRef->GetNextSibling()->GetNextSibling();
+  }
+
+  /**
+   * GetPreviousSiblingOfChildAtOffset() returns previous sibling of a child
+   * at offset.  If this refers the first child or the container cannot have
+   * children, this returns nullptr with warning.
+   */
+  nsIContent*
+  GetPreviousSiblingOfChildAtOffset() const
   {
+    if (NS_WARN_IF(!mParent) || NS_WARN_IF(!mParent->IsContainerNode())) {
+      return nullptr;
+    }
+    EnsureRef();
+    if (NS_WARN_IF(!mRef)) {
+      // Already referring the start of the container.
+      return nullptr;
+    }
+    return mRef;
+  }
+
+  uint32_t
+  Offset() const
+  {
+    if (mOffset.isSome()) {
+      return mOffset.value();
+    }
+    if (!mParent) {
+      MOZ_ASSERT(!mRef);
+      return 0;
+    }
+    MOZ_ASSERT(mParent->IsContainerNode(),
+      "If the container cannot have children, mOffset.isSome() should be true");
+    MOZ_ASSERT(mRef);
+    MOZ_ASSERT(mRef->GetParentNode() == mParent);
+    if (!mRef->GetPreviousSibling()) {
+      mOffset = mozilla::Some(1);
+      return mOffset.value();
+    }
+    if (!mRef->GetNextSibling()) {
+      mOffset = mozilla::Some(mParent->GetChildCount());
+      return mOffset.value();
+    }
+    // Use nsINode::IndexOf() as the last resort due to being expensive.
+    mOffset = mozilla::Some(mParent->IndexOf(mRef) + 1);
+    return mOffset.value();
+  }
+
+  /**
+   * Set() sets a point to aOffset or aChild.
+   * If it's set with offset, mRef is invalidated.  If it's set with aChild,
+   * mOffset may be invalidated unless the offset can be computed simply.
+   */
+  void
+  Set(nsINode* aContainer, int32_t aOffset)
+  {
+    mParent = aContainer;
+    mRef = nullptr;
+    mOffset = mozilla::Some(aOffset);
+  }
+  void
+  Set(const nsINode* aChild)
+  {
+    MOZ_ASSERT(aChild);
+    if (!aChild->IsContent()) {
+      Clear();
+      return;
+    }
+    mParent = aChild->GetParentNode();
+    mRef = aChild->GetPreviousSibling();
+    if (!mRef) {
+      mOffset = mozilla::Some(0);
+    } else {
+      mOffset.reset();
+    }
+  }
+
+  /**
+   * Clear() makes the instance not point anywhere.
+   */
+  void
+  Clear()
+  {
+    mParent = nullptr;
+    mRef = nullptr;
+    mOffset.reset();
   }
 
+  /**
+   * AdvanceOffset() tries to reference next sibling of mRef if its container
+   * can have children or increments offset if the container is a text node or
+   * something.
+   * If the container can have children and there is no next sibling, this
+   * outputs warning and does nothing.  So, callers need to check if there is
+   * next sibling which you need to refer.
+   *
+   * @return            true if there is a next sibling to refer.
+   */
+  bool
+  AdvanceOffset()
+  {
+    if (NS_WARN_IF(!mParent)) {
+      return false;
+    }
+    EnsureRef();
+    if (!mRef) {
+      if (!mParent->IsContainerNode()) {
+        // In text node or something, just increment the offset.
+        MOZ_ASSERT(mOffset.isSome());
+        if (NS_WARN_IF(mOffset.value() == mParent->Length())) {
+          // Already referring the end of the node.
+          return false;
+        }
+        mOffset = mozilla::Some(mOffset.value() + 1);
+        return true;
+      }
+      mRef = mParent->GetFirstChild();
+      if (NS_WARN_IF(!mRef)) {
+        // No children in the container.
+        mOffset = mozilla::Some(0);
+        return false;
+      }
+      mOffset = mozilla::Some(1);
+      return true;
+    }
+
+    nsIContent* nextSibling = mRef->GetNextSibling();
+    if (NS_WARN_IF(!nextSibling)) {
+      // Already referring the end of the container.
+      return false;
+    }
+    mRef = nextSibling;
+    if (mOffset.isSome()) {
+      mOffset = mozilla::Some(mOffset.value() + 1);
+    }
+    return true;
+  }
+
+  /**
+   * RewindOffset() tries to reference next sibling of mRef if its container
+   * can have children or decrements offset if the container is a text node or
+   * something.
+   * If the container can have children and there is no next previous, this
+   * outputs warning and does nothing.  So, callers need to check if there is
+   * previous sibling which you need to refer.
+   *
+   * @return            true if there is a previous sibling to refer.
+   */
+  bool
+  RewindOffset()
+  {
+    if (NS_WARN_IF(!mParent)) {
+      return false;
+    }
+    EnsureRef();
+    if (!mRef) {
+      if (NS_WARN_IF(mParent->IsContainerNode())) {
+        // Already referring the start of the container
+        mOffset = mozilla::Some(0);
+        return false;
+      }
+      // In text node or something, just decrement the offset.
+      MOZ_ASSERT(mOffset.isSome());
+      if (NS_WARN_IF(mOffset.value() == 0)) {
+        // Already referring the start of the node.
+        return false;
+      }
+      mOffset = mozilla::Some(mOffset.value() - 1);
+      return true;
+    }
+
+    mRef = mRef->GetPreviousSibling();
+    if (mOffset.isSome()) {
+      mOffset = mozilla::Some(mOffset.value() - 1);
+    }
+    return true;
+  }
+
+  void
+  SetAfterRef(nsINode* aParent, nsIContent* aRef)
+  {
+    mParent = aParent;
+    mRef = aRef;
+    if (!mRef) {
+      mOffset = mozilla::Some(0);
+    } else {
+      mOffset.reset();
+    }
+  }
+
+  bool
+  IsSet() const
+  {
+    return mParent && (mRef || mOffset.isSome());
+  }
+
+  bool
+  IsSetAndValid() const
+  {
+    if (!IsSet()) {
+      return false;
+    }
+
+    if (mRef && mRef->GetParentNode() != mParent) {
+      return false;
+    }
+    if (mOffset.isSome() && mOffset.value() > mParent->Length()) {
+      return false;
+    }
+    return true;
+  }
+
+  bool
+  IsStartOfContainer() const
+  {
+    // We're at the first point in the container if we don't have a reference,
+    // and our offset is 0. If we don't have a Ref, we should already have an
+    // offset, so we can just directly fetch it.
+    return !Ref() && mOffset.value() == 0;
+  }
+
+  bool
+  IsEndOfContainer() const
+  {
+    // We're at the last point in the container if Ref is a pointer to the last
+    // child in Container(), or our Offset() is the same as the length of our
+    // container. If we don't have a Ref, then we should already have an offset,
+    // so we can just directly fetch it.
+    return Ref()
+      ? !Ref()->GetNextSibling()
+      : mOffset.value() == Container()->Length();
+  }
+
+  // Convenience methods for switching between the two types
+  // of EditorDOMPointBase.
   EditorDOMPointBase<nsINode*, nsIContent*>
   AsRaw() const
   {
-    return EditorDOMPointBase<nsINode*, nsIContent*>(*this);
+    return EditorRawDOMPoint(*this);
+  }
+
+  template<typename A, typename B>
+  EditorDOMPointBase& operator=(const RangeBoundaryBase<A,B>& aOther)
+  {
+    mParent = aOther.mParent;
+    mRef = aOther.mRef;
+    mOffset = aOther.mOffset;
+    return *this;
   }
 
   template<typename A, typename B>
   EditorDOMPointBase& operator=(const EditorDOMPointBase<A, B>& aOther)
   {
-    RangeBoundaryBase<ParentType, RefType>::operator=(aOther);
+    mParent = aOther.mParent;
+    mRef = aOther.mRef;
+    mOffset = aOther.mOffset;
     return *this;
   }
 
   template<typename A, typename B>
-  EditorDOMPointBase& operator=(const RangeBoundaryBase<A, B>& aOther)
+  bool operator==(const EditorDOMPointBase<A, B>& aOther) const
   {
-    RangeBoundaryBase<ParentType, RefType>::operator=(aOther);
-    return *this;
+    if (mParent != aOther.mParent) {
+      return false;
+    }
+
+    if (mOffset.isSome() && aOther.mOffset.isSome()) {
+      // If both mOffset are set, we need to compare both mRef too because
+      // the relation of mRef and mOffset have already broken by DOM tree
+      // changes.
+      if (mOffset != aOther.mOffset) {
+        return false;
+      }
+      if (mRef == aOther.mRef) {
+        return true;
+      }
+      if (NS_WARN_IF(mRef && aOther.mRef)) {
+        // In this case, relation between mRef and mOffset of one of or both of
+        // them doesn't match with current DOM tree since the DOM tree might
+        // have been changed after computing mRef or mOffset.
+        return false;
+      }
+      // If one of mRef hasn't been computed yet, we should compare them only
+      // with mOffset.  Perhaps, we shouldn't copy mRef from non-nullptr one to
+      // nullptr one since if we copy it here, it may be unexpected behavior for
+      // some callers.
+      return true;
+    }
+
+    if (mOffset.isSome() && !mRef &&
+        !aOther.mOffset.isSome() && aOther.mRef) {
+      // If this has only mOffset and the other has only mRef, this needs to
+      // compute mRef now.
+      EnsureRef();
+      return mRef == aOther.mRef;
+    }
+
+    if (!mOffset.isSome() && mRef &&
+        aOther.mOffset.isSome() && !aOther.mRef) {
+      // If this has only mRef and the other has only mOffset, the other needs
+      // to compute mRef now.
+      aOther.EnsureRef();
+      return mRef == aOther.mRef;
+    }
+
+    // If mOffset of one of them hasn't been computed from mRef yet, we should
+    // compare only with mRef.  Perhaps, we shouldn't copy mOffset from being
+    // some one to not being some one since if we copy it here, it may be
+    // unexpected behavior for some callers.
+    return mRef == aOther.mRef;
+  }
+
+  template<typename A, typename B>
+  bool operator!=(const EditorDOMPointBase<A, B>& aOther) const
+  {
+    return !(*this == aOther);
+  }
+
+  template<typename A, typename B>
+  operator const RangeBoundaryBase<A, B>() const
+  {
+    if (mOffset.isSome()) {
+      if (mRef || !mOffset.value()) {
+        return RangeBoundaryBase<A, B>(mParent, mRef, mOffset.value());
+      }
+      return RangeBoundaryBase<A, B>(mParent, mOffset.value());
+    }
+    return RangeBoundaryBase<A, B>(mParent, mRef);
   }
 
 private:
   static nsIContent* GetRef(nsINode* aContainerNode, nsIContent* aPointedNode)
   {
     // If referring one of a child of the container, the 'ref' should be the
     // previous sibling of the referring child.
     if (aPointedNode) {
       return aPointedNode->GetPreviousSibling();
     }
     // If referring after the last child, the 'ref' should be the last child.
     if (aContainerNode && aContainerNode->IsContainerNode()) {
       return aContainerNode->GetLastChild();
     }
     return nullptr;
   }
+
+  /**
+   * InvalidOffset() is error prone method, unfortunately.  If somebody
+   * needs to call this method, it needs to call EnsureRef() before changing
+   * the position of the referencing point.
+   */
+  void
+  InvalidateOffset()
+  {
+    MOZ_ASSERT(mParent);
+    MOZ_ASSERT(mParent->IsContainerNode(),
+               "Range is positioned on a text node!");
+    MOZ_ASSERT(mRef || (mOffset.isSome() && mOffset.value() == 0),
+               "mRef should be computed before a call of InvalidateOffset()");
+
+    if (!mRef) {
+      return;
+    }
+    mOffset.reset();
+  }
+
+  void
+  EnsureRef() const
+  {
+    if (mRef) {
+      return;
+    }
+    if (!mParent) {
+      MOZ_ASSERT(!mOffset.isSome());
+      return;
+    }
+    MOZ_ASSERT(mOffset.isSome());
+    MOZ_ASSERT(mOffset.value() <= mParent->Length());
+    if (!mParent->IsContainerNode() ||
+        mOffset.value() == 0) {
+      return;
+    }
+    mRef = mParent->GetChildAt(mOffset.value() - 1);
+    MOZ_ASSERT(mRef);
+  }
+
+  ParentType mParent;
+  mutable RefType mRef;
+
+  mutable mozilla::Maybe<uint32_t> mOffset;
+
+  template<typename PT, typename RT>
+  friend class EditorDOMPointBase;
 };
 
 /**
  * AutoEditorDOMPointOffsetInvalidator is useful if DOM tree will be changed
  * when EditorDOMPoint instance is available and keeps referring same child
  * node.
  *
  * This class automatically guarantees that given EditorDOMPoint instance