Bug 1968843 - Make HTMLEditor::HandleInsertText() manage Selection if committing composition a=dmeehan DONTBUILD

When committing composition string,
`WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString()` may
replace commit string with normalized white-spaces.  Therefore, if
the last character is a white-space, it may be replaced with the other
white-space (ASCII white-space vs. NBSP).  Then, `Selection` collapsed
after the last character will be moved to start of the replaced
white-spaces.  So, when committing composition,
`HTMLEditor::HandleInsertText()` may need to change `Selection` by
itself.

Original Revision: https://phabricator.services.mozilla.com/D252278

Differential Revision: https://phabricator.services.mozilla.com/D253280
This commit is contained in:
Masayuki Nakano
2025-06-11 17:03:31 +00:00
committed by dmeehan@mozilla.com
parent e4865898e0
commit f3c6a960d9
4 changed files with 117 additions and 39 deletions

View File

@@ -1759,6 +1759,11 @@ class EditorBase : public nsIEditor,
return aPurpose == InsertTextFor::CompositionStart ||
aPurpose == InsertTextFor::CompositionStartAndEnd;
}
[[nodiscard]] static bool InsertingTextForCommittingComposition(
InsertTextFor aPurpose) {
return aPurpose == InsertTextFor::CompositionEnd ||
aPurpose == InsertTextFor::CompositionStartAndEnd;
}
[[nodiscard]] static bool NothingToDoIfInsertingEmptyText(
InsertTextFor aPurpose) {
return aPurpose == InsertTextFor::NormalText ||

View File

@@ -4,6 +4,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "EditorBase.h"
#include "HTMLEditor.h"
#include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"
@@ -1271,45 +1272,86 @@ Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
return EditActionResult::HandledResult();
}
const auto compositionEndPoint =
GetLastIMESelectionEndPoint<EditorDOMPoint>();
Result<InsertTextResult, nsresult> replaceTextResult =
WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString(
*this, aInsertionString,
compositionEndPoint.IsSet()
? EditorDOMRange(pointToInsert, compositionEndPoint)
: EditorDOMRange(pointToInsert),
aPurpose);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
return replaceTextResult.propagateErr();
EditorDOMPoint endOfInsertedText;
{
AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert);
const auto compositionEndPoint =
GetLastIMESelectionEndPoint<EditorDOMPoint>();
Result<InsertTextResult, nsresult> replaceTextResult =
WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString(
*this, aInsertionString,
compositionEndPoint.IsSet()
? EditorDOMRange(pointToInsert, compositionEndPoint)
: EditorDOMRange(pointToInsert),
aPurpose);
if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
return replaceTextResult.propagateErr();
}
InsertTextResult unwrappedReplaceTextResult = replaceTextResult.unwrap();
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
unwrappedReplaceTextResult.EndOfInsertedTextRef());
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
endOfInsertedText = unwrappedReplaceTextResult.EndOfInsertedTextRef();
if (InsertingTextForCommittingComposition(aPurpose)) {
// If we're committing the composition,
// WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString() may
// replace the last character of the composition string when it's a
// white-space. Then, Selection will be moved before the last
// character. So, we need to adjust Selection here.
nsresult rv = unwrappedReplaceTextResult.SuggestCaretPointTo(
*this, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
return Err(rv);
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPoint() failed, but ignored");
} else {
// CompositionTransaction should've set selection so that we should
// ignore caret suggestion.
unwrappedReplaceTextResult.IgnoreCaretPointSuggestion();
}
}
InsertTextResult unwrappedReplacedTextResult = replaceTextResult.unwrap();
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
unwrappedReplacedTextResult.EndOfInsertedTextRef());
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
// CompositionTransaction should've set selection so that we should ignore
// caret suggestion.
unwrappedReplacedTextResult.IgnoreCaretPointSuggestion();
const auto newCompositionStartPoint =
GetFirstIMESelectionStartPoint<EditorDOMPoint>();
const auto newCompositionEndPoint =
GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (NS_WARN_IF(!newCompositionStartPoint.IsSet()) ||
NS_WARN_IF(!newCompositionEndPoint.IsSet())) {
// Mutation event listener has changed the DOM tree...
return EditActionResult::HandledResult();
}
rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
newCompositionStartPoint.ToRawRangeBoundary(),
newCompositionEndPoint.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return Err(rv);
if (!InsertingTextForCommittingComposition(aPurpose)) {
const auto newCompositionStartPoint =
GetFirstIMESelectionStartPoint<EditorDOMPoint>();
const auto newCompositionEndPoint =
GetLastIMESelectionEndPoint<EditorDOMPoint>();
if (NS_WARN_IF(!newCompositionStartPoint.IsSet()) ||
NS_WARN_IF(!newCompositionEndPoint.IsSet())) {
// Mutation event listener has changed the DOM tree...
return EditActionResult::HandledResult();
}
nsresult rv =
TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
newCompositionStartPoint.ToRawRangeBoundary(),
newCompositionEndPoint.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return Err(rv);
}
} else {
if (NS_WARN_IF(!endOfInsertedText.IsSetAndValidInComposedDoc()) ||
NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) {
return EditActionResult::HandledResult();
}
nsresult rv =
TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
pointToInsert.ToRawRangeBoundary(),
endOfInsertedText.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return Err(rv);
}
}
return EditActionResult::HandledResult();
}

View File

@@ -3025,8 +3025,7 @@ WhiteSpaceVisibilityKeeper::InsertTextOrInsertOrUpdateCompositionString(
}
// If the composition is committed, we should normalize surrounding
// white-spaces of the commit string.
if (aPurpose != InsertTextFor::CompositionEnd &&
aPurpose != InsertTextFor::CompositionStartAndEnd) {
if (!EditorBase::InsertingTextForCommittingComposition(aPurpose)) {
return insertOrReplaceTextResultOrError;
}
InsertTextResult insertOrReplaceTextResult =

View File

@@ -11435,6 +11435,37 @@ function runCancelCompositionAfterCollapseWhileSpace() {
);
}
function runCompositionOnlyWhiteSpace() {
const selection = windowOfContenteditable.getSelection();
contenteditable.innerHTML = "<p>A</p>";
contenteditable.focus();
selection.collapse(
contenteditable.querySelector("[contenteditable] > p").firstChild,
"A".length
);
const description = "runCompositionOnlyWhiteSpace";
synthesizeCompositionChange({
composition: {string: " ", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}]},
});
is(
contenteditable.innerHTML,
kNewWhiteSpaceNormalizerEnabled ? "<p>A&nbsp;</p>" : "A <br>",
`${description}: Composing string " " should be inserted`
);
synthesizeComposition({type: "compositioncommitasis", data: " "});
is(
contenteditable.innerHTML,
kNewWhiteSpaceNormalizerEnabled ? "<p>A&nbsp;</p>" : "<p>A <br></p>",
`${description}: Committing the white-space should not change the value`
);
synthesizeKey("B");
is(
contenteditable.innerHTML,
"<p>A B</p>",
`${description}: Typing "B" should cause inserting it after the white-space`
);
}
async function runTest()
{
await SpecialPowers.pushPrefEnv({
@@ -11487,6 +11518,7 @@ async function runTest()
runBug1675313Test();
runCommitCompositionWithSpaceKey();
runCancelCompositionAfterCollapseWhileSpace();
runCompositionOnlyWhiteSpace();
runCompositionWithSelectionChange();
runCompositionWithClick();
runForceCommitTest();