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