From a269bbe4972bfd44d2e920c591b14642ed43d4a1 Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Wed, 29 Jul 2020 00:04:00 +0000 Subject: [PATCH] Bug 1644313 - part 1: Create new path to handle backspace or (forward) delete with the new normalizer r=m_kato `InputEvent.getTargetRange()` should have actual delete range. It means that the range should contain the invisible leading/trailing white-spaces which need to be removed for keep them invisible, but should not contain the range of normalizing white-space sequence because they are not target of edit action indicated by `InputEvent.inputType`. So, we can use new path which uses the new white-space normalizer for computing the value of `InputEvent.getTargetRanges()` because difference of white-space normalizer shouldn't affect the deleting ranges (although, some existing path calls `DeleteNodeIfInvisibleAndEditableTextNode()` later so that the new method, `ComputeRangeInTextNodesContainingInvisibleWhiteSpaces()`, does not exactly same thing, but the result shouldn't become different in usual cases). This new path can test with some WPTs under `editing/other`. This patch creates new backspace/delete key handler when caret is at next to a white-space as `HTMLEditor::HandleDeleteTextAroundCollapsedSelection()` and creates helper methods of `WSRunScanner` to treat invisible leading and trailing white-spaces. Note that new failures are caused by the difference whether adjacent white-space sequence at deletion is normalized or not in edge cases. They will be fixed by the part.5. Depends on D84943 Differential Revision: https://phabricator.services.mozilla.com/D84944 --- editor/libeditor/EditorBase.h | 5 + editor/libeditor/EditorDOMPoint.h | 3 +- editor/libeditor/HTMLEditSubActionHandler.cpp | 96 +++++- editor/libeditor/HTMLEditor.h | 13 + editor/libeditor/WSRunObject.cpp | 276 +++++++++++++++++- editor/libeditor/WSRunObject.h | 34 ++- ...fter-execCommand-delete.tentative.html.ini | 270 +---------------- 7 files changed, 424 insertions(+), 273 deletions(-) diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h index e8ed1f4110ef..23b0c68ec5c3 100644 --- a/editor/libeditor/EditorBase.h +++ b/editor/libeditor/EditorBase.h @@ -699,6 +699,10 @@ class EditorBase : public nsIEditor, // to restore it. bool mRestoreContentEditableCount; + // If we explicitly normalized whitespaces around the changed range, + // set to true. + bool mDidNormalizeWhitespaces; + /** * The following methods modifies some data of this struct and * `EditSubActionData` struct. Currently, these are required only @@ -747,6 +751,7 @@ class EditorBase : public nsIEditor, mDidDeleteNonCollapsedRange = false; mDidDeleteEmptyParentBlocks = false; mRestoreContentEditableCount = false; + mDidNormalizeWhitespaces = false; } /** diff --git a/editor/libeditor/EditorDOMPoint.h b/editor/libeditor/EditorDOMPoint.h index 296350307e5b..d34207932e38 100644 --- a/editor/libeditor/EditorDOMPoint.h +++ b/editor/libeditor/EditorDOMPoint.h @@ -1065,7 +1065,8 @@ class EditorDOMRangeBase final { } template bool operator==(const OtherRangeType& aOther) const { - return mStart == aOther.mStart && mEnd == aOther.mEnd; + return (!IsPositioned() && !aOther.IsPositioned()) || + (mStart == aOther.mStart && mEnd == aOther.mEnd); } template bool operator!=(const OtherRangeType& aOther) const { diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp index 462c18750888..04000b67128f 100644 --- a/editor/libeditor/HTMLEditSubActionHandler.cpp +++ b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -23,6 +23,7 @@ #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/RangeUtils.h" +#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* #include "mozilla/TextComposition.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" @@ -523,9 +524,13 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() { // attempt to transform any unneeded nbsp's into spaces after doing various // operations switch (GetTopLevelEditSubAction()) { + case EditSubAction::eDeleteSelectedContent: + if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) { + break; + } + [[fallthrough]]; case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: - case EditSubAction::eDeleteSelectedContent: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: @@ -2617,11 +2622,94 @@ EditActionResult HTMLEditor::HandleDeleteAroundCollapsedSelection( return EditActionIgnored(); } +EditActionResult HTMLEditor::HandleDeleteTextAroundCollapsedSelection( + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPosition) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext || + aDirectionAndAmount == nsIEditor::ePrevious); + + EditorDOMRangeInTexts rangeToDelete; + EditorDOMPointInText newCaretPosition; + if (aDirectionAndAmount == nsIEditor::eNext) { + Result result = + WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(*this, + aCaretPosition); + if (result.isErr()) { + NS_WARNING( + "WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom() failed"); + return EditActionHandled(result.unwrapErr()); + } + rangeToDelete = result.unwrap(); + if (!rangeToDelete.IsPositioned()) { + return EditActionHandled(); // no range to delete + } + newCaretPosition = rangeToDelete.StartRef(); + } else { + Result result = + WSRunScanner::GetRangeInTextNodesToBackspaceFrom(*this, aCaretPosition); + if (result.isErr()) { + NS_WARNING("WSRunScanner::GetRangeInTextNodesToBackspaceFrom() failed"); + return EditActionHandled(result.unwrapErr()); + } + rangeToDelete = result.unwrap(); + if (!rangeToDelete.IsPositioned()) { + return EditActionHandled(); // no range to delete + } + if (rangeToDelete.InSameContainer()) { + newCaretPosition = rangeToDelete.StartRef(); + } else { + newCaretPosition = + EditorDOMPointInText(rangeToDelete.EndRef().ContainerAsText(), 0); + } + } + + AutoTransactionsConserveSelection dontChangeMySelection(*this); + nsresult rv = DeleteTextAndNormalizeSurroundingWhiteSpaces( + rangeToDelete.StartRef(), rangeToDelete.EndRef()); + TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces = true; + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() failed"); + return EditActionHandled(rv); + } + + if (!newCaretPosition.IsSetAndValid() || + !newCaretPosition.ContainerAsText()->IsInComposedDoc()) { + return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + // For compatibility with Blink, we should move caret to end of previous + // text node if it's direct previous sibling of the first text node in the + // range. + if (newCaretPosition.IsStartOfContainer() && + newCaretPosition.GetContainer()->GetPreviousSibling() && + newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) { + newCaretPosition.SetToEndOf( + newCaretPosition.GetContainer()->GetPreviousSibling()->AsText()); + } + + DebugOnly rvIgnored = + SelectionRefPtr()->Collapse(newCaretPosition.ToRawRangeBoundary()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Selection::Collapse() failed, but ignored"); + return EditActionHandled(); +} + EditActionResult HTMLEditor::HandleDeleteCollapsedSelectionAtWhiteSpaces( nsIEditor::EDirection aDirectionAndAmount, const EditorDOMPoint& aPointToDelete) { MOZ_ASSERT(IsEditActionDataAvailable()); + if (StaticPrefs::editor_white_space_normalization_blink_compatible()) { + EditActionResult result = HandleDeleteTextAroundCollapsedSelection( + aDirectionAndAmount, aPointToDelete); + NS_WARNING_ASSERTION( + result.Succeeded(), + "HTMLEditor::HandleDeleteTextAroundCollapsedSelection() failed"); + return result; + } + if (aDirectionAndAmount == nsIEditor::eNext) { nsresult rv = WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace( *this, aPointToDelete); @@ -3913,9 +4001,9 @@ nsresult HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( startToDelete, endToDelete, normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode); - // If given range is collapsed, i.e., the caller just wants to normalize - // white-space sequence, but there is no white-spaces which need to be - // replaced, we need to do nothing here. + // If extended range is still collapsed, i.e., the caller just wants to + // normalize white-space sequence, but there is no white-spaces which need to + // be replaced, we need to do nothing here. if (startToDelete == endToDelete) { return NS_OK; } diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h index 213a3e1c4dfc..27d206621d13 100644 --- a/editor/libeditor/HTMLEditor.h +++ b/editor/libeditor/HTMLEditor.h @@ -2739,6 +2739,19 @@ class HTMLEditor final : public TextEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers); + /** + * HandleDeleteTextAroundCollapsedSelection() handles deletion of + * collapsed selection in a text node. + * + * @param aDirectionAndAmount Must be eNext or ePrevious. + * @param aCaretPoisition The position where caret is. This container + * must be a text node. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT EditActionResult + HandleDeleteTextAroundCollapsedSelection( + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPosition); + /** * HandleDeleteNonCollapsedSelection() handles deletion with non-collapsed * `Selection`. Callers must guarantee that this is called only when diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp index a0b533c95cd7..8f2602f71d53 100644 --- a/editor/libeditor/WSRunObject.cpp +++ b/editor/libeditor/WSRunObject.cpp @@ -1416,7 +1416,7 @@ nsresult WhiteSpaceVisibilityKeeper:: ReplaceRangeData WSRunScanner::TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange( - const TextFragmentData& aTextFragmentDataAtStartToDelete) { + const TextFragmentData& aTextFragmentDataAtStartToDelete) const { const EditorDOMPoint& startToDelete = aTextFragmentDataAtStartToDelete.ScanStartRef(); const EditorDOMPoint& endToDelete = mScanStartPoint; @@ -1495,7 +1495,7 @@ WSRunScanner::TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange( ReplaceRangeData WSRunScanner::TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange( - const TextFragmentData& aTextFragmentDataAtEndToDelete) { + const TextFragmentData& aTextFragmentDataAtEndToDelete) const { const EditorDOMPoint& startToDelete = mScanStartPoint; const EditorDOMPoint& endToDelete = aTextFragmentDataAtEndToDelete.ScanStartRef(); @@ -2437,4 +2437,276 @@ nsresult WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces( return NS_OK; } +/***************************************************************************** + * Implementation for new white-space normalizer + *****************************************************************************/ + +// static +EditorDOMRangeInTexts +WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( + const TextFragmentData& aStart, const TextFragmentData& aEnd) { + // Corresponding to handling invisible white-spaces part of + // `TextFragmentData::GetReplaceRangeDataAtEndOfDeletionRange()` and + // `TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange()` + + MOZ_ASSERT(aStart.ScanStartRef().IsSetAndValid()); + MOZ_ASSERT(aEnd.ScanStartRef().IsSetAndValid()); + MOZ_ASSERT(aStart.ScanStartRef().EqualsOrIsBefore(aEnd.ScanStartRef())); + MOZ_ASSERT(aStart.ScanStartRef().IsInTextNode()); + MOZ_ASSERT(aEnd.ScanStartRef().IsInTextNode()); + + // XXX `GetReplaceRangeDataAtEndOfDeletionRange()` and + // `GetReplaceRangeDataAtStartOfDeletionRange()` use + // `GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt()` and + // `GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt()`. + // However, they are really odd as mentioned with "XXX" comments + // in them. For the new white-space normalizer, we need to treat + // invisible white-spaces stricter because the legacy path handles + // white-spaces multiple times (e.g., calling `HTMLEditor:: + // DeleteNodeIfInvisibleAndEditableTextNode()` later) and that hides + // the bug, but in the new path, we should stop doing same things + // multiple times for both performance and footprint. Therefore, + // even though the result might be different in some edge cases, + // we should use clean path for now. Perhaps, we should fix the odd + // cases before shipping `beforeinput` event in release channel. + + const EditorDOMRange& invisibleLeadingWhiteSpaceRange = + aStart.InvisibleLeadingWhiteSpaceRangeRef(); + const EditorDOMRange& invisibleTrailingWhiteSpaceRange = + aEnd.InvisibleTrailingWhiteSpaceRangeRef(); + const bool hasInvisibleLeadingWhiteSpaces = + invisibleLeadingWhiteSpaceRange.IsPositioned() && + !invisibleLeadingWhiteSpaceRange.Collapsed(); + const bool hasInvisibleTrailingWhiteSpaces = + invisibleLeadingWhiteSpaceRange != invisibleTrailingWhiteSpaceRange && + invisibleTrailingWhiteSpaceRange.IsPositioned() && + !invisibleTrailingWhiteSpaceRange.Collapsed(); + + EditorDOMRangeInTexts result(aStart.ScanStartRef().AsInText(), + aEnd.ScanStartRef().AsInText()); + MOZ_ASSERT(result.IsPositionedAndValid()); + if (!hasInvisibleLeadingWhiteSpaces && !hasInvisibleTrailingWhiteSpaces) { + return result; + } + + MOZ_ASSERT_IF( + hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces, + invisibleLeadingWhiteSpaceRange.StartRef().IsBefore( + invisibleTrailingWhiteSpaceRange.StartRef())); + const EditorDOMPoint& aroundFirstInvisibleWhiteSpace = + hasInvisibleLeadingWhiteSpaces + ? invisibleLeadingWhiteSpaceRange.StartRef() + : invisibleTrailingWhiteSpaceRange.StartRef(); + if (aroundFirstInvisibleWhiteSpace.IsBefore(result.StartRef())) { + if (aroundFirstInvisibleWhiteSpace.IsInTextNode()) { + result.SetStart(aroundFirstInvisibleWhiteSpace.AsInText()); + MOZ_ASSERT(result.IsPositionedAndValid()); + } else { + const EditorDOMPointInText atFirstInvisibleWhiteSpace = + hasInvisibleLeadingWhiteSpaces + ? aStart.GetInclusiveNextEditableCharPoint( + aroundFirstInvisibleWhiteSpace) + : aEnd.GetInclusiveNextEditableCharPoint( + aroundFirstInvisibleWhiteSpace); + MOZ_ASSERT(atFirstInvisibleWhiteSpace.IsSet()); + MOZ_ASSERT( + atFirstInvisibleWhiteSpace.EqualsOrIsBefore(result.StartRef())); + result.SetStart(atFirstInvisibleWhiteSpace); + MOZ_ASSERT(result.IsPositionedAndValid()); + } + } + MOZ_ASSERT_IF( + hasInvisibleLeadingWhiteSpaces && hasInvisibleTrailingWhiteSpaces, + invisibleLeadingWhiteSpaceRange.EndRef().IsBefore( + invisibleTrailingWhiteSpaceRange.EndRef())); + const EditorDOMPoint& afterLastInvisibleWhiteSpace = + hasInvisibleTrailingWhiteSpaces + ? invisibleTrailingWhiteSpaceRange.EndRef() + : invisibleLeadingWhiteSpaceRange.EndRef(); + if (afterLastInvisibleWhiteSpace.EqualsOrIsBefore(result.EndRef())) { + MOZ_ASSERT(result.IsPositionedAndValid()); + return result; + } + if (afterLastInvisibleWhiteSpace.IsInTextNode()) { + result.SetEnd(afterLastInvisibleWhiteSpace.AsInText()); + MOZ_ASSERT(result.IsPositionedAndValid()); + return result; + } + const EditorDOMPointInText atLastInvisibleWhiteSpace = + hasInvisibleTrailingWhiteSpaces + ? aEnd.GetPreviousEditableCharPoint(afterLastInvisibleWhiteSpace) + : aStart.GetPreviousEditableCharPoint(afterLastInvisibleWhiteSpace); + MOZ_ASSERT(atLastInvisibleWhiteSpace.IsSet()); + MOZ_ASSERT(atLastInvisibleWhiteSpace.IsContainerEmpty() || + atLastInvisibleWhiteSpace.IsAtLastContent()); + MOZ_ASSERT(result.EndRef().EqualsOrIsBefore(atLastInvisibleWhiteSpace)); + result.SetEnd(atLastInvisibleWhiteSpace.IsEndOfContainer() + ? atLastInvisibleWhiteSpace + : atLastInvisibleWhiteSpace.NextPoint()); + MOZ_ASSERT(result.IsPositionedAndValid()); + return result; +} + +// static +Result +WSRunScanner::GetRangeInTextNodesToBackspaceFrom(const HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aPoint) { + // Corresponding to computing delete range part of + // `WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace()` + MOZ_ASSERT(aPoint.IsSetAndValid()); + + Element* editingHost = aHTMLEditor.GetActiveEditingHost(); + TextFragmentData textFragmentDataAtCaret(aPoint, editingHost); + EditorDOMPointInText atPreviousChar = + textFragmentDataAtCaret.GetPreviousEditableCharPoint(aPoint); + if (!atPreviousChar.IsSet()) { + return EditorDOMRangeInTexts(); // There is no content in the block. + } + // For now, we handle white-space deletion. + if (!atPreviousChar.IsCharASCIISpaceOrNBSP()) { + return EditorDOMRangeInTexts(); + } + + // XXX When previous char point is in an empty text node, we do nothing, + // but this must look odd from point of user view. We should delete + // something before aPoint. + if (atPreviousChar.IsEndOfContainer()) { + return EditorDOMRangeInTexts(); + } + + // If the text node is preformatted, just remove the previous character. + if (textFragmentDataAtCaret.IsPreformatted()) { + return EditorDOMRangeInTexts(atPreviousChar, atPreviousChar.NextPoint()); + } + + // If previous char is an ASCII white-spaces, delete all adjcent ASCII + // whitespaces. + EditorDOMRangeInTexts rangeToDelete; + if (atPreviousChar.IsCharASCIISpace()) { + EditorDOMPointInText startToDelete = + textFragmentDataAtCaret.GetFirstASCIIWhiteSpacePointCollapsedTo( + atPreviousChar); + if (!startToDelete.IsSet()) { + NS_WARNING( + "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed"); + return Err(NS_ERROR_FAILURE); + } + EditorDOMPointInText endToDelete = + textFragmentDataAtCaret.GetEndOfCollapsibleASCIIWhiteSpaces( + atPreviousChar); + if (!endToDelete.IsSet()) { + NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed"); + return Err(NS_ERROR_FAILURE); + } + rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete); + } + // if previous char is an NBSP, remove it. + else { + MOZ_ASSERT(atPreviousChar.IsCharNBSP()); + rangeToDelete = + EditorDOMRangeInTexts(atPreviousChar, atPreviousChar.NextPoint()); + } + + // If there is no removable and visible content, we should do nothing. + if (rangeToDelete.Collapsed()) { + return EditorDOMRangeInTexts(); + } + + // And also delete invisible white-spaces if they become visible. + TextFragmentData textFragmentDataAtStart = + rangeToDelete.StartRef() != aPoint + ? TextFragmentData(rangeToDelete.StartRef(), editingHost) + : textFragmentDataAtCaret; + TextFragmentData textFragmentDataAtEnd = + rangeToDelete.EndRef() != aPoint + ? TextFragmentData(rangeToDelete.EndRef(), editingHost) + : textFragmentDataAtCaret; + EditorDOMRangeInTexts extendedRangeToDelete = + WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( + textFragmentDataAtStart, textFragmentDataAtEnd); + MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid()); + return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete + : rangeToDelete; +} + +// static +Result +WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom( + const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) { + // Corresponding to computing delete range part of + // `WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace()` + MOZ_ASSERT(aPoint.IsSetAndValid()); + + Element* editingHost = aHTMLEditor.GetActiveEditingHost(); + TextFragmentData textFragmentDataAtCaret(aPoint, editingHost); + EditorDOMPointInText atCaret = + textFragmentDataAtCaret.GetInclusiveNextEditableCharPoint(aPoint); + if (!atCaret.IsSet()) { + return EditorDOMRangeInTexts(); // There is no content in the block. + } + // For now, we handle whitespace deletion. + if (!atCaret.IsCharASCIISpaceOrNBSP()) { + return EditorDOMRangeInTexts(); + } + + // XXX When next char point is in an empty text node, we do nothing, + // but this must look odd from point of user view. We should delete + // something after aPoint. + if (atCaret.IsEndOfContainer()) { + return EditorDOMRangeInTexts(); + } + + // If the text node is preformatted, just remove the previous character. + if (textFragmentDataAtCaret.IsPreformatted()) { + return EditorDOMRangeInTexts(atCaret, atCaret.NextPoint()); + } + + // If next char is an ASCII whitespaces, delete all adjcent ASCII + // whitespaces. + EditorDOMRangeInTexts rangeToDelete; + if (atCaret.IsCharASCIISpace()) { + EditorDOMPointInText startToDelete = + textFragmentDataAtCaret.GetFirstASCIIWhiteSpacePointCollapsedTo( + atCaret); + if (!startToDelete.IsSet()) { + NS_WARNING( + "WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo() failed"); + return Err(NS_ERROR_FAILURE); + } + EditorDOMPointInText endToDelete = + textFragmentDataAtCaret.GetEndOfCollapsibleASCIIWhiteSpaces(atCaret); + if (!endToDelete.IsSet()) { + NS_WARNING("WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces() failed"); + return Err(NS_ERROR_FAILURE); + } + rangeToDelete = EditorDOMRangeInTexts(startToDelete, endToDelete); + } + // if next char is an NBSP, remove it. + else { + MOZ_ASSERT(atCaret.IsCharNBSP()); + rangeToDelete = EditorDOMRangeInTexts(atCaret, atCaret.NextPoint()); + } + + // If there is no removable and visible content, we should do nothing. + if (rangeToDelete.Collapsed()) { + return EditorDOMRangeInTexts(); + } + + // And also delete invisible white-spaces if they become visible. + TextFragmentData textFragmentDataAtStart = + rangeToDelete.StartRef() != aPoint + ? TextFragmentData(rangeToDelete.StartRef(), editingHost) + : textFragmentDataAtCaret; + TextFragmentData textFragmentDataAtEnd = + rangeToDelete.EndRef() != aPoint + ? TextFragmentData(rangeToDelete.EndRef(), editingHost) + : textFragmentDataAtCaret; + EditorDOMRangeInTexts extendedRangeToDelete = + WSRunScanner::ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( + textFragmentDataAtStart, textFragmentDataAtEnd); + MOZ_ASSERT(extendedRangeToDelete.IsPositionedAndValid()); + return extendedRangeToDelete.IsPositioned() ? extendedRangeToDelete + : rangeToDelete; +} + } // namespace mozilla diff --git a/editor/libeditor/WSRunObject.h b/editor/libeditor/WSRunObject.h index d008fa9d87f5..7c3aa43b1215 100644 --- a/editor/libeditor/WSRunObject.h +++ b/editor/libeditor/WSRunObject.h @@ -329,6 +329,22 @@ class MOZ_STACK_CLASS WSRunScanner final { return scanner.GetPreviousEditableCharPoint(aPoint); } + /** + * GetRangeInTextNodesToForwardDeleteFrom() returns the range to remove + * text when caret is at aPoint. + */ + static Result + GetRangeInTextNodesToForwardDeleteFrom(const HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aPoint); + + /** + * GetRangeInTextNodesToBackspaceFrom() returns the range to remove text + * when caret is at aPoint. + */ + static Result + GetRangeInTextNodesToBackspaceFrom(const HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aPoint); + /** * GetStartReasonContent() and GetEndReasonContent() return a node which * was found by scanning from mScanStartPoint backward or forward. If there @@ -1035,9 +1051,9 @@ class MOZ_STACK_CLASS WSRunScanner final { * with an NBSP. */ ReplaceRangeData GetReplaceRangeDataAtEndOfDeletionRange( - const TextFragmentData& aTextFragmentDataAtStartToDelete); + const TextFragmentData& aTextFragmentDataAtStartToDelete) const; ReplaceRangeData GetReplaceRangeDataAtStartOfDeletionRange( - const TextFragmentData& aTextFragmentDataAtEndToDelete); + const TextFragmentData& aTextFragmentDataAtEndToDelete) const; /** * VisibleWhiteSpacesDataRef() returns reference to visible white-spaces @@ -1092,6 +1108,20 @@ class MOZ_STACK_CLASS WSRunScanner final { const HTMLEditor* mHTMLEditor; private: + /** + * ComputeRangeInTextNodesContainingInvisibleWhiteSpaces() returns range + * containing invisible white-spaces if deleting between aStart and aEnd + * causes them become visible. + * + * @param aStart TextFragmentData at start of deleting range. + * This must be initialized with DOM point in a text node. + * @param aEnd TextFragmentData at end of deleting range. + * This must be initialized with DOM point in a text node. + */ + static EditorDOMRangeInTexts + ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( + const TextFragmentData& aStart, const TextFragmentData& aEnd); + TextFragmentData mTextFragmentDataAtStart; friend class WhiteSpaceVisibilityKeeper; diff --git a/testing/web-platform/meta/editing/other/white-spaces-after-execCommand-delete.tentative.html.ini b/testing/web-platform/meta/editing/other/white-spaces-after-execCommand-delete.tentative.html.ini index f3e17a8945cb..7a6c5171cf09 100644 --- a/testing/web-platform/meta/editing/other/white-spaces-after-execCommand-delete.tentative.html.ini +++ b/testing/web-platform/meta/editing/other/white-spaces-after-execCommand-delete.tentative.html.ini @@ -1,366 +1,108 @@ [white-spaces-after-execCommand-delete.tentative.html] prefs: [editor.white_space_normalization.blink_compatible:true] - max-asserts: 3 + max-asserts: 4 [execCommand("delete", false, ""): "a    [\] "] expected: FAIL - [execCommand("delete", false, ""): "a    [\]b" (length of whiteSpace sequence: 4)] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a         [\] b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "         [\]  b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "          [\] b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a     [\]     b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "a    [\] b" (length of whiteSpace sequence: 5)] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] expected: FAIL - [execCommand("delete", false, ""): "a  [\]        b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\]def"] - expected: FAIL - - [execCommand("delete", false, ""): "a   [\]       b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): " [\]   b" (length of whiteSpace sequence: 4)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "     [\]      b" (length of whiteSpace sequence: 11)] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] expected: FAIL - [execCommand("delete", false, ""): "a          [\] b" (length of whiteSpace sequence: 11)] - expected: FAIL - [execCommand("delete", false, ""): "a    [\]||    c"] expected: FAIL - [execCommand("delete", false, ""): "    [\]b" (length of whiteSpace sequence: 4)] - expected: FAIL - - [execCommand("delete", false, ""): "a      [\]     b" (length of whiteSpace sequence: 11)] - expected: FAIL - [execCommand("delete", false, ""): "a  b[\]  |    c"] expected: FAIL - [execCommand("delete", false, ""): "a   [\] | |   c"] - expected: FAIL - - [execCommand("delete", false, ""): "abc   [\]def"] - expected: FAIL - [execCommand("delete", false, ""): "a b[\]  |    c"] expected: FAIL - [execCommand("delete", false, ""): "a [\]         b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "      [\]    b" (length of whiteSpace sequence: 10)] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] expected: FAIL - [execCommand("delete", false, ""): "a      [\]    b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a       [\]   b" (length of whiteSpace sequence: 10)] - expected: FAIL - [execCommand("delete", false, ""): "a   |b[\]     c"] expected: FAIL [execCommand("delete", false, ""): "a    |[\]|    c"] expected: FAIL - [execCommand("delete", false, ""): "a   [\]  | |   c"] - expected: FAIL - [execCommand("delete", false, ""): "a   | |[\]    c"] expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "    [\]       b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "a    [\]      b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "   [\]        b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "   [\]       b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "a    | [\]   b"] - expected: FAIL - - [execCommand("delete", false, ""): "   [\] b" (length of whiteSpace sequence: 4)] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] expected: FAIL - [execCommand("delete", false, ""): "a    [\]       b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a   | [\]  b"] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] expected: FAIL - [execCommand("delete", false, ""): "     [\]b" (length of whiteSpace sequence: 5)] - expected: FAIL - - [execCommand("delete", false, ""): "a [\]   b" (length of whiteSpace sequence: 4)] - expected: FAIL - - [execCommand("delete", false, ""): "a        [\]  b" (length of whiteSpace sequence: 10)] - expected: FAIL - [execCommand("delete", false, ""): "a    || [\]   c"] expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - [execCommand("delete", false, ""): "a    ||[\]    c"] expected: FAIL - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a       [\]    b" (length of whiteSpace sequence: 11)] - expected: FAIL - [execCommand("delete", false, ""): "abc [\]   def"] expected: FAIL - [execCommand("delete", false, ""): "   [\] b" (length of whiteSpace sequence: 4)] - expected: FAIL - [execCommand("delete", false, ""): "a   | [\]  b"] expected: FAIL - [execCommand("delete", false, ""): "a  [\]  b" (length of whiteSpace sequence: 4)] - expected: FAIL - [execCommand("delete", false, ""): "a   | [\]|    c"] expected: FAIL [execCommand("delete", false, ""): "abc  [\] def"] expected: FAIL - [execCommand("delete", false, ""): "a   [\] b" (length of whiteSpace sequence: 4)] - expected: FAIL - - [execCommand("delete", false, ""): "     [\]     b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "a   [\]        b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "a         [\]  b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "a    [\] b" (length of whiteSpace sequence: 5)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - [execCommand("delete", false, ""): "a   [\]|   b"] expected: FAIL [execCommand("delete", false, ""): "a   [\]|   b"] expected: FAIL - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "       [\]   b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "       [\]    b" (length of whiteSpace sequence: 11)] - expected: FAIL - [execCommand("delete", false, ""): "a    [\] "] expected: FAIL - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "  [\]  b" (length of whiteSpace sequence: 4)] - expected: FAIL - - [execCommand("delete", false, ""): "a   [\] ||    c"] - expected: FAIL - [execCommand("delete", false, ""): "a    [\]| |   c"] expected: FAIL [execCommand("delete", false, ""): "abc   [\]def"] expected: FAIL - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a [\]    b" (length of whiteSpace sequence: 5)] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\]   def"] expected: FAIL - [execCommand("delete", false, ""): "a  [\]   b" (length of whiteSpace sequence: 5)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - [execCommand("delete", false, ""): "a [\]b"] expected: FAIL - [execCommand("delete", false, ""): "abc   [\]def"] - expected: FAIL - [execCommand("delete", false, ""): "a   | |[\]    c"] expected: FAIL - [execCommand("delete", false, ""): "  [\]  b" (length of whiteSpace sequence: 4)] - expected: FAIL - [execCommand("delete", false, ""): "a   |[\]   b"] expected: FAIL - [execCommand("delete", false, ""): "         [\] b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a     [\]      b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "a   [\]  b" (length of whiteSpace sequence: 5)] - expected: FAIL - - [execCommand("delete", false, ""): " [\]   b" (length of whiteSpace sequence: 4)] - expected: FAIL - [execCommand("delete", false, ""): "abc  [\]   def"] expected: FAIL - [execCommand("delete", false, ""): "abc  [\]def"] - expected: FAIL - - [execCommand("delete", false, ""): "a   [\]  | |   c"] - expected: FAIL - - [execCommand("delete", false, ""): "          [\]b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "  [\]        b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "a   | [\]  b"] - expected: FAIL - - [execCommand("delete", false, ""): "a    [\]b" (length of whiteSpace sequence: 4)] - expected: FAIL - [execCommand("delete", false, ""): "abc [\]   def"] expected: FAIL - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "abc   [\]def"] - expected: FAIL - - [execCommand("delete", false, ""): "abc  [\]def"] - expected: FAIL - - [execCommand("delete", false, ""): "a   | [\]  b"] - expected: FAIL - - [execCommand("delete", false, ""): "        [\]   b" (length of whiteSpace sequence: 11)] - expected: FAIL - - [execCommand("delete", false, ""): "abc   [\]def"] - expected: FAIL - [execCommand("delete", false, ""): "a   |[\]   b"] expected: FAIL - [execCommand("delete", false, ""): "a   [\]  b" (length of whiteSpace sequence: 5)] + [execCommand("delete", false, ""): "a    |[\]    b"] expected: FAIL - [execCommand("delete", false, ""): "a     [\]b" (length of whiteSpace sequence: 5)] + [execCommand("delete", false, ""): "a   [\]|   b"] expected: FAIL - [execCommand("delete", false, ""): "    [\]      b" (length of whiteSpace sequence: 10)] + [execCommand("delete", false, ""): "a   |[\]   b"] expected: FAIL - [execCommand("delete", false, ""): "a        [\]   b" (length of whiteSpace sequence: 11)] + [execCommand("delete", false, ""): "abc  [\]   def"] expected: FAIL - [execCommand("delete", false, ""): "a          [\]b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "abc [\] def"] - expected: FAIL - - [execCommand("delete", false, ""): "        [\]  b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): " [\]         b" (length of whiteSpace sequence: 10)] - expected: FAIL - - [execCommand("delete", false, ""): "      [\]     b" (length of whiteSpace sequence: 11)] + [execCommand("delete", false, ""): " [\]   a"] expected: FAIL