presentation/SpellService.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "SpellService.h" | ||
| 2 | #include "ComposeLog.h" | ||
| 3 | #include "data/SpellDictionaryPaths.h" | ||
| 4 | |||
| 5 | #include <QLocale> | ||
| 6 | #include <QRegularExpression> | ||
| 7 | #include <QStandardPaths> | ||
| 8 | #include <QVariantMap> | ||
| 9 | |||
| 10 | #ifdef MAILJD_HAVE_SONNET | ||
| 11 | #include <Sonnet/GuessLanguage> | ||
| 12 | #include <Sonnet/Speller> | ||
| 13 | #endif | ||
| 14 | |||
| 15 | 795 | SpellService::SpellService(QObject *parent) : QObject(parent) {} | |
| 16 | |||
| 17 | 6222 | QStringList SpellService::languages() const { | |
| 18 |
2/2✓ Branch 2 → 3 taken 6120 times.
✓ Branch 2 → 4 taken 102 times.
|
6222 | if (m_loaded) return m_languages; |
| 19 | 102 | m_loaded = true; | |
| 20 | #ifdef MAILJD_HAVE_SONNET | ||
| 21 | // F108-17: the dictionaries other programs bring along, before Sonnet | ||
| 22 | // loads its Hunspell plugin (it reads the data directories then). | ||
| 23 |
1/1✓ Branch 4 → 5 taken 102 times.
|
102 | const QStringList roots = SpellDictionaryPaths::bundledRoots(); |
| 24 | const QStringList linked = SpellDictionaryPaths::expose( | ||
| 25 |
1/1✓ Branch 10 → 11 taken 102 times.
|
204 | SpellDictionaryPaths::findDictionaries(roots), |
| 26 |
3/3✓ Branch 8 → 9 taken 102 times.
✓ Branch 9 → 10 taken 102 times.
✓ Branch 11 → 12 taken 102 times.
|
306 | QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/spelling")); |
| 27 |
1/10✗ Branch 18 → 19 not taken.
✓ Branch 18 → 39 taken 102 times.
✗ Branch 19 → 20 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 27 → 28 not taken.
✗ Branch 29 → 30 not taken.
✗ Branch 37 → 21 not taken.
✗ Branch 37 → 38 not taken.
|
102 | if (!linked.isEmpty()) qCInfo(lcCompose) << "dictionaries of other programs" << linked << "from" << roots; |
| 28 |
2/2✓ Branch 40 → 41 taken 102 times.
✓ Branch 41 → 42 taken 102 times.
|
102 | m_languages = Sonnet::Speller().availableLanguages(); |
| 29 |
1/1✓ Branch 46 → 47 taken 102 times.
|
102 | m_languages.sort(); |
| 30 | #endif | ||
| 31 |
6/6✓ Branch 47 → 48 taken 102 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 58 → 49 taken 1 time.
✓ Branch 58 → 59 taken 102 times.
|
103 | qCInfo(lcCompose) << "spell checking languages" << m_languages.size(); |
| 32 | 102 | return m_languages; | |
| 33 | 102 | } | |
| 34 | |||
| 35 | 795 | SpellService::~SpellService() = default; | |
| 36 | |||
| 37 | 2128 | QVariantList SpellService::languageChoices() const { | |
| 38 | // Each language in its own words ("Deutsch", "American English"); the | ||
| 39 | // country only when two dictionaries would read the same. | ||
| 40 | 2128 | QList<std::pair<QString, QString>> named; | |
| 41 | 2128 | QHash<QString, int> uses; | |
| 42 |
5/5✓ Branch 2 → 3 taken 2128 times.
✓ Branch 3 → 4 taken 2128 times.
✓ Branch 4 → 5 taken 2128 times.
✓ Branch 26 → 6 taken 8512 times.
✓ Branch 26 → 27 taken 2128 times.
|
10640 | for (const QString &code : languages()) { |
| 43 |
1/1✓ Branch 7 → 8 taken 8512 times.
|
8512 | const QLocale locale(code); |
| 44 |
3/4✓ Branch 8 → 9 taken 8512 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8512 times.
✓ Branch 11 → 12 taken 8512 times.
|
8512 | QString name = locale.language() == QLocale::C ? code : locale.nativeLanguageName(); |
| 45 |
3/4✓ Branch 13 → 14 taken 8512 times.
✗ Branch 13 → 18 not taken.
✓ Branch 14 → 15 taken 8512 times.
✓ Branch 16 → 17 taken 8512 times.
|
8512 | if (!name.isEmpty()) name[0] = name[0].toUpper(); |
| 46 |
1/1✓ Branch 19 → 20 taken 8512 times.
|
8512 | named.append({code, name}); |
| 47 |
1/1✓ Branch 21 → 22 taken 8512 times.
|
8512 | ++uses[name]; |
| 48 | 10640 | } | |
| 49 | 2128 | QVariantList out; | |
| 50 |
4/4✓ Branch 28 → 29 taken 2128 times.
✓ Branch 29 → 30 taken 2128 times.
✓ Branch 84 → 31 taken 8512 times.
✓ Branch 84 → 85 taken 2128 times.
|
10640 | for (const auto &[code, name] : named) { |
| 51 |
1/1✓ Branch 34 → 35 taken 8512 times.
|
8512 | const QLocale locale(code); |
| 52 |
2/3✓ Branch 37 → 38 taken 6384 times.
✓ Branch 38 → 39 taken 6384 times.
✗ Branch 38 → 49 not taken.
|
6384 | const QString shown = uses.value(name) > 1 && locale.language() != QLocale::C |
| 53 |
15/25✓ Branch 36 → 37 taken 6384 times.
✓ Branch 36 → 49 taken 2128 times.
✓ Branch 41 → 42 taken 6384 times.
✓ Branch 45 → 46 taken 6384 times.
✓ Branch 46 → 47 taken 6384 times.
✓ Branch 50 → 51 taken 6384 times.
✓ Branch 50 → 52 taken 2128 times.
✓ Branch 52 → 53 taken 6384 times.
✓ Branch 52 → 54 taken 2128 times.
✓ Branch 54 → 55 taken 6384 times.
✓ Branch 54 → 56 taken 2128 times.
✓ Branch 56 → 57 taken 6384 times.
✓ Branch 56 → 58 taken 2128 times.
✓ Branch 58 → 59 taken 6384 times.
✓ Branch 58 → 60 taken 2128 times.
✗ Branch 100 → 101 not taken.
✗ Branch 100 → 102 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
|
29792 | ? name + QStringLiteral(" (") + locale.nativeTerritoryName() + QLatin1Char(')') |
| 54 |
1/1✓ Branch 47 → 48 taken 6384 times.
|
17024 | : name; |
| 55 |
3/5✓ Branch 70 → 71 taken 8512 times.
✓ Branch 74 → 75 taken 17024 times.
✓ Branch 74 → 76 taken 8512 times.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
|
42560 | out.append(QVariantMap{{QStringLiteral("code"), code}, {QStringLiteral("name"), shown}}); |
| 56 | 8512 | } | |
| 57 | 2128 | return out; | |
| 58 | 27664 | } | |
| 59 | |||
| 60 | 1911 | QString SpellService::bestLanguage(const QString &preferred) const { | |
| 61 |
2/3✓ Branch 2 → 3 taken 1911 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1911 times.
|
1911 | if (languages().isEmpty()) return {}; |
| 62 | 1911 | QString wanted = preferred; | |
| 63 |
1/1✓ Branch 12 → 13 taken 1911 times.
|
1911 | wanted.replace(QLatin1Char('-'), QLatin1Char('_')); |
| 64 |
2/2✓ Branch 14 → 15 taken 169 times.
✓ Branch 14 → 16 taken 1742 times.
|
1911 | if (m_languages.contains(wanted)) return wanted; |
| 65 |
2/2✓ Branch 19 → 20 taken 1742 times.
✓ Branch 20 → 21 taken 1742 times.
|
1742 | const QString base = wanted.section(QLatin1Char('_'), 0, 0).toLower(); |
| 66 | // The base language with its usual country first (de → de_DE, en → en_US). | ||
| 67 |
7/9✓ Branch 24 → 25 taken 871 times.
✓ Branch 24 → 28 taken 871 times.
✓ Branch 28 → 29 taken 871 times.
✓ Branch 31 → 32 taken 1742 times.
✓ Branch 32 → 33 taken 1742 times.
✓ Branch 35 → 36 taken 871 times.
✓ Branch 35 → 37 taken 871 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
|
2613 | const QString usual = base + QLatin1Char('_') + (base == QLatin1String("en") ? QStringLiteral("US") : base.toUpper()); |
| 68 |
2/2✓ Branch 38 → 39 taken 919 times.
✓ Branch 38 → 40 taken 823 times.
|
1742 | if (m_languages.contains(usual)) return usual; |
| 69 |
4/4✓ Branch 40 → 41 taken 823 times.
✓ Branch 41 → 42 taken 823 times.
✓ Branch 56 → 43 taken 3292 times.
✓ Branch 56 → 57 taken 823 times.
|
4115 | for (const QString &code : m_languages) |
| 70 |
3/4✓ Branch 47 → 48 taken 3292 times.
✓ Branch 48 → 49 taken 3292 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 3292 times.
|
3292 | if (code.section(QLatin1Char('_'), 0, 0).toLower() == base) return code; |
| 71 | 823 | return {}; | |
| 72 | 1911 | } | |
| 73 | |||
| 74 | 493 | Sonnet::Speller *SpellService::speller(const QString &language) { | |
| 75 | #ifdef MAILJD_HAVE_SONNET | ||
| 76 |
2/3✓ Branch 2 → 3 taken 493 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 493 times.
|
493 | if (!languages().contains(language)) return nullptr; |
| 77 | 493 | auto &slot = m_spellers[language]; | |
| 78 |
3/3✓ Branch 9 → 10 taken 62 times.
✓ Branch 9 → 14 taken 431 times.
✓ Branch 10 → 11 taken 62 times.
|
493 | if (!slot) slot = std::make_shared<Sonnet::Speller>(language); |
| 79 |
1/2✓ Branch 16 → 17 taken 493 times.
✗ Branch 16 → 18 not taken.
|
493 | return slot->isValid() ? slot.get() : nullptr; |
| 80 | #else | ||
| 81 | Q_UNUSED(language) | ||
| 82 | return nullptr; | ||
| 83 | #endif | ||
| 84 | } | ||
| 85 | |||
| 86 | 3038 | bool SpellService::isMisspelled(const QString &word, const QString &language) { | |
| 87 | #ifdef MAILJD_HAVE_SONNET | ||
| 88 | // G11: text repeats its words; a Hunspell lookup costs about 3 µs, a known | ||
| 89 | // word a hash lookup. Bounded: the cache of a language starts afresh at | ||
| 90 | // KnownWordsLimit words. | ||
| 91 |
1/1✓ Branch 2 → 3 taken 3038 times.
|
3038 | auto &known = m_known[language]; |
| 92 |
2/2✓ Branch 6 → 7 taken 2565 times.
✓ Branch 6 → 9 taken 473 times.
|
3038 | if (const auto it = known.constFind(word); it != known.constEnd()) return it.value(); |
| 93 |
1/1✓ Branch 9 → 11 taken 473 times.
|
473 | auto *checker = speller(language); |
| 94 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 473 times.
|
473 | if (!checker) return false; |
| 95 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 473 times.
|
473 | if (known.size() >= KnownWordsLimit) known.clear(); |
| 96 |
1/1✓ Branch 16 → 17 taken 473 times.
|
473 | const bool misspelled = checker->isMisspelled(word); |
| 97 |
1/1✓ Branch 17 → 18 taken 473 times.
|
473 | known.insert(word, misspelled); |
| 98 | 473 | return misspelled; | |
| 99 | #else | ||
| 100 | Q_UNUSED(word) | ||
| 101 | Q_UNUSED(language) | ||
| 102 | #endif | ||
| 103 | return false; | ||
| 104 | } | ||
| 105 | |||
| 106 | 20 | QStringList SpellService::suggestions(const QString &word, const QString &language, int maximum) { | |
| 107 | #ifdef MAILJD_HAVE_SONNET | ||
| 108 |
3/4✓ Branch 3 → 4 taken 20 times.
✗ Branch 3 → 10 not taken.
✓ Branch 4 → 5 taken 20 times.
✓ Branch 6 → 7 taken 20 times.
|
20 | if (auto *checker = speller(language)) return checker->suggest(word).mid(0, qMax(0, maximum)); |
| 109 | #else | ||
| 110 | Q_UNUSED(word) | ||
| 111 | Q_UNUSED(language) | ||
| 112 | Q_UNUSED(maximum) | ||
| 113 | #endif | ||
| 114 | ✗ | return {}; | |
| 115 | } | ||
| 116 | |||
| 117 | ✗ | void SpellService::addToDictionary(const QString &word, const QString &language) { | |
| 118 | #ifdef MAILJD_HAVE_SONNET | ||
| 119 | ✗ | if (auto *checker = speller(language)) checker->addToPersonal(word); | |
| 120 | ✗ | m_known[language].remove(word); | |
| 121 | #else | ||
| 122 | Q_UNUSED(word) | ||
| 123 | Q_UNUSED(language) | ||
| 124 | #endif | ||
| 125 | ✗ | } | |
| 126 | |||
| 127 | #ifdef MAILJD_HAVE_SONNET | ||
| 128 | namespace { | ||
| 129 | // Two words are enough for Sonnet (it also asks the dictionaries); one word | ||
| 130 | // ("Hi", "ok") belongs to too many languages (F108-23). | ||
| 131 | 28 | bool hasTwoWords(const QString &text) { | |
| 132 |
4/7✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 15 taken 25 times.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
|
31 | static const QRegularExpression word(QStringLiteral("[\\p{L}']{2,}")); |
| 133 |
1/1✓ Branch 16 → 17 taken 28 times.
|
28 | auto words = word.globalMatch(text); |
| 134 | 28 | int count = 0; | |
| 135 |
7/7✓ Branch 21 → 22 taken 52 times.
✓ Branch 21 → 25 taken 24 times.
✓ Branch 22 → 23 taken 52 times.
✓ Branch 23 → 24 taken 48 times.
✓ Branch 23 → 25 taken 4 times.
✓ Branch 26 → 18 taken 48 times.
✓ Branch 26 → 27 taken 28 times.
|
76 | while (count < 2 && words.hasNext()) { |
| 136 |
1/1✓ Branch 18 → 19 taken 48 times.
|
48 | words.next(); |
| 137 | 48 | ++count; | |
| 138 | } | ||
| 139 | 28 | return count >= 2; | |
| 140 | 28 | } | |
| 141 | } // namespace | ||
| 142 | #endif | ||
| 143 | |||
| 144 | 28 | QString SpellService::guess(const QString &text) const { | |
| 145 | #ifdef MAILJD_HAVE_SONNET | ||
| 146 |
7/8✓ Branch 2 → 3 taken 28 times.
✓ Branch 3 → 4 taken 28 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 28 times.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 24 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 24 times.
|
28 | if (!available() || !hasTwoWords(text)) return {}; |
| 147 |
1/1✓ Branch 10 → 11 taken 24 times.
|
24 | Sonnet::GuessLanguage guesser; |
| 148 |
3/3✓ Branch 11 → 12 taken 24 times.
✓ Branch 12 → 13 taken 24 times.
✓ Branch 13 → 14 taken 24 times.
|
24 | const QString found = guesser.identify(text.left(1000), languages()); |
| 149 | // The language, with its usual country (de_BE → de_DE), as bestLanguage. | ||
| 150 |
4/8✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 24 times.
✓ Branch 22 → 23 taken 24 times.
✓ Branch 23 → 24 taken 24 times.
✓ Branch 25 → 26 taken 24 times.
✗ Branch 25 → 27 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
|
48 | return found.isEmpty() ? QString() : bestLanguage(found.section(QLatin1Char('_'), 0, 0)); |
| 151 | #else | ||
| 152 | Q_UNUSED(text) | ||
| 153 | return {}; | ||
| 154 | #endif | ||
| 155 | 24 | } | |
| 156 |