Line data Source code
1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set sw=2 ts=2 sts=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : /**
8 : * This class is called by the editor to handle spellchecking after various
9 : * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
10 : * when the text is changed.
11 : *
12 : * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
13 : * notifications to be flushed when we are called from the editor. This is
14 : * because the call might originate from a frame, and flushing the
15 : * notifications might cause that frame to be deleted.
16 : *
17 : * Using the WordUtil class to find words causes DOM notifications to be
18 : * flushed because it asks for style information. As a result, we post an event
19 : * and do all of the spellchecking in that event handler, which occurs later.
20 : * We store all DOM pointers in ranges because they are kept up-to-date with
21 : * DOM changes that may have happened while the event was on the queue.
22 : *
23 : * We also allow the spellcheck to be suspended and resumed later. This makes
24 : * large pastes or initializations with a lot of text not hang the browser UI.
25 : *
26 : * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
27 : * true when we get any change, and false once there is no possibility
28 : * something changed that we need to check on navigation. Navigation events
29 : * tend to be a little tricky because we want to check the current word on
30 : * exit if something has changed. If we navigate inside the word, we don't want
31 : * to do anything. As a result, this flag is cleared in FinishNavigationEvent
32 : * when we know that we are checking as a result of navigation.
33 : */
34 :
35 : #include "mozilla/EditorBase.h"
36 : #include "mozilla/EditorUtils.h"
37 : #include "mozilla/Services.h"
38 : #include "mozilla/dom/Selection.h"
39 : #include "mozInlineSpellChecker.h"
40 : #include "mozInlineSpellWordUtil.h"
41 : #include "mozISpellI18NManager.h"
42 : #include "nsCOMPtr.h"
43 : #include "nsCRT.h"
44 : #include "nsIDOMNode.h"
45 : #include "nsIDOMDocument.h"
46 : #include "nsIDOMElement.h"
47 : #include "nsIDOMHTMLElement.h"
48 : #include "nsIDOMMouseEvent.h"
49 : #include "nsIDOMKeyEvent.h"
50 : #include "nsIDOMNode.h"
51 : #include "nsIDOMNodeList.h"
52 : #include "nsIDOMEvent.h"
53 : #include "nsGenericHTMLElement.h"
54 : #include "nsRange.h"
55 : #include "nsIPlaintextEditor.h"
56 : #include "nsIPrefBranch.h"
57 : #include "nsIPrefService.h"
58 : #include "nsIRunnable.h"
59 : #include "nsISelection.h"
60 : #include "nsISelectionPrivate.h"
61 : #include "nsISelectionController.h"
62 : #include "nsIServiceManager.h"
63 : #include "nsITextServicesFilter.h"
64 : #include "nsString.h"
65 : #include "nsThreadUtils.h"
66 : #include "nsUnicharUtils.h"
67 : #include "nsIContent.h"
68 : #include "nsRange.h"
69 : #include "nsContentUtils.h"
70 : #include "nsIObserverService.h"
71 : #include "nsITextControlElement.h"
72 : #include "prtime.h"
73 :
74 : using namespace mozilla;
75 : using namespace mozilla::dom;
76 :
77 : // Set to spew messages to the console about what is happening.
78 : //#define DEBUG_INLINESPELL
79 :
80 : // the number of milliseconds that we will take at once to do spellchecking
81 : #define INLINESPELL_CHECK_TIMEOUT 1
82 :
83 : // The number of words to check before we look at the time to see if
84 : // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from getting
85 : // stuck and not moving forward because the INLINESPELL_CHECK_TIMEOUT might
86 : // be too short to a low-end machine.
87 : #define INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT 5
88 :
89 : // These notifications are broadcast when spell check starts and ends. STARTED
90 : // must always be followed by ENDED.
91 : #define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
92 : #define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
93 :
94 : static bool ContentIsDescendantOf(nsINode* aPossibleDescendant,
95 : nsINode* aPossibleAncestor);
96 :
97 : static const char kMaxSpellCheckSelectionSize[] =
98 : "extensions.spellcheck.inline.max-misspellings";
99 : static const PRTime kMaxSpellCheckTimeInUsec =
100 : INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC;
101 :
102 0 : mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
103 0 : : mSpellChecker(aSpellChecker), mWordCount(0)
104 : {
105 0 : }
106 :
107 : // mozInlineSpellStatus::InitForEditorChange
108 : //
109 : // This is the most complicated case. For changes, we need to compute the
110 : // range of stuff that changed based on the old and new caret positions,
111 : // as well as use a range possibly provided by the editor (start and end,
112 : // which are usually nullptr) to get a range with the union of these.
113 :
114 : nsresult
115 0 : mozInlineSpellStatus::InitForEditorChange(
116 : EditAction aAction,
117 : nsIDOMNode* aAnchorNode, int32_t aAnchorOffset,
118 : nsIDOMNode* aPreviousNode, int32_t aPreviousOffset,
119 : nsIDOMNode* aStartNode, int32_t aStartOffset,
120 : nsIDOMNode* aEndNode, int32_t aEndOffset)
121 : {
122 : nsresult rv;
123 :
124 0 : nsCOMPtr<nsIDOMDocument> doc;
125 0 : rv = GetDocument(getter_AddRefs(doc));
126 0 : NS_ENSURE_SUCCESS(rv, rv);
127 :
128 : // save the anchor point as a range so we can find the current word later
129 0 : rv = PositionToCollapsedRange(doc, aAnchorNode, aAnchorOffset,
130 0 : getter_AddRefs(mAnchorRange));
131 0 : NS_ENSURE_SUCCESS(rv, rv);
132 :
133 0 : nsCOMPtr<nsINode> prevNode = do_QueryInterface(aPreviousNode);
134 0 : NS_ENSURE_STATE(prevNode);
135 :
136 0 : bool deleted = aAction == EditAction::deleteSelection;
137 0 : if (aAction == EditAction::insertIMEText) {
138 : // IME may remove the previous node if it cancels composition when
139 : // there is no text around the composition.
140 0 : deleted = !prevNode->IsInComposedDoc();
141 : }
142 :
143 0 : if (deleted) {
144 : // Deletes are easy, the range is just the current anchor. We set the range
145 : // to check to be empty, FinishInitOnEvent will fill in the range to be
146 : // the current word.
147 0 : mOp = eOpChangeDelete;
148 0 : mRange = nullptr;
149 0 : return NS_OK;
150 : }
151 :
152 0 : mOp = eOpChange;
153 :
154 : // range to check
155 0 : mRange = new nsRange(prevNode);
156 :
157 : // ...we need to put the start and end in the correct order
158 : int16_t cmpResult;
159 0 : rv = mAnchorRange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult);
160 0 : NS_ENSURE_SUCCESS(rv, rv);
161 0 : if (cmpResult < 0) {
162 : // previous anchor node is before the current anchor
163 0 : nsCOMPtr<nsINode> previousNode = do_QueryInterface(aPreviousNode);
164 0 : nsCOMPtr<nsINode> anchorNode = do_QueryInterface(aAnchorNode);
165 0 : rv = mRange->SetStartAndEnd(previousNode, aPreviousOffset,
166 0 : anchorNode, aAnchorOffset);
167 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
168 0 : return rv;
169 : }
170 : } else {
171 : // previous anchor node is after (or the same as) the current anchor
172 0 : nsCOMPtr<nsINode> previousNode = do_QueryInterface(aPreviousNode);
173 0 : nsCOMPtr<nsINode> anchorNode = do_QueryInterface(aAnchorNode);
174 0 : rv = mRange->SetStartAndEnd(anchorNode, aAnchorOffset,
175 0 : previousNode, aPreviousOffset);
176 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
177 0 : return rv;
178 : }
179 : }
180 :
181 : // On insert save this range: DoSpellCheck optimizes things in this range.
182 : // Otherwise, just leave this nullptr.
183 0 : if (aAction == EditAction::insertText)
184 0 : mCreatedRange = mRange;
185 :
186 : // if we were given a range, we need to expand our range to encompass it
187 0 : if (aStartNode && aEndNode) {
188 0 : rv = mRange->ComparePoint(aStartNode, aStartOffset, &cmpResult);
189 0 : NS_ENSURE_SUCCESS(rv, rv);
190 0 : if (cmpResult < 0) { // given range starts before
191 0 : rv = mRange->SetStart(aStartNode, aStartOffset);
192 0 : NS_ENSURE_SUCCESS(rv, rv);
193 : }
194 :
195 0 : rv = mRange->ComparePoint(aEndNode, aEndOffset, &cmpResult);
196 0 : NS_ENSURE_SUCCESS(rv, rv);
197 0 : if (cmpResult > 0) { // given range ends after
198 0 : rv = mRange->SetEnd(aEndNode, aEndOffset);
199 0 : NS_ENSURE_SUCCESS(rv, rv);
200 : }
201 : }
202 :
203 0 : return NS_OK;
204 : }
205 :
206 : // mozInlineSpellStatis::InitForNavigation
207 : //
208 : // For navigation events, we just need to store the new and old positions.
209 : //
210 : // In some cases, we detect that we shouldn't check. If this event should
211 : // not be processed, *aContinue will be false.
212 :
213 : nsresult
214 0 : mozInlineSpellStatus::InitForNavigation(
215 : bool aForceCheck, int32_t aNewPositionOffset,
216 : nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset,
217 : nsIDOMNode* aNewAnchorNode, int32_t aNewAnchorOffset,
218 : bool* aContinue)
219 : {
220 : nsresult rv;
221 0 : mOp = eOpNavigation;
222 :
223 0 : mForceNavigationWordCheck = aForceCheck;
224 0 : mNewNavigationPositionOffset = aNewPositionOffset;
225 :
226 : // get the root node for checking
227 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
228 0 : NS_ENSURE_SUCCESS(rv, rv);
229 0 : nsCOMPtr<nsIDOMElement> rootElt;
230 0 : rv = editor->GetRootElement(getter_AddRefs(rootElt));
231 0 : NS_ENSURE_SUCCESS(rv, rv);
232 :
233 : // the anchor node might not be in the DOM anymore, check
234 0 : nsCOMPtr<nsINode> root = do_QueryInterface(rootElt, &rv);
235 0 : NS_ENSURE_SUCCESS(rv, rv);
236 0 : nsCOMPtr<nsINode> currentAnchor = do_QueryInterface(aOldAnchorNode, &rv);
237 0 : NS_ENSURE_SUCCESS(rv, rv);
238 0 : if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) {
239 0 : *aContinue = false;
240 0 : return NS_OK;
241 : }
242 :
243 0 : nsCOMPtr<nsIDOMDocument> doc;
244 0 : rv = GetDocument(getter_AddRefs(doc));
245 0 : NS_ENSURE_SUCCESS(rv, rv);
246 :
247 0 : rv = PositionToCollapsedRange(doc, aOldAnchorNode, aOldAnchorOffset,
248 0 : getter_AddRefs(mOldNavigationAnchorRange));
249 0 : NS_ENSURE_SUCCESS(rv, rv);
250 0 : rv = PositionToCollapsedRange(doc, aNewAnchorNode, aNewAnchorOffset,
251 0 : getter_AddRefs(mAnchorRange));
252 0 : NS_ENSURE_SUCCESS(rv, rv);
253 :
254 0 : *aContinue = true;
255 0 : return NS_OK;
256 : }
257 :
258 : // mozInlineSpellStatus::InitForSelection
259 : //
260 : // It is easy for selections since we always re-check the spellcheck
261 : // selection.
262 :
263 : nsresult
264 0 : mozInlineSpellStatus::InitForSelection()
265 : {
266 0 : mOp = eOpSelection;
267 0 : return NS_OK;
268 : }
269 :
270 : // mozInlineSpellStatus::InitForRange
271 : //
272 : // Called to cause the spellcheck of the given range. This will look like
273 : // a change operation over the given range.
274 :
275 : nsresult
276 0 : mozInlineSpellStatus::InitForRange(nsRange* aRange)
277 : {
278 0 : mOp = eOpChange;
279 0 : mRange = aRange;
280 0 : return NS_OK;
281 : }
282 :
283 : // mozInlineSpellStatus::FinishInitOnEvent
284 : //
285 : // Called when the event is triggered to complete initialization that
286 : // might require the WordUtil. This calls to the operation-specific
287 : // initializer, and also sets the range to be the entire element if it
288 : // is nullptr.
289 : //
290 : // Watch out: the range might still be nullptr if there is nothing to do,
291 : // the caller will have to check for this.
292 :
293 : nsresult
294 0 : mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil)
295 : {
296 : nsresult rv;
297 0 : if (! mRange) {
298 0 : rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
299 0 : getter_AddRefs(mRange));
300 0 : NS_ENSURE_SUCCESS(rv, rv);
301 : }
302 :
303 0 : switch (mOp) {
304 : case eOpChange:
305 0 : if (mAnchorRange)
306 0 : return FillNoCheckRangeFromAnchor(aWordUtil);
307 0 : break;
308 : case eOpChangeDelete:
309 0 : if (mAnchorRange) {
310 0 : rv = FillNoCheckRangeFromAnchor(aWordUtil);
311 0 : NS_ENSURE_SUCCESS(rv, rv);
312 : }
313 : // Delete events will have no range for the changed text (because it was
314 : // deleted), and InitForEditorChange will set it to nullptr. Here, we select
315 : // the entire word to cause any underlining to be removed.
316 0 : mRange = mNoCheckRange;
317 0 : break;
318 : case eOpNavigation:
319 0 : return FinishNavigationEvent(aWordUtil);
320 : case eOpSelection:
321 : // this gets special handling in ResumeCheck
322 0 : break;
323 : case eOpResume:
324 : // everything should be initialized already in this case
325 0 : break;
326 : default:
327 0 : NS_NOTREACHED("Bad operation");
328 0 : return NS_ERROR_NOT_INITIALIZED;
329 : }
330 0 : return NS_OK;
331 : }
332 :
333 : // mozInlineSpellStatus::FinishNavigationEvent
334 : //
335 : // This verifies that we need to check the word at the previous caret
336 : // position. Now that we have the word util, we can find the word belonging
337 : // to the previous caret position. If the new position is inside that word,
338 : // we don't want to do anything. In this case, we'll nullptr out mRange so
339 : // that the caller will know not to continue.
340 : //
341 : // Notice that we don't set mNoCheckRange. We check here whether the cursor
342 : // is in the word that needs checking, so it isn't necessary. Plus, the
343 : // spellchecker isn't guaranteed to only check the given word, and it could
344 : // remove the underline from the new word under the cursor.
345 :
346 : nsresult
347 0 : mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil)
348 : {
349 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor);
350 0 : if (! editor)
351 0 : return NS_ERROR_FAILURE; // editor is gone
352 :
353 0 : NS_ASSERTION(mAnchorRange, "No anchor for navigation!");
354 0 : nsCOMPtr<nsIDOMNode> newAnchorNode, oldAnchorNode;
355 : int32_t newAnchorOffset, oldAnchorOffset;
356 :
357 : // get the DOM position of the old caret, the range should be collapsed
358 0 : nsresult rv = mOldNavigationAnchorRange->GetStartContainer(
359 0 : getter_AddRefs(oldAnchorNode));
360 0 : NS_ENSURE_SUCCESS(rv, rv);
361 0 : rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset);
362 0 : NS_ENSURE_SUCCESS(rv, rv);
363 :
364 : // find the word on the old caret position, this is the one that we MAY need
365 : // to check
366 0 : RefPtr<nsRange> oldWord;
367 0 : rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset,
368 0 : getter_AddRefs(oldWord));
369 0 : NS_ENSURE_SUCCESS(rv, rv);
370 :
371 : // aWordUtil.GetRangeForWord flushes pending notifications, check editor again.
372 0 : editor = do_QueryReferent(mSpellChecker->mEditor);
373 0 : if (! editor)
374 0 : return NS_ERROR_FAILURE; // editor is gone
375 :
376 : // get the DOM position of the new caret, the range should be collapsed
377 0 : rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode));
378 0 : NS_ENSURE_SUCCESS(rv, rv);
379 0 : rv = mAnchorRange->GetStartOffset(&newAnchorOffset);
380 0 : NS_ENSURE_SUCCESS(rv, rv);
381 :
382 : // see if the new cursor position is in the word of the old cursor position
383 0 : bool isInRange = false;
384 0 : if (! mForceNavigationWordCheck) {
385 0 : rv = oldWord->IsPointInRange(newAnchorNode,
386 0 : newAnchorOffset + mNewNavigationPositionOffset,
387 0 : &isInRange);
388 0 : NS_ENSURE_SUCCESS(rv, rv);
389 : }
390 :
391 0 : if (isInRange) {
392 : // caller should give up
393 0 : mRange = nullptr;
394 : } else {
395 : // check the old word
396 0 : mRange = oldWord;
397 :
398 : // Once we've spellchecked the current word, we don't need to spellcheck
399 : // for any more navigation events.
400 0 : mSpellChecker->mNeedsCheckAfterNavigation = false;
401 : }
402 0 : return NS_OK;
403 : }
404 :
405 : // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
406 : //
407 : // Given the mAnchorRange object, computes the range of the word it is on
408 : // (if any) and fills that range into mNoCheckRange. This is used for
409 : // change and navigation events to know which word we should skip spell
410 : // checking on
411 :
412 : nsresult
413 0 : mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
414 : mozInlineSpellWordUtil& aWordUtil)
415 : {
416 0 : nsCOMPtr<nsIDOMNode> anchorNode;
417 0 : nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode));
418 0 : NS_ENSURE_SUCCESS(rv, rv);
419 :
420 : int32_t anchorOffset;
421 0 : rv = mAnchorRange->GetStartOffset(&anchorOffset);
422 0 : NS_ENSURE_SUCCESS(rv, rv);
423 :
424 0 : return aWordUtil.GetRangeForWord(anchorNode, anchorOffset,
425 0 : getter_AddRefs(mNoCheckRange));
426 : }
427 :
428 : // mozInlineSpellStatus::GetDocument
429 : //
430 : // Returns the nsIDOMDocument object for the document for the
431 : // current spellchecker.
432 :
433 : nsresult
434 0 : mozInlineSpellStatus::GetDocument(nsIDOMDocument** aDocument)
435 : {
436 : nsresult rv;
437 0 : *aDocument = nullptr;
438 0 : if (! mSpellChecker->mEditor)
439 0 : return NS_ERROR_UNEXPECTED;
440 :
441 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
442 0 : NS_ENSURE_SUCCESS(rv, rv);
443 :
444 0 : nsCOMPtr<nsIDOMDocument> domDoc;
445 0 : rv = editor->GetDocument(getter_AddRefs(domDoc));
446 0 : NS_ENSURE_SUCCESS(rv, rv);
447 0 : NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER);
448 0 : domDoc.forget(aDocument);
449 0 : return NS_OK;
450 : }
451 :
452 : // mozInlineSpellStatus::PositionToCollapsedRange
453 : //
454 : // Converts a given DOM position to a collapsed range covering that
455 : // position. We use ranges to store DOM positions becuase they stay
456 : // updated as the DOM is changed.
457 :
458 : nsresult
459 0 : mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument,
460 : nsIDOMNode* aNode,
461 : int32_t aOffset,
462 : nsRange** aRange)
463 : {
464 0 : *aRange = nullptr;
465 0 : nsCOMPtr<nsINode> documentNode = do_QueryInterface(aDocument);
466 0 : RefPtr<nsRange> range = new nsRange(documentNode);
467 :
468 0 : nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
469 0 : nsresult rv = range->CollapseTo(node, aOffset);
470 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
471 0 : return rv;
472 : }
473 :
474 0 : range.swap(*aRange);
475 0 : return NS_OK;
476 : }
477 :
478 : // mozInlineSpellResume
479 :
480 0 : class mozInlineSpellResume : public Runnable
481 : {
482 : public:
483 0 : mozInlineSpellResume(UniquePtr<mozInlineSpellStatus>&& aStatus,
484 : uint32_t aDisabledAsyncToken)
485 0 : : Runnable("mozInlineSpellResume")
486 : , mDisabledAsyncToken(aDisabledAsyncToken)
487 0 : , mStatus(Move(aStatus))
488 : {
489 0 : }
490 :
491 0 : nsresult Post()
492 : {
493 0 : nsCOMPtr<nsIRunnable> runnable(this);
494 0 : return NS_IdleDispatchToCurrentThread(runnable.forget(), 1000);
495 : }
496 :
497 0 : NS_IMETHOD Run() override
498 : {
499 : // Discard the resumption if the spell checker was disabled after the
500 : // resumption was scheduled.
501 0 : if (mDisabledAsyncToken == mStatus->mSpellChecker->mDisabledAsyncToken) {
502 0 : mStatus->mSpellChecker->ResumeCheck(Move(mStatus));
503 : }
504 0 : return NS_OK;
505 : }
506 :
507 : private:
508 : uint32_t mDisabledAsyncToken;
509 : UniquePtr<mozInlineSpellStatus> mStatus;
510 : };
511 :
512 : // Used as the nsIEditorSpellCheck::InitSpellChecker callback.
513 : class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback
514 : {
515 0 : ~InitEditorSpellCheckCallback() {}
516 : public:
517 : NS_DECL_ISUPPORTS
518 :
519 0 : explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
520 0 : : mSpellChecker(aSpellChecker) {}
521 :
522 0 : NS_IMETHOD EditorSpellCheckDone() override
523 : {
524 0 : return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
525 : }
526 :
527 0 : void Cancel()
528 : {
529 0 : mSpellChecker = nullptr;
530 0 : }
531 :
532 : private:
533 : RefPtr<mozInlineSpellChecker> mSpellChecker;
534 : };
535 0 : NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
536 :
537 :
538 0 : NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
539 0 : NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
540 0 : NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
541 0 : NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
542 0 : NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
543 0 : NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
544 0 : NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
545 0 : NS_INTERFACE_MAP_END
546 :
547 0 : NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
548 0 : NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
549 :
550 0 : NS_IMPL_CYCLE_COLLECTION(mozInlineSpellChecker,
551 : mSpellCheck,
552 : mTreeWalker,
553 : mCurrentSelectionAnchorNode)
554 :
555 : mozInlineSpellChecker::SpellCheckingState
556 : mozInlineSpellChecker::gCanEnableSpellChecking =
557 : mozInlineSpellChecker::SpellCheck_Uninitialized;
558 :
559 0 : mozInlineSpellChecker::mozInlineSpellChecker() :
560 : mNumWordsInSpellSelection(0),
561 : mMaxNumWordsInSpellSelection(250),
562 : mNumPendingSpellChecks(0),
563 : mNumPendingUpdateCurrentDictionary(0),
564 : mDisabledAsyncToken(0),
565 : mNeedsCheckAfterNavigation(false),
566 0 : mFullSpellCheckScheduled(false)
567 : {
568 0 : nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
569 0 : if (prefs)
570 0 : prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection);
571 0 : mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4;
572 0 : }
573 :
574 0 : mozInlineSpellChecker::~mozInlineSpellChecker()
575 : {
576 0 : }
577 :
578 : NS_IMETHODIMP
579 0 : mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck)
580 : {
581 0 : *aSpellCheck = mSpellCheck;
582 0 : NS_IF_ADDREF(*aSpellCheck);
583 0 : return NS_OK;
584 : }
585 :
586 : NS_IMETHODIMP
587 0 : mozInlineSpellChecker::Init(nsIEditor *aEditor)
588 : {
589 0 : mEditor = do_GetWeakReference(aEditor);
590 0 : return NS_OK;
591 : }
592 :
593 : // mozInlineSpellChecker::Cleanup
594 : //
595 : // Called by the editor when the editor is going away. This is important
596 : // because we remove listeners. We do NOT clean up anything else in this
597 : // function, because it can get called while DoSpellCheck is running!
598 : //
599 : // Getting the style information there can cause DOM notifications to be
600 : // flushed, which can cause editors to go away which will bring us here.
601 : // We can not do anything that will cause DoSpellCheck to freak out.
602 :
603 0 : nsresult mozInlineSpellChecker::Cleanup(bool aDestroyingFrames)
604 : {
605 0 : mNumWordsInSpellSelection = 0;
606 0 : nsCOMPtr<nsISelection> spellCheckSelection;
607 0 : nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
608 0 : if (NS_FAILED(rv)) {
609 : // Ensure we still unregister event listeners (but return a failure code)
610 0 : UnregisterEventListeners();
611 : } else {
612 0 : if (!aDestroyingFrames) {
613 0 : spellCheckSelection->RemoveAllRanges();
614 : }
615 :
616 0 : rv = UnregisterEventListeners();
617 : }
618 :
619 : // Notify ENDED observers now. If we wait to notify as we normally do when
620 : // these async operations finish, then in the meantime the editor may create
621 : // another inline spell checker and cause more STARTED and ENDED
622 : // notifications to be broadcast. Interleaved notifications for the same
623 : // editor but different inline spell checkers could easily confuse
624 : // observers. They may receive two consecutive STARTED notifications for
625 : // example, which we guarantee will not happen.
626 :
627 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
628 0 : if (mPendingSpellCheck) {
629 : // Cancel the pending editor spell checker initialization.
630 0 : mPendingSpellCheck = nullptr;
631 0 : mPendingInitEditorSpellCheckCallback->Cancel();
632 0 : mPendingInitEditorSpellCheckCallback = nullptr;
633 0 : ChangeNumPendingSpellChecks(-1, editor);
634 : }
635 :
636 : // Increment this token so that pending UpdateCurrentDictionary calls and
637 : // scheduled spell checks are discarded when they finish.
638 0 : mDisabledAsyncToken++;
639 :
640 0 : if (mNumPendingUpdateCurrentDictionary > 0) {
641 : // Account for pending UpdateCurrentDictionary calls.
642 0 : ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary, editor);
643 0 : mNumPendingUpdateCurrentDictionary = 0;
644 : }
645 0 : if (mNumPendingSpellChecks > 0) {
646 : // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
647 : // pending scheduled spell checks.
648 0 : ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editor);
649 : }
650 :
651 0 : mEditor = nullptr;
652 0 : mFullSpellCheckScheduled = false;
653 :
654 0 : return rv;
655 : }
656 :
657 : // mozInlineSpellChecker::CanEnableInlineSpellChecking
658 : //
659 : // This function can be called to see if it seems likely that we can enable
660 : // spellchecking before actually creating the InlineSpellChecking objects.
661 : //
662 : // The problem is that we can't get the dictionary list without actually
663 : // creating a whole bunch of spellchecking objects. This function tries to
664 : // do that and caches the result so we don't have to keep allocating those
665 : // objects if there are no dictionaries or spellchecking.
666 : //
667 : // Whenever dictionaries are added or removed at runtime, this value must be
668 : // updated before an observer notification is sent out about the change, to
669 : // avoid editors getting a wrong cached result.
670 :
671 : bool // static
672 0 : mozInlineSpellChecker::CanEnableInlineSpellChecking()
673 : {
674 : nsresult rv;
675 0 : if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
676 0 : gCanEnableSpellChecking = SpellCheck_NotAvailable;
677 :
678 : nsCOMPtr<nsIEditorSpellCheck> spellchecker =
679 0 : do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv);
680 0 : NS_ENSURE_SUCCESS(rv, false);
681 :
682 0 : bool canSpellCheck = false;
683 0 : rv = spellchecker->CanSpellCheck(&canSpellCheck);
684 0 : NS_ENSURE_SUCCESS(rv, false);
685 :
686 0 : if (canSpellCheck)
687 0 : gCanEnableSpellChecking = SpellCheck_Available;
688 : }
689 0 : return (gCanEnableSpellChecking == SpellCheck_Available);
690 : }
691 :
692 : void // static
693 1 : mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking()
694 : {
695 1 : gCanEnableSpellChecking = SpellCheck_Uninitialized;
696 1 : }
697 :
698 : // mozInlineSpellChecker::RegisterEventListeners
699 : //
700 : // The inline spell checker listens to mouse events and keyboard navigation
701 : // events.
702 :
703 : nsresult
704 0 : mozInlineSpellChecker::RegisterEventListeners()
705 : {
706 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
707 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
708 :
709 0 : editor->AddEditActionListener(this);
710 :
711 0 : nsCOMPtr<nsIDOMDocument> doc;
712 0 : nsresult rv = editor->GetDocument(getter_AddRefs(doc));
713 0 : NS_ENSURE_SUCCESS(rv, rv);
714 :
715 0 : nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc, &rv);
716 0 : NS_ENSURE_SUCCESS(rv, rv);
717 :
718 0 : piTarget->AddEventListener(NS_LITERAL_STRING("blur"), this,
719 0 : true, false);
720 0 : piTarget->AddEventListener(NS_LITERAL_STRING("click"), this,
721 0 : false, false);
722 0 : piTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this,
723 0 : false, false);
724 0 : return NS_OK;
725 : }
726 :
727 : // mozInlineSpellChecker::UnregisterEventListeners
728 :
729 : nsresult
730 0 : mozInlineSpellChecker::UnregisterEventListeners()
731 : {
732 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
733 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
734 :
735 0 : editor->RemoveEditActionListener(this);
736 :
737 0 : nsCOMPtr<nsIDOMDocument> doc;
738 0 : editor->GetDocument(getter_AddRefs(doc));
739 0 : NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
740 :
741 0 : nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc);
742 0 : NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER);
743 :
744 0 : piTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true);
745 0 : piTarget->RemoveEventListener(NS_LITERAL_STRING("click"), this, false);
746 0 : piTarget->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, false);
747 0 : return NS_OK;
748 : }
749 :
750 : // mozInlineSpellChecker::GetEnableRealTimeSpell
751 :
752 : NS_IMETHODIMP
753 0 : mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled)
754 : {
755 0 : NS_ENSURE_ARG_POINTER(aEnabled);
756 0 : *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
757 0 : return NS_OK;
758 : }
759 :
760 : // mozInlineSpellChecker::SetEnableRealTimeSpell
761 :
762 : NS_IMETHODIMP
763 0 : mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled)
764 : {
765 0 : if (!aEnabled) {
766 0 : mSpellCheck = nullptr;
767 0 : return Cleanup(false);
768 : }
769 :
770 0 : if (mSpellCheck) {
771 : // spellcheck the current contents. SpellCheckRange doesn't supply a created
772 : // range to DoSpellCheck, which in our case is the entire range. But this
773 : // optimization doesn't matter because there is nothing in the spellcheck
774 : // selection when starting, which triggers a better optimization.
775 0 : return SpellCheckRange(nullptr);
776 : }
777 :
778 0 : if (mPendingSpellCheck) {
779 : // The editor spell checker is already being initialized.
780 0 : return NS_OK;
781 : }
782 :
783 : mPendingSpellCheck =
784 0 : do_CreateInstance("@mozilla.org/editor/editorspellchecker;1");
785 0 : NS_ENSURE_STATE(mPendingSpellCheck);
786 :
787 : nsCOMPtr<nsITextServicesFilter> filter =
788 0 : do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1");
789 0 : if (!filter) {
790 0 : mPendingSpellCheck = nullptr;
791 0 : NS_ENSURE_STATE(filter);
792 : }
793 0 : mPendingSpellCheck->SetFilter(filter);
794 :
795 0 : mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
796 0 : if (!mPendingInitEditorSpellCheckCallback) {
797 0 : mPendingSpellCheck = nullptr;
798 0 : NS_ENSURE_STATE(mPendingInitEditorSpellCheckCallback);
799 : }
800 :
801 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
802 0 : nsresult rv = mPendingSpellCheck->InitSpellChecker(
803 0 : editor, false, mPendingInitEditorSpellCheckCallback);
804 0 : if (NS_FAILED(rv)) {
805 0 : mPendingSpellCheck = nullptr;
806 0 : mPendingInitEditorSpellCheckCallback = nullptr;
807 0 : NS_ENSURE_SUCCESS(rv, rv);
808 : }
809 :
810 0 : ChangeNumPendingSpellChecks(1);
811 :
812 0 : return NS_OK;
813 : }
814 :
815 : // Called when nsIEditorSpellCheck::InitSpellChecker completes.
816 : nsresult
817 0 : mozInlineSpellChecker::EditorSpellCheckInited()
818 : {
819 0 : NS_ASSERTION(mPendingSpellCheck, "Spell check should be pending!");
820 :
821 : // spell checking is enabled, register our event listeners to track navigation
822 0 : RegisterEventListeners();
823 :
824 0 : mSpellCheck = mPendingSpellCheck;
825 0 : mPendingSpellCheck = nullptr;
826 0 : mPendingInitEditorSpellCheckCallback = nullptr;
827 0 : ChangeNumPendingSpellChecks(-1);
828 :
829 : // spellcheck the current contents. SpellCheckRange doesn't supply a created
830 : // range to DoSpellCheck, which in our case is the entire range. But this
831 : // optimization doesn't matter because there is nothing in the spellcheck
832 : // selection when starting, which triggers a better optimization.
833 0 : return SpellCheckRange(nullptr);
834 : }
835 :
836 : // Changes the number of pending spell checks by the given delta. If the number
837 : // becomes zero or nonzero, observers are notified. See NotifyObservers for
838 : // info on the aEditor parameter.
839 : void
840 0 : mozInlineSpellChecker::ChangeNumPendingSpellChecks(int32_t aDelta,
841 : nsIEditor* aEditor)
842 : {
843 0 : int8_t oldNumPending = mNumPendingSpellChecks;
844 0 : mNumPendingSpellChecks += aDelta;
845 0 : NS_ASSERTION(mNumPendingSpellChecks >= 0,
846 : "Unbalanced ChangeNumPendingSpellChecks calls!");
847 0 : if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
848 0 : NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditor);
849 0 : } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
850 0 : NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditor);
851 : }
852 0 : }
853 :
854 : // Broadcasts the given topic to observers. aEditor is passed to observers if
855 : // nonnull; otherwise mEditor is passed.
856 : void
857 0 : mozInlineSpellChecker::NotifyObservers(const char* aTopic, nsIEditor* aEditor)
858 : {
859 0 : nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
860 0 : if (!os)
861 0 : return;
862 0 : nsCOMPtr<nsIEditor> editor = aEditor;
863 0 : if (!editor) {
864 0 : editor = do_QueryReferent(mEditor);
865 : }
866 0 : os->NotifyObservers(editor, aTopic, nullptr);
867 : }
868 :
869 : // mozInlineSpellChecker::SpellCheckAfterEditorChange
870 : //
871 : // Called by the editor when nearly anything happens to change the content.
872 : //
873 : // The start and end positions specify a range for the thing that happened,
874 : // but these are usually nullptr, even when you'd think they would be useful
875 : // because you want the range (for example, pasting). We ignore them in
876 : // this case.
877 :
878 : NS_IMETHODIMP
879 0 : mozInlineSpellChecker::SpellCheckAfterEditorChange(
880 : int32_t aAction, nsISelection *aSelection,
881 : nsIDOMNode *aPreviousSelectedNode, int32_t aPreviousSelectedOffset,
882 : nsIDOMNode *aStartNode, int32_t aStartOffset,
883 : nsIDOMNode *aEndNode, int32_t aEndOffset)
884 : {
885 : nsresult rv;
886 0 : NS_ENSURE_ARG_POINTER(aSelection);
887 0 : if (!mSpellCheck)
888 0 : return NS_OK; // disabling spell checking is not an error
889 :
890 : // this means something has changed, and we never check the current word,
891 : // therefore, we should spellcheck for subsequent caret navigations
892 0 : mNeedsCheckAfterNavigation = true;
893 :
894 : // the anchor node is the position of the caret
895 0 : nsCOMPtr<nsIDOMNode> anchorNode;
896 0 : rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode));
897 0 : NS_ENSURE_SUCCESS(rv, rv);
898 : int32_t anchorOffset;
899 0 : rv = aSelection->GetAnchorOffset(&anchorOffset);
900 0 : NS_ENSURE_SUCCESS(rv, rv);
901 :
902 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
903 0 : rv = status->InitForEditorChange((EditAction)aAction,
904 : anchorNode, anchorOffset,
905 : aPreviousSelectedNode,
906 : aPreviousSelectedOffset,
907 : aStartNode, aStartOffset,
908 0 : aEndNode, aEndOffset);
909 0 : NS_ENSURE_SUCCESS(rv, rv);
910 0 : rv = ScheduleSpellCheck(Move(status));
911 0 : NS_ENSURE_SUCCESS(rv, rv);
912 :
913 : // remember the current caret position after every change
914 0 : SaveCurrentSelectionPosition();
915 0 : return NS_OK;
916 : }
917 :
918 : // mozInlineSpellChecker::SpellCheckRange
919 : //
920 : // Spellchecks all the words in the given range.
921 : // Supply a nullptr range and this will check the entire editor.
922 :
923 : nsresult
924 0 : mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange)
925 : {
926 0 : if (!mSpellCheck) {
927 0 : NS_WARNING_ASSERTION(
928 : mPendingSpellCheck,
929 : "Trying to spellcheck, but checking seems to be disabled");
930 0 : return NS_ERROR_NOT_INITIALIZED;
931 : }
932 :
933 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
934 0 : nsRange* range = static_cast<nsRange*>(aRange);
935 0 : nsresult rv = status->InitForRange(range);
936 0 : NS_ENSURE_SUCCESS(rv, rv);
937 0 : return ScheduleSpellCheck(Move(status));
938 : }
939 :
940 : // mozInlineSpellChecker::GetMisspelledWord
941 :
942 : NS_IMETHODIMP
943 0 : mozInlineSpellChecker::GetMisspelledWord(nsIDOMNode *aNode, int32_t aOffset,
944 : nsIDOMRange **newword)
945 : {
946 0 : NS_ENSURE_ARG_POINTER(aNode);
947 0 : nsCOMPtr<nsISelection> spellCheckSelection;
948 0 : nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
949 0 : NS_ENSURE_SUCCESS(res, res);
950 :
951 0 : return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword);
952 : }
953 :
954 : // mozInlineSpellChecker::ReplaceWord
955 :
956 : NS_IMETHODIMP
957 0 : mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, int32_t aOffset,
958 : const nsAString &newword)
959 : {
960 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
961 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
962 0 : NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
963 :
964 0 : nsCOMPtr<nsIDOMRange> range;
965 0 : nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
966 0 : NS_ENSURE_SUCCESS(res, res);
967 :
968 0 : if (range)
969 : {
970 : // This range was retrieved from the spellchecker selection. As
971 : // ranges cannot be shared between selections, we must clone it
972 : // before adding it to the editor's selection.
973 0 : nsCOMPtr<nsIDOMRange> editorRange;
974 0 : res = range->CloneRange(getter_AddRefs(editorRange));
975 0 : NS_ENSURE_SUCCESS(res, res);
976 :
977 0 : AutoPlaceHolderBatch phb(editor, nullptr);
978 :
979 0 : nsCOMPtr<nsISelection> selection;
980 0 : res = editor->GetSelection(getter_AddRefs(selection));
981 0 : NS_ENSURE_SUCCESS(res, res);
982 0 : selection->RemoveAllRanges();
983 0 : selection->AddRange(editorRange);
984 :
985 0 : nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
986 0 : if (textEditor)
987 0 : textEditor->InsertText(newword);
988 : }
989 :
990 0 : return NS_OK;
991 : }
992 :
993 : // mozInlineSpellChecker::AddWordToDictionary
994 :
995 : NS_IMETHODIMP
996 0 : mozInlineSpellChecker::AddWordToDictionary(const nsAString &word)
997 : {
998 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
999 :
1000 0 : nsAutoString wordstr(word);
1001 0 : nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get());
1002 0 : NS_ENSURE_SUCCESS(rv, rv);
1003 :
1004 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1005 0 : rv = status->InitForSelection();
1006 0 : NS_ENSURE_SUCCESS(rv, rv);
1007 0 : return ScheduleSpellCheck(Move(status));
1008 : }
1009 :
1010 : // mozInlineSpellChecker::RemoveWordFromDictionary
1011 :
1012 : NS_IMETHODIMP
1013 0 : mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word)
1014 : {
1015 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
1016 :
1017 0 : nsAutoString wordstr(word);
1018 0 : nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get());
1019 0 : NS_ENSURE_SUCCESS(rv, rv);
1020 :
1021 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1022 0 : rv = status->InitForRange(nullptr);
1023 0 : NS_ENSURE_SUCCESS(rv, rv);
1024 0 : return ScheduleSpellCheck(Move(status));
1025 : }
1026 :
1027 : // mozInlineSpellChecker::IgnoreWord
1028 :
1029 : NS_IMETHODIMP
1030 0 : mozInlineSpellChecker::IgnoreWord(const nsAString &word)
1031 : {
1032 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
1033 :
1034 0 : nsAutoString wordstr(word);
1035 0 : nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get());
1036 0 : NS_ENSURE_SUCCESS(rv, rv);
1037 :
1038 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1039 0 : rv = status->InitForSelection();
1040 0 : NS_ENSURE_SUCCESS(rv, rv);
1041 0 : return ScheduleSpellCheck(Move(status));
1042 : }
1043 :
1044 : // mozInlineSpellChecker::IgnoreWords
1045 :
1046 : NS_IMETHODIMP
1047 0 : mozInlineSpellChecker::IgnoreWords(const char16_t **aWordsToIgnore,
1048 : uint32_t aCount)
1049 : {
1050 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
1051 :
1052 : // add each word to the ignore list and then recheck the document
1053 0 : for (uint32_t index = 0; index < aCount; index++)
1054 0 : mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]);
1055 :
1056 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1057 0 : nsresult rv = status->InitForSelection();
1058 0 : NS_ENSURE_SUCCESS(rv, rv);
1059 0 : return ScheduleSpellCheck(Move(status));
1060 : }
1061 :
1062 0 : NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, int32_t aPosition)
1063 : {
1064 0 : return NS_OK;
1065 : }
1066 :
1067 0 : NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent,
1068 : int32_t aPosition, nsresult aResult)
1069 : {
1070 0 : return NS_OK;
1071 : }
1072 :
1073 0 : NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
1074 : int32_t aPosition)
1075 : {
1076 0 : return NS_OK;
1077 : }
1078 :
1079 0 : NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
1080 : int32_t aPosition, nsresult aResult)
1081 : {
1082 :
1083 0 : return NS_OK;
1084 : }
1085 :
1086 0 : NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild)
1087 : {
1088 0 : return NS_OK;
1089 : }
1090 :
1091 0 : NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
1092 : {
1093 0 : return NS_OK;
1094 : }
1095 :
1096 0 : NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, int32_t aOffset)
1097 : {
1098 0 : return NS_OK;
1099 : }
1100 :
1101 : NS_IMETHODIMP
1102 0 : mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode,
1103 : int32_t aOffset,
1104 : nsIDOMNode *aNewLeftNode, nsresult aResult)
1105 : {
1106 0 : return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
1107 : }
1108 :
1109 0 : NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent)
1110 : {
1111 0 : return NS_OK;
1112 : }
1113 :
1114 0 : NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode,
1115 : nsIDOMNode *aParent, nsresult aResult)
1116 : {
1117 0 : return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0);
1118 : }
1119 :
1120 0 : NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString & aString)
1121 : {
1122 0 : return NS_OK;
1123 : }
1124 :
1125 0 : NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset,
1126 : const nsAString & aString, nsresult aResult)
1127 : {
1128 0 : return NS_OK;
1129 : }
1130 :
1131 0 : NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength)
1132 : {
1133 0 : return NS_OK;
1134 : }
1135 :
1136 0 : NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult)
1137 : {
1138 0 : return NS_OK;
1139 : }
1140 :
1141 0 : NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection)
1142 : {
1143 0 : return NS_OK;
1144 : }
1145 :
1146 0 : NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection)
1147 : {
1148 0 : return NS_OK;
1149 : }
1150 :
1151 : // mozInlineSpellChecker::MakeSpellCheckRange
1152 : //
1153 : // Given begin and end positions, this function constructs a range as
1154 : // required for ScheduleSpellCheck. If the start and end nodes are nullptr,
1155 : // then the entire range will be selected, and you can supply -1 as the
1156 : // offset to the end range to select all of that node.
1157 : //
1158 : // If the resulting range would be empty, nullptr is put into *aRange and the
1159 : // function succeeds.
1160 :
1161 : nsresult
1162 0 : mozInlineSpellChecker::MakeSpellCheckRange(
1163 : nsIDOMNode* aStartNode, int32_t aStartOffset,
1164 : nsIDOMNode* aEndNode, int32_t aEndOffset,
1165 : nsRange** aRange)
1166 : {
1167 : nsresult rv;
1168 0 : *aRange = nullptr;
1169 :
1170 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1171 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1172 :
1173 0 : nsCOMPtr<nsIDOMDocument> doc;
1174 0 : rv = editor->GetDocument(getter_AddRefs(doc));
1175 0 : NS_ENSURE_SUCCESS(rv, rv);
1176 0 : NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
1177 :
1178 0 : nsCOMPtr<nsINode> documentNode = do_QueryInterface(doc);
1179 0 : RefPtr<nsRange> range = new nsRange(documentNode);
1180 :
1181 : // possibly use full range of the editor
1182 0 : nsCOMPtr<nsIDOMElement> rootElem;
1183 0 : if (! aStartNode || ! aEndNode) {
1184 0 : rv = editor->GetRootElement(getter_AddRefs(rootElem));
1185 0 : NS_ENSURE_SUCCESS(rv, rv);
1186 :
1187 0 : aStartNode = rootElem;
1188 0 : aStartOffset = 0;
1189 :
1190 0 : aEndNode = rootElem;
1191 0 : aEndOffset = -1;
1192 : }
1193 :
1194 0 : if (aEndOffset == -1) {
1195 0 : nsCOMPtr<nsIDOMNodeList> childNodes;
1196 0 : rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes));
1197 0 : NS_ENSURE_SUCCESS(rv, rv);
1198 :
1199 : uint32_t childCount;
1200 0 : rv = childNodes->GetLength(&childCount);
1201 0 : NS_ENSURE_SUCCESS(rv, rv);
1202 :
1203 0 : aEndOffset = childCount;
1204 : }
1205 :
1206 : // sometimes we are are requested to check an empty range (possibly an empty
1207 : // document). This will result in assertions later.
1208 0 : if (aStartNode == aEndNode && aStartOffset == aEndOffset)
1209 0 : return NS_OK;
1210 :
1211 0 : nsCOMPtr<nsINode> startNode = do_QueryInterface(aStartNode);
1212 0 : nsCOMPtr<nsINode> endNode = do_QueryInterface(aEndNode);
1213 0 : if (aEndOffset) {
1214 0 : rv = range->SetStartAndEnd(startNode, aStartOffset, endNode, aEndOffset);
1215 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
1216 0 : return rv;
1217 : }
1218 : } else {
1219 0 : int32_t endOffset = -1;
1220 0 : endNode = nsRange::GetContainerAndOffsetAfter(endNode, &endOffset);
1221 0 : rv = range->SetStartAndEnd(startNode, aStartOffset, endNode, endOffset);
1222 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
1223 0 : return rv;
1224 : }
1225 : }
1226 :
1227 0 : range.swap(*aRange);
1228 0 : return NS_OK;
1229 : }
1230 :
1231 : nsresult
1232 0 : mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
1233 : int32_t aStartOffset,
1234 : nsIDOMNode *aEndNode,
1235 : int32_t aEndOffset)
1236 : {
1237 0 : RefPtr<nsRange> range;
1238 0 : nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset,
1239 : aEndNode, aEndOffset,
1240 0 : getter_AddRefs(range));
1241 0 : NS_ENSURE_SUCCESS(rv, rv);
1242 :
1243 0 : if (! range)
1244 0 : return NS_OK; // range is empty: nothing to do
1245 :
1246 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1247 0 : rv = status->InitForRange(range);
1248 0 : NS_ENSURE_SUCCESS(rv, rv);
1249 0 : return ScheduleSpellCheck(Move(status));
1250 : }
1251 :
1252 : // mozInlineSpellChecker::ShouldSpellCheckNode
1253 : //
1254 : // There are certain conditions when we don't want to spell check a node. In
1255 : // particular quotations, moz signatures, etc. This routine returns false
1256 : // for these cases.
1257 :
1258 : bool
1259 0 : mozInlineSpellChecker::ShouldSpellCheckNode(nsIEditor* aEditor,
1260 : nsINode *aNode)
1261 : {
1262 0 : MOZ_ASSERT(aNode);
1263 0 : if (!aNode->IsContent())
1264 0 : return false;
1265 :
1266 0 : nsIContent *content = aNode->AsContent();
1267 :
1268 : uint32_t flags;
1269 0 : aEditor->GetFlags(&flags);
1270 0 : if (flags & nsIPlaintextEditor::eEditorMailMask) {
1271 0 : nsIContent *parent = content->GetParent();
1272 0 : while (parent) {
1273 0 : if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
1274 0 : parent->AttrValueIs(kNameSpaceID_None,
1275 : nsGkAtoms::type,
1276 : nsGkAtoms::cite,
1277 : eIgnoreCase)) {
1278 0 : return false;
1279 : }
1280 0 : if (parent->IsHTMLElement(nsGkAtoms::pre) &&
1281 0 : parent->AttrValueIs(kNameSpaceID_None,
1282 : nsGkAtoms::_class,
1283 : nsGkAtoms::mozsignature,
1284 : eIgnoreCase)) {
1285 0 : return false;
1286 : }
1287 :
1288 0 : parent = parent->GetParent();
1289 : }
1290 : } else {
1291 : // Check spelling only if the node is editable, and GetSpellcheck() is true
1292 : // on the nearest HTMLElement ancestor.
1293 0 : if (!content->IsEditable()) {
1294 0 : return false;
1295 : }
1296 :
1297 : // Make sure that we can always turn on spell checking for inputs/textareas.
1298 : // Note that because of the previous check, at this point we know that the
1299 : // node is editable.
1300 0 : if (content->IsInAnonymousSubtree()) {
1301 0 : nsIContent *node = content->GetParent();
1302 0 : while (node && node->IsInNativeAnonymousSubtree()) {
1303 0 : node = node->GetParent();
1304 : }
1305 0 : nsCOMPtr<nsITextControlElement> textControl = do_QueryInterface(node);
1306 0 : if (textControl) {
1307 0 : return true;
1308 : }
1309 : }
1310 :
1311 : // Get HTML element ancestor (might be aNode itself, although probably that
1312 : // has to be a text node in real life here)
1313 0 : nsIContent *parent = content;
1314 0 : while (!parent->IsHTMLElement()) {
1315 0 : parent = parent->GetParent();
1316 0 : if (!parent) {
1317 0 : return true;
1318 : }
1319 : }
1320 :
1321 : // See if it's spellcheckable
1322 0 : return static_cast<nsGenericHTMLElement *>(parent)->Spellcheck();
1323 : }
1324 :
1325 0 : return true;
1326 : }
1327 :
1328 : // mozInlineSpellChecker::ScheduleSpellCheck
1329 : //
1330 : // This is called by code to do the actual spellchecking. We will set up
1331 : // the proper structures for calls to DoSpellCheck.
1332 :
1333 : nsresult
1334 0 : mozInlineSpellChecker::ScheduleSpellCheck(UniquePtr<mozInlineSpellStatus>&& aStatus)
1335 : {
1336 0 : if (mFullSpellCheckScheduled) {
1337 : // Just ignore this; we're going to spell-check everything anyway
1338 0 : return NS_OK;
1339 : }
1340 : // Cache the value because we are going to move aStatus's ownership to
1341 : // the new created mozInlineSpellResume instance.
1342 0 : bool isFullSpellCheck = aStatus->IsFullSpellCheck();
1343 :
1344 : RefPtr<mozInlineSpellResume> resume =
1345 0 : new mozInlineSpellResume(Move(aStatus), mDisabledAsyncToken);
1346 0 : NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1347 :
1348 0 : nsresult rv = resume->Post();
1349 0 : if (NS_SUCCEEDED(rv)) {
1350 0 : if (isFullSpellCheck) {
1351 : // We're going to check everything. Suppress further spell-check attempts
1352 : // until that happens.
1353 0 : mFullSpellCheckScheduled = true;
1354 : }
1355 0 : ChangeNumPendingSpellChecks(1);
1356 : }
1357 0 : return rv;
1358 : }
1359 :
1360 : // mozInlineSpellChecker::DoSpellCheckSelection
1361 : //
1362 : // Called to re-check all misspelled words. We iterate over all ranges in
1363 : // the selection and call DoSpellCheck on them. This is used when a word
1364 : // is ignored or added to the dictionary: all instances of that word should
1365 : // be removed from the selection.
1366 : //
1367 : // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1368 : // Typically, checking this small amount of text is relatively fast, but
1369 : // for large numbers of words, a lag may be noticeable.
1370 :
1371 : nsresult
1372 0 : mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
1373 : Selection* aSpellCheckSelection)
1374 : {
1375 : nsresult rv;
1376 :
1377 : // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1378 0 : mNumWordsInSpellSelection = 0;
1379 :
1380 : // Since we could be modifying the ranges for the spellCheckSelection while
1381 : // looping on the spell check selection, keep a separate array of range
1382 : // elements inside the selection
1383 0 : nsTArray<RefPtr<nsRange>> ranges;
1384 :
1385 0 : int32_t count = aSpellCheckSelection->RangeCount();
1386 :
1387 0 : for (int32_t idx = 0; idx < count; idx++) {
1388 0 : nsRange *range = aSpellCheckSelection->GetRangeAt(idx);
1389 0 : if (range) {
1390 0 : ranges.AppendElement(range);
1391 : }
1392 : }
1393 :
1394 : // We have saved the ranges above. Clearing the spellcheck selection here
1395 : // isn't necessary (rechecking each word will modify it as necessary) but
1396 : // provides better performance. By ensuring that no ranges need to be
1397 : // removed in DoSpellCheck, we can save checking range inclusion which is
1398 : // slow.
1399 0 : aSpellCheckSelection->RemoveAllRanges();
1400 :
1401 : // We use this state object for all calls, and just update its range. Note
1402 : // that we don't need to call FinishInit since we will be filling in the
1403 : // necessary information.
1404 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1405 0 : rv = status->InitForRange(nullptr);
1406 0 : NS_ENSURE_SUCCESS(rv, rv);
1407 :
1408 : bool doneChecking;
1409 0 : for (int32_t idx = 0; idx < count; idx++) {
1410 : // We can consider this word as "added" since we know it has no spell
1411 : // check range over it that needs to be deleted. All the old ranges
1412 : // were cleared above. We also need to clear the word count so that we
1413 : // check all words instead of stopping early.
1414 0 : status->mRange = ranges[idx];
1415 : rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, status,
1416 0 : &doneChecking);
1417 0 : NS_ENSURE_SUCCESS(rv, rv);
1418 0 : MOZ_ASSERT(doneChecking,
1419 : "We gave the spellchecker one word, but it didn't finish checking?!?!");
1420 :
1421 0 : status->mWordCount = 0;
1422 : }
1423 :
1424 0 : return NS_OK;
1425 : }
1426 :
1427 : // mozInlineSpellChecker::DoSpellCheck
1428 : //
1429 : // This function checks words intersecting the given range, excluding those
1430 : // inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
1431 : // will have any spell selection removed (this is used to hide the
1432 : // underlining for the word that the caret is in). aNoCheckRange should be
1433 : // on word boundaries.
1434 : //
1435 : // mResume->mCreatedRange is a possibly nullptr range of new text that was
1436 : // inserted. Inside this range, we don't bother to check whether things are
1437 : // inside the spellcheck selection, which speeds up large paste operations
1438 : // considerably.
1439 : //
1440 : // Normal case when editing text by typing
1441 : // h e l l o w o r k d h o w a r e y o u
1442 : // ^ caret
1443 : // [-------] mRange
1444 : // [-------] mNoCheckRange
1445 : // -> does nothing (range is the same as the no check range)
1446 : //
1447 : // Case when pasting:
1448 : // [---------- pasted text ----------]
1449 : // h e l l o w o r k d h o w a r e y o u
1450 : // ^ caret
1451 : // [---] aNoCheckRange
1452 : // -> recheck all words in range except those in aNoCheckRange
1453 : //
1454 : // If checking is complete, *aDoneChecking will be set. If there is more
1455 : // but we ran out of time, this will be false and the range will be
1456 : // updated with the stuff that still needs checking.
1457 :
1458 0 : nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
1459 : Selection *aSpellCheckSelection,
1460 : const UniquePtr<mozInlineSpellStatus>& aStatus,
1461 : bool* aDoneChecking)
1462 : {
1463 0 : *aDoneChecking = true;
1464 :
1465 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
1466 :
1467 : // get the editor for ShouldSpellCheckNode, this may fail in reasonable
1468 : // circumstances since the editor could have gone away
1469 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1470 0 : if (! editor)
1471 0 : return NS_ERROR_FAILURE;
1472 :
1473 0 : if (aStatus->mRange->Collapsed())
1474 0 : return NS_OK;
1475 :
1476 : // see if the selection has any ranges, if not, then we can optimize checking
1477 : // range inclusion later (we have no ranges when we are initially checking or
1478 : // when there are no misspelled words yet).
1479 0 : int32_t originalRangeCount = aSpellCheckSelection->RangeCount();
1480 :
1481 : // set the starting DOM position to be the beginning of our range
1482 : {
1483 : // Scope for the node/offset pairs here so they don't get
1484 : // accidentally used later
1485 0 : nsINode* beginNode = aStatus->mRange->GetStartContainer();
1486 0 : int32_t beginOffset = aStatus->mRange->StartOffset();
1487 0 : nsINode* endNode = aStatus->mRange->GetEndContainer();
1488 0 : int32_t endOffset = aStatus->mRange->EndOffset();
1489 :
1490 : // Now check that we're still looking at a range that's under
1491 : // aWordUtil.GetRootNode()
1492 0 : nsINode* rootNode = aWordUtil.GetRootNode();
1493 0 : if (!nsContentUtils::ContentIsDescendantOf(beginNode, rootNode) ||
1494 0 : !nsContentUtils::ContentIsDescendantOf(endNode, rootNode)) {
1495 : // Just bail out and don't try to spell-check this
1496 0 : return NS_OK;
1497 : }
1498 :
1499 0 : aWordUtil.SetEnd(endNode, endOffset);
1500 0 : aWordUtil.SetPosition(beginNode, beginOffset);
1501 : }
1502 :
1503 : // aWordUtil.SetPosition flushes pending notifications, check editor again.
1504 : // XXX Uhhh, *we're* holding a strong ref to the editor.
1505 0 : editor = do_QueryReferent(mEditor);
1506 0 : if (! editor)
1507 0 : return NS_ERROR_FAILURE;
1508 :
1509 0 : int32_t wordsChecked = 0;
1510 0 : PRTime beginTime = PR_Now();
1511 :
1512 0 : nsAutoString wordText;
1513 0 : RefPtr<nsRange> wordRange;
1514 : bool dontCheckWord;
1515 0 : while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText,
1516 : getter_AddRefs(wordRange),
1517 0 : &dontCheckWord)) &&
1518 0 : wordRange) {
1519 :
1520 : // get the range for the current word.
1521 : nsINode *beginNode;
1522 : nsINode *endNode;
1523 : int32_t beginOffset, endOffset;
1524 :
1525 0 : ErrorResult erv;
1526 0 : beginNode = wordRange->GetStartContainer(erv);
1527 0 : endNode = wordRange->GetEndContainer(erv);
1528 0 : beginOffset = wordRange->GetStartOffset(erv);
1529 0 : endOffset = wordRange->GetEndOffset(erv);
1530 :
1531 : // see if we've done enough words in this round and run out of time.
1532 0 : if (wordsChecked >= INLINESPELL_MINIMUM_WORDS_BEFORE_TIMEOUT &&
1533 0 : PR_Now() > PRTime(beginTime + kMaxSpellCheckTimeInUsec)) {
1534 : // stop checking, our time limit has been exceeded.
1535 : #ifdef DEBUG_INLINESPELL
1536 : printf("We have run out of the time, schedule next round.");
1537 : #endif
1538 : // move the range to encompass the stuff that needs checking.
1539 0 : nsresult rv = aStatus->mRange->SetStart(beginNode, beginOffset);
1540 0 : if (NS_FAILED(rv)) {
1541 : // The range might be unhappy because the beginning is after the
1542 : // end. This is possible when the requested end was in the middle
1543 : // of a word, just ignore this situation and assume we're done.
1544 0 : return NS_OK;
1545 : }
1546 0 : *aDoneChecking = false;
1547 0 : return NS_OK;
1548 : }
1549 :
1550 : #ifdef DEBUG_INLINESPELL
1551 : printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get());
1552 : if (dontCheckWord)
1553 : printf(" (not checking)");
1554 : printf("\n");
1555 : #endif
1556 :
1557 : // see if there is a spellcheck range that already intersects the word
1558 : // and remove it. We only need to remove old ranges, so don't bother if
1559 : // there were no ranges when we started out.
1560 0 : if (originalRangeCount > 0) {
1561 : // likewise, if this word is inside new text, we won't bother testing
1562 0 : if (!aStatus->mCreatedRange ||
1563 0 : !aStatus->mCreatedRange->IsPointInRange(*beginNode, beginOffset, erv)) {
1564 0 : nsTArray<RefPtr<nsRange>> ranges;
1565 : aSpellCheckSelection->GetRangesForInterval(*beginNode, beginOffset,
1566 : *endNode, endOffset,
1567 0 : true, ranges, erv);
1568 0 : ENSURE_SUCCESS(erv, erv.StealNSResult());
1569 0 : for (uint32_t i = 0; i < ranges.Length(); i++)
1570 0 : RemoveRange(aSpellCheckSelection, ranges[i]);
1571 : }
1572 : }
1573 :
1574 : // some words are special and don't need checking
1575 0 : if (dontCheckWord)
1576 0 : continue;
1577 :
1578 : // some nodes we don't spellcheck
1579 0 : if (!ShouldSpellCheckNode(editor, beginNode))
1580 0 : continue;
1581 :
1582 : // Don't check spelling if we're inside the noCheckRange. This needs to
1583 : // be done after we clear any old selection because the excluded word
1584 : // might have been previously marked.
1585 : //
1586 : // We do a simple check to see if the beginning of our word is in the
1587 : // exclusion range. Because the exclusion range is a multiple of a word,
1588 : // this is sufficient.
1589 0 : if (aStatus->mNoCheckRange &&
1590 0 : aStatus->mNoCheckRange->IsPointInRange(*beginNode, beginOffset, erv)) {
1591 0 : continue;
1592 : }
1593 :
1594 : // check spelling and add to selection if misspelled
1595 : bool isMisspelled;
1596 0 : aWordUtil.NormalizeWord(wordText);
1597 0 : nsresult rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
1598 0 : if (NS_FAILED(rv))
1599 0 : continue;
1600 :
1601 0 : wordsChecked++;
1602 :
1603 0 : if (isMisspelled) {
1604 : // misspelled words count extra toward the max
1605 0 : AddRange(aSpellCheckSelection, wordRange);
1606 :
1607 0 : aStatus->mWordCount ++;
1608 0 : if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
1609 0 : SpellCheckSelectionIsFull()) {
1610 0 : break;
1611 : }
1612 : }
1613 : }
1614 :
1615 0 : return NS_OK;
1616 : }
1617 :
1618 : // An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
1619 : class AutoChangeNumPendingSpellChecks
1620 : {
1621 : public:
1622 0 : AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
1623 : int32_t aDelta)
1624 0 : : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
1625 :
1626 0 : ~AutoChangeNumPendingSpellChecks()
1627 0 : {
1628 0 : mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
1629 0 : }
1630 :
1631 : private:
1632 : RefPtr<mozInlineSpellChecker> mSpellChecker;
1633 : int32_t mDelta;
1634 : };
1635 :
1636 : // mozInlineSpellChecker::ResumeCheck
1637 : //
1638 : // Called by the resume event when it fires. We will try to pick up where
1639 : // the last resume left off.
1640 :
1641 : nsresult
1642 0 : mozInlineSpellChecker::ResumeCheck(UniquePtr<mozInlineSpellStatus>&& aStatus)
1643 : {
1644 : // Observers should be notified that spell check has ended only after spell
1645 : // check is done below, but since there are many early returns in this method
1646 : // and the number of pending spell checks must be decremented regardless of
1647 : // whether the spell check actually happens, use this RAII object.
1648 0 : AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
1649 :
1650 0 : if (aStatus->IsFullSpellCheck()) {
1651 : // Allow posting new spellcheck resume events from inside
1652 : // ResumeCheck, now that we're actually firing.
1653 0 : NS_ASSERTION(mFullSpellCheckScheduled,
1654 : "How could this be false? The full spell check is "
1655 : "calling us!!");
1656 0 : mFullSpellCheckScheduled = false;
1657 : }
1658 :
1659 0 : if (! mSpellCheck)
1660 0 : return NS_OK; // spell checking has been turned off
1661 :
1662 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
1663 0 : if (! editor)
1664 0 : return NS_OK; // editor is gone
1665 :
1666 0 : mozInlineSpellWordUtil wordUtil;
1667 0 : nsresult rv = wordUtil.Init(mEditor);
1668 0 : if (NS_FAILED(rv))
1669 0 : return NS_OK; // editor doesn't like us, don't assert
1670 :
1671 0 : nsCOMPtr<nsISelection> spellCheckSelectionRef;
1672 0 : rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelectionRef));
1673 0 : NS_ENSURE_SUCCESS(rv, rv);
1674 :
1675 : auto spellCheckSelection =
1676 0 : static_cast<Selection *>(spellCheckSelectionRef.get());
1677 :
1678 0 : nsAutoString currentDictionary;
1679 0 : rv = mSpellCheck->GetCurrentDictionary(currentDictionary);
1680 0 : if (NS_FAILED(rv)) {
1681 : // no active dictionary
1682 0 : int32_t count = spellCheckSelection->RangeCount();
1683 0 : for (int32_t index = count - 1; index >= 0; index--) {
1684 0 : nsRange *checkRange = spellCheckSelection->GetRangeAt(index);
1685 0 : if (checkRange) {
1686 0 : RemoveRange(spellCheckSelection, checkRange);
1687 : }
1688 : }
1689 0 : return NS_OK;
1690 : }
1691 :
1692 0 : CleanupRangesInSelection(spellCheckSelection);
1693 :
1694 0 : rv = aStatus->FinishInitOnEvent(wordUtil);
1695 0 : NS_ENSURE_SUCCESS(rv, rv);
1696 0 : if (! aStatus->mRange)
1697 0 : return NS_OK; // empty range, nothing to do
1698 :
1699 0 : bool doneChecking = true;
1700 0 : if (aStatus->mOp == mozInlineSpellStatus::eOpSelection)
1701 0 : rv = DoSpellCheckSelection(wordUtil, spellCheckSelection);
1702 : else
1703 0 : rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
1704 0 : NS_ENSURE_SUCCESS(rv, rv);
1705 :
1706 0 : if (! doneChecking)
1707 0 : rv = ScheduleSpellCheck(Move(aStatus));
1708 0 : return rv;
1709 : }
1710 :
1711 : // mozInlineSpellChecker::IsPointInSelection
1712 : //
1713 : // Determines if a given (node,offset) point is inside the given
1714 : // selection. If so, the specific range of the selection that
1715 : // intersects is places in *aRange. (There may be multiple disjoint
1716 : // ranges in a selection.)
1717 : //
1718 : // If there is no intersection, *aRange will be nullptr.
1719 :
1720 : nsresult
1721 0 : mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection,
1722 : nsIDOMNode *aNode,
1723 : int32_t aOffset,
1724 : nsIDOMRange **aRange)
1725 : {
1726 0 : *aRange = nullptr;
1727 :
1728 0 : nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(aSelection));
1729 :
1730 0 : nsTArray<nsRange*> ranges;
1731 0 : nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
1732 0 : nsresult rv = privSel->GetRangesForIntervalArray(node, aOffset, node, aOffset,
1733 0 : true, &ranges);
1734 0 : NS_ENSURE_SUCCESS(rv, rv);
1735 :
1736 0 : if (ranges.Length() == 0)
1737 0 : return NS_OK; // no matches
1738 :
1739 : // there may be more than one range returned, and we don't know what do
1740 : // do with that, so just get the first one
1741 0 : NS_ADDREF(*aRange = ranges[0]);
1742 0 : return NS_OK;
1743 : }
1744 :
1745 : nsresult
1746 0 : mozInlineSpellChecker::CleanupRangesInSelection(Selection *aSelection)
1747 : {
1748 : // integrity check - remove ranges that have collapsed to nothing. This
1749 : // can happen if the node containing a highlighted word was removed.
1750 0 : if (!aSelection)
1751 0 : return NS_ERROR_FAILURE;
1752 :
1753 0 : int32_t count = aSelection->RangeCount();
1754 :
1755 0 : for (int32_t index = 0; index < count; index++)
1756 : {
1757 0 : nsRange *checkRange = aSelection->GetRangeAt(index);
1758 0 : if (checkRange)
1759 : {
1760 0 : if (checkRange->Collapsed())
1761 : {
1762 0 : RemoveRange(aSelection, checkRange);
1763 0 : index--;
1764 0 : count--;
1765 : }
1766 : }
1767 : }
1768 :
1769 0 : return NS_OK;
1770 : }
1771 :
1772 :
1773 : // mozInlineSpellChecker::RemoveRange
1774 : //
1775 : // For performance reasons, we have an upper bound on the number of word
1776 : // ranges in the spell check selection. When removing a range from the
1777 : // selection, we need to decrement mNumWordsInSpellSelection
1778 :
1779 : nsresult
1780 0 : mozInlineSpellChecker::RemoveRange(Selection *aSpellCheckSelection,
1781 : nsRange *aRange)
1782 : {
1783 0 : NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1784 0 : NS_ENSURE_ARG_POINTER(aRange);
1785 :
1786 0 : ErrorResult rv;
1787 0 : aSpellCheckSelection->RemoveRange(*aRange, rv);
1788 0 : if (!rv.Failed() && mNumWordsInSpellSelection)
1789 0 : mNumWordsInSpellSelection--;
1790 :
1791 0 : return rv.StealNSResult();
1792 : }
1793 :
1794 :
1795 : // mozInlineSpellChecker::AddRange
1796 : //
1797 : // For performance reasons, we have an upper bound on the number of word
1798 : // ranges we'll add to the spell check selection. Once we reach that upper
1799 : // bound, stop adding the ranges
1800 :
1801 : nsresult
1802 0 : mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection,
1803 : nsIDOMRange* aRange)
1804 : {
1805 0 : NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1806 0 : NS_ENSURE_ARG_POINTER(aRange);
1807 :
1808 0 : nsresult rv = NS_OK;
1809 :
1810 0 : if (!SpellCheckSelectionIsFull())
1811 : {
1812 0 : rv = aSpellCheckSelection->AddRange(aRange);
1813 0 : if (NS_SUCCEEDED(rv))
1814 0 : mNumWordsInSpellSelection++;
1815 : }
1816 :
1817 0 : return rv;
1818 : }
1819 :
1820 0 : nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection)
1821 : {
1822 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1823 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1824 :
1825 0 : nsCOMPtr<nsISelectionController> selcon;
1826 0 : nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon));
1827 0 : NS_ENSURE_SUCCESS(rv, rv);
1828 :
1829 0 : return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection);
1830 : }
1831 :
1832 0 : nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
1833 : {
1834 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1835 0 : NS_ENSURE_TRUE(editor, NS_OK);
1836 :
1837 : // figure out the old caret position based on the current selection
1838 0 : nsCOMPtr<nsISelection> selection;
1839 0 : nsresult rv = editor->GetSelection(getter_AddRefs(selection));
1840 0 : NS_ENSURE_SUCCESS(rv, rv);
1841 :
1842 0 : rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode));
1843 0 : NS_ENSURE_SUCCESS(rv, rv);
1844 :
1845 0 : selection->GetFocusOffset(&mCurrentSelectionOffset);
1846 :
1847 0 : return NS_OK;
1848 : }
1849 :
1850 : // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
1851 : // for XPCOM's rap sheet
1852 : bool // static
1853 0 : ContentIsDescendantOf(nsINode* aPossibleDescendant,
1854 : nsINode* aPossibleAncestor)
1855 : {
1856 0 : NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
1857 0 : NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
1858 :
1859 0 : do {
1860 0 : if (aPossibleDescendant == aPossibleAncestor)
1861 0 : return true;
1862 0 : aPossibleDescendant = aPossibleDescendant->GetParentNode();
1863 0 : } while (aPossibleDescendant);
1864 :
1865 0 : return false;
1866 : }
1867 :
1868 : // mozInlineSpellChecker::HandleNavigationEvent
1869 : //
1870 : // Acts upon mouse clicks and keyboard navigation changes, spell checking
1871 : // the previous word if the new navigation location moves us to another
1872 : // word.
1873 : //
1874 : // This is complicated by the fact that our mouse events are happening after
1875 : // selection has been changed to account for the mouse click. But keyboard
1876 : // events are happening before the caret selection has changed. Working
1877 : // around this by letting keyboard events setting forceWordSpellCheck to
1878 : // true. aNewPositionOffset also tries to work around this for the
1879 : // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1880 :
1881 : nsresult
1882 0 : mozInlineSpellChecker::HandleNavigationEvent(bool aForceWordSpellCheck,
1883 : int32_t aNewPositionOffset)
1884 : {
1885 : nsresult rv;
1886 :
1887 : // If we already handled the navigation event and there is no possibility
1888 : // anything has changed since then, we don't have to do anything. This
1889 : // optimization makes a noticeable difference when you hold down a navigation
1890 : // key like Page Down.
1891 0 : if (! mNeedsCheckAfterNavigation)
1892 0 : return NS_OK;
1893 :
1894 0 : nsCOMPtr<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode;
1895 0 : int32_t currentAnchorOffset = mCurrentSelectionOffset;
1896 :
1897 : // now remember the new focus position resulting from the event
1898 0 : rv = SaveCurrentSelectionPosition();
1899 0 : NS_ENSURE_SUCCESS(rv, rv);
1900 :
1901 : bool shouldPost;
1902 0 : auto status = MakeUnique<mozInlineSpellStatus>(this);
1903 0 : rv = status->InitForNavigation(aForceWordSpellCheck, aNewPositionOffset,
1904 : currentAnchorNode, currentAnchorOffset,
1905 : mCurrentSelectionAnchorNode,
1906 : mCurrentSelectionOffset,
1907 0 : &shouldPost);
1908 0 : NS_ENSURE_SUCCESS(rv, rv);
1909 0 : if (shouldPost) {
1910 0 : rv = ScheduleSpellCheck(Move(status));
1911 0 : NS_ENSURE_SUCCESS(rv, rv);
1912 : }
1913 :
1914 0 : return NS_OK;
1915 : }
1916 :
1917 0 : NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent)
1918 : {
1919 0 : nsAutoString eventType;
1920 0 : aEvent->GetType(eventType);
1921 :
1922 0 : if (eventType.EqualsLiteral("blur")) {
1923 0 : return Blur(aEvent);
1924 : }
1925 0 : if (eventType.EqualsLiteral("click")) {
1926 0 : return MouseClick(aEvent);
1927 : }
1928 0 : if (eventType.EqualsLiteral("keypress")) {
1929 0 : return KeyPress(aEvent);
1930 : }
1931 :
1932 0 : return NS_OK;
1933 : }
1934 :
1935 0 : nsresult mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent)
1936 : {
1937 : // force spellcheck on blur, for instance when tabbing out of a textbox
1938 0 : HandleNavigationEvent(true);
1939 0 : return NS_OK;
1940 : }
1941 :
1942 0 : nsresult mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent)
1943 : {
1944 0 : nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent);
1945 0 : NS_ENSURE_TRUE(mouseEvent, NS_OK);
1946 :
1947 : // ignore any errors from HandleNavigationEvent as we don't want to prevent
1948 : // anyone else from seeing this event.
1949 : int16_t button;
1950 0 : mouseEvent->GetButton(&button);
1951 0 : HandleNavigationEvent(button != 0);
1952 0 : return NS_OK;
1953 : }
1954 :
1955 0 : nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
1956 : {
1957 0 : nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent);
1958 0 : NS_ENSURE_TRUE(keyEvent, NS_OK);
1959 :
1960 : uint32_t keyCode;
1961 0 : keyEvent->GetKeyCode(&keyCode);
1962 :
1963 : // we only care about navigation keys that moved selection
1964 0 : switch (keyCode)
1965 : {
1966 : case nsIDOMKeyEvent::DOM_VK_RIGHT:
1967 : case nsIDOMKeyEvent::DOM_VK_LEFT:
1968 0 : HandleNavigationEvent(false, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1);
1969 0 : break;
1970 : case nsIDOMKeyEvent::DOM_VK_UP:
1971 : case nsIDOMKeyEvent::DOM_VK_DOWN:
1972 : case nsIDOMKeyEvent::DOM_VK_HOME:
1973 : case nsIDOMKeyEvent::DOM_VK_END:
1974 : case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
1975 : case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
1976 0 : HandleNavigationEvent(true /* force a spelling correction */);
1977 0 : break;
1978 : }
1979 :
1980 0 : return NS_OK;
1981 : }
1982 :
1983 : // Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
1984 : class UpdateCurrentDictionaryCallback final : public nsIEditorSpellCheckCallback
1985 : {
1986 : public:
1987 : NS_DECL_ISUPPORTS
1988 :
1989 0 : explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
1990 : uint32_t aDisabledAsyncToken)
1991 0 : : mSpellChecker(aSpellChecker), mDisabledAsyncToken(aDisabledAsyncToken) {}
1992 :
1993 0 : NS_IMETHOD EditorSpellCheckDone() override
1994 : {
1995 : // Ignore this callback if SetEnableRealTimeSpell(false) was called after
1996 : // the UpdateCurrentDictionary call that triggered it.
1997 0 : return mSpellChecker->mDisabledAsyncToken > mDisabledAsyncToken ?
1998 : NS_OK :
1999 0 : mSpellChecker->CurrentDictionaryUpdated();
2000 : }
2001 :
2002 : private:
2003 0 : ~UpdateCurrentDictionaryCallback() {}
2004 :
2005 : RefPtr<mozInlineSpellChecker> mSpellChecker;
2006 : uint32_t mDisabledAsyncToken;
2007 : };
2008 0 : NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
2009 :
2010 0 : NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
2011 : {
2012 : // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
2013 : // checker is being initialized. Calling UpdateCurrentDictionary on
2014 : // mPendingSpellCheck simply queues the dictionary update after the init.
2015 : nsCOMPtr<nsIEditorSpellCheck> spellCheck = mSpellCheck ? mSpellCheck :
2016 0 : mPendingSpellCheck;
2017 0 : if (!spellCheck) {
2018 0 : return NS_OK;
2019 : }
2020 :
2021 0 : if (NS_FAILED(spellCheck->GetCurrentDictionary(mPreviousDictionary))) {
2022 0 : mPreviousDictionary.Truncate();
2023 : }
2024 :
2025 : RefPtr<UpdateCurrentDictionaryCallback> cb =
2026 0 : new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
2027 0 : NS_ENSURE_STATE(cb);
2028 0 : nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
2029 0 : if (NS_FAILED(rv)) {
2030 0 : cb = nullptr;
2031 0 : return rv;
2032 : }
2033 0 : mNumPendingUpdateCurrentDictionary++;
2034 0 : ChangeNumPendingSpellChecks(1);
2035 :
2036 0 : return NS_OK;
2037 : }
2038 :
2039 : // Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
2040 0 : nsresult mozInlineSpellChecker::CurrentDictionaryUpdated()
2041 : {
2042 0 : mNumPendingUpdateCurrentDictionary--;
2043 0 : NS_ASSERTION(mNumPendingUpdateCurrentDictionary >= 0,
2044 : "CurrentDictionaryUpdated called without corresponding "
2045 : "UpdateCurrentDictionary call!");
2046 0 : ChangeNumPendingSpellChecks(-1);
2047 :
2048 0 : nsAutoString currentDictionary;
2049 0 : if (!mSpellCheck ||
2050 0 : NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) {
2051 0 : currentDictionary.Truncate();
2052 : }
2053 :
2054 0 : nsresult rv = SpellCheckRange(nullptr);
2055 0 : NS_ENSURE_SUCCESS(rv, rv);
2056 :
2057 0 : return NS_OK;
2058 : }
2059 :
2060 : NS_IMETHODIMP
2061 0 : mozInlineSpellChecker::GetSpellCheckPending(bool* aPending)
2062 : {
2063 0 : *aPending = mNumPendingSpellChecks > 0;
2064 0 : return NS_OK;
2065 : }
|