Bug 452317 - FeedConverter.js: QueryInterface should throw NS_ERROR_NO_INTERFACE...
[wine-gecko.git] / extensions / spellcheck / src / mozInlineSpellChecker.cpp
blobb61e87e97e77e8658d227b61bf915345b1601e53
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
13 * License.
15 * The Original Code is Real-time Spellchecking
17 * The Initial Developer of the Original Code is Mozdev Group, Inc.
18 * Portions created by the Initial Developer are Copyright (C) 2004
19 * the Initial Developer. All Rights Reserved.
21 * Contributor(s): Neil Deakin (neil@mozdevgroup.com)
22 * Scott MacGregor (mscott@mozilla.org)
23 * Brett Wilson <brettw@gmail.com>
25 * Alternatively, the contents of this file may be used under the terms of
26 * either the GNU General Public License Version 2 or later (the "GPL"), or
27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 * in which case the provisions of the GPL or the LGPL are applicable instead
29 * of those above. If you wish to allow use of your version of this file only
30 * under the terms of either the GPL or the LGPL, and not to allow others to
31 * use your version of this file under the terms of the MPL, indicate your
32 * decision by deleting the provisions above and replace them with the notice
33 * and other provisions required by the GPL or the LGPL. If you do not delete
34 * the provisions above, a recipient may use your version of this file under
35 * the terms of any one of the MPL, the GPL or the LGPL.
37 * ***** END LICENSE BLOCK ***** */
39 /**
40 * This class is called by the editor to handle spellchecking after various
41 * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
42 * when the text is changed.
44 * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
45 * notifications to be flushed when we are called from the editor. This is
46 * because the call might originate from a frame, and flushing the
47 * notifications might cause that frame to be deleted.
49 * Using the WordUtil class to find words causes DOM notifications to be
50 * flushed because it asks for style information. As a result, we post an event
51 * and do all of the spellchecking in that event handler, which occurs later.
52 * We store all DOM pointers in ranges because they are kept up-to-date with
53 * DOM changes that may have happened while the event was on the queue.
55 * We also allow the spellcheck to be suspended and resumed later. This makes
56 * large pastes or initializations with a lot of text not hang the browser UI.
58 * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
59 * true when we get any change, and false once there is no possibility
60 * something changed that we need to check on navigation. Navigation events
61 * tend to be a little tricky because we want to check the current word on
62 * exit if something has changed. If we navigate inside the word, we don't want
63 * to do anything. As a result, this flag is cleared in FinishNavigationEvent
64 * when we know that we are checking as a result of navigation.
67 #include "mozInlineSpellChecker.h"
68 #include "mozInlineSpellWordUtil.h"
69 #include "mozISpellI18NManager.h"
70 #include "nsCOMPtr.h"
71 #include "nsCRT.h"
72 #include "nsIDocument.h"
73 #include "nsIDOMDocument.h"
74 #include "nsIDOMDocumentRange.h"
75 #include "nsIDOMElement.h"
76 #include "nsIDOMEventTarget.h"
77 #include "nsPIDOMEventTarget.h"
78 #include "nsIDOMMouseEvent.h"
79 #include "nsIDOMKeyEvent.h"
80 #include "nsIDOMNode.h"
81 #include "nsIDOMNodeList.h"
82 #include "nsIDOMNSRange.h"
83 #include "nsIDOMRange.h"
84 #include "nsIDOMText.h"
85 #include "nsIPlaintextEditor.h"
86 #include "nsIPrefBranch.h"
87 #include "nsIPrefService.h"
88 #include "nsIRunnable.h"
89 #include "nsISelection.h"
90 #include "nsISelection2.h"
91 #include "nsISelectionController.h"
92 #include "nsIServiceManager.h"
93 #include "nsITextServicesFilter.h"
94 #include "nsString.h"
95 #include "nsThreadUtils.h"
96 #include "nsUnicharUtils.h"
97 #include "nsIContent.h"
98 #include "nsIEventStateManager.h"
99 #include "nsIEventListenerManager.h"
100 #include "nsGUIEvent.h"
102 // Set to spew messages to the console about what is happening.
103 //#define DEBUG_INLINESPELL
105 // the number of milliseconds that we will take at once to do spellchecking
106 #define INLINESPELL_CHECK_TIMEOUT 50
108 // The number of words to check before we look at the time to see if
109 // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from spending
110 // too much time checking the clock. Note that misspelled words count for
111 // more than one word in this calculation.
112 #define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50
114 // This number is the number of checked words a misspelled word counts for
115 // when we're checking the time to see if the alloted time is up for
116 // spellchecking. Misspelled words take longer to process since we have to
117 // create a range, so they count more. The exact number isn't very important
118 // since this just controls how often we check the current time.
119 #define MISSPELLED_WORD_COUNT_PENALTY 4
122 static PRBool ContentIsDescendantOf(nsINode* aPossibleDescendant,
123 nsINode* aPossibleAncestor);
125 static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings";
127 mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
128 : mSpellChecker(aSpellChecker), mWordCount(0)
132 // mozInlineSpellStatus::InitForEditorChange
134 // This is the most complicated case. For changes, we need to compute the
135 // range of stuff that changed based on the old and new caret positions,
136 // as well as use a range possibly provided by the editor (start and end,
137 // which are usually NULL) to get a range with the union of these.
139 nsresult
140 mozInlineSpellStatus::InitForEditorChange(
141 PRInt32 aAction,
142 nsIDOMNode* aAnchorNode, PRInt32 aAnchorOffset,
143 nsIDOMNode* aPreviousNode, PRInt32 aPreviousOffset,
144 nsIDOMNode* aStartNode, PRInt32 aStartOffset,
145 nsIDOMNode* aEndNode, PRInt32 aEndOffset)
147 nsresult rv;
149 nsCOMPtr<nsIDOMDocumentRange> docRange;
150 rv = GetDocumentRange(getter_AddRefs(docRange));
151 NS_ENSURE_SUCCESS(rv, rv);
153 // save the anchor point as a range so we can find the current word later
154 rv = PositionToCollapsedRange(docRange, aAnchorNode, aAnchorOffset,
155 getter_AddRefs(mAnchorRange));
156 NS_ENSURE_SUCCESS(rv, rv);
158 if (aAction == mozInlineSpellChecker::kOpDeleteSelection) {
159 // Deletes are easy, the range is just the current anchor. We set the range
160 // to check to be empty, FinishInitOnEvent will fill in the range to be
161 // the current word.
162 mOp = eOpChangeDelete;
163 mRange = nsnull;
164 return NS_OK;
167 mOp = eOpChange;
169 // range to check
170 rv = docRange->CreateRange(getter_AddRefs(mRange));
171 NS_ENSURE_SUCCESS(rv, rv);
173 // ...we need to put the start and end in the correct order
174 nsCOMPtr<nsIDOMNSRange> nsrange = do_QueryInterface(mAnchorRange, &rv);
175 NS_ENSURE_SUCCESS(rv, rv);
176 PRInt16 cmpResult;
177 rv = nsrange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult);
178 NS_ENSURE_SUCCESS(rv, rv);
179 if (cmpResult < 0) {
180 // previous anchor node is before the current anchor
181 rv = mRange->SetStart(aPreviousNode, aPreviousOffset);
182 NS_ENSURE_SUCCESS(rv, rv);
183 rv = mRange->SetEnd(aAnchorNode, aAnchorOffset);
184 } else {
185 // previous anchor node is after (or the same as) the current anchor
186 rv = mRange->SetStart(aAnchorNode, aAnchorOffset);
187 NS_ENSURE_SUCCESS(rv, rv);
188 rv = mRange->SetEnd(aPreviousNode, aPreviousOffset);
190 NS_ENSURE_SUCCESS(rv, rv);
192 // On insert save this range: DoSpellCheck optimizes things in this range.
193 // Otherwise, just leave this NULL.
194 if (aAction == mozInlineSpellChecker::kOpInsertText)
195 mCreatedRange = mRange;
197 // if we were given a range, we need to expand our range to encompass it
198 if (aStartNode && aEndNode) {
199 nsrange = do_QueryInterface(mRange, &rv);
200 NS_ENSURE_SUCCESS(rv, rv);
202 rv = nsrange->ComparePoint(aStartNode, aStartOffset, &cmpResult);
203 NS_ENSURE_SUCCESS(rv, rv);
204 if (cmpResult < 0) { // given range starts before
205 rv = mRange->SetStart(aStartNode, aStartOffset);
206 NS_ENSURE_SUCCESS(rv, rv);
209 rv = nsrange->ComparePoint(aEndNode, aEndOffset, &cmpResult);
210 NS_ENSURE_SUCCESS(rv, rv);
211 if (cmpResult > 0) { // given range ends after
212 rv = mRange->SetEnd(aEndNode, aEndOffset);
213 NS_ENSURE_SUCCESS(rv, rv);
217 return NS_OK;
220 // mozInlineSpellStatis::InitForNavigation
222 // For navigation events, we just need to store the new and old positions.
224 // In some cases, we detect that we shouldn't check. If this event should
225 // not be processed, *aContinue will be false.
227 nsresult
228 mozInlineSpellStatus::InitForNavigation(
229 PRBool aForceCheck, PRInt32 aNewPositionOffset,
230 nsIDOMNode* aOldAnchorNode, PRInt32 aOldAnchorOffset,
231 nsIDOMNode* aNewAnchorNode, PRInt32 aNewAnchorOffset,
232 PRBool* aContinue)
234 nsresult rv;
235 mOp = eOpNavigation;
237 mForceNavigationWordCheck = aForceCheck;
238 mNewNavigationPositionOffset = aNewPositionOffset;
240 // get the root node for checking
241 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
242 NS_ENSURE_SUCCESS(rv, rv);
243 nsCOMPtr<nsIDOMElement> rootElt;
244 rv = editor->GetRootElement(getter_AddRefs(rootElt));
245 NS_ENSURE_SUCCESS(rv, rv);
247 // the anchor node might not be in the DOM anymore, check
248 nsCOMPtr<nsINode> root = do_QueryInterface(rootElt, &rv);
249 NS_ENSURE_SUCCESS(rv, rv);
250 nsCOMPtr<nsINode> currentAnchor = do_QueryInterface(aOldAnchorNode, &rv);
251 NS_ENSURE_SUCCESS(rv, rv);
252 if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) {
253 *aContinue = PR_FALSE;
254 return NS_OK;
257 nsCOMPtr<nsIDOMDocumentRange> docRange;
258 rv = GetDocumentRange(getter_AddRefs(docRange));
259 NS_ENSURE_SUCCESS(rv, rv);
261 rv = PositionToCollapsedRange(docRange, aOldAnchorNode, aOldAnchorOffset,
262 getter_AddRefs(mOldNavigationAnchorRange));
263 NS_ENSURE_SUCCESS(rv, rv);
264 rv = PositionToCollapsedRange(docRange, aNewAnchorNode, aNewAnchorOffset,
265 getter_AddRefs(mAnchorRange));
266 NS_ENSURE_SUCCESS(rv, rv);
268 *aContinue = PR_TRUE;
269 return NS_OK;
272 // mozInlineSpellStatus::InitForSelection
274 // It is easy for selections since we always re-check the spellcheck
275 // selection.
277 nsresult
278 mozInlineSpellStatus::InitForSelection()
280 mOp = eOpSelection;
281 return NS_OK;
284 // mozInlineSpellStatus::InitForRange
286 // Called to cause the spellcheck of the given range. This will look like
287 // a change operation over the given range.
289 nsresult
290 mozInlineSpellStatus::InitForRange(nsIDOMRange* aRange)
292 mOp = eOpChange;
293 mRange = aRange;
294 return NS_OK;
297 // mozInlineSpellStatus::FinishInitOnEvent
299 // Called when the event is triggered to complete initialization that
300 // might require the WordUtil. This calls to the operation-specific
301 // initializer, and also sets the range to be the entire element if it
302 // is NULL.
304 // Watch out: the range might still be NULL if there is nothing to do,
305 // the caller will have to check for this.
307 nsresult
308 mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil)
310 nsresult rv;
311 if (! mRange) {
312 rv = mSpellChecker->MakeSpellCheckRange(nsnull, 0, nsnull, 0,
313 getter_AddRefs(mRange));
314 NS_ENSURE_SUCCESS(rv, rv);
317 switch (mOp) {
318 case eOpChange:
319 if (mAnchorRange)
320 return FillNoCheckRangeFromAnchor(aWordUtil);
321 break;
322 case eOpChangeDelete:
323 if (mAnchorRange) {
324 rv = FillNoCheckRangeFromAnchor(aWordUtil);
325 NS_ENSURE_SUCCESS(rv, rv);
327 // Delete events will have no range for the changed text (because it was
328 // deleted), and InitForEditorChange will set it to NULL. Here, we select
329 // the entire word to cause any underlining to be removed.
330 mRange = mNoCheckRange;
331 break;
332 case eOpNavigation:
333 return FinishNavigationEvent(aWordUtil);
334 case eOpSelection:
335 // this gets special handling in ResumeCheck
336 break;
337 case eOpResume:
338 // everything should be initialized already in this case
339 break;
340 default:
341 NS_NOTREACHED("Bad operation");
342 return NS_ERROR_NOT_INITIALIZED;
344 return NS_OK;
347 // mozInlineSpellStatus::FinishNavigationEvent
349 // This verifies that we need to check the word at the previous caret
350 // position. Now that we have the word util, we can find the word belonging
351 // to the previous caret position. If the new position is inside that word,
352 // we don't want to do anything. In this case, we'll NULL out mRange so
353 // that the caller will know not to continue.
355 // Notice that we don't set mNoCheckRange. We check here whether the cursor
356 // is in the word that needs checking, so it isn't necessary. Plus, the
357 // spellchecker isn't guaranteed to only check the given word, and it could
358 // remove the underline from the new word under the cursor.
360 nsresult
361 mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil)
363 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor);
364 if (! editor)
365 return NS_ERROR_FAILURE; // editor is gone
367 NS_ASSERTION(mAnchorRange, "No anchor for navigation!");
368 nsCOMPtr<nsIDOMNode> newAnchorNode, oldAnchorNode;
369 PRInt32 newAnchorOffset, oldAnchorOffset;
371 // get the DOM position of the old caret, the range should be collapsed
372 nsresult rv = mOldNavigationAnchorRange->GetStartContainer(
373 getter_AddRefs(oldAnchorNode));
374 NS_ENSURE_SUCCESS(rv, rv);
375 rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset);
376 NS_ENSURE_SUCCESS(rv, rv);
378 // find the word on the old caret position, this is the one that we MAY need
379 // to check
380 nsCOMPtr<nsIDOMRange> oldWord;
381 rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset,
382 getter_AddRefs(oldWord));
383 NS_ENSURE_SUCCESS(rv, rv);
385 // aWordUtil.GetRangeForWord flushes pending notifications, check editor again.
386 editor = do_QueryReferent(mSpellChecker->mEditor);
387 if (! editor)
388 return NS_ERROR_FAILURE; // editor is gone
390 nsCOMPtr<nsIDOMNSRange> oldWordNS = do_QueryInterface(oldWord, &rv);
391 NS_ENSURE_SUCCESS(rv, rv);
393 // get the DOM position of the new caret, the range should be collapsed
394 rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode));
395 NS_ENSURE_SUCCESS(rv, rv);
396 rv = mAnchorRange->GetStartOffset(&newAnchorOffset);
397 NS_ENSURE_SUCCESS(rv, rv);
399 // see if the new cursor position is in the word of the old cursor position
400 PRBool isInRange = PR_FALSE;
401 if (! mForceNavigationWordCheck) {
402 rv = oldWordNS->IsPointInRange(newAnchorNode,
403 newAnchorOffset + mNewNavigationPositionOffset,
404 &isInRange);
405 NS_ENSURE_SUCCESS(rv, rv);
408 if (isInRange) {
409 // caller should give up
410 mRange = nsnull;
411 } else {
412 // check the old word
413 mRange = oldWord;
415 // Once we've spellchecked the current word, we don't need to spellcheck
416 // for any more navigation events.
417 mSpellChecker->mNeedsCheckAfterNavigation = PR_FALSE;
419 return NS_OK;
422 // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
424 // Given the mAnchorRange object, computes the range of the word it is on
425 // (if any) and fills that range into mNoCheckRange. This is used for
426 // change and navigation events to know which word we should skip spell
427 // checking on
429 nsresult
430 mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
431 mozInlineSpellWordUtil& aWordUtil)
433 nsCOMPtr<nsIDOMNode> anchorNode;
434 nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode));
435 NS_ENSURE_SUCCESS(rv, rv);
437 PRInt32 anchorOffset;
438 rv = mAnchorRange->GetStartOffset(&anchorOffset);
439 NS_ENSURE_SUCCESS(rv, rv);
441 return aWordUtil.GetRangeForWord(anchorNode, anchorOffset,
442 getter_AddRefs(mNoCheckRange));
445 // mozInlineSpellStatus::GetDocumentRange
447 // Returns the nsIDOMDocumentRange object for the document for the
448 // current spellchecker.
450 nsresult
451 mozInlineSpellStatus::GetDocumentRange(nsIDOMDocumentRange** aDocRange)
453 nsresult rv;
454 *aDocRange = nsnull;
455 if (! mSpellChecker->mEditor)
456 return NS_ERROR_UNEXPECTED;
458 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
459 NS_ENSURE_SUCCESS(rv, rv);
461 nsCOMPtr<nsIDOMDocument> domDoc;
462 rv = editor->GetDocument(getter_AddRefs(domDoc));
463 NS_ENSURE_SUCCESS(rv, rv);
465 nsCOMPtr<nsIDOMDocumentRange> docRange = do_QueryInterface(domDoc, &rv);
466 NS_ENSURE_SUCCESS(rv, rv);
468 docRange.swap(*aDocRange);
469 return NS_OK;
472 // mozInlineSpellStatus::PositionToCollapsedRange
474 // Converts a given DOM position to a collapsed range covering that
475 // position. We use ranges to store DOM positions becuase they stay
476 // updated as the DOM is changed.
478 nsresult
479 mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocumentRange* aDocRange,
480 nsIDOMNode* aNode, PRInt32 aOffset, nsIDOMRange** aRange)
482 *aRange = nsnull;
483 nsCOMPtr<nsIDOMRange> range;
484 nsresult rv = aDocRange->CreateRange(getter_AddRefs(range));
485 NS_ENSURE_SUCCESS(rv, rv);
487 rv = range->SetStart(aNode, aOffset);
488 NS_ENSURE_SUCCESS(rv, rv);
489 rv = range->SetEnd(aNode, aOffset);
490 NS_ENSURE_SUCCESS(rv, rv);
492 range.swap(*aRange);
493 return NS_OK;
496 // mozInlineSpellResume
498 class mozInlineSpellResume : public nsRunnable
500 public:
501 mozInlineSpellResume(const mozInlineSpellStatus& aStatus) : mStatus(aStatus) {}
502 mozInlineSpellStatus mStatus;
503 nsresult Post()
505 return NS_DispatchToMainThread(this);
508 NS_IMETHOD Run()
510 mStatus.mSpellChecker->ResumeCheck(&mStatus);
511 return NS_OK;
516 NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
517 NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
518 NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
519 NS_INTERFACE_MAP_ENTRY(nsIDOMFocusListener)
520 NS_INTERFACE_MAP_ENTRY(nsIDOMMouseListener)
521 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
522 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMKeyListener)
523 NS_INTERFACE_MAP_ENTRY(nsIDOMKeyListener)
524 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIDOMEventListener, nsIDOMKeyListener)
525 NS_INTERFACE_MAP_END
527 NS_IMPL_ADDREF(mozInlineSpellChecker)
528 NS_IMPL_RELEASE(mozInlineSpellChecker)
530 mozInlineSpellChecker::SpellCheckingState
531 mozInlineSpellChecker::gCanEnableSpellChecking =
532 mozInlineSpellChecker::SpellCheck_Uninitialized;
534 mozInlineSpellChecker::mozInlineSpellChecker() :
535 mNumWordsInSpellSelection(0),
536 mMaxNumWordsInSpellSelection(250),
537 mNeedsCheckAfterNavigation(PR_FALSE)
539 nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
540 if (prefs)
541 prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection);
542 mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4;
545 mozInlineSpellChecker::~mozInlineSpellChecker()
549 NS_IMETHODIMP
550 mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck)
552 *aSpellCheck = mSpellCheck;
553 NS_IF_ADDREF(*aSpellCheck);
554 return NS_OK;
557 NS_IMETHODIMP
558 mozInlineSpellChecker::Init(nsIEditor *aEditor)
560 mEditor = do_GetWeakReference(aEditor);
561 return NS_OK;
564 // mozInlineSpellChecker::Cleanup
566 // Called by the editor when the editor is going away. This is important
567 // because we remove listeners. We do NOT clean up anything else in this
568 // function, because it can get called while DoSpellCheck is running!
570 // Getting the style information there can cause DOM notifications to be
571 // flushed, which can cause editors to go away which will bring us here.
572 // We can not do anything that will cause DoSpellCheck to freak out.
574 nsresult mozInlineSpellChecker::Cleanup()
576 mNumWordsInSpellSelection = 0;
577 nsCOMPtr<nsISelection> spellCheckSelection;
578 nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
579 if (NS_FAILED(rv)) {
580 // Ensure we still unregister event listeners (but return a failure code)
581 UnregisterEventListeners();
582 } else {
583 spellCheckSelection->RemoveAllRanges();
585 rv = UnregisterEventListeners();
587 mEditor = nsnull;
589 return rv;
592 // mozInlineSpellChecker::CanEnableInlineSpellChecking
594 // This function can be called to see if it seems likely that we can enable
595 // spellchecking before actually creating the InlineSpellChecking objects.
597 // The problem is that we can't get the dictionary list without actually
598 // creating a whole bunch of spellchecking objects. This function tries to
599 // do that and caches the result so we don't have to keep allocating those
600 // objects if there are no dictionaries or spellchecking.
602 // This caching will prevent adding dictionaries at runtime if we start out
603 // with no dictionaries! Installing dictionaries as extensions will require
604 // a restart anyway, so it shouldn't be a problem.
606 PRBool // static
607 mozInlineSpellChecker::CanEnableInlineSpellChecking()
609 nsresult rv;
610 if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
611 gCanEnableSpellChecking = SpellCheck_NotAvailable;
613 nsCOMPtr<nsIEditorSpellCheck> spellchecker =
614 do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv);
615 NS_ENSURE_SUCCESS(rv, PR_FALSE);
617 PRBool canSpellCheck = PR_FALSE;
618 rv = spellchecker->CanSpellCheck(&canSpellCheck);
619 NS_ENSURE_SUCCESS(rv, PR_FALSE);
621 if (canSpellCheck)
622 gCanEnableSpellChecking = SpellCheck_Available;
624 return (gCanEnableSpellChecking == SpellCheck_Available);
627 // mozInlineSpellChecker::RegisterEventListeners
629 // The inline spell checker listens to mouse events and keyboard navigation+ // events.
631 nsresult
632 mozInlineSpellChecker::RegisterEventListeners()
634 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
635 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
637 editor->AddEditActionListener(this);
639 nsCOMPtr<nsIDOMDocument> doc;
640 nsresult rv = editor->GetDocument(getter_AddRefs(doc));
641 NS_ENSURE_SUCCESS(rv, rv);
643 nsCOMPtr<nsPIDOMEventTarget> piTarget = do_QueryInterface(doc, &rv);
644 NS_ENSURE_SUCCESS(rv, rv);
646 nsCOMPtr<nsIEventListenerManager> elmP;
647 piTarget->GetListenerManager(PR_TRUE, getter_AddRefs(elmP));
648 if (elmP) {
649 // Focus event doesn't bubble so adding the listener to capturing phase
650 elmP->AddEventListenerByIID(static_cast<nsIDOMFocusListener *>(this),
651 NS_GET_IID(nsIDOMFocusListener),
652 NS_EVENT_FLAG_CAPTURE);
655 piTarget->AddEventListenerByIID(static_cast<nsIDOMMouseListener*>(this),
656 NS_GET_IID(nsIDOMMouseListener));
657 piTarget->AddEventListenerByIID(static_cast<nsIDOMKeyListener*>(this),
658 NS_GET_IID(nsIDOMKeyListener));
660 return NS_OK;
663 // mozInlineSpellChecker::UnregisterEventListeners
665 nsresult
666 mozInlineSpellChecker::UnregisterEventListeners()
668 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
669 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
671 editor->RemoveEditActionListener(this);
673 nsCOMPtr<nsIDOMDocument> doc;
674 editor->GetDocument(getter_AddRefs(doc));
675 NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
677 nsCOMPtr<nsPIDOMEventTarget> piTarget = do_QueryInterface(doc);
678 NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER);
680 nsCOMPtr<nsIEventListenerManager> elmP;
681 piTarget->GetListenerManager(PR_TRUE, getter_AddRefs(elmP));
682 if (elmP) {
683 elmP->RemoveEventListenerByIID(static_cast<nsIDOMFocusListener *>(this),
684 NS_GET_IID(nsIDOMFocusListener),
685 NS_EVENT_FLAG_CAPTURE);
688 piTarget->RemoveEventListenerByIID(static_cast<nsIDOMMouseListener*>(this),
689 NS_GET_IID(nsIDOMMouseListener));
690 piTarget->RemoveEventListenerByIID(static_cast<nsIDOMKeyListener*>(this),
691 NS_GET_IID(nsIDOMKeyListener));
693 return NS_OK;
696 // mozInlineSpellChecker::GetEnableRealTimeSpell
698 NS_IMETHODIMP
699 mozInlineSpellChecker::GetEnableRealTimeSpell(PRBool* aEnabled)
701 NS_ENSURE_ARG_POINTER(aEnabled);
702 *aEnabled = mSpellCheck != nsnull;
703 return NS_OK;
706 // mozInlineSpellChecker::SetEnableRealTimeSpell
708 NS_IMETHODIMP
709 mozInlineSpellChecker::SetEnableRealTimeSpell(PRBool aEnabled)
711 if (!aEnabled) {
712 mSpellCheck = nsnull;
713 return Cleanup();
716 if (!mSpellCheck) {
717 nsresult res = NS_OK;
718 nsCOMPtr<nsIEditorSpellCheck> spellchecker = do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &res);
719 if (NS_SUCCEEDED(res) && spellchecker)
721 nsCOMPtr<nsITextServicesFilter> filter = do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1", &res);
722 spellchecker->SetFilter(filter);
723 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
724 res = spellchecker->InitSpellChecker(editor, PR_FALSE);
725 NS_ENSURE_SUCCESS(res, res);
727 nsCOMPtr<nsITextServicesDocument> tsDoc = do_CreateInstance("@mozilla.org/textservices/textservicesdocument;1", &res);
728 NS_ENSURE_SUCCESS(res, res);
730 res = tsDoc->SetFilter(filter);
731 NS_ENSURE_SUCCESS(res, res);
733 res = tsDoc->InitWithEditor(editor);
734 NS_ENSURE_SUCCESS(res, res);
736 mTextServicesDocument = tsDoc;
737 mSpellCheck = spellchecker;
739 // spell checking is enabled, register our event listeners to track navigation
740 RegisterEventListeners();
744 // spellcheck the current contents. SpellCheckRange doesn't supply a created
745 // range to DoSpellCheck, which in our case is the entire range. But this
746 // optimization doesn't matter because there is nothing in the spellcheck
747 // selection when starting, which triggers a better optimization.
748 return SpellCheckRange(nsnull);
751 // mozInlineSpellChecker::SpellCheckAfterEditorChange
753 // Called by the editor when nearly anything happens to change the content.
755 // The start and end positions specify a range for the thing that happened,
756 // but these are usually NULL, even when you'd think they would be useful
757 // because you want the range (for example, pasting). We ignore them in
758 // this case.
760 NS_IMETHODIMP
761 mozInlineSpellChecker::SpellCheckAfterEditorChange(
762 PRInt32 aAction, nsISelection *aSelection,
763 nsIDOMNode *aPreviousSelectedNode, PRInt32 aPreviousSelectedOffset,
764 nsIDOMNode *aStartNode, PRInt32 aStartOffset,
765 nsIDOMNode *aEndNode, PRInt32 aEndOffset)
767 nsresult rv;
768 NS_ENSURE_ARG_POINTER(aSelection);
769 if (!mSpellCheck)
770 return NS_OK; // disabling spell checking is not an error
772 // this means something has changed, and we never check the current word,
773 // therefore, we should spellcheck for subsequent caret navigations
774 mNeedsCheckAfterNavigation = PR_TRUE;
776 // the anchor node is the position of the caret
777 nsCOMPtr<nsIDOMNode> anchorNode;
778 rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode));
779 NS_ENSURE_SUCCESS(rv, rv);
780 PRInt32 anchorOffset;
781 rv = aSelection->GetAnchorOffset(&anchorOffset);
782 NS_ENSURE_SUCCESS(rv, rv);
784 mozInlineSpellStatus status(this);
785 rv = status.InitForEditorChange(aAction,
786 anchorNode, anchorOffset,
787 aPreviousSelectedNode, aPreviousSelectedOffset,
788 aStartNode, aStartOffset,
789 aEndNode, aEndOffset);
790 NS_ENSURE_SUCCESS(rv, rv);
791 rv = ScheduleSpellCheck(status);
792 NS_ENSURE_SUCCESS(rv, rv);
794 // remember the current caret position after every change
795 SaveCurrentSelectionPosition();
796 return NS_OK;
799 // mozInlineSpellChecker::SpellCheckRange
801 // Spellchecks all the words in the given range.
802 // Supply a NULL range and this will check the entire editor.
804 nsresult
805 mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange)
807 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
809 mozInlineSpellStatus status(this);
810 nsresult rv = status.InitForRange(aRange);
811 NS_ENSURE_SUCCESS(rv, rv);
812 return ScheduleSpellCheck(status);
815 // mozInlineSpellChecker::GetMispelledWord
817 NS_IMETHODIMP
818 mozInlineSpellChecker::GetMispelledWord(nsIDOMNode *aNode, PRInt32 aOffset,
819 nsIDOMRange **newword)
821 NS_ENSURE_ARG_POINTER(aNode);
822 nsCOMPtr<nsISelection> spellCheckSelection;
823 nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
824 NS_ENSURE_SUCCESS(res, res);
826 return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword);
829 // mozInlineSpellChecker::ReplaceWord
831 NS_IMETHODIMP
832 mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, PRInt32 aOffset,
833 const nsAString &newword)
835 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
836 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
837 NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
839 nsCOMPtr<nsIDOMRange> range;
840 nsresult res = GetMispelledWord(aNode, aOffset, getter_AddRefs(range));
841 NS_ENSURE_SUCCESS(res, res);
843 if (range)
845 editor->BeginTransaction();
847 nsCOMPtr<nsISelection> selection;
848 res = editor->GetSelection(getter_AddRefs(selection));
849 NS_ENSURE_SUCCESS(res, res);
850 selection->RemoveAllRanges();
851 selection->AddRange(range);
852 editor->DeleteSelection(nsIEditor::eNone);
854 nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
855 textEditor->InsertText(newword);
857 editor->EndTransaction();
860 return NS_OK;
863 // mozInlineSpellChecker::AddWordToDictionary
865 NS_IMETHODIMP
866 mozInlineSpellChecker::AddWordToDictionary(const nsAString &word)
868 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
870 nsAutoString wordstr(word);
871 nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get());
872 NS_ENSURE_SUCCESS(rv, rv);
874 mozInlineSpellStatus status(this);
875 rv = status.InitForSelection();
876 NS_ENSURE_SUCCESS(rv, rv);
877 return ScheduleSpellCheck(status);
880 // mozInlineSpellChecker::IgnoreWord
882 NS_IMETHODIMP
883 mozInlineSpellChecker::IgnoreWord(const nsAString &word)
885 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
887 nsAutoString wordstr(word);
888 nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get());
889 NS_ENSURE_SUCCESS(rv, rv);
891 mozInlineSpellStatus status(this);
892 rv = status.InitForSelection();
893 NS_ENSURE_SUCCESS(rv, rv);
894 return ScheduleSpellCheck(status);
897 // mozInlineSpellChecker::IgnoreWords
899 NS_IMETHODIMP
900 mozInlineSpellChecker::IgnoreWords(const PRUnichar **aWordsToIgnore,
901 PRUint32 aCount)
903 // add each word to the ignore list and then recheck the document
904 for (PRUint32 index = 0; index < aCount; index++)
905 mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]);
907 mozInlineSpellStatus status(this);
908 nsresult rv = status.InitForSelection();
909 NS_ENSURE_SUCCESS(rv, rv);
910 return ScheduleSpellCheck(status);
913 NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, PRInt32 aPosition)
915 return NS_OK;
918 NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent,
919 PRInt32 aPosition, nsresult aResult)
921 return NS_OK;
924 NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
925 PRInt32 aPosition)
927 return NS_OK;
930 NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
931 PRInt32 aPosition, nsresult aResult)
934 return NS_OK;
937 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild)
939 return NS_OK;
942 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
944 return NS_OK;
947 NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, PRInt32 aOffset)
949 return NS_OK;
952 NS_IMETHODIMP
953 mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode,
954 PRInt32 aOffset,
955 nsIDOMNode *aNewLeftNode, nsresult aResult)
957 return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
960 NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent)
962 return NS_OK;
965 NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode,
966 nsIDOMNode *aParent, nsresult aResult)
968 return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0);
971 NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, const nsAString & aString)
973 return NS_OK;
976 NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset,
977 const nsAString & aString, nsresult aResult)
979 return NS_OK;
982 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, PRInt32 aLength)
984 return NS_OK;
987 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, PRInt32 aLength, nsresult aResult)
989 return NS_OK;
992 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection)
994 return NS_OK;
997 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection)
999 return NS_OK;
1002 // mozInlineSpellChecker::MakeSpellCheckRange
1004 // Given begin and end positions, this function constructs a range as
1005 // required for ScheduleSpellCheck. If the start and end nodes are NULL,
1006 // then the entire range will be selected, and you can supply -1 as the
1007 // offset to the end range to select all of that node.
1009 // If the resulting range would be empty, NULL is put into *aRange and the
1010 // function succeeds.
1012 nsresult
1013 mozInlineSpellChecker::MakeSpellCheckRange(
1014 nsIDOMNode* aStartNode, PRInt32 aStartOffset,
1015 nsIDOMNode* aEndNode, PRInt32 aEndOffset,
1016 nsIDOMRange** aRange)
1018 nsresult rv;
1019 *aRange = nsnull;
1021 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1022 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1024 nsCOMPtr<nsIDOMDocument> doc;
1025 rv = editor->GetDocument(getter_AddRefs(doc));
1026 NS_ENSURE_SUCCESS(rv, rv);
1028 nsCOMPtr<nsIDOMDocumentRange> docrange = do_QueryInterface(doc);
1029 NS_ENSURE_TRUE(docrange, NS_ERROR_FAILURE);
1031 nsCOMPtr<nsIDOMRange> range;
1032 rv = docrange->CreateRange(getter_AddRefs(range));
1033 NS_ENSURE_SUCCESS(rv, rv);
1035 // possibly use full range of the editor
1036 nsCOMPtr<nsIDOMElement> rootElem;
1037 if (! aStartNode || ! aEndNode) {
1038 rv = editor->GetRootElement(getter_AddRefs(rootElem));
1039 NS_ENSURE_SUCCESS(rv, rv);
1041 aStartNode = rootElem;
1042 aStartOffset = 0;
1044 aEndNode = rootElem;
1045 aEndOffset = -1;
1048 if (aEndOffset == -1) {
1049 nsCOMPtr<nsIDOMNodeList> childNodes;
1050 rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes));
1051 NS_ENSURE_SUCCESS(rv, rv);
1053 PRUint32 childCount;
1054 rv = childNodes->GetLength(&childCount);
1055 NS_ENSURE_SUCCESS(rv, rv);
1057 aEndOffset = childCount;
1060 // sometimes we are are requested to check an empty range (possibly an empty
1061 // document). This will result in assertions later.
1062 if (aStartNode == aEndNode && aStartOffset == aEndOffset)
1063 return NS_OK;
1065 rv = range->SetStart(aStartNode, aStartOffset);
1066 NS_ENSURE_SUCCESS(rv, rv);
1067 if (aEndOffset)
1068 rv = range->SetEnd(aEndNode, aEndOffset);
1069 else
1070 rv = range->SetEndAfter(aEndNode);
1071 NS_ENSURE_SUCCESS(rv, rv);
1073 range.swap(*aRange);
1074 return NS_OK;
1077 nsresult
1078 mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
1079 PRInt32 aStartOffset,
1080 nsIDOMNode *aEndNode,
1081 PRInt32 aEndOffset)
1083 nsCOMPtr<nsIDOMRange> range;
1084 nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset,
1085 aEndNode, aEndOffset,
1086 getter_AddRefs(range));
1087 NS_ENSURE_SUCCESS(rv, rv);
1089 if (! range)
1090 return NS_OK; // range is empty: nothing to do
1092 mozInlineSpellStatus status(this);
1093 rv = status.InitForRange(range);
1094 NS_ENSURE_SUCCESS(rv, rv);
1095 return ScheduleSpellCheck(status);
1098 // mozInlineSpellChecker::SkipSpellCheckForNode
1100 // There are certain conditions when we don't want to spell check a node. In
1101 // particular quotations, moz signatures, etc. This routine returns false
1102 // for these cases.
1104 nsresult
1105 mozInlineSpellChecker::SkipSpellCheckForNode(nsIEditor* aEditor,
1106 nsIDOMNode *aNode,
1107 PRBool *checkSpelling)
1109 *checkSpelling = PR_TRUE;
1110 NS_ENSURE_ARG_POINTER(aNode);
1112 PRUint32 flags;
1113 aEditor->GetFlags(&flags);
1114 if (flags & nsIPlaintextEditor::eEditorMailMask)
1116 nsCOMPtr<nsIDOMNode> parent;
1117 aNode->GetParentNode(getter_AddRefs(parent));
1119 while (parent)
1121 nsCOMPtr<nsIDOMElement> parentElement = do_QueryInterface(parent);
1122 if (!parentElement)
1123 break;
1125 nsAutoString parentTagName;
1126 parentElement->GetTagName(parentTagName);
1128 if (parentTagName.Equals(NS_LITERAL_STRING("blockquote"), nsCaseInsensitiveStringComparator()))
1130 *checkSpelling = PR_FALSE;
1131 break;
1133 else if (parentTagName.Equals(NS_LITERAL_STRING("pre"), nsCaseInsensitiveStringComparator()))
1135 nsAutoString classname;
1136 parentElement->GetAttribute(NS_LITERAL_STRING("class"),classname);
1137 if (classname.Equals(NS_LITERAL_STRING("moz-signature")))
1138 *checkSpelling = PR_FALSE;
1141 nsCOMPtr<nsIDOMNode> nextParent;
1142 parent->GetParentNode(getter_AddRefs(nextParent));
1143 parent = nextParent;
1146 else {
1147 // XXX Do we really want this for all read-write content?
1148 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
1149 *checkSpelling = !!(content->IntrinsicState() & NS_EVENT_STATE_MOZ_READWRITE);
1152 return NS_OK;
1155 // mozInlineSpellChecker::ScheduleSpellCheck
1157 // This is called by code to do the actual spellchecking. We will set up
1158 // the proper structures for calls to DoSpellCheck.
1160 nsresult
1161 mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus)
1163 mozInlineSpellResume* resume = new mozInlineSpellResume(aStatus);
1164 NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1166 nsresult rv = resume->Post();
1167 if (NS_FAILED(rv))
1168 delete resume;
1169 return rv;
1172 // mozInlineSpellChecker::DoSpellCheckSelection
1174 // Called to re-check all misspelled words. We iterate over all ranges in
1175 // the selection and call DoSpellCheck on them. This is used when a word
1176 // is ignored or added to the dictionary: all instances of that word should
1177 // be removed from the selection.
1179 // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1180 // Typically, checking this small amount of text is relatively fast, but
1181 // for large numbers of words, a lag may be noticable.
1183 nsresult
1184 mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
1185 nsISelection* aSpellCheckSelection,
1186 mozInlineSpellStatus* aStatus)
1188 nsresult rv;
1190 // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1191 mNumWordsInSpellSelection = 0;
1193 // Since we could be modifying the ranges for the spellCheckSelection while
1194 // looping on the spell check selection, keep a separate array of range
1195 // elements inside the selection
1196 nsCOMArray<nsIDOMRange> ranges;
1198 PRInt32 count;
1199 aSpellCheckSelection->GetRangeCount(&count);
1201 PRInt32 idx;
1202 nsCOMPtr<nsIDOMRange> checkRange;
1203 for (idx = 0; idx < count; idx ++) {
1204 aSpellCheckSelection->GetRangeAt(idx, getter_AddRefs(checkRange));
1205 if (checkRange) {
1206 if (! ranges.AppendObject(checkRange))
1207 return NS_ERROR_OUT_OF_MEMORY;
1211 // We have saved the ranges above. Clearing the spellcheck selection here
1212 // isn't necessary (rechecking each word will modify it as necessary) but
1213 // provides better performance. By ensuring that no ranges need to be
1214 // removed in DoSpellCheck, we can save checking range inclusion which is
1215 // slow.
1216 aSpellCheckSelection->RemoveAllRanges();
1218 // We use this state object for all calls, and just update its range. Note
1219 // that we don't need to call FinishInit since we will be filling in the
1220 // necessary information.
1221 mozInlineSpellStatus status(this);
1222 rv = status.InitForRange(nsnull);
1223 NS_ENSURE_SUCCESS(rv, rv);
1225 PRBool doneChecking;
1226 for (idx = 0; idx < count; idx ++) {
1227 checkRange = ranges[idx];
1228 if (checkRange) {
1229 // We can consider this word as "added" since we know it has no spell
1230 // check range over it that needs to be deleted. All the old ranges
1231 // were cleared above. We also need to clear the word count so that we
1232 // check all words instead of stopping early.
1233 status.mRange = checkRange;
1234 rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status,
1235 &doneChecking);
1236 NS_ENSURE_SUCCESS(rv, rv);
1237 NS_ASSERTION(doneChecking, "We gave the spellchecker one word, but it didn't finish checking?!?!");
1239 status.mWordCount = 0;
1243 return NS_OK;
1246 // mozInlineSpellChecker::DoSpellCheck
1248 // This function checks words intersecting the given range, excluding those
1249 // inside mStatus->mNoCheckRange (can be NULL). Words inside aNoCheckRange
1250 // will have any spell selection removed (this is used to hide the
1251 // underlining for the word that the caret is in). aNoCheckRange should be
1252 // on word boundaries.
1254 // mResume->mCreatedRange is a possibly NULL range of new text that was
1255 // inserted. Inside this range, we don't bother to check whether things are
1256 // inside the spellcheck selection, which speeds up large paste operations
1257 // considerably.
1259 // Normal case when editing text by typing
1260 // h e l l o w o r k d h o w a r e y o u
1261 // ^ caret
1262 // [-------] mRange
1263 // [-------] mNoCheckRange
1264 // -> does nothing (range is the same as the no check range)
1266 // Case when pasting:
1267 // [---------- pasted text ----------]
1268 // h e l l o w o r k d h o w a r e y o u
1269 // ^ caret
1270 // [---] aNoCheckRange
1271 // -> recheck all words in range except those in aNoCheckRange
1273 // If checking is complete, *aDoneChecking will be set. If there is more
1274 // but we ran out of time, this will be false and the range will be
1275 // updated with the stuff that still needs checking.
1277 nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
1278 nsISelection *aSpellCheckSelection,
1279 mozInlineSpellStatus* aStatus,
1280 PRBool* aDoneChecking)
1282 nsCOMPtr<nsIDOMNode> beginNode, endNode;
1283 PRInt32 beginOffset, endOffset;
1284 *aDoneChecking = PR_TRUE;
1286 // get the editor for SkipSpellCheckForNode, this may fail in reasonable
1287 // circumstances since the editor could have gone away
1288 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1289 if (! editor)
1290 return NS_ERROR_FAILURE;
1292 PRBool iscollapsed;
1293 nsresult rv = aStatus->mRange->GetCollapsed(&iscollapsed);
1294 NS_ENSURE_SUCCESS(rv, rv);
1295 if (iscollapsed)
1296 return NS_OK;
1298 nsCOMPtr<nsISelection2> sel2 = do_QueryInterface(aSpellCheckSelection, &rv);
1299 NS_ENSURE_SUCCESS(rv, rv);
1301 // see if the selection has any ranges, if not, then we can optimize checking
1302 // range inclusion later (we have no ranges when we are initially checking or
1303 // when there are no misspelled words yet).
1304 PRInt32 originalRangeCount;
1305 rv = aSpellCheckSelection->GetRangeCount(&originalRangeCount);
1306 NS_ENSURE_SUCCESS(rv, rv);
1308 // set the starting DOM position to be the beginning of our range
1309 NS_ENSURE_SUCCESS(rv, rv);
1310 aStatus->mRange->GetStartContainer(getter_AddRefs(beginNode));
1311 aStatus->mRange->GetStartOffset(&beginOffset);
1312 aStatus->mRange->GetEndContainer(getter_AddRefs(endNode));
1313 aStatus->mRange->GetEndOffset(&endOffset);
1314 aWordUtil.SetEnd(endNode, endOffset);
1315 aWordUtil.SetPosition(beginNode, beginOffset);
1317 // aWordUtil.SetPosition flushes pending notifications, check editor again.
1318 editor = do_QueryReferent(mEditor);
1319 if (! editor)
1320 return NS_ERROR_FAILURE;
1322 // we need to use IsPointInRange which is on a more specific interface
1323 nsCOMPtr<nsIDOMNSRange> noCheckRange, createdRange;
1324 if (aStatus->mNoCheckRange)
1325 noCheckRange = do_QueryInterface(aStatus->mNoCheckRange);
1326 if (aStatus->mCreatedRange)
1327 createdRange = do_QueryInterface(aStatus->mCreatedRange);
1329 PRInt32 wordsSinceTimeCheck = 0;
1330 PRTime beginTime = PR_Now();
1332 nsAutoString wordText;
1333 nsCOMPtr<nsIDOMRange> wordRange;
1334 PRBool dontCheckWord;
1335 while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText,
1336 getter_AddRefs(wordRange),
1337 &dontCheckWord)) &&
1338 wordRange) {
1339 wordsSinceTimeCheck ++;
1341 // get the range for the current word
1342 wordRange->GetStartContainer(getter_AddRefs(beginNode));
1343 wordRange->GetEndContainer(getter_AddRefs(endNode));
1344 wordRange->GetStartOffset(&beginOffset);
1345 wordRange->GetEndOffset(&endOffset);
1347 #ifdef DEBUG_INLINESPELL
1348 printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get());
1349 if (dontCheckWord)
1350 printf(" (not checking)");
1351 printf("\n");
1352 #endif
1354 // see if there is a spellcheck range that already intersects the word
1355 // and remove it. We only need to remove old ranges, so don't bother if
1356 // there were no ranges when we started out.
1357 if (originalRangeCount > 0) {
1358 // likewise, if this word is inside new text, we won't bother testing
1359 PRBool inCreatedRange = PR_FALSE;
1360 if (createdRange)
1361 createdRange->IsPointInRange(beginNode, beginOffset, &inCreatedRange);
1362 if (! inCreatedRange) {
1363 nsCOMArray<nsIDOMRange> ranges;
1364 rv = sel2->GetRangesForIntervalCOMArray(beginNode, beginOffset,
1365 endNode, endOffset,
1366 PR_TRUE, &ranges);
1367 NS_ENSURE_SUCCESS(rv, rv);
1368 for (PRInt32 i = 0; i < ranges.Count(); i ++)
1369 RemoveRange(aSpellCheckSelection, ranges[i]);
1373 // some words are special and don't need checking
1374 if (dontCheckWord)
1375 continue;
1377 // some nodes we don't spellcheck
1378 PRBool checkSpelling;
1379 rv = SkipSpellCheckForNode(editor, beginNode, &checkSpelling);
1380 NS_ENSURE_SUCCESS(rv, rv);
1381 if (!checkSpelling)
1382 continue;
1384 // Don't check spelling if we're inside the noCheckRange. This needs to
1385 // be done after we clear any old selection because the excluded word
1386 // might have been previously marked.
1388 // We do a simple check to see if the beginning of our word is in the
1389 // exclusion range. Because the exclusion range is a multiple of a word,
1390 // this is sufficient.
1391 if (noCheckRange) {
1392 PRBool inExclusion = PR_FALSE;
1393 noCheckRange->IsPointInRange(beginNode, beginOffset, &inExclusion);
1394 if (inExclusion)
1395 continue;
1398 // check spelling and add to selection if misspelled
1399 PRBool isMisspelled;
1400 aWordUtil.NormalizeWord(wordText);
1401 rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
1402 if (isMisspelled) {
1403 // misspelled words count extra toward the max
1404 wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY;
1405 AddRange(aSpellCheckSelection, wordRange);
1407 aStatus->mWordCount ++;
1408 if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
1409 SpellCheckSelectionIsFull())
1410 break;
1413 // see if we've run out of time, only check every N words for perf
1414 if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) {
1415 wordsSinceTimeCheck = 0;
1416 if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) {
1417 // stop checking, our time limit has been exceeded
1419 // move the range to encompass the stuff that needs checking
1420 rv = aStatus->mRange->SetStart(endNode, endOffset);
1421 if (NS_FAILED(rv)) {
1422 // The range might be unhappy because the beginning is after the
1423 // end. This is possible when the requested end was in the middle
1424 // of a word, just ignore this situation and assume we're done.
1425 return NS_OK;
1427 *aDoneChecking = PR_FALSE;
1428 return NS_OK;
1433 return NS_OK;
1436 // mozInlineSpellChecker::ResumeCheck
1438 // Called by the resume event when it fires. We will try to pick up where
1439 // the last resume left off.
1441 nsresult
1442 mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus)
1444 if (! mSpellCheck)
1445 return NS_OK; // spell checking has been turned off
1447 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
1448 if (! editor)
1449 return NS_OK; // editor is gone
1451 mozInlineSpellWordUtil wordUtil;
1452 nsresult rv = wordUtil.Init(mEditor);
1453 if (NS_FAILED(rv))
1454 return NS_OK; // editor doesn't like us, don't assert
1456 nsCOMPtr<nsISelection> spellCheckSelection;
1457 rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
1458 NS_ENSURE_SUCCESS(rv, rv);
1459 CleanupRangesInSelection(spellCheckSelection);
1461 rv = aStatus->FinishInitOnEvent(wordUtil);
1462 NS_ENSURE_SUCCESS(rv, rv);
1463 if (! aStatus->mRange)
1464 return NS_OK; // empty range, nothing to do
1466 PRBool doneChecking = PR_TRUE;
1467 if (aStatus->mOp == mozInlineSpellStatus::eOpSelection)
1468 rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus);
1469 else
1470 rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
1471 NS_ENSURE_SUCCESS(rv, rv);
1473 if (! doneChecking)
1474 rv = ScheduleSpellCheck(*aStatus);
1475 return rv;
1478 // mozInlineSpellChecker::IsPointInSelection
1480 // Determines if a given (node,offset) point is inside the given
1481 // selection. If so, the specific range of the selection that
1482 // intersects is places in *aRange. (There may be multiple disjoint
1483 // ranges in a selection.)
1485 // If there is no intersection, *aRange will be NULL.
1487 nsresult
1488 mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection,
1489 nsIDOMNode *aNode,
1490 PRInt32 aOffset,
1491 nsIDOMRange **aRange)
1493 *aRange = nsnull;
1495 nsresult rv;
1496 nsCOMPtr<nsISelection2> sel2 = do_QueryInterface(aSelection, &rv);
1497 NS_ENSURE_SUCCESS(rv, rv);
1499 nsCOMArray<nsIDOMRange> ranges;
1500 rv = sel2->GetRangesForIntervalCOMArray(aNode, aOffset, aNode, aOffset,
1501 PR_TRUE, &ranges);
1502 NS_ENSURE_SUCCESS(rv, rv);
1504 if (ranges.Count() == 0)
1505 return NS_OK; // no matches
1507 // there may be more than one range returned, and we don't know what do
1508 // do with that, so just get the first one
1509 NS_ADDREF(*aRange = ranges[0]);
1510 return NS_OK;
1513 nsresult
1514 mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection)
1516 // integrity check - remove ranges that have collapsed to nothing. This
1517 // can happen if the node containing a highlighted word was removed.
1518 NS_ENSURE_ARG_POINTER(aSelection);
1520 PRInt32 count;
1521 aSelection->GetRangeCount(&count);
1523 for (PRInt32 index = 0; index < count; index++)
1525 nsCOMPtr<nsIDOMRange> checkRange;
1526 aSelection->GetRangeAt(index, getter_AddRefs(checkRange));
1528 if (checkRange)
1530 PRBool collapsed;
1531 checkRange->GetCollapsed(&collapsed);
1532 if (collapsed)
1534 RemoveRange(aSelection, checkRange);
1535 index--;
1536 count--;
1541 return NS_OK;
1545 // mozInlineSpellChecker::RemoveRange
1547 // For performance reasons, we have an upper bound on the number of word
1548 // ranges in the spell check selection. When removing a range from the
1549 // selection, we need to decrement mNumWordsInSpellSelection
1551 nsresult
1552 mozInlineSpellChecker::RemoveRange(nsISelection* aSpellCheckSelection,
1553 nsIDOMRange* aRange)
1555 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1556 NS_ENSURE_ARG_POINTER(aRange);
1558 nsresult rv = aSpellCheckSelection->RemoveRange(aRange);
1559 if (NS_SUCCEEDED(rv) && mNumWordsInSpellSelection)
1560 mNumWordsInSpellSelection--;
1562 return rv;
1566 // mozInlineSpellChecker::AddRange
1568 // For performance reasons, we have an upper bound on the number of word
1569 // ranges we'll add to the spell check selection. Once we reach that upper
1570 // bound, stop adding the ranges
1572 nsresult
1573 mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection,
1574 nsIDOMRange* aRange)
1576 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1577 NS_ENSURE_ARG_POINTER(aRange);
1579 nsresult rv = NS_OK;
1581 if (!SpellCheckSelectionIsFull())
1583 rv = aSpellCheckSelection->AddRange(aRange);
1584 if (NS_SUCCEEDED(rv))
1585 mNumWordsInSpellSelection++;
1588 return rv;
1591 nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection)
1593 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1594 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1596 nsCOMPtr<nsISelectionController> selcon;
1597 nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon));
1598 NS_ENSURE_SUCCESS(rv, rv);
1600 nsCOMPtr<nsISelection> spellCheckSelection;
1601 return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection);
1604 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
1606 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1607 NS_ENSURE_TRUE(editor, NS_OK);
1609 // figure out the old caret position based on the current selection
1610 nsCOMPtr<nsISelection> selection;
1611 nsresult rv = editor->GetSelection(getter_AddRefs(selection));
1612 NS_ENSURE_SUCCESS(rv, rv);
1614 rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode));
1615 NS_ENSURE_SUCCESS(rv, rv);
1617 selection->GetFocusOffset(&mCurrentSelectionOffset);
1619 return NS_OK;
1622 // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
1623 // for XPCOM's rap sheet
1624 PRBool // static
1625 ContentIsDescendantOf(nsINode* aPossibleDescendant,
1626 nsINode* aPossibleAncestor)
1628 NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
1629 NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
1631 do {
1632 if (aPossibleDescendant == aPossibleAncestor)
1633 return PR_TRUE;
1634 aPossibleDescendant = aPossibleDescendant->GetNodeParent();
1635 } while (aPossibleDescendant);
1637 return PR_FALSE;
1640 // mozInlineSpellChecker::HandleNavigationEvent
1642 // Acts upon mouse clicks and keyboard navigation changes, spell checking
1643 // the previous word if the new navigation location moves us to another
1644 // word.
1646 // This is complicated by the fact that our mouse events are happening after
1647 // selection has been changed to account for the mouse click. But keyboard
1648 // events are happening before the caret selection has changed. Working
1649 // around this by letting keyboard events setting forceWordSpellCheck to
1650 // true. aNewPositionOffset also tries to work around this for the
1651 // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1653 nsresult
1654 mozInlineSpellChecker::HandleNavigationEvent(nsIDOMEvent* aEvent,
1655 PRBool aForceWordSpellCheck,
1656 PRInt32 aNewPositionOffset)
1658 nsresult rv;
1660 // If we already handled the navigation event and there is no possibility
1661 // anything has changed since then, we don't have to do anything. This
1662 // optimization makes a noticable difference when you hold down a navigation
1663 // key like Page Down.
1664 if (! mNeedsCheckAfterNavigation)
1665 return NS_OK;
1667 nsCOMPtr<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode;
1668 PRInt32 currentAnchorOffset = mCurrentSelectionOffset;
1670 // now remember the new focus position resulting from the event
1671 rv = SaveCurrentSelectionPosition();
1672 NS_ENSURE_SUCCESS(rv, rv);
1674 PRBool shouldPost;
1675 mozInlineSpellStatus status(this);
1676 rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset,
1677 currentAnchorNode, currentAnchorOffset,
1678 mCurrentSelectionAnchorNode, mCurrentSelectionOffset,
1679 &shouldPost);
1680 NS_ENSURE_SUCCESS(rv, rv);
1681 if (shouldPost) {
1682 rv = ScheduleSpellCheck(status);
1683 NS_ENSURE_SUCCESS(rv, rv);
1686 return NS_OK;
1689 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent)
1691 return NS_OK;
1694 NS_IMETHODIMP mozInlineSpellChecker::Focus(nsIDOMEvent* aEvent)
1696 return NS_OK;
1699 NS_IMETHODIMP mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent)
1701 // force spellcheck on blur, for instance when tabbing out of a textbox
1702 HandleNavigationEvent(aEvent, PR_TRUE);
1703 return NS_OK;
1706 NS_IMETHODIMP mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent)
1708 nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent);
1709 NS_ENSURE_TRUE(mouseEvent, NS_OK);
1711 // ignore any errors from HandleNavigationEvent as we don't want to prevent
1712 // anyone else from seeing this event.
1713 PRUint16 button;
1714 mouseEvent->GetButton(&button);
1715 if (button == 0)
1716 HandleNavigationEvent(mouseEvent, PR_FALSE);
1717 else
1718 HandleNavigationEvent(mouseEvent, PR_TRUE);
1719 return NS_OK;
1722 NS_IMETHODIMP mozInlineSpellChecker::MouseDown(nsIDOMEvent* aMouseEvent)
1724 return NS_OK;
1727 NS_IMETHODIMP mozInlineSpellChecker::MouseUp(nsIDOMEvent* aMouseEvent)
1729 return NS_OK;
1732 NS_IMETHODIMP mozInlineSpellChecker::MouseDblClick(nsIDOMEvent* aMouseEvent)
1734 return NS_OK;
1737 NS_IMETHODIMP mozInlineSpellChecker::MouseOver(nsIDOMEvent* aMouseEvent)
1739 return NS_OK;
1742 NS_IMETHODIMP mozInlineSpellChecker::MouseOut(nsIDOMEvent* aMouseEvent)
1744 return NS_OK;
1747 NS_IMETHODIMP mozInlineSpellChecker::KeyDown(nsIDOMEvent* aKeyEvent)
1749 return NS_OK;
1753 NS_IMETHODIMP mozInlineSpellChecker::KeyUp(nsIDOMEvent* aKeyEvent)
1755 return NS_OK;
1759 NS_IMETHODIMP mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
1761 nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent);
1762 NS_ENSURE_TRUE(keyEvent, NS_OK);
1764 PRUint32 keyCode;
1765 keyEvent->GetKeyCode(&keyCode);
1767 // we only care about navigation keys that moved selection
1768 switch (keyCode)
1770 case nsIDOMKeyEvent::DOM_VK_RIGHT:
1771 case nsIDOMKeyEvent::DOM_VK_LEFT:
1772 HandleNavigationEvent(aKeyEvent, PR_FALSE, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1);
1773 break;
1774 case nsIDOMKeyEvent::DOM_VK_UP:
1775 case nsIDOMKeyEvent::DOM_VK_DOWN:
1776 case nsIDOMKeyEvent::DOM_VK_HOME:
1777 case nsIDOMKeyEvent::DOM_VK_END:
1778 case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
1779 case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
1780 HandleNavigationEvent(aKeyEvent, PR_TRUE /* force a spelling correction */);
1781 break;
1784 return NS_OK;