Bug 1419745 - part 2: Make EditorDOMPointBase store child node at offset instead of previous sibling of child node at offset r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 23 Nov 2017 04:09:00 +0900
changeset 704243 470a59bdd3a3ecd3be67394676d568b3f071b70a
parent 704242 e1e59134cbb46628f21a1cd5727f26f4c58b3e9b
child 704244 534765e957fb3c7f32c9ec3d597c41fe4358d7f2
push id91129
push usermasayuki@d-toybox.com
push dateTue, 28 Nov 2017 13:40:01 +0000
reviewersm_kato
bugs1419745
milestone59.0a1
Bug 1419745 - part 2: Make EditorDOMPointBase store child node at offset instead of previous sibling of child node at offset r?m_kato EditorDOMPointBase should store child node at offset directly rather than previous sibling of child node at offset because if referring child node or its previous sibling is moved to different DOM point, EditorDOMPointBase::GetChildAtOffset() may return different node. However, users of this class assumes that after it stores child node at offset, it'll return same node. So, EditorDOMPointBase should store child node instead of "ref". Additionally, when it stores nullptr as the child, it should set mOffset to length of the mContainer because it means it refers after the last child. MozReview-Commit-ID: 7ZdkwKLJEjo
editor/libeditor/EditorDOMPoint.h
--- a/editor/libeditor/EditorDOMPoint.h
+++ b/editor/libeditor/EditorDOMPoint.h
@@ -11,410 +11,492 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/RangeBoundary.h"
 #include "nsCOMPtr.h"
 #include "nsIContent.h"
 #include "nsINode.h"
 
 namespace mozilla {
 
-template<typename ParentType, typename RefType>
+template<typename ParentType, typename ChildType>
 class EditorDOMPointBase;
 
+/**
+ * EditorDOMPoint and EditorRawDOMPoint are simple classes which refers
+ * a point in the DOM tree at creating the instance or initializing the
+ * instance with calling Set().
+ *
+ * EditorDOMPoint refers container node (and child node if it's already set)
+ * with nsCOMPtr.  EditorRawDOMPoint refers them with raw pointer.
+ * So, EditorRawDOMPoint is useful when you access the nodes only before
+ * changing DOM tree since increasing refcount may appear in micro benchmark
+ * if it's in a hot path.  On the other hand, if you need to refer them even
+ * after changing DOM tree, you must use EditorDOMPoint.
+ *
+ * When initializing an instance only with child node or offset,  the instance
+ * starts to refer the child node or offset in the container.  In this case,
+ * the other information hasn't been initialized due to performance reason.
+ * When you retrieve the other information with calling Offset() or
+ * GetChildAtOffset(), the other information is computed with the current
+ * DOM tree.  Therefore, e.g., in the following case, the other information
+ * may be different:
+ *
+ * EditorDOMPoint pointA(container1, childNode1);
+ * EditorDOMPoint pointB(container1, childNode1);
+ * Unused << pointA.Offset(); // The offset is computed now.
+ * container1->RemoveChild(childNode1->GetPreviousSibling());
+ * Unused << pointB.Offset(); // Now, pointB.Offset() equals pointA.Offset() - 1
+ *
+ * similarly:
+ *
+ * EditorDOMPoint pointA(container1, 5);
+ * EditorDOMPoint pointB(container1, 5);
+ * Unused << pointA.GetChildAtOffset(); // The child is computed now.
+ * container1->RemoveChild(childNode1->GetFirstChild());
+ * Unused << pointB.GetChildAtOffset(); // Now, pointB.GetChildAtOffset() equals
+ *                                      // pointA.GetChildAtOffset()->
+ *                                      //          GetPreviousSibling().
+ *
+ * So, when you initialize an instance only with one information, you need to
+ * be careful when you access the other information after changing the DOM tree.
+ * When you need to lock the child node or offset and recompute the other
+ * information with new DOM tree, you can use
+ * AutoEditorDOMPointOffsetInvalidator and AutoEditorDOMPointChildInvalidator.
+ */
+
 typedef EditorDOMPointBase<nsCOMPtr<nsINode>,
                            nsCOMPtr<nsIContent>> EditorDOMPoint;
 typedef EditorDOMPointBase<nsINode*, nsIContent*> EditorRawDOMPoint;
 
-template<typename ParentType, typename RefType>
+template<typename ParentType, typename ChildType>
 class MOZ_STACK_CLASS EditorDOMPointBase final
 {
+  typedef EditorDOMPointBase<ParentType, ChildType> SelfType;
+
 public:
   EditorDOMPointBase()
     : mParent(nullptr)
-    , mRef(nullptr)
+    , mChild(nullptr)
+    , mIsChildInitialized(false)
   {
   }
 
   EditorDOMPointBase(nsINode* aContainer,
                      int32_t aOffset)
     : mParent(aContainer)
-    , mRef(nullptr)
+    , mChild(nullptr)
     , mOffset(mozilla::Some(aOffset))
+    , mIsChildInitialized(false)
   {
+    NS_WARNING_ASSERTION(!mParent || mOffset.value() <= mParent->Length(),
+      "The offset is larger than the length of aContainer or negative");
     if (!mParent) {
       mOffset.reset();
     }
   }
 
   EditorDOMPointBase(nsIDOMNode* aDOMContainer,
                      int32_t aOffset)
+    : mIsChildInitialized(false)
   {
     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().
+   * which you want to refer.
    */
   explicit EditorDOMPointBase(nsINode* aPointedNode)
     : mParent(aPointedNode && aPointedNode->IsContent() ?
                 aPointedNode->GetParentNode() : nullptr)
-    , mRef(aPointedNode && aPointedNode->IsContent() ?
-             GetRef(aPointedNode->GetParentNode(),
-                    aPointedNode->AsContent()) : nullptr)
+    , mChild(aPointedNode && aPointedNode->IsContent() ?
+                aPointedNode->AsContent() : nullptr)
+    , mIsChildInitialized(false)
   {
-    if (!mRef) {
-      mOffset = mozilla::Some(0);
-    } else {
-      NS_WARNING_ASSERTION(mRef->GetParentNode() == mParent,
-                           "Initializing RangeBoundary with invalid value");
-      mOffset.reset();
-    }
+    NS_WARNING_ASSERTION(IsSet(),
+      "The child is nullptr or doesn't have its parent");
+    NS_WARNING_ASSERTION(mChild && mChild->GetParentNode() == mParent,
+      "Initializing RangeBoundary with invalid value");
+    mIsChildInitialized = aPointedNode && mChild;
   }
 
   EditorDOMPointBase(nsINode* aContainer,
                      nsIContent* aPointedNode,
                      int32_t aOffset)
     : mParent(aContainer)
-    , mRef(GetRef(aContainer, aPointedNode))
+    , mChild(aPointedNode)
     , mOffset(mozilla::Some(aOffset))
+    , mIsChildInitialized(true)
   {
     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);
+    MOZ_ASSERT(mOffset.value() <= mParent->Length());
+    MOZ_ASSERT(mChild || mParent->Length() == mOffset.value());
+    MOZ_ASSERT(!mChild || mParent == mChild->GetParentNode());
+    MOZ_ASSERT(mParent->GetChildAt(mOffset.value()) == mChild);
   }
 
-  template<typename PT, typename RT>
-  explicit EditorDOMPointBase(const RangeBoundaryBase<PT, RT>& aOther)
+  template<typename PT, typename CT>
+  explicit EditorDOMPointBase(const RangeBoundaryBase<PT, CT>& aOther)
     : mParent(aOther.mParent)
-    , mRef(aOther.mRef)
+    , mChild(aOther.mRef ? aOther.mRef->GetNextSibling() :
+                           (aOther.mParent ? aOther.mParent->GetFirstChild() :
+                                             nullptr))
     , mOffset(aOther.mOffset)
+    , mIsChildInitialized(aOther.mRef ||
+                          (aOther.mOffset.isSome() && !aOther.mOffset.value()))
   {
   }
 
-  template<typename PT, typename RT>
-  explicit EditorDOMPointBase(const EditorDOMPointBase<PT, RT>& aOther)
+  template<typename PT, typename CT>
+  explicit EditorDOMPointBase(const EditorDOMPointBase<PT, CT>& aOther)
     : mParent(aOther.mParent)
-    , mRef(aOther.mRef)
+    , mChild(aOther.mChild)
     , mOffset(aOther.mOffset)
+    , mIsChildInitialized(aOther.mIsChildInitialized)
   {
   }
 
   // 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();
+    if (mIsChildInitialized) {
+      return mChild;
     }
-    MOZ_ASSERT(mParent->GetChildAt(Offset()) == mRef->GetNextSibling());
-    return mRef->GetNextSibling();
+    // Fix child node now.
+    const_cast<SelfType*>(this)->EnsureChild();
+    return mChild;
   }
 
   /**
    * 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.
+    if (mIsChildInitialized) {
+      return mChild ? mChild->GetNextSibling() : nullptr;
+    }
+    MOZ_ASSERT(mOffset.isSome());
+    if (NS_WARN_IF(mOffset.value() > mParent->Length())) {
+      // If this has been set only offset and now the offset is invalid,
+      // let's just return nullptr.
       return nullptr;
     }
-    return mRef->GetNextSibling()->GetNextSibling();
+    // Fix child node now.
+    const_cast<SelfType*>(this)->EnsureChild();
+    return mChild ? mChild->GetNextSibling() : nullptr;
   }
 
   /**
    * 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.
+    if (mIsChildInitialized) {
+      return mChild ? mChild->GetPreviousSibling() : mParent->GetLastChild();
+    }
+    MOZ_ASSERT(mOffset.isSome());
+    if (NS_WARN_IF(mOffset.value() > mParent->Length())) {
+      // If this has been set only offset and now the offset is invalid,
+      // let's just return nullptr.
       return nullptr;
     }
-    return mRef;
+    // Fix child node now.
+    const_cast<SelfType*>(this)->EnsureChild();
+    return mChild ? mChild->GetPreviousSibling() : mParent->GetLastChild();
   }
 
   uint32_t
   Offset() const
   {
     if (mOffset.isSome()) {
+      MOZ_ASSERT(mOffset.isSome());
       return mOffset.value();
     }
     if (!mParent) {
-      MOZ_ASSERT(!mRef);
+      MOZ_ASSERT(!mChild);
       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);
+    if (!mChild) {
+      // We're referring after the last child.  Fix offset now.
+      const_cast<SelfType*>(this)->mOffset = mozilla::Some(mParent->Length());
       return mOffset.value();
     }
-    if (!mRef->GetNextSibling()) {
-      mOffset = mozilla::Some(mParent->GetChildCount());
-      return mOffset.value();
+    MOZ_ASSERT(mChild->GetParentNode() == mParent);
+    // Fix offset now.
+    if (mChild == mParent->GetFirstChild()) {
+      const_cast<SelfType*>(this)->mOffset = mozilla::Some(0);
+    } else {
+      const_cast<SelfType*>(this)->mOffset =
+        mozilla::Some(mParent->IndexOf(mChild));
     }
-    // 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.
+   * If it's set with aOffset, mChild is invalidated.  If it's set with aChild,
+   * mOffset may be invalidated.
    */
   void
   Set(nsINode* aContainer, int32_t aOffset)
   {
     mParent = aContainer;
-    mRef = nullptr;
+    mChild = nullptr;
     mOffset = mozilla::Some(aOffset);
+    mIsChildInitialized = false;
+    NS_WARNING_ASSERTION(!mParent || mOffset.value() <= mParent->Length(),
+      "The offset is out of bounds");
   }
   void
   Set(const nsINode* aChild)
   {
     MOZ_ASSERT(aChild);
-    if (!aChild->IsContent()) {
+    if (NS_WARN_IF(!aChild->IsContent())) {
       Clear();
       return;
     }
     mParent = aChild->GetParentNode();
-    mRef = aChild->GetPreviousSibling();
-    if (!mRef) {
-      mOffset = mozilla::Some(0);
-    } else {
-      mOffset.reset();
-    }
+    mChild = const_cast<nsIContent*>(aChild->AsContent());
+    mOffset.reset();
+    mIsChildInitialized = true;
   }
 
   /**
    * Clear() makes the instance not point anywhere.
    */
   void
   Clear()
   {
     mParent = nullptr;
-    mRef = nullptr;
+    mChild = nullptr;
     mOffset.reset();
+    mIsChildInitialized = false;
   }
 
   /**
-   * 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.
+   * AdvanceOffset() tries to refer next sibling of mChild and/of next offset.
+   * If the container can have children and there is no next sibling or the
+   * offset reached the length of the container, 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.
+   * @return            true if there is a next DOM point 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);
+    // If only mOffset is available, just compute the offset.
+    if ((mOffset.isSome() && !mIsChildInitialized) ||
+        !mParent->IsContainerNode()) {
+      MOZ_ASSERT(mOffset.isSome());
+      MOZ_ASSERT(!mChild);
+      if (NS_WARN_IF(mOffset.value() >= mParent->Length())) {
+        // We're already referring the start of the container.
         return false;
       }
-      mOffset = mozilla::Some(1);
+      mOffset = mozilla::Some(mOffset.value() + 1);
       return true;
     }
 
-    nsIContent* nextSibling = mRef->GetNextSibling();
-    if (NS_WARN_IF(!nextSibling)) {
-      // Already referring the end of the container.
+    MOZ_ASSERT(mIsChildInitialized);
+    MOZ_ASSERT(!mOffset.isSome() || mOffset.isSome());
+    if (NS_WARN_IF(!mParent->HasChildren()) ||
+        NS_WARN_IF(!mChild) ||
+        NS_WARN_IF(mOffset.isSome() && mOffset.value() >= mParent->Length())) {
+      // We're already referring the end of the container (or outside).
       return false;
     }
-    mRef = nextSibling;
+
     if (mOffset.isSome()) {
+      MOZ_ASSERT(mOffset.isSome());
       mOffset = mozilla::Some(mOffset.value() + 1);
     }
+    mChild = mChild->GetNextSibling();
     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.
+   * RewindOffset() tries to refer previous sibling of mChild and/or previous
+   * offset.  If the container can have children and there is no next previous
+   * or the offset is 0, 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.
+   * @return            true if there is a previous DOM point 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.
+    // If only mOffset is available, just compute the offset.
+    if ((mOffset.isSome() && !mIsChildInitialized) ||
+        !mParent->IsContainerNode()) {
       MOZ_ASSERT(mOffset.isSome());
-      if (NS_WARN_IF(mOffset.value() == 0)) {
-        // Already referring the start of the node.
+      MOZ_ASSERT(!mChild);
+      if (NS_WARN_IF(!mOffset.value()) ||
+          NS_WARN_IF(mOffset.value() >= mParent->Length())) {
+        // We're already referring the start of the container.
         return false;
       }
       mOffset = mozilla::Some(mOffset.value() - 1);
       return true;
     }
 
-    mRef = mRef->GetPreviousSibling();
+    MOZ_ASSERT(mIsChildInitialized);
+    MOZ_ASSERT(!mOffset.isSome() || mOffset.isSome());
+    if (NS_WARN_IF(!mParent->HasChildren()) ||
+        NS_WARN_IF(mChild && !mChild->GetPreviousSibling()) ||
+        NS_WARN_IF(mOffset.isSome() && !mOffset.value())) {
+      // We're already referring the start of the container (or the child has
+      // been moved from the container?).
+      return false;
+    }
+
+    nsIContent* previousSibling =
+        mChild ? mChild->GetPreviousSibling() : mParent->GetLastChild();
+    if (NS_WARN_IF(!previousSibling)) {
+      // We're already referring the first child of the container.
+      return false;
+    }
+
     if (mOffset.isSome()) {
       mOffset = mozilla::Some(mOffset.value() - 1);
     }
+    mChild = previousSibling;
     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());
+    return mParent && (mIsChildInitialized || mOffset.isSome());
   }
 
   bool
   IsSetAndValid() const
   {
     if (!IsSet()) {
       return false;
     }
 
-    if (mRef && mRef->GetParentNode() != mParent) {
+    if (mChild && mChild->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;
+    // If we're referring the first point in the container:
+    //   If mParent is not a container like a text node, mOffset is 0.
+    //   If mChild is initialized and it's first child of mParent.
+    //   If mChild isn't initialized and the offset is 0.
+    if (NS_WARN_IF(!mParent)) {
+      return false;
+    }
+    if (!mParent->IsContainerNode()) {
+      return !mOffset.value();
+    }
+    if (mIsChildInitialized) {
+      NS_WARNING_ASSERTION(!mOffset.isSome() || !mOffset.value(),
+        "If offset was initialized, mOffset should be 0");
+      return mParent->GetFirstChild() == mChild;
+    }
+    MOZ_ASSERT(mOffset.isSome());
+    return !mOffset.value();
   }
 
   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();
+    // If we're referring after the last point of the container:
+    //   If mParent is not a container like text node, mOffset is same as the
+    //   length of the container.
+    //   If mChild is initialized and it's nullptr.
+    //   If mChild isn't initialized and mOffset is same as the length of the
+    //   container.
+    if (NS_WARN_IF(!mParent)) {
+      return false;
+    }
+    if (mIsChildInitialized) {
+      NS_WARNING_ASSERTION(!mOffset.isSome() ||
+                           mOffset.value() == mParent->Length(),
+        "If offset was initialized, mOffset should be length of the container");
+      return !mChild;
+    }
+    MOZ_ASSERT(mOffset.isSome());
+    return mOffset.value() == mParent->Length();
   }
 
   // Convenience methods for switching between the two types
   // of EditorDOMPointBase.
   EditorDOMPointBase<nsINode*, nsIContent*>
   AsRaw() const
   {
     return EditorRawDOMPoint(*this);
   }
 
   template<typename A, typename B>
   EditorDOMPointBase& operator=(const RangeBoundaryBase<A,B>& aOther)
   {
     mParent = aOther.mParent;
-    mRef = aOther.mRef;
+    mChild =
+      aOther.mRef ? aOther.mRef->GetNextSibling() :
+                    (aOther.mParent && aOther.mParent->IsContainerNode() ?
+                       aOther.mParent->GetFirstChild() : nullptr);
     mOffset = aOther.mOffset;
+    mIsChildInitialized =
+      aOther.mRef ||
+      (aOther.mParent && !aOther.mParent->IsContainerNode()) ||
+      (aOther.mOffset.isSome() && !aOther.mOffset.value());
     return *this;
   }
 
   template<typename A, typename B>
   EditorDOMPointBase& operator=(const EditorDOMPointBase<A, B>& aOther)
   {
     mParent = aOther.mParent;
-    mRef = aOther.mRef;
+    mChild = aOther.mChild;
     mOffset = aOther.mOffset;
+    mIsChildInitialized = aOther.mIsChildInitialized;
     return *this;
   }
 
   template<typename A, typename B>
   bool operator==(const EditorDOMPointBase<A, B>& aOther) const
   {
     if (mParent != aOther.mParent) {
       return false;
@@ -422,134 +504,127 @@ public:
 
     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) {
+      if (mChild == aOther.mChild) {
         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.
+      if (NS_WARN_IF(mIsChildInitialized && aOther.mIsChildInitialized)) {
+        // In this case, relation between mChild 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 mChild 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.
+      // If one of mChild hasn't been computed yet, we should compare them only
+      // with mOffset.  Perhaps, we shouldn't copy mChild from non-nullptr one
+      // to the other 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;
+    MOZ_ASSERT(mIsChildInitialized || aOther.mIsChildInitialized);
+
+    if (mOffset.isSome() && !mIsChildInitialized &&
+        !aOther.mOffset.isSome() && aOther.mIsChildInitialized) {
+      // If this has only mOffset and the other has only mChild, this needs to
+      // compute mChild now.
+      const_cast<SelfType*>(this)->EnsureChild();
+      return mChild == aOther.mChild;
     }
 
-    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.isSome() && mIsChildInitialized &&
+        aOther.mOffset.isSome() && !aOther.mIsChildInitialized) {
+      // If this has only mChild and the other has only mOffset, the other needs
+      // to compute mChild now.
+      const_cast<EditorDOMPointBase<A, B>&>(aOther).EnsureChild();
+      return mChild == aOther.mChild;
     }
 
-    // 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
+    // If mOffset of one of them hasn't been computed from mChild yet, we should
+    // compare only with mChild.  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;
+    return mChild == aOther.mChild;
   }
 
   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());
-      }
+    if (!IsSet() ||
+        NS_WARN_IF(!mIsChildInitialized && !mOffset.isSome())) {
+      return RangeBoundaryBase<A, B>();
+    }
+    if (!mParent->IsContainerNode()) {
+      // If the container is a data node like a text node, we need to create
+      // RangeBoundaryBase instance only with mOffset because mChild is always
+      // nullptr.
       return RangeBoundaryBase<A, B>(mParent, mOffset.value());
     }
-    return RangeBoundaryBase<A, B>(mParent, mRef);
+    if (mIsChildInitialized && mOffset.isSome()) {
+      // If we've already set both child and offset, we should use both of them
+      // to create RangeBoundaryBase instance because the constructor will
+      // validate the relation in debug build.
+      if (mChild) {
+        return RangeBoundaryBase<A, B>(mParent, mChild->GetPreviousSibling(),
+                                       mOffset.value());
+      }
+      return RangeBoundaryBase<A, B>(mParent, mParent->GetLastChild(),
+                                     mOffset.value());
+    }
+    // Otherwise, we should create RangeBoundaryBase only with available
+    // information.
+    if (mOffset.isSome()) {
+      return RangeBoundaryBase<A, B>(mParent, mOffset.value());
+    }
+    if (mChild) {
+      return RangeBoundaryBase<A, B>(mParent, mChild->GetPreviousSibling());
+    }
+    return RangeBoundaryBase<A, B>(mParent, mParent->GetLastChild());
   }
 
 private:
-  static nsIContent* GetRef(nsINode* aContainerNode, nsIContent* aPointedNode)
+  void
+  EnsureChild()
   {
-    // 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) {
+    if (mIsChildInitialized) {
       return;
     }
     if (!mParent) {
       MOZ_ASSERT(!mOffset.isSome());
       return;
     }
     MOZ_ASSERT(mOffset.isSome());
     MOZ_ASSERT(mOffset.value() <= mParent->Length());
-    if (!mParent->IsContainerNode() ||
-        mOffset.value() == 0) {
+    mIsChildInitialized = true;
+    if (!mParent->IsContainerNode()) {
       return;
     }
-    mRef = mParent->GetChildAt(mOffset.value() - 1);
-    MOZ_ASSERT(mRef);
+    mChild = mParent->GetChildAt(mOffset.value());
+    MOZ_ASSERT(mChild || mOffset.value() == mParent->Length());
   }
 
   ParentType mParent;
-  mutable RefType mRef;
+  ChildType mChild;
+
+  mozilla::Maybe<uint32_t> mOffset;
 
-  mutable mozilla::Maybe<uint32_t> mOffset;
+  bool mIsChildInitialized;
 
-  template<typename PT, typename RT>
+  template<typename PT, typename CT>
   friend class EditorDOMPointBase;
 };
 
 /**
  * AutoEditorDOMPointOffsetInvalidator is useful if DOM tree will be changed
  * when EditorDOMPoint instance is available and keeps referring same child
  * node.
  *