diff --git a/src/AppleSpellChecker.cpp b/src/AppleSpellChecker.cpp index 71f7e1e25b..f763471f5f 100644 --- a/src/AppleSpellChecker.cpp +++ b/src/AppleSpellChecker.cpp @@ -88,12 +88,19 @@ SpellChecker::Result AppleSpellChecker::check(WordLangTuple const & word) } +void AppleSpellChecker::advanceChangeNumber() +{ + nextChangeNumber(); +} + + // add to personal dictionary void AppleSpellChecker::insert(WordLangTuple const & word) { string const word_str = to_utf8(word.word()); AppleSpeller_learn(d->speller, word_str.c_str()); LYXERR(Debug::GUI, "learn word: \"" << word.word() << "\"") ; + advanceChangeNumber(); } @@ -103,6 +110,7 @@ void AppleSpellChecker::remove(WordLangTuple const & word) string const word_str = to_utf8(word.word()); AppleSpeller_unlearn(d->speller, word_str.c_str()); LYXERR(Debug::GUI, "unlearn word: \"" << word.word() << "\"") ; + advanceChangeNumber(); } @@ -111,6 +119,7 @@ void AppleSpellChecker::accept(WordLangTuple const & word) { string const word_str = to_utf8(word.word()); AppleSpeller_ignore(d->speller, word_str.c_str()); + advanceChangeNumber(); } diff --git a/src/AppleSpellChecker.h b/src/AppleSpellChecker.h index 9d1aeeee3c..25c205aafc 100644 --- a/src/AppleSpellChecker.h +++ b/src/AppleSpellChecker.h @@ -34,6 +34,7 @@ public: int numMisspelledWords() const; void misspelledWord(int index, int & start, int & length) const; docstring const error(); + void advanceChangeNumber(); //@} private: diff --git a/src/AspellChecker.cpp b/src/AspellChecker.cpp index 06cc6bf49a..0b277923d6 100644 --- a/src/AspellChecker.cpp +++ b/src/AspellChecker.cpp @@ -280,6 +280,12 @@ SpellChecker::Result AspellChecker::check(WordLangTuple const & word) } +void AspellChecker::advanceChangeNumber() +{ + nextChangeNumber(); +} + + void AspellChecker::insert(WordLangTuple const & word) { Spellers::iterator it = d->spellers_.find( @@ -287,6 +293,7 @@ void AspellChecker::insert(WordLangTuple const & word) if (it != d->spellers_.end()) { AspellSpeller * speller = to_aspell_speller(it->second.e_speller); aspell_speller_add_to_personal(speller, to_utf8(word.word()).c_str(), -1); + advanceChangeNumber(); } } @@ -298,6 +305,7 @@ void AspellChecker::accept(WordLangTuple const & word) if (it != d->spellers_.end()) { AspellSpeller * speller = to_aspell_speller(it->second.e_speller); aspell_speller_add_to_session(speller, to_utf8(word.word()).c_str(), -1); + advanceChangeNumber(); } } diff --git a/src/AspellChecker.h b/src/AspellChecker.h index 6bef6a854a..0450ea6ed2 100644 --- a/src/AspellChecker.h +++ b/src/AspellChecker.h @@ -32,6 +32,7 @@ public: void accept(WordLangTuple const &); bool hasDictionary(Language const * lang) const; docstring const error(); + void advanceChangeNumber(); //@} private: diff --git a/src/Buffer.cpp b/src/Buffer.cpp index 3f36d6f3ed..a068ecfe3a 100644 --- a/src/Buffer.cpp +++ b/src/Buffer.cpp @@ -4033,6 +4033,7 @@ int Buffer::spellCheck(DocIterator & from, DocIterator & to, if (from == end) break; to = from; + from.paragraph().spellCheck(); SpellChecker::Result res = from.paragraph().spellCheck(from.pos(), to.pos(), wl, suggestions); if (SpellChecker::misspelled(res)) { word_lang = wl; diff --git a/src/Cursor.cpp b/src/Cursor.cpp index e20b42ef7c..e4a34b158c 100644 --- a/src/Cursor.cpp +++ b/src/Cursor.cpp @@ -2265,9 +2265,7 @@ void Cursor::setCurrentFont() // get font BufferParams const & bufparams = buffer()->params(); current_font = par.getFontSettings(bufparams, cpos); - current_font.setMisspelled(false); real_current_font = tm.displayFont(cpit, cpos); - real_current_font.setMisspelled(false); // special case for paragraph end if (cs.pos() == lastpos() diff --git a/src/EnchantChecker.cpp b/src/EnchantChecker.cpp index 4e9494ad2a..8cc37812b4 100644 --- a/src/EnchantChecker.cpp +++ b/src/EnchantChecker.cpp @@ -124,19 +124,29 @@ SpellChecker::Result EnchantChecker::check(WordLangTuple const & word) } +void EnchantChecker::advanceChangeNumber() +{ + nextChangeNumber(); +} + + void EnchantChecker::insert(WordLangTuple const & word) { Spellers::iterator it = d->spellers_.find(word.lang()->code()); - if (it != d->spellers_.end()) + if (it != d->spellers_.end()) { it->second.speller->add(to_utf8(word.word())); + advanceChangeNumber(); + } } void EnchantChecker::accept(WordLangTuple const & word) { Spellers::iterator it = d->spellers_.find(word.lang()->code()); - if (it != d->spellers_.end()) + if (it != d->spellers_.end()) { it->second.speller->add_to_session(to_utf8(word.word())); + advanceChangeNumber(); + } } diff --git a/src/EnchantChecker.h b/src/EnchantChecker.h index 5016ed3d7b..e44f2a5457 100644 --- a/src/EnchantChecker.h +++ b/src/EnchantChecker.h @@ -38,6 +38,7 @@ public: void accept(WordLangTuple const &); bool hasDictionary(Language const * lang) const; docstring const error(); + void advanceChangeNumber(); ///@} private: diff --git a/src/Font.cpp b/src/Font.cpp index bd9541f0d1..cf8ea61785 100644 --- a/src/Font.cpp +++ b/src/Font.cpp @@ -95,7 +95,7 @@ char const * LaTeXSizeNames[14] = Font::Font(FontInfo bits, Language const * l) - : bits_(bits), lang_(l), misspelled_(false), open_encoding_(false) + : bits_(bits), lang_(l), open_encoding_(false) { if (!lang_) lang_ = default_language; diff --git a/src/Font.h b/src/Font.h index 7c0b61134f..2cdd5c1f2e 100644 --- a/src/Font.h +++ b/src/Font.h @@ -42,10 +42,6 @@ public: /// Language const * language() const { return lang_; } /// - void setMisspelled(bool misspelled) { misspelled_ = misspelled; } - /// - bool isMisspelled() const { return misspelled_; } - /// bool isRightToLeft() const; /// bool isVisibleRightToLeft() const; @@ -116,9 +112,6 @@ private: FontInfo bits_; /// Language const * lang_; - /// - bool misspelled_; - /// Did latexWriteStartChanges open an encoding environment? mutable bool open_encoding_; }; @@ -128,8 +121,7 @@ private: inline bool operator==(Font const & font1, Font const & font2) { - return font1.bits_ == font2.bits_ && font1.lang_ == font2.lang_ - && font1.misspelled_ == font2.misspelled_; + return font1.bits_ == font2.bits_ && font1.lang_ == font2.lang_; } /// diff --git a/src/FontList.cpp b/src/FontList.cpp index d4cb66e46e..afa7894ce9 100644 --- a/src/FontList.cpp +++ b/src/FontList.cpp @@ -181,20 +181,6 @@ void FontList::set(pos_type pos, Font const & font) } -void FontList::setMisspelled(pos_type startpos, pos_type endpos, - bool misspelled) -{ - // FIXME: move misspelled state out of font!? - for (pos_type p = startpos; p <= endpos; ++p) { - Font f = fontIterator(p)->font(); - if (f.isMisspelled() != misspelled) { - f.setMisspelled(misspelled); - set(p, f); - } - } -} - - FontSize FontList::highestInRange(pos_type startpos, pos_type endpos, FontSize def_size) const { diff --git a/src/FontList.h b/src/FontList.h index 388a04ee21..61ab2205f2 100644 --- a/src/FontList.h +++ b/src/FontList.h @@ -105,12 +105,6 @@ public: /// void decreasePosAfterPos(pos_type pos); - /// - void setMisspelled( - pos_type startpos, - pos_type endpos, - bool misspelled); - /// Returns the height of the highest font in range FontSize highestInRange( pos_type startpos, diff --git a/src/HunspellChecker.cpp b/src/HunspellChecker.cpp index f3b8c25edb..83bd586dcf 100644 --- a/src/HunspellChecker.cpp +++ b/src/HunspellChecker.cpp @@ -233,6 +233,12 @@ SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl) } +void HunspellChecker::advanceChangeNumber() +{ + nextChangeNumber(); +} + + void HunspellChecker::insert(WordLangTuple const & wl) { string const word_to_check = to_utf8(wl.word()); @@ -240,12 +246,14 @@ void HunspellChecker::insert(WordLangTuple const & wl) if (!h) return; h->add(word_to_check.c_str()); + advanceChangeNumber(); } void HunspellChecker::accept(WordLangTuple const & wl) { d->ignored_.push_back(wl); + advanceChangeNumber(); } diff --git a/src/HunspellChecker.h b/src/HunspellChecker.h index f5daaf3c43..27b634446d 100644 --- a/src/HunspellChecker.h +++ b/src/HunspellChecker.h @@ -32,6 +32,7 @@ public: void accept(WordLangTuple const &); bool hasDictionary(Language const * lang) const; docstring const error(); + void advanceChangeNumber(); ///@} private: diff --git a/src/LyX.cpp b/src/LyX.cpp index 298fe09fd8..03f2ba9ed3 100644 --- a/src/LyX.cpp +++ b/src/LyX.cpp @@ -1354,39 +1354,48 @@ SpellChecker * theSpellChecker() void setSpellChecker() { -#if defined(USE_MACOSX_PACKAGING) + SpellChecker::ChangeNumber speller_change_number =singleton_->pimpl_->spell_checker_ ? + singleton_->pimpl_->spell_checker_->changeNumber() : 0; + if (lyxrc.spellchecker == "native") { +#if defined(USE_MACOSX_PACKAGING) if (!singleton_->pimpl_->apple_spell_checker_) singleton_->pimpl_->apple_spell_checker_ = new AppleSpellChecker(); singleton_->pimpl_->spell_checker_ = singleton_->pimpl_->apple_spell_checker_; - return; - } +#else + singleton_->pimpl_->spell_checker_ = 0; #endif + } else if (lyxrc.spellchecker == "aspell") { #if defined(USE_ASPELL) - if (lyxrc.spellchecker == "aspell") { if (!singleton_->pimpl_->aspell_checker_) singleton_->pimpl_->aspell_checker_ = new AspellChecker(); singleton_->pimpl_->spell_checker_ = singleton_->pimpl_->aspell_checker_; - return; - } +#else + singleton_->pimpl_->spell_checker_ = 0; #endif + } else if (lyxrc.spellchecker == "enchant") { #if defined(USE_ENCHANT) - if (lyxrc.spellchecker == "enchant") { if (!singleton_->pimpl_->enchant_checker_) singleton_->pimpl_->enchant_checker_ = new EnchantChecker(); singleton_->pimpl_->spell_checker_ = singleton_->pimpl_->enchant_checker_; - return; - } +#else + singleton_->pimpl_->spell_checker_ = 0; #endif + } else if (lyxrc.spellchecker == "hunspell") { #if defined(USE_HUNSPELL) - if (lyxrc.spellchecker == "hunspell") { if (!singleton_->pimpl_->hunspell_checker_) singleton_->pimpl_->hunspell_checker_ = new HunspellChecker(); singleton_->pimpl_->spell_checker_ = singleton_->pimpl_->hunspell_checker_; - return; - } +#else + singleton_->pimpl_->spell_checker_ = 0; #endif - singleton_->pimpl_->spell_checker_ = 0; + } else { + singleton_->pimpl_->spell_checker_ = 0; + } + if (singleton_->pimpl_->spell_checker_) { + singleton_->pimpl_->spell_checker_->changeNumber(speller_change_number); + singleton_->pimpl_->spell_checker_->advanceChangeNumber(); + } } } // namespace lyx diff --git a/src/Paragraph.cpp b/src/Paragraph.cpp index 0481d8a5cb..05d9dbc170 100644 --- a/src/Paragraph.cpp +++ b/src/Paragraph.cpp @@ -73,6 +73,140 @@ namespace { char_type const META_INSET = 0x200001; }; + +///////////////////////////////////////////////////////////////////// +// +// SpellCheckerState +// +///////////////////////////////////////////////////////////////////// + +class SpellCheckerState { +public: + SpellCheckerState() { + needs_refresh_ = true; + current_change_number_ = 0; + } + + void setRange(FontSpan const fp, SpellChecker::Result state) + { + eraseCoveredRanges(fp); + if (state != SpellChecker::WORD_OK) + ranges_[fp] = state; + } + + void increasePosAfterPos(pos_type pos) + { + correctRangesAfterPos(pos, 1); + needsRefresh(pos); + } + + void decreasePosAfterPos(pos_type pos) + { + correctRangesAfterPos(pos, -1); + needsRefresh(pos); + } + + SpellChecker::Result getState(pos_type pos) const + { + SpellChecker::Result result = SpellChecker::WORD_OK; + RangesIterator et = ranges_.end(); + RangesIterator it = ranges_.begin(); + for (; it != et; ++it) { + FontSpan fc = it->first; + if(fc.first <= pos && pos <= fc.last) { + result = it->second; + break; + } + } + return result; + } + + bool needsRefresh() const { + return needs_refresh_; + } + + SpellChecker::ChangeNumber currentChangeNumber() const { + return current_change_number_; + } + + void refreshRange(pos_type & first, pos_type & last) const { + first = refresh_.first; + last = refresh_.last; + } + + void needsRefresh(pos_type pos) { + if (needs_refresh_ && pos != -1) { + if (pos < refresh_.first) + refresh_.first = pos; + if (pos > refresh_.last) + refresh_.last = pos; + } else if (pos != -1) { + refresh_.first = pos; + refresh_.last = pos; + } + needs_refresh_ = pos != -1; + } + + void needsCompleteRefresh(SpellChecker::ChangeNumber change_number) { + needs_refresh_ = true; + refresh_.first = 0; + refresh_.last = -1; + current_change_number_ = change_number; + } + +private: + /// store the ranges as map of FontSpan and spell result pairs + typedef map Ranges; + typedef Ranges::const_iterator RangesIterator; + Ranges ranges_; + /// + FontSpan refresh_; + bool needs_refresh_; + SpellChecker::ChangeNumber current_change_number_; + + void eraseCoveredRanges(FontSpan const fp) + { + Ranges result; + RangesIterator et = ranges_.end(); + RangesIterator it = ranges_.begin(); + for (; it != et; ++it) { + FontSpan fc = it->first; + // 1. first of new range inside current range or + // 2. last of new range inside current range or + // 3. first of current range inside new range or + // 4. last of current range inside new range or + if ((fc.first <= fp.first && fp.first <= fc.last) || + (fc.first <= fp.last && fp.last <= fc.last) || + (fp.first <= fc.first && fc.first <= fp.last) || + (fp.first <= fc.last && fc.last <= fp.last)) + { + continue; + } + result[fc] = it->second; + } + ranges_ = result; + } + + void correctRangesAfterPos(pos_type pos, int offset) + { + Ranges result; + RangesIterator et = ranges_.end(); + RangesIterator it = ranges_.begin(); + for (; it != et; ++it) { + FontSpan m = it->first; + if (m.first > pos) { + m.first += offset; + m.last += offset; + } else if (m.last > pos) { + m.last += offset; + } + result[m] = it->second; + } + ranges_ = result; + } + +}; + ///////////////////////////////////////////////////////////////////// // // Paragraph::Private @@ -173,18 +307,69 @@ public: /// match a string against a particular point in the paragraph bool isTextAt(string const & str, pos_type pos) const; - void setMisspelled(pos_type from, pos_type to, bool misspelled) + /// a vector of inset positions + typedef vector Positions; + typedef Positions::const_iterator PositionsIterator; + + Language * getSpellLanguage(pos_type const from) const; + + Language * locateSpellRange(pos_type & from, pos_type & to, + Positions & softbreaks) const; + + bool hasSpellerChange() const { + SpellChecker::ChangeNumber speller_change_number = 0; + if (theSpellChecker()) + speller_change_number = theSpellChecker()->changeNumber(); + return speller_change_number > speller_state_.currentChangeNumber(); + } + + void setMisspelled(pos_type from, pos_type to, SpellChecker::Result state) { pos_type textsize = owner_->size(); // check for sane arguments - if (to < from || from >= textsize) return; + if (to < from || from >= textsize) + return; + FontSpan fp = FontSpan(from, to); // don't mark end of paragraph - if (to >= textsize) - to = textsize - 1; - fontlist_.setMisspelled(from, to, misspelled); + if (fp.last >= textsize) + fp.last = textsize - 1; + speller_state_.setRange(fp, state); + } + + void requestSpellCheck(pos_type pos) { + speller_state_.needsRefresh(pos); } - + void readySpellCheck() { + speller_state_.needsRefresh(-1); + } + + bool needsSpellCheck() const + { + return speller_state_.needsRefresh(); + } + + void rangeOfSpellCheck(pos_type & first, pos_type & last) const + { + speller_state_.refreshRange(first, last); + if (last == -1) { + last = owner_->size(); + return; + } + pos_type endpos = last; + owner_->locateWord(first, endpos, WHOLE_WORD); + if (endpos < last) { + endpos = last; + owner_->locateWord(last, endpos, WHOLE_WORD); + } + last = endpos; + } + + void markMisspelledWords(pos_type const & first, pos_type const & last, + SpellChecker::Result result, + docstring const & word, + Positions const & softbreaks); + InsetCode ownerCode() const { return inset_owner_ ? inset_owner_->lyxCode() : NO_CODE; @@ -224,6 +409,8 @@ public: LangWordsMap words_; /// Layout const * layout_; + /// + SpellCheckerState speller_state_; }; @@ -265,9 +452,10 @@ Paragraph::Private::Private(Private const & p, Paragraph * owner) : owner_(owner), inset_owner_(p.inset_owner_), fontlist_(p.fontlist_), params_(p.params_), changes_(p.changes_), insetlist_(p.insetlist_), begin_of_body_(p.begin_of_body_), text_(p.text_), words_(p.words_), - layout_(p.layout_) + layout_(p.layout_), speller_state_(p.speller_state_) { id_ = ++paragraph_id; + requestSpellCheck(p.text_.size()); } @@ -277,7 +465,7 @@ Paragraph::Private::Private(Private const & p, Paragraph * owner, params_(p.params_), changes_(p.changes_), insetlist_(p.insetlist_, beg, end), begin_of_body_(p.begin_of_body_), words_(p.words_), - layout_(p.layout_) + layout_(p.layout_), speller_state_(p.speller_state_) { id_ = ++paragraph_id; if (beg >= pos_type(p.text_.size())) @@ -297,6 +485,7 @@ Paragraph::Private::Private(Private const & p, Paragraph * owner, // Add a new entry in the fontlist_. fontlist_.set(fcit->pos() - beg, fcit->font()); } + requestSpellCheck(p.text_.size()); } @@ -464,6 +653,8 @@ void Paragraph::Private::insertChar(pos_type pos, char_type c, if (pos == pos_type(text_.size())) { // when appending characters, no need to update tables text_.push_back(c); + // but we want spell checking + requestSpellCheck(pos); return; } @@ -474,6 +665,9 @@ void Paragraph::Private::insertChar(pos_type pos, char_type c, // Update the insets insetlist_.increasePosAfterPos(pos); + + // Update list of misspelled positions + speller_state_.increasePosAfterPos(pos); } @@ -493,6 +687,9 @@ bool Paragraph::insertInset(pos_type pos, Inset * inset, // Add a new entry in the insetlist_. d->insetlist_.insert(inset, pos); + + // Some insets require run of spell checker + requestSpellCheck(pos); return true; } @@ -542,6 +739,9 @@ bool Paragraph::eraseChar(pos_type pos, bool trackChanges) // Update the insetlist_ d->insetlist_.decreasePosAfterPos(pos); + // Update list of misspelled positions + d->speller_state_.decreasePosAfterPos(pos); + return true; } @@ -1249,9 +1449,8 @@ void Paragraph::write(ostream & os, BufferParams const & bparams, if (i == size()) break; - // Write font changes (ignore spelling markers) + // Write font changes Font font2 = getFontSettings(bparams, i); - font2.setMisspelled(false); if (font2 != font1) { flushString(os, write_buffer); font2.lyxWriteChanges(font1, os); @@ -2605,6 +2804,7 @@ void Paragraph::changeLanguage(BufferParams const & bparams, setFont(i, font); } } + d->requestSpellCheck(size()); } @@ -3172,61 +3372,223 @@ void Paragraph::updateWords() } -SpellChecker::Result Paragraph::spellCheck(pos_type & from, pos_type & to, WordLangTuple & wl, - docstring_list & suggestions, bool do_suggestion) const +Language * Paragraph::Private::locateSpellRange( + pos_type & from, pos_type & to, + Positions & softbreaks) const { - SpellChecker * speller = theSpellChecker(); - if (!speller) - return SpellChecker::WORD_OK; + // skip leading white space + while (from < to && owner_->isWordSeparator(from)) + ++from; + // don't check empty range + if (from >= to) + return 0; + // get current language + Language * lang = getSpellLanguage(from); + pos_type last = from; + bool samelang = true; + bool sameinset = true; + while (last < to && samelang && sameinset) { + // hop to end of word + while (last < to && !owner_->isWordSeparator(last)) { + if (owner_->getInset(last)) { + softbreaks.insert(softbreaks.end(), last); + } + ++last; + } + // hop to next word while checking for insets + while (sameinset && last < to && owner_->isWordSeparator(last)) { + if (Inset const * inset = owner_->getInset(last)) + sameinset = inset->isChar() && inset->isLetter(); + if (sameinset) + last++; + } + if (sameinset && last < to) { + // now check for language change + samelang = lang == getSpellLanguage(last); + } + } + // if language change detected backstep is needed + if (!samelang) + --last; + to = last; + return lang; +} - if (!d->layout_->spellcheck || !inInset().allowSpellCheck()) - return SpellChecker::WORD_OK; - locateWord(from, to, WHOLE_WORD); - if (from == to || from >= pos_type(d->text_.size())) - return SpellChecker::WORD_OK; - - docstring word = asString(from, to, AS_STR_INSETS); - // Ignore words with digits - // FIXME: make this customizable - // (note that hunspell ignores words with digits by default) - bool const ignored = hasDigit(word); - Language * lang = const_cast(getFontSettings( - d->inset_owner_->buffer().params(), from).language()); - if (lang == d->inset_owner_->buffer().params().language - && !lyxrc.spellchecker_alt_lang.empty()) { +Language * Paragraph::Private::getSpellLanguage(pos_type const from) const +{ + Language * lang = + const_cast(owner_->getFontSettings( + inset_owner_->buffer().params(), from).language()); + if (lang == inset_owner_->buffer().params().language + && !lyxrc.spellchecker_alt_lang.empty()) { string lang_code; string const lang_variety = split(lyxrc.spellchecker_alt_lang, lang_code, '-'); lang->setCode(lang_code); lang->setVariety(lang_variety); } - wl = WordLangTuple(word, lang); - SpellChecker::Result res = ignored ? - SpellChecker::WORD_OK : speller->check(wl); - - bool const misspelled_ = SpellChecker::misspelled(res) ; - - if (lyxrc.spellcheck_continuously) - d->setMisspelled(from, to, misspelled_); - - if (misspelled_ && do_suggestion) - speller->suggest(wl, suggestions); - else - suggestions.clear(); - - return res; + return lang; } +void Paragraph::requestSpellCheck(pos_type pos) +{ + d->requestSpellCheck(pos == -1 ? size() : pos); +} + + +bool Paragraph::needsSpellCheck() const +{ + SpellChecker::ChangeNumber speller_change_number = 0; + if (theSpellChecker()) + speller_change_number = theSpellChecker()->changeNumber(); + if (speller_change_number > d->speller_state_.currentChangeNumber()) { + d->speller_state_.needsCompleteRefresh(speller_change_number); + } + return d->needsSpellCheck(); +} + + +SpellChecker::Result Paragraph::spellCheck(pos_type & from, pos_type & to, + WordLangTuple & wl, docstring_list & suggestions, + bool do_suggestion, bool check_learned) const +{ + SpellChecker::Result result = SpellChecker::WORD_OK; + SpellChecker * speller = theSpellChecker(); + if (!speller) + return result; + + if (!d->layout_->spellcheck || !inInset().allowSpellCheck()) + return result; + + locateWord(from, to, WHOLE_WORD); + if (from == to || from >= pos_type(d->text_.size())) + return result; + + docstring word = asString(from, to, AS_STR_INSETS); + Language * lang = d->getSpellLanguage(from); + + wl = WordLangTuple(word, lang); + + if (!word.size()) + return result; + + if (needsSpellCheck() || check_learned) { + // Ignore words with digits + // FIXME: make this customizable + // (note that some checkers ignore words with digits by default) + if (!hasDigit(word)) + result = speller->check(wl); + d->setMisspelled(from, to, result); + } else { + result = d->speller_state_.getState(from); + } + + bool const misspelled_ = SpellChecker::misspelled(result) ; + if (misspelled_ && do_suggestion) + speller->suggest(wl, suggestions); + else if (misspelled_) + LYXERR(Debug::GUI, "misspelled word: \"" << + word << "\" [" << + from << ".." << to << "]"); + else + suggestions.clear(); + + return result; +} + + +void Paragraph::Private::markMisspelledWords( + pos_type const & first, pos_type const & last, + SpellChecker::Result result, + docstring const & word, + Positions const & softbreaks) +{ + pos_type snext = first; + if (SpellChecker::misspelled(result)) { + SpellChecker * speller = theSpellChecker(); + // locate and enumerate the error positions + int nerrors = speller->numMisspelledWords(); + int numbreaks = 0; + PositionsIterator it = softbreaks.begin(); + PositionsIterator et = softbreaks.end(); + for (int index = 0; index < nerrors; ++index) { + int wstart; + int wlen = 0; + speller->misspelledWord(index, wstart, wlen); + if (wlen) { + docstring const misspelled = word.substr(wstart, wlen); + wstart += first + numbreaks; + if (snext < wstart) { + while (it != et && *it < wstart) { + ++wstart; + ++numbreaks; + ++it; + } + setMisspelled(snext, + wstart - 1, SpellChecker::WORD_OK); + } + snext = wstart + wlen; + while (it != et && *it < snext) { + ++snext; + ++numbreaks; + ++it; + } + setMisspelled(wstart, snext, result); + LYXERR(Debug::GUI, "misspelled word: \"" << + misspelled << "\" [" << + wstart << ".." << (snext-1) << "]"); + ++snext; + } + } + } + if (snext <= last) { + setMisspelled(snext, last, SpellChecker::WORD_OK); + } +} + + +void Paragraph::spellCheck() const +{ + SpellChecker * speller = theSpellChecker(); + if (!speller || !size() ||!needsSpellCheck()) + return; + pos_type start; + pos_type endpos; + d->rangeOfSpellCheck(start, endpos); + if (speller->canCheckParagraph()) { + // loop until we leave the range argument + for (pos_type first = start; first < endpos; ) { + pos_type last = endpos; + Private::Positions softbreaks; + Language * lang = d->locateSpellRange(first, last, softbreaks); + if (first >= endpos) + break; + // start the spell checker on the unit of meaning + docstring word = asString(first, last, AS_STR_INSETS); + WordLangTuple wl = WordLangTuple(word, lang); + SpellChecker::Result result = word.size() ? + speller->check(wl) : SpellChecker::WORD_OK; + d->markMisspelledWords(first, last, result, word, softbreaks); + first = ++last; + } + } else { + static docstring_list suggestions; + pos_type to = endpos; + while (start < endpos) { + WordLangTuple wl; + spellCheck(start, to, wl, suggestions, false); + start = to + 1; + } + } + d->readySpellCheck(); +} + + bool Paragraph::isMisspelled(pos_type pos) const { - pos_type from = pos; - pos_type to = pos; - WordLangTuple wl; - docstring_list suggestions; - SpellChecker::Result res = spellCheck(from, to, wl, suggestions, false); - return SpellChecker::misspelled(res) ; + return SpellChecker::misspelled(d->speller_state_.getState(pos)); } diff --git a/src/Paragraph.h b/src/Paragraph.h index 26389c6ced..041c33e70c 100644 --- a/src/Paragraph.h +++ b/src/Paragraph.h @@ -66,6 +66,18 @@ public: public: /// Range including first and last. pos_type first, last; + + inline bool operator<(FontSpan const & s) const + { + return first < s.first; + } + + inline bool operator==(FontSpan const & s) const + { + return first == s.first && last == s.last; + } + + }; /// @@ -147,7 +159,7 @@ public: /// \param force means: output even if layout.inpreamble is true. void latex(BufferParams const &, Font const & outerfont, odocstream &, TexRow & texrow, OutputParams const &, - int start_pos = 0, int end_pos = -1, bool force = false) const; + int start_pos = 0, int end_pos = -1, bool force = false) const; /// Can we drop the standard paragraph wrapper? bool emptyTag() const; @@ -425,11 +437,23 @@ public: /// and \p suggestions if \p do_suggestion is true. /// \return result from spell checker, SpellChecker::UNKNOWN_WORD when misspelled. SpellChecker::Result spellCheck(pos_type & from, pos_type & to, WordLangTuple & wl, - docstring_list & suggestions, bool do_suggestion = true) const; + docstring_list & suggestions, bool do_suggestion = true, + bool check_learned = false) const; - /// Spellcheck word at position \p pos. - /// \return true if pointed word is misspelled. + /// Spell checker status at position \p pos. + /// \return true if pointed position is misspelled. bool isMisspelled(pos_type pos) const; + + /// spell check of whole paragraph + /// remember results until call of requestSpellCheck() + void spellCheck() const; + + /// query state of spell checker results + bool needsSpellCheck() const; + /// mark position of text manipulation to inform the spell checker + /// default value -1 marks the whole paragraph to be checked (again) + void requestSpellCheck(pos_type pos = -1); + /// an automatically generated identifying label for this paragraph. /// presently used only in the XHTML output routines. std::string magicLabel() const; diff --git a/src/SpellChecker.h b/src/SpellChecker.h index 04b657cbd2..62125860fe 100644 --- a/src/SpellChecker.h +++ b/src/SpellChecker.h @@ -14,6 +14,7 @@ #define SPELL_BASE_H #include "support/strfwd.h" +#include "support/lyxtime.h" namespace lyx { @@ -89,6 +90,16 @@ public: /// give an error message on messy exit virtual docstring const error() = 0; + + /// spell checker state versioning support + typedef unsigned long int ChangeNumber ; + ChangeNumber changeNumber() const { return change_number_; } + void changeNumber(ChangeNumber value) { change_number_ = value; } + void nextChangeNumber() { ++change_number_; } + virtual void advanceChangeNumber() = 0; + +private: + ChangeNumber change_number_; }; /// Access to the singleton SpellChecker. diff --git a/src/Text.cpp b/src/Text.cpp index f392cee920..702bcdf780 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -519,6 +519,9 @@ void Text::readParagraph(Paragraph & par, Lexer & lex, // Initialize begin_of_body_ on load; redoParagraph maintains par.setBeginOfBody(); + + // mark paragraph for spell checking on load + // par.requestSpellCheck(); } diff --git a/src/Text2.cpp b/src/Text2.cpp index 88aa09c7cd..9ce5d98fa9 100644 --- a/src/Text2.cpp +++ b/src/Text2.cpp @@ -356,6 +356,9 @@ void Text::setFont(BufferView const & bv, CursorSlice const & begin, Font f = tm.displayFont(pit, pos); f.update(font, language, toggleall); setCharFont(pit, pos, f, tm.font_); + // font change may change language... + // spell checker has to know that + pars_[pit].requestSpellCheck(pos); } } diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp index 48dbdd7a69..ea8e07fae6 100644 --- a/src/TextMetrics.cpp +++ b/src/TextMetrics.cpp @@ -2132,15 +2132,7 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type pit, int x, int y) co // Take this opportunity to spellcheck the row contents. if (row_has_changed && lyxrc.spellcheck_continuously) { - WordLangTuple wl; - // dummy variable, not used. - static docstring_list suggestions; - pos_type from = row.pos(); - pos_type to = row.endpos(); - while (from < row.endpos()) { - text_->getPar(pit).spellCheck(from, to, wl, suggestions, false); - from = to + 1; - } + text_->getPar(pit).spellCheck(); } // Don't paint the row if a full repaint has not been requested diff --git a/src/frontends/qt4/Menus.cpp b/src/frontends/qt4/Menus.cpp index d0003cd034..bc0f469bf2 100644 --- a/src/frontends/qt4/Menus.cpp +++ b/src/frontends/qt4/Menus.cpp @@ -738,7 +738,7 @@ void MenuDefinition::expandSpellingSuggestions(BufferView const * bv) pos_type from = bv->cursor().pos(); pos_type to = from; Paragraph const & par = bv->cursor().paragraph(); - SpellChecker::Result res = par.spellCheck(from, to, wl, suggestions); + SpellChecker::Result res = par.spellCheck(from, to, wl, suggestions, true, true); switch (res) { case SpellChecker::UNKNOWN_WORD: if (lyxrc.spellcheck_continuously) { diff --git a/src/rowpainter.cpp b/src/rowpainter.cpp index 2850c39d94..8dfb106077 100644 --- a/src/rowpainter.cpp +++ b/src/rowpainter.cpp @@ -234,6 +234,10 @@ void RowPainter::paintChars(pos_type & vpos, FontInfo const & font, bool const selection = (pos >= row_.sel_beg && pos < row_.sel_end) || pi_.selected; + // spelling correct? + bool const spell_state = + lyxrc.spellcheck_continuously && par_.isMisspelled(pos); + char_type prev_char = ' '; // collect as much similar chars as we can for (++vpos ; vpos < end ; ++vpos) { @@ -246,6 +250,12 @@ void RowPainter::paintChars(pos_type & vpos, FontInfo const & font, // Selection ends or starts here. break; + bool const new_spell_state = + lyxrc.spellcheck_continuously && par_.isMisspelled(pos); + if (new_spell_state != spell_state) + // Spell checker state changed here. + break; + Change const & change = par_.lookupChange(pos); if (!change_running.isSimilarTo(change)) // Track change type or author has changed. @@ -357,6 +367,10 @@ void RowPainter::paintFromPos(pos_type & vpos) bool const arabic = lang == "arabic_arabtex" || lang == "arabic_arabi" || lang == "farsi"; + // spelling correct? + bool const misspelled_ = + lyxrc.spellcheck_continuously && par_.isMisspelled(pos); + // draw as many chars as we can if ((!hebrew && !arabic) || (hebrew && !Encodings::isHebrewComposeChar(c)) @@ -370,8 +384,9 @@ void RowPainter::paintFromPos(pos_type & vpos) paintForeignMark(orig_x, orig_font.language()); - if (lyxrc.spellcheck_continuously && orig_font.isMisspelled()) + if (lyxrc.spellcheck_continuously && misspelled_) { paintMisspelledMark(orig_x, 2); + } }