init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/platform/linux/language_linux.h"
namespace Platform::Language {
Id Recognize(QStringView text) {
return {};
}
} // namespace Platform::Language

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_language.h"
namespace Platform::Language {
} // namespace Platform::Language

View File

@@ -0,0 +1,251 @@
/* enchant
* Copyright (C) 2003 Dom Lachowicz
*
* 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.
*
* In addition, as a special exception, Dom Lachowicz
* gives permission to link the code of this program with
* non-LGPL Spelling Provider libraries (eg: a MSFT Office
* spell checker backend) and distribute linked combinations including
* the two. You must obey the GNU Lesser General Public License in all
* respects for all of the code used other than said providers. If you modify
* this file, you may extend this exception to your version of the
* file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Nicholas Guriev (email: guriev-ns@ya.ru) split the full <enchant++.h> header
* into two files, linux_enchant.h and linux_enchant.cpp, to use within Desktop
* App Toolkit. He also implemented explicit linking with dlopen/dlsym to avoid
* rigid dependency on the Enchant library at runtime.
*/
#include <enchant.h>
#include "base/platform/linux/base_linux_library.h"
#include "spellcheck/platform/linux/linux_enchant.h"
namespace {
struct {
//decltype (enchant_broker_describe) * broker_describe;
//decltype (enchant_broker_dict_exists) * broker_dict_exists;
decltype (enchant_broker_free) * broker_free;
decltype (enchant_broker_free_dict) * broker_free_dict;
decltype (enchant_broker_get_error) * broker_get_error;
decltype (enchant_broker_init) * broker_init;
decltype (enchant_broker_list_dicts) * broker_list_dicts;
decltype (enchant_broker_request_dict) * broker_request_dict;
//decltype (enchant_broker_request_pwl_dict) * broker_request_pwl_dict;
decltype (enchant_broker_set_ordering) * broker_set_ordering;
decltype (enchant_dict_add) * dict_add;
decltype (enchant_dict_add_to_session) * dict_add_to_session;
decltype (enchant_dict_check) * dict_check;
decltype (enchant_dict_describe) * dict_describe;
decltype (enchant_dict_free_string_list) * dict_free_string_list;
decltype (enchant_dict_get_error) * dict_get_error;
decltype (enchant_dict_is_added) * dict_is_added;
//decltype (enchant_dict_is_removed) * dict_is_removed;
decltype (enchant_dict_remove) * dict_remove;
decltype (enchant_dict_remove_from_session) * dict_remove_from_session;
//decltype (enchant_dict_store_replacement) * dict_store_replacement;
decltype (enchant_dict_suggest) * dict_suggest;
} f_enchant;
} // anonymous namespace
enchant::Exception::Exception (const char * ex)
: std::exception (), m_ex ("") {
if (ex)
m_ex = ex;
}
enchant::Exception::~Exception () = default;
const char * enchant::Exception::what () const noexcept {
return m_ex.c_str();
}
enchant::Dict::Dict (EnchantDict * dict, EnchantBroker * broker)
: m_dict (dict), m_broker (broker) {
f_enchant.dict_describe (m_dict, s_describe_fn, this);
}
enchant::Dict::~Dict () {
f_enchant.broker_free_dict (m_broker, m_dict);
}
bool enchant::Dict::check (const std::string & utf8word) {
int val;
val = f_enchant.dict_check (m_dict, utf8word.c_str(), utf8word.size());
if (val == 0)
return true;
else if (val > 0)
return false;
else {
throw enchant::Exception (f_enchant.dict_get_error (m_dict));
}
return false; // never reached
}
void enchant::Dict::suggest (const std::string & utf8word,
std::vector<std::string> & out_suggestions) {
size_t n_suggs;
char ** suggs;
out_suggestions.clear ();
suggs = f_enchant.dict_suggest (m_dict, utf8word.c_str(),
utf8word.size(), &n_suggs);
if (suggs && n_suggs) {
out_suggestions.reserve(n_suggs);
for (size_t i = 0; i < n_suggs; i++) {
out_suggestions.push_back (suggs[i]);
}
f_enchant.dict_free_string_list (m_dict, suggs);
}
}
void enchant::Dict::add (const std::string & utf8word) {
f_enchant.dict_add (m_dict, utf8word.c_str(), utf8word.size());
}
void enchant::Dict::add_to_session (const std::string & utf8word) {
f_enchant.dict_add_to_session (m_dict, utf8word.c_str(), utf8word.size());
}
bool enchant::Dict::is_added (const std::string & utf8word) {
return f_enchant.dict_is_added (m_dict, utf8word.c_str(),
utf8word.size());
}
void enchant::Dict::remove (const std::string & utf8word) {
f_enchant.dict_remove (m_dict, utf8word.c_str(), utf8word.size());
}
void enchant::Dict::remove_from_session (const std::string & utf8word) {
f_enchant.dict_remove_from_session (m_dict, utf8word.c_str(),
utf8word.size());
}
//bool enchant::Dict::is_removed (const std::string & utf8word) {
// return f_enchant.dict_is_removed (m_dict, utf8word.c_str(),
// utf8word.size());
//}
//void enchant::Dict::store_replacement (const std::string & utf8bad,
// const std::string & utf8good) {
// f_enchant.dict_store_replacement (m_dict,
// utf8bad.c_str(), utf8bad.size(),
// utf8good.c_str(), utf8good.size());
//}
enchant::Broker::Broker ()
: m_broker (f_enchant.broker_init ())
{
}
enchant::Broker::~Broker () {
f_enchant.broker_free (m_broker);
}
enchant::Dict * enchant::Broker::request_dict (const std::string & lang) {
EnchantDict * dict = f_enchant.broker_request_dict (m_broker, lang.c_str());
if (!dict) {
throw enchant::Exception (f_enchant.broker_get_error (m_broker));
return 0; // never reached
}
return new Dict (dict, m_broker);
}
//enchant::Dict * enchant::Broker::request_pwl_dict (const std::string & pwl) {
// EnchantDict * dict = f_enchant.broker_request_pwl_dict (m_broker, pwl.c_str());
//
// if (!dict) {
// throw enchant::Exception (f_enchant.broker_get_error (m_broker));
// return 0; // never reached
// }
//
// return new Dict (dict, m_broker);
//}
//bool enchant::Broker::dict_exists (const std::string & lang) {
// if (f_enchant.broker_dict_exists (m_broker, lang.c_str()))
// return true;
// return false;
//}
void enchant::Broker::set_ordering (const std::string & tag, const std::string & ordering) {
f_enchant.broker_set_ordering (m_broker, tag.c_str(), ordering.c_str());
}
//void enchant::Broker::describe (EnchantBrokerDescribeFn fn, void * user_data) {
// f_enchant.broker_describe (m_broker, fn, user_data);
//}
void enchant::Broker::list_dicts (EnchantDictDescribeFn fn, void * user_data) {
f_enchant.broker_list_dicts (m_broker, fn, user_data);
}
#define GET_SYMBOL_enchant(func_name) \
if (!base::Platform::LoadSymbol (handle, "enchant_" # func_name, f_enchant.func_name)) { \
return false; \
}
bool enchant::loader::do_explicit_linking () {
static enum { NotLoadedYet, LoadSuccessful, LoadFailed = -1 } load_status;
if (load_status == NotLoadedYet) {
load_status = LoadFailed;
const auto handle = base::Platform::LoadLibrary ("libenchant.so.1", RTLD_NODELETE)
?: base::Platform::LoadLibrary ("libenchant-2.so.2", RTLD_NODELETE)
?: base::Platform::LoadLibrary ("libenchant.so.2", RTLD_NODELETE);
if (!handle) {
// logs ?
return false;
}
//GET_SYMBOL_enchant (broker_describe);
//GET_SYMBOL_enchant (broker_dict_exists);
GET_SYMBOL_enchant (broker_free);
GET_SYMBOL_enchant (broker_free_dict);
GET_SYMBOL_enchant (broker_get_error);
GET_SYMBOL_enchant (broker_init);
GET_SYMBOL_enchant (broker_list_dicts);
GET_SYMBOL_enchant (broker_request_dict);
//GET_SYMBOL_enchant (broker_request_pwl_dict);
GET_SYMBOL_enchant (broker_set_ordering);
GET_SYMBOL_enchant (dict_add);
GET_SYMBOL_enchant (dict_add_to_session);
GET_SYMBOL_enchant (dict_check);
GET_SYMBOL_enchant (dict_describe);
GET_SYMBOL_enchant (dict_free_string_list);
GET_SYMBOL_enchant (dict_get_error);
GET_SYMBOL_enchant (dict_is_added);
//GET_SYMBOL_enchant (dict_is_removed);
GET_SYMBOL_enchant (dict_remove);
GET_SYMBOL_enchant (dict_remove_from_session);
//GET_SYMBOL_enchant (dict_store_replacement);
GET_SYMBOL_enchant (dict_suggest);
load_status = LoadSuccessful;
}
return load_status == LoadSuccessful;
}
// vi: ts=8 sw=8

View File

@@ -0,0 +1,201 @@
/* enchant
* Copyright (C) 2003 Dom Lachowicz
*
* 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.
*
* In addition, as a special exception, Dom Lachowicz
* gives permission to link the code of this program with
* non-LGPL Spelling Provider libraries (eg: a MSFT Office
* spell checker backend) and distribute linked combinations including
* the two. You must obey the GNU Lesser General Public License in all
* respects for all of the code used other than said providers. If you modify
* this file, you may extend this exception to your version of the
* file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Nicholas Guriev (email: guriev-ns@ya.ru) split the full <enchant++.h> header
* into two files, linux_enchant.h and linux_enchant.cpp, to use within Desktop
* App Toolkit. He also implemented explicit linking with dlopen/dlsym to avoid
* rigid dependency on the Enchant library at runtime.
*/
#pragma once
#include <string>
#include <vector>
#include <exception>
#ifndef ENCHANT_H
typedef struct str_enchant_broker EnchantBroker;
typedef struct str_enchant_dict EnchantDict;
/**
* EnchantBrokerDescribeFn
* @provider_name: The provider's identifier, such as "ispell" or "aspell" in UTF8 encoding
* @provider_desc: A description of the provider, such as "Aspell 0.53" in UTF8 encoding
* @provider_dll_file: The provider's DLL filename in Glib file encoding (UTF8 on Windows)
* @user_data: Supplied user data, or %null if you don't care
*
* Callback used to enumerate and describe Enchant's various providers
*/
typedef void (*EnchantBrokerDescribeFn) (const char * const provider_name,
const char * const provider_desc,
const char * const provider_dll_file,
void * user_data);
/**
* EnchantDictDescribeFn
* @lang_tag: The dictionary's language tag (eg: en_US, de_AT, ...)
* @provider_name: The provider's name (eg: Aspell) in UTF8 encoding
* @provider_desc: The provider's description (eg: Aspell 0.50.3) in UTF8 encoding
* @provider_file: The DLL/SO where this dict's provider was loaded from in Glib file encoding (UTF8 on Windows)
* @user_data: Supplied user data, or %null if you don't care
*
* Callback used to describe an individual dictionary
*/
typedef void (*EnchantDictDescribeFn) (const char * const lang_tag,
const char * const provider_name,
const char * const provider_desc,
const char * const provider_file,
void * user_data);
#endif // !ENCHANT_H
namespace enchant
{
class Broker;
class Exception : public std::exception
{
public:
explicit Exception (const char * ex);
virtual ~Exception () noexcept;
virtual const char * what () const noexcept;
private:
std::string m_ex;
}; // class enchant::Exception
class Dict
{
friend class enchant::Broker;
public:
~Dict ();
bool check (const std::string & utf8word);
void suggest (const std::string & utf8word,
std::vector<std::string> & out_suggestions);
std::vector<std::string> suggest (const std::string & utf8word) {
std::vector<std::string> result;
suggest (utf8word, result);
return result;
}
void add (const std::string & utf8word);
void add_to_session (const std::string & utf8word);
bool is_added (const std::string & utf8word);
void remove (const std::string & utf8word);
void remove_from_session (const std::string & utf8word);
bool is_removed (const std::string & utf8word);
void store_replacement (const std::string & utf8bad,
const std::string & utf8good);
const std::string & get_lang () const {
return m_lang;
}
const std::string & get_provider_name () const {
return m_provider_name;
}
const std::string & get_provider_desc () const {
return m_provider_desc;
}
const std::string & get_provider_file () const {
return m_provider_file;
}
private:
// space reserved for API/ABI expansion
void * _private[5];
static void s_describe_fn (const char * const lang,
const char * const provider_name,
const char * const provider_desc,
const char * const provider_file,
void * user_data) {
enchant::Dict * dict = static_cast<enchant::Dict *> (user_data);
dict->m_lang = lang;
dict->m_provider_name = provider_name;
dict->m_provider_desc = provider_desc;
dict->m_provider_file = provider_file;
}
Dict (EnchantDict * dict, EnchantBroker * broker);
// private, unimplemented
Dict () = delete;
Dict (const Dict & rhs) = delete;
Dict& operator=(const Dict & rhs) = delete;
EnchantDict * m_dict;
EnchantBroker * m_broker;
std::string m_lang;
std::string m_provider_name;
std::string m_provider_desc;
std::string m_provider_file;
}; // class enchant::Dict
class Broker
{
public:
Broker ();
~Broker ();
Dict * request_dict (const std::string & lang);
Dict * request_pwl_dict (const std::string & pwl);
bool dict_exists (const std::string & lang);
void set_ordering (const std::string & tag, const std::string & ordering);
void describe (EnchantBrokerDescribeFn fn, void * user_data = nullptr);
void list_dicts (EnchantDictDescribeFn fn, void * user_data = nullptr);
private:
// space reserved for API/ABI expansion
void * _private[5];
// not implemented
Broker (const Broker & rhs) = delete;
Broker& operator=(const Broker & rhs) = delete;
EnchantBroker * m_broker;
}; // class enchant::Broker
namespace loader {
bool do_explicit_linking ();
} // loader subnamespace
} // enchant namespace
// vi: ts=8 sw=8

View File

@@ -0,0 +1,289 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// Author: Nicholas Guriev <guriev-ns@ya.ru>, public domain, 2019
// License: CC0, https://creativecommons.org/publicdomain/zero/1.0/legalcode
#include <set>
#include <QLocale>
#include "spellcheck/platform/linux/linux_enchant.h"
#include "spellcheck/platform/linux/spellcheck_linux.h"
#include "base/debug_log.h"
namespace Platform::Spellchecker {
namespace {
constexpr auto kHspell = "hspell";
constexpr auto kMySpell = "myspell";
constexpr auto kHunspell = "hunspell";
constexpr auto kOrdering = "hspell,aspell,hunspell,myspell";
constexpr auto kMaxValidators = 10;
constexpr auto kMaxMySpellCount = 3;
constexpr auto kMaxWordLength = 15;
using DictPtr = std::unique_ptr<enchant::Dict>;
auto CheckProvider(DictPtr &validator, const std::string &provider) {
auto p = validator->get_provider_name();
std::transform(begin(p), end(p), begin(p), ::tolower);
return (p.find(provider) == 0); // startsWith.
}
auto IsHebrew(const QString &word) {
// Words with mixed scripts will be automatically ignored,
// so this check should be fine.
return ::Spellchecker::WordScript(word) == QChar::Script_Hebrew;
}
class EnchantSpellChecker {
public:
auto knownLanguages();
bool checkSpelling(const QString &word);
auto findSuggestions(const QString &word);
void addWord(const QString &wordToAdd);
void ignoreWord(const QString &word);
void removeWord(const QString &word);
bool isWordInDictionary(const QString &word);
static EnchantSpellChecker *instance();
private:
EnchantSpellChecker();
EnchantSpellChecker(const EnchantSpellChecker&) = delete;
EnchantSpellChecker& operator =(const EnchantSpellChecker&) = delete;
std::unique_ptr<enchant::Broker> _brokerHandle;
std::vector<DictPtr> _validators;
std::vector<not_null<enchant::Dict*>> _hspells;
};
EnchantSpellChecker::EnchantSpellChecker() {
if (!enchant::loader::do_explicit_linking()) return;
std::set<std::string> langs;
_brokerHandle = std::make_unique<enchant::Broker>();
_brokerHandle->list_dicts([](
const char *language,
const char *provider,
const char *description,
const char *filename,
void *our_payload) {
static_cast<decltype(langs)*>(our_payload)->insert(language);
}, &langs);
_validators.reserve(langs.size());
try {
std::string langTag = QLocale::system().name().toStdString();
_brokerHandle->set_ordering(langTag, kOrdering);
_validators.push_back(DictPtr(_brokerHandle->request_dict(langTag)));
langs.erase(langTag);
} catch (const enchant::Exception &e) {
// no first dictionary found
}
auto mySpellCount = 0;
for (const std::string &language : langs) {
try {
_brokerHandle->set_ordering(language, kOrdering);
auto validator = DictPtr(_brokerHandle->request_dict(language));
if (!validator) {
continue;
}
if (CheckProvider(validator, kHspell)) {
_hspells.push_back(validator.get());
}
if (CheckProvider(validator, kMySpell)
|| CheckProvider(validator, kHunspell)) {
if (mySpellCount > kMaxMySpellCount) {
continue;
} else {
mySpellCount++;
}
}
_validators.push_back(std::move(validator));
if (_validators.size() > kMaxValidators) {
break;
}
} catch (const enchant::Exception &e) {
DEBUG_LOG(("Catch after request_dict: %1").arg(e.what()));
}
}
}
EnchantSpellChecker *EnchantSpellChecker::instance() {
static EnchantSpellChecker capsule;
return &capsule;
}
auto EnchantSpellChecker::knownLanguages() {
return _validators | ranges::views::transform([](const auto &validator) {
return QString(validator->get_lang().c_str());
}) | ranges::to_vector;
}
bool EnchantSpellChecker::checkSpelling(const QString &word) {
auto w = word.toStdString();
const auto checkWord = [&](const auto &validator, auto w) {
try {
return validator->check(w);
} catch (const enchant::Exception &e) {
DEBUG_LOG(("Catch after check '%1': %2").arg(word, e.what()));
return true;
}
};
if (IsHebrew(word) && _hspells.size()) {
return ranges::any_of(_hspells, [&](const auto &validator) {
return checkWord(validator, w);
});
}
return ranges::any_of(_validators, [&](const auto &validator) {
// Hspell is the spell checker that only checks words in Hebrew.
// It returns 'true' for any non-Hebrew word,
// so we should skip Hspell if a word is not in Hebrew.
if (ranges::any_of(_hspells, [&](auto &v) {
return v == validator.get();
})) {
return false;
}
if (validator->get_lang().find("uk") == 0) {
return false;
}
return checkWord(validator, w);
}) || _validators.empty();
}
auto EnchantSpellChecker::findSuggestions(const QString &word) {
const auto wordScript = ::Spellchecker::WordScript(word);
auto w = word.toStdString();
std::vector<QString> result;
if (!_validators.size()) {
return result;
}
const auto convertSuggestions = [&](auto suggestions) {
for (const auto &replacement : suggestions) {
if (result.size() >= kMaxSuggestions) {
break;
}
if (!replacement.empty()) {
result.push_back(replacement.c_str());
}
}
};
if (word.size() >= kMaxWordLength) {
// The first element is the validator of the system language.
auto *v = _validators[0].get();
const auto lang = QString::fromStdString(v->get_lang());
if (wordScript == ::Spellchecker::LocaleToScriptCode(lang)) {
convertSuggestions(v->suggest(w));
}
return result;
}
if (IsHebrew(word) && _hspells.size()) {
for (const auto &h : _hspells) {
convertSuggestions(h->suggest(w));
if (result.size()) {
return result;
}
}
}
for (const auto &validator : _validators) {
const auto lang = QString::fromStdString(validator->get_lang());
if (wordScript != ::Spellchecker::LocaleToScriptCode(lang)) {
continue;
}
convertSuggestions(validator->suggest(w));
if (!result.empty()) {
break;
}
}
return result;
}
void EnchantSpellChecker::addWord(const QString &wordToAdd) {
auto word = wordToAdd.toStdString();
auto &&first = _validators.at(0);
first->add(word);
first->add_to_session(word);
}
void EnchantSpellChecker::ignoreWord(const QString &word) {
_validators.at(0)->add_to_session(word.toStdString());
}
void EnchantSpellChecker::removeWord(const QString &word) {
auto w = word.toStdString();
for (const auto &validator : _validators) {
validator->remove_from_session(w);
validator->remove(w);
}
}
bool EnchantSpellChecker::isWordInDictionary(const QString &word) {
auto w = word.toStdString();
return ranges::any_of(_validators, [&w](const auto &validator) {
return validator->is_added(w);
});
}
} // namespace
void Init() {
}
std::vector<QString> ActiveLanguages() {
return EnchantSpellChecker::instance()->knownLanguages();
}
void UpdateLanguages(std::vector<int> languages) {
::Spellchecker::UpdateSupportedScripts(ActiveLanguages());
crl::async([=] {
const auto result = ActiveLanguages();
crl::on_main([=] {
::Spellchecker::UpdateSupportedScripts(result);
});
});
}
bool CheckSpelling(const QString &wordToCheck) {
return EnchantSpellChecker::instance()->checkSpelling(wordToCheck);
}
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *variants) {
*variants = EnchantSpellChecker::instance()->findSuggestions(wrongWord);
}
void AddWord(const QString &word) {
EnchantSpellChecker::instance()->addWord(word);
}
void RemoveWord(const QString &word) {
EnchantSpellChecker::instance()->removeWord(word);
}
void IgnoreWord(const QString &word) {
EnchantSpellChecker::instance()->ignoreWord(word);
}
bool IsWordInDictionary(const QString &wordToCheck) {
return EnchantSpellChecker::instance()->isWordInDictionary(wordToCheck);
}
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords) {
*misspelledWords = ::Spellchecker::RangesFromText(
text,
::Spellchecker::CheckSkipAndSpell);
}
bool IsSystemSpellchecker() {
return true;
}
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_spellcheck.h"
namespace Platform::Spellchecker {
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_language.h"
namespace Platform::Language {
} // namespace Platform::Language

View File

@@ -0,0 +1,46 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/platform/mac/language_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#import <NaturalLanguage/NLLanguageRecognizer.h>
using Platform::Q2NSString;
using Platform::NS2QString;
namespace Platform::Language {
LanguageId Recognize(QStringView text) {
if (@available(macOS 10.14, *)) {
constexpr auto kMaxHypotheses = 3;
static thread_local auto r = [] {
return [[NLLanguageRecognizer alloc] init];
}();
[r processString:Q2NSString(text)];
const auto hypotheses =
[r languageHypothesesWithMaximum:kMaxHypotheses];
[r reset];
auto maxProbability = 0.;
auto language = NLLanguage(nil);
for (NLLanguage lang in hypotheses) {
const auto probability = [hypotheses[lang] floatValue];
if (probability > maxProbability) {
maxProbability = probability;
language = lang;
}
}
if (language) {
return { QLocale(NS2QString(language)).language() };
}
}
return {};
}
} // namespace Platform::Language

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_spellcheck.h"
namespace Platform::Spellchecker {
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,219 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/platform/mac/spellcheck_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "spellcheck/third_party/language_cld3.h"
#import <AppKit/NSSpellChecker.h>
#import <QuartzCore/QuartzCore.h>
#include <QtCore/QLocale>
#include <set>
using Platform::Q2NSString;
using Platform::NS2QString;
namespace {
// +[NSSpellChecker sharedSpellChecker] can throw exceptions depending
// on the state of the pasteboard, or possibly as a result of
// third-party code (when setting up services entries). The following
// receives nil if an exception is thrown, in which case
// spell-checking will not work, but it also will not crash the
// browser.
NSSpellChecker *SharedSpellChecker() {
@try {
return [NSSpellChecker sharedSpellChecker];
} @catch (id exception) {
return nil;
}
}
inline auto SystemLanguages() {
static auto languages = std::vector<QString>();
if (!languages.size()) {
const auto uiLanguages = QLocale::system().uiLanguages();
languages = (
uiLanguages
) | ranges::views::transform([&](const auto &lang) {
return lang.left(std::max(lang.indexOf('_'), lang.indexOf('-')));
}) | ranges::views::unique | ranges::to_vector;
}
return languages;
}
[[nodiscard]] auto RegionalVariantMap() {
static auto map = std::map<QString, QString>();
static auto initialized = false;
if (!initialized) {
initialized = true;
const auto uiLanguages = QLocale::system().uiLanguages();
for (const auto &ui : uiLanguages) {
if (ui == u"en-AU"_q || ui == u"en_AU"_q) {
map[u"en"_q] = ui;
} else if (ui == u"en-GB"_q || ui == u"en_GB"_q) {
if (!map.contains(u"en"_q)) {
map[u"en"_q] = ui;
}
} else if (ui == u"en-CA"_q || ui == u"en_CA"_q) {
if (!map.contains(u"en"_q)) {
map[u"en"_q] = ui;
}
} else if (ui == u"pt-BR"_q || ui == u"pt_BR"_q) {
map[u"pt"_q] = ui;
}
}
}
return map;
}
[[nodiscard]] auto PreferredRegionalVariant(const QString &lang) {
const auto &map = RegionalVariantMap();
if (const auto it = map.find(lang); it != map.end()) {
return it->second;
}
return lang;
}
} // namespace
namespace Platform::Spellchecker {
void Init() {
}
std::vector<QString> ActiveLanguages() {
return SystemLanguages();
}
bool CheckSpelling(const QString &word) {
if (@available(macOS 10.14, *)) {
const auto lang = PreferredRegionalVariant(
Language::Recognize(word).twoLetterCode());
return [SharedSpellChecker()
checkSpellingOfString:Q2NSString(word)
startingAt:0
language:Q2NSString(lang)
wrap:false
inSpellDocumentWithTag:0
wordCount:nil].location == NSNotFound;
}
const auto wordLength = word.length();
NSArray<NSTextCheckingResult*> *spellRanges =
[SharedSpellChecker()
checkString:Q2NSString(word)
range:NSMakeRange(0, wordLength)
types:NSTextCheckingTypeSpelling
options:nil
inSpellDocumentWithTag:0
orthography:nil
wordCount:nil];
// If the length of the misspelled word == 0,
// then there is no misspelled word.
return (spellRanges.count == 0);
}
// There's no need to check the language on the Mac.
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords) {
// Probably never gonna be defined.
#ifdef SPELLCHECKER_MAC_AUTO_CHECK_TEXT
NSArray<NSTextCheckingResult*> *spellRanges =
[SharedSpellChecker()
checkString:Q2NSString(text)
range:NSMakeRange(0, text.length())
types:NSTextCheckingTypeSpelling
options:nil
inSpellDocumentWithTag:0
orthography:nil
wordCount:nil];
misspelledWords->reserve(spellRanges.count);
for (NSTextCheckingResult *result in spellRanges) {
if (result.resultType != NSTextCheckingTypeSpelling) {
continue;
}
misspelledWords->push_back({
result.range.location,
result.range.length});
}
#else
// Known Issue: Despite the explicitly defined parameter,
// the correctness of a single word depends on the rest of the text.
// For example, "testt testtttyy" - this string will be marked as correct.
// But at the same time "testtttyy" will be marked as misspelled word.
// So we have to manually split the text into words and check them separately.
*misspelledWords = ::Spellchecker::RangesFromText(
text,
::Spellchecker::CheckSkipAndSpell);
#endif
}
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions) {
const auto wordRange = NSMakeRange(0, wrongWord.length());
auto *nsWord = Q2NSString(wrongWord);
const auto guesses = [&](auto *lang) {
return [SharedSpellChecker() guessesForWordRange:wordRange
inString:nsWord
language:lang
inSpellDocumentWithTag:0];
};
auto wordCounter = 0;
const auto wordScript = ::Spellchecker::WordScript(wrongWord);
optionalSuggestions->reserve(kMaxSuggestions);
// for (NSString *lang in [SharedSpellChecker() availableLanguages]) {
for (const auto &lang : SystemLanguages()) {
if (wordScript != ::Spellchecker::LocaleToScriptCode(lang)) {
continue;
}
for (NSString *guess in guesses(Q2NSString(lang))) {
optionalSuggestions->push_back(NS2QString(guess));
if (++wordCounter >= kMaxSuggestions) {
return;
}
}
}
}
void AddWord(const QString &word) {
[SharedSpellChecker() learnWord:Q2NSString(word)];
}
void RemoveWord(const QString &word) {
[SharedSpellChecker() unlearnWord:Q2NSString(word)];
}
void IgnoreWord(const QString &word) {
[SharedSpellChecker() ignoreWord:Q2NSString(word)
inSpellDocumentWithTag:0];
}
bool IsWordInDictionary(const QString &wordToCheck) {
return [SharedSpellChecker() hasLearnedWord:Q2NSString(wordToCheck)];
}
bool IsSystemSpellchecker() {
return true;
}
void UpdateLanguages(std::vector<int> languages) {
::Spellchecker::UpdateSupportedScripts(SystemLanguages());
}
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/spellcheck_types.h"
namespace Platform::Language {
[[nodiscard]] LanguageId Recognize(QStringView text);
} // namespace Platform::Language

View File

@@ -0,0 +1,36 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/spellcheck_types.h"
#include "spellcheck/spellcheck_utils.h"
namespace Platform::Spellchecker {
constexpr auto kMaxSuggestions = 5;
[[nodiscard]] bool IsSystemSpellchecker();
[[nodiscard]] bool CheckSpelling(const QString &wordToCheck);
[[nodiscard]] bool IsWordInDictionary(const QString &wordToCheck);
void Init();
std::vector<QString> ActiveLanguages();
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions);
void AddWord(const QString &word);
void RemoveWord(const QString &word);
void IgnoreWord(const QString &word);
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords);
void UpdateLanguages(std::vector<int> languages);
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,143 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/platform/win/language_win.h"
#include "base/platform/win/base_windows_safe_library.h"
#include <Windows.h>
#include <ElsCore.h>
#include <ElsSrvc.h> // ELS_GUID_LANGUAGE_DETECTION.
namespace Platform::Language {
namespace {
HRESULT (__stdcall *MappingGetServices)(
_In_opt_ PMAPPING_ENUM_OPTIONS pOptions,
_Out_ PMAPPING_SERVICE_INFO *prgServices,
_Out_ DWORD *pdwServicesCount
);
HRESULT (__stdcall *MappingFreeServices)(
_In_ PMAPPING_SERVICE_INFO pServiceInfo
);
HRESULT (__stdcall *MappingRecognizeText)(
_In_ PMAPPING_SERVICE_INFO pServiceInfo,
_In_reads_(dwLength) LPCWSTR pszText,
_In_ DWORD dwLength,
_In_ DWORD dwIndex,
_In_opt_ PMAPPING_OPTIONS pOptions,
_Inout_ PMAPPING_PROPERTY_BAG pbag
);
HRESULT (__stdcall *MappingFreePropertyBag)(
_In_ PMAPPING_PROPERTY_BAG pBag
);
[[nodiscard]] inline bool Supported() {
static const auto Result = [] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto els = base::Platform::SafeLoadLibrary(L"elscore.dll");
return LOAD_SYMBOL(els, MappingGetServices)
&& LOAD_SYMBOL(els, MappingRecognizeText)
&& LOAD_SYMBOL(els, MappingFreeServices)
&& LOAD_SYMBOL(els, MappingFreePropertyBag);
#undef LOAD_SYMBOL
}();
return Result;
}
struct unique_services final {
operator MAPPING_SERVICE_INFO *() {
return services;
}
MAPPING_SERVICE_INFO **operator&() {
return &services;
}
MAPPING_SERVICE_INFO *services = nullptr;
unique_services() = default;
unique_services(unique_services const &other) = delete;
~unique_services() {
if (services) {
MappingFreeServices(services);
}
}
};
struct unique_bag final : public MAPPING_PROPERTY_BAG {
unique_bag() : MAPPING_PROPERTY_BAG{} {
}
unique_bag(unique_bag const &other) = delete;
~unique_bag() {
MappingFreePropertyBag(this);
}
};
inline void MappingRecognizeTextFromService(
REFGUID service,
LPCWSTR text,
DWORD length,
unique_bag &bag) {
auto options = MAPPING_ENUM_OPTIONS{};
options.Size = sizeof(options);
options.pGuid = const_cast<GUID*>(&service);
auto dwServicesCount = DWORD(0);
auto services = unique_services();
const auto hr = MappingGetServices(&options, &services, &dwServicesCount);
if (FAILED(hr)) {
return;
}
bag.Size = sizeof(bag);
MappingRecognizeText(services, text, length, 0, nullptr, &bag);
}
} // namespace
void RecognizeTextLanguages(
LPCWSTR text,
DWORD length,
Fn<void(LPCWSTR, int)> &&callback) {
if (!length) {
return;
}
auto bag = unique_bag();
MappingRecognizeTextFromService(
ELS_GUID_LANGUAGE_DETECTION,
text,
length,
bag);
auto pos = reinterpret_cast<LPCWSTR>(bag.prgResultRanges[0].pData);
for (; *pos;) {
const auto len = wcslen(pos);
if (len >= 2) {
callback(pos, len);
}
pos += (len + 1);
}
}
Id Recognize(QStringView text) {
if (Supported()) {
auto locales = std::vector<QLocale>();
RecognizeTextLanguages(
(LPCWSTR)text.utf16(),
DWORD(text.size()),
[&](LPCWSTR r, int length) {
// Cut complex result, e.g. "sr-Cyrl".
locales.emplace_back(QString::fromWCharArray(r, 2));
});
return { locales[0].language() };
}
return {};
}
} // namespace Platform::Language

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_language.h"
namespace Platform::Language {
} // namespace Platform::Language

View File

@@ -0,0 +1,443 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/platform/win/spellcheck_win.h"
#include "base/platform/base_platform_info.h"
#include "spellcheck/third_party/hunspell_controller.h"
#include <wrl/client.h>
#include <spellcheck.h>
#include <QtCore/QDir>
#include <QtCore/QLocale>
#include <QVector>
using namespace Microsoft::WRL;
namespace Platform::Spellchecker {
namespace {
constexpr auto kChunk = 5000;
// Seems like ISpellChecker API has bugs for Persian language (aka Farsi).
[[nodiscard]] inline bool IsPersianLanguage(const QString &langTag) {
return langTag.startsWith(QStringLiteral("fa"));
}
[[nodiscard]] inline LPCWSTR Q2WString(QStringView string) {
return (LPCWSTR)string.utf16();
}
[[nodiscard]] inline auto SystemLanguages() {
const auto appdata = qEnvironmentVariable("appdata");
const auto dir = QDir(appdata + QString("\\Microsoft\\Spelling"));
auto list = QStringList(SystemLanguage());
list << (dir.exists()
? dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)
: QLocale::system().uiLanguages());
list.removeDuplicates();
return list | ranges::to_vector;
}
// WindowsSpellChecker class is used to store all the COM objects and
// control their lifetime. The class also provides wrappers for
// ISpellCheckerFactory and ISpellChecker APIs. All COM calls are on the
// background thread.
class WindowsSpellChecker {
public:
WindowsSpellChecker();
void addWord(LPCWSTR word);
void removeWord(LPCWSTR word);
void ignoreWord(LPCWSTR word);
[[nodiscard]] bool checkSpelling(LPCWSTR word);
void fillSuggestionList(
LPCWSTR wrongWord,
std::vector<QString> *optionalSuggestions);
void checkSpellingText(
LPCWSTR text,
MisspelledWords *misspelledWordRanges,
int offset);
[[nodiscard]] std::vector<QString> systemLanguages();
void chunkedCheckSpellingText(
QStringView textView,
MisspelledWords *misspelledWords);
private:
void createFactory();
[[nodiscard]] bool isLanguageSupported(const LPCWSTR &lang);
void createSpellCheckers();
ComPtr<ISpellCheckerFactory> _spellcheckerFactory;
std::vector<std::pair<QString, ComPtr<ISpellChecker>>> _spellcheckerMap;
};
WindowsSpellChecker::WindowsSpellChecker() {
createFactory();
createSpellCheckers();
}
void WindowsSpellChecker::createFactory() {
if (FAILED(CoCreateInstance(
__uuidof(SpellCheckerFactory),
nullptr,
(CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER),
IID_PPV_ARGS(&_spellcheckerFactory)))) {
_spellcheckerFactory = nullptr;
}
}
void WindowsSpellChecker::createSpellCheckers() {
if (!_spellcheckerFactory) {
return;
}
for (const auto &lang : SystemLanguages()) {
const auto wlang = Q2WString(lang);
if (!isLanguageSupported(wlang)) {
continue;
}
if (ranges::contains(ranges::views::keys(_spellcheckerMap), lang)) {
continue;
}
auto spellchecker = ComPtr<ISpellChecker>();
auto hr = _spellcheckerFactory->CreateSpellChecker(
wlang,
&spellchecker);
if (SUCCEEDED(hr)) {
_spellcheckerMap.push_back({ lang, spellchecker });
}
}
}
bool WindowsSpellChecker::isLanguageSupported(const LPCWSTR &lang) {
if (!_spellcheckerFactory) {
return false;
}
auto isSupported = (BOOL)false;
auto hr = _spellcheckerFactory->IsSupported(lang, &isSupported);
return SUCCEEDED(hr) && isSupported;
}
void WindowsSpellChecker::fillSuggestionList(
LPCWSTR wrongWord,
std::vector<QString> *optionalSuggestions) {
auto i = 0;
for (const auto &[langTag, spellchecker] : _spellcheckerMap) {
if (IsPersianLanguage(langTag)) {
continue;
}
auto suggestions = ComPtr<IEnumString>();
auto hr = spellchecker->Suggest(wrongWord, &suggestions);
if (hr != S_OK) {
continue;
}
while (true) {
wchar_t *suggestion = nullptr;
hr = suggestions->Next(1, &suggestion, nullptr);
if (hr != S_OK) {
break;
}
const auto guess = QString::fromWCharArray(
suggestion,
wcslen(suggestion));
CoTaskMemFree(suggestion);
if (!guess.isEmpty()) {
optionalSuggestions->push_back(guess);
if (++i >= kMaxSuggestions) {
return;
}
}
}
}
}
bool WindowsSpellChecker::checkSpelling(LPCWSTR word) {
for (const auto &[_, spellchecker] : _spellcheckerMap) {
auto spellingErrors = ComPtr<IEnumSpellingError>();
auto hr = spellchecker->Check(word, &spellingErrors);
if (SUCCEEDED(hr) && spellingErrors) {
auto spellingError = ComPtr<ISpellingError>();
auto startIndex = ULONG(0);
auto errorLength = ULONG(0);
auto action = CORRECTIVE_ACTION_NONE;
hr = spellingErrors->Next(&spellingError);
if (SUCCEEDED(hr) &&
spellingError &&
SUCCEEDED(spellingError->get_StartIndex(&startIndex)) &&
SUCCEEDED(spellingError->get_Length(&errorLength)) &&
SUCCEEDED(spellingError->get_CorrectiveAction(&action)) &&
(action == CORRECTIVE_ACTION_GET_SUGGESTIONS ||
action == CORRECTIVE_ACTION_REPLACE)) {
} else {
return true;
}
}
}
return false;
}
void WindowsSpellChecker::checkSpellingText(
LPCWSTR text,
MisspelledWords *misspelledWordRanges,
int offset) {
// The spellchecker marks words not from its own language as misspelled.
// So we only return words that are marked
// as misspelled in all spellcheckers.
auto misspelledWords = MisspelledWords();
constexpr auto isActionGood = [](auto action) {
return action == CORRECTIVE_ACTION_GET_SUGGESTIONS
|| action == CORRECTIVE_ACTION_REPLACE;
};
for (const auto &[langTag, spellchecker] : _spellcheckerMap) {
auto spellingErrors = ComPtr<IEnumSpellingError>();
auto hr = IsPersianLanguage(langTag)
? spellchecker->Check(text, &spellingErrors)
: spellchecker->ComprehensiveCheck(text, &spellingErrors);
if (!(SUCCEEDED(hr) && spellingErrors)) {
continue;
}
auto tempMisspelled = MisspelledWords();
auto spellingError = ComPtr<ISpellingError>();
for (; hr == S_OK; hr = spellingErrors->Next(&spellingError)) {
auto startIndex = ULONG(0);
auto errorLength = ULONG(0);
auto action = CORRECTIVE_ACTION_NONE;
if (!(SUCCEEDED(hr)
&& spellingError
&& SUCCEEDED(spellingError->get_StartIndex(&startIndex))
&& SUCCEEDED(spellingError->get_Length(&errorLength))
&& SUCCEEDED(spellingError->get_CorrectiveAction(&action))
&& isActionGood(action))) {
continue;
}
const auto word = std::pair(
(int)startIndex + offset,
(int)errorLength);
if (misspelledWords.empty()
|| ranges::contains(misspelledWords, word)) {
tempMisspelled.push_back(std::move(word));
}
}
// If the tempMisspelled vector is empty at least once,
// it means that the all words will be correct in the end
// and it makes no sense to check other languages.
if (tempMisspelled.empty()) {
return;
}
misspelledWords = std::move(tempMisspelled);
}
if (offset) {
for (auto &m : misspelledWords) {
misspelledWordRanges->push_back(std::move(m));
}
} else {
*misspelledWordRanges = misspelledWords;
}
}
void WindowsSpellChecker::addWord(LPCWSTR word) {
for (const auto &[_, spellchecker] : _spellcheckerMap) {
spellchecker->Add(word);
}
}
void WindowsSpellChecker::removeWord(LPCWSTR word) {
for (const auto &[_, spellchecker] : _spellcheckerMap) {
auto spellchecker2 = ComPtr<ISpellChecker2>();
spellchecker->QueryInterface(IID_PPV_ARGS(&spellchecker2));
if (spellchecker2) {
spellchecker2->Remove(word);
}
}
}
void WindowsSpellChecker::ignoreWord(LPCWSTR word) {
for (const auto &[_, spellchecker] : _spellcheckerMap) {
spellchecker->Ignore(word);
}
}
std::vector<QString> WindowsSpellChecker::systemLanguages() {
return ranges::views::keys(_spellcheckerMap) | ranges::to_vector;
}
void WindowsSpellChecker::chunkedCheckSpellingText(
QStringView textView,
MisspelledWords *misspelledWords) {
auto i = 0;
auto chunkBuffer = std::vector<wchar_t>();
while (i != textView.size()) {
const auto provisionalChunkSize = std::min(
kChunk,
int(textView.size() - i));
const auto chunkSize = [&] {
const auto until = std::max(
0,
provisionalChunkSize - ::Spellchecker::kMaxWordSize);
for (auto n = provisionalChunkSize; n > until; n--) {
if (textView.at(i + n - 1).isLetterOrNumber()) {
continue;
} else {
return n;
}
}
return provisionalChunkSize;
}();
const auto chunk = textView.mid(i, chunkSize);
chunkBuffer.resize(chunk.size() + 1);
const auto count = chunk.toWCharArray(chunkBuffer.data());
chunkBuffer[count] = '\0';
checkSpellingText(
(LPCWSTR)chunkBuffer.data(),
misspelledWords,
i);
i += chunk.size();
}
}
////// End of WindowsSpellChecker class.
WindowsSpellChecker &SharedSpellChecker() {
static auto spellchecker = WindowsSpellChecker();
return spellchecker;
}
} // namespace
// TODO: Add a better work with the Threading Models.
// All COM objects should be created asynchronously
// if we want to work with them asynchronously.
// Some calls can be made in the main thread before spellchecking
// (e.g. KnownLanguages), so we have to init it asynchronously first.
void Init() {
if (IsSystemSpellchecker()) {
crl::async(SharedSpellChecker);
}
}
bool IsSystemSpellchecker() {
// Windows 7 does not support spellchecking.
// https://docs.microsoft.com/en-us/windows/win32/api/spellcheck/nn-spellcheck-ispellchecker
return IsWindows8OrGreater();
}
std::vector<QString> ActiveLanguages() {
if (IsSystemSpellchecker()) {
return SharedSpellChecker().systemLanguages();
}
return ThirdParty::ActiveLanguages();
}
bool CheckSpelling(const QString &wordToCheck) {
if (!IsSystemSpellchecker()) {
return ThirdParty::CheckSpelling(wordToCheck);
}
return SharedSpellChecker().checkSpelling(Q2WString(wordToCheck));
}
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions) {
if (IsSystemSpellchecker()) {
SharedSpellChecker().fillSuggestionList(
Q2WString(wrongWord),
optionalSuggestions);
return;
}
ThirdParty::FillSuggestionList(
wrongWord,
optionalSuggestions);
}
void AddWord(const QString &word) {
if (IsSystemSpellchecker()) {
SharedSpellChecker().addWord(Q2WString(word));
} else {
ThirdParty::AddWord(word);
}
}
void RemoveWord(const QString &word) {
if (IsSystemSpellchecker()) {
SharedSpellChecker().removeWord(Q2WString(word));
} else {
ThirdParty::RemoveWord(word);
}
}
void IgnoreWord(const QString &word) {
if (IsSystemSpellchecker()) {
SharedSpellChecker().ignoreWord(Q2WString(word));
} else {
ThirdParty::IgnoreWord(word);
}
}
bool IsWordInDictionary(const QString &wordToCheck) {
if (IsSystemSpellchecker()) {
// ISpellChecker can't check if a word is in the dictionary.
return false;
}
return ThirdParty::IsWordInDictionary(wordToCheck);
}
void UpdateLanguages(std::vector<int> languages) {
if (!IsSystemSpellchecker()) {
ThirdParty::UpdateLanguages(languages);
return;
}
crl::async([=] {
const auto result = ActiveLanguages();
crl::on_main([=] {
::Spellchecker::UpdateSupportedScripts(result);
});
});
}
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords) {
if (IsSystemSpellchecker()) {
// There are certain strings with a lot of 'paragraph separators'
// that crash the native Windows spellchecker. We replace them
// with spaces (no difference for the checking), they don't crash.
const auto check = QString(text).replace(QChar(8233), QChar(32));
if (check.size() > kChunk) {
// On some versions of Windows 10,
// checking large text with specific characters (e.g. @)
// will throw the std::regex_error::error_complexity exception,
// so we have to split the text.
SharedSpellChecker().chunkedCheckSpellingText(
check,
misspelledWords);
} else {
SharedSpellChecker().checkSpellingText(
(LPCWSTR)check.utf16(),
misspelledWords,
0);
}
return;
}
ThirdParty::CheckSpellingText(text, misspelledWords);
}
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_spellcheck.h"
namespace Platform::Spellchecker {
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,295 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/spellcheck_highlight_syntax.h"
#include "base/base_file_utilities.h"
#include "base/debug_log.h"
#include "base/flat_map.h"
#include "crl/crl_object_on_queue.h"
#include "SyntaxHighlighter.h"
#include <QtCore/QFile>
#include <xxhash.h>
#include <variant>
#include <string>
void spellchecker_InitHighlightingResource() {
#ifdef Q_OS_MAC // Use resources from the .app bundle on macOS.
base::RegisterBundledResources(u"lib_spellcheck.rcc"_q);
#else // Q_OS_MAC
Q_INIT_RESOURCE(highlighting);
#endif // Q_OS_MAC
}
namespace Spellchecker {
namespace {
base::flat_map<XXH64_hash_t, EntitiesInText> Cache;
HighlightProcessId ProcessIdAutoIncrement/* = 0*/;
rpl::event_stream<HighlightProcessId> ReadyStream;
class QueuedHighlighter final {
public:
QueuedHighlighter();
struct Request {
uint64 hash = 0;
QString text;
QString language;
};
void process(Request request);
void notify(HighlightProcessId id);
private:
using Task = std::variant<Request, HighlightProcessId>;
std::vector<Task> _tasks;
std::unique_ptr<SyntaxHighlighter> _highlighter;
};
[[nodiscard]] crl::object_on_queue<QueuedHighlighter> &Highlighter() {
static auto result = crl::object_on_queue<QueuedHighlighter>();
return result;
}
[[nodiscard]] const QString &LookupAlias(const QString &language) {
static const auto kAliases = base::flat_map<QString, QString>{
{ u"diff"_q, u"git"_q },
{ u"patch"_q, u"git"_q },
};
const auto i = kAliases.find(language);
return (i != end(kAliases)) ? i->second : language;
}
QueuedHighlighter::QueuedHighlighter() {
spellchecker_InitHighlightingResource();
}
void QueuedHighlighter::process(Request request) {
if (!_highlighter) {
auto file = QFile(":/misc/grammars.dat");
const auto size = file.size();
const auto ok1 = file.open(QIODevice::ReadOnly);
auto grammars = std::string();
grammars.resize(size);
const auto ok2 = (file.read(grammars.data(), size) == size);
Assert(ok1 && ok2);
_highlighter = std::make_unique<SyntaxHighlighter>(grammars);
}
const auto text = request.text.toStdString();
const auto language = LookupAlias(request.language.toLower());
const auto tokens = _highlighter->tokenize(text, language.toStdString());
static const auto colors = base::flat_map<std::string, int>{
{ "comment" , 1 },
{ "block-comment", 1 },
{ "prolog" , 1 },
{ "doctype" , 1 },
{ "cdata" , 1 },
{ "punctuation" , 2 },
{ "property" , 3 },
{ "tag" , 3 },
{ "boolean" , 3 },
{ "number" , 3 },
{ "constant" , 3 },
{ "symbol" , 3 },
{ "deleted" , 3 },
{ "selector" , 4 },
{ "attr-name" , 4 },
{ "string" , 4 },
{ "char" , 4 },
{ "builtin" , 4 },
{ "operator" , 5 },
{ "entity" , 5 },
{ "url" , 5 },
{ "atrule" , 6 },
{ "attr-value" , 6 },
{ "keyword" , 6 },
{ "function" , 6 },
{ "class-name" , 7 },
{ "inserted" , 8 },
};
auto offset = 0;
auto entities = EntitiesInText();
auto rebuilt = QString();
rebuilt.reserve(request.text.size());
const auto enumerate = [&](
const TokenList &list,
const std::string &type,
auto &&self) -> void {
for (const auto &node : list) {
if (node.isSyntax()) {
const auto &syntax = static_cast<const Syntax&>(node);
self(syntax.children(), syntax.type(), self);
} else {
const auto text = static_cast<const Text&>(node).value();
const auto utf16 = QString::fromUtf8(
text.data(),
text.size());
const auto length = utf16.size();
rebuilt.append(utf16);
if (!type.empty()) {
const auto i = colors.find(type);
if (i != end(colors)) {
entities.push_back(EntityInText(
EntityType::Colorized,
offset,
length,
QChar(ushort(i->second))));
}
}
offset += length;
}
}
};
enumerate(tokens, std::string(), enumerate);
const auto hash = request.hash;
if (offset != request.text.size()) {
// Something went wrong.
LOG(("Highlighting Error: for language '%1', text: %2"
).arg(request.language, request.text));
entities.clear();
}
crl::on_main([hash, entities = std::move(entities)]() mutable {
Cache.emplace(hash, std::move(entities));
});
}
void QueuedHighlighter::notify(HighlightProcessId id) {
crl::on_main([=] {
ReadyStream.fire_copy(id);
});
}
struct CacheResult {
uint64 hash = 0;
const EntitiesInText *list = nullptr;
explicit operator bool() const {
return list != nullptr;
}
};
[[nodiscard]] CacheResult FindInCache(
const TextWithEntities &text,
EntitiesInText::const_iterator i) {
const auto view = QStringView(text.text).mid(i->offset(), i->length());
const auto language = i->data();
struct Destroyer {
void operator()(XXH64_state_t *state) {
if (state) {
XXH64_freeState(state);
}
}
};
static const auto S = std::unique_ptr<XXH64_state_t, Destroyer>(
XXH64_createState());
const auto state = S.get();
XXH64_reset(state, 0);
XXH64_update(state, view.data(), view.size() * sizeof(ushort));
XXH64_update(state, language.data(), language.size() * sizeof(ushort));
const auto hash = XXH64_digest(state);
const auto j = Cache.find(hash);
return { hash, (j != Cache.cend()) ? &j->second : nullptr };
}
EntitiesInText::iterator Insert(
TextWithEntities &text,
EntitiesInText::iterator i,
const EntitiesInText &entities) {
auto next = i + 1;
if (entities.empty()) {
return next;
}
const auto offset = i->offset();
if (next != text.entities.cend()
&& next->type() == entities.front().type()
&& next->offset() == offset + entities.front().offset()) {
return next;
}
const auto length = i->length();
for (const auto &entity : entities) {
if (entity.offset() + entity.length() > length) {
break;
}
auto j = text.entities.insert(next, entity);
j->shiftRight(offset);
next = j + 1;
}
return next;
}
void Schedule(
uint64 hash,
const TextWithEntities &text,
EntitiesInText::const_iterator i) {
Highlighter().with([
hash,
text = text.text.mid(i->offset(), i->length()),
language = i->data()
](QueuedHighlighter &instance) mutable {
instance.process({ hash, std::move(text), std::move(language) });
});
}
void Notify(uint64 processId) {
Highlighter().with([processId](QueuedHighlighter &instance) {
instance.notify(processId);
});
}
} // namespace
HighlightProcessId TryHighlightSyntax(TextWithEntities &text) {
auto b = text.entities.begin();
auto i = b;
auto e = text.entities.end();
const auto checking = [](const EntityInText &entity) {
return (entity.type() == EntityType::Pre)
&& !entity.data().isEmpty();
};
auto processId = HighlightProcessId();
while (true) {
i = std::find_if(i, e, checking);
if (i == e) {
break;
} else if (const auto already = FindInCache(text, i)) {
i = Insert(text, i, *already.list);
b = text.entities.begin();
e = text.entities.end();
} else {
Schedule(already.hash, text, i);
if (!processId) {
processId = ++ProcessIdAutoIncrement;
}
++i;
}
}
if (processId) {
Notify(processId);
}
return processId;
}
rpl::producer<HighlightProcessId> HighlightReady() {
return ReadyStream.events();
}
} // namespace Spellchecker

View File

@@ -0,0 +1,19 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/text/text_entity.h"
namespace Spellchecker {
using HighlightProcessId = uint64;
// Returning zero means we highlighted everything already.
[[nodiscard]] HighlightProcessId TryHighlightSyntax(TextWithEntities &text);
[[nodiscard]] rpl::producer<HighlightProcessId> HighlightReady();
} // namespace Spellchecker

View File

@@ -0,0 +1,23 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include <QtCore/QString>
#include <range/v3/all.hpp>
#include <vector>
#include <unordered_map>
#include <crl/crl_async.h>
#include <crl/crl_on_main.h>
#include <crl/crl_time.h>
#include <rpl/variable.h>
#include <rpl/map.h>
#include "base/algorithm.h"
#include "base/basic_types.h"

View File

@@ -0,0 +1,57 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <QtCore/QLocale>
using MisspelledWord = std::pair<int, int>;
using MisspelledWords = std::vector<MisspelledWord>;
struct LanguageId {
QLocale::Language value = QLocale::AnyLanguage;
[[nodiscard]] static LanguageId FromName(const QString &name) {
auto exact = QLocale(name);
return {
((exact.language() == QLocale::C)
? QLocale(name.mid(0, 2))
: exact).language()
};
}
[[nodiscard]] QLocale::Language language() const {
return (value == QLocale::C) ? QLocale::English : value;
}
[[nodiscard]] QLocale locale() const {
return QLocale(language());
}
[[nodiscard]] QString name() const {
return locale().name();
}
[[nodiscard]] QString twoLetterCode() const {
return name().toLower().mid(0, 2);
}
[[nodiscard]] bool known() const noexcept {
return (value != QLocale::AnyLanguage);
}
explicit operator bool() const noexcept {
return known();
}
friend inline constexpr auto operator<=>(
LanguageId a,
LanguageId b) noexcept {
return (a.value == QLocale::C ? QLocale::English : a.value)
<=> (b.value == QLocale::C ? QLocale::English : b.value);
}
friend inline constexpr bool operator==(
LanguageId a,
LanguageId b) noexcept {
return (a <=> b) == 0;
}
};

View File

@@ -0,0 +1,321 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/spellcheck_utils.h"
#include "spellcheck/platform/platform_spellcheck.h"
#include <QtCore/QStringList>
#include <QTextBoundaryFinder>
namespace Spellchecker {
namespace {
struct SubtagScript {
const char *subtag;
QChar::Script script;
};
// https://chromium.googlesource.com/chromium/src/+/refs/heads/master/third_party/blink/renderer/platform/text/locale_to_script_mapping.cc
std::vector<QChar::Script> SupportedScripts;
rpl::event_stream<> SupportedScriptsEventStream;
constexpr auto kFactor = 1000;
constexpr auto kAcuteAccentChars = {
QChar(769), QChar(833), // QChar(180),
QChar(714), QChar(779), QChar(733),
QChar(758), QChar(791), QChar(719),
};
constexpr auto kUnspellcheckableScripts = {
QChar::Script_Katakana,
QChar::Script_Han,
};
constexpr SubtagScript kLocaleScriptList[] = {
{"aa", QChar::Script_Latin}, {"ab", QChar::Script_Cyrillic},
{"ady", QChar::Script_Cyrillic}, {"aeb", QChar::Script_Arabic},
{"af", QChar::Script_Latin}, {"ak", QChar::Script_Latin},
{"am", QChar::Script_Ethiopic}, {"ar", QChar::Script_Arabic},
{"arq", QChar::Script_Arabic}, {"ary", QChar::Script_Arabic},
{"arz", QChar::Script_Arabic}, {"as", QChar::Script_Bengali},
{"ast", QChar::Script_Latin}, {"av", QChar::Script_Cyrillic},
{"ay", QChar::Script_Latin}, {"az", QChar::Script_Latin},
{"azb", QChar::Script_Arabic}, {"ba", QChar::Script_Cyrillic},
{"bal", QChar::Script_Arabic}, {"be", QChar::Script_Cyrillic},
{"bej", QChar::Script_Arabic}, {"bg", QChar::Script_Cyrillic},
{"bi", QChar::Script_Latin}, {"bn", QChar::Script_Bengali},
{"bo", QChar::Script_Tibetan}, {"bqi", QChar::Script_Arabic},
{"brh", QChar::Script_Arabic}, {"bs", QChar::Script_Latin},
{"ca", QChar::Script_Latin}, {"ce", QChar::Script_Cyrillic},
{"ceb", QChar::Script_Latin}, {"ch", QChar::Script_Latin},
{"chk", QChar::Script_Latin}, {"cja", QChar::Script_Arabic},
{"cjm", QChar::Script_Arabic}, {"ckb", QChar::Script_Arabic},
{"cs", QChar::Script_Latin}, {"cy", QChar::Script_Latin},
{"da", QChar::Script_Latin}, {"dcc", QChar::Script_Arabic},
{"de", QChar::Script_Latin}, {"doi", QChar::Script_Arabic},
{"dv", QChar::Script_Thaana}, {"dyo", QChar::Script_Arabic},
{"dz", QChar::Script_Tibetan}, {"ee", QChar::Script_Latin},
{"efi", QChar::Script_Latin}, {"el", QChar::Script_Greek},
{"en", QChar::Script_Latin}, {"es", QChar::Script_Latin},
{"et", QChar::Script_Latin}, {"eu", QChar::Script_Latin},
{"fa", QChar::Script_Arabic}, {"fi", QChar::Script_Latin},
{"fil", QChar::Script_Latin}, {"fj", QChar::Script_Latin},
{"fo", QChar::Script_Latin}, {"fr", QChar::Script_Latin},
{"fur", QChar::Script_Latin}, {"fy", QChar::Script_Latin},
{"ga", QChar::Script_Latin}, {"gaa", QChar::Script_Latin},
{"gba", QChar::Script_Arabic}, {"gbz", QChar::Script_Arabic},
{"gd", QChar::Script_Latin}, {"gil", QChar::Script_Latin},
{"gl", QChar::Script_Latin}, {"gjk", QChar::Script_Arabic},
{"gju", QChar::Script_Arabic}, {"glk", QChar::Script_Arabic},
{"gn", QChar::Script_Latin}, {"gsw", QChar::Script_Latin},
{"gu", QChar::Script_Gujarati}, {"ha", QChar::Script_Latin},
{"haw", QChar::Script_Latin}, {"haz", QChar::Script_Arabic},
{"he", QChar::Script_Hebrew}, {"hi", QChar::Script_Devanagari},
{"hil", QChar::Script_Latin}, {"hnd", QChar::Script_Arabic},
{"hno", QChar::Script_Arabic}, {"ho", QChar::Script_Latin},
{"hr", QChar::Script_Latin}, {"ht", QChar::Script_Latin},
{"hu", QChar::Script_Latin}, {"hy", QChar::Script_Armenian},
{"id", QChar::Script_Latin}, {"ig", QChar::Script_Latin},
{"ii", QChar::Script_Yi}, {"ilo", QChar::Script_Latin},
{"inh", QChar::Script_Cyrillic}, {"is", QChar::Script_Latin},
{"it", QChar::Script_Latin}, {"iu", QChar::Script_CanadianAboriginal},
{"ja", QChar::Script_Katakana}, // or Script_Hiragana.
{"jv", QChar::Script_Latin}, {"ka", QChar::Script_Georgian},
{"kaj", QChar::Script_Latin}, {"kam", QChar::Script_Latin},
{"kbd", QChar::Script_Cyrillic}, {"kha", QChar::Script_Latin},
{"khw", QChar::Script_Arabic}, {"kk", QChar::Script_Cyrillic},
{"kl", QChar::Script_Latin}, {"km", QChar::Script_Khmer},
{"kn", QChar::Script_Kannada}, {"ko", QChar::Script_Hangul},
{"kok", QChar::Script_Devanagari}, {"kos", QChar::Script_Latin},
{"kpe", QChar::Script_Latin}, {"krc", QChar::Script_Cyrillic},
{"ks", QChar::Script_Arabic}, {"ku", QChar::Script_Arabic},
{"kum", QChar::Script_Cyrillic}, {"kvx", QChar::Script_Arabic},
{"kxp", QChar::Script_Arabic}, {"ky", QChar::Script_Cyrillic},
{"la", QChar::Script_Latin}, {"lah", QChar::Script_Arabic},
{"lb", QChar::Script_Latin}, {"lez", QChar::Script_Cyrillic},
{"lki", QChar::Script_Arabic}, {"ln", QChar::Script_Latin},
{"lo", QChar::Script_Lao}, {"lrc", QChar::Script_Arabic},
{"lt", QChar::Script_Latin}, {"luz", QChar::Script_Arabic},
{"lv", QChar::Script_Latin}, {"mai", QChar::Script_Devanagari},
{"mdf", QChar::Script_Cyrillic}, {"mfa", QChar::Script_Arabic},
{"mg", QChar::Script_Latin}, {"mh", QChar::Script_Latin},
{"mi", QChar::Script_Latin}, {"mk", QChar::Script_Cyrillic},
{"ml", QChar::Script_Malayalam}, {"mn", QChar::Script_Cyrillic},
{"mr", QChar::Script_Devanagari},{"ms", QChar::Script_Latin},
{"mt", QChar::Script_Latin}, {"mvy", QChar::Script_Arabic},
{"my", QChar::Script_Myanmar}, {"myv", QChar::Script_Cyrillic},
{"mzn", QChar::Script_Arabic}, {"na", QChar::Script_Latin},
{"nb", QChar::Script_Latin}, {"ne", QChar::Script_Devanagari},
{"niu", QChar::Script_Latin}, {"nl", QChar::Script_Latin},
{"nn", QChar::Script_Latin}, {"nr", QChar::Script_Latin},
{"nso", QChar::Script_Latin}, {"ny", QChar::Script_Latin},
{"oc", QChar::Script_Latin}, {"om", QChar::Script_Latin},
{"or", QChar::Script_Oriya}, {"os", QChar::Script_Cyrillic},
{"pa", QChar::Script_Gurmukhi}, {"pag", QChar::Script_Latin},
{"pap", QChar::Script_Latin}, {"pau", QChar::Script_Latin},
{"pl", QChar::Script_Latin}, {"pon", QChar::Script_Latin},
{"prd", QChar::Script_Arabic}, {"prs", QChar::Script_Arabic},
{"ps", QChar::Script_Arabic}, {"pt", QChar::Script_Latin},
{"qu", QChar::Script_Latin}, {"rm", QChar::Script_Latin},
{"rmt", QChar::Script_Arabic}, {"rn", QChar::Script_Latin},
{"ro", QChar::Script_Latin}, {"ru", QChar::Script_Cyrillic},
{"rw", QChar::Script_Latin}, {"sa", QChar::Script_Devanagari},
{"sah", QChar::Script_Cyrillic}, {"sat", QChar::Script_Latin},
{"sd", QChar::Script_Arabic}, {"sdh", QChar::Script_Arabic},
{"se", QChar::Script_Latin}, {"sg", QChar::Script_Latin},
{"shi", QChar::Script_Arabic}, {"si", QChar::Script_Sinhala},
{"sid", QChar::Script_Latin}, {"sk", QChar::Script_Latin},
{"skr", QChar::Script_Arabic}, {"sl", QChar::Script_Latin},
{"sm", QChar::Script_Latin}, {"so", QChar::Script_Latin},
{"sq", QChar::Script_Latin}, {"sr", QChar::Script_Cyrillic},
{"ss", QChar::Script_Latin}, {"st", QChar::Script_Latin},
{"su", QChar::Script_Latin}, {"sus", QChar::Script_Arabic},
{"sv", QChar::Script_Latin}, {"sw", QChar::Script_Latin},
{"swb", QChar::Script_Arabic}, {"syr", QChar::Script_Arabic},
{"ta", QChar::Script_Tamil}, {"te", QChar::Script_Telugu},
{"tet", QChar::Script_Latin}, {"tg", QChar::Script_Cyrillic},
{"th", QChar::Script_Thai}, {"ti", QChar::Script_Ethiopic},
{"tig", QChar::Script_Ethiopic}, {"tk", QChar::Script_Latin},
{"tkl", QChar::Script_Latin}, {"tl", QChar::Script_Latin},
{"tn", QChar::Script_Latin}, {"to", QChar::Script_Latin},
{"tpi", QChar::Script_Latin}, {"tr", QChar::Script_Latin},
{"trv", QChar::Script_Latin}, {"ts", QChar::Script_Latin},
{"tt", QChar::Script_Cyrillic}, {"ttt", QChar::Script_Arabic},
{"tvl", QChar::Script_Latin}, {"tw", QChar::Script_Latin},
{"ty", QChar::Script_Latin}, {"tyv", QChar::Script_Cyrillic},
{"udm", QChar::Script_Cyrillic}, {"ug", QChar::Script_Arabic},
{"uk", QChar::Script_Cyrillic}, {"und", QChar::Script_Latin},
{"ur", QChar::Script_Arabic}, {"uz", QChar::Script_Cyrillic},
{"ve", QChar::Script_Latin}, {"vi", QChar::Script_Latin},
{"wal", QChar::Script_Ethiopic}, {"war", QChar::Script_Latin},
{"wo", QChar::Script_Latin}, {"xh", QChar::Script_Latin},
{"yap", QChar::Script_Latin}, {"yo", QChar::Script_Latin},
{"za", QChar::Script_Latin}, {"zdj", QChar::Script_Arabic},
{"zh", QChar::Script_Han}, {"zu", QChar::Script_Latin},
// Encompassed languages within the Chinese macrolanguage.
// http://www-01.sil.org/iso639-3/documentation.asp?id=zho
// http://lists.w3.org/Archives/Public/public-i18n-cjk/2016JulSep/0022.html
// {"cdo", USCRIPT_SIMPLIFIED_HAN},
// {"cjy", USCRIPT_SIMPLIFIED_HAN},
// {"cmn", USCRIPT_SIMPLIFIED_HAN},
// {"cpx", USCRIPT_SIMPLIFIED_HAN},
// {"czh", USCRIPT_SIMPLIFIED_HAN},
// {"czo", USCRIPT_SIMPLIFIED_HAN},
// {"gan", USCRIPT_SIMPLIFIED_HAN},
// {"hsn", USCRIPT_SIMPLIFIED_HAN},
// {"mnp", USCRIPT_SIMPLIFIED_HAN},
// {"wuu", USCRIPT_SIMPLIFIED_HAN},
// {"hak", USCRIPT_TRADITIONAL_HAN},
// {"lzh", USCRIPT_TRADITIONAL_HAN},
// {"nan", USCRIPT_TRADITIONAL_HAN},
// {"yue", USCRIPT_TRADITIONAL_HAN},
// {"zh-cdo", USCRIPT_SIMPLIFIED_HAN},
// {"zh-cjy", USCRIPT_SIMPLIFIED_HAN},
// {"zh-cmn", USCRIPT_SIMPLIFIED_HAN},
// {"zh-cpx", USCRIPT_SIMPLIFIED_HAN},
// {"zh-czh", USCRIPT_SIMPLIFIED_HAN},
// {"zh-czo", USCRIPT_SIMPLIFIED_HAN},
// {"zh-gan", USCRIPT_SIMPLIFIED_HAN},
// {"zh-hsn", USCRIPT_SIMPLIFIED_HAN},
// {"zh-mnp", USCRIPT_SIMPLIFIED_HAN},
// {"zh-wuu", USCRIPT_SIMPLIFIED_HAN},
// {"zh-hak", USCRIPT_TRADITIONAL_HAN},
// {"zh-lzh", USCRIPT_TRADITIONAL_HAN},
// {"zh-nan", USCRIPT_TRADITIONAL_HAN},
// {"zh-yue", USCRIPT_TRADITIONAL_HAN},
// // Chinese with regions. Logically, regions should be handled
// // separately, but this works for the current purposes.
// {"zh-hk", USCRIPT_TRADITIONAL_HAN},
// {"zh-mo", USCRIPT_TRADITIONAL_HAN},
// {"zh-tw", USCRIPT_TRADITIONAL_HAN},
};
inline auto IsAcuteAccentChar(const QChar &c) {
return ranges::contains(kAcuteAccentChars, c);
}
inline auto IsSpellcheckableScripts(const QChar::Script &s) {
return !ranges::contains(kUnspellcheckableScripts, s);
}
} // namespace
QChar::Script LocaleToScriptCode(const QString &locale) {
const auto subtag = locale.left(
std::max(locale.indexOf('_'), locale.indexOf('-')));
for (const auto &kv : kLocaleScriptList) {
if (subtag == kv.subtag) {
return kv.script;
}
}
return QChar::Script_Common;
}
QChar::Script WordScript(QStringView word) {
// Find the first letter.
const auto firstLetter = ranges::find_if(word, [](QChar c) {
return c.isLetter();
});
return firstLetter == word.end()
? QChar::Script_Common
: firstLetter->script();
}
bool IsWordSkippable(QStringView word, bool checkSupportedScripts) {
if (word.size() > kMaxWordSize) {
return true;
}
const auto wordScript = WordScript(word);
if (checkSupportedScripts
&& !ranges::contains(SupportedScripts, wordScript)) {
return true;
}
return ranges::any_of(word, [&](QChar c) {
return (c.script() != wordScript)
&& !IsAcuteAccentChar(c)
&& (c.unicode() != '\'') // Patched Qt to make it a non-separator.
&& (c.unicode() != '_'); // This is not a word separator.
});
}
void UpdateSupportedScripts(std::vector<QString> languages) {
// It should be called at least once from Platform::Spellchecker::Init().
SupportedScripts = ranges::views::all(
languages
) | ranges::views::transform(
LocaleToScriptCode
) | ranges::views::unique | ranges::views::filter(
IsSpellcheckableScripts
) | ranges::to_vector;
SupportedScriptsEventStream.fire({});
}
rpl::producer<> SupportedScriptsChanged() {
return SupportedScriptsEventStream.events();
}
MisspelledWords RangesFromText(
const QString &text,
Fn<bool(const QString &word)> filterCallback) {
MisspelledWords ranges;
if (text.isEmpty()) {
return ranges;
}
auto finder = QTextBoundaryFinder(QTextBoundaryFinder::Word, text);
const auto isEnd = [&] {
return (finder.toNextBoundary() == -1);
};
while (finder.position() < text.length()) {
if (!finder.boundaryReasons().testFlag(
QTextBoundaryFinder::StartOfItem)) {
if (isEnd()) {
break;
}
continue;
}
const auto start = finder.position();
const auto end = finder.toNextBoundary();
if (end == -1) {
break;
}
const auto length = end - start;
if (length < 1) {
continue;
}
if (!filterCallback(text.mid(start, length))) {
ranges.push_back(std::make_pair(start, length));
}
if (isEnd()) {
break;
}
}
return ranges;
}
bool CheckSkipAndSpell(const QString &word) {
return !IsWordSkippable(word)
&& Platform::Spellchecker::CheckSpelling(word);
}
QLocale LocaleFromLangId(int langId) {
if (langId < kFactor) {
return QLocale(static_cast<QLocale::Language>(langId));
}
const auto l = langId / kFactor;
const auto lang = static_cast<QLocale::Language>(l);
const auto country = static_cast<QLocale::Country>(langId - l * kFactor);
return QLocale(lang, country);
}
} // namespace Spellchecker

View File

@@ -0,0 +1,35 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/spellcheck_types.h"
#include <QLocale>
namespace Spellchecker {
constexpr auto kMaxWordSize = 99;
QChar::Script LocaleToScriptCode(const QString &locale);
QChar::Script WordScript(QStringView word);
bool IsWordSkippable(
QStringView word,
bool checkSupportedScripts = true);
MisspelledWords RangesFromText(
const QString &text,
Fn<bool(const QString &word)> filterCallback);
// For Linux and macOS, which use RangesFromText.
bool CheckSkipAndSpell(const QString &word);
QLocale LocaleFromLangId(int langId);
void UpdateSupportedScripts(std::vector<QString> languages);
rpl::producer<> SupportedScriptsChanged();
} // namespace Spellchecker

View File

@@ -0,0 +1,33 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/spellcheck_value.h"
namespace ph {
phrase lng_spellchecker_submenu = "Spelling";
phrase lng_spellchecker_add = "Add to Dictionary";
phrase lng_spellchecker_remove = "Remove from Dictionary";
phrase lng_spellchecker_ignore = "Ignore word";
} // namespace ph
namespace Spellchecker {
namespace {
QString WorkingDir = QString();
} // namespace
QString WorkingDirPath() {
return WorkingDir;
}
void SetWorkingDirPath(const QString &path) {
WorkingDir = path;
}
} // namespace Spellchecker

View File

@@ -0,0 +1,35 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "ui/ph.h"
namespace ph {
extern phrase lng_spellchecker_submenu;
extern phrase lng_spellchecker_add;
extern phrase lng_spellchecker_remove;
extern phrase lng_spellchecker_ignore;
} // namespace ph
namespace Spellchecker {
////// Phrases.
inline constexpr auto kPhrasesCount = 4;
inline void SetPhrases(ph::details::phrase_value_array<kPhrasesCount> data) {
ph::details::set_values(std::move(data));
}
//////
[[nodiscard]] QString WorkingDirPath();
void SetWorkingDirPath(const QString &path);
} // namespace Spellchecker

View File

@@ -0,0 +1,947 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/spelling_highlighter.h"
#include "base/weak_qptr.h"
#include "spellcheck/spellcheck_value.h"
#include "spellcheck/spellcheck_utils.h"
#include "spellcheck/spelling_highlighter_helper.h"
#include "ui/widgets/menu/menu.h"
#include "ui/text/text_entity.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h"
#include "base/qt/qt_common_adapters.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "styles/palette.h"
namespace Spellchecker {
namespace {
constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
const auto kUnspellcheckableTags = {
&Ui::InputField::kTagCode,
&Ui::InputField::kTagPre,
&Ui::InputField::kTagUnderline
};
constexpr auto kColdSpellcheckingTimeout = crl::time(1000);
constexpr auto kMaxDeadKeys = 1;
constexpr auto kSkippableFlags = 0
| TextParseLinks
| TextParseMentions
| TextParseHashtags
| TextParseBotCommands;
const auto kKeysToCheck = {
Qt::Key_Up,
Qt::Key_Down,
Qt::Key_Left,
Qt::Key_Right,
Qt::Key_PageUp,
Qt::Key_PageDown,
Qt::Key_Home,
Qt::Key_End,
};
inline int EndOfWord(const MisspelledWord &range) {
return range.first + range.second;
}
inline bool IntersectsWordRanges(
const MisspelledWord &range,
int pos2,
int len2) {
const auto l1 = range.first;
const auto r1 = EndOfWord(range) - 1;
const auto l2 = pos2;
const auto r2 = pos2 + len2 - 1;
return !(l1 > r2 || l2 > r1);
}
inline bool IntersectsWordRanges(
const MisspelledWord &range,
const MisspelledWord &range2) {
const auto l1 = range.first;
const auto r1 = EndOfWord(range) - 1;
const auto l2 = range2.first;
const auto r2 = EndOfWord(range2) - 1;
return !(l1 > r2 || l2 > r1);
}
inline bool IntersectsWordRanges(const EntityInText &e, int pos2, int len2) {
return IntersectsWordRanges({ e.offset(), e.length() }, pos2, len2);
}
inline bool IsTagUnspellcheckable(const QString &tag) {
if (tag.isEmpty()) {
return false;
}
for (const auto &single : TextUtilities::SplitTags(tag)) {
const auto isCommonFormatting = ranges::any_of(
kUnspellcheckableTags,
[&](const auto *t) { return (*t) == single; });
if (isCommonFormatting) {
return true;
}
if (Ui::InputField::IsValidMarkdownLink(single)) {
return true;
}
if (TextUtilities::IsMentionLink(single)) {
return true;
}
}
return false;
}
inline auto FindEntities(const QString &text) {
return TextUtilities::ParseEntities(text, kSkippableFlags).entities;
}
inline auto IntersectsAnyOfEntities(
int pos,
int len,
EntitiesInText entities) {
return !entities.empty() && ranges::any_of(entities, [&](const auto &e) {
return IntersectsWordRanges(e, pos, len);
});
}
inline QChar AddedSymbol(QStringView text, int position, int added) {
if (added != 1 || position >= text.size()) {
return QChar();
}
return text.at(position);
}
inline MisspelledWord CorrectAccentValues(
const QString &oldText,
const QString &newText) {
auto diff = std::vector<int>();
const auto sizeOfDiff = newText.size() - oldText.size();
if (sizeOfDiff <= 0 || sizeOfDiff > kMaxDeadKeys) {
return MisspelledWord();
}
for (auto i = 0; i < oldText.size(); i++) {
if (oldText.at(i) != newText.at(i + diff.size())) {
diff.push_back(i);
if (diff.size() > kMaxDeadKeys) {
return MisspelledWord();
}
}
}
if (diff.size() == 0) {
return MisspelledWord(oldText.size(), sizeOfDiff);
}
return MisspelledWord(diff.front(), diff.size() > 1 ? diff.back() : 1);
}
inline MisspelledWord RangeFromCursorSelection(const QTextCursor &cursor) {
const auto start = cursor.selectionStart();
return MisspelledWord(start, cursor.selectionEnd() - start);
}
[[nodiscard]] bool SpellcheckSuggestEvent(not_null<QKeyEvent*> e) {
const auto modifier = Platform::IsMac()
? Qt::MetaModifier
: Qt::ControlModifier;
return (e->key() == Qt::Key_Space) && e->modifiers().testFlag(modifier);
}
} // namespace
SpellingHighlighter::SpellingHighlighter(
not_null<Ui::InputField*> field,
rpl::producer<bool> enabled,
std::optional<CustomContextMenuItem> customContextMenuItem)
: QSyntaxHighlighter(field->rawTextEdit()->document())
, _cursor(QTextCursor(document()))
, _coldSpellcheckingTimer([=] { checkChangedText(); })
, _field(field)
, _textEdit(field->rawTextEdit())
, _customContextMenuItem(customContextMenuItem) {
#ifdef Q_OS_WIN
Platform::Spellchecker::Init();
#endif // !Q_OS_WIN
_cachedRanges = MisspelledWords();
// Use the patched SpellCheckUnderline style.
_misspelledFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
style::PaletteChanged(
) | rpl::on_next([=] {
updatePalette();
rehighlight();
}, _lifetime);
updatePalette();
_field->documentContentsChanges(
) | rpl::on_next([=](const auto &value) {
const auto &[pos, removed, added] = value;
contentsChange(pos, removed, added);
}, _lifetime);
_field->markdownTagApplies(
) | rpl::on_next([=](auto markdownTag) {
if (!IsTagUnspellcheckable(markdownTag.tag)) {
return;
}
_cachedRanges = ranges::views::all(
_cachedRanges
) | ranges::views::filter([&](const auto &range) {
return !IntersectsWordRanges(
range,
markdownTag.internalStart,
markdownTag.internalLength);
}) | ranges::to_vector;
rehighlight();
}, _lifetime);
updateDocumentText();
std::move(
enabled
) | rpl::on_next([=](bool value) {
setEnabled(value);
if (_enabled) {
_field->installEventFilter(this);
_textEdit->installEventFilter(this);
_textEdit->viewport()->installEventFilter(this);
} else {
_field->installEventFilter(this);
_textEdit->removeEventFilter(this);
_textEdit->viewport()->removeEventFilter(this);
}
}, _lifetime);
Spellchecker::SupportedScriptsChanged(
) | rpl::on_next([=] {
checkCurrentText();
}, _lifetime);
}
void SpellingHighlighter::updatePalette() {
_misspelledFormat.setUnderlineColor(st::spellUnderline->c);
}
void SpellingHighlighter::contentsChange(int pos, int removed, int added) {
if (!_enabled) {
return;
}
if (document()->isEmpty()) {
updateDocumentText();
_cachedRanges.clear();
return;
}
{
const auto oldText = documentText().mid(
pos,
documentText().indexOf(QChar::ParagraphSeparator, pos));
updateDocumentText();
const auto b = findBlock(pos);
const auto bLen = (document()->blockCount() > 1)
? b.length()
: b.text().size();
// This is a workaround for typing accents.
// For example, when the user press the dead key (e.g. ` or ´),
// Qt sends wrong values. E.g. if a text length is 100,
// then values will be 0, 100, 100.
// This invokes to re-check the entire text.
// The Mac's accent menu has a pretty similar behavior.
if ((b.position() == pos) && (bLen == added)) {
const auto newText = b.text();
const auto diff = added - removed;
// The plain text of the document cannot contain dead keys.
if (!diff) {
if (!oldText.compare(newText, Qt::CaseSensitive)) {
const auto c = RangeFromCursorSelection(
_textEdit->textCursor());
// If the cursor has a selection for the entire text,
// we probably just changed its formatting.
// So if we find the unspellcheckable tag,
// we can clear cached ranges of misspelled words.
if (!c.first && c.second == bLen) {
if (hasUnspellcheckableTag(pos, added)) {
_cachedRanges.clear();
rehighlight();
} else {
checkCurrentText();
}
}
return;
}
} else if (diff > 0 && diff <= kMaxDeadKeys) {
const auto [p, l] = CorrectAccentValues(oldText, newText);
if (l) {
pos = p + b.position();
added = l;
removed = 0;
}
}
}
}
const auto shift = [&](auto chars) {
ranges::for_each(_cachedRanges, [&](auto &range) {
if (range.first >= pos + removed) {
range.first += chars;
}
});
};
// Shift to the right all words after the cursor, when adding text.
if (added > 0) {
shift(added);
}
// Remove all words that are in the selection.
// Remove the word that is under the cursor.
const auto wordUnderPos = getWordUnderPosition(pos);
// If the cursor is between spaces,
// QTextCursor::WordUnderCursor highlights the word on the left
// even if the word is not under the cursor.
// Example: "super | test", where | is the cursor position.
// In this example QTextCursor::WordUnderCursor will select "super".
const auto isPosNotInWord = pos > EndOfWord(wordUnderPos);
_cachedRanges = (
_cachedRanges
) | ranges::views::filter([&](const auto &range) {
const auto isIntersected = IntersectsWordRanges(range, wordUnderPos);
if (isIntersected) {
return isPosNotInWord;
}
return !(isIntersected
|| (removed > 0 && IntersectsWordRanges(range, pos, removed)));
}) | ranges::to_vector;
// Shift to the left all words after the cursor, when deleting text.
if (removed > 0) {
shift(-removed);
}
// Normally we should to invoke rehighlighting to immediately apply
// shifting of ranges. But we don't have to do this because the call of
// contentsChange() is performed before the application's call of
// highlightBlock().
_addedSymbols += added;
_removedSymbols += removed;
// The typing of text character by character should produce
// the same _lastPosition, _addedSymbols and _removedSymbols values
// as removing and pasting several characters at a time.
if (!_lastPosition || (removed == 1)) {
_lastPosition = pos;
}
const auto addedSymbol = AddedSymbol(documentText(), pos, added);
if ((removed == 1) || addedSymbol.isLetterOrNumber()) {
if (_coldSpellcheckingTimer.isActive()) {
_coldSpellcheckingTimer.cancel();
}
_coldSpellcheckingTimer.callOnce(kColdSpellcheckingTimeout);
} else {
// We forcefully increase the range of check
// when inserting a non-char. This can help when the user inserts
// a non-char in the middle of a word.
if (!(addedSymbol.isNull()
|| addedSymbol.isSpace()
|| addedSymbol.isLetterOrNumber())) {
_lastPosition--;
_addedSymbols++;
}
if (_isLastKeyRepeat) {
return;
}
checkChangedText();
}
}
void SpellingHighlighter::checkChangedText() {
const auto pos = _lastPosition;
const auto added = _addedSymbols;
const auto removed = _removedSymbols;
_lastPosition = 0;
_removedSymbols = 0;
_addedSymbols = 0;
if (_coldSpellcheckingTimer.isActive()) {
_coldSpellcheckingTimer.cancel();
}
const auto wordUnderCursor = getWordUnderPosition(pos);
// If the length of the word is 0, there is no sense in checking it.
if (!wordUnderCursor.second) {
return;
}
const auto wordInCacheIt = [=] {
return ranges::find_if(_cachedRanges, [&](auto &&w) {
return w.first >= wordUnderCursor.first;
});
};
if (added > 0) {
const auto lastWordNewSelection = getWordUnderPosition(pos + added);
// This is the same word.
if (wordUnderCursor == lastWordNewSelection) {
checkSingleWord(wordUnderCursor);
return;
}
const auto beginNewSelection = wordUnderCursor.first;
const auto endNewSelection = EndOfWord(lastWordNewSelection);
auto callback = [=](MisspelledWords &&r) {
ranges::insert(_cachedRanges, wordInCacheIt(), std::move(r));
};
invokeCheckText(
beginNewSelection,
endNewSelection - beginNewSelection,
std::move(callback));
return;
}
if (removed > 0) {
checkSingleWord(wordUnderCursor);
}
}
MisspelledWords SpellingHighlighter::filterSkippableWords(
MisspelledWords &ranges) {
const auto text = documentText();
if (text.isEmpty()) {
return MisspelledWords();
}
return ranges | ranges::views::filter([&](const auto &range) {
return !isSkippableWord(range);
}) | ranges::to_vector;
}
bool SpellingHighlighter::isSkippableWord(const MisspelledWord &range) {
return isSkippableWord(range.first, range.second);
}
bool SpellingHighlighter::isSkippableWord(int position, int length) {
if (hasUnspellcheckableTag(position, length)) {
return true;
}
const auto text = documentText();
const auto ref = base::StringViewMid(text, position, length);
if (ref.isNull()) {
return true;
}
return IsWordSkippable(ref);
}
void SpellingHighlighter::checkCurrentText() {
if (document()->isEmpty()) {
_cachedRanges.clear();
return;
}
invokeCheckText(0, size(), [&](MisspelledWords &&ranges) {
_cachedRanges = std::move(ranges);
});
}
void SpellingHighlighter::invokeCheckText(
int textPosition,
int textLength,
Fn<void(MisspelledWords &&ranges)> callback) {
if (!_enabled) {
return;
}
const auto rangesOffset = textPosition;
const auto text = partDocumentText(textPosition, textLength);
const auto weak = base::make_weak(this);
_countOfCheckingTextAsync++;
crl::async([=,
text = std::move(text),
callback = std::move(callback)]() mutable {
MisspelledWords misspelledWordRanges;
Platform::Spellchecker::CheckSpellingText(
text,
&misspelledWordRanges);
if (rangesOffset) {
ranges::for_each(misspelledWordRanges, [&](auto &&range) {
range.first += rangesOffset;
});
}
crl::on_main(weak, [=,
text = std::move(text),
ranges = std::move(misspelledWordRanges),
callback = std::move(callback)]() mutable {
_countOfCheckingTextAsync--;
// Checking a large part of text can take an unknown amount of
// time. So we have to compare the text before and after async
// work.
// If the text has changed during async and we have more async,
// we don't perform further refreshing of cache and underlines.
// But if it was the last async, we should invoke a new one.
if (compareDocumentText(text, textPosition, textLength)) {
if (!_countOfCheckingTextAsync) {
checkCurrentText();
}
return;
}
auto filtered = filterSkippableWords(ranges);
// When we finish checking the text, the user can
// supplement the last word and there may be a situation where
// a part of the last word may not be underlined correctly.
// Example:
// 1. We insert a text with an incomplete last word.
// "Time in a bottl".
// 2. We don't wait for the check to be finished
// and end the last word with the letter "e".
// 3. invokeCheckText() will mark the last word "bottl" as
// misspelled.
// 4. checkSingleWord() will mark the "bottle" as correct and
// leave it as it is.
// 5. The first five letters of the "bottle" will be underlined
// and the sixth will not be underlined.
// We can fix it with a check of completeness of the last word.
if (filtered.size()) {
const auto lastWord = filtered.back();
if (const auto endOfText = textPosition + textLength;
EndOfWord(lastWord) == endOfText) {
const auto word = getWordUnderPosition(endOfText);
if (EndOfWord(word) != endOfText) {
filtered.pop_back();
checkSingleWord(word);
}
}
}
callback(std::move(filtered));
for (const auto &b : blocksFromRange(textPosition, textLength)) {
rehighlightBlock(b);
}
});
});
}
void SpellingHighlighter::checkSingleWord(const MisspelledWord &singleWord) {
const auto weak = base::make_weak(this);
auto w = partDocumentText(singleWord.first, singleWord.second);
if (isSkippableWord(singleWord)) {
return;
}
crl::async([=,
w = std::move(w),
singleWord = std::move(singleWord)]() mutable {
if (Platform::Spellchecker::CheckSpelling(std::move(w))) {
return;
}
crl::on_main(weak, [=,
singleWord = std::move(singleWord)]() mutable {
const auto posOfWord = singleWord.first;
ranges::insert(
_cachedRanges,
ranges::find_if(_cachedRanges, [&](auto &&w) {
return w.first >= posOfWord;
}),
singleWord);
rehighlightBlock(findBlock(posOfWord));
});
});
}
bool SpellingHighlighter::hasUnspellcheckableTag(int begin, int length) {
// This method is called only in the context of separate words,
// so it is not supposed that the word can be in more than one block.
const auto block = findBlock(begin);
length = std::min(block.position() + block.length() - begin, length);
for (auto it = block.begin(); !(it.atEnd()); ++it) {
const auto fragment = it.fragment();
if (!fragment.isValid()) {
continue;
}
const auto frPos = fragment.position();
const auto frLen = fragment.length();
if (!IntersectsWordRanges({ frPos, frLen }, begin, length)) {
continue;
}
const auto format = fragment.charFormat();
if (!format.hasProperty(kTagProperty)) {
continue;
}
const auto tag = format.property(kTagProperty).toString();
if (IsTagUnspellcheckable(tag)) {
return true;
}
}
return false;
}
MisspelledWord SpellingHighlighter::getWordUnderPosition(int position) {
if (position < 0) {
position = 0;
}
_cursor.setPosition(std::min(position, size()));
_cursor.select(QTextCursor::WordUnderCursor);
return RangeFromCursorSelection(_cursor);
}
void SpellingHighlighter::highlightBlock(const QString &text) {
if (_cachedRanges.empty() || !_enabled || text.isEmpty()) {
return;
}
const auto entities = FindEntities(text);
const auto bPos = currentBlock().position();
const auto bLen = currentBlock().length();
ranges::for_each((
_cachedRanges
// Skip the all words outside the current block.
) | ranges::views::filter([&](const auto &range) {
return IntersectsWordRanges(range, bPos, bLen);
}), [&](const auto &range) {
const auto posInBlock = range.first - bPos;
if (IntersectsAnyOfEntities(posInBlock, range.second, entities)) {
return;
}
setFormat(posInBlock, range.second, _misspelledFormat);
});
setCurrentBlockState(0);
}
bool SpellingHighlighter::eventFilter(QObject *o, QEvent *e) {
if (!_enabled) {
return false;
} else if (o == _field) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e);
if (SpellcheckSuggestEvent(k)) {
showSpellcheckerMenu();
return true;
}
}
return false;
}
if (e->type() == QEvent::ContextMenu) {
const auto c = static_cast<QContextMenuEvent*>(e);
const auto menu = _textEdit->createStandardContextMenu();
if (!menu || !c) {
return false;
}
// Copy of QContextMenuEvent.
auto copyEvent = std::make_shared<QContextMenuEvent>(
c->reason(),
c->pos(),
c->globalPos());
auto showMenu = [=, copyEvent = std::move(copyEvent)] {
_contextMenuCreated.fire({ menu, copyEvent });
};
addSpellcheckerActions(
std::move(menu),
_textEdit->cursorForPosition(c->pos()),
std::move(showMenu),
c->globalPos());
return true;
} else if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e);
if (ranges::contains(kKeysToCheck, k->key())) {
if (_addedSymbols + _removedSymbols + _lastPosition) {
checkCurrentText();
}
} else if ((o == _textEdit) && k->isAutoRepeat()) {
_isLastKeyRepeat = true;
}
} else if (_isLastKeyRepeat && (o == _textEdit)) {
if (e->type() == QEvent::FocusOut) {
_isLastKeyRepeat = false;
if (_addedSymbols + _removedSymbols + _lastPosition) {
checkCurrentText();
}
} else if (e->type() == QEvent::KeyRelease) {
const auto k = static_cast<QKeyEvent*>(e);
if (!k->isAutoRepeat()) {
_isLastKeyRepeat = false;
_coldSpellcheckingTimer.callOnce(kColdSpellcheckingTimeout);
}
}
} else if ((o == _textEdit->viewport())
&& (e->type() == QEvent::MouseButtonPress)) {
if (_addedSymbols + _removedSymbols + _lastPosition) {
checkCurrentText();
}
}
return false;
}
bool SpellingHighlighter::enabled() {
return _enabled;
}
void SpellingHighlighter::setEnabled(bool enabled) {
_enabled = enabled;
if (_enabled) {
updateDocumentText();
checkCurrentText();
} else {
_cachedRanges.clear();
rehighlight();
}
}
QString SpellingHighlighter::documentText() {
return _lastPlainText;
}
void SpellingHighlighter::updateDocumentText() {
_lastPlainText = document()->toRawText();
}
QString SpellingHighlighter::partDocumentText(int pos, int length) {
return _lastPlainText.mid(pos, length);
}
int SpellingHighlighter::size() {
return document()->characterCount() - 1;
}
QTextBlock SpellingHighlighter::findBlock(int pos) {
return document()->findBlock(pos);
}
std::vector<QTextBlock> SpellingHighlighter::blocksFromRange(
int pos,
int length) {
auto b = findBlock(pos);
auto blocks = std::vector<QTextBlock>{b};
const auto end = pos + length;
while (!b.contains(end) && (b != document()->end())) {
if ((b = b.next()).isValid()) {
blocks.push_back(b);
}
}
return blocks;
}
int SpellingHighlighter::compareDocumentText(
const QString &text,
int textPos,
int textLen) {
if (_lastPlainText.size() < textPos + textLen) {
return -1;
}
const auto p = base::StringViewMid(_lastPlainText, textPos, textLen);
if (p.isNull()) {
return -1;
}
return text.compare(p, Qt::CaseSensitive);
}
void SpellingHighlighter::addSpellcheckerActions(
not_null<QMenu*> parentMenu,
QTextCursor cursorForPosition,
Fn<void()> showMenuCallback,
QPoint mousePosition) {
const auto menu = new QMenu(
ph::lng_spellchecker_submenu(ph::now),
parentMenu);
auto addToParentAndShow = [=](int) {
if (!menu->isEmpty()) {
using namespace Spelling::Helper;
if (IsContextMenuTop(parentMenu, mousePosition)) {
parentMenu->addSeparator();
parentMenu->addMenu(menu);
} else {
const auto first = parentMenu->actions().first();
parentMenu->insertMenu(first, menu);
parentMenu->insertSeparator(first);
}
}
showMenuCallback();
};
fillSpellcheckerMenu(menu, cursorForPosition, addToParentAndShow);
}
void SpellingHighlighter::showSpellcheckerMenu() {
auto menu = std::make_unique<QMenu>();
const auto raw = menu.get();
const auto cursor = _textEdit->textCursor();
auto rect = _textEdit->cursorRect(cursor);
rect.setTopLeft(_textEdit->viewport()->mapToGlobal(rect.topLeft()));
auto show = [=, menu = std::move(menu)](int firstSuggestion) mutable {
if (!menu->isEmpty()) {
_menu = base::make_unique_q<Ui::PopupMenu>(
_textEdit,
menu.release(),
_field->st().menu);
_menu->setForcedVerticalOrigin(
Ui::PopupMenu::VerticalOrigin::Top);
base::install_event_filter(_menu.get(), [=](
not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e.get());
if (SpellcheckSuggestEvent(k)) {
auto event = QKeyEvent(
QEvent::KeyPress,
Qt::Key_Down,
Qt::KeyboardModifiers());
_menu->menu()->handleKeyPress(&event);
}
}
return base::EventFilterResult::Continue;
});
_menu->popup(rect.topLeft());
if (_menu && firstSuggestion >= 0) {
_menu->menu()->setSelected(firstSuggestion, false);
}
}
};
fillSpellcheckerMenu(raw, cursor, std::move(show));
}
void SpellingHighlighter::fillSpellcheckerMenu(
not_null<QMenu*> menu,
QTextCursor cursorForPosition,
FnMut<void(int firstSuggestionIndex)> show) {
const auto customItem = !Platform::Spellchecker::IsSystemSpellchecker()
&& _customContextMenuItem.has_value();
cursorForPosition.select(QTextCursor::WordUnderCursor);
// There is no reason to call async work if the word is skippable.
const auto skippable = [&] {
const auto &[p, l] = RangeFromCursorSelection(cursorForPosition);
const auto e = FindEntities(findBlock(p).text());
return (!l
|| isSkippableWord(p, l)
|| IntersectsAnyOfEntities(p, l, e));
}();
if (customItem) {
menu->addAction(
_customContextMenuItem->title,
_customContextMenuItem->callback);
}
if (skippable) {
show(-1);
return;
}
const auto word = cursorForPosition.selectedText();
auto fillMenu = [
=,
show = std::move(show),
menu = std::move(menu)
](
bool isCorrect,
const auto &suggestions,
const auto &newTextCursor) mutable {
auto firstSuggestionIndex = -1;
const auto guard = gsl::finally([&] {
show(firstSuggestionIndex);
});
const auto addSeparator = [&] {
if (!menu->isEmpty()) {
menu->addSeparator();
}
};
if (isCorrect) {
if (Platform::Spellchecker::IsWordInDictionary(word)) {
addSeparator();
auto remove = [=] {
Platform::Spellchecker::RemoveWord(word);
checkCurrentText();
};
menu->addAction(
ph::lng_spellchecker_remove(ph::now),
std::move(remove));
}
return;
}
addSeparator();
auto add = [=] {
Platform::Spellchecker::AddWord(word);
checkCurrentText();
};
menu->addAction(ph::lng_spellchecker_add(ph::now), std::move(add));
auto ignore = [=] {
Platform::Spellchecker::IgnoreWord(word);
checkCurrentText();
};
menu->addAction(
ph::lng_spellchecker_ignore(ph::now),
std::move(ignore));
if (suggestions.empty()) {
return;
}
addSeparator();
for (const auto &suggestion : suggestions) {
if (firstSuggestionIndex < 0) {
firstSuggestionIndex = menu->actions().size();
}
auto replaceWord = [=] {
const auto oldTextCursor = _textEdit->textCursor();
_textEdit->setTextCursor(newTextCursor);
_textEdit->textCursor().insertText(suggestion);
_textEdit->setTextCursor(oldTextCursor);
};
menu->addAction(suggestion, std::move(replaceWord));
}
};
const auto weak = base::make_weak(this);
crl::async([=,
newTextCursor = std::move(cursorForPosition),
fillMenu = std::move(fillMenu),
word = std::move(word)
]() mutable {
const auto isCorrect = Platform::Spellchecker::CheckSpelling(word);
auto suggestions = std::vector<QString>();
if (!isCorrect) {
Platform::Spellchecker::FillSuggestionList(word, &suggestions);
}
crl::on_main(weak, [=,
newTextCursor = std::move(newTextCursor),
suggestions = std::move(suggestions),
fillMenu = std::move(fillMenu)
]() mutable {
fillMenu(
isCorrect,
std::move(suggestions),
std::move(newTextCursor));
});
});
}
} // namespace Spellchecker

View File

@@ -0,0 +1,132 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <QtWidgets/QWidget> // input_fields.h
#include "base/timer.h"
#include "spellcheck/platform/platform_spellcheck.h"
#include "spellcheck/spellcheck_types.h"
#include "ui/widgets/fields/input_field.h"
#include <QtGui/QSyntaxHighlighter>
#include <QtGui/QTextBlock>
#include <QtWidgets/QMenu>
#include <QtWidgets/QTextEdit>
#include <rpl/event_stream.h>
namespace Ui {
struct ExtendedContextMenu;
class PopupMenu;
} // namespace Ui
namespace Spellchecker {
class SpellingHighlighter final : public QSyntaxHighlighter {
public:
struct CustomContextMenuItem {
QString title;
Fn<void()> callback;
};
SpellingHighlighter(
not_null<Ui::InputField*> field,
rpl::producer<bool> enabled,
std::optional<CustomContextMenuItem> customContextMenuItem
= std::nullopt);
void contentsChange(int pos, int removed, int added);
void checkCurrentText();
bool enabled();
auto contextMenuCreated() {
return _contextMenuCreated.events();
}
// Windows system spellchecker forces us to perform spell operations
// In another thread, so the word check and getting a list of suggestions
// Are run asynchronously.
// And then the context menu is filled in the main thread.
void addSpellcheckerActions(
not_null<QMenu*> parentMenu,
QTextCursor cursorForPosition,
Fn<void()> showMenuCallback,
QPoint mousePosition);
void fillSpellcheckerMenu(
not_null<QMenu*> menu,
QTextCursor cursorForPosition,
FnMut<void(int firstSuggestionIndex)> show);
protected:
void highlightBlock(const QString &text) override;
bool eventFilter(QObject *o, QEvent *e) override;
private:
void updatePalette();
void setEnabled(bool enabled);
void checkText(const QString &text);
void showSpellcheckerMenu();
void invokeCheckText(
int textPosition,
int textLength,
Fn<void(MisspelledWords &&ranges)> callback);
void checkChangedText();
void checkSingleWord(const MisspelledWord &singleWord);
MisspelledWords filterSkippableWords(MisspelledWords &ranges);
bool isSkippableWord(const MisspelledWord &range);
bool isSkippableWord(int position, int length);
bool hasUnspellcheckableTag(int begin, int length);
MisspelledWord getWordUnderPosition(int position);
QString documentText();
void updateDocumentText();
QString partDocumentText(int pos, int length);
int compareDocumentText(const QString &text, int textPos, int textLen);
QString _lastPlainText;
std::vector<QTextBlock> blocksFromRange(int pos, int length);
int size();
QTextBlock findBlock(int pos);
int _countOfCheckingTextAsync = 0;
QTextCharFormat _misspelledFormat;
QTextCursor _cursor;
MisspelledWords _cachedRanges;
EntitiesInText _cachedSkippableEntities;
int _addedSymbols = 0;
int _removedSymbols = 0;
int _lastPosition = 0;
bool _enabled = true;
bool _isLastKeyRepeat = false;
base::Timer _coldSpellcheckingTimer;
not_null<Ui::InputField*> _field;
not_null<QTextEdit*> _textEdit;
base::unique_qptr<Ui::PopupMenu> _menu;
const std::optional<CustomContextMenuItem> _customContextMenuItem;
rpl::lifetime _lifetime;
using ContextMenu = Ui::InputField::ExtendedContextMenu;
rpl::event_stream<ContextMenu> _contextMenuCreated;
};
} // namespace Spellchecker

View File

@@ -0,0 +1,64 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/spelling_highlighter.h"
#include "ui/platform/ui_platform_utility.h"
#include "styles/style_widgets.h"
#include "styles/palette.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QScreen>
namespace Spelling::Helper {
namespace {
constexpr auto kFormattingItem = 1;
constexpr auto kSpellingItem = 1;
} // namespace
bool IsContextMenuTop(not_null<QMenu*> menu, QPoint mousePosition) {
const auto &st = st::defaultMenu;
const auto &stPopup = st::defaultPopupMenu;
const auto itemHeight = st.itemPadding.top()
+ st.itemStyle.font->height
+ st.itemPadding.bottom();
const auto sepHeight = st.separator.padding.top()
+ st.separator.width
+ st.separator.padding.bottom();
const auto line = st::lineWidth;
const auto p = Ui::Platform::TranslucentWindowsSupported()
? stPopup.shadow.extend
: style::margins(line, line, line, line);
const auto additional = kFormattingItem + kSpellingItem;
const auto actions = menu->actions() | ranges::to_vector;
auto sepCount = ranges::count_if(actions, &QAction::isSeparator);
auto itemsCount = actions.size() - sepCount;
sepCount += additional;
itemsCount += additional;
const auto w = mousePosition - QPoint(0, p.top());
const auto screen = QGuiApplication::screenAt(mousePosition);
if (!screen) {
return false;
}
const auto r = screen->availableGeometry();
const auto height = itemHeight * itemsCount
+ sepHeight * sepCount
+ p.top()
+ stPopup.scrollPadding.top()
+ stPopup.scrollPadding.bottom()
+ p.bottom();
return (w.y() + height - p.bottom() > r.y() + r.height());
}
} // namespace Spelling::Helper

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace Spelling::Helper {
[[nodiscard]] bool IsContextMenuTop(
not_null<QMenu*> menu,
QPoint mousePosition);
} // namespace Spelling::Helper

View File

@@ -0,0 +1,640 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/third_party/hunspell_controller.h"
#include "spellcheck/spellcheck_value.h"
#include <mutex>
#include <shared_mutex>
#include <QDir>
#include <QFileInfo>
#include <hunspell/hunspell.hxx>
#if __has_include(<glib/glib.hpp>)
#include <glib/glib.hpp>
using namespace gi::repository;
#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // __has_include(<glib/glib.hpp>)
#include <QTextCodec>
#endif // Qt < 6.0.0
namespace Platform::Spellchecker::ThirdParty {
namespace {
using WordsMap = std::map<QChar::Script, std::vector<QString>>;
// Maximum number of words in the custom spellcheck dictionary.
constexpr auto kMaxSyncableDictionaryWords = 1300;
constexpr auto kTimeLimitSuggestion = crl::time(1000);
#ifdef Q_OS_WIN
const auto kLineBreak = QByteArrayLiteral("\r\n");
#else // Q_OS_WIN
const auto kLineBreak = QByteArrayLiteral("\n");
#endif // Q_OS_WIN
struct PathPair {
QByteArray aff;
QByteArray dic;
};
[[nodiscard]] PathPair PreparePaths(const QString &aff, const QString &dic) {
const auto convert = [&](const QString &path) {
const auto result = QDir::toNativeSeparators(path).toUtf8();
#ifdef Q_OS_WIN
return "\\\\?\\" + result;
#else // Q_OS_WIN
return result;
#endif // !Q_OS_WIN
};
return {
.aff = convert(aff),
.dic = convert(dic),
};
}
auto LocaleNameFromLangId(int langId) {
return ::Spellchecker::LocaleFromLangId(langId).name();
}
QString CustomDictionaryPath() {
return QStringLiteral("%1/%2").arg(
::Spellchecker::WorkingDirPath(),
"custom");
}
[[nodiscard]] Hunspell LoadUtfInitializer() {
const auto full = [&](const QString &name) {
return ::Spellchecker::WorkingDirPath() + '/' + name;
};
const auto aff = full(u"utf_helper.aff"_q);
const auto dic = full(u"utf_helper.dic"_q);
if (!QFile::exists(aff)) {
QDir().mkpath(::Spellchecker::WorkingDirPath());
auto f = QFile(aff);
if (f.open(QIODevice::WriteOnly)) {
f.write("SET UTF-8" + kLineBreak);
}
}
if (!QFile::exists(dic)) {
auto f = QFile(dic);
if (f.open(QIODevice::WriteOnly)) {
f.write("1" + kLineBreak + "Zzz" + kLineBreak);
}
}
const auto prepared = PreparePaths(aff, dic);
return Hunspell(prepared.aff.constData(), prepared.dic.constData());
}
class CharsetConverter final {
public:
CharsetConverter(const std::string &charset)
#if __has_include(<glib/glib.hpp>)
: _charset(charset)
#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // __has_include(<glib/glib.hpp>)
: _codec(QTextCodec::codecForName(charset.c_str()))
#endif // Qt < 6.0.0
{}
[[nodiscard]] bool isValid() const {
#if __has_include(<glib/glib.hpp>)
const uchar empty[] = "";
return GLib::convert(empty, 0, _charset, "UTF-8")
&& GLib::convert(empty, 0, "UTF-8", _charset);
#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // __has_include(<glib/glib.hpp>)
return _codec;
#else // Qt < 6.0.0
return false;
#endif // Qt >= 6.0.0 && !__has_include(<glib/glib.hpp>)
}
[[nodiscard]] std::string fromUnicode(const QString &data) {
#if __has_include(<glib/glib.hpp>)
const auto utf8 = data.toStdString();
return GLib::convert(
reinterpret_cast<const uchar*>(utf8.data()),
utf8.size(),
_charset,
"UTF-8",
nullptr,
nullptr) | ranges::to<std::string>;
#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // __has_include(<glib/glib.hpp>)
return _codec->fromUnicode(data).toStdString();
#else // Qt < 6.0.0
return {};
#endif // Qt >= 6.0.0 && !__has_include(<glib/glib.hpp>)
}
[[nodiscard]] QString toUnicode(const std::string &data) {
#if __has_include(<glib/glib.hpp>)
return QString::fromStdString(GLib::convert(
reinterpret_cast<const uchar*>(data.data()),
data.size(),
"UTF-8",
_charset,
nullptr,
nullptr) | ranges::to<std::string>);
#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // __has_include(<glib/glib.hpp>)
return _codec->toUnicode(data.data(), data.size());
#else // Qt < 6.0.0
return {};
#endif // Qt >= 6.0.0 && !__has_include(<glib/glib.hpp>)
}
private:
#if __has_include(<glib/glib.hpp>)
std::string _charset;
#elif QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // __has_include(<glib/glib.hpp>)
QTextCodec *_codec;
#endif // Qt < 6.0.0
};
class HunspellEngine {
public:
HunspellEngine(const QString &lang);
~HunspellEngine() = default;
bool isValid() const;
bool spell(const QString &word) const;
void suggest(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions);
QString lang();
QChar::Script script();
HunspellEngine(const HunspellEngine &) = delete;
HunspellEngine &operator=(const HunspellEngine &) = delete;
private:
QString _lang;
QChar::Script _script;
std::unique_ptr<Hunspell> _hunspell;
std::unique_ptr<CharsetConverter> _converter;
};
class HunspellService {
public:
HunspellService();
~HunspellService();
void updateLanguages(std::vector<QString> langs);
std::vector<QString> activeLanguages();
[[nodiscard]] bool checkSpelling(const QString &wordToCheck);
void fillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions);
void addWord(const QString &word);
void removeWord(const QString &word);
void ignoreWord(const QString &word);
bool isWordInDictionary(const QString &word);
private:
void writeToFile();
void readFile();
std::vector<QString> &addedWords(const QString &word);
std::shared_ptr<std::vector<std::unique_ptr<HunspellEngine>>> _engines;
std::vector<QString> _activeLanguages;
// Use an empty Hunspell dictionary to fill it with our remembered words
// for getting suggests.
std::unique_ptr<Hunspell> _customDict;
WordsMap _ignoredWords;
WordsMap _addedWords;
std::shared_ptr<std::atomic<int>> _epoch;
std::atomic<int> _suggestionsEpoch = 0;
std::shared_ptr<std::shared_mutex> _engineMutex;
};
HunspellEngine::HunspellEngine(const QString &lang)
: _lang(lang)
, _script(::Spellchecker::LocaleToScriptCode(lang)) {
const auto workingDir = ::Spellchecker::WorkingDirPath();
if (workingDir.isEmpty()) {
return;
}
const auto rawPath = QString("%1/%2/%2").arg(workingDir, lang);
const auto affPath = rawPath + ".aff";
const auto dicPath = rawPath + ".dic";
if (!QFileInfo(affPath).isFile() || !QFileInfo(dicPath).isFile()) {
return;
}
const auto prepared = PreparePaths(affPath, dicPath);
_hunspell = std::make_unique<Hunspell>(
prepared.aff.constData(),
prepared.dic.constData());
_converter = std::make_unique<CharsetConverter>(
_hunspell->get_dic_encoding());
if (!_converter->isValid()) {
_hunspell.reset();
}
}
bool HunspellEngine::isValid() const {
return _hunspell != nullptr;
}
bool HunspellEngine::spell(const QString &word) const {
return _hunspell->spell(_converter->fromUnicode(word));
}
void HunspellEngine::suggest(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions) {
const auto stdWord = _converter->fromUnicode(wrongWord);
for (const auto &guess : _hunspell->suggest(stdWord)) {
if (optionalSuggestions->size() == kMaxSuggestions) {
return;
}
const auto qguess = _converter->toUnicode(guess);
if (ranges::contains(*optionalSuggestions, qguess)) {
continue;
}
optionalSuggestions->push_back(qguess);
}
}
QString HunspellEngine::lang() {
return _lang;
}
QChar::Script HunspellEngine::script() {
return _script;
}
std::vector<QString> HunspellService::activeLanguages() {
return _activeLanguages;
}
// Thread: Any.
HunspellService::HunspellService()
: _engines(std::make_shared<std::vector<std::unique_ptr<HunspellEngine>>>())
, _customDict(std::make_unique<Hunspell>("", ""))
, _epoch(std::make_shared<std::atomic<int>>(0))
, _engineMutex(std::make_shared<std::shared_mutex>()) {
// This is not perfectly safe, but should be mostly fine.
static const auto UtfInitializer = LoadUtfInitializer();
readFile();
}
// Thread: Main.
HunspellService::~HunspellService() {
std::unique_lock lock(*_engineMutex);
}
// Thread: Main.
std::vector<QString> &HunspellService::addedWords(const QString &word) {
return _addedWords[::Spellchecker::WordScript(word)];
}
// Thread: Main.
void HunspellService::updateLanguages(std::vector<QString> langs) {
Expects(_suggestionsEpoch.load() == 0);
*_epoch += 1;
_activeLanguages.clear();
const auto savedEpoch = _epoch.get()->load();
crl::async([=,
epoch = _epoch,
engineMutex = _engineMutex,
engines = _engines] {
using UniqueEngine = std::unique_ptr<HunspellEngine>;
const auto engineLangFilter = [&](const UniqueEngine &engine) {
return engine ? ranges::contains(langs, engine->lang()) : false;
};
if (savedEpoch != epoch.get()->load()) {
return;
}
const auto engineLang = [](const UniqueEngine &engine) {
return engine ? engine->lang() : QString();
};
const auto missedLangs = [&] {
std::shared_lock lock(*engineMutex);
return ranges::views::all(
langs
) | ranges::views::filter([&](auto &lang) {
return !ranges::contains(*engines, lang, engineLang);
}) | ranges::to_vector;
}();
// Added new enabled engines.
auto localEngines = ranges::views::all(
missedLangs
) | ranges::views::transform([&](auto &lang) -> UniqueEngine {
if (savedEpoch != epoch.get()->load()) {
return nullptr;
}
auto engine = std::make_unique<HunspellEngine>(lang);
if (!engine->isValid()) {
return nullptr;
}
return engine;
}) | ranges::to_vector;
if (savedEpoch != epoch.get()->load()) {
return;
}
{
std::unique_lock lock(*engineMutex);
*engines = ranges::views::concat(
*engines, localEngines
) | ranges::views::filter(
// All filtered objects will be automatically released.
engineLangFilter
) | ranges::views::transform([](auto &engine) {
return std::move(engine);
}) | ranges::to_vector;
}
crl::on_main([=] {
if (savedEpoch != epoch.get()->load()) {
return;
}
*epoch = 0;
_activeLanguages = ranges::views::all(
*engines
) | ranges::views::transform(&HunspellEngine::lang)
| ranges::to_vector;
::Spellchecker::UpdateSupportedScripts(_activeLanguages);
});
});
}
// Thread: Any.
bool HunspellService::checkSpelling(const QString &wordToCheck) {
const auto wordScript = ::Spellchecker::WordScript(wordToCheck);
if (ranges::contains(_ignoredWords[wordScript], wordToCheck)) {
return true;
}
if (ranges::contains(_addedWords[wordScript], wordToCheck)) {
return true;
}
std::shared_lock lock(*_engineMutex);
for (const auto &engine : *_engines) {
if (wordScript != engine->script()) {
continue;
}
if (engine->spell(wordToCheck)) {
return true;
}
}
return false;
}
// Thread: Any.
void HunspellService::fillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions) {
const auto wordScript = ::Spellchecker::WordScript(wrongWord);
const auto customGuesses = _customDict->suggest(wrongWord.toStdString());
*optionalSuggestions = ranges::views::all(
customGuesses
) | ranges::views::take(
kMaxSuggestions
) | ranges::views::transform([](auto &guess) {
return QString::fromStdString(guess);
}) | ranges::to_vector;
const auto startTime = crl::now();
_suggestionsEpoch++;
const auto savedEpoch = _suggestionsEpoch.load();
{
std::shared_lock lock(*_engineMutex);
for (const auto &engine : *_engines) {
if (_suggestionsEpoch.load() > savedEpoch) {
// There is a newer request to fill suggestion list,
// So we should drop the current one.
optionalSuggestions->clear();
break;
}
if (optionalSuggestions->size() == kMaxSuggestions
|| ((crl::now() - startTime) > kTimeLimitSuggestion)) {
break;
}
if (wordScript != engine->script()) {
continue;
}
engine->suggest(wrongWord, optionalSuggestions);
}
}
_suggestionsEpoch--;
}
// Thread: Main.
void HunspellService::ignoreWord(const QString &word) {
const auto wordScript = ::Spellchecker::WordScript(word);
_customDict->add(word.toStdString());
_ignoredWords[wordScript].push_back(word);
}
// Thread: Main.
bool HunspellService::isWordInDictionary(const QString &word) {
return ranges::contains(addedWords(word), word);
}
// Thread: Main.
void HunspellService::addWord(const QString &word) {
const auto count = ranges::accumulate(
ranges::views::values(_addedWords),
0,
ranges::plus(),
&std::vector<QString>::size);
if (count > kMaxSyncableDictionaryWords) {
return;
}
_customDict->add(word.toStdString());
addedWords(word).push_back(word);
writeToFile();
}
// Thread: Main.
void HunspellService::removeWord(const QString &word) {
_customDict->remove(word.toStdString());
auto &vector = addedWords(word);
vector.erase(ranges::remove(vector, word), end(vector));
writeToFile();
}
// Thread: Main.
void HunspellService::writeToFile() {
auto f = QFile(CustomDictionaryPath());
if (!f.open(QIODevice::WriteOnly)) {
return;
}
auto &&temp = ranges::views::join(
ranges::views::values(_addedWords)
) | ranges::views::transform([&](auto &str) {
return str + kLineBreak;
});
const auto result = ranges::accumulate(std::move(temp), QString{});
f.write(result.toUtf8());
f.close();
}
// Thread: Main.
void HunspellService::readFile() {
using namespace ::Spellchecker;
auto f = QFile(CustomDictionaryPath());
if (const auto info = QFileInfo(f);
!info.isFile()
|| (info.size() > 100 * 1024)
|| !f.open(QIODevice::ReadOnly)) {
if (info.isDir()) {
QDir(info.path()).removeRecursively();
}
return;
}
const auto data = f.readAll();
f.close();
if (data.isEmpty()) {
return;
}
// {"a", "1", "β"};
auto splitedWords = QString::fromUtf8(data).split(kLineBreak)
| ranges::to_vector
| ranges::actions::sort
| ranges::actions::unique;
auto filteredWords = (
splitedWords
) | ranges::views::filter([](auto &word) {
// Ignore words with mixed scripts or non-words characters.
return !word.isEmpty() && !IsWordSkippable(word, false);
}) | ranges::views::take(
kMaxSyncableDictionaryWords
) | ranges::views::transform([](auto &word) {
return std::move(word);
}) | ranges::to_vector;
ranges::for_each(filteredWords, [&](auto &word) {
_customDict->add(word.toStdString());
});
// {{"a"}, {"β"}};
auto groupedWords = ranges::views::all(
filteredWords
) | ranges::views::chunk_by([](auto &a, auto &b) {
return WordScript(a) == WordScript(b);
}) | ranges::views::transform([](auto &&rng) {
return rng | ranges::to_vector;
}) | ranges::to_vector;
// {QChar::Script_Latin, QChar::Script_Greek};
auto scripts = ranges::views::all(
groupedWords
) | ranges::views::transform([](auto &vector) {
return WordScript(vector.front());
}) | ranges::to_vector;
// {QChar::Script_Latin : {"a"}, QChar::Script_Greek : {"β"}};
auto &&zip = ranges::views::zip(
scripts, groupedWords
);
_addedWords = zip | ranges::to<WordsMap>();
}
////// End of HunspellService class.
HunspellService &SharedSpellChecker() {
static auto spellchecker = HunspellService();
return spellchecker;
}
} // namespace
bool CheckSpelling(const QString &wordToCheck) {
return SharedSpellChecker().checkSpelling(wordToCheck);
}
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions) {
SharedSpellChecker().fillSuggestionList(wrongWord, optionalSuggestions);
}
void AddWord(const QString &word) {
SharedSpellChecker().addWord(word);
}
void RemoveWord(const QString &word) {
SharedSpellChecker().removeWord(word);
}
void IgnoreWord(const QString &word) {
SharedSpellChecker().ignoreWord(word);
}
bool IsWordInDictionary(const QString &wordToCheck) {
return SharedSpellChecker().isWordInDictionary(wordToCheck);
}
void UpdateLanguages(std::vector<int> languages) {
const auto languageCodes = ranges::views::all(
languages
) | ranges::views::transform(
LocaleNameFromLangId
) | ranges::to_vector;
::Spellchecker::UpdateSupportedScripts(std::vector<QString>());
SharedSpellChecker().updateLanguages(languageCodes);
}
std::vector<QString> ActiveLanguages() {
return SharedSpellChecker().activeLanguages();
}
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords) {
*misspelledWords = ::Spellchecker::RangesFromText(
text,
[](const QString &word) {
return !::Spellchecker::IsWordSkippable(word)
&& CheckSpelling(word);
});
}
} // namespace Platform::Spellchecker::ThirdParty

View File

@@ -0,0 +1,31 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_spellcheck.h"
namespace Platform::Spellchecker::ThirdParty {
[[nodiscard]] bool CheckSpelling(const QString &wordToCheck);
[[nodiscard]] bool IsWordInDictionary(const QString &wordToCheck);
std::vector<QString> ActiveLanguages();
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *optionalSuggestions);
void AddWord(const QString &word);
void RemoveWord(const QString &word);
void IgnoreWord(const QString &word);
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords);
void UpdateLanguages(std::vector<int> languages);
} // namespace Platform::Spellchecker::ThirdParty

View File

@@ -0,0 +1,39 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/third_party/language_cld3.h"
#include <nnet_language_identifier.h>
namespace Platform::Language {
LanguageId Recognize(QStringView text) {
using chrome_lang_id::NNetLanguageIdentifier;
constexpr auto kMinNumBytes = 0;
constexpr auto kMaxNumBytes = 1000;
constexpr auto kMaxLangs = 3;
auto lang_id = NNetLanguageIdentifier(kMinNumBytes, kMaxNumBytes);
const auto string = text.toUtf8().toStdString();
const auto results = lang_id.FindTopNMostFreqLangs(string, kMaxLangs);
auto maxRatio = 0.;
auto final = NNetLanguageIdentifier::Result();
for (const auto &result : results) {
const auto ratio = result.probability * result.proportion;
if (ratio > maxRatio) {
maxRatio = ratio;
final = result;
}
}
if (final.language == NNetLanguageIdentifier::kUnknown) {
return {};
}
return { QLocale(QString::fromStdString(final.language)).language() };
}
} // namespace Platform::Language

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_language.h"
namespace Platform::Language {
} // namespace Platform::Language

View File

@@ -0,0 +1,59 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/third_party/spellcheck_hunspell.h"
#include "spellcheck/third_party/hunspell_controller.h"
namespace Platform::Spellchecker {
void Init() {
}
std::vector<QString> ActiveLanguages() {
return ThirdParty::ActiveLanguages();
}
bool CheckSpelling(const QString &wordToCheck) {
return ThirdParty::CheckSpelling(wordToCheck);
}
void FillSuggestionList(
const QString &wrongWord,
std::vector<QString> *variants) {
ThirdParty::FillSuggestionList(wrongWord, variants);
}
void AddWord(const QString &word) {
ThirdParty::AddWord(word);
}
void RemoveWord(const QString &word) {
ThirdParty::RemoveWord(word);
}
void IgnoreWord(const QString &word) {
ThirdParty::IgnoreWord(word);
}
bool IsWordInDictionary(const QString &wordToCheck) {
return ThirdParty::IsWordInDictionary(wordToCheck);
}
void CheckSpellingText(
const QString &text,
MisspelledWords *misspelledWords) {
ThirdParty::CheckSpellingText(text, misspelledWords);
}
bool IsSystemSpellchecker() {
return false;
}
void UpdateLanguages(std::vector<int> languages) {
ThirdParty::UpdateLanguages(languages);
}
} // namespace Platform::Spellchecker

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "spellcheck/platform/platform_spellcheck.h"
namespace Platform::Spellchecker {
} // namespace Platform::Spellchecker