From d9f54372987e22511d71c7651439954eef6e13e1 Mon Sep 17 00:00:00 2001 From: "brettw%gmail.com" Date: Wed, 2 Aug 2006 16:17:06 +0000 Subject: [PATCH] Bug 345112 r+sr=bryner Make the spellchecker work incrementally --- .../spellcheck/src/mozInlineSpellChecker.cpp | 248 +++++++++++++----- .../spellcheck/src/mozInlineSpellChecker.h | 52 +++- 2 files changed, 227 insertions(+), 73 deletions(-) diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp index 983e50637c4..4af174cf1d1 100644 --- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp +++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -61,6 +61,7 @@ #include "nsCOMArray.h" #include "nsIDOMText.h" #include "nsIDOMNodeList.h" +#include "nsIRunnable.h" #include "nsISelection.h" #include "nsISelection2.h" #include "nsISelectionController.h" @@ -75,6 +76,7 @@ #include "nsIContent.h" #include "nsIContentIterator.h" #include "nsCRT.h" +#include "nsThreadUtils.h" #include "cattable.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" @@ -83,10 +85,54 @@ //#define DEBUG_INLINESPELL +// 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 + #include "nsIDocument.h" static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings"; +// Event stuff for suspending & resuming checks +mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker, + nsIDOMRange* aRange, + nsIDOMRange* aNoCheckRange, + nsIDOMRange* aCreatedRange) + : mSpellChecker(aSpellChecker), mRange(aRange), mNoCheckRange(aNoCheckRange), + mCreatedRange(aCreatedRange), mWordCount(0) +{ +} + +class mozInlineSpellResume : public nsRunnable +{ +public: + mozInlineSpellResume(const mozInlineSpellStatus& aStatus) : mStatus(aStatus) {} + mozInlineSpellStatus mStatus; + nsresult Post() + { + return NS_DispatchToMainThread(this); + } + + NS_IMETHOD Run() + { + mStatus.mSpellChecker->ResumeCheck(&mStatus); + return NS_OK; + } +}; + + NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker) NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker) NS_INTERFACE_MAP_ENTRY(nsIEditActionListener) @@ -109,6 +155,7 @@ mozInlineSpellChecker::mozInlineSpellChecker():mNumWordsInSpellSelection(0),mMax nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (prefs) prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); + mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4; } mozInlineSpellChecker::~mozInlineSpellChecker() @@ -308,12 +355,6 @@ 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)); - NS_ENSURE_SUCCESS(rv, rv); - CleanupRangesInSelection(spellCheckSelection); - mozInlineSpellWordUtil wordUtil; rv = wordUtil.Init(mEditor); if (NS_FAILED(rv)) @@ -374,13 +415,10 @@ mozInlineSpellChecker::SpellCheckAfterEditorChange( } if (rangeToCheck) { - if (aAction == kOpInsertText) { - rv = DoSpellCheck(wordUtil, rangeToCheck, wordRange, rangeToCheck, - spellCheckSelection); - } else { - rv = DoSpellCheck(wordUtil, rangeToCheck, wordRange, nsnull, - spellCheckSelection); - } + if (aAction == kOpInsertText) + rv = ScheduleSpellCheck(wordUtil, rangeToCheck, wordRange, rangeToCheck); + else + rv = ScheduleSpellCheck(wordUtil, rangeToCheck, wordRange, nsnull); NS_ENSURE_SUCCESS(rv, rv); } @@ -397,22 +435,16 @@ mozInlineSpellChecker::SpellCheckAfterEditorChange( nsresult mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange) { + nsresult rv; 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 - { + rv = ScheduleSpellCheck(wordUtil, aRange, nsnull, nsnull); + } 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 @@ -422,7 +454,7 @@ mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange) nsCOMPtr rootElem; rv = editor->GetRootElement(getter_AddRefs(rootElem)); NS_ENSURE_SUCCESS(rv, rv); - rv = SpellCheckBetweenNodes(rootElem, 0, rootElem, -1, spellCheckSelection); + rv = SpellCheckBetweenNodes(rootElem, 0, rootElem, -1); } return rv; } @@ -598,8 +630,7 @@ mozInlineSpellChecker::SpellCheckSelection(nsISelection* aSelection) // 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); + rv = ScheduleSpellCheck(wordUtil, checkRange, nsnull, checkRange); NS_ENSURE_SUCCESS(rv, rv); } } @@ -651,7 +682,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) @@ -662,7 +693,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) @@ -700,26 +731,18 @@ NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection // mozInlineSpellChecker::SpellCheckBetweenNodes // // Given begin and end positions, this function constructs a range as -// required for DoSpellCheck, which then does the actual checking. +// required for ScheduleSpellCheck, which then does the actual checking. nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, PRInt32 aStartOffset, nsIDOMNode *aEndNode, - PRInt32 aEndOffset, - nsISelection *aSpellCheckSelection) + PRInt32 aEndOffset) { nsresult res; - nsCOMPtr spellCheckSelection = aSpellCheckSelection; 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); @@ -775,7 +798,7 @@ mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, res = wordUtil.Init(mEditor); if (NS_FAILED(res)) return NS_OK; // editor doesn't like us - return DoSpellCheck(wordUtil, range, nsnull, nsnull, spellCheckSelection); + return ScheduleSpellCheck(wordUtil, range, nsnull, nsnull); } static inline PRBool IsNonwordChar(PRUnichar chr) @@ -847,23 +870,59 @@ 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(mozInlineSpellWordUtil& aWordUtil, + mozInlineSpellStatus* aStatus) +{ + nsresult rv; + + // the spell check selection includes all misspelled words + nsCOMPtr spellCheckSelection; + rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + NS_ENSURE_SUCCESS(rv, rv); + CleanupRangesInSelection(spellCheckSelection); + + PRBool doneChecking; + rv = DoSpellCheck(aWordUtil, spellCheckSelection, aStatus, &doneChecking); + NS_ENSURE_SUCCESS(rv, rv); + + if (! doneChecking) { + // schedule an event so we can continue spellchecking in the future + mozInlineSpellResume* resume = new mozInlineSpellResume(*aStatus); + NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY); + + rv = resume->Post(); + if (NS_FAILED(rv)) { + delete resume; + return rv; + } + } + 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: @@ -872,18 +931,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; @@ -900,27 +963,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)); @@ -981,13 +1049,63 @@ 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 + + rv = ScheduleSpellCheck(wordUtil, aStatus); + if (NS_FAILED(rv)) { + // give up, FIXME: we may want to re-check the entire document at this + // point. + } + return NS_OK; +} // mozInlineSpellChecker::IsPointInSelection // @@ -1213,11 +1331,7 @@ mozInlineSpellChecker::HandleNavigationEvent(nsIDOMEvent* aEvent, 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); + rv = ScheduleSpellCheck(wordUtil, currentWordRange, nsnull, nsnull); NS_ENSURE_SUCCESS(rv, rv); } diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.h b/extensions/spellcheck/src/mozInlineSpellChecker.h index e9f7e58f656..9c0c6a84be4 100644 --- a/extensions/spellcheck/src/mozInlineSpellChecker.h +++ b/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" @@ -53,6 +55,26 @@ class nsIDOMMouseEventListener; class mozInlineSpellWordUtil; +class mozInlineSpellChecker; +class mozInlineSpellResume; + +class mozInlineSpellStatus +{ +public: + mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker, + nsIDOMRange* aRange, + nsIDOMRange* aNoCheckRange, + nsIDOMRange* aCreatedRange); + + nsRefPtr mSpellChecker; + + nsCOMPtr mRange; + nsCOMPtr mNoCheckRange; + nsCOMPtr mCreatedRange; + + // total number of words checked in this sequence + PRInt32 mWordCount; +}; class mozInlineSpellChecker : public nsIInlineSpellChecker, nsIEditActionListener, nsIDOMMouseListener, nsIDOMKeyListener, nsSupportsWeakReference @@ -74,6 +96,13 @@ 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; @@ -154,8 +183,7 @@ public: 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,11 +194,21 @@ 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(mozInlineSpellWordUtil& aWordUtil, + mozInlineSpellStatus* aStatus); + nsresult ScheduleSpellCheck(mozInlineSpellWordUtil& aWordUtil, + nsIDOMRange *aRange, nsIDOMRange* aNoCheckRange, + nsIDOMRange *aCreatedRange) { + mozInlineSpellStatus status(this, aRange, aNoCheckRange, aCreatedRange); + return ScheduleSpellCheck(aWordUtil, &status); + } + 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, @@ -192,6 +230,8 @@ public: nsresult GetSpellCheckSelection(nsISelection ** aSpellCheckSelection); nsresult SaveCurrentSelectionPosition(); + + nsresult ResumeCheck(mozInlineSpellStatus* aStatus); }; #endif /* __mozinlinespellchecker_h__ */