File indexing completed on 2024-05-19 09:22:10

0001 /*
0002  *   SPDX-FileCopyrightText: 2007 Ryan P. Bitanga <ephebiphobic@gmail.com>
0003  *
0004  *   SPDX-License-Identifier: LGPL-2.0-only
0005  */
0006 
0007 #include "spellcheck.h"
0008 
0009 #include <QClipboard>
0010 #include <QDebug>
0011 #include <QGuiApplication>
0012 #include <QIcon>
0013 #include <QLocale>
0014 #include <QMimeData>
0015 #include <QSet>
0016 
0017 #include <KConfigGroup>
0018 #include <KLocalizedString>
0019 
0020 SpellCheckRunner::SpellCheckRunner(QObject *parent, const KPluginMetaData &metaData)
0021     : AbstractRunner(parent, metaData)
0022 {
0023 }
0024 
0025 SpellCheckRunner::~SpellCheckRunner() = default;
0026 
0027 // Load a default dictionary and some locale names
0028 void SpellCheckRunner::loadData()
0029 {
0030     // Load the default speller, with the default language
0031     auto defaultSpellerIt = m_spellers.find(QString());
0032     if (defaultSpellerIt == m_spellers.end()) {
0033         defaultSpellerIt = m_spellers.insert(QString(), QSharedPointer<Sonnet::Speller>(new Sonnet::Speller(QString())));
0034     }
0035     auto &defaultSpeller = defaultSpellerIt.value();
0036 
0037     // store all language names, makes it possible to type "spell german TERM" if english locale is set
0038     // Need to construct a map between natual language names and names the spell-check recognises.
0039     m_availableLangCodes = defaultSpeller->availableLanguages();
0040     // We need to filter the available languages so that we associate the natural language
0041     // name (eg. 'german') with one sub-code.
0042     QSet<QString> families;
0043     // First get the families
0044     for (const QString &code : std::as_const(m_availableLangCodes)) {
0045         families += code.left(2);
0046     }
0047     // Now for each family figure out which is the main code.
0048     for (const QString &fcode : std::as_const(families)) {
0049         const QStringList family = m_availableLangCodes.filter(fcode);
0050         QString code;
0051         // If we only have one code, use it.
0052         // If a string is the default language, use it
0053         if (family.contains(defaultSpeller->language())) {
0054             code = defaultSpeller->language();
0055         } else if (fcode == QLatin1String("en")) {
0056             // If the family is english, default to en_US.
0057             const auto enUS = QStringLiteral("en_US");
0058             if (family.contains(enUS)) {
0059                 code = enUS;
0060             }
0061         } else if (family.contains(fcode + QLatin1Char('_') + fcode.toUpper())) {
0062             // If we have a speller of the form xx_XX, try that.
0063             // This gets us most European languages with more than one spelling.
0064             code = fcode + QLatin1Char('_') + fcode.toUpper();
0065         } else {
0066             // Otherwise, pick the first value as it is highest priority.
0067             code = family.first();
0068         }
0069         // Finally, add code to the map.
0070         // FIXME: We need someway to map languageCodeToName
0071         const QString name; // = locale->languageCodeToName(fcode);
0072         if (!name.isEmpty()) {
0073             m_languages[name.toLower()] = code;
0074         }
0075     }
0076 }
0077 
0078 void SpellCheckRunner::reloadConfiguration()
0079 {
0080     const KConfigGroup cfg = config();
0081     m_triggerWord = cfg.readEntry("trigger", i18n("spell"));
0082     // Processing will be triggered by "keyword "
0083     m_requireTriggerWord = cfg.readEntry("requireTriggerWord", true) && !m_triggerWord.isEmpty();
0084     m_triggerWord += QLatin1Char(' ');
0085 
0086     QStringList exampleQueries{i18nc("Spelling checking runner syntax, first word is trigger word, e.g.  \"spell\".", "%1:q:", m_triggerWord)};
0087     if (!m_requireTriggerWord) {
0088         exampleQueries.append(QStringLiteral(":q:"));
0089     }
0090     RunnerSyntax s(exampleQueries, i18n("Checks the spelling of :q:."));
0091 
0092     if (m_requireTriggerWord) {
0093         setTriggerWords({m_triggerWord});
0094         setMinLetterCount(minLetterCount() + 2); // We want at least two letters after the trigger word
0095     } else {
0096         setMinLetterCount(2);
0097         setMatchRegex(QRegularExpression());
0098     }
0099 
0100     setSyntaxes({RunnerSyntax(s)});
0101     // Clear the data arrays to save memory
0102     m_spellers.clear();
0103     m_availableLangCodes.clear();
0104 }
0105 
0106 /* Take the input query, split into a list, and see if it contains a language to spell in.
0107  * Return the empty string if we can't match a language. */
0108 QString SpellCheckRunner::findLang(const QStringList &terms)
0109 {
0110     if (terms.isEmpty()) {
0111         return QString();
0112     }
0113 
0114     // If first term is a language code (like en_GB, en_gb or en), set it as the spell-check language
0115     const auto langCodeIt = std::find_if(m_availableLangCodes.cbegin(), m_availableLangCodes.cend(), [&terms](const QString &languageCode) {
0116         return languageCode.startsWith(terms[0], Qt::CaseInsensitive);
0117     });
0118     if (langCodeIt != m_availableLangCodes.cend()) {
0119         return *langCodeIt;
0120     }
0121 
0122     // If we have two terms and the first is a language name (eg 'french'),
0123     // set it as the available language
0124     if (terms.count() >= 2) {
0125         QString code;
0126         {
0127             // Is this a descriptive language name?
0128             QMap<QString, QString>::const_iterator it = m_languages.constFind(terms[0].toLower());
0129             if (it != m_languages.constEnd()) {
0130                 code = *it;
0131             }
0132             // Maybe it is a subset of a language code?
0133             else {
0134                 QStringList codes = QStringList(m_languages.values()).filter(terms[0]);
0135                 if (!codes.isEmpty()) {
0136                     code = codes.first();
0137                 }
0138             }
0139         }
0140 
0141         if (!code.isEmpty()) {
0142             // We found a valid language! Check still available
0143             // Does the spell-checker like it?
0144             if (m_availableLangCodes.contains(code)) {
0145                 return code;
0146             }
0147         }
0148         // FIXME: Support things like 'british english' or 'canadian french'
0149     }
0150     return QString();
0151 }
0152 
0153 void SpellCheckRunner::match(RunnerContext &context)
0154 {
0155     const QString term = context.query();
0156     QString query = term;
0157 
0158     if (m_requireTriggerWord) {
0159         int len = m_triggerWord.length();
0160         if (query.left(len) != m_triggerWord) {
0161             return;
0162         }
0163         query = query.mid(len).trimmed();
0164     }
0165     if (m_spellers.isEmpty()) {
0166         loadData();
0167     }
0168 
0169     // Pointer to speller object with our chosen language
0170     QSharedPointer<Sonnet::Speller> speller = m_spellers[QString()];
0171 
0172     QString lang;
0173     if (speller->isValid()) {
0174         QStringList terms = query.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0175         lang = findLang(terms);
0176         // If we found a language, create a new speller object using it.
0177         if (!lang.isEmpty()) {
0178             // First term is the language
0179             terms.removeFirst();
0180             // New speller object if we don't already have one
0181             if (!m_spellers.contains(lang)) {
0182                 // Check nothing happened while we were acquiring the lock
0183                 if (!m_spellers.contains(lang)) {
0184                     m_spellers[lang] = QSharedPointer<Sonnet::Speller>(new Sonnet::Speller(lang));
0185                 }
0186             }
0187             speller = m_spellers[lang];
0188             // Rejoin the strings
0189             query = terms.join(QLatin1Char(' '));
0190         }
0191     }
0192 
0193     if (query.size() < 2) {
0194         return;
0195     }
0196 
0197     if (speller->isValid()) {
0198         const auto fillMatch = [this, &context, &query, &speller](const QString &langCode = QString()) {
0199             if (!langCode.isEmpty()) {
0200                 speller->setLanguage(langCode);
0201             }
0202 
0203             QStringList suggestions;
0204             const bool correct = speller->checkAndSuggest(query, suggestions);
0205 
0206             if (correct) {
0207                 QueryMatch match(this);
0208                 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest);
0209                 match.setIconName(QStringLiteral("checkbox"));
0210                 match.setText(query);
0211                 match.setSubtext(i18nc("Term is spelled correctly", "Correct"));
0212                 match.setData(query);
0213                 context.addMatch(match);
0214             } else if (!suggestions.isEmpty()) {
0215                 for (const auto &suggestion : std::as_const(suggestions)) {
0216                     QueryMatch match(this);
0217                     match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest);
0218                     match.setIconName(QStringLiteral("edit-rename"));
0219                     match.setText(suggestion);
0220                     match.setSubtext(i18n("Suggested term"));
0221                     match.setData(suggestion);
0222                     context.addMatch(match);
0223                 }
0224             } else {
0225                 return false;
0226             }
0227 
0228             return true;
0229         };
0230 
0231         if (!fillMatch() && lang.isEmpty() && m_availableLangCodes.count() >= 2) {
0232             // Perhaps the term is not in the default dictionary, try other dictionaries.
0233             const QString defaultLangCode = speller->language();
0234             for (const QString &langCode : std::as_const(m_availableLangCodes)) {
0235                 if (langCode == defaultLangCode) {
0236                     continue;
0237                 }
0238 
0239                 if (fillMatch(langCode)) {
0240                     // The dictionary returns valid results
0241                     break;
0242                 }
0243             }
0244             // No need to reset the default language as the speller will be reset in destroydata()
0245         }
0246     } else {
0247         QueryMatch match(this);
0248         match.setCategoryRelevance(QueryMatch::CategoryRelevance::Highest);
0249         match.setIconName(QStringLiteral("data-error"));
0250         match.setText(xi18nc("@info", "No dictionary found. Please install <resource>hunspell</resource> package using your package manager"));
0251         context.addMatch(match);
0252     }
0253 }
0254 
0255 void SpellCheckRunner::run(const RunnerContext & /*context*/, const QueryMatch &match)
0256 {
0257     QGuiApplication::clipboard()->setText(match.data().toString());
0258 }
0259 
0260 QMimeData *SpellCheckRunner::mimeDataForMatch(const QueryMatch &match)
0261 {
0262     QMimeData *result = new QMimeData();
0263     const QString text = match.data().toString();
0264     result->setText(text);
0265     return result;
0266 }
0267 
0268 K_PLUGIN_CLASS_WITH_JSON(SpellCheckRunner, "plasma-runner-spellchecker.json")
0269 
0270 #include "spellcheck.moc"