From 7f0c1c3f7fea58a77dfef2aae0e52513f6c837cf Mon Sep 17 00:00:00 2001 From: "scott%scott-macgregor.org" Date: Mon, 27 Feb 2006 22:35:35 +0000 Subject: [PATCH] Bug #324521 --> poor inline spell checking performance on large text documents. When adding or ignoring a word, just spell check the existing spell selection instead of the entire document since those are the only words that would possibly be effected. Limit the size of the spell check selection to the first 250 mistyped words. sr=bienvenu git-svn-id: svn://10.0.0.236/trunk@191344 18797224-902f-48f8-a5cc-f745e15eee43 --- .../spellcheck/src/mozInlineSpellChecker.cpp | 200 +++++++++++------- .../spellcheck/src/mozInlineSpellChecker.h | 15 +- .../compose/content/MsgComposeCommands.js | 2 +- mozilla/modules/libpref/src/init/all.js | 4 + 4 files changed, 145 insertions(+), 76 deletions(-) diff --git a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp index ae22eef897e..073bd4e2b42 100644 --- a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp +++ b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -38,6 +38,7 @@ #include "nsCOMPtr.h" #include "nsString.h" +#include "nsArray.h" #include "nsIServiceManager.h" #include "nsIEnumerator.h" #include "nsUnicharUtils.h" @@ -69,6 +70,10 @@ #include "nsIContentIterator.h" #include "nsCRT.h" #include "cattable.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" + +static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings"; NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker) NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker) @@ -87,8 +92,11 @@ mozInlineSpellChecker::SpellCheckingState mozInlineSpellChecker::gCanEnableSpellChecking = mozInlineSpellChecker::SpellCheck_Uninitialized; -mozInlineSpellChecker::mozInlineSpellChecker() +mozInlineSpellChecker::mozInlineSpellChecker():mNumWordsInSpellSelection(0), mMaxNumWordsInSpellSelection(250) { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) + prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection); } mozInlineSpellChecker::~mozInlineSpellChecker() @@ -229,7 +237,7 @@ NS_IMETHODIMP mozInlineSpellChecker::SetEnableRealTimeSpell(PRBool aEnabled) } } - // spellcheck the current contents + // spellcheck the current content res = SpellCheckRange(nsnull); } else @@ -238,6 +246,7 @@ NS_IMETHODIMP mozInlineSpellChecker::SetEnableRealTimeSpell(PRBool aEnabled) res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); NS_ENSURE_SUCCESS(res, res); spellCheckSelection->RemoveAllRanges(); + mNumWordsInSpellSelection = 0; UnregisterEventListeners(); mSpellCheck = nsnull; } @@ -329,7 +338,7 @@ NS_IMETHODIMP mozInlineSpellChecker::SpellCheckAfterEditorChange(PRInt32 action, case kOpRemoveTextProperty: case kOpResetTextProperties: - res = SpellCheckSelection(aSelection, spellCheckSelection); + res = SpellCheckSelection(aSelection); break; case kOpMakeBasicBlock: @@ -351,11 +360,11 @@ NS_IMETHODIMP mozInlineSpellChecker::SpellCheckAfterEditorChange(PRInt32 action, if (iscollapsed) res = AdjustSpellHighlighting(anchorNode, anchorOffset, spellCheckSelection, PR_FALSE); else - res = SpellCheckSelection(aSelection, spellCheckSelection); + res = SpellCheckSelection(aSelection); break; case kOpSetTextProperty: - res = SpellCheckSelection(aSelection,spellCheckSelection); + res = SpellCheckSelection(aSelection); break; case kOpUndo: @@ -393,7 +402,8 @@ NS_IMETHODIMP mozInlineSpellChecker::SpellCheckRange(nsIDOMRange *aRange) if(aRange) { // use the given range rv = SpellCheckRange(aRange,spellCheckSelection); - } else { + } 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 @@ -477,14 +487,11 @@ NS_IMETHODIMP mozInlineSpellChecker::AddWordToDictionary(const nsAString &word) nsresult res = mSpellCheck->AddWordToDictionary(wordstr.get()); NS_ENSURE_SUCCESS(res, res); - nsCOMPtr editor (do_QueryReferent(mEditor)); - NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + nsCOMPtr spellCheckSelection; + nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr rootElem; - res = editor->GetRootElement(getter_AddRefs(rootElem)); - NS_ENSURE_SUCCESS(res, res); - - return SpellCheckBetweenNodes(rootElem, 0, rootElem, -1, NULL); + return SpellCheckSelection(spellCheckSelection); } NS_IMETHODIMP mozInlineSpellChecker::IgnoreWord(const nsAString &word) @@ -495,14 +502,10 @@ NS_IMETHODIMP mozInlineSpellChecker::IgnoreWord(const nsAString &word) nsresult res = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get()); NS_ENSURE_SUCCESS(res, res); - nsCOMPtr editor (do_QueryReferent(mEditor)); - NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); - - nsCOMPtr rootElem; - res = editor->GetRootElement(getter_AddRefs(rootElem)); - NS_ENSURE_SUCCESS(res, res); - - return SpellCheckBetweenNodes(rootElem, 0, rootElem, -1, NULL); + nsCOMPtr spellCheckSelection; + nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + NS_ENSURE_SUCCESS(rv, rv); + return SpellCheckSelection(spellCheckSelection); } NS_IMETHODIMP mozInlineSpellChecker::IgnoreWords(const PRUnichar **aWordsToIgnore, PRUint32 aCount) @@ -511,14 +514,68 @@ NS_IMETHODIMP mozInlineSpellChecker::IgnoreWords(const PRUnichar **aWordsToIgnor for (PRUint32 index = 0; index < aCount; index++) mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]); - nsCOMPtr editor (do_QueryReferent(mEditor)); - NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER); + nsCOMPtr spellCheckSelection; + nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection)); + NS_ENSURE_SUCCESS(rv, rv); + return SpellCheckSelection(spellCheckSelection); +} - nsCOMPtr rootElem; - nsresult res = editor->GetRootElement(getter_AddRefs(rootElem)); - NS_ENSURE_SUCCESS(res, res); +// 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). +// Spell check the spell check selection instead of the entire document for ignore word and add word +nsresult mozInlineSpellChecker::SpellCheckSelection(nsISelection * aSelection) +{ + NS_ENSURE_ARG_POINTER(aSelection); - return SpellCheckBetweenNodes(rootElem, 0, rootElem, -1, NULL); + 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; + PRInt32 startOffset, endOffset; + nsCOMPtr checkRange; + + for (index = 0; index < count; index++) + { + checkRange = do_QueryElementAt(ranges, index); + if (checkRange) + { + checkRange->GetStartContainer(getter_AddRefs(startNode)); + checkRange->GetEndContainer(getter_AddRefs(endNode)); + checkRange->GetStartOffset(&startOffset); + checkRange->GetEndOffset(&endOffset); + + rv |= SpellCheckBetweenNodes(startNode, startOffset, endNode, endOffset, spellCheckSelection); + } + } + + return rv; } NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, PRInt32 aPosition) @@ -608,39 +665,6 @@ NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection return NS_OK; } -nsresult -mozInlineSpellChecker::SpellCheckSelection(nsISelection *aSelection, nsISelection *aSpellCheckSelection) -{ - NS_ENSURE_ARG_POINTER(aSelection); - if (!mSpellCheck) - return NS_ERROR_NOT_INITIALIZED; - - PRInt32 count; - aSelection->GetRangeCount(&count); - - for (PRInt32 index =0; index < count; index++) - { - nsCOMPtr checkRange; - aSelection->GetRangeAt(index, getter_AddRefs(checkRange)); - - if (checkRange) - { - nsCOMPtr startNode; - nsCOMPtr endNode; - PRInt32 startOffset, endOffset; - - checkRange->GetStartContainer(getter_AddRefs(startNode)); - checkRange->GetEndContainer(getter_AddRefs(endNode)); - checkRange->GetStartOffset(&startOffset); - checkRange->GetEndOffset(&endOffset); - - return SpellCheckBetweenNodes(startNode, startOffset, endNode, endOffset, aSpellCheckSelection); - } - } - - return NS_OK; -} - nsresult mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode, PRInt32 aStartOffset, @@ -723,7 +747,7 @@ static inline PRBool IsNonwordChar(PRUnichar chr) // There are certain conditions when we don't want to spell check a node. In particular // quotations, moz signatures, etc. This routine returns false for these cases. -nsresult mozInlineSpellChecker::CheckShouldSpellCheck(nsIDOMNode *aNode, PRBool *checkSpelling) +nsresult mozInlineSpellChecker::SkipSpellCheckForNode(nsIDOMNode *aNode, PRBool *checkSpelling) { *checkSpelling = PR_TRUE; NS_ENSURE_ARG_POINTER(aNode); @@ -923,7 +947,7 @@ mozInlineSpellChecker::AdjustSpellHighlighting(nsIDOMNode *aNode, return NS_OK; PRBool checkSpelling; - CheckShouldSpellCheck(currentNode, &checkSpelling); + SkipSpellCheckForNode(currentNode, &checkSpelling); if (!checkSpelling) return NS_OK; @@ -948,7 +972,7 @@ mozInlineSpellChecker::AdjustSpellHighlighting(nsIDOMNode *aNode, NS_ENSURE_SUCCESS(rv, rv); if (isMisspelled) - aSpellCheckSelection->AddRange(wordRange); + AddRange(aSpellCheckSelection, wordRange); return NS_OK; } @@ -976,7 +1000,7 @@ nsresult mozInlineSpellChecker::SpellCheckRange(nsIDOMRange *aRange, nsISelectio nsCOMPtr startNode; nsCOMPtr endNode; - while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) + while (!SpellCheckSelectionIsFull() && NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { nsAutoString textblock; res = mTextServicesDocument->GetCurrentTextBlock(&textblock); @@ -1002,7 +1026,7 @@ nsresult mozInlineSpellChecker::SpellCheckRange(nsIDOMRange *aRange, nsISelectio wordrange->GetEndOffset(&endOffset); PRBool checkSpelling; - CheckShouldSpellCheck(startNode, &checkSpelling); + SkipSpellCheckForNode(startNode, &checkSpelling); if (!checkSpelling) break; @@ -1013,12 +1037,12 @@ nsresult mozInlineSpellChecker::SpellCheckRange(nsIDOMRange *aRange, nsISelectio IsPointInSelection(aSpellCheckSelection, endNode, endOffset - 1, getter_AddRefs(currentRange)); if (currentRange) // remove the old range first - aSpellCheckSelection->RemoveRange(currentRange); + RemoveRange(aSpellCheckSelection, currentRange); if (isMisspelled) - aSpellCheckSelection->AddRange(wordrange); + AddRange(aSpellCheckSelection, wordrange); } selOffset = end; - } while (end != -1); + } while (end != -1 && !SpellCheckSelectionIsFull()); mTextServicesDocument->NextBlock(); selOffset = 0; @@ -1107,7 +1131,7 @@ mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection) checkRange->GetCollapsed(&collapsed); if (collapsed) { - aSelection->RemoveRange(checkRange); + RemoveRange(aSelection, checkRange); index--; count--; } @@ -1117,6 +1141,40 @@ mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection) return NS_OK; } +// For performance reasons, we have an upper bound on the number of word ranges +// in the spell check selection. When removing a range from the selection, we need to decrement +// mNumWordsInSpellSelection +nsresult mozInlineSpellChecker::RemoveRange(nsISelection *aSpellCheckSelection, nsIDOMRange * aRange) +{ + NS_ENSURE_ARG_POINTER(aSpellCheckSelection); + NS_ENSURE_ARG_POINTER(aRange); + + nsresult rv = aSpellCheckSelection->RemoveRange(aRange); + if (NS_SUCCEEDED(rv) && mNumWordsInSpellSelection) + mNumWordsInSpellSelection--; + + return rv; +} + +// For performance reasons, we have an upper bound on the number of word ranges we'll add +// to the spell check selection. Once we reach that upper bound, stop adding the ranges +nsresult mozInlineSpellChecker::AddRange(nsISelection *aSpellCheckSelection, nsIDOMRange * aRange) +{ + NS_ENSURE_ARG_POINTER(aSpellCheckSelection); + NS_ENSURE_ARG_POINTER(aRange); + + nsresult rv = NS_OK; + + if (!SpellCheckSelectionIsFull()) + { + rv = aSpellCheckSelection->AddRange(aRange); + if (NS_SUCCEEDED(rv)) + mNumWordsInSpellSelection++; + } + + return rv; +} + // a helper routine that expands the current position in the selection to the surrounding word // and then removes it from the spell checker selection nsresult mozInlineSpellChecker::RemoveCurrentWordFromSpellSelection(nsISelection *aSpellCheckSelection, nsIDOMRange * aWordRange) @@ -1137,11 +1195,11 @@ nsresult mozInlineSpellChecker::RemoveCurrentWordFromSpellSelection(nsISelection IsPointInSelection(aSpellCheckSelection, startNode, startOffset, getter_AddRefs(currentRange)); if (currentRange) - aSpellCheckSelection->RemoveRange(currentRange); + RemoveRange(aSpellCheckSelection, currentRange); IsPointInSelection(aSpellCheckSelection, endNode, endOffset - 1, getter_AddRefs(currentRange)); if (currentRange) - aSpellCheckSelection->RemoveRange(currentRange); + RemoveRange(aSpellCheckSelection, currentRange); return NS_OK; } diff --git a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h index f981922b3d6..fcc26147137 100644 --- a/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h +++ b/mozilla/extensions/spellcheck/src/mozInlineSpellChecker.h @@ -69,6 +69,9 @@ private: nsCOMPtr mTreeWalker; nsCOMPtr mConverter; + PRInt32 mNumWordsInSpellSelection; + PRInt32 mMaxNumWordsInSpellSelection; + // 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; @@ -143,8 +146,7 @@ public: virtual ~mozInlineSpellChecker(); // spell check the selected text specified by aSelection - nsresult SpellCheckSelection(nsISelection *aSelection, - nsISelection *aSpellCheckSelection); + nsresult SpellCheckSelection(nsISelection *aSelection); // spell checks all of the words between two nodes nsresult SpellCheckBetweenNodes(nsIDOMNode *aStartNode, @@ -152,10 +154,11 @@ public: nsIDOMNode *aEndNode, PRInt32 aEndOffset, nsISelection *aSpellCheckSelection); + // 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 // or an e-mail signature...) - nsresult CheckShouldSpellCheck(nsIDOMNode *aNode, PRBool * aCheckSpelling); + nsresult SkipSpellCheckForNode(nsIDOMNode *aNode, PRBool * aCheckSpelling); nsresult AdjustSpellHighlighting(nsIDOMNode *aNode, PRInt32 aOffset, @@ -174,12 +177,16 @@ public: nsresult CleanupRangesInSelection(nsISelection *aSelection); nsresult RemoveCurrentWordFromSpellSelection(nsISelection *aSpellCheckSelection, nsIDOMRange * aWordRange); + // 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; } + // DOM and editor event registration helper routines nsresult RegisterEventListeners(); nsresult UnregisterEventListeners(); nsresult HandleNavigationEvent(nsIDOMEvent * aEvent, PRBool aForceWordSpellCheck, PRInt32 aNewPositionOffset = 0); - // helper routine which expands a point in a text node out to the range for the containing word. nsresult GenerateRangeForSurroundingWord(nsIDOMNode * aNode, PRInt32 aOffset, nsIDOMRange ** aWordRange); diff --git a/mozilla/mail/components/compose/content/MsgComposeCommands.js b/mozilla/mail/components/compose/content/MsgComposeCommands.js index 9ffbd179d9a..8d865420cb5 100644 --- a/mozilla/mail/components/compose/content/MsgComposeCommands.js +++ b/mozilla/mail/components/compose/content/MsgComposeCommands.js @@ -247,6 +247,7 @@ var gComposeRecyclingListener = { } //Reset editor + InlineSpellChecker.Init(GetCurrentEditor(), false); // unregister inline spell checking listeners and release the spell checker EditorResetFontAndColorAttributes(); EditorCleanup(); @@ -3551,5 +3552,4 @@ function InitEditor() var editor = GetCurrentEditor(); gMsgCompose.initEditor(editor, window.content); InlineSpellChecker.Init(editor, sPrefs.getBoolPref("mail.spellcheck.inline")); - InlineSpellChecker.checkDocument(window.content.document); } diff --git a/mozilla/modules/libpref/src/init/all.js b/mozilla/modules/libpref/src/init/all.js index 935e8aae4fe..4c8341ad494 100644 --- a/mozilla/modules/libpref/src/init/all.js +++ b/mozilla/modules/libpref/src/init/all.js @@ -225,6 +225,10 @@ pref("print.print_edge_left", 0); // 1/100 of an inch pref("print.print_edge_right", 0); // 1/100 of an inch pref("print.print_edge_bottom", 0); // 1/100 of an inch +// Pref used by the spellchecker extension to control the +// maximum number of misspelled words that will be underlined +// in a document. +pref("extensions.spellcheck.inline.max-misspellings", 250); // Prefs used by libeditor. Prefs specific to seamonkey composer // belong in mozilla/editor/ui/composer.js