/** * \file ControlSpellchecker.C * This file is part of LyX, the document processor. * Licence details can be found in the file COPYING. * * \author Edwin Leuven * * Full author contact details are available in file CREDITS. */ #include #include "ControlSpellchecker.h" #include "buffer.h" #include "bufferparams.h" #include "BufferView.h" #include "cursor.h" #include "CutAndPaste.h" #include "debug.h" #include "gettext.h" #include "language.h" #include "lyxrc.h" #include "paragraph.h" #if defined(USE_ASPELL) # include "aspell_local.h" #elif defined(USE_PSPELL) # include "pspell.h" #endif #if defined(USE_ISPELL) # include "ispell.h" #else # include "SpellBase.h" #endif #include "support/textutils.h" #include "support/convert.h" #include "support/docstring.h" #include "frontends/Alert.h" // FIXME: those two headers are needed because of the // WorkArea::redraw() call below. #include "frontends/LyXView.h" #include "frontends/WorkArea.h" using std::advance; using std::distance; using std::endl; using std::string; namespace lyx { using support::bformat; using support::contains; namespace frontend { ControlSpellchecker::ControlSpellchecker(Dialog & parent) : Dialog::Controller(parent), exitEarly_(false), oldval_(0), newvalue_(0), count_(0) { } ControlSpellchecker::~ControlSpellchecker() {} namespace { SpellBase * getSpeller(BufferParams const & bp) { string lang = (lyxrc.isp_use_alt_lang) ? lyxrc.isp_alt_lang : bp.language->code(); #if defined(USE_ASPELL) if (lyxrc.use_spell_lib) return new ASpell(bp, lang); #elif defined(USE_PSPELL) if (lyxrc.use_spell_lib) return new PSpell(bp, lang); #endif #if defined(USE_ISPELL) lang = (lyxrc.isp_use_alt_lang) ? lyxrc.isp_alt_lang : bp.language->lang(); return new ISpell(bp, lang); #else return new SpellBase; #endif } } // namespace anon bool ControlSpellchecker::initialiseParams(std::string const &) { lyxerr[Debug::GUI] << "Spellchecker::initialiseParams" << endl; speller_.reset(getSpeller(kernel().buffer().params())); if (!speller_.get()) return false; // reset values to initial oldval_ = 0; newvalue_ = 0; count_ = 0; bool const success = speller_->error().empty(); if (!success) { Alert::error(_("Spellchecker error"), _("The spellchecker could not be started\n") + speller_->error()); speller_.reset(0); } return success; } void ControlSpellchecker::clearParams() { lyxerr[Debug::GUI] << "Spellchecker::clearParams" << endl; speller_.reset(0); } namespace { bool isLetter(DocIterator const & dit) { return dit.inTexted() && dit.inset().allowSpellCheck() && dit.pos() != dit.lastpos() && (dit.paragraph().isLetter(dit.pos()) // We want to pass the ' and escape chars to ispell || contains(lyx::from_utf8(lyxrc.isp_esc_chars + '\''), dit.paragraph().getChar(dit.pos()))) && !dit.paragraph().isDeleted(dit.pos()); } WordLangTuple nextWord(LCursor & cur, ptrdiff_t & progress) { BufferParams const & bp = cur.bv().buffer()->params(); bool inword = false; bool ignoreword = false; cur.resetAnchor(); docstring word; string lang_code; while (cur.depth()) { if (isLetter(cur)) { if (!inword) { inword = true; ignoreword = false; cur.resetAnchor(); word.clear(); lang_code = cur.paragraph().getFontSettings(bp, cur.pos()).language()->code(); } // Insets like optional hyphens and ligature // break are part of a word. if (!cur.paragraph().isInset(cur.pos())) { Paragraph::value_type const c = cur.paragraph().getChar(cur.pos()); word += c; if (isDigit(c)) ignoreword = true; } } else { // !isLetter(cur) if (inword) if (!word.empty() && !ignoreword) { cur.setSelection(); return WordLangTuple(word, lang_code); } else inword = false; } cur.forwardPos(); ++progress; } return WordLangTuple(docstring(), string()); } } // namespace anon void ControlSpellchecker::check() { lyxerr[Debug::GUI] << "Check the spelling of a word" << endl; SpellBase::Result res = SpellBase::OK; LCursor cur = kernel().bufferview()->cursor(); while (cur && cur.pos() && isLetter(cur)) { cur.backwardPos(); } ptrdiff_t start = 0, total = 0; DocIterator it = DocIterator(kernel().buffer().inset()); for (start = 0; it != cur; it.forwardPos()) ++start; for (total = start; it; it.forwardPos()) ++total; exitEarly_ = false; while (res == SpellBase::OK || res == SpellBase::IGNORED_WORD) { word_ = nextWord(cur, start); // end of document if (getWord().empty()) { showSummary(); exitEarly_ = true; return; } ++count_; // Update slider if and only if value has changed float progress = total ? float(start)/total : 1; newvalue_ = int(100.0 * progress); if (newvalue_!= oldval_) { lyxerr[Debug::GUI] << "Updating spell progress." << endl; oldval_ = newvalue_; // set progress bar dialog().view().partialUpdate(SPELL_PROGRESSED); } // speller might be dead ... if (!checkAlive()) return; res = speller_->check(word_); // ... or it might just be reporting an error if (!checkAlive()) return; } lyxerr[Debug::GUI] << "Found word \"" << to_utf8(getWord()) << "\"" << endl; int const size = cur.selEnd().pos() - cur.selBegin().pos(); cur.pos() -= size; kernel().bufferview()->putSelectionAt(cur, size, false); // if we used a lfun like in find/replace, dispatch would do // that for us kernel().bufferview()->update(); // FIXME: this Controller is very badly designed... kernel().lyxview().currentWorkArea()->redraw(); // set suggestions if (res != SpellBase::OK && res != SpellBase::IGNORED_WORD) { lyxerr[Debug::GUI] << "Found a word needing checking." << endl; dialog().view().partialUpdate(SPELL_FOUND_WORD); } } bool ControlSpellchecker::checkAlive() { if (speller_->alive() && speller_->error().empty()) return true; docstring message; if (speller_->error().empty()) message = _("The spellchecker has died for some reason.\n" "Maybe it has been killed."); else message = _("The spellchecker has failed.\n") + speller_->error(); dialog().CancelButton(); Alert::error(_("The spellchecker has failed"), message); return false; } void ControlSpellchecker::showSummary() { if (!checkAlive() || count_ == 0) { dialog().CancelButton(); return; } docstring message; if (count_ != 1) message = bformat(_("%1$d words checked."), count_); else message = _("One word checked."); dialog().CancelButton(); Alert::information(_("Spelling check completed"), message); } void ControlSpellchecker::replace(docstring const & replacement) { lyxerr[Debug::GUI] << "ControlSpellchecker::replace(" << to_utf8(replacement) << ")" << std::endl; BufferView & bufferview = *kernel().bufferview(); cap::replaceSelectionWithString(bufferview.cursor(), replacement, true); kernel().buffer().markDirty(); // If we used an LFUN, we would not need that bufferview.update(); // fix up the count --count_; check(); } void ControlSpellchecker::replaceAll(docstring const & replacement) { // TODO: add to list replace(replacement); } void ControlSpellchecker::insert() { speller_->insert(word_); check(); } docstring const ControlSpellchecker::getSuggestion() const { return speller_->nextMiss(); } docstring const ControlSpellchecker::getWord() const { return word_.word(); } void ControlSpellchecker::ignoreAll() { speller_->accept(word_); check(); } } // namespace frontend } // namespace lyx