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 | |
19 | KateSpellCheckManager::KateSpellCheckManager(QObject *parent) |
20 | : QObject(parent) |
21 | { |
22 | } |
23 | |
24 | KateSpellCheckManager::~KateSpellCheckManager() = default; |
25 | |
26 | QStringList KateSpellCheckManager::suggestions(const QString &word, const QString &dictionary) |
27 | { |
28 | Sonnet::Speller speller; |
29 | speller.setLanguage(dictionary); |
30 | return speller.suggest(word); |
31 | } |
32 | |
33 | void 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 | |
41 | void 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 | |
49 | QList<KTextEditor::Range> KateSpellCheckManager::rangeDifference(KTextEditor::Range r1, KTextEditor::Range r2) |
50 | { |
51 | Q_ASSERT(r1.contains(r2)); |
52 | QList<KTextEditor::Range> toReturn; |
53 | KTextEditor::Range before(r1.start(), r2.start()); |
54 | KTextEditor::Range after(r2.end(), r1.end()); |
55 | if (!before.isEmpty()) { |
56 | toReturn.push_back(t: before); |
57 | } |
58 | if (!after.isEmpty()) { |
59 | toReturn.push_back(t: after); |
60 | } |
61 | return toReturn; |
62 | } |
63 | |
64 | namespace |
65 | { |
66 | bool 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 | |
72 | QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range) |
73 | { |
74 | QString defaultDict = doc->defaultDictionary(); |
75 | QList<RangeDictionaryPair> toReturn; |
76 | QList<QPair<KTextEditor::MovingRange *, QString>> dictionaryRanges = doc->dictionaryRanges(); |
77 | if (dictionaryRanges.isEmpty()) { |
78 | toReturn.push_back(t: RangeDictionaryPair(range, defaultDict)); |
79 | return toReturn; |
80 | } |
81 | QList<KTextEditor::Range> splitQueue; |
82 | splitQueue.push_back(t: 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(range: consideredRange); |
89 | if (intersection.isEmpty()) { |
90 | continue; |
91 | } |
92 | toReturn.push_back(t: RangeDictionaryPair(intersection, (*i).second)); |
93 | splitQueue += rangeDifference(r1: consideredRange, r2: 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(t: RangeDictionaryPair(consideredRange, defaultDict)); |
100 | } |
101 | } |
102 | // finally, we still have to sort the list |
103 | std::stable_sort(first: toReturn.begin(), last: toReturn.end(), comp: lessThanRangeDictionaryPair); |
104 | return toReturn; |
105 | } |
106 | |
107 | QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *document, |
108 | KTextEditor::Range range, |
109 | const QString &dictionary, |
110 | bool singleLine, |
111 | bool returnSingleRange) |
112 | { |
113 | QList<QPair<KTextEditor::Range, QString>> toReturn; |
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(t: 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(t: toAdd); |
134 | } |
135 | } |
136 | } |
137 | for (QList<KTextEditor::Range>::iterator i = rangesToSplit.begin(); i != rangesToSplit.end(); ++i) { |
138 | KTextEditor::Range rangeToSplit = *i; |
139 | KTextEditor::Cursor begin = KTextEditor::Cursor::invalid(); |
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(i: 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(pos: i); |
151 | const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attrib: attr); |
152 | QString prefixFound = prefixStore.findPrefix(line: kateTextLine, start: i); |
153 | if (!document->highlight()->attributeRequiresSpellchecking(attr: 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(doc: document, r&: spellCheckRange); |
161 | if (!spellCheckRange.isEmpty()) { |
162 | toReturn.push_back(t: RangeDictionaryPair(spellCheckRange, dictionary)); |
163 | if (returnSingleRange) { |
164 | return toReturn; |
165 | } |
166 | } |
167 | begin = KTextEditor::Cursor::invalid(); |
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(doc: document, r&: spellCheckRange); |
185 | if (!spellCheckRange.isEmpty()) { |
186 | toReturn.push_back(t: RangeDictionaryPair(spellCheckRange, dictionary)); |
187 | if (returnSingleRange) { |
188 | return toReturn; |
189 | } |
190 | } |
191 | } |
192 | } |
193 | |
194 | return toReturn; |
195 | } |
196 | |
197 | QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range, bool singleLine) |
198 | { |
199 | QList<RangeDictionaryPair> toReturn; |
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(document: doc, range: p.first, dictionary: p.second, singleLine); |
204 | } |
205 | return toReturn; |
206 | } |
207 | |
208 | void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, KTextEditor::Range replacementRange) |
209 | { |
210 | const int attr = doc->kateTextLine(i: replacementRange.start().line()).attribute(pos: replacementRange.start().column()); |
211 | if (!doc->highlight()->getCharacterEncodings(attrib: attr).isEmpty() && doc->containsCharacterEncoding(range: replacementRange)) { |
212 | doc->replaceText(range: replacementRange, s: newWord); |
213 | doc->replaceCharactersByEncoding(range: KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length()))); |
214 | } else { |
215 | doc->replaceText(range: replacementRange, s: newWord); |
216 | } |
217 | } |
218 | |
219 | void 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(line: cursor.line()) > 0 && !doc->characterAt(position: cursor).isSpace() && doc->characterAt(position: cursor).category() != QChar::Other_Control) { |
227 | break; |
228 | } |
229 | cursor.setColumn(cursor.column() + 1); |
230 | if (cursor.column() >= doc->lineLength(line: cursor.line())) { |
231 | cursor.setPosition(line: cursor.line() + 1, column: 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(line: cursor.line() - 1, column: doc->lineLength(line: cursor.line() - 1)); |
246 | } else { |
247 | cursor.setColumn(cursor.column() - 1); |
248 | } |
249 | if (cursor.column() < doc->lineLength(line: cursor.line()) && !doc->characterAt(position: cursor).isSpace() |
250 | && doc->characterAt(position: cursor).category() != QChar::Other_Control) { |
251 | break; |
252 | } |
253 | } while (cursor > r.start()); |
254 | r.setEnd(prevCursor); |
255 | } |
256 | |
257 | #include "moc_spellcheck.cpp" |
258 | |