From 25a04b74ebff9300da731d9665c91951ed97fff8 Mon Sep 17 00:00:00 2001 From: "brettw%gmail.com" Date: Fri, 4 Aug 2006 18:27:21 +0000 Subject: [PATCH] Bug 344560 and 345112 r=bryner a=schrep Make the spellchecker asynchronos to avoid DOM notification flush when called from frame code git-svn-id: svn://10.0.0.236/branches/MOZILLA_1_8_BRANCH@206604 18797224-902f-48f8-a5cc-f745e15eee43 --- .../spellcheck/src/mozInlineSpellChecker.cpp | 1169 ++++++++++++----- .../spellcheck/src/mozInlineSpellChecker.h | 130 +- .../spellcheck/src/mozInlineSpellWordUtil.cpp | 11 - .../spellcheck/src/mozInlineSpellWordUtil.h | 4 - 4 files changed, 925 insertions(+), 389 deletions(-) diff --git a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp index ebd78859a32..be4a5880214 100644 --- a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp +++ b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -36,55 +36,513 @@ * * ***** END LICENSE BLOCK ***** */ -// TODO FIXME: We should remember whether we checked the last text entered, so that -// we don't have to re-check every single word that the caret moves over. +/** + * This class is called by the editor to handle spellchecking after various + * events. The main entrypoint is SpellCheckAfterEditorChange, which is called + * when the text is changed. + * + * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM + * notifications to be flushed when we are called from the editor. This is + * because the call might originate from a frame, and flushing the + * notifications might cause that frame to be deleted. + * + * Using the WordUtil class to find words causes DOM notifications to be + * flushed because it asks for style information. As a result, we post an event + * and do all of the spellchecking in that event handler, which occurs later. + * We store all DOM pointers in ranges because they are kept up-to-date with + * DOM changes that may have happened while the event was on the queue. + * + * We also allow the spellcheck to be suspended and resumed later. This makes + * large pastes or initializations with a lot of text not hang the browser UI. + * + * An optimization is the mNeedsCheckAfterNavigation flag. This is set to + * true when we get any change, and false once there is no possibility + * something changed that we need to check on navigation. Navigation events + * tend to be a little tricky because we want to check the current word on + * exit if something has changed. If we navigate inside the word, we don't want + * to do anything. As a result, this flag is cleared in FinishNavigationEvent + * when we know that we are checking as a result of navigation. + */ -#include "nsCOMPtr.h" - -#include "nsString.h" -#include "nsArray.h" -#include "nsIServiceManager.h" -#include "nsIEnumerator.h" -#include "nsUnicharUtils.h" -#include "nsReadableUtils.h" - -#include "mozISpellI18NManager.h" #include "mozInlineSpellChecker.h" - -#include "nsIPlaintextEditor.h" +#include "mozInlineSpellWordUtil.h" +#include "mozISpellI18NManager.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsCRT.h" +#include "nsIDocument.h" #include "nsIDOMDocument.h" #include "nsIDOMDocumentRange.h" -#include "nsIDOMNode.h" -#include "nsIDOMNSUIEvent.h" #include "nsIDOMElement.h" -#include "nsIDOMText.h" +#include "nsIDOMEventReceiver.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMNode.h" #include "nsIDOMNodeList.h" +#include "nsIDOMNSRange.h" +#include "nsIDOMRange.h" +#include "nsIDOMText.h" +#include "nsIEventQueueService.h" +#include "nsIPlaintextEditor.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" #include "nsISelection.h" #include "nsISelection2.h" #include "nsISelectionController.h" -#include "nsITextServicesDocument.h" +#include "nsIServiceManager.h" #include "nsITextServicesFilter.h" -#include "nsIDOMRange.h" -#include "nsIDOMNSRange.h" -#include "nsIDOMCharacterData.h" -#include "nsIDOMDocumentTraversal.h" -#include "nsIDOMNodeFilter.h" -#include "nsIDOMEventReceiver.h" -#include "nsIContent.h" -#include "nsIContentIterator.h" -#include "nsCRT.h" -#include "cattable.h" -#include "nsIPrefService.h" -#include "nsIPrefBranch.h" - -#include "mozInlineSpellWordUtil.h" +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "plevent.h" //#define DEBUG_INLINESPELL -#include "nsIDocument.h" +// the number of milliseconds that we will take at once to do spellchecking +#define INLINESPELL_CHECK_TIMEOUT 50 + +// The number of words to check before we look at the time to see if +// INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from spending +// too much time checking the clock. Note that misspelled words count for +// more than one word in this calculation. +#define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50 + +// This number is the number of checked words a misspelled word counts for +// when we're checking the time to see if the alloted time is up for +// spellchecking. Misspelled words take longer to process since we have to +// create a range, so they count more. The exact number isn't very important +// since this just controls how often we check the current time. +#define MISSPELLED_WORD_COUNT_PENALTY 4 + +static PRBool ContentIsDescendantOf(nsIContent* aPossibleDescendant, + nsIContent* aPossibleAncestor); static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings"; +mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker) + : mSpellChecker(aSpellChecker), mWordCount(0) +{ +} + +// mozInlineSpellStatus::InitForEditorChange +// +// This is the most complicated case. For changes, we need to compute the +// range of stuff that changed based on the old and new caret positions, +// as well as use a range possibly provided by the editor (start and end, +// which are usually NULL) to get a range with the union of these. + +nsresult +mozInlineSpellStatus::InitForEditorChange( + PRInt32 aAction, + nsIDOMNode* aAnchorNode, PRInt32 aAnchorOffset, + nsIDOMNode* aPreviousNode, PRInt32 aPreviousOffset, + nsIDOMNode* aStartNode, PRInt32 aStartOffset, + nsIDOMNode* aEndNode, PRInt32 aEndOffset) +{ + nsresult rv; + + nsCOMPtr docRange; + rv = GetDocumentRange(getter_AddRefs(docRange)); + NS_ENSURE_SUCCESS(rv, rv); + + // save the anchor point as a range so we can find the current word later + rv = PositionToCollapsedRange(docRange, aAnchorNode, aAnchorOffset, + getter_AddRefs(mAnchorRange)); + NS_ENSURE_SUCCESS(rv, rv); + + if (aAction == mozInlineSpellChecker::kOpDeleteSelection) { + // Deletes are easy, the range is just the current anchor. We set the range + // to check to be empty, FinishInitOnEvent will fill in the range to be + // the current word. + mOp = eOpChangeDelete; + mRange = nsnull; + return NS_OK; + } + + mOp = eOpChange; + + // range to check + rv = docRange->CreateRange(getter_AddRefs(mRange)); + NS_ENSURE_SUCCESS(rv, rv); + + // ...we need to put the start and end in the correct order + nsCOMPtr nsrange = do_QueryInterface(mAnchorRange, &rv); + NS_ENSURE_SUCCESS(rv, rv); + PRInt16 cmpResult; + rv = nsrange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult); + NS_ENSURE_SUCCESS(rv, rv); + if (cmpResult < 0) { + // previous anchor node is before the current anchor + rv = mRange->SetStart(aPreviousNode, aPreviousOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mRange->SetEnd(aAnchorNode, aAnchorOffset); + } else { + // previous anchor node is after (or the same as) the current anchor + rv = mRange->SetStart(aAnchorNode, aAnchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mRange->SetEnd(aPreviousNode, aPreviousOffset); + } + NS_ENSURE_SUCCESS(rv, rv); + + // On insert save this range: DoSpellCheck optimizes things in this range. + // Otherwise, just leave this NULL. + if (aAction == mozInlineSpellChecker::kOpInsertText) + mCreatedRange = mRange; + + // if we were given a range, we need to expand our range to encompass it + if (aStartNode && aEndNode) { + nsrange = do_QueryInterface(mRange, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = nsrange->ComparePoint(aStartNode, aStartOffset, &cmpResult); + NS_ENSURE_SUCCESS(rv, rv); + if (cmpResult < 0) { // given range starts before + rv = mRange->SetStart(aStartNode, aStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = nsrange->ComparePoint(aStartNode, aStartOffset, &cmpResult); + NS_ENSURE_SUCCESS(rv, rv); + if (cmpResult > 0) { // given range ends after + rv = mRange->SetEnd(aEndNode, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +// mozInlineSpellStatis::InitForNavigation +// +// For navigation events, we just need to store the new and old positions. +// +// In some cases, we detect that we shouldn't check. If this event should +// not be processed, *aContinue will be false. + +nsresult +mozInlineSpellStatus::InitForNavigation( + PRBool aForceCheck, PRInt32 aNewPositionOffset, + nsIDOMNode* aOldAnchorNode, PRInt32 aOldAnchorOffset, + nsIDOMNode* aNewAnchorNode, PRInt32 aNewAnchorOffset, + PRBool* aContinue) +{ + nsresult rv; + mOp = eOpNavigation; + + mForceNavigationWordCheck = aForceCheck; + mNewNavigationPositionOffset = aNewPositionOffset; + + // get the root node for checking + nsCOMPtr editor = do_QueryReferent(mSpellChecker->mEditor, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr rootElt; + rv = editor->GetRootElement(getter_AddRefs(rootElt)); + NS_ENSURE_SUCCESS(rv, rv); + + // the anchor node might not be in the DOM anymore, check + nsCOMPtr root = do_QueryInterface(rootElt, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr currentAnchor = do_QueryInterface(aOldAnchorNode, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) { + *aContinue = PR_FALSE; + return NS_OK; + } + + nsCOMPtr docRange; + rv = GetDocumentRange(getter_AddRefs(docRange)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PositionToCollapsedRange(docRange, aOldAnchorNode, aOldAnchorOffset, + getter_AddRefs(mOldNavigationAnchorRange)); + NS_ENSURE_SUCCESS(rv, rv); + rv = PositionToCollapsedRange(docRange, aNewAnchorNode, aNewAnchorOffset, + getter_AddRefs(mAnchorRange)); + NS_ENSURE_SUCCESS(rv, rv); + + *aContinue = PR_TRUE; + return NS_OK; +} + +// mozInlineSpellStatus::InitForSelection +// +// It is easy for selections since we always re-check the spellcheck +// selection. + +nsresult +mozInlineSpellStatus::InitForSelection() +{ + mOp = eOpSelection; + return NS_OK; +} + +// mozInlineSpellStatus::InitForRange +// +// Called to cause the spellcheck of the given range. This will look like +// a change operation over the given range. + +nsresult +mozInlineSpellStatus::InitForRange(nsIDOMRange* aRange) +{ + mOp = eOpChange; + mRange = aRange; + return NS_OK; +} + +// mozInlineSpellStatus::FinishInitOnEvent +// +// Called when the event is triggered to complete initialization that +// might require the WordUtil. This calls to the operation-specific +// initializer, and also sets the range to be the entire element if it +// is NULL. +// +// Watch out: the range might still be NULL if there is nothing to do, +// the caller will have to check for this. + +nsresult +mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil) +{ + nsresult rv; + if (! mRange) { + rv = mSpellChecker->MakeSpellCheckRange(nsnull, 0, nsnull, 0, + getter_AddRefs(mRange)); + NS_ENSURE_SUCCESS(rv, rv); + } + + switch (mOp) { + case eOpChange: + if (mAnchorRange) + return FillNoCheckRangeFromAnchor(aWordUtil); + break; + case eOpChangeDelete: + if (mAnchorRange) + return FillNoCheckRangeFromAnchor(aWordUtil); + // Delete events will have no range for the changed text (because it was + // deleted), and InitForEditorChange will set it to NULL. Here, we select + // the entire word to cause any underlining to be removed. + mRange = mNoCheckRange; + break; + case eOpNavigation: + return FinishNavigationEvent(aWordUtil); + case eOpSelection: + // this gets special handling in ResumeCheck + break; + case eOpResume: + // everything should be initialized already in this case + break; + default: + NS_NOTREACHED("Bad operation"); + return NS_ERROR_NOT_INITIALIZED; + } + return NS_OK; +} + +// mozInlineSpellStatus::FinishNavigationEvent +// +// This verifies that we need to check the word at the previous caret +// position. Now that we have the word util, we can find the word belonging +// to the previous caret position. If the new position is inside that word, +// we don't want to do anything. In this case, we'll NULL out mRange so +// that the caller will know not to continue. +// +// Notice that we don't set mNoCheckRange. We check here whether the cursor +// is in the word that needs checking, so it isn't necessary. Plus, the +// spellchecker isn't guaranteed to only check the given word, and it could +// remove the underline from the new word under the cursor. + +nsresult +mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil) +{ + NS_ASSERTION(mAnchorRange, "No anchor for navigation!"); + nsCOMPtr newAnchorNode, oldAnchorNode; + PRInt32 newAnchorOffset, oldAnchorOffset; + + // get the DOM position of the old caret, the range should be collapsed + nsresult rv = mOldNavigationAnchorRange->GetStartContainer( + getter_AddRefs(oldAnchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // find the word on the old caret position, this is the one that we MAY need + // to check + nsCOMPtr oldWord; + rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset, + getter_AddRefs(oldWord)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr oldWordNS = do_QueryInterface(oldWord, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // get the DOM position of the new caret, the range should be collapsed + rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mAnchorRange->GetStartOffset(&newAnchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + // see if the new cursor position is in the word of the old cursor position + PRBool isInRange = PR_FALSE; + if (! mForceNavigationWordCheck) { + rv = oldWordNS->IsPointInRange(newAnchorNode, + newAnchorOffset + mNewNavigationPositionOffset, + &isInRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (isInRange) { + // caller should give up + mRange = nsnull; + } else { + // check the old word + mRange = oldWord; + + // Once we've spellchecked the current word, we don't need to spellcheck + // for any more navigation events. + mSpellChecker->mNeedsCheckAfterNavigation = PR_FALSE; + } + return NS_OK; +} + +// mozInlineSpellStatus::FillNoCheckRangeFromAnchor +// +// Given the mAnchorRange object, computes the range of the word it is on +// (if any) and fills that range into mNoCheckRange. This is used for +// change and navigation events to know which word we should skip spell +// checking on + +nsresult +mozInlineSpellStatus::FillNoCheckRangeFromAnchor( + mozInlineSpellWordUtil& aWordUtil) +{ + nsCOMPtr anchorNode; + nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode)); + NS_ENSURE_SUCCESS(rv, rv); + + PRInt32 anchorOffset; + rv = mAnchorRange->GetStartOffset(&anchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + + return aWordUtil.GetRangeForWord(anchorNode, anchorOffset, + getter_AddRefs(mNoCheckRange)); +} + +// mozInlineSpellStatus::GetDocumentRange +// +// Returns the nsIDOMDocumentRange object for the document for the +// current spellchecker. + +nsresult +mozInlineSpellStatus::GetDocumentRange(nsIDOMDocumentRange** aDocRange) +{ + nsresult rv; + *aDocRange = nsnull; + if (! mSpellChecker->mEditor) + return NS_ERROR_UNEXPECTED; + + nsCOMPtr editor = do_QueryReferent(mSpellChecker->mEditor, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr domDoc; + rv = editor->GetDocument(getter_AddRefs(domDoc)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr docRange = do_QueryInterface(domDoc, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + docRange.swap(*aDocRange); + return NS_OK; +} + +// mozInlineSpellStatus::PositionToCollapsedRange +// +// Converts a given DOM position to a collapsed range covering that +// position. We use ranges to store DOM positions becuase they stay +// updated as the DOM is changed. + +nsresult +mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocumentRange* aDocRange, + nsIDOMNode* aNode, PRInt32 aOffset, nsIDOMRange** aRange) +{ + *aRange = nsnull; + nsCOMPtr range; + nsresult rv = aDocRange->CreateRange(getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = range->SetStart(aNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = range->SetEnd(aNode, aOffset); + NS_ENSURE_SUCCESS(rv, rv); + + range.swap(*aRange); + return NS_OK; +} + +// mozInlineSpellResume + +class mozInlineSpellResume : public PLEvent +{ +public: + mozInlineSpellResume(const mozInlineSpellStatus& status); + mozInlineSpellStatus mStatus; + nsresult Post(nsCOMPtr* aEventQueueService); +}; + +PR_STATIC_CALLBACK(void*) +HandleSpellCheckResumePLEvent(PLEvent* aEvent) +{ + mozInlineSpellResume* resume = NS_REINTERPRET_CAST(mozInlineSpellResume*, + aEvent); + resume->mStatus.mSpellChecker->ResumeCheck(&resume->mStatus); + return nsnull; +} + +PR_STATIC_CALLBACK(void) +DestroySpellCheckResumePLEvent(PLEvent* aEvent) +{ + mozInlineSpellResume* resume = NS_REINTERPRET_CAST(mozInlineSpellResume*, + aEvent); + delete resume; +} + +mozInlineSpellResume::mozInlineSpellResume(const mozInlineSpellStatus& aStatus) + : mStatus(aStatus) +{ + PL_InitEvent(this, aStatus.mSpellChecker, HandleSpellCheckResumePLEvent, + DestroySpellCheckResumePLEvent); +} + +// mozInlineSpellResume::Post +// +// Post this event to the given UI thread event queue. It takes a pointer +// to a COM pointer. The COM pointer will be filled automatically if its +// contents are NULL. + +nsresult +mozInlineSpellResume::Post(nsCOMPtr* aEventQueueService) +{ + nsresult rv; + + // get the event queue, creating the service if necessary + if (! *aEventQueueService) { + *aEventQueueService = do_GetService(NS_EVENTQUEUESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCOMPtr eventQueue; + (*aEventQueueService)-> + GetSpecialEventQueue(nsIEventQueueService::UI_THREAD_EVENT_QUEUE, + getter_AddRefs(eventQueue)); + if (!eventQueue) { + return NS_ERROR_FAILURE; + } + + // post + rv = eventQueue->PostEvent(this); + if (NS_FAILED(rv)) { + PL_DestroyEvent(this); + return rv; + } + + return NS_OK; +} + + NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker) NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker) NS_INTERFACE_MAP_ENTRY(nsIEditActionListener) @@ -102,11 +560,15 @@ mozInlineSpellChecker::SpellCheckingState mozInlineSpellChecker::gCanEnableSpellChecking = mozInlineSpellChecker::SpellCheck_Uninitialized; -mozInlineSpellChecker::mozInlineSpellChecker():mNumWordsInSpellSelection(0),mMaxNumWordsInSpellSelection(250) +mozInlineSpellChecker::mozInlineSpellChecker() : + mNumWordsInSpellSelection(0), + mMaxNumWordsInSpellSelection(250), + mNeedsCheckAfterNavigation(PR_FALSE) { nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (prefs) prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); + mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4; } mozInlineSpellChecker::~mozInlineSpellChecker() @@ -290,6 +752,13 @@ mozInlineSpellChecker::SetEnableRealTimeSpell(PRBool aEnabled) } // mozInlineSpellChecker::SpellCheckAfterEditorChange +// +// Called by the editor when nearly anything happens to change the content. +// +// The start and end positions specify a range for the thing that happened, +// but these are usually NULL, even when you'd think they would be useful +// because you want the range (for example, pasting). We ignore them in +// this case. NS_IMETHODIMP mozInlineSpellChecker::SpellCheckAfterEditorChange( @@ -303,6 +772,10 @@ mozInlineSpellChecker::SpellCheckAfterEditorChange( if (!mSpellCheck) return NS_OK; // disabling spell checking is not an error + // this means something has changed, and we never check the current word, + // therefore, we should spellcheck for subsequent caret navigations + mNeedsCheckAfterNavigation = PR_TRUE; + // the anchor node is the position of the caret nsCOMPtr anchorNode; rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode)); @@ -311,83 +784,17 @@ mozInlineSpellChecker::SpellCheckAfterEditorChange( rv = aSelection->GetAnchorOffset(&anchorOffset); NS_ENSURE_SUCCESS(rv, rv); - // the spell check selection includes all misspelled words - nsCOMPtr spellCheckSelection; - rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + mozInlineSpellStatus status(this); + rv = status.InitForEditorChange(aAction, + anchorNode, anchorOffset, + aPreviousSelectedNode, aPreviousSelectedOffset, + aStartNode, aStartOffset, + aEndNode, aEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = ScheduleSpellCheck(status); NS_ENSURE_SUCCESS(rv, rv); - CleanupRangesInSelection(spellCheckSelection); - mozInlineSpellWordUtil wordUtil; - rv = wordUtil.Init(mEditor); - if (NS_FAILED(rv)) - return NS_OK; // editor doesn't like us - - nsCOMPtr wordRange; - rv = wordUtil.GetRangeForWord(anchorNode, anchorOffset, - getter_AddRefs(wordRange)); - NS_ENSURE_SUCCESS(rv, rv); -#ifdef DEBUG_INLINESPELL - nsString wordRangeText; - wordRange->ToString(wordRangeText); - printf("->Editor change, current word is \"%s\"\n", - NS_ConvertUTF16toUTF8(wordRangeText).get()); -#endif - - nsCOMPtr rangeToCheck; - if (aAction != kOpDeleteSelection) { - // Construct a range of everything between the current caret position and - // the previous caret position. We need to check these. This may fail - // because text was deleted. - // - // We don't do this for deletions and fall through to seeing the range to - // be the same as the word. This will cause the word to be unhighlighted. - // When we delete something, nothing but the current word changes. - wordUtil.GetDocumentRange()->CreateRange(getter_AddRefs(rangeToCheck)); - rv = rangeToCheck->SetStart(aPreviousSelectedNode, aPreviousSelectedOffset); - NS_ENSURE_SUCCESS(rv, rv); - rv = rangeToCheck->SetEnd(aPreviousSelectedNode, aPreviousSelectedOffset); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr nsRange = do_QueryInterface(rangeToCheck, &rv); - NS_ENSURE_SUCCESS(rv, rv); - PRInt16 cmpResult; - rv = nsRange->ComparePoint(anchorNode, anchorOffset, &cmpResult); - NS_ENSURE_SUCCESS(rv, rv); - if (cmpResult < 0) { - rv = rangeToCheck->SetStart(anchorNode, anchorOffset); - } else { - rv = rangeToCheck->SetEnd(anchorNode, anchorOffset); - } - // fall through with NULL rangeToCheck on failure - } - if (!rangeToCheck) { - // On failure, set the range to check to be the range of the current - // word. This is correct for deletes, and if we have some other error, - // this is probably the best we can do. Since the range is the same - // as the word, any selection will be cleared from the word. - // This may be NULL. - rangeToCheck = wordRange; - } - - // When we are given a range of stuff that changed (usually not the case), - // we should be sure to check all of that stuff, too, so create the union - // of this and our checking range. - if (aStartNode && aEndNode) { - wordUtil.ExpandFor(aStartNode, aStartOffset, aEndNode, aEndOffset); - } - - if (rangeToCheck) { - if (aAction == kOpInsertText) { - rv = DoSpellCheck(wordUtil, rangeToCheck, wordRange, rangeToCheck, - spellCheckSelection); - } else { - rv = DoSpellCheck(wordUtil, rangeToCheck, wordRange, nsnull, - spellCheckSelection); - } - NS_ENSURE_SUCCESS(rv, rv); - } - - // remember the current caret position after every change.. + // remember the current caret position after every change SaveCurrentSelectionPosition(); return NS_OK; } @@ -402,31 +809,10 @@ mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange) { NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); - nsCOMPtr spellCheckSelection; - nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); - NS_ENSURE_SUCCESS(rv, rv); - - CleanupRangesInSelection(spellCheckSelection); - - if(aRange) { - mozInlineSpellWordUtil wordUtil; - rv = wordUtil.Init(mEditor); - if (NS_FAILED(rv)) - return NS_OK; // editor doesn't like us - rv = DoSpellCheck(wordUtil, aRange, nsnull, nsnull, spellCheckSelection); - } else { - // use full range: SpellCheckBetweenNodes will do the somewhat complicated - // task of creating a range over the element we give it and call - // SpellCheckRange(range,selection) for us - nsCOMPtr editor (do_QueryReferent(mEditor)); - if (!editor) - return NS_ERROR_NOT_INITIALIZED; - nsCOMPtr rootElem; - rv = editor->GetRootElement(getter_AddRefs(rootElem)); - NS_ENSURE_SUCCESS(rv, rv); - rv = SpellCheckBetweenNodes(rootElem, 0, rootElem, -1, spellCheckSelection); - } - return rv; + mozInlineSpellStatus status(this); + nsresult rv = status.InitForRange(aRange); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); } // mozInlineSpellChecker::GetMispelledWord @@ -484,14 +870,13 @@ mozInlineSpellChecker::AddWordToDictionary(const nsAString &word) NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); nsAutoString wordstr(word); - nsresult res = mSpellCheck->AddWordToDictionary(wordstr.get()); - NS_ENSURE_SUCCESS(res, res); + nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get()); + NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr spellCheckSelection; - nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + mozInlineSpellStatus status(this); + rv = status.InitForSelection(); NS_ENSURE_SUCCESS(rv, rv); - - return SpellCheckSelection(spellCheckSelection); + return ScheduleSpellCheck(status); } // mozInlineSpellChecker::IgnoreWord @@ -502,13 +887,13 @@ mozInlineSpellChecker::IgnoreWord(const nsAString &word) NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED); nsAutoString wordstr(word); - nsresult res = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get()); - NS_ENSURE_SUCCESS(res, res); - - nsCOMPtr spellCheckSelection; - nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get()); NS_ENSURE_SUCCESS(rv, rv); - return SpellCheckSelection(spellCheckSelection); + + mozInlineSpellStatus status(this); + rv = status.InitForSelection(); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); } // mozInlineSpellChecker::IgnoreWords @@ -521,91 +906,10 @@ mozInlineSpellChecker::IgnoreWords(const PRUnichar **aWordsToIgnore, for (PRUint32 index = 0; index < aCount; index++) mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]); - nsCOMPtr spellCheckSelection; - nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + mozInlineSpellStatus status(this); + nsresult rv = status.InitForSelection(); NS_ENSURE_SUCCESS(rv, rv); - - return SpellCheckSelection(spellCheckSelection); -} - -// mozInlineSpellChecker::SpellCheckSelection -// -// When the user ignores a word or adds a word to the dictionary, we used to -// re check the entire document, starting at the root element and walking -// through all the nodes. -// -// Optimization: The only words in the document that would change as a -// result of these actions are words that are already in the spell check -// selection (i.e. words previsouly marked as misspelled). Therefore callers -// spell check the spell check selection instead of the entire document for -// ignore word and add word. -// -// FIXME TODO Performance: Make sure this works nicely with the empty -// selection optimization of DoSpellCheck - -nsresult -mozInlineSpellChecker::SpellCheckSelection(nsISelection* aSelection) -{ - NS_ENSURE_ARG_POINTER(aSelection); - - nsCOMPtr spellCheckSelection; - nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); - NS_ENSURE_SUCCESS(rv, rv); - - // if we are going to be checking the spell check selection, then - // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges. - if (aSelection == spellCheckSelection.get()) - mNumWordsInSpellSelection = 0; - - // Optimize for the case where aSelection is in fact the spell check selection. - // Since we could be modifying the ranges for the spellCheckSelection while looping - // on the spell check selection, keep a separate array of range elements inside the selection - PRInt32 count; - nsCOMPtr ranges = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); - - aSelection->GetRangeCount(&count); - PRInt32 index; - for (index = 0; index < count; index++) - { - nsCOMPtr checkRange; - aSelection->GetRangeAt(index, getter_AddRefs(checkRange)); - if (checkRange) - ranges->AppendElement(checkRange, PR_FALSE); - } - - // now loop over these ranges and spell check each range - nsCOMPtr startNode; - nsCOMPtr endNode; - nsCOMPtr checkRange; - - // We have saved the ranges above. Clearing the spellcheck selection here - // isn't necessary (rechecking each word will modify it as necessary) but - // provides better performance. By ensuring that no ranges need to be - // removed in DoSpellCheck, we can save checking range inclusion which is - // slow. - spellCheckSelection->RemoveAllRanges(); - - mozInlineSpellWordUtil wordUtil; - rv = wordUtil.Init(mEditor); - if (NS_FAILED(rv)) - return NS_OK; // editor doesn't like us - - for (index = 0; index < count; index++) - { - checkRange = do_QueryElementAt(ranges, index); - if (checkRange) - { - // We can consider this word as "added" since we know it has no spell - // check range over it that needs to be deleted. All the old ranges - // were cleared above. - rv = DoSpellCheck(wordUtil, checkRange, nsnull, checkRange, - spellCheckSelection); - NS_ENSURE_SUCCESS(rv, rv); - } - } - - return NS_OK; + return ScheduleSpellCheck(status); } NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, PRInt32 aPosition) @@ -652,7 +956,7 @@ mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode, PRInt32 aOffset, nsIDOMNode *aNewLeftNode, nsresult aResult) { - return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0, NULL); + return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0); } NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent) @@ -663,7 +967,7 @@ NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOM NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent, nsresult aResult) { - return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0, NULL); + return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0); } NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, const nsAString & aString) @@ -697,50 +1001,60 @@ NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection return NS_OK; } - -// mozInlineSpellChecker::SpellCheckBetweenNodes +// mozInlineSpellChecker::MakeSpellCheckRange // // Given begin and end positions, this function constructs a range as -// required for DoSpellCheck, which then does the actual checking. +// required for ScheduleSpellCheck. If the start and end nodes are NULL, +// then the entire range will be selected, and you can supply -1 as the +// offset to the end range to select all of that node. +// +// If the resulting range would be empty, NULL is put into *aRange and the +// function succeeds. nsresult -mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, - PRInt32 aStartOffset, - nsIDOMNode *aEndNode, - PRInt32 aEndOffset, - nsISelection *aSpellCheckSelection) +mozInlineSpellChecker::MakeSpellCheckRange( + nsIDOMNode* aStartNode, PRInt32 aStartOffset, + nsIDOMNode* aEndNode, PRInt32 aEndOffset, + nsIDOMRange** aRange) { - nsresult res; - nsCOMPtr spellCheckSelection = aSpellCheckSelection; + nsresult rv; + *aRange = nsnull; + nsCOMPtr editor (do_QueryReferent(mEditor)); NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); - if (!spellCheckSelection) - { - res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); - NS_ENSURE_SUCCESS(res, res); - } - nsCOMPtr doc; - res = editor->GetDocument(getter_AddRefs(doc)); - NS_ENSURE_SUCCESS(res, res); + rv = editor->GetDocument(getter_AddRefs(doc)); + NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr docrange = do_QueryInterface(doc); NS_ENSURE_TRUE(docrange, NS_ERROR_FAILURE); nsCOMPtr range; - res = docrange->CreateRange(getter_AddRefs(range)); - NS_ENSURE_SUCCESS(res, res); + rv = docrange->CreateRange(getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); - if (aEndOffset == -1) - { + // possibly use full range of the editor + nsCOMPtr rootElem; + if (! aStartNode || ! aEndNode) { + rv = editor->GetRootElement(getter_AddRefs(rootElem)); + NS_ENSURE_SUCCESS(rv, rv); + + aStartNode = rootElem; + aStartOffset = 0; + + aEndNode = rootElem; + aEndOffset = -1; + } + + if (aEndOffset == -1) { nsCOMPtr childNodes; - res = aEndNode->GetChildNodes(getter_AddRefs(childNodes)); - NS_ENSURE_SUCCESS(res, res); + rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes)); + NS_ENSURE_SUCCESS(rv, rv); PRUint32 childCount; - res = childNodes->GetLength(&childCount); - NS_ENSURE_SUCCESS(res, res); + rv = childNodes->GetLength(&childCount); + NS_ENSURE_SUCCESS(rv, rv); aEndOffset = childCount; } @@ -750,48 +1064,38 @@ mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, if (aStartNode == aEndNode && aStartOffset == aEndOffset) return NS_OK; - range->SetStart(aStartNode,aStartOffset); + rv = range->SetStart(aStartNode, aStartOffset); + NS_ENSURE_SUCCESS(rv, rv); + if (aEndOffset) + rv = range->SetEnd(aEndNode, aEndOffset); + else + rv = range->SetEndAfter(aEndNode); + NS_ENSURE_SUCCESS(rv, rv); - if (aEndOffset) - range->SetEnd(aEndNode, aEndOffset); - else - range->SetEndAfter(aEndNode); - - // HACK: try to avoid an assertion later on in the code when we iterate - // over a range with only one character in it. if the range is really only one - // character, bail out early.. - nsCOMPtr startNode; - nsCOMPtr endNode; - PRInt32 startOffset; - PRInt32 endOffset; - range->GetStartContainer(getter_AddRefs(startNode)); - range->GetStartOffset(&startOffset); - range->GetEndContainer(getter_AddRefs(endNode)); - range->GetEndOffset(&endOffset); - - if (startNode == endNode && startOffset == endOffset) - return NS_OK; // don't call adjust spell highlighting for a single character - - mozInlineSpellWordUtil wordUtil; - res = wordUtil.Init(mEditor); - if (NS_FAILED(res)) - return NS_OK; // editor doesn't like us - return DoSpellCheck(wordUtil, range, nsnull, nsnull, spellCheckSelection); + range.swap(*aRange); + return NS_OK; } -static inline PRBool IsNonwordChar(PRUnichar chr) +nsresult +mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, + PRInt32 aStartOffset, + nsIDOMNode *aEndNode, + PRInt32 aEndOffset) { - // a non-word character is one that can end a word, such as whitespace or - // most punctuation. - // mscott: We probably need to modify this to make it work for non ascii - // based languages... but then again our spell checker doesn't support - // multi byte languages anyway. - // jshin: one way to make the word boundary checker more generic is to - // use 'word breaker(s)' in intl. - // Need to fix callers (of IsNonwordChar) to pass PRUint32 - return ((chr != '\'') && (GetCat(PRUint32(chr)) != 5)); -} + nsCOMPtr range; + nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset, + aEndNode, aEndOffset, + getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); + if (! range) + return NS_OK; // range is empty: nothing to do + + mozInlineSpellStatus status(this); + rv = status.InitForRange(range); + NS_ENSURE_SUCCESS(rv, rv); + return ScheduleSpellCheck(status); +} // mozInlineSpellChecker::SkipSpellCheckForNode // @@ -848,23 +1152,115 @@ mozInlineSpellChecker::SkipSpellCheckForNode(nsIDOMNode *aNode, } +// mozInlineSpellChecker::ScheduleSpellCheck +// +// This is called by code to do the actual spellchecking. We will set up +// the proper structures for calls to DoSpellCheck. + +nsresult +mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus) +{ + mozInlineSpellResume* resume = new mozInlineSpellResume(aStatus); + NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = resume->Post(&mEventQueueService); + if (NS_FAILED(rv)) + delete resume; + return rv; +} + +// mozInlineSpellChecker::DoSpellCheckSelection +// +// Called to re-check all misspelled words. We iterate over all ranges in +// the selection and call DoSpellCheck on them. This is used when a word +// is ignored or added to the dictionary: all instances of that word should +// be removed from the selection. +// +// FIXME-PERFORMANCE: This takes as long as it takes and is not resumable. +// Typically, checking this small amount of text is relatively fast, but +// for large numbers of words, a lag may be noticable. + +nsresult +mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil, + nsISelection* aSpellCheckSelection, + mozInlineSpellStatus* aStatus) +{ + nsresult rv; + + // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges. + mNumWordsInSpellSelection = 0; + + // Since we could be modifying the ranges for the spellCheckSelection while + // looping on the spell check selection, keep a separate array of range + // elements inside the selection + nsCOMArray ranges; + + PRInt32 count; + aSpellCheckSelection->GetRangeCount(&count); + + PRInt32 idx; + nsCOMPtr checkRange; + for (idx = 0; idx < count; idx ++) { + aSpellCheckSelection->GetRangeAt(idx, getter_AddRefs(checkRange)); + if (checkRange) { + if (! ranges.AppendObject(checkRange)) + return NS_ERROR_OUT_OF_MEMORY; + } + } + + // We have saved the ranges above. Clearing the spellcheck selection here + // isn't necessary (rechecking each word will modify it as necessary) but + // provides better performance. By ensuring that no ranges need to be + // removed in DoSpellCheck, we can save checking range inclusion which is + // slow. + aSpellCheckSelection->RemoveAllRanges(); + + // We use this state object for all calls, and just update its range. Note + // that we don't need to call FinishInit since we will be filling in the + // necessary information. + mozInlineSpellStatus status(this); + rv = status.InitForRange(nsnull); + NS_ENSURE_SUCCESS(rv, rv); + + PRBool doneChecking; + for (idx = 0; idx < count; idx ++) { + checkRange = ranges[idx]; + if (checkRange) { + // We can consider this word as "added" since we know it has no spell + // check range over it that needs to be deleted. All the old ranges + // were cleared above. We also need to clear the word count so that we + // check all words instead of stopping early. + status.mRange = checkRange; + rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status, + &doneChecking); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(doneChecking, "We gave the spellchecker one word, but it didn't finish checking?!?!"); + + status.mWordCount = 0; + } + } + + return NS_OK; +} + // mozInlineSpellChecker::DoSpellCheck // // This function checks words intersecting the given range, excluding those -// inside aNoCheckRange (can be NULL). Words inside aNoCheckRange will have -// any spell selection removed (this is used to hide the underlining for the -// word that the caret is in). aNoCheckRange should be on word boundaries. +// inside mStatus->mNoCheckRange (can be NULL). Words inside aNoCheckRange +// will have any spell selection removed (this is used to hide the +// underlining for the word that the caret is in). aNoCheckRange should be +// on word boundaries. // -// aCreatedRange is a possibly NULL range of new text that was inserted. -// Inside this range, we don't bother to check whether things are inside -// the spellcheck selection, which speeds up large paste operations +// mResume->mCreatedRange is a possibly NULL range of new text that was +// inserted. Inside this range, we don't bother to check whether things are +// inside the spellcheck selection, which speeds up large paste operations // considerably. // // Normal case when editing text by typing // h e l l o w o r k d h o w a r e y o u // ^ caret -// [-------] aRange -// [-------] aNoCheckRange +// [-------] mRange +// [-------] mNoCheckRange // -> does nothing (range is the same as the no check range) // // Case when pasting: @@ -873,18 +1269,22 @@ mozInlineSpellChecker::SkipSpellCheckForNode(nsIDOMNode *aNode, // ^ caret // [---] aNoCheckRange // -> recheck all words in range except those in aNoCheckRange +// +// If checking is complete, *aDoneChecking will be set. If there is more +// but we ran out of time, this will be false and the range will be +// updated with the stuff that still needs checking. nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, - nsIDOMRange *aRange, - nsIDOMRange *aNoCheckRange, - nsIDOMRange *aCreatedRange, - nsISelection *aSpellCheckSelection) + nsISelection *aSpellCheckSelection, + mozInlineSpellStatus* aStatus, + PRBool* aDoneChecking) { nsCOMPtr beginNode, endNode; PRInt32 beginOffset, endOffset; + *aDoneChecking = PR_TRUE; PRBool iscollapsed; - nsresult rv = aRange->GetCollapsed(&iscollapsed); + nsresult rv = aStatus->mRange->GetCollapsed(&iscollapsed); NS_ENSURE_SUCCESS(rv, rv); if (iscollapsed) return NS_OK; @@ -901,27 +1301,32 @@ nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, // set the starting DOM position to be the beginning of our range NS_ENSURE_SUCCESS(rv, rv); - aRange->GetStartContainer(getter_AddRefs(beginNode)); - aRange->GetStartOffset(&beginOffset); - aRange->GetEndContainer(getter_AddRefs(endNode)); - aRange->GetEndOffset(&endOffset); + aStatus->mRange->GetStartContainer(getter_AddRefs(beginNode)); + aStatus->mRange->GetStartOffset(&beginOffset); + aStatus->mRange->GetEndContainer(getter_AddRefs(endNode)); + aStatus->mRange->GetEndOffset(&endOffset); aWordUtil.SetEnd(endNode, endOffset); aWordUtil.SetPosition(beginNode, beginOffset); // we need to use IsPointInRange which is on a more specific interface nsCOMPtr noCheckRange, createdRange; - if (aNoCheckRange) - noCheckRange = do_QueryInterface(aNoCheckRange); - if (aCreatedRange) - createdRange = do_QueryInterface(aCreatedRange); + if (aStatus->mNoCheckRange) + noCheckRange = do_QueryInterface(aStatus->mNoCheckRange); + if (aStatus->mCreatedRange) + createdRange = do_QueryInterface(aStatus->mCreatedRange); + + PRInt32 wordsSinceTimeCheck = 0; + PRTime beginTime = PR_Now(); nsAutoString wordText; nsCOMPtr wordRange; PRBool dontCheckWord; while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText, - getter_AddRefs(wordRange), - &dontCheckWord)) && + getter_AddRefs(wordRange), + &dontCheckWord)) && wordRange) { + wordsSinceTimeCheck ++; + // get the range for the current word wordRange->GetStartContainer(getter_AddRefs(beginNode)); wordRange->GetEndContainer(getter_AddRefs(endNode)); @@ -982,13 +1387,77 @@ nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, PRBool isMisspelled; aWordUtil.NormalizeWord(wordText); rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled); - if (isMisspelled) + if (isMisspelled) { + // misspelled words count extra toward the max + wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY; AddRange(aSpellCheckSelection, wordRange); + + aStatus->mWordCount ++; + if (aStatus->mWordCount >= mMaxMisspellingsPerCheck || + SpellCheckSelectionIsFull()) + break; + } + + // see if we've run out of time, only check every N words for perf + if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) { + wordsSinceTimeCheck = 0; + if (PR_Now() > beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC) { + // stop checking, our time limit has been exceeded + + // move the range to encompass the stuff that needs checking + rv = aStatus->mRange->SetStart(endNode, endOffset); + if (NS_FAILED(rv)) { + // The range might be unhappy because the beginning is after the + // end. This is possible when the requested end was in the middle + // of a word, just ignore this situation and assume we're done. + return NS_OK; + } + *aDoneChecking = PR_FALSE; + return NS_OK; + } + } } return NS_OK; } +// mozInlineSpellChecker::ResumeCheck +// +// Called by the resume event when it fires. We will try to pick up where +// the last resume left off. + +nsresult +mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus) +{ + if (! mSpellCheck) + return NS_OK; // spell checking has been turned off + + mozInlineSpellWordUtil wordUtil; + nsresult rv = wordUtil.Init(mEditor); + if (NS_FAILED(rv)) + return NS_OK; // editor doesn't like us + + nsCOMPtr spellCheckSelection; + rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + NS_ENSURE_SUCCESS(rv, rv); + CleanupRangesInSelection(spellCheckSelection); + + rv = aStatus->FinishInitOnEvent(wordUtil); + NS_ENSURE_SUCCESS(rv, rv); + if (! aStatus->mRange) + return NS_OK; // empty range, nothing to do + + PRBool doneChecking = PR_TRUE; + if (aStatus->mOp == mozInlineSpellStatus::eOpSelection) + rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus); + else + rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking); + NS_ENSURE_SUCCESS(rv, rv); + + if (! doneChecking) + rv = ScheduleSpellCheck(*aStatus); + return rv; +} // mozInlineSpellChecker::IsPointInSelection // @@ -1136,7 +1605,7 @@ nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition() // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime // for XPCOM's rap sheet -static PRBool +PRBool // static ContentIsDescendantOf(nsIContent* aPossibleDescendant, nsIContent* aPossibleAncestor) { @@ -1170,9 +1639,15 @@ mozInlineSpellChecker::HandleNavigationEvent(nsIDOMEvent* aEvent, PRBool aForceWordSpellCheck, PRInt32 aNewPositionOffset) { - // get the current selection and compare it to the new selection. nsresult rv; + // If we already handled the navigation event and there is no possibility + // anything has changed since then, we don't have to do anything. This + // optimization makes a noticable different when you hold down a navigation + // key like Page Down. + if (! mNeedsCheckAfterNavigation) + return NS_OK; + nsCOMPtr currentAnchorNode = mCurrentSelectionAnchorNode; PRInt32 currentAnchorOffset = mCurrentSelectionOffset; @@ -1180,48 +1655,18 @@ mozInlineSpellChecker::HandleNavigationEvent(nsIDOMEvent* aEvent, rv = SaveCurrentSelectionPosition(); NS_ENSURE_SUCCESS(rv, rv); - // No current selection (this can happen the first time you focus empty - // windows). Since we just called SaveCurrentSelectionPosition it will be - // initialized for next time. - if (! currentAnchorNode) - return NS_OK; - - mozInlineSpellWordUtil wordUtil; - rv = wordUtil.Init(mEditor); - if (NS_FAILED(rv)) - return NS_OK; // editor doesn't like us - - // mCurrentSelectionAnchorNode might not be in the DOM anymore! check - nsCOMPtr root = do_QueryInterface(wordUtil.GetRootNode()); - nsCOMPtr currentAnchor = do_QueryInterface(currentAnchorNode); - if (root && currentAnchor && !ContentIsDescendantOf(currentAnchor, root)) - return NS_OK; - - // expand the old selection into a range for the nearest word boundary - nsCOMPtr currentWordRange; - rv = wordUtil.GetRangeForWord(currentAnchorNode, currentAnchorOffset, - getter_AddRefs(currentWordRange)); + PRBool shouldPost; + mozInlineSpellStatus status(this); + rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset, + currentAnchorNode, currentAnchorOffset, + mCurrentSelectionAnchorNode, mCurrentSelectionOffset, + &shouldPost); NS_ENSURE_SUCCESS(rv, rv); - if (! currentWordRange) - return NS_OK; - - nsCOMPtr currentWordNSRange = do_QueryInterface(currentWordRange, &rv); - NS_ENSURE_SUCCESS(rv, rv); - - PRBool isInRange; - rv = currentWordNSRange->IsPointInRange(mCurrentSelectionAnchorNode, mCurrentSelectionOffset + aNewPositionOffset, &isInRange); - NS_ENSURE_SUCCESS(rv, rv); - - if (!isInRange || aForceWordSpellCheck) // selection is moving to a new word, spell check the current word - { - nsCOMPtr spellCheckSelection; - GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); - - rv = DoSpellCheck(wordUtil, currentWordRange, nsnull, nsnull, - spellCheckSelection); + if (shouldPost) { + rv = ScheduleSpellCheck(status); NS_ENSURE_SUCCESS(rv, rv); } - + return NS_OK; } @@ -1234,10 +1679,6 @@ NS_IMETHODIMP mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent) { // ignore any errors from HandleNavigationEvent as we don't want to prevent // anyone else from seeing this event. - // - // FIXME-performance: this re-checks the word on all mouse click events, - // even right click or other clicks that do not change the caret position. - // We should notice when this happens and not waste time re-checking. HandleNavigationEvent(aMouseEvent, PR_FALSE); return NS_OK; } diff --git a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h index e9f7e58f656..3e5d25e3420 100644 --- a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h +++ b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h @@ -39,6 +39,8 @@ #ifndef __mozinlinespellchecker_h__ #define __mozinlinespellchecker_h__ +#include "nsAutoPtr.h" +#include "nsIDOMRange.h" #include "nsIEditorSpellCheck.h" #include "nsIEditActionListener.h" #include "nsIInlineSpellChecker.h" @@ -51,13 +53,97 @@ #include "nsWeakReference.h" #include "mozISpellI18NUtil.h" +class nsIDOMDocumentRange; class nsIDOMMouseEventListener; +class nsIEventQueueService; class mozInlineSpellWordUtil; +class mozInlineSpellChecker; +class mozInlineSpellResume; + +class mozInlineSpellStatus +{ +public: + mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker); + + nsresult InitForEditorChange(PRInt32 aAction, + nsIDOMNode* aAnchorNode, PRInt32 aAnchorOffset, + nsIDOMNode* aPreviousNode, PRInt32 aPreviousOffset, + nsIDOMNode* aStartNode, PRInt32 aStartOffset, + nsIDOMNode* aEndNode, PRInt32 aEndOffset); + nsresult InitForNavigation(PRBool aForceCheck, PRInt32 aNewPositionOffset, + nsIDOMNode* aOldAnchorNode, PRInt32 aOldAnchorOffset, + nsIDOMNode* aNewAnchorNode, PRInt32 aNewAnchorOffset, + PRBool* aContinue); + nsresult InitForSelection(); + nsresult InitForRange(nsIDOMRange* aRange); + + nsresult FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil); + + nsRefPtr mSpellChecker; + + // The total number of words checked in this sequence, using this tally tells + // us when to stop. This count is preserved as we continue checking in new + // messages. + PRInt32 mWordCount; + + // what happened? + enum Operation { eOpChange, // for SpellCheckAfterChange except kOpDeleteSelection + eOpChangeDelete, // for SpellCheckAfterChange kOpDeleteSelection + eOpNavigation, // for HandleNavigationEvent + eOpSelection, // re-check all misspelled words + eOpResume }; // for resuming a previously started check + Operation mOp; + + // Used for events where we have already computed the range to use. It can + // also be NULL in these cases where we need to check the entire range. + nsCOMPtr mRange; + + // If we happen to know something was inserted, this is that range. + // Can be NULL (this only allows an optimization, so not setting doesn't hurt) + nsCOMPtr mCreatedRange; + + // Contains the range computed for the current word. Can be NULL. + nsCOMPtr mNoCheckRange; + + // Indicates the position of the cursor for the event (so we can compute + // mNoCheckRange). It can be NULL if we don't care about the cursor position + // (such as for the intial check of everything). + // + // For mOp == eOpNavigation, this is the NEW position of the cursor + nsCOMPtr mAnchorRange; + + // ----- + // The following members are only for navigation events and are only + // stored for FinishNavigationEvent to initialize the other members. + // ----- + + // this is the OLD position of the cursor + nsCOMPtr mOldNavigationAnchorRange; + + // Set when we should force checking the current word. See + // mozInlineSpellChecker::HandleNavigationEvent for a description of why we + // have this. + PRBool mForceNavigationWordCheck; + + // Contains the offset passed in to HandleNavigationEvent + PRInt32 mNewNavigationPositionOffset; + +protected: + nsresult FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil); + + nsresult FillNoCheckRangeFromAnchor(mozInlineSpellWordUtil& aWordUtil); + + nsresult GetDocumentRange(nsIDOMDocumentRange** aDocRange); + nsresult PositionToCollapsedRange(nsIDOMDocumentRange* aDocRange, + nsIDOMNode* aNode, PRInt32 aOffset, + nsIDOMRange** aRange); +}; class mozInlineSpellChecker : public nsIInlineSpellChecker, nsIEditActionListener, nsIDOMMouseListener, nsIDOMKeyListener, nsSupportsWeakReference { private: + friend class mozInlineSpellStatus; // Access with CanEnableInlineSpellChecking enum SpellCheckingState { SpellCheck_Uninitialized = -1, @@ -74,11 +160,25 @@ private: PRInt32 mNumWordsInSpellSelection; PRInt32 mMaxNumWordsInSpellSelection; + // How many misspellings we can add at once. This is often less than the max + // total number of misspellings. When you have a large textarea prepopulated + // with text with many misspellings, we can hit this limit. By making it + // lower than the total number of misspelled words, new text typed by the + // user can also have spellchecking in it. + PRInt32 mMaxMisspellingsPerCheck; + // we need to keep track of the current text position in the document // so we can spell check the old word when the user clicks around the document. nsCOMPtr mCurrentSelectionAnchorNode; PRInt32 mCurrentSelectionOffset; + // Set when we have spellchecked after the last edit operation. See the + // commment at the top of the .cpp file for more info. + PRBool mNeedsCheckAfterNavigation; + + // lazily created, may be NULL + nsCOMPtr mEventQueueService; + // TODO: these should be defined somewhere so that they don't have to be copied // from editor! enum OperationID @@ -147,15 +247,14 @@ public: mozInlineSpellChecker(); virtual ~mozInlineSpellChecker(); - // spell check the selected text specified by aSelection - nsresult SpellCheckSelection(nsISelection *aSelection); + // re-spellcheck the currently marked ranges + nsresult SpellCheckSelection(); // spell checks all of the words between two nodes nsresult SpellCheckBetweenNodes(nsIDOMNode *aStartNode, PRInt32 aStartOffset, nsIDOMNode *aEndNode, - PRInt32 aEndOffset, - nsISelection *aSpellCheckSelection); + PRInt32 aEndOffset); // examines the dom node in question and returns true if the inline spell // checker should skip the node (i.e. the text is inside of a block quote @@ -166,25 +265,34 @@ public: nsIDOMNode* aPreviousNode, PRInt32 aPreviousOffset, nsISelection* aSpellCheckSelection); - // spell check the text contained within aRange + // spell check the text contained within aRange, potentially scheduling + // another check in the future if the time threshold is reached + nsresult ScheduleSpellCheck(const mozInlineSpellStatus& aStatus); + + nsresult DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil, + nsISelection* aSpellCheckSelection, + mozInlineSpellStatus* aStatus); nsresult DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, - nsIDOMRange *aRange, nsIDOMRange* aNoCheckRange, - nsIDOMRange *aCreatedRange, - nsISelection *aSpellCheckSelection); + nsISelection *aSpellCheckSelection, + mozInlineSpellStatus* aStatus, + PRBool* aDoneChecking); // helper routine to determine if a point is inside of a the passed in selection. nsresult IsPointInSelection(nsISelection *aSelection, nsIDOMNode *aNode, PRInt32 aOffset, nsIDOMRange **aRange); - + nsresult CleanupRangesInSelection(nsISelection *aSelection); - // helper routines used to control how many ranges we allow into the spell check selection nsresult RemoveRange(nsISelection *aSpellCheckSelection, nsIDOMRange * aRange); nsresult AddRange(nsISelection *aSpellCheckSelection, nsIDOMRange * aRange); PRBool SpellCheckSelectionIsFull() { return mNumWordsInSpellSelection >= mMaxNumWordsInSpellSelection; } + nsresult MakeSpellCheckRange(nsIDOMNode* aStartNode, PRInt32 aStartOffset, + nsIDOMNode* aEndNode, PRInt32 aEndOffset, + nsIDOMRange** aRange); + // DOM and editor event registration helper routines nsresult RegisterEventListeners(); nsresult UnregisterEventListeners(); @@ -192,6 +300,8 @@ public: nsresult GetSpellCheckSelection(nsISelection ** aSpellCheckSelection); nsresult SaveCurrentSelectionPosition(); + + nsresult ResumeCheck(mozInlineSpellStatus* resume); }; #endif /* __mozinlinespellchecker_h__ */ diff --git a/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp b/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp index fb734497fcc..543453f5381 100644 --- a/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp +++ b/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp @@ -241,17 +241,6 @@ mozInlineSpellWordUtil::SetEnd(nsIDOMNode* aEndNode, PRInt32 aEndOffset) return NS_OK; } - -// mozInlineSpellWordUtil::ExpandFor - -nsresult -mozInlineSpellWordUtil::ExpandFor(nsIDOMNode* aBeginNode, PRInt32 aBeginOffset, - nsIDOMNode* aEndNode, PRInt32 aEndOffset) -{ - // InvalidateWords(); - return NS_ERROR_NOT_IMPLEMENTED; -} - nsresult mozInlineSpellWordUtil::SetPosition(nsIDOMNode* aNode, PRInt32 aOffset) { diff --git a/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.h b/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.h index ec54627f17c..04fd4c43a05 100644 --- a/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.h +++ b/mozilla/extensions/spellcheck/src/mozInlineSpellWordUtil.h @@ -88,10 +88,6 @@ public: nsresult SetEnd(nsIDOMNode* aEndNode, PRInt32 aEndOffset); - // expands the current range to incorporate this new range - nsresult ExpandFor(nsIDOMNode* aBeginNode, PRInt32 aBeginOffset, - nsIDOMNode* aEndNode, PRInt32 aEndOffset); - // sets the current position, this should be inside the range. If we are in // the middle of a word, we'll move to its start. nsresult SetPosition(nsIDOMNode* aNode, PRInt32 aOffset);