diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -6,16 +6,17 @@ ########### unittests ############### -macro(SONNET_UNIT_TESTS) - foreach(_testname ${ARGN}) - ecm_add_test(${_testname}.cpp TEST_NAME sonnet-${_testname} LINK_LIBRARIES KF5::SonnetCore Qt5::Test) - endforeach() -endmacro(SONNET_UNIT_TESTS) - -sonnet_unit_tests( +ecm_add_tests( test_filter test_core test_suggest test_settings + NAME_PREFIX "sonnet-" + LINK_LIBRARIES KF5::SonnetCore Qt5::Test ) +ecm_add_tests( + test_highlighter + NAME_PREFIX "sonnet-" + LINK_LIBRARIES KF5::SonnetUi KF5::SonnetCore Qt5::Test +) diff --git a/autotests/test_highlighter.cpp b/autotests/test_highlighter.cpp new file mode 100644 --- /dev/null +++ b/autotests/test_highlighter.cpp @@ -0,0 +1,139 @@ +// krazy:excludeall=spelling +/** + * Copyright (C) 2017 David Faure + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "highlighter.h" +#include "speller.h" + +#include +#include +#include +#include +#include + +using namespace Sonnet; + +class HighlighterTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testEnglish(); + void testFrench(); + void testMultipleLanguages(); +}; + +void HighlighterTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + + Speller speller(QStringLiteral("en_US")); + if (!speller.availableBackends().contains(QLatin1String("ASpell"))) { + QSKIP("ASpell not available"); + } + // Doing this here affects all the highlighters created later on, due to the weird hidden "Settings" class saving stuff behind the API's back... + speller.setDefaultClient(QStringLiteral("ASpell")); + if (!speller.availableLanguages().contains(QLatin1String("en"))) { + QSKIP("'en' not available"); + } + + // How weird to have to do this here and not with the Highlighter API.... + speller.setAttribute(Speller::AutoDetectLanguage, true); +} + +static const char s_englishSentence[] = "Hello helo this is the highlighter test enviroment guvernment"; // words from test_suggest.cpp + +void HighlighterTest::testEnglish() +{ + // GIVEN + QPlainTextEdit textEdit; + textEdit.setPlainText(QString::fromLatin1(s_englishSentence)); + Sonnet::Highlighter highlighter(&textEdit); + highlighter.setCurrentLanguage(QStringLiteral("en")); + QVERIFY(highlighter.spellCheckerFound()); + highlighter.rehighlight(); + QTextCursor cursor(textEdit.document()); + + // WHEN + cursor.setPosition(6); + const QStringList suggestionsForHelo = highlighter.suggestionsForWord(QStringLiteral("helo"), cursor); + cursor.setPosition(40); + const QStringList suggestionsForEnviroment = highlighter.suggestionsForWord(QStringLiteral("enviroment"), cursor); + + // THEN + QCOMPARE(suggestionsForHelo.count(), 10); + QVERIFY2(suggestionsForHelo.contains(QStringLiteral("hello")), qPrintable(suggestionsForHelo.join(QLatin1Char(',')))); + QVERIFY2(suggestionsForEnviroment.contains(QStringLiteral("environment")), qPrintable(suggestionsForEnviroment.join(QLatin1Char(',')))); +} + +static const char s_frenchSentence[] = "Bnjour est un bon mot pour tester le dictionnare."; + +void HighlighterTest::testFrench() +{ + // GIVEN + QPlainTextEdit textEdit; + textEdit.setPlainText(QString::fromLatin1(s_frenchSentence)); + Sonnet::Highlighter highlighter(&textEdit); + highlighter.setCurrentLanguage(QStringLiteral("fr_FR")); + QVERIFY(highlighter.spellCheckerFound()); + highlighter.rehighlight(); + QTextCursor cursor(textEdit.document()); + + // WHEN + cursor.setPosition(0); + const QStringList suggestionsForBnjour = highlighter.suggestionsForWord(QStringLiteral("Bnjour"), cursor); + cursor.setPosition(37); + const QStringList suggestionsForDict = highlighter.suggestionsForWord(QStringLiteral("dictionnare"), cursor); + + // THEN + QVERIFY2(suggestionsForBnjour.contains(QStringLiteral("Bonjour")), qPrintable(suggestionsForBnjour.join(QLatin1Char(',')))); + QVERIFY2(suggestionsForDict.contains(QStringLiteral("dictionnaire")), qPrintable(suggestionsForDict.join(QLatin1Char(',')))); +} + +void HighlighterTest::testMultipleLanguages() +{ + // GIVEN + QPlainTextEdit textEdit; + const QString englishSentence = QString::fromLatin1(s_englishSentence) + QLatin1Char('\n'); + textEdit.setPlainText(englishSentence + QString::fromLatin1(s_frenchSentence)); + Sonnet::Highlighter highlighter(&textEdit); + highlighter.rehighlight(); + QTextCursor cursor(textEdit.document()); + + // WHEN + cursor.setPosition(6); + const QStringList suggestionsForHelo = highlighter.suggestionsForWord(QStringLiteral("helo"), cursor); + cursor.setPosition(40); + const QStringList suggestionsForEnviroment = highlighter.suggestionsForWord(QStringLiteral("enviroment"), cursor); + cursor.setPosition(englishSentence.size()); + const QStringList suggestionsForBnjour = highlighter.suggestionsForWord(QStringLiteral("Bnjour"), cursor); + cursor.setPosition(englishSentence.size() + 37); + const QStringList suggestionsForDict = highlighter.suggestionsForWord(QStringLiteral("dictionnare"), cursor); + + // THEN + QVERIFY2(suggestionsForHelo.contains(QStringLiteral("hello")), qPrintable(suggestionsForHelo.join(QLatin1Char(',')))); + QVERIFY2(suggestionsForEnviroment.contains(QStringLiteral("environment")), qPrintable(suggestionsForEnviroment.join(QLatin1Char(',')))); + QVERIFY2(suggestionsForBnjour.contains(QStringLiteral("Bonjour")), qPrintable(suggestionsForBnjour.join(QLatin1Char(',')))); + QVERIFY2(suggestionsForDict.contains(QStringLiteral("dictionnaire")), qPrintable(suggestionsForDict.join(QLatin1Char(',')))); +} + +QTEST_MAIN(HighlighterTest) + +#include "test_highlighter.moc" diff --git a/src/core/loader.cpp b/src/core/loader.cpp --- a/src/core/loader.cpp +++ b/src/core/loader.cpp @@ -85,9 +85,12 @@ SpellerPlugin *Loader::createSpeller(const QString &language, const QString &clientName) const { - QString pclient = clientName; + QString backend = clientName; QString plang = language; + if (backend.isEmpty()) { + backend = d->settings->defaultClient(); + } if (plang.isEmpty()) { plang = d->settings->defaultLanguage(); } @@ -102,8 +105,8 @@ QVectorIterator itr(lClients); while (itr.hasNext()) { Client *item = itr.next(); - if (!pclient.isEmpty()) { - if (pclient == item->name()) { + if (!backend.isEmpty()) { + if (backend == item->name()) { SpellerPlugin *dict = item->createSpeller(plang); return dict; } diff --git a/src/ui/highlighter.h b/src/ui/highlighter.h --- a/src/ui/highlighter.h +++ b/src/ui/highlighter.h @@ -139,6 +139,21 @@ QStringList suggestionsForWord(const QString &word, int max = 10); /** + * Returns a list of suggested replacements for the given misspelled word. + * If the word is not misspelled, the list will be empty. + * + * @param word the misspelled word + * @param cursor the cursor pointing to the beginning of that word. This is used + * to determine the language to use, when AutoDetectLanguage is enabled. + * @param max at most this many suggestions will be returned. If this is + * -1, as many suggestions as the spell backend supports will + * be returned. + * @return a list of suggested replacements for the word + * @since 5.42 + */ + QStringList suggestionsForWord(const QString &word, const QTextCursor& cursor, int max = 10); + + /** * Checks if a given word is marked as misspelled by the highlighter. * * @param word the word to be checked diff --git a/src/ui/highlighter.cpp b/src/ui/highlighter.cpp --- a/src/ui/highlighter.cpp +++ b/src/ui/highlighter.cpp @@ -45,9 +45,15 @@ namespace Sonnet { +// Cache of previously-determined languages (when using AutoDetectLanguage) +// There is one such cache per block (paragraph) class LanguageCache : public QTextBlockUserData { public: + // Key: QPair + // Value: language name QMap, QString> languages; + + // Remove all cached language information after @p pos void invalidate(int pos) { QMutableMapIterator, QString> it(languages); it.toBack(); @@ -57,6 +63,18 @@ else break; } } + + QString languageAtPos(int pos) const { + // The data structure isn't really great for such lookups... + QMapIterator, QString> it(languages); + while (it.hasNext()) { + it.next(); + if (it.key().first <= pos && it.key().first + it.key().second >= pos) { + return it.value(); + } + } + return QString(); + } }; @@ -450,11 +468,25 @@ QStringList Highlighter::suggestionsForWord(const QString &word, int max) { QStringList suggestions = d->spellchecker->suggest(word); - if (max != -1) { - while (suggestions.count() > max) { - suggestions.removeLast(); + if (max != -1 && suggestions.count() > max) { + suggestions = suggestions.mid(0, max); + } + return suggestions; +} + +QStringList Highlighter::suggestionsForWord(const QString &word, const QTextCursor &cursor, int max) +{ + LanguageCache* cache = dynamic_cast(cursor.block().userData()); + if (cache) { + const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock()); + if (!cachedLanguage.isEmpty()) { + d->spellchecker->setLanguage(cachedLanguage); } } + QStringList suggestions = d->spellchecker->suggest(word); + if (max != -1 && suggestions.count() > max) { + suggestions = suggestions.mid(0, max); + } return suggestions; } diff --git a/src/ui/spellcheckdecorator.cpp b/src/ui/spellcheckdecorator.cpp --- a/src/ui/spellcheckdecorator.cpp +++ b/src/ui/spellcheckdecorator.cpp @@ -171,7 +171,7 @@ QMenu menu; //don't use KMenu here we don't want auto management accelerator //Add the suggestions to the menu - const QStringList reps = m_highlighter->suggestionsForWord(selectedWord); + const QStringList reps = m_highlighter->suggestionsForWord(selectedWord, cursor); if (reps.isEmpty()) { QAction *suggestionsAction = menu.addAction(tr("No suggestions for %1").arg(selectedWord)); suggestionsAction->setEnabled(false);