Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / editor / libeditor / CompositionTransaction.cpp
blob81df04255865872004d5b3730d8c70c1d759acef
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "CompositionTransaction.h"
8 #include "mozilla/EditorBase.h" // mEditorBase
9 #include "mozilla/Logging.h"
10 #include "mozilla/SelectionState.h" // RangeUpdater
11 #include "mozilla/TextComposition.h" // TextComposition
12 #include "mozilla/ToString.h"
13 #include "mozilla/dom/Selection.h" // local var
14 #include "mozilla/dom/Text.h" // mTextNode
15 #include "nsAString.h" // params
16 #include "nsDebug.h" // for NS_ASSERTION, etc
17 #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc
18 #include "nsRange.h" // local var
19 #include "nsISelectionController.h" // for nsISelectionController constants
20 #include "nsQueryObject.h" // for do_QueryObject
22 namespace mozilla {
24 using namespace dom;
26 // static
27 already_AddRefed<CompositionTransaction> CompositionTransaction::Create(
28 EditorBase& aEditorBase, const nsAString& aStringToInsert,
29 const EditorDOMPointInText& aPointToInsert) {
30 MOZ_ASSERT(aPointToInsert.IsSetAndValid());
32 TextComposition* composition = aEditorBase.GetComposition();
33 MOZ_RELEASE_ASSERT(composition);
34 // XXX Actually, we get different text node and offset from editor in some
35 // cases. If composition stores text node, we should use it and offset
36 // in it.
37 EditorDOMPointInText pointToInsert;
38 if (Text* textNode = composition->GetContainerTextNode()) {
39 pointToInsert.Set(textNode, composition->XPOffsetInTextNode());
40 NS_WARNING_ASSERTION(
41 pointToInsert.GetContainerAs<Text>() ==
42 composition->GetContainerTextNode(),
43 "The editor tries to insert composition string into different node");
44 NS_WARNING_ASSERTION(
45 pointToInsert.Offset() == composition->XPOffsetInTextNode(),
46 "The editor tries to insert composition string into different offset");
47 } else {
48 pointToInsert = aPointToInsert;
50 RefPtr<CompositionTransaction> transaction =
51 new CompositionTransaction(aEditorBase, aStringToInsert, pointToInsert);
52 return transaction.forget();
55 CompositionTransaction::CompositionTransaction(
56 EditorBase& aEditorBase, const nsAString& aStringToInsert,
57 const EditorDOMPointInText& aPointToInsert)
58 : mTextNode(aPointToInsert.ContainerAs<Text>()),
59 mOffset(aPointToInsert.Offset()),
60 mReplaceLength(aEditorBase.GetComposition()->XPLengthInTextNode()),
61 mRanges(aEditorBase.GetComposition()->GetRanges()),
62 mStringToInsert(aStringToInsert),
63 mEditorBase(&aEditorBase),
64 mFixed(false) {
65 MOZ_ASSERT(mTextNode->TextLength() >= mOffset);
68 std::ostream& operator<<(std::ostream& aStream,
69 const CompositionTransaction& aTransaction) {
70 aStream << "{ mTextNode=" << aTransaction.mTextNode.get();
71 if (aTransaction.mTextNode) {
72 aStream << " (" << *aTransaction.mTextNode << ")";
74 aStream << ", mOffset=" << aTransaction.mOffset
75 << ", mReplaceLength=" << aTransaction.mReplaceLength
76 << ", mRanges={ Length()=" << aTransaction.mRanges->Length() << " }"
77 << ", mStringToInsert=\""
78 << NS_ConvertUTF16toUTF8(aTransaction.mStringToInsert).get() << "\""
79 << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }";
80 return aStream;
83 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase,
84 mEditorBase, mTextNode)
85 // mRangeList can't lead to cycles
87 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction)
88 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
89 NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase)
90 NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase)
92 NS_IMETHODIMP CompositionTransaction::DoTransaction() {
93 MOZ_LOG(GetLogModule(), LogLevel::Info,
94 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__,
95 ToString(*this).c_str()));
97 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
98 return NS_ERROR_NOT_AVAILABLE;
101 // Fail before making any changes if there's no selection controller
102 if (NS_WARN_IF(!mEditorBase->GetSelectionController())) {
103 return NS_ERROR_NOT_AVAILABLE;
106 OwningNonNull<EditorBase> editorBase = *mEditorBase;
107 OwningNonNull<Text> textNode = *mTextNode;
109 // Advance caret: This requires the presentation shell to get the selection.
110 if (mReplaceLength == 0) {
111 ErrorResult error;
112 editorBase->DoInsertText(textNode, mOffset, mStringToInsert, error);
113 if (error.Failed()) {
114 NS_WARNING("EditorBase::DoInsertText() failed");
115 return error.StealNSResult();
117 editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
118 mStringToInsert.Length());
119 } else {
120 // If composition string is split to multiple text nodes, we should put
121 // whole new composition string to the first text node and remove the
122 // compostion string in other nodes.
123 // TODO: This should be handled by `TextComposition` because this assumes
124 // that composition string has never touched by JS. However, it
125 // would occur if the web app is a corrabolation software which
126 // multiple users can modify anyware in an editor.
127 // TODO: And if composition starts from a following text node, the offset
128 // here is outdated and it will cause inserting composition string
129 // **before** the proper point from point of view of the users.
130 uint32_t replaceableLength = textNode->TextLength() - mOffset;
131 ErrorResult error;
132 editorBase->DoReplaceText(textNode, mOffset, mReplaceLength,
133 mStringToInsert, error);
134 if (error.Failed()) {
135 NS_WARNING("EditorBase::DoReplaceText() failed");
136 return error.StealNSResult();
139 // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
140 // this transaction will remove whole composition string. Therefore,
141 // selection should be restored at start of composition string.
142 // XXX Perhaps, this is a bug of our selection managemnt at undoing.
143 editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
144 replaceableLength);
145 // But some ranges which after the composition string should be restored
146 // as-is.
147 editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
148 mStringToInsert.Length());
150 if (replaceableLength < mReplaceLength) {
151 // XXX Perhaps, scanning following sibling text nodes with composition
152 // string length which we know is wrong because there may be
153 // non-empty text nodes which are inserted by JS. Instead, we
154 // should remove all text in the ranges of IME selections.
155 uint32_t remainLength = mReplaceLength - replaceableLength;
156 IgnoredErrorResult ignoredError;
157 for (nsIContent* nextSibling = textNode->GetNextSibling();
158 nextSibling && nextSibling->IsText() && remainLength;
159 nextSibling = nextSibling->GetNextSibling()) {
160 OwningNonNull<Text> followingTextNode =
161 *static_cast<Text*>(nextSibling);
162 uint32_t textLength = followingTextNode->TextLength();
163 editorBase->DoDeleteText(followingTextNode, 0, remainLength,
164 ignoredError);
165 NS_WARNING_ASSERTION(!ignoredError.Failed(),
166 "EditorBase::DoDeleteText() failed, but ignored");
167 ignoredError.SuppressException();
168 // XXX Needs to check whether the text is deleted as expected.
169 editorBase->RangeUpdaterRef().SelAdjDeleteText(followingTextNode, 0,
170 remainLength);
171 remainLength -= textLength;
176 nsresult rv = SetSelectionForRanges();
177 NS_WARNING_ASSERTION(
178 NS_SUCCEEDED(rv),
179 "CompositionTransaction::SetSelectionForRanges() failed");
181 if (TextComposition* composition = editorBase->GetComposition()) {
182 composition->OnUpdateCompositionInEditor(mStringToInsert, textNode,
183 mOffset);
186 return rv;
189 NS_IMETHODIMP CompositionTransaction::UndoTransaction() {
190 MOZ_LOG(GetLogModule(), LogLevel::Info,
191 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__,
192 ToString(*this).c_str()));
194 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode))) {
195 return NS_ERROR_NOT_AVAILABLE;
198 OwningNonNull<EditorBase> editorBase = *mEditorBase;
199 OwningNonNull<Text> textNode = *mTextNode;
200 IgnoredErrorResult error;
201 editorBase->DoDeleteText(textNode, mOffset, mStringToInsert.Length(), error);
202 if (MOZ_UNLIKELY(error.Failed())) {
203 NS_WARNING("EditorBase::DoDeleteText() failed");
204 return error.StealNSResult();
207 // set the selection to the insertion point where the string was removed
208 editorBase->CollapseSelectionTo(EditorRawDOMPoint(textNode, mOffset), error);
209 NS_ASSERTION(!error.Failed(), "EditorBase::CollapseSelectionTo() failed");
210 return error.StealNSResult();
213 NS_IMETHODIMP CompositionTransaction::RedoTransaction() {
214 MOZ_LOG(GetLogModule(), LogLevel::Info,
215 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__,
216 ToString(*this).c_str()));
217 return DoTransaction();
220 NS_IMETHODIMP CompositionTransaction::Merge(nsITransaction* aOtherTransaction,
221 bool* aDidMerge) {
222 MOZ_LOG(GetLogModule(), LogLevel::Debug,
223 ("%p CompositionTransaction::%s(aOtherTransaction=%p) this=%s", this,
224 __FUNCTION__, aOtherTransaction, ToString(*this).c_str()));
226 if (NS_WARN_IF(!aOtherTransaction) || NS_WARN_IF(!aDidMerge)) {
227 return NS_ERROR_INVALID_ARG;
229 *aDidMerge = false;
231 // Check to make sure we aren't fixed, if we are then nothing gets merged.
232 if (mFixed) {
233 MOZ_LOG(GetLogModule(), LogLevel::Debug,
234 ("%p CompositionTransaction::%s returned false due to fixed", this,
235 __FUNCTION__));
236 return NS_OK;
239 RefPtr<EditTransactionBase> otherTransactionBase =
240 aOtherTransaction->GetAsEditTransactionBase();
241 if (!otherTransactionBase) {
242 MOZ_LOG(GetLogModule(), LogLevel::Debug,
243 ("%p CompositionTransaction::%s returned false due to not edit "
244 "transaction",
245 this, __FUNCTION__));
246 return NS_OK;
249 // If aTransaction is another CompositionTransaction then merge it
250 CompositionTransaction* otherCompositionTransaction =
251 otherTransactionBase->GetAsCompositionTransaction();
252 if (!otherCompositionTransaction) {
253 return NS_OK;
256 // We merge the next IME transaction by adopting its insert string.
257 mStringToInsert = otherCompositionTransaction->mStringToInsert;
258 mRanges = otherCompositionTransaction->mRanges;
259 *aDidMerge = true;
260 MOZ_LOG(GetLogModule(), LogLevel::Debug,
261 ("%p CompositionTransaction::%s returned true", this, __FUNCTION__));
262 return NS_OK;
265 void CompositionTransaction::MarkFixed() { mFixed = true; }
267 /* ============ private methods ================== */
269 nsresult CompositionTransaction::SetSelectionForRanges() {
270 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
271 return NS_ERROR_NOT_AVAILABLE;
273 OwningNonNull<EditorBase> editorBase = *mEditorBase;
274 OwningNonNull<Text> textNode = *mTextNode;
275 RefPtr<TextRangeArray> ranges = mRanges;
276 nsresult rv = SetIMESelection(editorBase, textNode, mOffset,
277 mStringToInsert.Length(), ranges);
278 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
279 "CompositionTransaction::SetIMESelection() failed");
280 return rv;
283 // static
284 nsresult CompositionTransaction::SetIMESelection(
285 EditorBase& aEditorBase, Text* aTextNode, uint32_t aOffsetInNode,
286 uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges) {
287 RefPtr<Selection> selection = aEditorBase.GetSelection();
288 if (NS_WARN_IF(!selection)) {
289 return NS_ERROR_NOT_INITIALIZED;
292 SelectionBatcher selectionBatcher(selection, __FUNCTION__);
294 // First, remove all selections of IME composition.
295 static const RawSelectionType kIMESelections[] = {
296 nsISelectionController::SELECTION_IME_RAWINPUT,
297 nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT,
298 nsISelectionController::SELECTION_IME_CONVERTEDTEXT,
299 nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT};
301 nsCOMPtr<nsISelectionController> selectionController =
302 aEditorBase.GetSelectionController();
303 if (NS_WARN_IF(!selectionController)) {
304 return NS_ERROR_NOT_INITIALIZED;
307 IgnoredErrorResult ignoredError;
308 for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) {
309 RefPtr<Selection> selectionOfIME =
310 selectionController->GetSelection(kIMESelections[i]);
311 if (!selectionOfIME) {
312 NS_WARNING("nsISelectionController::GetSelection() failed");
313 continue;
315 selectionOfIME->RemoveAllRanges(ignoredError);
316 NS_WARNING_ASSERTION(!ignoredError.Failed(),
317 "Selection::RemoveAllRanges() failed, but ignored");
318 ignoredError.SuppressException();
321 // Set caret position and selection of IME composition with TextRangeArray.
322 bool setCaret = false;
323 uint32_t countOfRanges = aRanges ? aRanges->Length() : 0;
325 #ifdef DEBUG
326 // Bounds-checking on debug builds
327 uint32_t maxOffset = aTextNode->Length();
328 #endif
330 // NOTE: composition string may be truncated when it's committed and
331 // maxlength attribute value doesn't allow input of all text of this
332 // composition.
333 nsresult rv = NS_OK;
334 for (uint32_t i = 0; i < countOfRanges; ++i) {
335 const TextRange& textRange = aRanges->ElementAt(i);
337 // Caret needs special handling since its length may be 0 and if it's not
338 // specified explicitly, we need to handle it ourselves later.
339 if (textRange.mRangeType == TextRangeType::eCaret) {
340 NS_ASSERTION(!setCaret, "The ranges already has caret position");
341 NS_ASSERTION(!textRange.Length(),
342 "EditorBase doesn't support wide caret");
343 CheckedUint32 caretOffset(aOffsetInNode);
344 caretOffset +=
345 std::min(textRange.mStartOffset, aLengthOfCompositionString);
346 MOZ_ASSERT(caretOffset.isValid());
347 MOZ_ASSERT(caretOffset.value() <= maxOffset);
348 rv = selection->CollapseInLimiter(aTextNode, caretOffset.value());
349 NS_WARNING_ASSERTION(
350 NS_SUCCEEDED(rv),
351 "Selection::CollapseInLimiter() failed, but might be ignored");
352 setCaret = setCaret || NS_SUCCEEDED(rv);
353 if (!setCaret) {
354 continue;
356 // If caret range is specified explicitly, we should show the caret if
357 // it should be so.
358 aEditorBase.HideCaret(false);
359 continue;
362 // If the clause length is 0, it should be a bug.
363 if (!textRange.Length()) {
364 NS_WARNING("Any clauses must not be empty");
365 continue;
368 RefPtr<nsRange> clauseRange;
369 CheckedUint32 startOffset = aOffsetInNode;
370 startOffset += std::min(textRange.mStartOffset, aLengthOfCompositionString);
371 MOZ_ASSERT(startOffset.isValid());
372 MOZ_ASSERT(startOffset.value() <= maxOffset);
373 CheckedUint32 endOffset = aOffsetInNode;
374 endOffset += std::min(textRange.mEndOffset, aLengthOfCompositionString);
375 MOZ_ASSERT(endOffset.isValid());
376 MOZ_ASSERT(endOffset.value() >= startOffset.value());
377 MOZ_ASSERT(endOffset.value() <= maxOffset);
378 clauseRange = nsRange::Create(aTextNode, startOffset.value(), aTextNode,
379 endOffset.value(), IgnoreErrors());
380 if (!clauseRange) {
381 NS_WARNING("nsRange::Create() failed, but might be ignored");
382 break;
385 // Set the range of the clause to selection.
386 RefPtr<Selection> selectionOfIME = selectionController->GetSelection(
387 ToRawSelectionType(textRange.mRangeType));
388 if (!selectionOfIME) {
389 NS_WARNING(
390 "nsISelectionController::GetSelection() failed, but might be "
391 "ignored");
392 break;
395 IgnoredErrorResult ignoredError;
396 selectionOfIME->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange,
397 ignoredError);
398 if (ignoredError.Failed()) {
399 NS_WARNING(
400 "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but "
401 "might be ignored");
402 break;
405 // Set the style of the clause.
406 rv = selectionOfIME->SetTextRangeStyle(clauseRange, textRange.mRangeStyle);
407 if (NS_FAILED(rv)) {
408 NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored");
409 break; // but this is unexpected...
413 // If the ranges doesn't include explicit caret position, let's set the
414 // caret to the end of composition string.
415 if (!setCaret) {
416 CheckedUint32 caretOffset = aOffsetInNode;
417 caretOffset += aLengthOfCompositionString;
418 MOZ_ASSERT(caretOffset.isValid());
419 MOZ_ASSERT(caretOffset.value() <= maxOffset);
420 rv = selection->CollapseInLimiter(aTextNode, caretOffset.value());
421 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
422 "Selection::CollapseInLimiter() failed");
424 // If caret range isn't specified explicitly, we should hide the caret.
425 // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
426 // However, when there is no range, we should keep showing caret.
427 if (countOfRanges) {
428 aEditorBase.HideCaret(true);
432 return rv;
435 } // namespace mozilla