feat: load local directories for spellchecking (#6730)
Reviewed-by: pajlada <rasmus.karlsson+github@pajlada.com>
This commit is contained in:
+1
-1
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace {
|
||||
|
||||
/// Returns a list of available dictionaries in the given directory
|
||||
std::vector<DictionaryInfo> loadDictionariesFromDirectory(
|
||||
const QDir &searchDirectory)
|
||||
const QDir &searchDirectory, bool isSystem)
|
||||
{
|
||||
std::vector<DictionaryInfo> dictionaries;
|
||||
|
||||
@@ -47,16 +47,34 @@ std::vector<DictionaryInfo> 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> 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<std::string> SpellChecker::suggestions(const QString &word)
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
std::vector<DictionaryInfo> SpellChecker::getSystemDictionaries() const
|
||||
std::vector<DictionaryInfo> SpellChecker::getAvailableDictionaries() const
|
||||
{
|
||||
#ifdef CHATTERINO_WITH_SPELLCHECK
|
||||
std::vector<std::pair<QString, bool>> 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<DictionaryInfo> 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
|
||||
|
||||
@@ -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<std::string> 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<DictionaryInfo> getSystemDictionaries() const;
|
||||
/// System-dictionary loading is currently only implemented on Linux.
|
||||
std::vector<DictionaryInfo> getAvailableDictionaries() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<SpellCheckerPrivate> private_;
|
||||
|
||||
@@ -361,8 +361,8 @@ public:
|
||||
"/behaviour/spellChecking/enabled",
|
||||
false,
|
||||
};
|
||||
QStringSetting spellCheckingFallback = {
|
||||
"/behaviour/spellChecking/systemFallback",
|
||||
QStringSetting spellCheckingDefaultDictionary = {
|
||||
"/behaviour/spellChecking/defaultDictionary",
|
||||
"",
|
||||
};
|
||||
|
||||
|
||||
@@ -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<std::pair<QString, QVariant>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user