diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6ec815..21fa21ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,7 @@ - Dev: Unwrapped `LimitedQueueSnapshot` to `std::vector`. (#6606) - Dev: Simplified uses of `getMessageSnapshot`. (#6607) - Dev: Disabled `llvm-prefer-static-over-anonymous-namespace` in clang-tidy. (#6610) -- Dev: Added experimental spell checker support. (#6446, #6703, #6722) +- Dev: Added experimental spell checker support. (#6446, #6703, #6722, #6730) - Dev: Added Clazy linting in CI. (#6623) - Dev: Added custom clang-tidy module linting in CI. (#6626) - Dev: CMake option `USE_ALTERNATE_LINKER` now errors if the given linker can't be found. (#6692) diff --git a/src/controllers/commands/builtin/chatterino/Debugging.cpp b/src/controllers/commands/builtin/chatterino/Debugging.cpp index 6ad4e093..e331cec3 100644 --- a/src/controllers/commands/builtin/chatterino/Debugging.cpp +++ b/src/controllers/commands/builtin/chatterino/Debugging.cpp @@ -207,18 +207,6 @@ QString debugTest(const CommandContext &ctx) getApp()->getUpdates().checkForUpdates(); ctx.channel->addSystemMessage(QString("checking for updates")); } - else if (command == "spellcheck-get-system-dictionaries") - { -#ifdef CHATTERINO_WITH_SPELLCHECK - auto dicts = getApp()->getSpellChecker()->getSystemDictionaries(); - for (const auto &dict : dicts) - { - ctx.channel->addSystemMessage(QString("system dictionary: %1 at %2") - .arg(dict.name) - .arg(dict.path)); - } -#endif - } else if (command == "save-settings") { ctx.channel->addSystemMessage(u"requesting settings save"_s); diff --git a/src/controllers/spellcheck/SpellChecker.cpp b/src/controllers/spellcheck/SpellChecker.cpp index eb9dafe4..6ca15702 100644 --- a/src/controllers/spellcheck/SpellChecker.cpp +++ b/src/controllers/spellcheck/SpellChecker.cpp @@ -24,7 +24,7 @@ namespace { /// Returns a list of available dictionaries in the given directory std::vector loadDictionariesFromDirectory( - const QDir &searchDirectory) + const QDir &searchDirectory, bool isSystem) { std::vector dictionaries; @@ -47,16 +47,34 @@ std::vector loadDictionariesFromDirectory( auto isSymbolicLink = affInfo.isSymbolicLink() || dicInfo.isSymbolicLink(); + QString path = [&] { + if (isSystem) + { + return searchDirectory.absoluteFilePath(dictName); + } + return dictName; + }(); + dictionaries.push_back(DictionaryInfo{ .name = dictName, - .path = searchDirectory.absoluteFilePath(dictName), + .path = path, .isSymbolicLink = isSymbolicLink, + .isSystem = isSystem, }); } return dictionaries; } +QString resolveDictionaryPath(const QString &path) +{ + if (QDir::isAbsolutePath(path)) + { + return path; + } + return combinePath(getApp()->getPaths().dictionariesDirectory, path); +} + } // namespace class SpellCheckerPrivate @@ -75,25 +93,20 @@ private: std::unique_ptr SpellCheckerPrivate::tryLoad( const QString &path) { - std::filesystem::path aff; - std::filesystem::path dic; - if (path.isNull()) + if (path.isEmpty()) { - auto stdPath = - qStringToStdPath(getApp()->getPaths().dictionariesDirectory); - aff = stdPath / "index.aff"; - dic = stdPath / "index.dic"; - } - else - { - aff = qStringToStdPath(path % ".aff"); - dic = qStringToStdPath(path % ".dic"); + qCDebug(chatterinoSpellcheck) << "No path specified"; + return nullptr; } + auto resolvedPath = resolveDictionaryPath(path); + auto aff = qStringToStdPath(resolvedPath % ".aff"); + auto dic = qStringToStdPath(resolvedPath % ".dic"); + if (!std::filesystem::exists(aff) || !std::filesystem::exists(dic)) { - qCInfo(chatterinoSpellcheck) - << "Failed to find index.aff or index.dic in 'Dictionaries'"; + qCInfo(chatterinoSpellcheck).nospace().noquote() + << "Failed to find " << resolvedPath << ".{aff,dic}"; return nullptr; } std::error_code ec; @@ -124,20 +137,9 @@ SpellCheckerPrivate::SpellCheckerPrivate(const char *affpath, const char *dpath) } SpellChecker::SpellChecker() - : private_(SpellCheckerPrivate::tryLoad()) + : private_(SpellCheckerPrivate::tryLoad( + getSettings()->spellCheckingDefaultDictionary)) { - // The method we load dictionaries by is bound to change, so it's OK for this to be a bit ugly as we test things. - - if (!this->private_) - { - // The default dictionary was not found, try the fallback if it's set - auto fallbackDictionary = - getSettings()->spellCheckingFallback.getValue(); - if (!fallbackDictionary.isEmpty()) - { - this->private_ = SpellCheckerPrivate::tryLoad(fallbackDictionary); - } - } } #else class SpellCheckerPrivate @@ -192,70 +194,72 @@ std::vector SpellChecker::suggestions(const QString &word) } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -std::vector SpellChecker::getSystemDictionaries() const +std::vector SpellChecker::getAvailableDictionaries() const { #ifdef CHATTERINO_WITH_SPELLCHECK + std::vector> searchDirectories{ + {getApp()->getPaths().dictionariesDirectory, false}, + }; + # if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) // For each XDG data directory, search in hunspell, myspell, and myspell/dicts. // This somewhat matches where dictionaries are stored on Ubuntu & Fedora // if we want to support defaulting to your LC_ALL language. - QStringList searchDirectories; auto dataDirs = getXDGBaseDirectories(XDGDirectoryType::Data); for (const auto &dataDir : dataDirs) { - searchDirectories.push_back(combinePath(dataDir, "hunspell")); - searchDirectories.push_back(combinePath(dataDir, "myspell")); - searchDirectories.push_back(combinePath(dataDir, "myspell/dicts")); + searchDirectories.emplace_back(combinePath(dataDir, "hunspell"), true); + searchDirectories.emplace_back(combinePath(dataDir, "myspell"), true); + searchDirectories.emplace_back(combinePath(dataDir, "myspell/dicts"), + true); } +# endif std::vector dictionaries; - for (const auto &searchDirectory : searchDirectories) + for (const auto &[searchDirectory, isSystem] : searchDirectories) { qCDebug(chatterinoSpellcheck) - << "Looking for system dictionaries in" << searchDirectory; - for (const auto &dict : loadDictionariesFromDirectory(searchDirectory)) + << "Looking for dictionaries in" << searchDirectory + << "isSystem:" << isSystem; + for (const auto &dict : + loadDictionariesFromDirectory(searchDirectory, isSystem)) { - if (dict.isSymbolicLink) + if (dict.isSymbolicLink && dict.isSystem) { // NOTE: We currently filter out symbolic links from system-loaded dictionaries. // Without this, the list of dictionaries we "support" would be too high on Linux distros. // As an example, this is the symlinks the installation of `hunspell-en-gb` creates on Arch Linux: // - en_AG.aff -> en_GB-large.aff - // - en_BS.aff -> en_GB-large.aff - // - en_BW.aff -> en_GB-large.aff - // - en_BZ.aff -> en_GB-large.aff - // - en_DK.aff -> en_GB-large.aff - // - en_GB.aff -> en_GB-large.aff + // - ... // - en_GB-large.aff - // - en_GH.aff -> en_GB-large.aff - // - en_HK.aff -> en_GB-large.aff - // - en_IE.aff -> en_GB-large.aff - // - en_IN.aff -> en_GB-large.aff - // - en_JM.aff -> en_GB-large.aff - // - en_NA.aff -> en_GB-large.aff - // - en_NG.aff -> en_GB-large.aff - // - en_NZ.aff -> en_GB-large.aff - // - en_SG.aff -> en_GB-large.aff - // - en_TT.aff -> en_GB-large.aff - // - en_ZA.aff -> en_GB-large.aff - // - en_ZW.aff -> en_GB-large.aff continue; } + auto name = [&] -> QString { + if (dict.isSystem) + { + return dict.name % " (System)"; + } + return dict.name; + }(); dictionaries.push_back(DictionaryInfo{ - .name = dict.name % " (System)", + .name = name, .path = dict.path, - .isSymbolicLink = false, + .isSymbolicLink = dict.isSymbolicLink, + .isSystem = dict.isSystem, }); } } + std::ranges::sort(dictionaries, + [](const DictionaryInfo &lhs, const DictionaryInfo &rhs) { + return std::tie(lhs.isSystem, lhs.name, lhs.path) < + std::tie(rhs.isSystem, rhs.name, rhs.path); + }); + return dictionaries; -# else - return {}; -# endif #else return {}; #endif diff --git a/src/controllers/spellcheck/SpellChecker.hpp b/src/controllers/spellcheck/SpellChecker.hpp index 50d0455a..3be39598 100644 --- a/src/controllers/spellcheck/SpellChecker.hpp +++ b/src/controllers/spellcheck/SpellChecker.hpp @@ -19,10 +19,15 @@ struct DictionaryInfo { /// The name of the dictionary to be shown to users (e.g. "en_GB (System)") QString name; - /// The absolute path to the dictionary without the .aff or .dic suffix (e.g. "/foo/bar/en_GB") + /// The path to the dictionary without the .aff or .dic suffix (e.g. "/foo/bar/en_GB" or "en_GB") + /// + /// Paths must be absolute if they're marked as a system dictionary, + /// otherwise they must be relative to the Chatterino Dictionaries + /// directory. QString path; bool isSymbolicLink; + bool isSystem; }; class SpellCheckerPrivate; @@ -37,10 +42,11 @@ public: bool check(const QString &word); std::vector suggestions(const QString &word); - /// Return a list of system-installed dictionaries. + /// Get a list of dictionaries from the Chatterino Dictionaries directory + /// and the system directories if supported. /// - /// Currently only implemented on Linux. - std::vector getSystemDictionaries() const; + /// System-dictionary loading is currently only implemented on Linux. + std::vector getAvailableDictionaries() const; private: std::unique_ptr private_; diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 97a9c38c..7ea15869 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -361,8 +361,8 @@ public: "/behaviour/spellChecking/enabled", false, }; - QStringSetting spellCheckingFallback = { - "/behaviour/spellChecking/systemFallback", + QStringSetting spellCheckingDefaultDictionary = { + "/behaviour/spellChecking/defaultDictionary", "", }; diff --git a/src/widgets/settingspages/ExternalToolsPage.cpp b/src/widgets/settingspages/ExternalToolsPage.cpp index 08a2aabc..102ff912 100644 --- a/src/widgets/settingspages/ExternalToolsPage.cpp +++ b/src/widgets/settingspages/ExternalToolsPage.cpp @@ -248,9 +248,8 @@ void ExternalToolsPage::initLayout(GeneralPageView &layout) "dictionaries in " % formatRichNamedLink(getApp()->getPaths().dictionariesDirectory, getApp()->getPaths().dictionariesDirectory) % - u". The file index.aff has to contain the affixes and " - u"index.dic " - u"must contain the dictionary (subject to change)."); + u". Dictionaries are pairs of .aff (affixes) and .dic (dictionary) " + u"files."); SettingWidget::checkbox("Check spelling by default", s.enableSpellChecking) @@ -268,14 +267,13 @@ void ExternalToolsPage::initLayout(GeneralPageView &layout) std::vector> dictList{{"None", ""}}; std::ranges::transform( - getApp()->getSpellChecker()->getSystemDictionaries(), + getApp()->getSpellChecker()->getAvailableDictionaries(), std::back_inserter(dictList), toItem); if (dictList.size() > 1) { - SettingWidget::dropdown( - "Fallback spellchecking dictionary (requires restart)", - s.spellCheckingFallback, dictList) + SettingWidget::dropdown("Default dictionary (requires restart)", + s.spellCheckingDefaultDictionary, dictList) ->addTo(layout); } }