Bug 1686495 [wpt PR 27132] - Add tests for proposed WebDriver Shadow DOM support...
[gecko.git] / editor / libeditor / CompositionTransaction.cpp
blob136718201a267a6f39068f337251f8628ecfa7a2
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/SelectionState.h" // RangeUpdater
10 #include "mozilla/TextComposition.h" // TextComposition
11 #include "mozilla/dom/Selection.h" // local var
12 #include "mozilla/dom/Text.h" // mTextNode
13 #include "nsAString.h" // params
14 #include "nsDebug.h" // for NS_ASSERTION, etc
15 #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc
16 #include "nsRange.h" // local var
17 #include "nsISelectionController.h" // for nsISelectionController constants
18 #include "nsQueryObject.h" // for do_QueryObject
20 namespace mozilla {
22 using namespace dom;
24 // static
25 already_AddRefed<CompositionTransaction> CompositionTransaction::Create(
26 EditorBase& aEditorBase, const nsAString& aStringToInsert,
27 const EditorDOMPointInText& aPointToInsert) {
28 MOZ_ASSERT(aPointToInsert.IsSetAndValid());
30 TextComposition* composition = aEditorBase.GetComposition();
31 MOZ_RELEASE_ASSERT(composition);
32 // XXX Actually, we get different text node and offset from editor in some
33 // cases. If composition stores text node, we should use it and offset
34 // in it.
35 EditorDOMPointInText pointToInsert;
36 if (Text* textNode = composition->GetContainerTextNode()) {
37 pointToInsert.Set(textNode, composition->XPOffsetInTextNode());
38 NS_WARNING_ASSERTION(
39 pointToInsert.GetContainerAsText() ==
40 composition->GetContainerTextNode(),
41 "The editor tries to insert composition string into different node");
42 NS_WARNING_ASSERTION(
43 pointToInsert.Offset() == composition->XPOffsetInTextNode(),
44 "The editor tries to insert composition string into different offset");
45 } else {
46 pointToInsert = aPointToInsert;
48 RefPtr<CompositionTransaction> transaction =
49 new CompositionTransaction(aEditorBase, aStringToInsert, pointToInsert);
50 // XXX Now, it might be better to modify the text node information of
51 // the TextComposition instance in DoTransaction() because updating
52 // the information before changing actual DOM tree is pretty odd.
53 composition->OnCreateCompositionTransaction(
54 aStringToInsert, pointToInsert.ContainerAsText(), pointToInsert.Offset());
55 return transaction.forget();
58 CompositionTransaction::CompositionTransaction(
59 EditorBase& aEditorBase, const nsAString& aStringToInsert,
60 const EditorDOMPointInText& aPointToInsert)
61 : mTextNode(aPointToInsert.ContainerAsText()),
62 mOffset(aPointToInsert.Offset()),
63 mReplaceLength(aEditorBase.GetComposition()->XPLengthInTextNode()),
64 mRanges(aEditorBase.GetComposition()->GetRanges()),
65 mStringToInsert(aStringToInsert),
66 mEditorBase(&aEditorBase),
67 mFixed(false) {
68 MOZ_ASSERT(mTextNode->TextLength() >= mOffset);
71 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase,
72 mEditorBase, mTextNode)
73 // mRangeList can't lead to cycles
75 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction)
76 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
77 NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase)
78 NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase)
80 NS_IMETHODIMP CompositionTransaction::DoTransaction() {
81 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
82 return NS_ERROR_NOT_AVAILABLE;
85 // Fail before making any changes if there's no selection controller
86 if (NS_WARN_IF(!mEditorBase->GetSelectionController())) {
87 return NS_ERROR_NOT_AVAILABLE;
90 OwningNonNull<EditorBase> editorBase = *mEditorBase;
91 OwningNonNull<Text> textNode = *mTextNode;
93 // Advance caret: This requires the presentation shell to get the selection.
94 if (mReplaceLength == 0) {
95 ErrorResult error;
96 editorBase->DoInsertText(textNode, mOffset, mStringToInsert, error);
97 if (error.Failed()) {
98 NS_WARNING("EditorBase::DoInsertText() failed");
99 return error.StealNSResult();
101 editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
102 mStringToInsert.Length());
103 } else {
104 // If composition string is split to multiple text nodes, we should put
105 // whole new composition string to the first text node and remove the
106 // compostion string in other nodes.
107 uint32_t replaceableLength = textNode->TextLength() - mOffset;
108 ErrorResult error;
109 editorBase->DoReplaceText(textNode, mOffset, mReplaceLength,
110 mStringToInsert, error);
111 if (error.Failed()) {
112 NS_WARNING("EditorBase::DoReplaceText() failed");
113 return error.StealNSResult();
116 // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
117 // this transaction will remove whole composition string. Therefore,
118 // selection should be restored at start of composition string.
119 // XXX Perhaps, this is a bug of our selection managemnt at undoing.
120 editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset,
121 replaceableLength);
122 // But some ranges which after the composition string should be restored
123 // as-is.
124 editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset,
125 mStringToInsert.Length());
127 if (replaceableLength < mReplaceLength) {
128 // XXX Perhaps, scanning following sibling text nodes with composition
129 // string length which we know is wrong because there may be
130 // non-empty text nodes which are inserted by JS. Instead, we
131 // should remove all text in the ranges of IME selections.
132 int32_t remainLength = mReplaceLength - replaceableLength;
133 IgnoredErrorResult ignoredError;
134 for (nsIContent* nextSibling = textNode->GetNextSibling();
135 nextSibling && nextSibling->IsText() && remainLength;
136 nextSibling = nextSibling->GetNextSibling()) {
137 OwningNonNull<Text> followingTextNode =
138 *static_cast<Text*>(nextSibling);
139 uint32_t textLength = followingTextNode->TextLength();
140 editorBase->DoDeleteText(followingTextNode, 0, remainLength,
141 ignoredError);
142 NS_WARNING_ASSERTION(!ignoredError.Failed(),
143 "EditorBase::DoDeleteText() failed, but ignored");
144 ignoredError.SuppressException();
145 // XXX Needs to check whether the text is deleted as expected.
146 editorBase->RangeUpdaterRef().SelAdjDeleteText(followingTextNode, 0,
147 remainLength);
148 remainLength -= textLength;
153 nsresult rv = SetSelectionForRanges();
154 NS_WARNING_ASSERTION(
155 NS_SUCCEEDED(rv),
156 "CompositionTransaction::SetSelectionForRanges() failed");
157 return rv;
160 NS_IMETHODIMP CompositionTransaction::UndoTransaction() {
161 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
162 return NS_ERROR_NOT_AVAILABLE;
165 // Get the selection first so we'll fail before making any changes if we
166 // can't get it
167 RefPtr<Selection> selection = mEditorBase->GetSelection();
168 if (NS_WARN_IF(!selection)) {
169 return NS_ERROR_NOT_AVAILABLE;
172 OwningNonNull<EditorBase> editorBase = *mEditorBase;
173 OwningNonNull<Text> textNode = *mTextNode;
174 ErrorResult error;
175 editorBase->DoDeleteText(textNode, mOffset, mStringToInsert.Length(), error);
176 if (error.Failed()) {
177 NS_WARNING("EditorBase::DoDeleteText() failed");
178 return error.StealNSResult();
181 // set the selection to the insertion point where the string was removed
182 nsresult rv = selection->CollapseInLimiter(textNode, mOffset);
183 NS_ASSERTION(NS_SUCCEEDED(rv), "Selection::CollapseInLimiter() failed");
184 return rv;
187 NS_IMETHODIMP CompositionTransaction::Merge(nsITransaction* aOtherTransaction,
188 bool* aDidMerge) {
189 if (NS_WARN_IF(!aOtherTransaction) || NS_WARN_IF(!aDidMerge)) {
190 return NS_ERROR_INVALID_ARG;
192 *aDidMerge = false;
194 // Check to make sure we aren't fixed, if we are then nothing gets merged.
195 if (mFixed) {
196 return NS_OK;
199 RefPtr<EditTransactionBase> otherTransactionBase =
200 aOtherTransaction->GetAsEditTransactionBase();
201 if (!otherTransactionBase) {
202 return NS_OK;
205 // If aTransaction is another CompositionTransaction then merge it
206 CompositionTransaction* otherCompositionTransaction =
207 otherTransactionBase->GetAsCompositionTransaction();
208 if (!otherCompositionTransaction) {
209 return NS_OK;
212 // We merge the next IME transaction by adopting its insert string.
213 mStringToInsert = otherCompositionTransaction->mStringToInsert;
214 mRanges = otherCompositionTransaction->mRanges;
215 *aDidMerge = true;
216 return NS_OK;
219 void CompositionTransaction::MarkFixed() { mFixed = true; }
221 /* ============ private methods ================== */
223 nsresult CompositionTransaction::SetSelectionForRanges() {
224 if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) {
225 return NS_ERROR_NOT_AVAILABLE;
227 OwningNonNull<EditorBase> editorBase = *mEditorBase;
228 OwningNonNull<Text> textNode = *mTextNode;
229 RefPtr<TextRangeArray> ranges = mRanges;
230 nsresult rv = SetIMESelection(editorBase, textNode, mOffset,
231 mStringToInsert.Length(), ranges);
232 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
233 "CompositionTransaction::SetIMESelection() failed");
234 return rv;
237 // static
238 nsresult CompositionTransaction::SetIMESelection(
239 EditorBase& aEditorBase, Text* aTextNode, uint32_t aOffsetInNode,
240 uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges) {
241 RefPtr<Selection> selection = aEditorBase.GetSelection();
242 if (NS_WARN_IF(!selection)) {
243 return NS_ERROR_NOT_INITIALIZED;
246 SelectionBatcher selectionBatcher(selection);
248 // First, remove all selections of IME composition.
249 static const RawSelectionType kIMESelections[] = {
250 nsISelectionController::SELECTION_IME_RAWINPUT,
251 nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT,
252 nsISelectionController::SELECTION_IME_CONVERTEDTEXT,
253 nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT};
255 nsCOMPtr<nsISelectionController> selectionController =
256 aEditorBase.GetSelectionController();
257 if (NS_WARN_IF(!selectionController)) {
258 return NS_ERROR_NOT_INITIALIZED;
261 IgnoredErrorResult ignoredError;
262 for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) {
263 RefPtr<Selection> selectionOfIME =
264 selectionController->GetSelection(kIMESelections[i]);
265 if (!selectionOfIME) {
266 NS_WARNING("nsISelectionController::GetSelection() failed");
267 continue;
269 selectionOfIME->RemoveAllRanges(ignoredError);
270 NS_WARNING_ASSERTION(!ignoredError.Failed(),
271 "Selection::RemoveAllRanges() failed, but ignored");
272 ignoredError.SuppressException();
275 // Set caret position and selection of IME composition with TextRangeArray.
276 bool setCaret = false;
277 uint32_t countOfRanges = aRanges ? aRanges->Length() : 0;
279 #ifdef DEBUG
280 // Bounds-checking on debug builds
281 uint32_t maxOffset = aTextNode->Length();
282 #endif
284 // NOTE: composition string may be truncated when it's committed and
285 // maxlength attribute value doesn't allow input of all text of this
286 // composition.
287 nsresult rv = NS_OK;
288 for (uint32_t i = 0; i < countOfRanges; ++i) {
289 const TextRange& textRange = aRanges->ElementAt(i);
291 // Caret needs special handling since its length may be 0 and if it's not
292 // specified explicitly, we need to handle it ourselves later.
293 if (textRange.mRangeType == TextRangeType::eCaret) {
294 NS_ASSERTION(!setCaret, "The ranges already has caret position");
295 NS_ASSERTION(!textRange.Length(),
296 "EditorBase doesn't support wide caret");
297 int32_t caretOffset = static_cast<int32_t>(
298 aOffsetInNode +
299 std::min(textRange.mStartOffset, aLengthOfCompositionString));
300 MOZ_ASSERT(caretOffset >= 0 &&
301 static_cast<uint32_t>(caretOffset) <= maxOffset);
302 rv = selection->CollapseInLimiter(aTextNode, caretOffset);
303 NS_WARNING_ASSERTION(
304 NS_SUCCEEDED(rv),
305 "Selection::CollapseInLimiter() failed, but might be ignored");
306 setCaret = setCaret || NS_SUCCEEDED(rv);
307 if (!setCaret) {
308 continue;
310 // If caret range is specified explicitly, we should show the caret if
311 // it should be so.
312 aEditorBase.HideCaret(false);
313 continue;
316 // If the clause length is 0, it should be a bug.
317 if (!textRange.Length()) {
318 NS_WARNING("Any clauses must not be empty");
319 continue;
322 RefPtr<nsRange> clauseRange;
323 int32_t startOffset = static_cast<int32_t>(
324 aOffsetInNode +
325 std::min(textRange.mStartOffset, aLengthOfCompositionString));
326 MOZ_ASSERT(startOffset >= 0 &&
327 static_cast<uint32_t>(startOffset) <= maxOffset);
328 int32_t endOffset = static_cast<int32_t>(
329 aOffsetInNode +
330 std::min(textRange.mEndOffset, aLengthOfCompositionString));
331 MOZ_ASSERT(endOffset >= startOffset &&
332 static_cast<uint32_t>(endOffset) <= maxOffset);
333 clauseRange = nsRange::Create(aTextNode, startOffset, aTextNode, endOffset,
334 IgnoreErrors());
335 if (!clauseRange) {
336 NS_WARNING("nsRange::Create() failed, but might be ignored");
337 break;
340 // Set the range of the clause to selection.
341 RefPtr<Selection> selectionOfIME = selectionController->GetSelection(
342 ToRawSelectionType(textRange.mRangeType));
343 if (!selectionOfIME) {
344 NS_WARNING(
345 "nsISelectionController::GetSelection() failed, but might be "
346 "ignored");
347 break;
350 IgnoredErrorResult ignoredError;
351 selectionOfIME->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange,
352 ignoredError);
353 if (ignoredError.Failed()) {
354 NS_WARNING(
355 "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but "
356 "might be ignored");
357 break;
360 // Set the style of the clause.
361 rv = selectionOfIME->SetTextRangeStyle(clauseRange, textRange.mRangeStyle);
362 if (NS_FAILED(rv)) {
363 NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored");
364 break; // but this is unexpected...
368 // If the ranges doesn't include explicit caret position, let's set the
369 // caret to the end of composition string.
370 if (!setCaret) {
371 int32_t caretOffset =
372 static_cast<int32_t>(aOffsetInNode + aLengthOfCompositionString);
373 MOZ_ASSERT(caretOffset >= 0 &&
374 static_cast<uint32_t>(caretOffset) <= maxOffset);
375 rv = selection->CollapseInLimiter(aTextNode, caretOffset);
376 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
377 "Selection::CollapseInLimiter() failed");
379 // If caret range isn't specified explicitly, we should hide the caret.
380 // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
381 // However, when there is no range, we should keep showing caret.
382 if (countOfRanges) {
383 aEditorBase.HideCaret(true);
387 return rv;
390 } // namespace mozilla