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
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
143
Telegram/lib_spellcheck/spellcheck/platform/win/language_win.cpp
Normal file
143
Telegram/lib_spellcheck/spellcheck/platform/win/language_win.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
23
Telegram/lib_spellcheck/spellcheck/spellcheck_pch.h
Normal file
23
Telegram/lib_spellcheck/spellcheck/spellcheck_pch.h
Normal 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"
|
||||
57
Telegram/lib_spellcheck/spellcheck/spellcheck_types.h
Normal file
57
Telegram/lib_spellcheck/spellcheck/spellcheck_types.h
Normal 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;
|
||||
}
|
||||
};
|
||||
321
Telegram/lib_spellcheck/spellcheck/spellcheck_utils.cpp
Normal file
321
Telegram/lib_spellcheck/spellcheck/spellcheck_utils.cpp
Normal 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
|
||||
35
Telegram/lib_spellcheck/spellcheck/spellcheck_utils.h
Normal file
35
Telegram/lib_spellcheck/spellcheck/spellcheck_utils.h
Normal 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
|
||||
33
Telegram/lib_spellcheck/spellcheck/spellcheck_value.cpp
Normal file
33
Telegram/lib_spellcheck/spellcheck/spellcheck_value.cpp
Normal 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
|
||||
35
Telegram/lib_spellcheck/spellcheck/spellcheck_value.h
Normal file
35
Telegram/lib_spellcheck/spellcheck/spellcheck_value.h
Normal 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
|
||||
947
Telegram/lib_spellcheck/spellcheck/spelling_highlighter.cpp
Normal file
947
Telegram/lib_spellcheck/spellcheck/spelling_highlighter.cpp
Normal 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
|
||||
132
Telegram/lib_spellcheck/spellcheck/spelling_highlighter.h
Normal file
132
Telegram/lib_spellcheck/spellcheck/spelling_highlighter.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
640
Telegram/lib_spellcheck/spellcheck/third_party/hunspell_controller.cpp
vendored
Normal file
640
Telegram/lib_spellcheck/spellcheck/third_party/hunspell_controller.cpp
vendored
Normal 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
|
||||
31
Telegram/lib_spellcheck/spellcheck/third_party/hunspell_controller.h
vendored
Normal file
31
Telegram/lib_spellcheck/spellcheck/third_party/hunspell_controller.h
vendored
Normal 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
|
||||
39
Telegram/lib_spellcheck/spellcheck/third_party/language_cld3.cpp
vendored
Normal file
39
Telegram/lib_spellcheck/spellcheck/third_party/language_cld3.cpp
vendored
Normal 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
|
||||
13
Telegram/lib_spellcheck/spellcheck/third_party/language_cld3.h
vendored
Normal file
13
Telegram/lib_spellcheck/spellcheck/third_party/language_cld3.h
vendored
Normal 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
|
||||
59
Telegram/lib_spellcheck/spellcheck/third_party/spellcheck_hunspell.cpp
vendored
Normal file
59
Telegram/lib_spellcheck/spellcheck/third_party/spellcheck_hunspell.cpp
vendored
Normal 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
|
||||
13
Telegram/lib_spellcheck/spellcheck/third_party/spellcheck_hunspell.h
vendored
Normal file
13
Telegram/lib_spellcheck/spellcheck/third_party/spellcheck_hunspell.h
vendored
Normal 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
|
||||
Reference in New Issue
Block a user