Bug 1574852 - part 67-3: Split `HTMLEditRules::WillDeleteSelection()` to 3 methods r=m_kato
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 05 Sep 2019 13:37:57 +0000
changeset 491927 09cbd757c16bd342f3695c3735a026168b15cac1
parent 491926 58347c61ae4e5fcb1c4f458c5063fd2b67090958
child 491928 dca9b6dd135f3230cb7f5516003df450efcb06bf
push id36540
push userapavel@mozilla.com
push dateFri, 06 Sep 2019 03:58:29 +0000
treeherdermozilla-central@3bfc0e27d038 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersm_kato
bugs1574852
milestone71.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1574852 - part 67-3: Split `HTMLEditRules::WillDeleteSelection()` to 3 methods r=m_kato First, we can split it with 3 parts: 1. preparation with current selection part. 2. handling deletion with collapsed selection part. 3. handling deletion with non-collapsed selection part. The first should leave in `WillDeleteSelection()`, but the others should be moved to independent methods. With this change, we need to fix an odd case when there is no visible node and `GetFirstSelectedTableCellElement()` returned error due to crossing the method boundary. The method returns error only when there is no selection ranges. In such case, we cannot handle deletion of current selection anyway. So, it must not be a problem to handle the error immediately after calling `GetFirstSelectedTableCellElement()`. Note that this shouldn't occur in normal cases since `WillDoAction()` checks it before calling `WillDeleteSelection()`. Differential Revision: https://phabricator.services.mozilla.com/D44453
editor/libeditor/HTMLEditRules.cpp
editor/libeditor/HTMLEditRules.h
--- a/editor/libeditor/HTMLEditRules.cpp
+++ b/editor/libeditor/HTMLEditRules.cpp
@@ -2343,41 +2343,48 @@ EditActionResult HTMLEditRules::WillDele
   // If there is only padding `<br>` element for empty editor, cancel the
   // operation.
   if (HTMLEditorRef().HasPaddingBRElementForEmptyEditor()) {
     return EditActionCanceled();
   }
 
   // First check for table selection mode.  If so, hand off to table editor.
   ErrorResult error;
-  RefPtr<Element> cellElement =
-      HTMLEditorRef().GetFirstSelectedTableCellElement(error);
-  if (cellElement) {
+  if (RefPtr<Element> cellElement =
+          HTMLEditorRef().GetFirstSelectedTableCellElement(error)) {
     error.SuppressException();
     nsresult rv =
         MOZ_KnownLive(HTMLEditorRef()).DeleteTableCellContentsWithTransaction();
     if (NS_WARN_IF(!CanHandleEditAction())) {
       return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
     }
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                          "DeleteTableCellContentsWithTransaction() failed");
     return EditActionHandled(rv);
   }
-  nsresult rv = error.StealNSResult();
-  cellElement = nullptr;
-
-  // origCollapsed is used later to determine whether we should join blocks. We
-  // don't really care about bCollapsed because it will be modified by
-  // ExtendSelectionForDelete later. TryToJoinBlocksWithTransaction() should
-  // happen if the original selection is collapsed and the cursor is at the end
-  // of a block element, in which case ExtendSelectionForDelete would always
-  // make the selection not collapsed.
-  bool origCollapsed = SelectionRefPtr()->IsCollapsed();
-
-  if (origCollapsed) {
+
+  // Only when there is no selection range, GetFirstSelectedTableCellElement()
+  // returns error and in this case, anyway we cannot handle delete selection.
+  // So, let's return error here even though we haven't handled this.
+  if (NS_WARN_IF(error.Failed())) {
+    return EditActionResult(error.StealNSResult());
+  }
+
+  // selectionWasCollapsed is used later to determine whether we should join
+  // blocks in HandleDeleteNonCollapasedSelection(). We don't really care
+  // about collapsed because it will be modified by ExtendSelectionForDelete()
+  // later. TryToJoinBlocksWithTransaction() should happen if the original
+  // selection is collapsed and the cursor is at the end of a block element,
+  // in which case ExtendSelectionForDelete() would always make the selection
+  // not collapsed.
+  SelectionWasCollapsed selectionWasCollapsed = SelectionRefPtr()->IsCollapsed()
+                                                    ? SelectionWasCollapsed::Yes
+                                                    : SelectionWasCollapsed::No;
+
+  if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
     EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
     if (NS_WARN_IF(!startPoint.IsSet())) {
       return EditActionResult(NS_ERROR_FAILURE);
     }
 
     // If we are inside an empty block, delete it.
     RefPtr<Element> editingHost = HTMLEditorRef().GetActiveEditingHost();
     if (NS_WARN_IF(!editingHost)) {
@@ -2406,559 +2413,583 @@ EditActionResult HTMLEditRules::WillDele
 
     // ExtendSelectionForDelete will use selection controller to set
     // selection for delete.  But if focus event doesn't receive yet,
     // ancestor isn't set.  So we must set root eleement of editor to
     // ancestor.
     AutoSetTemporaryAncestorLimiter autoSetter(
         HTMLEditorRef(), *SelectionRefPtr(), *startPoint.GetContainer());
 
-    rv = HTMLEditorRef().ExtendSelectionForDelete(&aDirectionAndAmount);
+    nsresult rv =
+        HTMLEditorRef().ExtendSelectionForDelete(&aDirectionAndAmount);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return EditActionResult(rv);
     }
 
     // We should delete nothing.
     if (aDirectionAndAmount == nsIEditor::eNone) {
       return EditActionIgnored();
     }
-  }
-
-  if (SelectionRefPtr()->IsCollapsed()) {
-    // ExtendSelectionForDelete() won't change the selection.
-
-    EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
-    if (NS_WARN_IF(!startPoint.IsSet())) {
-      return EditActionResult(NS_ERROR_FAILURE);
-    }
-
-    // What's in the direction we are deleting?
-    WSRunObject wsObj(&HTMLEditorRef(), startPoint);
-    nsCOMPtr<nsINode> visibleNode;
-    int32_t visibleNodeOffset;
-    WSType wsType;
-
-    // Find next visible node
+
+    if (SelectionRefPtr()->IsCollapsed()) {
+      EditActionResult result = HandleDeleteAroundCollapsedSelection(
+          aDirectionAndAmount, aStripWrappers);
+      NS_WARNING_ASSERTION(result.Succeeded(),
+                           "HandleDeleteAroundCollapsedSelection() failed");
+      return result;
+    }
+  }
+
+  EditActionResult result = HandleDeleteNonCollapsedSelection(
+      aDirectionAndAmount, aStripWrappers, selectionWasCollapsed);
+  NS_WARNING_ASSERTION(result.Succeeded(),
+                       "HandleDeleteNonCollapasedSelection() failed");
+  return result;
+}
+
+EditActionResult HTMLEditRules::HandleDeleteAroundCollapsedSelection(
+    nsIEditor::EDirection aDirectionAndAmount,
+    nsIEditor::EStripWrappers aStripWrappers) {
+  MOZ_ASSERT(IsEditorDataAvailable());
+  MOZ_ASSERT(HTMLEditorRef().IsTopLevelEditSubActionDataAvailable());
+  MOZ_ASSERT(SelectionRefPtr()->IsCollapsed());
+  MOZ_ASSERT(aDirectionAndAmount != nsIEditor::eNone);
+
+  EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
+  if (NS_WARN_IF(!startPoint.IsSet())) {
+    return EditActionResult(NS_ERROR_FAILURE);
+  }
+
+  // What's in the direction we are deleting?
+  WSRunObject wsObj(&HTMLEditorRef(), startPoint);
+  nsCOMPtr<nsINode> visibleNode;
+  int32_t visibleNodeOffset;
+  WSType wsType;
+
+  // Find next visible node
+  if (aDirectionAndAmount == nsIEditor::eNext) {
+    wsObj.NextVisibleNode(startPoint, address_of(visibleNode),
+                          &visibleNodeOffset, &wsType);
+  } else {
+    wsObj.PriorVisibleNode(startPoint, address_of(visibleNode),
+                           &visibleNodeOffset, &wsType);
+  }
+
+  if (!visibleNode) {
+    return EditActionCanceled();
+  }
+
+  if (wsType == WSType::normalWS) {
+    // We found some visible ws to delete.  Let ws code handle it.
     if (aDirectionAndAmount == nsIEditor::eNext) {
-      wsObj.NextVisibleNode(startPoint, address_of(visibleNode),
-                            &visibleNodeOffset, &wsType);
-    } else {
-      wsObj.PriorVisibleNode(startPoint, address_of(visibleNode),
-                             &visibleNodeOffset, &wsType);
-    }
-
-    if (!visibleNode) {
-      // XXX This is the result of GetFirstSelectedTableCellElement().
-      //     This must be a bug.
-      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
-                           "GetFirstSelectedTableCellElement() failed");
-      return EditActionCanceled(rv);
-    }
-
-    if (wsType == WSType::normalWS) {
-      // We found some visible ws to delete.  Let ws code handle it.
-      if (aDirectionAndAmount == nsIEditor::eNext) {
-        nsresult rv = wsObj.DeleteWSForward();
-        if (NS_WARN_IF(!CanHandleEditAction())) {
-          return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
-        }
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-          return EditActionHandled(rv);
-        }
-      } else {
-        nsresult rv = wsObj.DeleteWSBackward();
-        if (NS_WARN_IF(!CanHandleEditAction())) {
-          return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
-        }
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-          return EditActionHandled(rv);
-        }
-      }
-      nsresult rv =
-          MOZ_KnownLive(HTMLEditorRef())
-              .InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
-                  EditorBase::GetStartPoint(*SelectionRefPtr()));
-      NS_WARNING_ASSERTION(
-          NS_SUCCEEDED(rv),
-          "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed");
-      return EditActionHandled(rv);
-    }
-
-    if (wsType == WSType::text) {
-      // Found normal text to delete.
-      OwningNonNull<Text> visibleTextNode = *visibleNode->GetAsText();
-      int32_t startOffset = visibleNodeOffset;
-      int32_t endOffset = visibleNodeOffset + 1;
-      if (aDirectionAndAmount == nsIEditor::ePrevious) {
-        if (!startOffset) {
-          return EditActionResult(NS_ERROR_UNEXPECTED);
-        }
-        startOffset--;
-        endOffset--;
-        // Bug 1068979: delete both codepoints if surrogate pair
-        if (startOffset > 0) {
-          const nsTextFragment* text = &visibleTextNode->TextFragment();
-          if (text->IsLowSurrogateFollowingHighSurrogateAt(startOffset)) {
-            startOffset--;
-          }
-        }
-      } else {
-        RefPtr<nsRange> range = SelectionRefPtr()->GetRangeAt(0);
-        if (NS_WARN_IF(!range)) {
-          return EditActionResult(NS_ERROR_FAILURE);
-        }
-
-        NS_ASSERTION(range->GetStartContainer() == visibleNode,
-                     "selection start not in visibleNode");
-        NS_ASSERTION(range->GetEndContainer() == visibleNode,
-                     "selection end not in visibleNode");
-
-        startOffset = range->StartOffset();
-        endOffset = range->EndOffset();
-      }
-      nsresult rv = WSRunObject::PrepareToDeleteRange(
-          MOZ_KnownLive(&HTMLEditorRef()), address_of(visibleNode),
-          &startOffset, address_of(visibleNode), &endOffset);
+      nsresult rv = wsObj.DeleteWSForward();
       if (NS_WARN_IF(!CanHandleEditAction())) {
-        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
       }
       if (NS_WARN_IF(NS_FAILED(rv))) {
-        return EditActionResult(rv);
-      }
-      rv = MOZ_KnownLive(HTMLEditorRef())
-               .DeleteTextWithTransaction(
-                   visibleTextNode, std::min(startOffset, endOffset),
-                   DeprecatedAbs(endOffset - startOffset));
+        return EditActionHandled(rv);
+      }
+    } else {
+      nsresult rv = wsObj.DeleteWSBackward();
       if (NS_WARN_IF(!CanHandleEditAction())) {
         return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
       }
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return EditActionHandled(rv);
       }
-
-      // XXX When Backspace key is pressed, Chromium removes following empty
-      //     text nodes when removing the last character of the non-empty text
-      //     node.  However, Edge never removes empty text nodes even if
-      //     selection is in the following empty text node(s).  For now, we
-      //     should keep our traditional behavior same as Edge for backward
-      //     compatibility.
-      // XXX When Delete key is pressed, Edge removes all preceding empty
-      //     text nodes when removing the first character of the non-empty
-      //     text node.  Chromium removes only selected empty text node and
-      //     following empty text nodes and the first character of the
-      //     non-empty text node.  For now, we should keep our traditional
-      //     behavior same as Chromium for backward compatibility.
-
-      rv = MOZ_KnownLive(HTMLEditorRef())
-               .DeleteNodeIfInvisibleAndEditableTextNode(visibleTextNode);
-      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
-        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
-      }
-      NS_WARNING_ASSERTION(
-          NS_SUCCEEDED(rv),
-          "DeleteNodeIfInvisibleAndEditableTextNode() failed, but ignored");
-
-      rv = MOZ_KnownLive(HTMLEditorRef())
-               .InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
-                   EditorBase::GetStartPoint(*SelectionRefPtr()));
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return EditActionHandled(rv);
-      }
-
-      // Remember that we did a ranged delete for the benefit of
-      // AfterEditInner().
-      HTMLEditorRef()
-          .TopLevelEditSubActionDataRef()
-          .mDidDeleteNonCollapsedRange = true;
-
+    }
+    nsresult rv = MOZ_KnownLive(HTMLEditorRef())
+                      .InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+                          EditorBase::GetStartPoint(*SelectionRefPtr()));
+    NS_WARNING_ASSERTION(
+        NS_SUCCEEDED(rv),
+        "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed");
+    return EditActionHandled(rv);
+  }
+
+  if (wsType == WSType::text) {
+    // Found normal text to delete.
+    OwningNonNull<Text> visibleTextNode = *visibleNode->GetAsText();
+    int32_t startOffset = visibleNodeOffset;
+    int32_t endOffset = visibleNodeOffset + 1;
+    if (aDirectionAndAmount == nsIEditor::ePrevious) {
+      if (!startOffset) {
+        return EditActionResult(NS_ERROR_UNEXPECTED);
+      }
+      startOffset--;
+      endOffset--;
+      // Bug 1068979: delete both codepoints if surrogate pair
+      if (startOffset > 0) {
+        const nsTextFragment* text = &visibleTextNode->TextFragment();
+        if (text->IsLowSurrogateFollowingHighSurrogateAt(startOffset)) {
+          startOffset--;
+        }
+      }
+    } else {
+      RefPtr<nsRange> range = SelectionRefPtr()->GetRangeAt(0);
+      if (NS_WARN_IF(!range)) {
+        return EditActionResult(NS_ERROR_FAILURE);
+      }
+
+      NS_ASSERTION(range->GetStartContainer() == visibleNode,
+                   "selection start not in visibleNode");
+      NS_ASSERTION(range->GetEndContainer() == visibleNode,
+                   "selection end not in visibleNode");
+
+      startOffset = range->StartOffset();
+      endOffset = range->EndOffset();
+    }
+    nsresult rv = WSRunObject::PrepareToDeleteRange(
+        MOZ_KnownLive(&HTMLEditorRef()), address_of(visibleNode), &startOffset,
+        address_of(visibleNode), &endOffset);
+    if (NS_WARN_IF(!CanHandleEditAction())) {
+      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+    }
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return EditActionResult(rv);
+    }
+    rv = MOZ_KnownLive(HTMLEditorRef())
+             .DeleteTextWithTransaction(visibleTextNode,
+                                        std::min(startOffset, endOffset),
+                                        DeprecatedAbs(endOffset - startOffset));
+    if (NS_WARN_IF(!CanHandleEditAction())) {
+      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+    }
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return EditActionHandled(rv);
+    }
+
+    // XXX When Backspace key is pressed, Chromium removes following empty
+    //     text nodes when removing the last character of the non-empty text
+    //     node.  However, Edge never removes empty text nodes even if
+    //     selection is in the following empty text node(s).  For now, we
+    //     should keep our traditional behavior same as Edge for backward
+    //     compatibility.
+    // XXX When Delete key is pressed, Edge removes all preceding empty
+    //     text nodes when removing the first character of the non-empty
+    //     text node.  Chromium removes only selected empty text node and
+    //     following empty text nodes and the first character of the
+    //     non-empty text node.  For now, we should keep our traditional
+    //     behavior same as Chromium for backward compatibility.
+
+    rv = MOZ_KnownLive(HTMLEditorRef())
+             .DeleteNodeIfInvisibleAndEditableTextNode(visibleTextNode);
+    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+    }
+    NS_WARNING_ASSERTION(
+        NS_SUCCEEDED(rv),
+        "DeleteNodeIfInvisibleAndEditableTextNode() failed, but ignored");
+
+    rv = MOZ_KnownLive(HTMLEditorRef())
+             .InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+                 EditorBase::GetStartPoint(*SelectionRefPtr()));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return EditActionHandled(rv);
+    }
+
+    // Remember that we did a ranged delete for the benefit of
+    // AfterEditInner().
+    HTMLEditorRef().TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange =
+        true;
+
+    return EditActionHandled();
+  }
+
+  if (wsType == WSType::special || wsType == WSType::br ||
+      visibleNode->IsHTMLElement(nsGkAtoms::hr)) {
+    // If the void element is editing host, we should do nothing.
+    if (visibleNode == wsObj.GetEditingHost()) {
       return EditActionHandled();
     }
 
-    if (wsType == WSType::special || wsType == WSType::br ||
-        visibleNode->IsHTMLElement(nsGkAtoms::hr)) {
-      // If the void element is editing host, we should do nothing.
-      if (visibleNode == wsObj.GetEditingHost()) {
-        return EditActionHandled();
-      }
-
-      // Short circuit for invisible breaks.  delete them and recurse.
-      if (visibleNode->IsHTMLElement(nsGkAtoms::br) &&
-          !HTMLEditorRef().IsVisibleBRElement(visibleNode)) {
-        nsresult rv = MOZ_KnownLive(HTMLEditorRef())
-                          .DeleteNodeWithTransaction(*visibleNode);
-        if (NS_WARN_IF(!CanHandleEditAction())) {
-          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
-        }
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-          return EditActionResult(rv);
-        }
-        EditActionResult result =
-            WillDeleteSelection(aDirectionAndAmount, aStripWrappers);
-        NS_WARNING_ASSERTION(result.Succeeded(),
-                             "Nested WillDeleteSelection() failed");
-        return result;
-      }
-
-      // Special handling for backspace when positioned after <hr>
-      if (aDirectionAndAmount == nsIEditor::ePrevious &&
-          visibleNode->IsHTMLElement(nsGkAtoms::hr)) {
-        // Only if the caret is positioned at the end-of-hr-line position, we
-        // want to delete the <hr>.
-        //
-        // In other words, we only want to delete, if our selection position
-        // (indicated by startPoint) is the position directly
-        // after the <hr>, on the same line as the <hr>.
-        //
-        // To detect this case we check:
-        // startPoint's container == parentOfVisNode
-        // and
-        // startPoint's offset -1 == visibleNodeOffsetToVisNodeParent
-        // and
-        // interline position is false (left)
-        //
-        // In any other case we set the position to startPoint's container -1
-        // and interlineposition to false, only moving the caret to the
-        // end-of-hr-line position.
-        bool moveOnly = true;
-
-        EditorRawDOMPoint atHRElement(visibleNode);
-
-        ErrorResult err;
-        bool interLineIsRight = SelectionRefPtr()->GetInterlinePosition(err);
-        if (NS_WARN_IF(err.Failed())) {
-          return EditActionResult(err.StealNSResult());
-        }
-
-        if (startPoint.GetContainer() == atHRElement.GetContainer() &&
-            startPoint.Offset() - 1 == atHRElement.Offset() &&
-            !interLineIsRight) {
-          moveOnly = false;
-        }
-
-        if (moveOnly) {
-          // Go to the position after the <hr>, but to the end of the <hr> line
-          // by setting the interline position to left.
-          EditorDOMPoint atNextOfHRElement(visibleNode);
-          DebugOnly<bool> advanced = atNextOfHRElement.AdvanceOffset();
-          NS_WARNING_ASSERTION(advanced,
-                               "Failed to advance offset after <hr> element");
-
-          {
-            AutoEditorDOMPointChildInvalidator lockOffset(atNextOfHRElement);
-
-            IgnoredErrorResult ignoredError;
-            SelectionRefPtr()->Collapse(atNextOfHRElement, ignoredError);
-            if (NS_WARN_IF(!CanHandleEditAction())) {
-              return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
-            }
-            NS_WARNING_ASSERTION(
-                !ignoredError.Failed(),
-                "Failed to collapse selection at after the <hr>");
-          }
-
-          IgnoredErrorResult ignoredError;
-          SelectionRefPtr()->SetInterlinePosition(false, ignoredError);
-          NS_WARNING_ASSERTION(!ignoredError.Failed(),
-                               "Failed to unset interline position");
-          HTMLEditorRef()
-              .TopLevelEditSubActionDataRef()
-              .mDidExplicitlySetInterLine = true;
-
-          // There is one exception to the move only case.  If the <hr> is
-          // followed by a <br> we want to delete the <br>.
-
-          WSType otherWSType;
-          nsCOMPtr<nsINode> otherNode;
-
-          wsObj.NextVisibleNode(startPoint, address_of(otherNode), nullptr,
-                                &otherWSType);
-
-          if (otherWSType != WSType::br) {
-            return EditActionHandled();
-          }
-
-          // Delete the <br>
-          if (NS_WARN_IF(!otherNode->IsContent())) {
-            return EditActionHandled(NS_ERROR_FAILURE);
-          }
-          nsIContent* otherContent = otherNode->AsContent();
-          nsresult rv = WSRunObject::PrepareToDeleteNode(
-              MOZ_KnownLive(&HTMLEditorRef()), MOZ_KnownLive(otherContent));
-          if (NS_WARN_IF(!CanHandleEditAction())) {
-            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
-          }
-          if (NS_WARN_IF(NS_FAILED(rv))) {
-            return EditActionHandled(rv);
-          }
-          rv = MOZ_KnownLive(HTMLEditorRef())
-                   .DeleteNodeWithTransaction(MOZ_KnownLive(*otherContent));
-          if (NS_WARN_IF(!CanHandleEditAction())) {
-            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
-          }
-          NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
-                               "DeleteNodeWithTransaction() failed");
-          return EditActionHandled(rv);
-        }
-        // Else continue with normal delete code
-      }
-
-      if (NS_WARN_IF(!visibleNode->IsContent())) {
-        return EditActionResult(NS_ERROR_FAILURE);
-      }
-      // Found break or image, or hr.
-      nsresult rv = WSRunObject::PrepareToDeleteNode(
-          MOZ_KnownLive(&HTMLEditorRef()),
-          MOZ_KnownLive(visibleNode->AsContent()));
-      if (NS_WARN_IF(!CanHandleEditAction())) {
-        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
-      }
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return EditActionResult(rv);
-      }
-      // Remember sibling to visnode, if any
-      nsCOMPtr<nsIContent> previousEditableSibling =
-          HTMLEditorRef().GetPriorHTMLSibling(visibleNode);
-      // Delete the node, and join like nodes if appropriate
-      rv = MOZ_KnownLive(HTMLEditorRef())
-               .DeleteNodeWithTransaction(*visibleNode);
+    // Short circuit for invisible breaks.  delete them and recurse.
+    if (visibleNode->IsHTMLElement(nsGkAtoms::br) &&
+        !HTMLEditorRef().IsVisibleBRElement(visibleNode)) {
+      nsresult rv = MOZ_KnownLive(HTMLEditorRef())
+                        .DeleteNodeWithTransaction(*visibleNode);
       if (NS_WARN_IF(!CanHandleEditAction())) {
         return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
       }
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return EditActionResult(rv);
       }
-      // Is there a prior node and are they siblings?
-      nsCOMPtr<nsINode> nextEditableSibling;
-      if (previousEditableSibling) {
-        nextEditableSibling =
-            HTMLEditorRef().GetNextHTMLSibling(previousEditableSibling);
-      }
-      // Are they both text nodes?  If so, join them!
-      if (startPoint.GetContainer() == nextEditableSibling &&
-          startPoint.GetContainerAsText() &&
-          previousEditableSibling->GetAsText()) {
-        EditorDOMPoint atFirstChildOfRightNode;
-        nsresult rv =
-            MOZ_KnownLive(HTMLEditorRef())
-                .JoinNearestEditableNodesWithTransaction(
-                    *previousEditableSibling,
-                    MOZ_KnownLive(*startPoint.GetContainerAsContent()),
-                    &atFirstChildOfRightNode);
+      EditActionResult result =
+          WillDeleteSelection(aDirectionAndAmount, aStripWrappers);
+      NS_WARNING_ASSERTION(result.Succeeded(),
+                           "Nested WillDeleteSelection() failed");
+      return result;
+    }
+
+    // Special handling for backspace when positioned after <hr>
+    if (aDirectionAndAmount == nsIEditor::ePrevious &&
+        visibleNode->IsHTMLElement(nsGkAtoms::hr)) {
+      // Only if the caret is positioned at the end-of-hr-line position, we
+      // want to delete the <hr>.
+      //
+      // In other words, we only want to delete, if our selection position
+      // (indicated by startPoint) is the position directly
+      // after the <hr>, on the same line as the <hr>.
+      //
+      // To detect this case we check:
+      // startPoint's container == parentOfVisNode
+      // and
+      // startPoint's offset -1 == visibleNodeOffsetToVisNodeParent
+      // and
+      // interline position is false (left)
+      //
+      // In any other case we set the position to startPoint's container -1
+      // and interlineposition to false, only moving the caret to the
+      // end-of-hr-line position.
+      bool moveOnly = true;
+
+      EditorRawDOMPoint atHRElement(visibleNode);
+
+      ErrorResult err;
+      bool interLineIsRight = SelectionRefPtr()->GetInterlinePosition(err);
+      if (NS_WARN_IF(err.Failed())) {
+        return EditActionResult(err.StealNSResult());
+      }
+
+      if (startPoint.GetContainer() == atHRElement.GetContainer() &&
+          startPoint.Offset() - 1 == atHRElement.Offset() &&
+          !interLineIsRight) {
+        moveOnly = false;
+      }
+
+      if (moveOnly) {
+        // Go to the position after the <hr>, but to the end of the <hr> line
+        // by setting the interline position to left.
+        EditorDOMPoint atNextOfHRElement(visibleNode);
+        DebugOnly<bool> advanced = atNextOfHRElement.AdvanceOffset();
+        NS_WARNING_ASSERTION(advanced,
+                             "Failed to advance offset after <hr> element");
+
+        {
+          AutoEditorDOMPointChildInvalidator lockOffset(atNextOfHRElement);
+
+          IgnoredErrorResult ignoredError;
+          SelectionRefPtr()->Collapse(atNextOfHRElement, ignoredError);
+          if (NS_WARN_IF(!CanHandleEditAction())) {
+            return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+          }
+          NS_WARNING_ASSERTION(
+              !ignoredError.Failed(),
+              "Failed to collapse selection at after the <hr>");
+        }
+
+        IgnoredErrorResult ignoredError;
+        SelectionRefPtr()->SetInterlinePosition(false, ignoredError);
+        NS_WARNING_ASSERTION(!ignoredError.Failed(),
+                             "Failed to unset interline position");
+        HTMLEditorRef()
+            .TopLevelEditSubActionDataRef()
+            .mDidExplicitlySetInterLine = true;
+
+        // There is one exception to the move only case.  If the <hr> is
+        // followed by a <br> we want to delete the <br>.
+
+        WSType otherWSType;
+        nsCOMPtr<nsINode> otherNode;
+
+        wsObj.NextVisibleNode(startPoint, address_of(otherNode), nullptr,
+                              &otherWSType);
+
+        if (otherWSType != WSType::br) {
+          return EditActionHandled();
+        }
+
+        // Delete the <br>
+        if (NS_WARN_IF(!otherNode->IsContent())) {
+          return EditActionHandled(NS_ERROR_FAILURE);
+        }
+        nsIContent* otherContent = otherNode->AsContent();
+        nsresult rv = WSRunObject::PrepareToDeleteNode(
+            MOZ_KnownLive(&HTMLEditorRef()), MOZ_KnownLive(otherContent));
+        if (NS_WARN_IF(!CanHandleEditAction())) {
+          return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+        }
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return EditActionHandled(rv);
         }
-        if (NS_WARN_IF(!atFirstChildOfRightNode.IsSet())) {
-          return EditActionHandled(NS_ERROR_FAILURE);
-        }
-        // Fix up selection
-        ErrorResult error;
-        SelectionRefPtr()->Collapse(atFirstChildOfRightNode, error);
-        if (NS_WARN_IF(!CanHandleEditAction())) {
-          error.SuppressException();
-          return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
-        }
-        if (NS_WARN_IF(error.Failed())) {
-          return EditActionHandled(error.StealNSResult());
-        }
-      }
-      rv = MOZ_KnownLive(HTMLEditorRef())
-               .InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
-                   EditorBase::GetStartPoint(*SelectionRefPtr()));
-      NS_WARNING_ASSERTION(
-          NS_SUCCEEDED(rv),
-          "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed");
-      return EditActionHandled(rv);
-    }
-
-    if (wsType == WSType::otherBlock) {
-      // Make sure it's not a table element.  If so, cancel the operation
-      // (translation: users cannot backspace or delete across table cells)
-      if (HTMLEditUtils::IsTableElement(visibleNode)) {
-        return EditActionCanceled();
-      }
-
-      // Next to a block.  See if we are between a block and a br.  If so, we
-      // really want to delete the br.  Else join content at selection to the
-      // block.
-      WSType otherWSType;
-      nsCOMPtr<nsINode> otherNode;
-
-      // Find node in other direction
-      if (aDirectionAndAmount == nsIEditor::eNext) {
-        wsObj.PriorVisibleNode(startPoint, address_of(otherNode), nullptr,
-                               &otherWSType);
-      } else {
-        wsObj.NextVisibleNode(startPoint, address_of(otherNode), nullptr,
-                              &otherWSType);
-      }
-
-      // First find the adjacent node in the block
-      nsCOMPtr<nsIContent> leafNode;
-      nsCOMPtr<nsINode> leftNode, rightNode;
-      if (aDirectionAndAmount == nsIEditor::ePrevious) {
-        leafNode = HTMLEditorRef().GetLastEditableLeaf(*visibleNode);
-        leftNode = leafNode;
-        rightNode = startPoint.GetContainer();
-      } else {
-        leafNode = HTMLEditorRef().GetFirstEditableLeaf(*visibleNode);
-        leftNode = startPoint.GetContainer();
-        rightNode = leafNode;
-      }
-
-      bool didBRElementDeleted = false;
-      if (otherNode->IsHTMLElement(nsGkAtoms::br)) {
-        nsresult rv = MOZ_KnownLive(HTMLEditorRef())
-                          .DeleteNodeWithTransaction(*otherNode);
-        if (NS_WARN_IF(!CanHandleEditAction())) {
-          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
-        }
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-          return EditActionResult(rv);
-        }
-        didBRElementDeleted = true;
-      }
-
-      // Don't cross table boundaries
-      if (leftNode && rightNode &&
-          HTMLEditor::NodesInDifferentTableElements(*leftNode, *rightNode)) {
-        return didBRElementDeleted ? EditActionHandled() : EditActionIgnored();
-      }
-
-      if (didBRElementDeleted) {
-        // Put selection at edge of block and we are done.
-        if (NS_WARN_IF(!leafNode)) {
-          return EditActionHandled(NS_ERROR_FAILURE);
-        }
-        EditorDOMPoint newSel = HTMLEditorRef().GetGoodCaretPointFor(
-            *leafNode, aDirectionAndAmount);
-        if (NS_WARN_IF(!newSel.IsSet())) {
-          return EditActionHandled(NS_ERROR_FAILURE);
-        }
-        IgnoredErrorResult error;
-        SelectionRefPtr()->Collapse(newSel, error);
+        rv = MOZ_KnownLive(HTMLEditorRef())
+                 .DeleteNodeWithTransaction(MOZ_KnownLive(*otherContent));
         if (NS_WARN_IF(!CanHandleEditAction())) {
           return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
         }
-        NS_WARNING_ASSERTION(!error.Failed(),
-                             "Selection::Collapse() failed, but ignored");
-        return EditActionHandled();
-      }
-
-      // Else we are joining content to block
-      EditActionResult result(NS_OK);
-      EditorDOMPoint pointToPutCaret(startPoint);
-      {
-        AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
-                                  &pointToPutCaret);
-        if (NS_WARN_IF(!leftNode) || NS_WARN_IF(!leftNode->IsContent()) ||
-            NS_WARN_IF(!rightNode) || NS_WARN_IF(!rightNode->IsContent())) {
-          return EditActionResult(NS_ERROR_FAILURE);
-        }
-        result |= MOZ_KnownLive(HTMLEditorRef())
-                      .TryToJoinBlocksWithTransaction(
-                          MOZ_KnownLive(*leftNode->AsContent()),
-                          MOZ_KnownLive(*rightNode->AsContent()));
-        if (NS_WARN_IF(result.Failed())) {
-          return result;
-        }
-      }
-
-      // If TryToJoinBlocksWithTransaction() didn't handle it  and it's not
-      // canceled, user may want to modify the start leaf node or the last leaf
-      // node of the block.
-      if (!result.Handled() && !result.Canceled() &&
-          leafNode != startPoint.GetContainer()) {
-        int32_t offset = aDirectionAndAmount == nsIEditor::ePrevious
-                             ? static_cast<int32_t>(leafNode->Length())
-                             : 0;
-        DebugOnly<nsresult> rv = SelectionRefPtr()->Collapse(leafNode, offset);
-        if (NS_WARN_IF(!CanHandleEditAction())) {
-          return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
-        }
         NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
-                             "Selection::Collapse() failed, but ignored");
-        EditActionResult result =
-            WillDeleteSelection(aDirectionAndAmount, aStripWrappers);
-        NS_WARNING_ASSERTION(result.Succeeded(),
-                             "Nested WillDeleteSelection() failed");
+                             "DeleteNodeWithTransaction() failed");
+        return EditActionHandled(rv);
+      }
+      // Else continue with normal delete code
+    }
+
+    if (NS_WARN_IF(!visibleNode->IsContent())) {
+      return EditActionResult(NS_ERROR_FAILURE);
+    }
+    // Found break or image, or hr.
+    nsresult rv = WSRunObject::PrepareToDeleteNode(
+        MOZ_KnownLive(&HTMLEditorRef()),
+        MOZ_KnownLive(visibleNode->AsContent()));
+    if (NS_WARN_IF(!CanHandleEditAction())) {
+      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+    }
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return EditActionResult(rv);
+    }
+    // Remember sibling to visnode, if any
+    nsCOMPtr<nsIContent> previousEditableSibling =
+        HTMLEditorRef().GetPriorHTMLSibling(visibleNode);
+    // Delete the node, and join like nodes if appropriate
+    rv = MOZ_KnownLive(HTMLEditorRef()).DeleteNodeWithTransaction(*visibleNode);
+    if (NS_WARN_IF(!CanHandleEditAction())) {
+      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+    }
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return EditActionResult(rv);
+    }
+    // Is there a prior node and are they siblings?
+    nsCOMPtr<nsINode> nextEditableSibling;
+    if (previousEditableSibling) {
+      nextEditableSibling =
+          HTMLEditorRef().GetNextHTMLSibling(previousEditableSibling);
+    }
+    // Are they both text nodes?  If so, join them!
+    if (startPoint.GetContainer() == nextEditableSibling &&
+        startPoint.GetContainerAsText() &&
+        previousEditableSibling->GetAsText()) {
+      EditorDOMPoint atFirstChildOfRightNode;
+      nsresult rv = MOZ_KnownLive(HTMLEditorRef())
+                        .JoinNearestEditableNodesWithTransaction(
+                            *previousEditableSibling,
+                            MOZ_KnownLive(*startPoint.GetContainerAsContent()),
+                            &atFirstChildOfRightNode);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return EditActionHandled(rv);
+      }
+      if (NS_WARN_IF(!atFirstChildOfRightNode.IsSet())) {
+        return EditActionHandled(NS_ERROR_FAILURE);
+      }
+      // Fix up selection
+      ErrorResult error;
+      SelectionRefPtr()->Collapse(atFirstChildOfRightNode, error);
+      if (NS_WARN_IF(!CanHandleEditAction())) {
+        error.SuppressException();
+        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+      }
+      if (NS_WARN_IF(error.Failed())) {
+        return EditActionHandled(error.StealNSResult());
+      }
+    }
+    rv = MOZ_KnownLive(HTMLEditorRef())
+             .InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+                 EditorBase::GetStartPoint(*SelectionRefPtr()));
+    NS_WARNING_ASSERTION(
+        NS_SUCCEEDED(rv),
+        "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed");
+    return EditActionHandled(rv);
+  }
+
+  if (wsType == WSType::otherBlock) {
+    // Make sure it's not a table element.  If so, cancel the operation
+    // (translation: users cannot backspace or delete across table cells)
+    if (HTMLEditUtils::IsTableElement(visibleNode)) {
+      return EditActionCanceled();
+    }
+
+    // Next to a block.  See if we are between a block and a br.  If so, we
+    // really want to delete the br.  Else join content at selection to the
+    // block.
+    WSType otherWSType;
+    nsCOMPtr<nsINode> otherNode;
+
+    // Find node in other direction
+    if (aDirectionAndAmount == nsIEditor::eNext) {
+      wsObj.PriorVisibleNode(startPoint, address_of(otherNode), nullptr,
+                             &otherWSType);
+    } else {
+      wsObj.NextVisibleNode(startPoint, address_of(otherNode), nullptr,
+                            &otherWSType);
+    }
+
+    // First find the adjacent node in the block
+    nsCOMPtr<nsIContent> leafNode;
+    nsCOMPtr<nsINode> leftNode, rightNode;
+    if (aDirectionAndAmount == nsIEditor::ePrevious) {
+      leafNode = HTMLEditorRef().GetLastEditableLeaf(*visibleNode);
+      leftNode = leafNode;
+      rightNode = startPoint.GetContainer();
+    } else {
+      leafNode = HTMLEditorRef().GetFirstEditableLeaf(*visibleNode);
+      leftNode = startPoint.GetContainer();
+      rightNode = leafNode;
+    }
+
+    bool didBRElementDeleted = false;
+    if (otherNode->IsHTMLElement(nsGkAtoms::br)) {
+      nsresult rv =
+          MOZ_KnownLive(HTMLEditorRef()).DeleteNodeWithTransaction(*otherNode);
+      if (NS_WARN_IF(!CanHandleEditAction())) {
+        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+      }
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return EditActionResult(rv);
+      }
+      didBRElementDeleted = true;
+    }
+
+    // Don't cross table boundaries
+    if (leftNode && rightNode &&
+        HTMLEditor::NodesInDifferentTableElements(*leftNode, *rightNode)) {
+      return didBRElementDeleted ? EditActionHandled() : EditActionIgnored();
+    }
+
+    if (didBRElementDeleted) {
+      // Put selection at edge of block and we are done.
+      if (NS_WARN_IF(!leafNode)) {
+        return EditActionHandled(NS_ERROR_FAILURE);
+      }
+      EditorDOMPoint newSel =
+          HTMLEditorRef().GetGoodCaretPointFor(*leafNode, aDirectionAndAmount);
+      if (NS_WARN_IF(!newSel.IsSet())) {
+        return EditActionHandled(NS_ERROR_FAILURE);
+      }
+      IgnoredErrorResult error;
+      SelectionRefPtr()->Collapse(newSel, error);
+      if (NS_WARN_IF(!CanHandleEditAction())) {
+        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+      }
+      NS_WARNING_ASSERTION(!error.Failed(),
+                           "Selection::Collapse() failed, but ignored");
+      return EditActionHandled();
+    }
+
+    // Else we are joining content to block
+    EditActionResult result(NS_OK);
+    EditorDOMPoint pointToPutCaret(startPoint);
+    {
+      AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
+                                &pointToPutCaret);
+      if (NS_WARN_IF(!leftNode) || NS_WARN_IF(!leftNode->IsContent()) ||
+          NS_WARN_IF(!rightNode) || NS_WARN_IF(!rightNode->IsContent())) {
+        return EditActionResult(NS_ERROR_FAILURE);
+      }
+      result |= MOZ_KnownLive(HTMLEditorRef())
+                    .TryToJoinBlocksWithTransaction(
+                        MOZ_KnownLive(*leftNode->AsContent()),
+                        MOZ_KnownLive(*rightNode->AsContent()));
+      if (NS_WARN_IF(result.Failed())) {
         return result;
       }
-
-      // Otherwise, we must have deleted the selection as user expected.
-      IgnoredErrorResult ignoredError;
-      SelectionRefPtr()->Collapse(pointToPutCaret, ignoredError);
+    }
+
+    // If TryToJoinBlocksWithTransaction() didn't handle it  and it's not
+    // canceled, user may want to modify the start leaf node or the last leaf
+    // node of the block.
+    if (!result.Handled() && !result.Canceled() &&
+        leafNode != startPoint.GetContainer()) {
+      int32_t offset = aDirectionAndAmount == nsIEditor::ePrevious
+                           ? static_cast<int32_t>(leafNode->Length())
+                           : 0;
+      DebugOnly<nsresult> rv = SelectionRefPtr()->Collapse(leafNode, offset);
       if (NS_WARN_IF(!CanHandleEditAction())) {
         return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
       }
-      NS_WARNING_ASSERTION(!ignoredError.Failed(),
-                           "Failed to selection at deleted point");
+      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+                           "Selection::Collapse() failed, but ignored");
+      EditActionResult result =
+          WillDeleteSelection(aDirectionAndAmount, aStripWrappers);
+      NS_WARNING_ASSERTION(result.Succeeded(),
+                           "Nested WillDeleteSelection() failed");
       return result;
     }
 
-    if (wsType == WSType::thisBlock) {
-      // At edge of our block.  Look beside it and see if we can join to an
-      // adjacent block
-
-      // Make sure it's not a table element.  If so, cancel the operation
-      // (translation: users cannot backspace or delete across table cells)
-      if (HTMLEditUtils::IsTableElement(visibleNode)) {
-        return EditActionCanceled();
-      }
-
-      // First find the relevant nodes
-      nsCOMPtr<nsINode> leftNode, rightNode;
-      if (aDirectionAndAmount == nsIEditor::ePrevious) {
-        leftNode = HTMLEditorRef().GetPreviousEditableHTMLNode(*visibleNode);
-        rightNode = startPoint.GetContainer();
-      } else {
-        rightNode = HTMLEditorRef().GetNextEditableHTMLNode(*visibleNode);
-        leftNode = startPoint.GetContainer();
-      }
-
-      // Nothing to join
-      if (!leftNode || !rightNode) {
-        return EditActionCanceled();
-      }
-
-      // Don't cross table boundaries -- cancel it
-      if (HTMLEditor::NodesInDifferentTableElements(*leftNode, *rightNode)) {
-        return EditActionCanceled();
-      }
-
-      EditActionResult result(NS_OK);
-      EditorDOMPoint pointToPutCaret(startPoint);
-      {
-        AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
-                                  &pointToPutCaret);
-        if (NS_WARN_IF(!leftNode->IsContent()) ||
-            NS_WARN_IF(!rightNode->IsContent())) {
-          return EditActionResult(NS_ERROR_FAILURE);
-        }
-        result |= MOZ_KnownLive(HTMLEditorRef())
-                      .TryToJoinBlocksWithTransaction(
-                          MOZ_KnownLive(*leftNode->AsContent()),
-                          MOZ_KnownLive(*rightNode->AsContent()));
-        // This should claim that trying to join the block means that
-        // this handles the action because the caller shouldn't do anything
-        // anymore in this case.
-        result.MarkAsHandled();
-        if (NS_WARN_IF(result.Failed())) {
-          return result;
-        }
-      }
-      IgnoredErrorResult ignoredError;
-      SelectionRefPtr()->Collapse(pointToPutCaret, ignoredError);
-      if (NS_WARN_IF(!CanHandleEditAction())) {
-        return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
-      }
-      NS_WARNING_ASSERTION(!ignoredError.Failed(),
-                           "Failed to collapse selection");
-      return result;
-    }
-  }
+    // Otherwise, we must have deleted the selection as user expected.
+    IgnoredErrorResult ignoredError;
+    SelectionRefPtr()->Collapse(pointToPutCaret, ignoredError);
+    if (NS_WARN_IF(!CanHandleEditAction())) {
+      return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
+    }
+    NS_WARNING_ASSERTION(!ignoredError.Failed(),
+                         "Failed to selection at deleted point");
+    return result;
+  }
+
+  if (wsType == WSType::thisBlock) {
+    // At edge of our block.  Look beside it and see if we can join to an
+    // adjacent block
+
+    // Make sure it's not a table element.  If so, cancel the operation
+    // (translation: users cannot backspace or delete across table cells)
+    if (HTMLEditUtils::IsTableElement(visibleNode)) {
+      return EditActionCanceled();
+    }
+
+    // First find the relevant nodes
+    nsCOMPtr<nsINode> leftNode, rightNode;
+    if (aDirectionAndAmount == nsIEditor::ePrevious) {
+      leftNode = HTMLEditorRef().GetPreviousEditableHTMLNode(*visibleNode);
+      rightNode = startPoint.GetContainer();
+    } else {
+      rightNode = HTMLEditorRef().GetNextEditableHTMLNode(*visibleNode);
+      leftNode = startPoint.GetContainer();
+    }
+
+    // Nothing to join
+    if (!leftNode || !rightNode) {
+      return EditActionCanceled();
+    }
+
+    // Don't cross table boundaries -- cancel it
+    if (HTMLEditor::NodesInDifferentTableElements(*leftNode, *rightNode)) {
+      return EditActionCanceled();
+    }
+
+    EditActionResult result(NS_OK);
+    EditorDOMPoint pointToPutCaret(startPoint);
+    {
+      AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
+                                &pointToPutCaret);
+      if (NS_WARN_IF(!leftNode->IsContent()) ||
+          NS_WARN_IF(!rightNode->IsContent())) {
+        return EditActionResult(NS_ERROR_FAILURE);
+      }
+      result |= MOZ_KnownLive(HTMLEditorRef())
+                    .TryToJoinBlocksWithTransaction(
+                        MOZ_KnownLive(*leftNode->AsContent()),
+                        MOZ_KnownLive(*rightNode->AsContent()));
+      // This should claim that trying to join the block means that
+      // this handles the action because the caller shouldn't do anything
+      // anymore in this case.
+      result.MarkAsHandled();
+      if (NS_WARN_IF(result.Failed())) {
+        return result;
+      }
+    }
+    IgnoredErrorResult ignoredError;
+    SelectionRefPtr()->Collapse(pointToPutCaret, ignoredError);
+    if (NS_WARN_IF(!CanHandleEditAction())) {
+      return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
+    }
+    NS_WARNING_ASSERTION(!ignoredError.Failed(),
+                         "Failed to collapse selection");
+    return result;
+  }
+
+  MOZ_ASSERT_UNREACHABLE("New WSType value hasn't been handled yet");
+  return EditActionIgnored();
+}
+
+EditActionResult HTMLEditRules::HandleDeleteNonCollapsedSelection(
+    nsIEditor::EDirection aDirectionAndAmount,
+    nsIEditor::EStripWrappers aStripWrappers,
+    SelectionWasCollapsed aSelectionWasCollapsed) {
+  MOZ_ASSERT(IsEditorDataAvailable());
+  MOZ_ASSERT(HTMLEditorRef().IsTopLevelEditSubActionDataAvailable());
+  MOZ_ASSERT(!SelectionRefPtr()->IsCollapsed());
 
   // Else we have a non-collapsed selection.  First adjust the selection.
   // XXX Why do we extend selection only when there is only one range?
   if (SelectionRefPtr()->RangeCount() == 1) {
     if (nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0)) {
       RefPtr<StaticRange> extendedRange =
           MOZ_KnownLive(HTMLEditorRef())
               .GetExtendedRangeToIncludeInvisibleNodes(*firstRange);
@@ -3136,17 +3167,17 @@ EditActionResult HTMLEditRules::WillDele
               return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
             }
             NS_WARNING_ASSERTION(
                 NS_SUCCEEDED(rv),
                 "Failed to elements except table related elements");
             arrayOfNodes.RemoveElementAt(0);
             // If something visible is deleted, no need to join.  Visible means
             // all nodes except non-visible textnodes and breaks.
-            if (join && origCollapsed) {
+            if (join && aSelectionWasCollapsed == SelectionWasCollapsed::Yes) {
               if (!node->IsContent()) {
                 join = false;
                 continue;
               }
               nsIContent* content = node->AsContent();
               if (Text* text = content->GetAsText()) {
                 join = !HTMLEditorRef().IsInVisibleTextFrames(*text);
               } else {
@@ -3273,17 +3304,17 @@ EditActionResult HTMLEditRules::WillDele
     nsresult rv = SelectionRefPtr()->Collapse(endNode, endOffset);
     if (NS_WARN_IF(!CanHandleEditAction())) {
       return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
     }
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Selection::Collapse() failed");
     return result.SetResult(rv);
   }
 
-  rv = SelectionRefPtr()->Collapse(startNode, startOffset);
+  nsresult rv = SelectionRefPtr()->Collapse(startNode, startOffset);
   if (NS_WARN_IF(!CanHandleEditAction())) {
     return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
   }
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Selection::Collapse() failed");
   return result.SetResult(rv);
 }
 
 nsresult HTMLEditor::DeleteNodeIfInvisibleAndEditableTextNode(nsINode& aNode) {
--- a/editor/libeditor/HTMLEditRules.h
+++ b/editor/libeditor/HTMLEditRules.h
@@ -125,16 +125,49 @@ class HTMLEditRules : public TextEditRul
    * @param aDirectionAndAmount Direction of the deletion.
    * @param aStripWrappers      Must be eStrip or eNoStrip.
    */
   MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE EditActionResult
   WillDeleteSelection(nsIEditor::EDirection aDirectionAndAmount,
                       nsIEditor::EStripWrappers aStripWrappers);
 
   /**
+   * HandleDeleteAroundCollapsedSelection() handles deletion with collapsed
+   * `Selection`.  Callers must guarantee that this is called only when
+   * `Selection` is collapsed.
+   *
+   * @param aDirectionAndAmount Direction of the deletion.
+   * @param aStripWrappers      Must be eStrip or eNoStrip.
+   */
+  MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE EditActionResult
+  HandleDeleteAroundCollapsedSelection(
+      nsIEditor::EDirection aDirectionAndAmount,
+      nsIEditor::EStripWrappers aStripWrappers);
+
+  /**
+   * HandleDeleteNonCollapsedSelection() handles deletion with non-collapsed
+   * `Selection`.  Callers must guarantee that this is called only when
+   * `Selection` is NOT collapsed.
+   *
+   * @param aDirectionAndAmount         Direction of the deletion.
+   * @param aStripWrappers              Must be eStrip or eNoStrip.
+   * @param aSelectionWasCollpased      If the caller extended `Selection`
+   *                                    from collapsed, set this to `Yes`.
+   *                                    Otherwise, i.e., `Selection` is not
+   *                                    collapsed from the beginning, set
+   *                                    this to `No`.
+   */
+  enum class SelectionWasCollapsed { Yes, No };
+  MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE EditActionResult
+  HandleDeleteNonCollapsedSelection(
+      nsIEditor::EDirection aDirectionAndAmount,
+      nsIEditor::EStripWrappers aStripWrappers,
+      SelectionWasCollapsed aSelectionWasCollapsed);
+
+  /**
    * Called after deleting selected content.
    * This method removes unnecessary empty nodes and/or inserts <br> if
    * necessary.
    */
   MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE nsresult DidDeleteSelection();
 
   /**
    * DeleteElementsExceptTableRelatedElements() removes elements except