Bug 470455 - test_database_sync_embed_visits.js leaks, r=sdwilsh
[wine-gecko.git] / extensions / spellcheck / src / mozInlineSpellChecker.cpp
blobb654a28c9c32e1ccd158db4d3c44426fb94bd44c
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(PRBool aDestroyingFrames)
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 if (!aDestroyingFrames) {
584 spellCheckSelection->RemoveAllRanges();
587 rv = UnregisterEventListeners();
589 mEditor = nsnull;
591 return rv;
594 // mozInlineSpellChecker::CanEnableInlineSpellChecking
596 // This function can be called to see if it seems likely that we can enable
597 // spellchecking before actually creating the InlineSpellChecking objects.
599 // The problem is that we can't get the dictionary list without actually
600 // creating a whole bunch of spellchecking objects. This function tries to
601 // do that and caches the result so we don't have to keep allocating those
602 // objects if there are no dictionaries or spellchecking.
604 // This caching will prevent adding dictionaries at runtime if we start out
605 // with no dictionaries! Installing dictionaries as extensions will require
606 // a restart anyway, so it shouldn't be a problem.
608 PRBool // static
609 mozInlineSpellChecker::CanEnableInlineSpellChecking()
611 nsresult rv;
612 if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
613 gCanEnableSpellChecking = SpellCheck_NotAvailable;
615 nsCOMPtr<nsIEditorSpellCheck> spellchecker =
616 do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv);
617 NS_ENSURE_SUCCESS(rv, PR_FALSE);
619 PRBool canSpellCheck = PR_FALSE;
620 rv = spellchecker->CanSpellCheck(&canSpellCheck);
621 NS_ENSURE_SUCCESS(rv, PR_FALSE);
623 if (canSpellCheck)
624 gCanEnableSpellChecking = SpellCheck_Available;
626 return (gCanEnableSpellChecking == SpellCheck_Available);
629 // mozInlineSpellChecker::RegisterEventListeners
631 // The inline spell checker listens to mouse events and keyboard navigation+ // events.
633 nsresult
634 mozInlineSpellChecker::RegisterEventListeners()
636 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
637 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
639 editor->AddEditActionListener(this);
641 nsCOMPtr<nsIDOMDocument> doc;
642 nsresult rv = editor->GetDocument(getter_AddRefs(doc));
643 NS_ENSURE_SUCCESS(rv, rv);
645 nsCOMPtr<nsPIDOMEventTarget> piTarget = do_QueryInterface(doc, &rv);
646 NS_ENSURE_SUCCESS(rv, rv);
648 nsCOMPtr<nsIEventListenerManager> elmP;
649 piTarget->GetListenerManager(PR_TRUE, getter_AddRefs(elmP));
650 if (elmP) {
651 // Focus event doesn't bubble so adding the listener to capturing phase
652 elmP->AddEventListenerByIID(static_cast<nsIDOMFocusListener *>(this),
653 NS_GET_IID(nsIDOMFocusListener),
654 NS_EVENT_FLAG_CAPTURE);
657 piTarget->AddEventListenerByIID(static_cast<nsIDOMMouseListener*>(this),
658 NS_GET_IID(nsIDOMMouseListener));
659 piTarget->AddEventListenerByIID(static_cast<nsIDOMKeyListener*>(this),
660 NS_GET_IID(nsIDOMKeyListener));
662 return NS_OK;
665 // mozInlineSpellChecker::UnregisterEventListeners
667 nsresult
668 mozInlineSpellChecker::UnregisterEventListeners()
670 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
671 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
673 editor->RemoveEditActionListener(this);
675 nsCOMPtr<nsIDOMDocument> doc;
676 editor->GetDocument(getter_AddRefs(doc));
677 NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
679 nsCOMPtr<nsPIDOMEventTarget> piTarget = do_QueryInterface(doc);
680 NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER);
682 nsCOMPtr<nsIEventListenerManager> elmP;
683 piTarget->GetListenerManager(PR_TRUE, getter_AddRefs(elmP));
684 if (elmP) {
685 elmP->RemoveEventListenerByIID(static_cast<nsIDOMFocusListener *>(this),
686 NS_GET_IID(nsIDOMFocusListener),
687 NS_EVENT_FLAG_CAPTURE);
690 piTarget->RemoveEventListenerByIID(static_cast<nsIDOMMouseListener*>(this),
691 NS_GET_IID(nsIDOMMouseListener));
692 piTarget->RemoveEventListenerByIID(static_cast<nsIDOMKeyListener*>(this),
693 NS_GET_IID(nsIDOMKeyListener));
695 return NS_OK;
698 // mozInlineSpellChecker::GetEnableRealTimeSpell
700 NS_IMETHODIMP
701 mozInlineSpellChecker::GetEnableRealTimeSpell(PRBool* aEnabled)
703 NS_ENSURE_ARG_POINTER(aEnabled);
704 *aEnabled = mSpellCheck != nsnull;
705 return NS_OK;
708 // mozInlineSpellChecker::SetEnableRealTimeSpell
710 NS_IMETHODIMP
711 mozInlineSpellChecker::SetEnableRealTimeSpell(PRBool aEnabled)
713 if (!aEnabled) {
714 mSpellCheck = nsnull;
715 return Cleanup(PR_FALSE);
718 if (!mSpellCheck) {
719 nsresult res = NS_OK;
720 nsCOMPtr<nsIEditorSpellCheck> spellchecker = do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &res);
721 if (NS_SUCCEEDED(res) && spellchecker)
723 nsCOMPtr<nsITextServicesFilter> filter = do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1", &res);
724 spellchecker->SetFilter(filter);
725 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
726 res = spellchecker->InitSpellChecker(editor, PR_FALSE);
727 NS_ENSURE_SUCCESS(res, res);
729 nsCOMPtr<nsITextServicesDocument> tsDoc = do_CreateInstance("@mozilla.org/textservices/textservicesdocument;1", &res);
730 NS_ENSURE_SUCCESS(res, res);
732 res = tsDoc->SetFilter(filter);
733 NS_ENSURE_SUCCESS(res, res);
735 res = tsDoc->InitWithEditor(editor);
736 NS_ENSURE_SUCCESS(res, res);
738 mTextServicesDocument = tsDoc;
739 mSpellCheck = spellchecker;
741 // spell checking is enabled, register our event listeners to track navigation
742 RegisterEventListeners();
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(nsnull);
753 // mozInlineSpellChecker::SpellCheckAfterEditorChange
755 // Called by the editor when nearly anything happens to change the content.
757 // The start and end positions specify a range for the thing that happened,
758 // but these are usually NULL, even when you'd think they would be useful
759 // because you want the range (for example, pasting). We ignore them in
760 // this case.
762 NS_IMETHODIMP
763 mozInlineSpellChecker::SpellCheckAfterEditorChange(
764 PRInt32 aAction, nsISelection *aSelection,
765 nsIDOMNode *aPreviousSelectedNode, PRInt32 aPreviousSelectedOffset,
766 nsIDOMNode *aStartNode, PRInt32 aStartOffset,
767 nsIDOMNode *aEndNode, PRInt32 aEndOffset)
769 nsresult rv;
770 NS_ENSURE_ARG_POINTER(aSelection);
771 if (!mSpellCheck)
772 return NS_OK; // disabling spell checking is not an error
774 // this means something has changed, and we never check the current word,
775 // therefore, we should spellcheck for subsequent caret navigations
776 mNeedsCheckAfterNavigation = PR_TRUE;
778 // the anchor node is the position of the caret
779 nsCOMPtr<nsIDOMNode> anchorNode;
780 rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode));
781 NS_ENSURE_SUCCESS(rv, rv);
782 PRInt32 anchorOffset;
783 rv = aSelection->GetAnchorOffset(&anchorOffset);
784 NS_ENSURE_SUCCESS(rv, rv);
786 mozInlineSpellStatus status(this);
787 rv = status.InitForEditorChange(aAction,
788 anchorNode, anchorOffset,
789 aPreviousSelectedNode, aPreviousSelectedOffset,
790 aStartNode, aStartOffset,
791 aEndNode, aEndOffset);
792 NS_ENSURE_SUCCESS(rv, rv);
793 rv = ScheduleSpellCheck(status);
794 NS_ENSURE_SUCCESS(rv, rv);
796 // remember the current caret position after every change
797 SaveCurrentSelectionPosition();
798 return NS_OK;
801 // mozInlineSpellChecker::SpellCheckRange
803 // Spellchecks all the words in the given range.
804 // Supply a NULL range and this will check the entire editor.
806 nsresult
807 mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange)
809 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
811 mozInlineSpellStatus status(this);
812 nsresult rv = status.InitForRange(aRange);
813 NS_ENSURE_SUCCESS(rv, rv);
814 return ScheduleSpellCheck(status);
817 // mozInlineSpellChecker::GetMispelledWord
819 NS_IMETHODIMP
820 mozInlineSpellChecker::GetMispelledWord(nsIDOMNode *aNode, PRInt32 aOffset,
821 nsIDOMRange **newword)
823 NS_ENSURE_ARG_POINTER(aNode);
824 nsCOMPtr<nsISelection> spellCheckSelection;
825 nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
826 NS_ENSURE_SUCCESS(res, res);
828 return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword);
831 // mozInlineSpellChecker::ReplaceWord
833 NS_IMETHODIMP
834 mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, PRInt32 aOffset,
835 const nsAString &newword)
837 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
838 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
839 NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
841 nsCOMPtr<nsIDOMRange> range;
842 nsresult res = GetMispelledWord(aNode, aOffset, getter_AddRefs(range));
843 NS_ENSURE_SUCCESS(res, res);
845 if (range)
847 editor->BeginTransaction();
849 nsCOMPtr<nsISelection> selection;
850 res = editor->GetSelection(getter_AddRefs(selection));
851 NS_ENSURE_SUCCESS(res, res);
852 selection->RemoveAllRanges();
853 selection->AddRange(range);
854 editor->DeleteSelection(nsIEditor::eNone);
856 nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
857 textEditor->InsertText(newword);
859 editor->EndTransaction();
862 return NS_OK;
865 // mozInlineSpellChecker::AddWordToDictionary
867 NS_IMETHODIMP
868 mozInlineSpellChecker::AddWordToDictionary(const nsAString &word)
870 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
872 nsAutoString wordstr(word);
873 nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get());
874 NS_ENSURE_SUCCESS(rv, rv);
876 mozInlineSpellStatus status(this);
877 rv = status.InitForSelection();
878 NS_ENSURE_SUCCESS(rv, rv);
879 return ScheduleSpellCheck(status);
882 // mozInlineSpellChecker::IgnoreWord
884 NS_IMETHODIMP
885 mozInlineSpellChecker::IgnoreWord(const nsAString &word)
887 NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
889 nsAutoString wordstr(word);
890 nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get());
891 NS_ENSURE_SUCCESS(rv, rv);
893 mozInlineSpellStatus status(this);
894 rv = status.InitForSelection();
895 NS_ENSURE_SUCCESS(rv, rv);
896 return ScheduleSpellCheck(status);
899 // mozInlineSpellChecker::IgnoreWords
901 NS_IMETHODIMP
902 mozInlineSpellChecker::IgnoreWords(const PRUnichar **aWordsToIgnore,
903 PRUint32 aCount)
905 // add each word to the ignore list and then recheck the document
906 for (PRUint32 index = 0; index < aCount; index++)
907 mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]);
909 mozInlineSpellStatus status(this);
910 nsresult rv = status.InitForSelection();
911 NS_ENSURE_SUCCESS(rv, rv);
912 return ScheduleSpellCheck(status);
915 NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, PRInt32 aPosition)
917 return NS_OK;
920 NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent,
921 PRInt32 aPosition, nsresult aResult)
923 return NS_OK;
926 NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
927 PRInt32 aPosition)
929 return NS_OK;
932 NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
933 PRInt32 aPosition, nsresult aResult)
936 return NS_OK;
939 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild)
941 return NS_OK;
944 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
946 return NS_OK;
949 NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, PRInt32 aOffset)
951 return NS_OK;
954 NS_IMETHODIMP
955 mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode,
956 PRInt32 aOffset,
957 nsIDOMNode *aNewLeftNode, nsresult aResult)
959 return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
962 NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent)
964 return NS_OK;
967 NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode,
968 nsIDOMNode *aParent, nsresult aResult)
970 return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0);
973 NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, const nsAString & aString)
975 return NS_OK;
978 NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset,
979 const nsAString & aString, nsresult aResult)
981 return NS_OK;
984 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, PRInt32 aLength)
986 return NS_OK;
989 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, PRInt32 aLength, nsresult aResult)
991 return NS_OK;
994 NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection)
996 return NS_OK;
999 NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection)
1001 return NS_OK;
1004 // mozInlineSpellChecker::MakeSpellCheckRange
1006 // Given begin and end positions, this function constructs a range as
1007 // required for ScheduleSpellCheck. If the start and end nodes are NULL,
1008 // then the entire range will be selected, and you can supply -1 as the
1009 // offset to the end range to select all of that node.
1011 // If the resulting range would be empty, NULL is put into *aRange and the
1012 // function succeeds.
1014 nsresult
1015 mozInlineSpellChecker::MakeSpellCheckRange(
1016 nsIDOMNode* aStartNode, PRInt32 aStartOffset,
1017 nsIDOMNode* aEndNode, PRInt32 aEndOffset,
1018 nsIDOMRange** aRange)
1020 nsresult rv;
1021 *aRange = nsnull;
1023 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1024 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1026 nsCOMPtr<nsIDOMDocument> doc;
1027 rv = editor->GetDocument(getter_AddRefs(doc));
1028 NS_ENSURE_SUCCESS(rv, rv);
1030 nsCOMPtr<nsIDOMDocumentRange> docrange = do_QueryInterface(doc);
1031 NS_ENSURE_TRUE(docrange, NS_ERROR_FAILURE);
1033 nsCOMPtr<nsIDOMRange> range;
1034 rv = docrange->CreateRange(getter_AddRefs(range));
1035 NS_ENSURE_SUCCESS(rv, rv);
1037 // possibly use full range of the editor
1038 nsCOMPtr<nsIDOMElement> rootElem;
1039 if (! aStartNode || ! aEndNode) {
1040 rv = editor->GetRootElement(getter_AddRefs(rootElem));
1041 NS_ENSURE_SUCCESS(rv, rv);
1043 aStartNode = rootElem;
1044 aStartOffset = 0;
1046 aEndNode = rootElem;
1047 aEndOffset = -1;
1050 if (aEndOffset == -1) {
1051 nsCOMPtr<nsIDOMNodeList> childNodes;
1052 rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes));
1053 NS_ENSURE_SUCCESS(rv, rv);
1055 PRUint32 childCount;
1056 rv = childNodes->GetLength(&childCount);
1057 NS_ENSURE_SUCCESS(rv, rv);
1059 aEndOffset = childCount;
1062 // sometimes we are are requested to check an empty range (possibly an empty
1063 // document). This will result in assertions later.
1064 if (aStartNode == aEndNode && aStartOffset == aEndOffset)
1065 return NS_OK;
1067 rv = range->SetStart(aStartNode, aStartOffset);
1068 NS_ENSURE_SUCCESS(rv, rv);
1069 if (aEndOffset)
1070 rv = range->SetEnd(aEndNode, aEndOffset);
1071 else
1072 rv = range->SetEndAfter(aEndNode);
1073 NS_ENSURE_SUCCESS(rv, rv);
1075 range.swap(*aRange);
1076 return NS_OK;
1079 nsresult
1080 mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
1081 PRInt32 aStartOffset,
1082 nsIDOMNode *aEndNode,
1083 PRInt32 aEndOffset)
1085 nsCOMPtr<nsIDOMRange> range;
1086 nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset,
1087 aEndNode, aEndOffset,
1088 getter_AddRefs(range));
1089 NS_ENSURE_SUCCESS(rv, rv);
1091 if (! range)
1092 return NS_OK; // range is empty: nothing to do
1094 mozInlineSpellStatus status(this);
1095 rv = status.InitForRange(range);
1096 NS_ENSURE_SUCCESS(rv, rv);
1097 return ScheduleSpellCheck(status);
1100 // mozInlineSpellChecker::SkipSpellCheckForNode
1102 // There are certain conditions when we don't want to spell check a node. In
1103 // particular quotations, moz signatures, etc. This routine returns false
1104 // for these cases.
1106 nsresult
1107 mozInlineSpellChecker::SkipSpellCheckForNode(nsIEditor* aEditor,
1108 nsIDOMNode *aNode,
1109 PRBool *checkSpelling)
1111 *checkSpelling = PR_TRUE;
1112 NS_ENSURE_ARG_POINTER(aNode);
1114 PRUint32 flags;
1115 aEditor->GetFlags(&flags);
1116 if (flags & nsIPlaintextEditor::eEditorMailMask)
1118 nsCOMPtr<nsIDOMNode> parent;
1119 aNode->GetParentNode(getter_AddRefs(parent));
1121 while (parent)
1123 nsCOMPtr<nsIDOMElement> parentElement = do_QueryInterface(parent);
1124 if (!parentElement)
1125 break;
1127 nsAutoString parentTagName;
1128 parentElement->GetTagName(parentTagName);
1130 if (parentTagName.Equals(NS_LITERAL_STRING("blockquote"), nsCaseInsensitiveStringComparator()))
1132 *checkSpelling = PR_FALSE;
1133 break;
1135 else if (parentTagName.Equals(NS_LITERAL_STRING("pre"), nsCaseInsensitiveStringComparator()))
1137 nsAutoString classname;
1138 parentElement->GetAttribute(NS_LITERAL_STRING("class"),classname);
1139 if (classname.Equals(NS_LITERAL_STRING("moz-signature")))
1140 *checkSpelling = PR_FALSE;
1143 nsCOMPtr<nsIDOMNode> nextParent;
1144 parent->GetParentNode(getter_AddRefs(nextParent));
1145 parent = nextParent;
1148 else {
1149 // XXX Do we really want this for all read-write content?
1150 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
1151 *checkSpelling = !!(content->IntrinsicState() & NS_EVENT_STATE_MOZ_READWRITE);
1154 return NS_OK;
1157 // mozInlineSpellChecker::ScheduleSpellCheck
1159 // This is called by code to do the actual spellchecking. We will set up
1160 // the proper structures for calls to DoSpellCheck.
1162 nsresult
1163 mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus)
1165 mozInlineSpellResume* resume = new mozInlineSpellResume(aStatus);
1166 NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1168 nsresult rv = resume->Post();
1169 if (NS_FAILED(rv))
1170 delete resume;
1171 return rv;
1174 // mozInlineSpellChecker::DoSpellCheckSelection
1176 // Called to re-check all misspelled words. We iterate over all ranges in
1177 // the selection and call DoSpellCheck on them. This is used when a word
1178 // is ignored or added to the dictionary: all instances of that word should
1179 // be removed from the selection.
1181 // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1182 // Typically, checking this small amount of text is relatively fast, but
1183 // for large numbers of words, a lag may be noticable.
1185 nsresult
1186 mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
1187 nsISelection* aSpellCheckSelection,
1188 mozInlineSpellStatus* aStatus)
1190 nsresult rv;
1192 // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1193 mNumWordsInSpellSelection = 0;
1195 // Since we could be modifying the ranges for the spellCheckSelection while
1196 // looping on the spell check selection, keep a separate array of range
1197 // elements inside the selection
1198 nsCOMArray<nsIDOMRange> ranges;
1200 PRInt32 count;
1201 aSpellCheckSelection->GetRangeCount(&count);
1203 PRInt32 idx;
1204 nsCOMPtr<nsIDOMRange> checkRange;
1205 for (idx = 0; idx < count; idx ++) {
1206 aSpellCheckSelection->GetRangeAt(idx, getter_AddRefs(checkRange));
1207 if (checkRange) {
1208 if (! ranges.AppendObject(checkRange))
1209 return NS_ERROR_OUT_OF_MEMORY;
1213 // We have saved the ranges above. Clearing the spellcheck selection here
1214 // isn't necessary (rechecking each word will modify it as necessary) but
1215 // provides better performance. By ensuring that no ranges need to be
1216 // removed in DoSpellCheck, we can save checking range inclusion which is
1217 // slow.
1218 aSpellCheckSelection->RemoveAllRanges();
1220 // We use this state object for all calls, and just update its range. Note
1221 // that we don't need to call FinishInit since we will be filling in the
1222 // necessary information.
1223 mozInlineSpellStatus status(this);
1224 rv = status.InitForRange(nsnull);
1225 NS_ENSURE_SUCCESS(rv, rv);
1227 PRBool doneChecking;
1228 for (idx = 0; idx < count; idx ++) {
1229 checkRange = ranges[idx];
1230 if (checkRange) {
1231 // We can consider this word as "added" since we know it has no spell
1232 // check range over it that needs to be deleted. All the old ranges
1233 // were cleared above. We also need to clear the word count so that we
1234 // check all words instead of stopping early.
1235 status.mRange = checkRange;
1236 rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status,
1237 &doneChecking);
1238 NS_ENSURE_SUCCESS(rv, rv);
1239 NS_ASSERTION(doneChecking, "We gave the spellchecker one word, but it didn't finish checking?!?!");
1241 status.mWordCount = 0;
1245 return NS_OK;
1248 // mozInlineSpellChecker::DoSpellCheck
1250 // This function checks words intersecting the given range, excluding those
1251 // inside mStatus->mNoCheckRange (can be NULL). Words inside aNoCheckRange
1252 // will have any spell selection removed (this is used to hide the
1253 // underlining for the word that the caret is in). aNoCheckRange should be
1254 // on word boundaries.
1256 // mResume->mCreatedRange is a possibly NULL range of new text that was
1257 // inserted. Inside this range, we don't bother to check whether things are
1258 // inside the spellcheck selection, which speeds up large paste operations
1259 // considerably.
1261 // Normal case when editing text by typing
1262 // h e l l o w o r k d h o w a r e y o u
1263 // ^ caret
1264 // [-------] mRange
1265 // [-------] mNoCheckRange
1266 // -> does nothing (range is the same as the no check range)
1268 // Case when pasting:
1269 // [---------- pasted text ----------]
1270 // h e l l o w o r k d h o w a r e y o u
1271 // ^ caret
1272 // [---] aNoCheckRange
1273 // -> recheck all words in range except those in aNoCheckRange
1275 // If checking is complete, *aDoneChecking will be set. If there is more
1276 // but we ran out of time, this will be false and the range will be
1277 // updated with the stuff that still needs checking.
1279 nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
1280 nsISelection *aSpellCheckSelection,
1281 mozInlineSpellStatus* aStatus,
1282 PRBool* aDoneChecking)
1284 nsCOMPtr<nsIDOMNode> beginNode, endNode;
1285 PRInt32 beginOffset, endOffset;
1286 *aDoneChecking = PR_TRUE;
1288 // get the editor for SkipSpellCheckForNode, this may fail in reasonable
1289 // circumstances since the editor could have gone away
1290 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1291 if (! editor)
1292 return NS_ERROR_FAILURE;
1294 PRBool iscollapsed;
1295 nsresult rv = aStatus->mRange->GetCollapsed(&iscollapsed);
1296 NS_ENSURE_SUCCESS(rv, rv);
1297 if (iscollapsed)
1298 return NS_OK;
1300 nsCOMPtr<nsISelection2> sel2 = do_QueryInterface(aSpellCheckSelection, &rv);
1301 NS_ENSURE_SUCCESS(rv, rv);
1303 // see if the selection has any ranges, if not, then we can optimize checking
1304 // range inclusion later (we have no ranges when we are initially checking or
1305 // when there are no misspelled words yet).
1306 PRInt32 originalRangeCount;
1307 rv = aSpellCheckSelection->GetRangeCount(&originalRangeCount);
1308 NS_ENSURE_SUCCESS(rv, rv);
1310 // set the starting DOM position to be the beginning of our range
1311 NS_ENSURE_SUCCESS(rv, rv);
1312 aStatus->mRange->GetStartContainer(getter_AddRefs(beginNode));
1313 aStatus->mRange->GetStartOffset(&beginOffset);
1314 aStatus->mRange->GetEndContainer(getter_AddRefs(endNode));
1315 aStatus->mRange->GetEndOffset(&endOffset);
1316 aWordUtil.SetEnd(endNode, endOffset);
1317 aWordUtil.SetPosition(beginNode, beginOffset);
1319 // aWordUtil.SetPosition flushes pending notifications, check editor again.
1320 editor = do_QueryReferent(mEditor);
1321 if (! editor)
1322 return NS_ERROR_FAILURE;
1324 // we need to use IsPointInRange which is on a more specific interface
1325 nsCOMPtr<nsIDOMNSRange> noCheckRange, createdRange;
1326 if (aStatus->mNoCheckRange)
1327 noCheckRange = do_QueryInterface(aStatus->mNoCheckRange);
1328 if (aStatus->mCreatedRange)
1329 createdRange = do_QueryInterface(aStatus->mCreatedRange);
1331 PRInt32 wordsSinceTimeCheck = 0;
1332 PRTime beginTime = PR_Now();
1334 nsAutoString wordText;
1335 nsCOMPtr<nsIDOMRange> wordRange;
1336 PRBool dontCheckWord;
1337 while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText,
1338 getter_AddRefs(wordRange),
1339 &dontCheckWord)) &&
1340 wordRange) {
1341 wordsSinceTimeCheck ++;
1343 // get the range for the current word
1344 wordRange->GetStartContainer(getter_AddRefs(beginNode));
1345 wordRange->GetEndContainer(getter_AddRefs(endNode));
1346 wordRange->GetStartOffset(&beginOffset);
1347 wordRange->GetEndOffset(&endOffset);
1349 #ifdef DEBUG_INLINESPELL
1350 printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get());
1351 if (dontCheckWord)
1352 printf(" (not checking)");
1353 printf("\n");
1354 #endif
1356 // see if there is a spellcheck range that already intersects the word
1357 // and remove it. We only need to remove old ranges, so don't bother if
1358 // there were no ranges when we started out.
1359 if (originalRangeCount > 0) {
1360 // likewise, if this word is inside new text, we won't bother testing
1361 PRBool inCreatedRange = PR_FALSE;
1362 if (createdRange)
1363 createdRange->IsPointInRange(beginNode, beginOffset, &inCreatedRange);
1364 if (! inCreatedRange) {
1365 nsCOMArray<nsIDOMRange> ranges;
1366 rv = sel2->GetRangesForIntervalCOMArray(beginNode, beginOffset,
1367 endNode, endOffset,
1368 PR_TRUE, &ranges);
1369 NS_ENSURE_SUCCESS(rv, rv);
1370 for (PRInt32 i = 0; i < ranges.Count(); i ++)
1371 RemoveRange(aSpellCheckSelection, ranges[i]);
1375 // some words are special and don't need checking
1376 if (dontCheckWord)
1377 continue;
1379 // some nodes we don't spellcheck
1380 PRBool checkSpelling;
1381 rv = SkipSpellCheckForNode(editor, beginNode, &checkSpelling);
1382 NS_ENSURE_SUCCESS(rv, rv);
1383 if (!checkSpelling)
1384 continue;
1386 // Don't check spelling if we're inside the noCheckRange. This needs to
1387 // be done after we clear any old selection because the excluded word
1388 // might have been previously marked.
1390 // We do a simple check to see if the beginning of our word is in the
1391 // exclusion range. Because the exclusion range is a multiple of a word,
1392 // this is sufficient.
1393 if (noCheckRange) {
1394 PRBool inExclusion = PR_FALSE;
1395 noCheckRange->IsPointInRange(beginNode, beginOffset, &inExclusion);
1396 if (inExclusion)
1397 continue;
1400 // check spelling and add to selection if misspelled
1401 PRBool isMisspelled;
1402 aWordUtil.NormalizeWord(wordText);
1403 rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
1404 if (isMisspelled) {
1405 // misspelled words count extra toward the max
1406 wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY;
1407 AddRange(aSpellCheckSelection, wordRange);
1409 aStatus->mWordCount ++;
1410 if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
1411 SpellCheckSelectionIsFull())
1412 break;
1415 // see if we've run out of time, only check every N words for perf
1416 if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) {
1417 wordsSinceTimeCheck = 0;
1418 if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) {
1419 // stop checking, our time limit has been exceeded
1421 // move the range to encompass the stuff that needs checking
1422 rv = aStatus->mRange->SetStart(endNode, endOffset);
1423 if (NS_FAILED(rv)) {
1424 // The range might be unhappy because the beginning is after the
1425 // end. This is possible when the requested end was in the middle
1426 // of a word, just ignore this situation and assume we're done.
1427 return NS_OK;
1429 *aDoneChecking = PR_FALSE;
1430 return NS_OK;
1435 return NS_OK;
1438 // mozInlineSpellChecker::ResumeCheck
1440 // Called by the resume event when it fires. We will try to pick up where
1441 // the last resume left off.
1443 nsresult
1444 mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus)
1446 if (! mSpellCheck)
1447 return NS_OK; // spell checking has been turned off
1449 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
1450 if (! editor)
1451 return NS_OK; // editor is gone
1453 mozInlineSpellWordUtil wordUtil;
1454 nsresult rv = wordUtil.Init(mEditor);
1455 if (NS_FAILED(rv))
1456 return NS_OK; // editor doesn't like us, don't assert
1458 nsCOMPtr<nsISelection> spellCheckSelection;
1459 rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
1460 NS_ENSURE_SUCCESS(rv, rv);
1461 CleanupRangesInSelection(spellCheckSelection);
1463 rv = aStatus->FinishInitOnEvent(wordUtil);
1464 NS_ENSURE_SUCCESS(rv, rv);
1465 if (! aStatus->mRange)
1466 return NS_OK; // empty range, nothing to do
1468 PRBool doneChecking = PR_TRUE;
1469 if (aStatus->mOp == mozInlineSpellStatus::eOpSelection)
1470 rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus);
1471 else
1472 rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
1473 NS_ENSURE_SUCCESS(rv, rv);
1475 if (! doneChecking)
1476 rv = ScheduleSpellCheck(*aStatus);
1477 return rv;
1480 // mozInlineSpellChecker::IsPointInSelection
1482 // Determines if a given (node,offset) point is inside the given
1483 // selection. If so, the specific range of the selection that
1484 // intersects is places in *aRange. (There may be multiple disjoint
1485 // ranges in a selection.)
1487 // If there is no intersection, *aRange will be NULL.
1489 nsresult
1490 mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection,
1491 nsIDOMNode *aNode,
1492 PRInt32 aOffset,
1493 nsIDOMRange **aRange)
1495 *aRange = nsnull;
1497 nsresult rv;
1498 nsCOMPtr<nsISelection2> sel2 = do_QueryInterface(aSelection, &rv);
1499 NS_ENSURE_SUCCESS(rv, rv);
1501 nsCOMArray<nsIDOMRange> ranges;
1502 rv = sel2->GetRangesForIntervalCOMArray(aNode, aOffset, aNode, aOffset,
1503 PR_TRUE, &ranges);
1504 NS_ENSURE_SUCCESS(rv, rv);
1506 if (ranges.Count() == 0)
1507 return NS_OK; // no matches
1509 // there may be more than one range returned, and we don't know what do
1510 // do with that, so just get the first one
1511 NS_ADDREF(*aRange = ranges[0]);
1512 return NS_OK;
1515 nsresult
1516 mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection)
1518 // integrity check - remove ranges that have collapsed to nothing. This
1519 // can happen if the node containing a highlighted word was removed.
1520 NS_ENSURE_ARG_POINTER(aSelection);
1522 PRInt32 count;
1523 aSelection->GetRangeCount(&count);
1525 for (PRInt32 index = 0; index < count; index++)
1527 nsCOMPtr<nsIDOMRange> checkRange;
1528 aSelection->GetRangeAt(index, getter_AddRefs(checkRange));
1530 if (checkRange)
1532 PRBool collapsed;
1533 checkRange->GetCollapsed(&collapsed);
1534 if (collapsed)
1536 RemoveRange(aSelection, checkRange);
1537 index--;
1538 count--;
1543 return NS_OK;
1547 // mozInlineSpellChecker::RemoveRange
1549 // For performance reasons, we have an upper bound on the number of word
1550 // ranges in the spell check selection. When removing a range from the
1551 // selection, we need to decrement mNumWordsInSpellSelection
1553 nsresult
1554 mozInlineSpellChecker::RemoveRange(nsISelection* aSpellCheckSelection,
1555 nsIDOMRange* aRange)
1557 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1558 NS_ENSURE_ARG_POINTER(aRange);
1560 nsresult rv = aSpellCheckSelection->RemoveRange(aRange);
1561 if (NS_SUCCEEDED(rv) && mNumWordsInSpellSelection)
1562 mNumWordsInSpellSelection--;
1564 return rv;
1568 // mozInlineSpellChecker::AddRange
1570 // For performance reasons, we have an upper bound on the number of word
1571 // ranges we'll add to the spell check selection. Once we reach that upper
1572 // bound, stop adding the ranges
1574 nsresult
1575 mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection,
1576 nsIDOMRange* aRange)
1578 NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1579 NS_ENSURE_ARG_POINTER(aRange);
1581 nsresult rv = NS_OK;
1583 if (!SpellCheckSelectionIsFull())
1585 rv = aSpellCheckSelection->AddRange(aRange);
1586 if (NS_SUCCEEDED(rv))
1587 mNumWordsInSpellSelection++;
1590 return rv;
1593 nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection)
1595 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1596 NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1598 nsCOMPtr<nsISelectionController> selcon;
1599 nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon));
1600 NS_ENSURE_SUCCESS(rv, rv);
1602 nsCOMPtr<nsISelection> spellCheckSelection;
1603 return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection);
1606 nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
1608 nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1609 NS_ENSURE_TRUE(editor, NS_OK);
1611 // figure out the old caret position based on the current selection
1612 nsCOMPtr<nsISelection> selection;
1613 nsresult rv = editor->GetSelection(getter_AddRefs(selection));
1614 NS_ENSURE_SUCCESS(rv, rv);
1616 rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode));
1617 NS_ENSURE_SUCCESS(rv, rv);
1619 selection->GetFocusOffset(&mCurrentSelectionOffset);
1621 return NS_OK;
1624 // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
1625 // for XPCOM's rap sheet
1626 PRBool // static
1627 ContentIsDescendantOf(nsINode* aPossibleDescendant,
1628 nsINode* aPossibleAncestor)
1630 NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
1631 NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
1633 do {
1634 if (aPossibleDescendant == aPossibleAncestor)
1635 return PR_TRUE;
1636 aPossibleDescendant = aPossibleDescendant->GetNodeParent();
1637 } while (aPossibleDescendant);
1639 return PR_FALSE;
1642 // mozInlineSpellChecker::HandleNavigationEvent
1644 // Acts upon mouse clicks and keyboard navigation changes, spell checking
1645 // the previous word if the new navigation location moves us to another
1646 // word.
1648 // This is complicated by the fact that our mouse events are happening after
1649 // selection has been changed to account for the mouse click. But keyboard
1650 // events are happening before the caret selection has changed. Working
1651 // around this by letting keyboard events setting forceWordSpellCheck to
1652 // true. aNewPositionOffset also tries to work around this for the
1653 // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1655 nsresult
1656 mozInlineSpellChecker::HandleNavigationEvent(nsIDOMEvent* aEvent,
1657 PRBool aForceWordSpellCheck,
1658 PRInt32 aNewPositionOffset)
1660 nsresult rv;
1662 // If we already handled the navigation event and there is no possibility
1663 // anything has changed since then, we don't have to do anything. This
1664 // optimization makes a noticable difference when you hold down a navigation
1665 // key like Page Down.
1666 if (! mNeedsCheckAfterNavigation)
1667 return NS_OK;
1669 nsCOMPtr<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode;
1670 PRInt32 currentAnchorOffset = mCurrentSelectionOffset;
1672 // now remember the new focus position resulting from the event
1673 rv = SaveCurrentSelectionPosition();
1674 NS_ENSURE_SUCCESS(rv, rv);
1676 PRBool shouldPost;
1677 mozInlineSpellStatus status(this);
1678 rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset,
1679 currentAnchorNode, currentAnchorOffset,
1680 mCurrentSelectionAnchorNode, mCurrentSelectionOffset,
1681 &shouldPost);
1682 NS_ENSURE_SUCCESS(rv, rv);
1683 if (shouldPost) {
1684 rv = ScheduleSpellCheck(status);
1685 NS_ENSURE_SUCCESS(rv, rv);
1688 return NS_OK;
1691 NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent)
1693 return NS_OK;
1696 NS_IMETHODIMP mozInlineSpellChecker::Focus(nsIDOMEvent* aEvent)
1698 return NS_OK;
1701 NS_IMETHODIMP mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent)
1703 // force spellcheck on blur, for instance when tabbing out of a textbox
1704 HandleNavigationEvent(aEvent, PR_TRUE);
1705 return NS_OK;
1708 NS_IMETHODIMP mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent)
1710 nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent);
1711 NS_ENSURE_TRUE(mouseEvent, NS_OK);
1713 // ignore any errors from HandleNavigationEvent as we don't want to prevent
1714 // anyone else from seeing this event.
1715 PRUint16 button;
1716 mouseEvent->GetButton(&button);
1717 if (button == 0)
1718 HandleNavigationEvent(mouseEvent, PR_FALSE);
1719 else
1720 HandleNavigationEvent(mouseEvent, PR_TRUE);
1721 return NS_OK;
1724 NS_IMETHODIMP mozInlineSpellChecker::MouseDown(nsIDOMEvent* aMouseEvent)
1726 return NS_OK;
1729 NS_IMETHODIMP mozInlineSpellChecker::MouseUp(nsIDOMEvent* aMouseEvent)
1731 return NS_OK;
1734 NS_IMETHODIMP mozInlineSpellChecker::MouseDblClick(nsIDOMEvent* aMouseEvent)
1736 return NS_OK;
1739 NS_IMETHODIMP mozInlineSpellChecker::MouseOver(nsIDOMEvent* aMouseEvent)
1741 return NS_OK;
1744 NS_IMETHODIMP mozInlineSpellChecker::MouseOut(nsIDOMEvent* aMouseEvent)
1746 return NS_OK;
1749 NS_IMETHODIMP mozInlineSpellChecker::KeyDown(nsIDOMEvent* aKeyEvent)
1751 return NS_OK;
1755 NS_IMETHODIMP mozInlineSpellChecker::KeyUp(nsIDOMEvent* aKeyEvent)
1757 return NS_OK;
1761 NS_IMETHODIMP mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
1763 nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent);
1764 NS_ENSURE_TRUE(keyEvent, NS_OK);
1766 PRUint32 keyCode;
1767 keyEvent->GetKeyCode(&keyCode);
1769 // we only care about navigation keys that moved selection
1770 switch (keyCode)
1772 case nsIDOMKeyEvent::DOM_VK_RIGHT:
1773 case nsIDOMKeyEvent::DOM_VK_LEFT:
1774 HandleNavigationEvent(aKeyEvent, PR_FALSE, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1);
1775 break;
1776 case nsIDOMKeyEvent::DOM_VK_UP:
1777 case nsIDOMKeyEvent::DOM_VK_DOWN:
1778 case nsIDOMKeyEvent::DOM_VK_HOME:
1779 case nsIDOMKeyEvent::DOM_VK_END:
1780 case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
1781 case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
1782 HandleNavigationEvent(aKeyEvent, PR_TRUE /* force a spelling correction */);
1783 break;
1786 return NS_OK;