kdelibs/kdeui/spell/kspeller.cpp
Ivailo Monev 2b45ac1a73 kdeui: fix spell check of words at ends
Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2024-04-11 14:39:22 +03:00

345 lines
9.2 KiB
C++

/* This file is part of the KDE libraries
Copyright (C) 2023 Ivailo Monev <xakepa10@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2, as published by the Free Software Foundation.
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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "kspeller.h"
#include "kconfiggroup.h"
#include "kdebug.h"
#include <QLocale>
#include <stdlib.h>
#include <enchant.h>
class KSpellerPrivate
{
public:
KSpellerPrivate(KConfig *config);
~KSpellerPrivate();
static void dictsCallback(const char* const lang_tag,
const char* const provider_name,
const char* const provider_desc,
const char* const provider_file,
void* user_data);
KConfig* config;
QString dictionary;
QStringList dictionaries;
QStringList personalwords;
QString text;
bool interrupt;
EnchantBroker* enchantbroker;
EnchantDict* enchantdict;
};
KSpellerPrivate::KSpellerPrivate(KConfig *kconfig)
: config(kconfig),
interrupt(false),
enchantbroker(nullptr),
enchantdict(nullptr)
{
enchantbroker = enchant_broker_init();
if (Q_UNLIKELY(!enchantbroker)) {
kWarning() << "Could not create broker";
return;
}
enchant_broker_list_dicts(
enchantbroker,
KSpellerPrivate::dictsCallback,
this
);
}
KSpellerPrivate::~KSpellerPrivate()
{
if (enchantbroker) {
if (enchantdict) {
enchant_broker_free_dict(enchantbroker, enchantdict);
}
enchant_broker_free(enchantbroker);
}
}
void KSpellerPrivate::dictsCallback(const char* const lang_tag,
const char* const provider_name,
const char* const provider_desc,
const char* const provider_file,
void* user_data)
{
// qDebug() << Q_FUNC_INFO << lang_tag << provider_name << provider_desc << provider_file;
Q_UNUSED(provider_name);
Q_UNUSED(provider_desc);
Q_UNUSED(provider_file);
KSpellerPrivate* kspellprivate = static_cast<KSpellerPrivate*>(user_data);
Q_ASSERT(kspellprivate);
kspellprivate->dictionaries.append(QString::fromLatin1(lang_tag));
}
KSpeller::KSpeller(KConfig *config, QObject *parent)
: QObject(parent),
d(new KSpellerPrivate(config))
{
if (Q_UNLIKELY(!config)) {
setDictionary(KSpeller::defaultLanguage());
kWarning() << "Null config passed";
return;
}
KConfigGroup spellgroup = config->group("Spelling");
setDictionary(spellgroup.readEntry("defaultLanguage", KSpeller::defaultLanguage()));
const QStringList personalWords = spellgroup.readEntry("personalWords", QStringList());
foreach (const QString &word, personalWords) {
addToPersonal(word);
}
}
KSpeller::~KSpeller()
{
if (d->config) {
KConfigGroup spellgroup = d->config->group("Spelling");
spellgroup.writeEntry("personalWords", d->personalwords);
}
delete d;
}
QString KSpeller::dictionary() const
{
return d->dictionary;
}
QStringList KSpeller::dictionaries() const
{
return d->dictionaries;
}
bool KSpeller::setDictionary(const QString &dictionary)
{
if (Q_UNLIKELY(!d->enchantbroker || dictionary.isEmpty())) {
return false;
}
if (d->enchantdict) {
enchant_broker_free_dict(d->enchantbroker, d->enchantdict);
d->enchantdict = nullptr;
}
const QByteArray dictionarybytes = dictionary.toUtf8();
d->enchantdict = enchant_broker_request_dict(d->enchantbroker, dictionarybytes.constData());
if (Q_UNLIKELY(!d->enchantdict)) {
kWarning() << "Could not create dictionary" << enchant_broker_get_error(d->enchantbroker);
return false;
}
d->dictionary = dictionary;
foreach (const QString &word, d->personalwords) {
const QByteArray wordbytes = word.toUtf8();
enchant_dict_add(
d->enchantdict,
wordbytes.constData(), wordbytes.size()
);
}
return true;
}
bool KSpeller::check(const QString &word)
{
if (Q_UNLIKELY(!d->enchantdict)) {
return true;
}
const QByteArray wordbytes = word.toUtf8();
const int enchantresult = enchant_dict_check(
d->enchantdict,
wordbytes.constData(), wordbytes.size()
);
if (Q_UNLIKELY(enchantresult < 0)) {
kWarning() << "Could not check word" << enchant_dict_get_error(d->enchantdict);
return true;
}
return (enchantresult == 0);
}
QStringList KSpeller::suggest(const QString &word)
{
QStringList result;
if (Q_UNLIKELY(!d->enchantdict)) {
return result;
}
const QByteArray wordbytes = word.toUtf8();
size_t enchantsuggestions = 0;
char **enchantwords = enchant_dict_suggest(
d->enchantdict,
wordbytes.constData(), wordbytes.size(),
&enchantsuggestions
);
for (size_t i = 0; i < enchantsuggestions; i++) {
result.append(QString::fromUtf8(enchantwords[i]));
::free(enchantwords[i]);
}
return result;
}
bool KSpeller::addToPersonal(const QString &word)
{
if (Q_UNLIKELY(!d->enchantdict)) {
return false;
}
const QByteArray wordbytes = word.toUtf8();
enchant_dict_add(
d->enchantdict,
wordbytes.constData(), wordbytes.size()
);
d->personalwords.append(word);
return true;
}
bool KSpeller::removeFromPersonal(const QString &word)
{
if (Q_UNLIKELY(!d->enchantdict)) {
return false;
}
const QByteArray wordbytes = word.toUtf8();
enchant_dict_remove(
d->enchantdict,
wordbytes.constData(), wordbytes.size()
);
d->personalwords.removeAll(word);
return true;
}
bool KSpeller::addToSession(const QString &word)
{
if (Q_UNLIKELY(!d->enchantdict)) {
return false;
}
const QByteArray wordbytes = word.toUtf8();
enchant_dict_add_to_session(
d->enchantdict,
wordbytes.constData(), wordbytes.size()
);
return true;
}
bool KSpeller::removeFromSession(const QString &word)
{
if (Q_UNLIKELY(!d->enchantdict)) {
return false;
}
const QByteArray wordbytes = word.toUtf8();
enchant_dict_remove_from_session(
d->enchantdict,
wordbytes.constData(), wordbytes.size()
);
return true;
}
QString KSpeller::defaultLanguage()
{
// NOTE: enchant_get_user_language() just returns the value of some environment variable used
// to specify localizations language and C.UTF-8 is not valid dictionary for example
QString result = QLocale::system().name();
if (result == QLatin1String("C")) {
result = QLatin1String("en");
}
// qDebug() << Q_FUNC_INFO << result;
return result;
}
void KSpeller::setText(const QString &text)
{
d->text = text;
}
QString KSpeller::text() const
{
return d->text;
}
void KSpeller::start()
{
// qDebug() << Q_FUNC_INFO << d->text.size() << d->text;
d->interrupt = false;
int wordstart = -1;
int counter = 0;
while (counter < d->text.size()) {
if (d->interrupt) {
break;
}
const bool atseparator = KSpeller::isWordSeparator(d->text.at(counter));
if (!atseparator && wordstart == -1) {
wordstart = counter;
} else if ((atseparator || (counter + 1) == d->text.size()) && wordstart != -1) {
const QString word = d->text.mid(wordstart, counter - wordstart + (atseparator ? 0 : 1));
// not worth checking if it is less than two characters
if (word.size() >= 2) {
// qDebug() << Q_FUNC_INFO << wordstart << counter << word;
if (!check(word)) {
emit misspelling(word, wordstart);
}
}
wordstart = -1;
}
counter++;
}
emit done();
}
void KSpeller::stop()
{
d->interrupt = true;
}
// the code is similar to the code in QTextEngine on purpose, kate uses different logic for
// selection tho
bool KSpeller::isWordSeparator(const QChar &c)
{
switch (c.toLatin1()) {
case '.':
case ',':
case '?':
case '!':
case '@':
case '#':
case '$':
case ':':
case ';':
case '-':
case '<':
case '>':
case '[':
case ']':
case '(':
case ')':
case '{':
case '}':
case '=':
case '/':
case '+':
case '%':
case '&':
case '^':
case '*':
case '\'':
case '"':
case '`':
case '~':
case '|': {
return true;
}
default: {
return c.isSpace();
}
}
}
#include "moc_kspeller.cpp"