Bug 1893351 - part 2: Make HTMLEditor::HandleInsertText stop inserting text into existing text nodes if it's a middle line of inserting text r=m_kato

When 2nd or later line, the method inserts one-line text to start of a `Text`
node following `<br>` which is inserted by the method.  Then, splits the `Text`
node to insert another `<br>`.  This creates a lot of unnecessary
`SplitNodeTransaction`s and that causes a lot of copying memory operation to
set the data of the right `Text` node.

This patch makes the method creates a `Text` node when inserting a middle line
of inserting text.  Therefore, `SplitNodeTransaction` is created at most one
(to split a `Text` node if the caller wants to insert a text middle of it).

Depends on D211697

Differential Revision: https://phabricator.services.mozilla.com/D211698
This commit is contained in:
Masayuki Nakano 2024-05-30 00:42:41 +00:00
parent 08de69e4f7
commit d8f302fe60

View file

@ -1281,37 +1281,71 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
// don't change my selection in subtransactions
AutoTransactionsConserveSelection dontChangeMySelection(*this);
int32_t pos = 0;
constexpr auto newlineStr = NS_LITERAL_STRING_FROM_CSTRING(LFSTR);
{
AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);
const auto GetInsertTextTo = [](int32_t aInclusiveNextLinefeedOffset,
uint32_t aLineStartOffset) {
if (aInclusiveNextLinefeedOffset > 0) {
return aLineStartOffset > 0
// If we'll insert a <br> and we're inserting 2nd or later
// line, we should always create new `Text` since it'll be
// between 2 <br> elements.
? InsertTextTo::AlwaysCreateNewTextNode
// If we'll insert a <br> and we're inserting first line,
// we should append text to preceding text node, but
// we don't want to insert it to a a following text node
// because of avoiding to split the `Text`.
: InsertTextTo::ExistingTextNodeIfAvailableAndNotStart;
}
// If we're inserting the last line, the text should be inserted to
// start of the following `Text` if there is or middle of the `Text`
// at insertion position if we're inserting only the line.
return InsertTextTo::ExistingTextNodeIfAvailable;
};
// for efficiency, break out the pre case separately. This is because
// its a lot cheaper to search the input string for only newlines than
// it is to search for both tabs and newlines.
if (!isWhiteSpaceCollapsible || IsPlaintextMailComposer()) {
while (pos != -1 &&
pos < AssertedCast<int32_t>(aInsertionString.Length())) {
int32_t oldPos = pos;
int32_t subStrLen;
pos = aInsertionString.FindChar(nsCRT::LF, oldPos);
if (pos != -1) {
subStrLen = pos - oldPos;
// if first char is newline, then use just it
if (!subStrLen) {
subStrLen = 1;
uint32_t nextOffset = 0;
while (nextOffset < aInsertionString.Length()) {
const uint32_t lineStartOffset = nextOffset;
const int32_t inclusiveNextLinefeedOffset =
aInsertionString.FindChar(nsCRT::LF, lineStartOffset);
const uint32_t lineLength =
inclusiveNextLinefeedOffset != -1
? static_cast<uint32_t>(inclusiveNextLinefeedOffset) -
lineStartOffset
: aInsertionString.Length() - lineStartOffset;
if (lineLength) {
// lineText does not include the preformatted line break.
const nsDependentSubstring lineText(aInsertionString, lineStartOffset,
lineLength);
Result<InsertTextResult, nsresult> insertTextResult =
InsertTextWithTransaction(
*document, lineText, currentPoint,
GetInsertTextTo(inclusiveNextLinefeedOffset,
lineStartOffset));
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
return insertTextResult.propagateErr();
}
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint = insertTextResult.unwrap()
.EndOfInsertedTextRef()
.To<EditorDOMPoint>();
} else {
subStrLen = aInsertionString.Length() - oldPos;
pos = aInsertionString.Length();
pointToInsert = currentPoint;
}
nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen);
// is it a return?
if (subStr.Equals(newlineStr)) {
if (inclusiveNextLinefeedOffset < 0) {
break; // We reached the last line
}
}
MOZ_ASSERT(inclusiveNextLinefeedOffset >= 0);
Result<CreateElementResult, nsresult> insertBRElementResult =
InsertBRElement(WithTransaction::Yes, currentPoint);
if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
@ -1327,7 +1361,7 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
MOZ_ASSERT(!AllowsTransactionsToChangeSelection());
pos++;
nextOffset = inclusiveNextLinefeedOffset + 1;
RefPtr<Element> brElement =
unwrappedInsertBRElementResult.UnwrapNewNode();
if (brElement->GetNextSibling()) {
@ -1342,59 +1376,40 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
NS_WARNING_ASSERTION(currentPoint.IsSet(),
"Failed to set after the <br> element");
NS_WARNING_ASSERTION(currentPoint == pointToInsert,
"Perhaps, <br> element position has been moved "
"to different point "
"by mutation observer");
} else {
Result<InsertTextResult, nsresult> insertTextResult =
InsertTextWithTransaction(
*document, subStr, currentPoint,
InsertTextTo::ExistingTextNodeIfAvailable);
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
return insertTextResult.propagateErr();
}
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint = insertTextResult.unwrap()
.EndOfInsertedTextRef()
.To<EditorDOMPoint>();
} else {
pointToInsert = currentPoint;
}
}
"Perhaps, <br> element position has been moved to "
"different point by mutation observer");
}
} else {
constexpr auto tabStr = u"\t"_ns;
constexpr auto spacesStr = u" "_ns;
nsAutoString insertionString(aInsertionString); // For FindCharInSet().
while (pos != -1 &&
pos < AssertedCast<int32_t>(insertionString.Length())) {
int32_t oldPos = pos;
int32_t subStrLen;
pos = insertionString.FindCharInSet(u"\t\n", oldPos);
uint32_t nextOffset = 0;
while (nextOffset < aInsertionString.Length()) {
const uint32_t lineStartOffset = nextOffset;
const int32_t inclusiveNextLinefeedOffset =
aInsertionString.FindChar(nsCRT::LF, lineStartOffset);
const uint32_t lineLength =
inclusiveNextLinefeedOffset != -1
? static_cast<uint32_t>(inclusiveNextLinefeedOffset) -
lineStartOffset
: aInsertionString.Length() - lineStartOffset;
if (pos != -1) {
subStrLen = pos - oldPos;
// if first char is newline, then use just it
if (!subStrLen) {
subStrLen = 1;
if (lineLength) {
auto insertTextResult =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<InsertTextResult, nsresult> {
// lineText does not include the preformatted line break.
const nsDependentSubstring lineText(aInsertionString,
lineStartOffset, lineLength);
if (!lineText.Contains(u'\t')) {
return WhiteSpaceVisibilityKeeper::InsertText(
*this, lineText, currentPoint,
GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset),
*editingHost);
}
} else {
subStrLen = insertionString.Length() - oldPos;
pos = insertionString.Length();
}
nsDependentSubstring subStr(insertionString, oldPos, subStrLen);
// is it a tab?
if (subStr.Equals(tabStr)) {
Result<InsertTextResult, nsresult> insertTextResult =
WhiteSpaceVisibilityKeeper::InsertText(
*this, spacesStr, currentPoint,
InsertTextTo::ExistingTextNodeIfAvailable, *editingHost);
nsAutoString formattedLineText(lineText);
formattedLineText.ReplaceSubstring(u"\t"_ns, u" "_ns);
return WhiteSpaceVisibilityKeeper::InsertText(
*this, formattedLineText, currentPoint,
GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset),
*editingHost);
}();
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed");
return insertTextResult.propagateErr();
@ -1402,19 +1417,18 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
pos++;
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint = insertTextResult.unwrap()
.EndOfInsertedTextRef()
.To<EditorDOMPoint>();
MOZ_ASSERT(pointToInsert.IsSet());
} else {
pointToInsert = currentPoint;
MOZ_ASSERT(pointToInsert.IsSet());
}
if (inclusiveNextLinefeedOffset < 0) {
break; // We reached the last line
}
}
// is it a return?
else if (subStr.Equals(newlineStr)) {
Result<CreateElementResult, nsresult> insertBRElementResult =
WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint,
*editingHost);
@ -1438,7 +1452,7 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CreateElementResult::SuggestCaretPointTo() failed, but ignored");
pos++;
nextOffset = inclusiveNextLinefeedOffset + 1;
RefPtr<Element> newBRElement =
unwrappedInsertBRElementResult.UnwrapNewNode();
MOZ_DIAGNOSTIC_ASSERT(newBRElement);
@ -1457,28 +1471,6 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
NS_WARNING_ASSERTION(
currentPoint == pointToInsert,
"Perhaps, newBRElement has been moved or removed unexpectedly");
} else {
Result<InsertTextResult, nsresult> insertTextResult =
WhiteSpaceVisibilityKeeper::InsertText(
*this, subStr, currentPoint,
InsertTextTo::ExistingTextNodeIfAvailable, *editingHost);
if (MOZ_UNLIKELY(insertTextResult.isErr())) {
NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed");
return insertTextResult.propagateErr();
}
// Ignore the caret suggestion because of `dontChangeMySelection`
// above.
insertTextResult.inspect().IgnoreCaretPointSuggestion();
if (insertTextResult.inspect().Handled()) {
pointToInsert = currentPoint = insertTextResult.unwrap()
.EndOfInsertedTextRef()
.To<EditorDOMPoint>();
MOZ_ASSERT(pointToInsert.IsSet());
} else {
pointToInsert = currentPoint;
MOZ_ASSERT(pointToInsert.IsSet());
}
}
}
}