KTextEditor

spellcheck.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Michel Ludwig <michel.ludwig@kdemail.net>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "spellcheck.h"
8
9#include <QHash>
10#include <QTimer>
11#include <QtAlgorithms>
12
13#include <KActionCollection>
14#include <ktexteditor/view.h>
15
16#include "katedocument.h"
17#include "katehighlight.h"
18
19KateSpellCheckManager::KateSpellCheckManager(QObject *parent)
20 : QObject(parent)
21{
22}
23
24KateSpellCheckManager::~KateSpellCheckManager() = default;
25
26QStringList KateSpellCheckManager::suggestions(const QString &word, const QString &dictionary)
27{
28 Sonnet::Speller speller;
29 speller.setLanguage(dictionary);
30 return speller.suggest(word);
31}
32
33void KateSpellCheckManager::ignoreWord(const QString &word, const QString &dictionary)
34{
35 Sonnet::Speller speller;
36 speller.setLanguage(dictionary);
37 speller.addToSession(word);
38 Q_EMIT wordIgnored(word);
39}
40
41void KateSpellCheckManager::addToDictionary(const QString &word, const QString &dictionary)
42{
43 Sonnet::Speller speller;
44 speller.setLanguage(dictionary);
45 speller.addToPersonal(word);
46 Q_EMIT wordAddedToDictionary(word);
47}
48
49QList<KTextEditor::Range> KateSpellCheckManager::rangeDifference(KTextEditor::Range r1, KTextEditor::Range r2)
50{
51 Q_ASSERT(r1.contains(r2));
53 KTextEditor::Range before(r1.start(), r2.start());
54 KTextEditor::Range after(r2.end(), r1.end());
55 if (!before.isEmpty()) {
56 toReturn.push_back(before);
57 }
58 if (!after.isEmpty()) {
59 toReturn.push_back(after);
60 }
61 return toReturn;
62}
63
64namespace
65{
66bool lessThanRangeDictionaryPair(const QPair<KTextEditor::Range, QString> &s1, const QPair<KTextEditor::Range, QString> &s2)
67{
68 return s1.first.end() <= s2.first.start();
69}
70}
71
72QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range)
73{
74 QString defaultDict = doc->defaultDictionary();
76 QList<QPair<KTextEditor::MovingRange *, QString>> dictionaryRanges = doc->dictionaryRanges();
77 if (dictionaryRanges.isEmpty()) {
78 toReturn.push_back(RangeDictionaryPair(range, defaultDict));
79 return toReturn;
80 }
82 splitQueue.push_back(range);
83 while (!splitQueue.isEmpty()) {
84 bool handled = false;
85 KTextEditor::Range consideredRange = splitQueue.takeFirst();
86 for (QList<QPair<KTextEditor::MovingRange *, QString>>::iterator i = dictionaryRanges.begin(); i != dictionaryRanges.end(); ++i) {
87 KTextEditor::Range languageRange = *((*i).first);
88 KTextEditor::Range intersection = languageRange.intersect(consideredRange);
89 if (intersection.isEmpty()) {
90 continue;
91 }
92 toReturn.push_back(RangeDictionaryPair(intersection, (*i).second));
93 splitQueue += rangeDifference(consideredRange, intersection);
94 handled = true;
95 break;
96 }
97 if (!handled) {
98 // 'consideredRange' did not intersect with any dictionary range, so we add it with the default dictionary
99 toReturn.push_back(RangeDictionaryPair(consideredRange, defaultDict));
100 }
101 }
102 // finally, we still have to sort the list
103 std::stable_sort(toReturn.begin(), toReturn.end(), lessThanRangeDictionaryPair);
104 return toReturn;
105}
106
107QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *document,
108 KTextEditor::Range range,
109 const QString &dictionary,
110 bool singleLine,
111 bool returnSingleRange)
112{
114 if (range.isEmpty()) {
115 return toReturn;
116 }
117
118 KateHighlighting *highlighting = document->highlight();
119
120 QList<KTextEditor::Range> rangesToSplit;
121 if (!singleLine || range.onSingleLine()) {
122 rangesToSplit.push_back(range);
123 } else {
124 const int startLine = range.start().line();
125 const int startColumn = range.start().column();
126 const int endLine = range.end().line();
127 const int endColumn = range.end().column();
128 for (int line = startLine; line <= endLine; ++line) {
129 const int start = (line == startLine) ? startColumn : 0;
130 const int end = (line == endLine) ? endColumn : document->lineLength(line);
131 KTextEditor::Range toAdd(line, start, line, end);
132 if (!toAdd.isEmpty()) {
133 rangesToSplit.push_back(toAdd);
134 }
135 }
136 }
137 for (QList<KTextEditor::Range>::iterator i = rangesToSplit.begin(); i != rangesToSplit.end(); ++i) {
138 KTextEditor::Range rangeToSplit = *i;
140 const int startLine = rangeToSplit.start().line();
141 const int startColumn = rangeToSplit.start().column();
142 const int endLine = rangeToSplit.end().line();
143 const int endColumn = rangeToSplit.end().column();
144 bool inSpellCheckArea = false;
145 for (int line = startLine; line <= endLine; ++line) {
146 const auto kateTextLine = document->kateTextLine(line);
147 const int start = (line == startLine) ? startColumn : 0;
148 const int end = (line == endLine) ? endColumn : kateTextLine.length();
149 for (int i = start; i < end;) { // WARNING: 'i' has to be incremented manually!
150 int attr = kateTextLine.attribute(i);
151 const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
152 QString prefixFound = prefixStore.findPrefix(kateTextLine, i);
153 if (!document->highlight()->attributeRequiresSpellchecking(static_cast<unsigned int>(attr)) && prefixFound.isEmpty()) {
154 if (i == start) {
155 ++i;
156 continue;
157 } else if (inSpellCheckArea) {
158 KTextEditor::Range spellCheckRange(begin, KTextEditor::Cursor(line, i));
159 // work around Qt bug 6498
160 trimRange(document, spellCheckRange);
161 if (!spellCheckRange.isEmpty()) {
162 toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
163 if (returnSingleRange) {
164 return toReturn;
165 }
166 }
168 inSpellCheckArea = false;
169 }
170 } else if (!inSpellCheckArea) {
171 begin = KTextEditor::Cursor(line, i);
172 inSpellCheckArea = true;
173 }
174 if (!prefixFound.isEmpty()) {
175 i += prefixFound.length();
176 } else {
177 ++i;
178 }
179 }
180 }
181 if (inSpellCheckArea) {
182 KTextEditor::Range spellCheckRange(begin, rangeToSplit.end());
183 // work around Qt bug 6498
184 trimRange(document, spellCheckRange);
185 if (!spellCheckRange.isEmpty()) {
186 toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
187 if (returnSingleRange) {
188 return toReturn;
189 }
190 }
191 }
192 }
193
194 return toReturn;
195}
196
197QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range, bool singleLine)
198{
200 QList<RangeDictionaryPair> languageRangeList = spellCheckLanguageRanges(doc, range);
201 for (QList<RangeDictionaryPair>::iterator i = languageRangeList.begin(); i != languageRangeList.end(); ++i) {
202 const RangeDictionaryPair &p = *i;
203 toReturn += spellCheckWrtHighlightingRanges(doc, p.first, p.second, singleLine);
204 }
205 return toReturn;
206}
207
208void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, KTextEditor::Range replacementRange)
209{
210 const int attr = doc->kateTextLine(replacementRange.start().line()).attribute(replacementRange.start().column());
211 if (!doc->highlight()->getCharacterEncodings(attr).isEmpty() && doc->containsCharacterEncoding(replacementRange)) {
212 doc->replaceText(replacementRange, newWord);
213 doc->replaceCharactersByEncoding(KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length())));
214 } else {
215 doc->replaceText(replacementRange, newWord);
216 }
217}
218
219void KateSpellCheckManager::trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r)
220{
221 if (r.isEmpty()) {
222 return;
223 }
224 KTextEditor::Cursor cursor = r.start();
225 while (cursor < r.end()) {
226 if (doc->lineLength(cursor.line()) > 0 && !doc->characterAt(cursor).isSpace() && doc->characterAt(cursor).category() != QChar::Other_Control) {
227 break;
228 }
229 cursor.setColumn(cursor.column() + 1);
230 if (cursor.column() >= doc->lineLength(cursor.line())) {
231 cursor.setPosition(cursor.line() + 1, 0);
232 }
233 }
234 r.setStart(cursor);
235 if (r.isEmpty()) {
236 return;
237 }
238
239 cursor = r.end();
240 KTextEditor::Cursor prevCursor = cursor;
241 // the range cannot be empty now
242 do {
243 prevCursor = cursor;
244 if (cursor.column() <= 0) {
245 cursor.setPosition(cursor.line() - 1, doc->lineLength(cursor.line() - 1));
246 } else {
247 cursor.setColumn(cursor.column() - 1);
248 }
249 if (cursor.column() < doc->lineLength(cursor.line()) && !doc->characterAt(cursor).isSpace()
250 && doc->characterAt(cursor).category() != QChar::Other_Control) {
251 break;
252 }
253 } while (cursor > r.start());
254 r.setEnd(prevCursor);
255}
256
257#include "moc_spellcheck.cpp"
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
void setColumn(int column) noexcept
Set the cursor column to column.
Definition cursor.h:201
void setPosition(Cursor position) noexcept
Set the current cursor position to position.
Definition cursor.h:150
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Definition cursor.h:112
Backend of KTextEditor::Document related public KTextEditor interfaces.
QChar characterAt(KTextEditor::Cursor position) const override
Get the character at text position cursor.
Kate::TextLine kateTextLine(int i)
Same as plainKateTextLine(), except that it is made sure the line is highlighted.
int lineLength(int line) const override
Get the length of a given line in characters.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
void setEnd(Cursor end) noexcept
Set the end cursor to end.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
constexpr bool onSingleLine() const noexcept
Check whether this range is wholly contained within one line, ie.
constexpr bool contains(Range range) const noexcept
Check whether the this range wholly encompasses range.
constexpr Range intersect(Range range) const noexcept
Intersects this range with another, returning the shared area of the two ranges.
void setStart(Cursor start) noexcept
Set the start cursor to start.
This class can be used to efficiently search for occurrences of strings in a given string.
Definition prefixstore.h:27
QString findPrefix(const QString &s, int start=0) const
Returns the shortest prefix of the given string that is contained in this prefix store starting at po...
int attribute(int pos) const
Gets the attribute at the given position use KRenderer::attributes to get the KTextAttribute for this...
bool addToSession(const QString &word)
QStringList suggest(const QString &word) const
bool addToPersonal(const QString &word)
void setLanguage(const QString &lang)
Q_SCRIPTABLE Q_NOREPLY void start()
const QList< QKeySequence > & begin()
const QList< QKeySequence > & end()
Category category(char32_t ucs4)
bool isSpace(char32_t ucs4)
bool isEmpty() const const
iterator begin()
iterator end()
bool isEmpty() const const
void push_back(parameter_type value)
value_type takeFirst()
Q_EMITQ_EMIT
bool isEmpty() const const
qsizetype length() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:11:27 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.