diff --git a/editor/libeditor/nsEditor.cpp b/editor/libeditor/nsEditor.cpp index de7d89d6f75..47b8a2a7f21 100644 --- a/editor/libeditor/nsEditor.cpp +++ b/editor/libeditor/nsEditor.cpp @@ -26,6 +26,7 @@ #include "mozFlushType.h" // for mozFlushType::Flush_Frames #include "mozISpellCheckingEngine.h" #include "mozInlineSpellChecker.h" // for mozInlineSpellChecker +#include "mozilla/CheckedInt.h" // for CheckedInt #include "mozilla/IMEStateManager.h" // for IMEStateManager #include "mozilla/Preferences.h" // for Preferences #include "mozilla/dom/Selection.h" // for Selection, etc @@ -2274,6 +2275,70 @@ NS_IMETHODIMP nsEditor::ScrollSelectionIntoView(bool aScrollToAnchor) return NS_OK; } +void +nsEditor::FindBetterInsertionPoint(nsCOMPtr& aNode, + int32_t& aOffset) +{ + if (aNode->IsNodeOfType(nsINode::eTEXT)) { + // There is no "better" insertion point. + return; + } + + if (!IsPlaintextEditor()) { + // We cannot find "better" insertion point in HTML editor. + // WARNING: When you add some code to find better node in HTML editor, + // you need to call this before calling InsertTextImpl() in + // nsHTMLEditRules. + return; + } + + nsCOMPtr node = aNode; + int32_t offset = aOffset; + + nsCOMPtr root = GetRoot(); + if (aNode == root) { + // In some cases, aNode is the anonymous DIV, and offset is 0. To avoid + // injecting unneeded text nodes, we first look to see if we have one + // available. In that case, we'll just adjust node and offset accordingly. + if (offset == 0 && node->HasChildren() && + node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) { + aNode = node->GetFirstChild(); + aOffset = 0; + return; + } + + // In some other cases, aNode is the anonymous DIV, and offset points to the + // terminating mozBR. In that case, we'll adjust aInOutNode and + // aInOutOffset to the preceding text node, if any. + if (offset > 0 && node->GetChildAt(offset - 1) && + node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) { + NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX); + aNode = node->GetChildAt(offset - 1); + aOffset = static_cast(aNode->Length()); + return; + } + } + + // Sometimes, aNode is the mozBR element itself. In that case, we'll adjust + // the insertion point to the previous text node, if one exists, or to the + // parent anonymous DIV. + if (nsTextEditUtils::IsMozBR(node) && !offset) { + if (node->GetPreviousSibling() && + node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) { + NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX); + aNode = node->GetPreviousSibling(); + aOffset = static_cast(aNode->Length()); + return; + } + + if (node->GetParentNode() && node->GetParentNode() == root) { + aNode = node->GetParentNode(); + aOffset = 0; + return; + } + } +} + nsresult nsEditor::InsertTextImpl(const nsAString& aStringToInsert, nsCOMPtr* aInOutNode, @@ -2286,46 +2351,27 @@ nsEditor::InsertTextImpl(const nsAString& aStringToInsert, NS_ENSURE_TRUE(aInOutNode && *aInOutNode && aInOutOffset && aDoc, NS_ERROR_NULL_POINTER); + if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) { return NS_OK; } - nsCOMPtr node = *aInOutNode; - uint32_t offset = static_cast(*aInOutOffset); + // This method doesn't support over INT32_MAX length text since aInOutOffset + // is int32_t*. + CheckedInt lengthToInsert(aStringToInsert.Length()); + NS_ENSURE_TRUE(lengthToInsert.isValid(), NS_ERROR_INVALID_ARG); - if (!node->IsNodeOfType(nsINode::eTEXT) && IsPlaintextEditor()) { - nsCOMPtr root = GetRoot(); - // In some cases, node is the anonymous DIV, and offset is 0. To avoid - // injecting unneeded text nodes, we first look to see if we have one - // available. In that case, we'll just adjust node and offset accordingly. - if (node == root && offset == 0 && node->HasChildren() && - node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) { - node = node->GetFirstChild(); - } - // In some other cases, node is the anonymous DIV, and offset points to the - // terminating mozBR. In that case, we'll adjust aInOutNode and - // aInOutOffset to the preceding text node, if any. - if (node == root && offset > 0 && node->GetChildAt(offset - 1) && - node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) { - node = node->GetChildAt(offset - 1); - offset = node->Length(); - } - // Sometimes, node is the mozBR element itself. In that case, we'll adjust - // the insertion point to the previous text node, if one exists, or to the - // parent anonymous DIV. - if (nsTextEditUtils::IsMozBR(node) && offset == 0) { - if (node->GetPreviousSibling() && - node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) { - node = node->GetPreviousSibling(); - offset = node->Length(); - } else if (node->GetParentNode() && node->GetParentNode() == root) { - node = node->GetParentNode(); - } - } - } + nsCOMPtr node = *aInOutNode; + int32_t offset = *aInOutOffset; + + // In some cases, the node may be the anonymous div elemnt or a mozBR + // element. Let's try to look for better insertion point in the nearest + // text node if there is. + FindBetterInsertionPoint(node, offset); nsresult res; if (ShouldHandleIMEComposition()) { + CheckedInt newOffset; if (!node->IsNodeOfType(nsINode::eTEXT)) { // create a text node nsRefPtr newNode = aDoc->CreateTextNode(EmptyString()); @@ -2334,18 +2380,24 @@ nsEditor::InsertTextImpl(const nsAString& aStringToInsert, NS_ENSURE_SUCCESS(res, res); node = newNode; offset = 0; + newOffset = lengthToInsert; + } else { + newOffset = lengthToInsert + offset; + NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE); } res = InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset); NS_ENSURE_SUCCESS(res, res); - offset += aStringToInsert.Length(); + offset = newOffset.value(); } else { if (node->IsNodeOfType(nsINode::eTEXT)) { + CheckedInt newOffset = lengthToInsert + offset; + NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE); // we are inserting text into an existing text node. res = InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset); NS_ENSURE_SUCCESS(res, res); - offset += aStringToInsert.Length(); + offset = newOffset.value(); } else { // we are inserting text into a non-text node. first we have to create a // textnode (this also populates it with the text) @@ -2354,12 +2406,12 @@ nsEditor::InsertTextImpl(const nsAString& aStringToInsert, res = InsertNode(*newNode, *node, offset); NS_ENSURE_SUCCESS(res, res); node = newNode; - offset = aStringToInsert.Length(); + offset = lengthToInsert.value(); } } *aInOutNode = node; - *aInOutOffset = static_cast(offset); + *aInOutOffset = offset; return NS_OK; } @@ -2371,6 +2423,8 @@ nsEditor::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, { nsRefPtr txn; bool isIMETransaction = false; + int32_t replacedOffset = 0; + int32_t replacedLength = 0; // aSuppressIME is used when editor must insert text, yet this text is not // part of the current IME operation. Example: adjusting whitespace around an // IME insertion. @@ -2397,6 +2451,11 @@ nsEditor::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, txn = CreateTxnForIMEText(aStringToInsert); isIMETransaction = true; + // All characters of the composition string will be replaced with + // aStringToInsert. So, we need to emulate to remove the composition + // string. + replacedOffset = mIMETextOffset; + replacedLength = mIMETextLength; mIMETextLength = aStringToInsert.Length(); } else { txn = CreateTxnForInsertText(aStringToInsert, aTextNode, aOffset); @@ -2415,6 +2474,11 @@ nsEditor::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, nsresult res = DoTransaction(txn); EndUpdateViewBatch(); + if (replacedLength) { + mRangeUpdater.SelAdjDeleteText( + static_cast(aTextNode.AsDOMNode()), + replacedOffset, replacedLength); + } mRangeUpdater.SelAdjInsertText(aTextNode, aOffset, aStringToInsert); // let listeners know what happened @@ -4722,8 +4786,10 @@ nsEditor::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget) // XXX If selection is changed during reframe, this doesn't work well! nsRange* firstRange = selection->GetRangeAt(0); NS_ENSURE_TRUE(firstRange, NS_ERROR_FAILURE); - nsCOMPtr parentNode = firstRange->GetStartParent(); - Text* textNode = parentNode->GetAsText(); + nsCOMPtr startNode = firstRange->GetStartParent(); + int32_t startOffset = firstRange->StartOffset(); + FindBetterInsertionPoint(startNode, startOffset); + Text* textNode = startNode->GetAsText(); MOZ_ASSERT(textNode, "There must be text node if mIMETextLength is larger than 0"); if (textNode) { @@ -5111,3 +5177,49 @@ nsEditor::GetIsInEditAction(bool* aIsInEditAction) *aIsInEditAction = mIsInEditAction; return NS_OK; } + +int32_t +nsEditor::GetIMESelectionStartOffsetIn(nsINode* aTextNode) +{ + MOZ_ASSERT(aTextNode, "aTextNode must not be nullptr"); + + nsCOMPtr selectionController; + nsresult rv = GetSelectionController(getter_AddRefs(selectionController)); + NS_ENSURE_SUCCESS(rv, -1); + NS_ENSURE_TRUE(selectionController, -1); + + int32_t minOffset = INT32_MAX; + static const SelectionType kIMESelectionTypes[] = { + nsISelectionController::SELECTION_IME_RAWINPUT, + nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT, + nsISelectionController::SELECTION_IME_CONVERTEDTEXT, + nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT + }; + for (auto selectionType : kIMESelectionTypes) { + nsRefPtr selection = GetSelection(selectionType); + if (!selection) { + continue; + } + for (uint32_t i = 0; i < selection->RangeCount(); i++) { + nsRefPtr range = selection->GetRangeAt(i); + if (NS_WARN_IF(!range)) { + continue; + } + if (NS_WARN_IF(range->GetStartParent() != aTextNode)) { + // ignore the start offset... + } else { + MOZ_ASSERT(range->StartOffset() >= 0, + "start offset shouldn't be negative"); + minOffset = std::min(minOffset, range->StartOffset()); + } + if (NS_WARN_IF(range->GetEndParent() != aTextNode)) { + // ignore the end offset... + } else { + MOZ_ASSERT(range->EndOffset() >= 0, + "start offset shouldn't be negative"); + minOffset = std::min(minOffset, range->EndOffset()); + } + } + } + return minOffset < INT32_MAX ? minOffset : -1; +} diff --git a/editor/libeditor/nsEditor.h b/editor/libeditor/nsEditor.h index a4ee24dfdee..d4eac0784cf 100644 --- a/editor/libeditor/nsEditor.h +++ b/editor/libeditor/nsEditor.h @@ -807,6 +807,19 @@ public: virtual already_AddRefed FindUserSelectAllNode(nsIDOMNode* aNode) { return nullptr; } + /** + * GetIMESelectionStartOffsetIn() returns the start offset of IME selection in + * the aTextNode. If there is no IME selection, returns -1. + */ + int32_t GetIMESelectionStartOffsetIn(nsINode* aTextNode); + + /** + * FindBetterInsertionPoint() tries to look for better insertion point which + * is typically the nearest text node and offset in it. + */ + void FindBetterInsertionPoint(nsCOMPtr& aNode, + int32_t& aOffset); + protected: enum Tristate { eTriUnset, diff --git a/editor/libeditor/nsHTMLEditRules.cpp b/editor/libeditor/nsHTMLEditRules.cpp index eed6003713b..d61aa1c350b 100644 --- a/editor/libeditor/nsHTMLEditRules.cpp +++ b/editor/libeditor/nsHTMLEditRules.cpp @@ -1333,15 +1333,21 @@ nsHTMLEditRules::WillInsertText(EditAction aAction, if (aAction == EditAction::insertIMEText) { // Right now the nsWSRunObject code bails on empty strings, but IME needs // the InsertTextImpl() call to still happen since empty strings are meaningful there. + NS_ENSURE_STATE(mHTMLEditor); + // If there is one or more IME selections, its minimum offset should be + // the insertion point. + int32_t IMESelectionOffset = + mHTMLEditor->GetIMESelectionStartOffsetIn(selNode); + if (IMESelectionOffset >= 0) { + selOffset = IMESelectionOffset; + } if (inString->IsEmpty()) { - NS_ENSURE_STATE(mHTMLEditor); res = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode), &selOffset, doc); } else { - NS_ENSURE_STATE(mHTMLEditor); nsWSRunObject wsObj(mHTMLEditor, selNode, selOffset); res = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc); } diff --git a/editor/libeditor/nsTextEditRules.cpp b/editor/libeditor/nsTextEditRules.cpp index 8a36d2c516a..dbb8edf3887 100644 --- a/editor/libeditor/nsTextEditRules.cpp +++ b/editor/libeditor/nsTextEditRules.cpp @@ -725,6 +725,14 @@ nsTextEditRules::WillInsertText(EditAction aAction, if (aAction == EditAction::insertIMEText) { NS_ENSURE_STATE(mEditor); + // Find better insertion point to insert text. + mEditor->FindBetterInsertionPoint(selNode, selOffset); + // If there is one or more IME selections, its minimum offset should be + // the insertion point. + int32_t IMESelectionOffset = mEditor->GetIMESelectionStartOffsetIn(selNode); + if (IMESelectionOffset >= 0) { + selOffset = IMESelectionOffset; + } res = mEditor->InsertTextImpl(*outString, address_of(selNode), &selOffset, doc); NS_ENSURE_SUCCESS(res, res); } else {