extensions/spellcheck/src/mozInlineSpellChecker.cpp

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

mercurial