tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

DeleteRangeTransaction.cpp (14865B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 #include "DeleteRangeTransaction.h"
      7 
      8 #include "DeleteContentTransactionBase.h"
      9 #include "DeleteNodeTransaction.h"
     10 #include "DeleteTextTransaction.h"
     11 #include "EditTransactionBase.h"
     12 #include "EditorBase.h"
     13 #include "EditorDOMPoint.h"
     14 #include "EditorUtils.h"
     15 #include "HTMLEditUtils.h"
     16 
     17 #include "mozilla/Assertions.h"
     18 #include "mozilla/ContentIterator.h"
     19 #include "mozilla/Logging.h"
     20 #include "mozilla/mozalloc.h"
     21 #include "mozilla/RangeBoundary.h"
     22 #include "mozilla/StaticPrefs_editor.h"
     23 #include "mozilla/dom/Selection.h"
     24 
     25 #include "nsAtom.h"
     26 #include "nsCOMPtr.h"
     27 #include "nsDebug.h"
     28 #include "nsError.h"
     29 #include "nsGkAtoms.h"
     30 #include "nsIContent.h"
     31 #include "nsINode.h"
     32 #include "nsAString.h"
     33 
     34 namespace mozilla {
     35 
     36 using namespace dom;
     37 
     38 using EditorType = EditorUtils::EditorType;
     39 
     40 DeleteRangeTransaction::DeleteRangeTransaction(EditorBase& aEditorBase,
     41                                               const nsRange& aRangeToDelete)
     42    : mEditorBase(&aEditorBase), mRangeToDelete(aRangeToDelete.CloneRange()) {}
     43 
     44 NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteRangeTransaction,
     45                                   EditAggregateTransaction, mEditorBase,
     46                                   mRangeToDelete, mPointToPutCaret)
     47 
     48 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteRangeTransaction)
     49 NS_INTERFACE_MAP_END_INHERITING(EditAggregateTransaction)
     50 
     51 void DeleteRangeTransaction::AppendChild(
     52    DeleteContentTransactionBase& aTransaction) {
     53  mChildren.AppendElement(aTransaction);
     54 }
     55 
     56 nsresult
     57 DeleteRangeTransaction::MaybeExtendDeletingRangeWithSurroundingWhitespace(
     58    nsRange& aRange) const {
     59  if (!mEditorBase->mEditActionData->SelectionCreatedByDoubleclick() ||
     60      !StaticPrefs::
     61          editor_word_select_delete_space_after_doubleclick_selection()) {
     62    return NS_OK;
     63  }
     64  EditorRawDOMPoint startPoint(aRange.StartRef());
     65  EditorRawDOMPoint endPoint(aRange.EndRef());
     66  const bool maybeRangeStartsAfterWhiteSpace =
     67      startPoint.IsInTextNode() && !startPoint.IsStartOfContainer();
     68  const bool maybeRangeEndsAtWhiteSpace =
     69      endPoint.IsInTextNode() && !endPoint.IsEndOfContainer();
     70 
     71  if (!maybeRangeStartsAfterWhiteSpace && !maybeRangeEndsAtWhiteSpace) {
     72    // no whitespace before or after word => nothing to do here.
     73    return NS_OK;
     74  }
     75 
     76  const bool precedingCharIsWhitespace =
     77      maybeRangeStartsAfterWhiteSpace
     78          ? startPoint.IsPreviousCharASCIISpaceOrNBSP()
     79          : false;
     80  const bool trailingCharIsWhitespace =
     81      maybeRangeEndsAtWhiteSpace ? endPoint.IsCharASCIISpaceOrNBSP() : false;
     82 
     83  // if possible, try to remove the preceding whitespace
     84  // so the caret is at the end of the previous word.
     85  if (precedingCharIsWhitespace) {
     86    // "one [two]", "one [two] three" or "one [two], three"
     87    ErrorResult err;
     88    aRange.SetStart(startPoint.PreviousPoint(), err);
     89    if (auto rv = err.StealNSResult(); NS_FAILED(rv)) {
     90      NS_WARNING(
     91          "DeleteRangeTransaction::"
     92          "MaybeExtendDeletingRangeWithSurroundingWhitespace"
     93          " failed to update the start of the deleting range");
     94      return rv;
     95    }
     96  } else if (trailingCharIsWhitespace) {
     97    // "[one] two"
     98    ErrorResult err;
     99    aRange.SetEnd(endPoint.NextPoint(), err);
    100    if (auto rv = err.StealNSResult(); NS_FAILED(rv)) {
    101      NS_WARNING(
    102          "DeleteRangeTransaction::"
    103          "MaybeExtendDeletingRangeWithSurroundingWhitespace"
    104          " failed to update the end of the deleting range");
    105      return rv;
    106    }
    107  }
    108 
    109  return NS_OK;
    110 }
    111 
    112 NS_IMETHODIMP DeleteRangeTransaction::DoTransaction() {
    113  MOZ_LOG(GetLogModule(), LogLevel::Info,
    114          ("%p DeleteRangeTransaction::%s this={ mName=%s } "
    115           "Start==============================",
    116           this, __FUNCTION__,
    117           nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
    118 
    119  if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mRangeToDelete)) {
    120    return NS_ERROR_NOT_AVAILABLE;
    121  }
    122 
    123  // Swap mRangeToDelete out into a stack variable, so we make sure to null it
    124  // out on return from this function.  Once this function returns, we no longer
    125  // need mRangeToDelete, and keeping it alive in the long term slows down all
    126  // DOM mutations because it's observing them.
    127  RefPtr<nsRange> rangeToDelete;
    128  rangeToDelete.swap(mRangeToDelete);
    129 
    130  MaybeExtendDeletingRangeWithSurroundingWhitespace(*rangeToDelete);
    131 
    132  // build the child transactions
    133  // XXX We should move this to the constructor.  Then, we don't need to make
    134  //     this class has mRangeToDelete with nullptr since transaction instances
    135  //     may be created a lot and live long.
    136  {
    137    EditorRawDOMRange extendedRange(*rangeToDelete);
    138    MOZ_ASSERT(extendedRange.IsPositionedAndValid());
    139 
    140    if (extendedRange.InSameContainer()) {
    141      // the selection begins and ends in the same node
    142      nsresult rv = AppendTransactionsToDeleteIn(extendedRange);
    143      if (NS_FAILED(rv)) {
    144        NS_WARNING(
    145            "DeleteRangeTransaction::AppendTransactionsToDeleteIn() failed");
    146        return rv;
    147      }
    148    } else {
    149      // If the range ends at end of a node, we may need to extend the range to
    150      // make ContentSubtreeIterator iterates close tag of the unnecessary nodes
    151      // in AppendTransactionsToDeleteNodesEntirelyIn.
    152      for (EditorRawDOMPoint endOfRange = extendedRange.EndRef();
    153           endOfRange.IsInContentNode() && endOfRange.IsEndOfContainer() &&
    154           endOfRange.GetContainer() != extendedRange.StartRef().GetContainer();
    155           endOfRange = extendedRange.EndRef()) {
    156        extendedRange.SetEnd(
    157            EditorRawDOMPoint::After(*endOfRange.ContainerAs<nsIContent>()));
    158      }
    159 
    160      // the selection ends in a different node from where it started.  delete
    161      // the relevant content in the start node
    162      nsresult rv = AppendTransactionToDeleteText(extendedRange.StartRef(),
    163                                                  nsIEditor::eNext);
    164      if (NS_FAILED(rv)) {
    165        NS_WARNING(
    166            "DeleteRangeTransaction::AppendTransactionToDeleteText() failed");
    167        return rv;
    168      }
    169      // delete the intervening nodes
    170      rv = AppendTransactionsToDeleteNodesWhoseEndBoundaryIn(extendedRange);
    171      if (NS_FAILED(rv)) {
    172        NS_WARNING(
    173            "DeleteRangeTransaction::"
    174            "AppendTransactionsToDeleteNodesWhoseEndBoundaryIn() failed");
    175        return rv;
    176      }
    177      // delete the relevant content in the end node
    178      rv = AppendTransactionToDeleteText(extendedRange.EndRef(),
    179                                         nsIEditor::ePrevious);
    180      if (NS_FAILED(rv)) {
    181        NS_WARNING(
    182            "DeleteRangeTransaction::AppendTransactionToDeleteText() failed");
    183        return rv;
    184      }
    185    }
    186  }
    187 
    188  // if we've successfully built this aggregate transaction, then do it.
    189  nsresult rv = EditAggregateTransaction::DoTransaction();
    190  if (NS_FAILED(rv)) {
    191    NS_WARNING("EditAggregateTransaction::DoTransaction() failed");
    192    return rv;
    193  }
    194 
    195  MOZ_LOG(GetLogModule(), LogLevel::Info,
    196          ("%p DeleteRangeTransaction::%s this={ mName=%s } "
    197           "End==============================",
    198           this, __FUNCTION__,
    199           nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
    200 
    201  mPointToPutCaret = rangeToDelete->StartRef();
    202  if (MOZ_UNLIKELY(!mPointToPutCaret.IsSetAndValid())) {
    203    for (const OwningNonNull<EditTransactionBase>& transaction :
    204         Reversed(mChildren)) {
    205      if (const DeleteContentTransactionBase* deleteContentTransaction =
    206              transaction->GetAsDeleteContentTransactionBase()) {
    207        mPointToPutCaret = deleteContentTransaction->SuggestPointToPutCaret();
    208        if (mPointToPutCaret.IsSetAndValid()) {
    209          break;
    210        }
    211        continue;
    212      }
    213      MOZ_ASSERT_UNREACHABLE(
    214          "Child transactions must be DeleteContentTransactionBase");
    215    }
    216  }
    217  return NS_OK;
    218 }
    219 
    220 NS_IMETHODIMP DeleteRangeTransaction::UndoTransaction() {
    221  MOZ_LOG(GetLogModule(), LogLevel::Info,
    222          ("%p DeleteRangeTransaction::%s this={ mName=%s } "
    223           "Start==============================",
    224           this, __FUNCTION__,
    225           nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
    226 
    227  nsresult rv = EditAggregateTransaction::UndoTransaction();
    228  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    229                       "EditAggregateTransaction::UndoTransaction() failed");
    230 
    231  MOZ_LOG(GetLogModule(), LogLevel::Info,
    232          ("%p DeleteRangeTransaction::%s this={ mName=%s } "
    233           "End==============================",
    234           this, __FUNCTION__,
    235           nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
    236  return rv;
    237 }
    238 
    239 NS_IMETHODIMP DeleteRangeTransaction::RedoTransaction() {
    240  MOZ_LOG(GetLogModule(), LogLevel::Info,
    241          ("%p DeleteRangeTransaction::%s this={ mName=%s } "
    242           "Start==============================",
    243           this, __FUNCTION__,
    244           nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
    245 
    246  nsresult rv = EditAggregateTransaction::RedoTransaction();
    247  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    248                       "EditAggregateTransaction::RedoTransaction() failed");
    249 
    250  MOZ_LOG(GetLogModule(), LogLevel::Info,
    251          ("%p DeleteRangeTransaction::%s this={ mName=%s } "
    252           "End==============================",
    253           this, __FUNCTION__,
    254           nsAtomCString(mName ? mName.get() : nsGkAtoms::_empty).get()));
    255  return rv;
    256 }
    257 
    258 nsresult DeleteRangeTransaction::AppendTransactionsToDeleteIn(
    259    const EditorRawDOMRange& aRangeToDelete) {
    260  if (NS_WARN_IF(!aRangeToDelete.IsPositionedAndValid())) {
    261    return NS_ERROR_INVALID_ARG;
    262  }
    263  MOZ_ASSERT(aRangeToDelete.InSameContainer());
    264 
    265  if (NS_WARN_IF(!mEditorBase)) {
    266    return NS_ERROR_NOT_AVAILABLE;
    267  }
    268 
    269  // see what kind of node we have
    270  if (Text* textNode = aRangeToDelete.StartRef().GetContainerAs<Text>()) {
    271    if (mEditorBase->IsHTMLEditor() &&
    272        NS_WARN_IF(
    273            !EditorUtils::IsEditableContent(*textNode, EditorType::HTML))) {
    274      // Just ignore to append the transaction for non-editable node.
    275      return NS_OK;
    276    }
    277    // if the node is a chardata node, then delete chardata content
    278    uint32_t textLengthToDelete;
    279    if (aRangeToDelete.Collapsed()) {
    280      textLengthToDelete = 1;
    281    } else {
    282      textLengthToDelete =
    283          aRangeToDelete.EndRef().Offset() - aRangeToDelete.StartRef().Offset();
    284      MOZ_DIAGNOSTIC_ASSERT(textLengthToDelete > 0);
    285    }
    286 
    287    RefPtr<DeleteTextTransaction> deleteTextTransaction =
    288        DeleteTextTransaction::MaybeCreate(*mEditorBase, *textNode,
    289                                           aRangeToDelete.StartRef().Offset(),
    290                                           textLengthToDelete);
    291    // If the text node isn't editable, it should be never undone/redone.
    292    // So, the transaction shouldn't be recorded.
    293    if (!deleteTextTransaction) {
    294      NS_WARNING("DeleteTextTransaction::MaybeCreate() failed");
    295      return NS_ERROR_FAILURE;
    296    }
    297    AppendChild(*deleteTextTransaction);
    298    return NS_OK;
    299  }
    300 
    301  MOZ_ASSERT(mEditorBase->IsHTMLEditor());
    302 
    303  // Even if we detect invalid range, we should ignore it for removing
    304  // specified range's nodes as far as possible.
    305  // XXX This is super expensive.  Probably, we should make
    306  //     DeleteNodeTransaction() can treat multiple siblings.
    307  for (nsIContent* child = aRangeToDelete.StartRef().GetChild();
    308       child && child != aRangeToDelete.EndRef().GetChild();
    309       child = child->GetNextSibling()) {
    310    if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*child))) {
    311      continue;  // Should we abort?
    312    }
    313    RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
    314        DeleteNodeTransaction::MaybeCreate(*mEditorBase, *child);
    315    if (deleteNodeTransaction) {
    316      AppendChild(*deleteNodeTransaction);
    317    }
    318  }
    319 
    320  return NS_OK;
    321 }
    322 
    323 nsresult DeleteRangeTransaction::AppendTransactionToDeleteText(
    324    const EditorRawDOMPoint& aMaybePointInText, nsIEditor::EDirection aAction) {
    325  if (NS_WARN_IF(!aMaybePointInText.IsSetAndValid())) {
    326    return NS_ERROR_INVALID_ARG;
    327  }
    328 
    329  if (NS_WARN_IF(!mEditorBase)) {
    330    return NS_ERROR_NOT_AVAILABLE;
    331  }
    332 
    333  if (!aMaybePointInText.IsInTextNode()) {
    334    return NS_OK;
    335  }
    336 
    337  // If the node is a text node, then delete text before or after the point.
    338  Text& textNode = *aMaybePointInText.ContainerAs<Text>();
    339  uint32_t startOffset, numToDelete;
    340  if (nsIEditor::eNext == aAction) {
    341    startOffset = aMaybePointInText.Offset();
    342    numToDelete = textNode.TextDataLength() - startOffset;
    343  } else {
    344    startOffset = 0;
    345    numToDelete = aMaybePointInText.Offset();
    346  }
    347 
    348  if (!numToDelete) {
    349    return NS_OK;
    350  }
    351 
    352  RefPtr<DeleteTextTransaction> deleteTextTransaction =
    353      DeleteTextTransaction::MaybeCreate(*mEditorBase, textNode, startOffset,
    354                                         numToDelete);
    355  // If the text node isn't editable, it should be never undone/redone.
    356  // So, the transaction shouldn't be recorded.
    357  if (MOZ_UNLIKELY(!deleteTextTransaction)) {
    358    NS_WARNING("DeleteTextTransaction::MaybeCreate() failed");
    359    return NS_ERROR_FAILURE;
    360  }
    361  AppendChild(*deleteTextTransaction);
    362  return NS_OK;
    363 }
    364 
    365 nsresult
    366 DeleteRangeTransaction::AppendTransactionsToDeleteNodesWhoseEndBoundaryIn(
    367    const EditorRawDOMRange& aRangeToDelete) {
    368  if (NS_WARN_IF(!mEditorBase)) {
    369    return NS_ERROR_NOT_AVAILABLE;
    370  }
    371 
    372  ContentSubtreeIterator subtreeIter;
    373  nsresult rv = subtreeIter.Init(aRangeToDelete.StartRef().ToRawRangeBoundary(),
    374                                 aRangeToDelete.EndRef().ToRawRangeBoundary());
    375  if (NS_FAILED(rv)) {
    376    NS_WARNING("ContentSubtreeIterator::Init() failed");
    377    return rv;
    378  }
    379 
    380  for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
    381    nsINode* node = subtreeIter.GetCurrentNode();
    382    if (NS_WARN_IF(!node) || NS_WARN_IF(!node->IsContent())) {
    383      return NS_ERROR_FAILURE;
    384    }
    385 
    386    if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*node->AsContent()))) {
    387      continue;
    388    }
    389    RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
    390        DeleteNodeTransaction::MaybeCreate(*mEditorBase, *node->AsContent());
    391    if (deleteNodeTransaction) {
    392      AppendChild(*deleteNodeTransaction);
    393    }
    394  }
    395  return NS_OK;
    396 }
    397 
    398 EditorDOMPoint DeleteRangeTransaction::SuggestPointToPutCaret() const {
    399  if (!mPointToPutCaret.IsSetAndValidInComposedDoc()) {
    400    return EditorDOMPoint();
    401  }
    402  if (!mPointToPutCaret.IsInNativeAnonymousSubtreeInTextControl() &&
    403      !HTMLEditUtils::IsSimplyEditableNode(*mPointToPutCaret.GetContainer())) {
    404    return EditorDOMPoint();
    405  }
    406  return mPointToPutCaret;
    407 }
    408 
    409 }  // namespace mozilla