Bug 1961521 - Make HTMLEditor::HandleInsertText() collapse Selection properly after deleting composition r=m_kato

When inserting text or deleting text is for updating composition string,
`InsertTextWithTransaction()` does not suggest caret position because in the
most cases, `CompositionTransaction` updating `Selection` properly.  However,
if it's deleting existing composition (i.e., replacing the composition with
empty string), `HTMLEditor::HandleInsertText()` doesn't use
`CompositionTransaction` to update composition.  Instead, it uses the suggested
caret position from `InsertTextWithTransaction()` even though it's always unset.
Therefore, if the composition string follows some collapsible white-spaces and
they are replaced, `Selection` is automatically moved to the start position of
the replaced range and `HTMLEditor::HandleInsertText()` does not maintain the
caret position.

Therefore, this patch make it collapse `Selection` to end of the inserted text
where the composition string was.

Differential Revision: https://phabricator.services.mozilla.com/D246087
This commit is contained in:
Masayuki Nakano
2025-04-21 23:49:31 +00:00
parent 9fb43aa799
commit fb1d603cfd
2 changed files with 46 additions and 11 deletions

View File

@@ -1199,6 +1199,10 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
} }
InsertTextResult insertEmptyTextResult = InsertTextResult insertEmptyTextResult =
insertEmptyTextResultOrError.unwrap(); insertEmptyTextResultOrError.unwrap();
// InsertTextWithTransaction() doesn not suggest caret position if it's
// called for IME composition. However, for the safety, let's ignore the
// caret position explicitly.
insertEmptyTextResult.IgnoreCaretPointSuggestion();
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak( nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
insertEmptyTextResult.EndOfInsertedTextRef()); insertEmptyTextResult.EndOfInsertedTextRef());
if (NS_FAILED(rv)) { if (NS_FAILED(rv)) {
@@ -1236,21 +1240,20 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) { if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
NS_WARNING( NS_WARNING(
"HTMLEditor::InsertPaddingBRElementIfNeeded(eNoStrip) failed"); "HTMLEditor::InsertPaddingBRElementIfNeeded(eNoStrip) failed");
insertEmptyTextResult.IgnoreCaretPointSuggestion();
return insertPaddingBRElementResultOrError.propagateErr(); return insertPaddingBRElementResultOrError.propagateErr();
} }
insertPaddingBRElementResultOrError.unwrap().IgnoreCaretPointSuggestion(); insertPaddingBRElementResultOrError.unwrap().IgnoreCaretPointSuggestion();
rv = insertEmptyTextResult.SuggestCaretPointTo( // Then, collapse caret after the empty text inserted position, i.e.,
*this, {SuggestCaret::OnlyIfHasSuggestion, // whether the removed composition string was.
SuggestCaret::OnlyIfTransactionsAllowedToDoIt, if (AllowsTransactionsToChangeSelection()) {
SuggestCaret::AndIgnoreTrivialError}); nsresult rv = CollapseSelectionTo(endOfInsertedText);
if (NS_FAILED(rv)) { if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return Err(rv);
return Err(rv); }
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
} }
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
return EditActionResult::HandledResult(); return EditActionResult::HandledResult();
} }

View File

@@ -11404,6 +11404,37 @@ function runPaddingLineBreakTest() {
contenteditable.innerHTML = ""; contenteditable.innerHTML = "";
} }
function runCancelCompositionAfterCollapseWhileSpace() {
const selection = windowOfContenteditable.getSelection();
contenteditable.innerHTML = "<p>A&nbsp;</p>";
contenteditable.focus();
selection.collapse(
contenteditable.querySelector("[contenteditable] > p").firstChild,
"A ".length
);
const description = "runCancelCompositionAfterCollapseWhileSpace";
synthesizeCompositionChange({
composition: {string: "B", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
});
is(
contenteditable.innerHTML.replaceAll("&nbsp;", " "),
"<p>A B</p>",
`${description}: Composing string "B" should be inserted after the collapsible white-space`
);
synthesizeComposition({type: "compositioncommit", data: ""});
is(
contenteditable.innerHTML,
kNewWhiteSpaceNormalizerEnabled ? "<p>A&nbsp;</p>" : "<p>A <br></p>",
`${description}: Canceling the composition should keep the preceding collapsible white-space visible`
);
synthesizeKey("B");
is(
contenteditable.innerHTML,
"<p>A B</p>",
`${description}: Typing "B" after canceling composition should cause inserting it after the collpsible white-space`
);
}
async function runTest() async function runTest()
{ {
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
@@ -11455,6 +11486,7 @@ async function runTest()
runBug1571375Test(); runBug1571375Test();
runBug1675313Test(); runBug1675313Test();
runCommitCompositionWithSpaceKey(); runCommitCompositionWithSpaceKey();
runCancelCompositionAfterCollapseWhileSpace();
runCompositionWithSelectionChange(); runCompositionWithSelectionChange();
runCompositionWithClick(); runCompositionWithClick();
runForceCommitTest(); runForceCommitTest();