kdeui: KSpeller and KSpellHighlighter optimization

2x to 3x faster on practical test cases

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
This commit is contained in:
Ivailo Monev 2024-04-07 18:46:21 +03:00
parent e774460cf3
commit 802b68d0a3
3 changed files with 81 additions and 69 deletions

View file

@ -21,7 +21,6 @@
#include "kdebug.h"
#include <QLocale>
#include <QTextBoundaryFinder>
#include <stdlib.h>
#include <enchant.h>
@ -271,43 +270,27 @@ void KSpeller::start()
{
// qDebug() << Q_FUNC_INFO << d->text.size() << d->text;
d->interrupt = false;
int wordstart = 0;
QTextBoundaryFinder finder(QTextBoundaryFinder::Word, d->text);
while (finder.toNextBoundary() >= 0) {
int wordstart = -1;
int counter = 0;
while (counter < d->text.size()) {
if (d->interrupt) {
break;
}
const QTextBoundaryFinder::BoundaryReasons boundary = finder.boundaryReasons();
if (boundary & QTextBoundaryFinder::StartWord) {
wordstart = finder.position();
}
if (boundary & QTextBoundaryFinder::EndWord) {
QString word = d->text.mid(wordstart, finder.position() - wordstart);
// remove whitespace at the start and end
while (!word.isEmpty() && word.at(0).isSpace()) {
word = word.mid(1, word.size() - 1);
wordstart++;
}
while (!word.isEmpty() && word.at(word.size() - 1).isSpace()) {
word = word.mid(0, word.size() - 1);
}
// chop punctuation
if (!word.isEmpty() && word.at(word.size() - 1).isPunct()) {
word = word.mid(0, word.size() - 1);
}
const bool atseparator = KSpeller::isWordSeparator(d->text.at(counter));
if (!atseparator && wordstart == -1) {
wordstart = counter;
} else if (atseparator && wordstart != -1) {
const QString word = d->text.mid(wordstart, counter - wordstart);
// not worth checking if it is less than two characters
if (word.size() < 2) {
continue;
}
// qDebug() << Q_FUNC_INFO << boundary << wordstart << finder.position() << word;
if (!check(word)) {
emit misspelling(word, wordstart);
if (word.size() >= 2) {
// qDebug() << Q_FUNC_INFO << wordstart << counter << word;
if (!check(word)) {
emit misspelling(word, wordstart);
}
}
wordstart = -1;
}
counter++;
}
emit done();
}
@ -317,4 +300,47 @@ 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"

View file

@ -52,6 +52,7 @@ public:
bool removeFromSession(const QString &word);
static QString defaultLanguage();
static bool isWordSeparator(const QChar c);
void setText(const QString &text);
QString text() const;

View file

@ -21,24 +21,24 @@
#include "kcolorscheme.h"
#include "kdebug.h"
#include <QTextBoundaryFinder>
class KSpellHighlighterPrivate
{
public:
KSpellHighlighterPrivate(KConfig *config);
KSpeller speller;
QTextCharFormat charformat;
QTextCharFormat emptyformat;
QTextCharFormat spellformat;
};
KSpellHighlighterPrivate::KSpellHighlighterPrivate(KConfig *config)
: speller(config)
{
charformat.setFontUnderline(true);
charformat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
spellformat.setFontUnderline(true);
spellformat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
// NOTE: same as the default of Kate
charformat.setUnderlineColor(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color());
spellformat.setUnderlineColor(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color());
}
KSpellHighlighter::KSpellHighlighter(KConfig *config, QTextEdit *parent)
@ -93,41 +93,26 @@ void KSpellHighlighter::highlightBlock(const QString &text)
if (text.isEmpty() || d->speller.dictionary().isEmpty()) {
return;
}
int wordstart = 0;
QTextBoundaryFinder finder(QTextBoundaryFinder::Word, text);
while (finder.toNextBoundary() >= 0) {
const QTextBoundaryFinder::BoundaryReasons boundary = finder.boundaryReasons();
if (boundary & QTextBoundaryFinder::StartWord) {
wordstart = finder.position();
}
if (boundary & QTextBoundaryFinder::EndWord) {
QString word = text.mid(wordstart, finder.position() - wordstart);
// remove whitespace at the start and end
while (!word.isEmpty() && word.at(0).isSpace()) {
word = word.mid(1, word.size() - 1);
wordstart++;
}
while (!word.isEmpty() && word.at(word.size() - 1).isSpace()) {
word = word.mid(0, word.size() - 1);
}
// chop punctuation
if (!word.isEmpty() && word.at(word.size() - 1).isPunct()) {
word = word.mid(0, word.size() - 1);
}
int wordstart = -1;
int counter = 0;
while (counter < text.size()) {
const bool atseparator = KSpeller::isWordSeparator(text.at(counter));
if (!atseparator && wordstart == -1) {
wordstart = counter;
} else if (atseparator && wordstart != -1) {
const QString word = text.mid(wordstart, counter - wordstart);
// not worth checking if it is less than two characters
if (word.size() < 2) {
continue;
}
if (!d->speller.check(word)) {
setFormat(wordstart, word.size(), d->charformat);
} else {
setFormat(wordstart, word.size(), QTextCharFormat());
if (word.size() >= 2) {
if (!d->speller.check(word)) {
setFormat(wordstart, word.size(), d->spellformat);
} else {
setFormat(wordstart, word.size(), d->emptyformat);
}
}
wordstart = -1;
}
counter++;
}
}