1 | /* |
2 | SPDX-FileCopyrightText: 2008-2010 Michel Ludwig <michel.ludwig@kdemail.net> |
3 | SPDX-FileCopyrightText: 2009 Joseph Wenninger <jowenn@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | #include "ontheflycheck.h" |
8 | |
9 | #include <QRegularExpression> |
10 | #include <QTimer> |
11 | |
12 | #include "katebuffer.h" |
13 | #include "kateconfig.h" |
14 | #include "kateglobal.h" |
15 | #include "katepartdebug.h" |
16 | #include "kateview.h" |
17 | #include "spellcheck.h" |
18 | #include "spellingmenu.h" |
19 | |
20 | #define ON_THE_FLY_DEBUG qCDebug(LOG_KTE) |
21 | |
22 | namespace |
23 | { |
24 | inline const QPair<KTextEditor::MovingRange *, QString> &invalidSpellCheckQueueItem() |
25 | { |
26 | static const auto item = QPair<KTextEditor::MovingRange *, QString>(nullptr, QString()); |
27 | return item; |
28 | } |
29 | |
30 | } |
31 | |
32 | KateOnTheFlyChecker::KateOnTheFlyChecker(KTextEditor::DocumentPrivate *document) |
33 | : QObject(document) |
34 | , m_document(document) |
35 | , m_backgroundChecker(nullptr) |
36 | , m_currentlyCheckedItem(invalidSpellCheckQueueItem()) |
37 | , m_refreshView(nullptr) |
38 | { |
39 | ON_THE_FLY_DEBUG << "created" ; |
40 | |
41 | m_viewRefreshTimer = new QTimer(this); |
42 | m_viewRefreshTimer->setSingleShot(true); |
43 | connect(sender: m_viewRefreshTimer, signal: &QTimer::timeout, context: this, slot: &KateOnTheFlyChecker::viewRefreshTimeout); |
44 | |
45 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: &KateOnTheFlyChecker::textInserted); |
46 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::textRemoved, context: this, slot: &KateOnTheFlyChecker::textRemoved); |
47 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::viewCreated, context: this, slot: &KateOnTheFlyChecker::addView); |
48 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::highlightingModeChanged, context: this, slot: &KateOnTheFlyChecker::updateConfig); |
49 | connect(sender: &document->buffer(), signal: &KateBuffer::respellCheckBlock, context: this, slot: &KateOnTheFlyChecker::handleRespellCheckBlock); |
50 | |
51 | connect(sender: document, signal: &KTextEditor::Document::reloaded, context: this, slot: [this](KTextEditor::Document *) { |
52 | refreshSpellCheck(); |
53 | }); |
54 | |
55 | // load the settings for the speller |
56 | updateConfig(); |
57 | |
58 | const auto views = document->views(); |
59 | for (KTextEditor::View *view : views) { |
60 | addView(document, view); |
61 | } |
62 | refreshSpellCheck(); |
63 | } |
64 | |
65 | KateOnTheFlyChecker::~KateOnTheFlyChecker() |
66 | { |
67 | freeDocument(); |
68 | } |
69 | |
70 | QPair<KTextEditor::Range, QString> KateOnTheFlyChecker::getMisspelledItem(const KTextEditor::Cursor cursor) const |
71 | { |
72 | for (const MisspelledItem &item : m_misspelledList) { |
73 | KTextEditor::MovingRange *movingRange = item.first; |
74 | if (movingRange->contains(cursor)) { |
75 | return QPair<KTextEditor::Range, QString>(*movingRange, item.second); |
76 | } |
77 | } |
78 | return QPair<KTextEditor::Range, QString>(KTextEditor::Range::invalid(), QString()); |
79 | } |
80 | |
81 | QString KateOnTheFlyChecker::dictionaryForMisspelledRange(KTextEditor::Range range) const |
82 | { |
83 | for (const MisspelledItem &item : m_misspelledList) { |
84 | KTextEditor::MovingRange *movingRange = item.first; |
85 | if (*movingRange == range) { |
86 | return item.second; |
87 | } |
88 | } |
89 | return QString(); |
90 | } |
91 | |
92 | void KateOnTheFlyChecker::clearMisspellingForWord(const QString &word) |
93 | { |
94 | const MisspelledList misspelledList = m_misspelledList; // make a copy |
95 | for (const MisspelledItem &item : misspelledList) { |
96 | KTextEditor::MovingRange *movingRange = item.first; |
97 | if (m_document->text(range: *movingRange) == word) { |
98 | deleteMovingRange(range: movingRange); |
99 | } |
100 | } |
101 | } |
102 | |
103 | void KateOnTheFlyChecker::handleRespellCheckBlock(int start, int end) |
104 | { |
105 | ON_THE_FLY_DEBUG << start << end; |
106 | KTextEditor::Range range(start, 0, end, m_document->lineLength(line: end)); |
107 | bool listEmpty = m_modificationList.isEmpty(); |
108 | KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range); |
109 | movingRange->setFeedback(this); |
110 | // we don't handle this directly as the highlighting information might not be up-to-date yet |
111 | m_modificationList.push_back(t: ModificationItem(TEXT_INSERTED, movingRange)); |
112 | ON_THE_FLY_DEBUG << "added" << *movingRange; |
113 | if (listEmpty) { |
114 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::handleModifiedRanges); |
115 | } |
116 | } |
117 | |
118 | void KateOnTheFlyChecker::textInserted(KTextEditor::Document *document, KTextEditor::Range range) |
119 | { |
120 | Q_ASSERT(document == m_document); |
121 | Q_UNUSED(document); |
122 | if (!range.isValid()) { |
123 | return; |
124 | } |
125 | |
126 | bool listEmptyAtStart = m_modificationList.isEmpty(); |
127 | |
128 | // don't consider a range that is not within the document range |
129 | const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range); |
130 | if (!documentIntersection.isValid()) { |
131 | return; |
132 | } |
133 | // for performance reasons we only want to schedule spellchecks for ranges that are visible |
134 | const auto views = m_document->views(); |
135 | for (KTextEditor::View *i : views) { |
136 | KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(i); |
137 | KTextEditor::Range visibleIntersection = documentIntersection.intersect(range: view->visibleRange()); |
138 | if (visibleIntersection.isValid()) { // allow empty intersections |
139 | // we don't handle this directly as the highlighting information might not be up-to-date yet |
140 | KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range: visibleIntersection); |
141 | movingRange->setFeedback(this); |
142 | m_modificationList.push_back(t: ModificationItem(TEXT_INSERTED, movingRange)); |
143 | ON_THE_FLY_DEBUG << "added" << *movingRange; |
144 | } |
145 | } |
146 | |
147 | if (listEmptyAtStart && !m_modificationList.isEmpty()) { |
148 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::handleModifiedRanges); |
149 | } |
150 | } |
151 | |
152 | void KateOnTheFlyChecker::handleInsertedText(KTextEditor::Range range) |
153 | { |
154 | KTextEditor::Range consideredRange = range; |
155 | ON_THE_FLY_DEBUG << m_document << range; |
156 | |
157 | bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem(); |
158 | |
159 | if (spellCheckInProgress) { |
160 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
161 | if (spellCheckRange->contains(range: consideredRange)) { |
162 | consideredRange = *spellCheckRange; |
163 | stopCurrentSpellCheck(); |
164 | deleteMovingRangeQuickly(range: spellCheckRange); |
165 | } else if (consideredRange.contains(range: *spellCheckRange)) { |
166 | stopCurrentSpellCheck(); |
167 | deleteMovingRangeQuickly(range: spellCheckRange); |
168 | } else if (consideredRange.overlaps(range: *spellCheckRange)) { |
169 | consideredRange.expandToRange(range: *spellCheckRange); |
170 | stopCurrentSpellCheck(); |
171 | deleteMovingRangeQuickly(range: spellCheckRange); |
172 | } else { |
173 | spellCheckInProgress = false; |
174 | } |
175 | } |
176 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
177 | KTextEditor::MovingRange *spellCheckRange = (*i).first; |
178 | if (spellCheckRange->contains(range: consideredRange)) { |
179 | consideredRange = *spellCheckRange; |
180 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
181 | i = m_spellCheckQueue.erase(pos: i); |
182 | deleteMovingRangeQuickly(range: spellCheckRange); |
183 | } else if (consideredRange.contains(range: *spellCheckRange)) { |
184 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
185 | i = m_spellCheckQueue.erase(pos: i); |
186 | deleteMovingRangeQuickly(range: spellCheckRange); |
187 | } else if (consideredRange.overlaps(range: *spellCheckRange)) { |
188 | consideredRange.expandToRange(range: *spellCheckRange); |
189 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
190 | i = m_spellCheckQueue.erase(pos: i); |
191 | deleteMovingRangeQuickly(range: spellCheckRange); |
192 | } else { |
193 | ++i; |
194 | } |
195 | } |
196 | KTextEditor::Range spellCheckRange = findWordBoundaries(begin: consideredRange.start(), end: consideredRange.end()); |
197 | const bool emptyAtStart = m_spellCheckQueue.isEmpty(); |
198 | |
199 | queueSpellCheckVisibleRange(range: spellCheckRange); |
200 | |
201 | if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) { |
202 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
203 | } |
204 | } |
205 | |
206 | void KateOnTheFlyChecker::textRemoved(KTextEditor::Document *document, KTextEditor::Range range) |
207 | { |
208 | Q_ASSERT(document == m_document); |
209 | Q_UNUSED(document); |
210 | if (!range.isValid()) { |
211 | return; |
212 | } |
213 | |
214 | bool listEmptyAtStart = m_modificationList.isEmpty(); |
215 | |
216 | // don't consider a range that is behind the end of the document |
217 | const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range); |
218 | if (!documentIntersection.isValid()) { // the intersection might however be empty if the last |
219 | return; // word has been removed, for example |
220 | } |
221 | |
222 | // for performance reasons we only want to schedule spellchecks for ranges that are visible |
223 | const auto views = m_document->views(); |
224 | for (KTextEditor::View *i : views) { |
225 | KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(i); |
226 | KTextEditor::Range visibleIntersection = documentIntersection.intersect(range: view->visibleRange()); |
227 | if (visibleIntersection.isValid()) { // see above |
228 | // we don't handle this directly as the highlighting information might not be up-to-date yet |
229 | KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range: visibleIntersection); |
230 | movingRange->setFeedback(this); |
231 | m_modificationList.push_back(t: ModificationItem(TEXT_REMOVED, movingRange)); |
232 | ON_THE_FLY_DEBUG << "added" << *movingRange << view->visibleRange(); |
233 | } |
234 | } |
235 | if (listEmptyAtStart && !m_modificationList.isEmpty()) { |
236 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::handleModifiedRanges); |
237 | } |
238 | } |
239 | |
240 | inline bool rangesAdjacent(KTextEditor::Range r1, KTextEditor::Range r2) |
241 | { |
242 | return (r1.end() == r2.start()) || (r2.end() == r1.start()); |
243 | } |
244 | |
245 | void KateOnTheFlyChecker::handleRemovedText(KTextEditor::Range range) |
246 | { |
247 | ON_THE_FLY_DEBUG << range; |
248 | |
249 | QList<KTextEditor::Range> rangesToReCheck; |
250 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
251 | KTextEditor::MovingRange *spellCheckRange = (*i).first; |
252 | if (rangesAdjacent(r1: *spellCheckRange, r2: range) || spellCheckRange->contains(range)) { |
253 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
254 | if (!spellCheckRange->isEmpty()) { |
255 | rangesToReCheck.push_back(t: *spellCheckRange); |
256 | } |
257 | deleteMovingRangeQuickly(range: spellCheckRange); |
258 | i = m_spellCheckQueue.erase(pos: i); |
259 | } else { |
260 | ++i; |
261 | } |
262 | } |
263 | bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem(); |
264 | const bool emptyAtStart = m_spellCheckQueue.isEmpty(); |
265 | if (spellCheckInProgress) { |
266 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
267 | ON_THE_FLY_DEBUG << *spellCheckRange; |
268 | if (m_document->documentRange().contains(range: *spellCheckRange) && (rangesAdjacent(r1: *spellCheckRange, r2: range) || spellCheckRange->contains(range)) |
269 | && !spellCheckRange->isEmpty()) { |
270 | rangesToReCheck.push_back(t: *spellCheckRange); |
271 | ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange; |
272 | stopCurrentSpellCheck(); |
273 | deleteMovingRangeQuickly(range: spellCheckRange); |
274 | } else if (spellCheckRange->isEmpty()) { |
275 | stopCurrentSpellCheck(); |
276 | deleteMovingRangeQuickly(range: spellCheckRange); |
277 | } else { |
278 | spellCheckInProgress = false; |
279 | } |
280 | } |
281 | for (QList<KTextEditor::Range>::iterator i = rangesToReCheck.begin(); i != rangesToReCheck.end(); ++i) { |
282 | queueSpellCheckVisibleRange(range: *i); |
283 | } |
284 | |
285 | KTextEditor::Range spellCheckRange = findWordBoundaries(begin: range.start(), end: range.start()); |
286 | KTextEditor::Cursor spellCheckEnd = spellCheckRange.end(); |
287 | |
288 | queueSpellCheckVisibleRange(range: spellCheckRange); |
289 | |
290 | if (range.numberOfLines() > 0) { |
291 | // FIXME: there is no currently no way of doing this better as we only get notifications for removals of |
292 | // of single lines, i.e. we don't know here how many lines have been removed in total |
293 | KTextEditor::Cursor nextLineStart(spellCheckEnd.line() + 1, 0); |
294 | const KTextEditor::Cursor documentEnd = m_document->documentEnd(); |
295 | if (nextLineStart < documentEnd) { |
296 | KTextEditor::Range rangeBelow = KTextEditor::Range(nextLineStart, documentEnd); |
297 | |
298 | const QList<KTextEditor::View *> &viewList = m_document->views(); |
299 | for (QList<KTextEditor::View *>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) { |
300 | KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(*i); |
301 | const KTextEditor::Range visibleRange = view->visibleRange(); |
302 | KTextEditor::Range intersection = visibleRange.intersect(range: rangeBelow); |
303 | if (intersection.isValid()) { |
304 | queueSpellCheckVisibleRange(view, range: intersection); |
305 | } |
306 | } |
307 | } |
308 | } |
309 | |
310 | ON_THE_FLY_DEBUG << "finished" ; |
311 | if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) { |
312 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
313 | } |
314 | } |
315 | |
316 | void KateOnTheFlyChecker::freeDocument() |
317 | { |
318 | ON_THE_FLY_DEBUG; |
319 | |
320 | // empty the spell check queue |
321 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
322 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
323 | KTextEditor::MovingRange *movingRange = (*i).first; |
324 | deleteMovingRangeQuickly(range: movingRange); |
325 | i = m_spellCheckQueue.erase(pos: i); |
326 | } |
327 | if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) { |
328 | KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first; |
329 | deleteMovingRangeQuickly(range: movingRange); |
330 | } |
331 | stopCurrentSpellCheck(); |
332 | |
333 | const MisspelledList misspelledList = m_misspelledList; // make a copy! |
334 | for (const MisspelledItem &i : misspelledList) { |
335 | deleteMovingRange(range: i.first); |
336 | } |
337 | m_misspelledList.clear(); |
338 | clearModificationList(); |
339 | } |
340 | |
341 | void KateOnTheFlyChecker::performSpellCheck() |
342 | { |
343 | if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) { |
344 | ON_THE_FLY_DEBUG << "exited as a check is currently in progress" ; |
345 | return; |
346 | } |
347 | if (m_spellCheckQueue.isEmpty()) { |
348 | ON_THE_FLY_DEBUG << "exited as there is nothing to do" ; |
349 | return; |
350 | } |
351 | m_currentlyCheckedItem = m_spellCheckQueue.takeFirst(); |
352 | |
353 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
354 | const QString &language = m_currentlyCheckedItem.second; |
355 | ON_THE_FLY_DEBUG << "for the range " << *spellCheckRange; |
356 | // clear all the highlights that are currently present in the range that |
357 | // is supposed to be checked |
358 | const MovingRangeList highlightsList = installedMovingRanges(range: *spellCheckRange); // make a copy! |
359 | deleteMovingRanges(list: highlightsList); |
360 | |
361 | m_currentDecToEncOffsetList.clear(); |
362 | KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; |
363 | QString text = m_document->decodeCharacters(range: *spellCheckRange, decToEncOffsetList&: m_currentDecToEncOffsetList, encToDecOffsetList); |
364 | ON_THE_FLY_DEBUG << "next spell checking" << text; |
365 | if (text.isEmpty()) { // passing an empty string to Sonnet can lead to a bad allocation exception |
366 | spellCheckDone(); // (bug 225867) |
367 | return; |
368 | } |
369 | if (m_speller.language() != language) { |
370 | m_speller.setLanguage(language); |
371 | } |
372 | if (!m_backgroundChecker) { |
373 | m_backgroundChecker = new Sonnet::BackgroundChecker(m_speller, this); |
374 | connect(sender: m_backgroundChecker, signal: &Sonnet::BackgroundChecker::misspelling, context: this, slot: &KateOnTheFlyChecker::misspelling); |
375 | connect(sender: m_backgroundChecker, signal: &Sonnet::BackgroundChecker::done, context: this, slot: &KateOnTheFlyChecker::spellCheckDone); |
376 | |
377 | KateSpellCheckManager *m_spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager(); |
378 | connect(sender: m_spellCheckManager, signal: &KateSpellCheckManager::wordAddedToDictionary, context: this, slot: &KateOnTheFlyChecker::addToDictionary); |
379 | connect(sender: m_spellCheckManager, signal: &KateSpellCheckManager::wordIgnored, context: this, slot: &KateOnTheFlyChecker::addToSession); |
380 | } |
381 | m_backgroundChecker->setSpeller(m_speller); |
382 | m_backgroundChecker->setText(text); // don't call 'start()' after this! |
383 | } |
384 | |
385 | void KateOnTheFlyChecker::addToDictionary(const QString &word) |
386 | { |
387 | if (m_backgroundChecker) { |
388 | m_backgroundChecker->addWordToPersonal(word); |
389 | } |
390 | } |
391 | |
392 | void KateOnTheFlyChecker::addToSession(const QString &word) |
393 | { |
394 | if (m_backgroundChecker) { |
395 | m_backgroundChecker->addWordToSession(word); |
396 | } |
397 | } |
398 | |
399 | void KateOnTheFlyChecker::removeRangeFromEverything(KTextEditor::MovingRange *movingRange) |
400 | { |
401 | Q_ASSERT(m_document == movingRange->document()); |
402 | ON_THE_FLY_DEBUG << *movingRange << "(" << movingRange << ")" ; |
403 | |
404 | if (removeRangeFromModificationList(range: movingRange)) { |
405 | return; // range was part of the modification queue, so we don't have |
406 | // to look further for it |
407 | } |
408 | |
409 | if (removeRangeFromSpellCheckQueue(range: movingRange)) { |
410 | return; // range was part of the spell check queue, so it cannot have been |
411 | // a misspelled range |
412 | } |
413 | |
414 | for (MisspelledList::iterator i = m_misspelledList.begin(); i != m_misspelledList.end();) { |
415 | if ((*i).first == movingRange) { |
416 | i = m_misspelledList.erase(pos: i); |
417 | } else { |
418 | ++i; |
419 | } |
420 | } |
421 | } |
422 | |
423 | bool KateOnTheFlyChecker::removeRangeFromCurrentSpellCheck(KTextEditor::MovingRange *range) |
424 | { |
425 | if (m_currentlyCheckedItem != invalidSpellCheckQueueItem() && m_currentlyCheckedItem.first == range) { |
426 | stopCurrentSpellCheck(); |
427 | return true; |
428 | } |
429 | return false; |
430 | } |
431 | |
432 | void KateOnTheFlyChecker::stopCurrentSpellCheck() |
433 | { |
434 | m_currentDecToEncOffsetList.clear(); |
435 | m_currentlyCheckedItem = invalidSpellCheckQueueItem(); |
436 | if (m_backgroundChecker) { |
437 | m_backgroundChecker->stop(); |
438 | } |
439 | } |
440 | |
441 | bool KateOnTheFlyChecker::removeRangeFromSpellCheckQueue(KTextEditor::MovingRange *range) |
442 | { |
443 | if (removeRangeFromCurrentSpellCheck(range)) { |
444 | if (!m_spellCheckQueue.isEmpty()) { |
445 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
446 | } |
447 | return true; |
448 | } |
449 | bool found = false; |
450 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
451 | if ((*i).first == range) { |
452 | i = m_spellCheckQueue.erase(pos: i); |
453 | found = true; |
454 | } else { |
455 | ++i; |
456 | } |
457 | } |
458 | return found; |
459 | } |
460 | |
461 | void KateOnTheFlyChecker::rangeEmpty(KTextEditor::MovingRange *range) |
462 | { |
463 | ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")" ; |
464 | deleteMovingRange(range); |
465 | } |
466 | |
467 | void KateOnTheFlyChecker::rangeInvalid(KTextEditor::MovingRange *range) |
468 | { |
469 | ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")" ; |
470 | deleteMovingRange(range); |
471 | } |
472 | |
473 | /** |
474 | * It is not enough to use 'caret/Entered/ExitedRange' only as the cursor doesn't move when some |
475 | * text has been selected. |
476 | **/ |
477 | void KateOnTheFlyChecker::caretEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view) |
478 | { |
479 | KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view); |
480 | kateView->spellingMenu()->caretEnteredMisspelledRange(range); |
481 | } |
482 | |
483 | void KateOnTheFlyChecker::caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view) |
484 | { |
485 | KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view); |
486 | kateView->spellingMenu()->caretExitedMisspelledRange(range); |
487 | } |
488 | |
489 | void KateOnTheFlyChecker::deleteMovingRange(KTextEditor::MovingRange *range) |
490 | { |
491 | ON_THE_FLY_DEBUG << range; |
492 | // remove it from all our structures |
493 | removeRangeFromEverything(movingRange: range); |
494 | range->setFeedback(nullptr); |
495 | const auto views = m_document->views(); |
496 | for (KTextEditor::View *view : views) { |
497 | static_cast<KTextEditor::ViewPrivate *>(view)->spellingMenu()->rangeDeleted(range); |
498 | } |
499 | delete (range); |
500 | } |
501 | |
502 | void KateOnTheFlyChecker::deleteMovingRanges(const QList<KTextEditor::MovingRange *> &list) |
503 | { |
504 | for (KTextEditor::MovingRange *r : list) { |
505 | deleteMovingRange(range: r); |
506 | } |
507 | } |
508 | |
509 | KTextEditor::Range KateOnTheFlyChecker::findWordBoundaries(const KTextEditor::Cursor begin, const KTextEditor::Cursor end) |
510 | { |
511 | // FIXME: QTextBoundaryFinder should be ideally used for this, but it is currently |
512 | // still broken in Qt |
513 | static const QRegularExpression boundaryRegExp(QStringLiteral("\\b" ), QRegularExpression::UseUnicodePropertiesOption); |
514 | // handle spell checking of QLatin1String("isn't"), QLatin1String("doesn't"), etc. |
515 | static const QRegularExpression boundaryQuoteRegExp(QStringLiteral("\\b\\w+'\\w*$" ), QRegularExpression::UseUnicodePropertiesOption); |
516 | static const QRegularExpression extendedBoundaryRegExp(QStringLiteral("\\W|$" ), QRegularExpression::UseUnicodePropertiesOption); |
517 | static const QRegularExpression extendedBoundaryQuoteRegExp(QStringLiteral("^\\w*'\\w+\\b" ), QRegularExpression::UseUnicodePropertiesOption); // see above |
518 | KTextEditor::DocumentPrivate::OffsetList decToEncOffsetList; |
519 | KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; |
520 | const int startLine = begin.line(); |
521 | const int startColumn = begin.column(); |
522 | KTextEditor::Cursor boundaryStart; |
523 | KTextEditor::Cursor boundaryEnd; |
524 | // first we take care of the start position |
525 | const KTextEditor::Range startLineRange(startLine, 0, startLine, m_document->lineLength(line: startLine)); |
526 | QString decodedLineText = m_document->decodeCharacters(range: startLineRange, decToEncOffsetList, encToDecOffsetList); |
527 | int translatedColumn = m_document->computePositionWrtOffsets(offsetList: encToDecOffsetList, pos: startColumn); |
528 | QString text = decodedLineText.mid(position: 0, n: translatedColumn); |
529 | boundaryStart.setLine(startLine); |
530 | int match = text.lastIndexOf(re: boundaryQuoteRegExp); |
531 | if (match < 0) { |
532 | match = text.lastIndexOf(re: boundaryRegExp, from: -2); |
533 | } |
534 | boundaryStart.setColumn(m_document->computePositionWrtOffsets(offsetList: decToEncOffsetList, pos: qMax(a: 0, b: match))); |
535 | // and now the end position |
536 | const int endLine = end.line(); |
537 | const int endColumn = end.column(); |
538 | if (endLine != startLine) { |
539 | decToEncOffsetList.clear(); |
540 | encToDecOffsetList.clear(); |
541 | const KTextEditor::Range endLineRange(endLine, 0, endLine, m_document->lineLength(line: endLine)); |
542 | decodedLineText = m_document->decodeCharacters(range: endLineRange, decToEncOffsetList, encToDecOffsetList); |
543 | } |
544 | translatedColumn = m_document->computePositionWrtOffsets(offsetList: encToDecOffsetList, pos: endColumn); |
545 | text = decodedLineText.mid(position: translatedColumn); |
546 | boundaryEnd.setLine(endLine); |
547 | |
548 | QRegularExpressionMatch reMatch; |
549 | match = text.indexOf(re: extendedBoundaryQuoteRegExp, from: 0 /* from */, rmatch: &reMatch); |
550 | if (match == 0) { |
551 | match = reMatch.capturedLength(nth: 0); |
552 | } else { |
553 | match = text.indexOf(re: extendedBoundaryRegExp); |
554 | } |
555 | boundaryEnd.setColumn(m_document->computePositionWrtOffsets(offsetList: decToEncOffsetList, pos: translatedColumn + qMax(a: 0, b: match))); |
556 | return KTextEditor::Range(boundaryStart, boundaryEnd); |
557 | } |
558 | |
559 | void KateOnTheFlyChecker::misspelling(const QString &word, int start) |
560 | { |
561 | if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) { |
562 | ON_THE_FLY_DEBUG << "exited as no spell check is taking place" ; |
563 | return; |
564 | } |
565 | int translatedStart = m_document->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: start); |
566 | // ON_THE_FLY_DEBUG << "misspelled " << word |
567 | // << " at line " |
568 | // << *m_currentlyCheckedItem.first |
569 | // << " column " << start; |
570 | |
571 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
572 | int line = spellCheckRange->start().line(); |
573 | int rangeStart = spellCheckRange->start().column(); |
574 | int translatedEnd = m_document->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: start + word.length()); |
575 | |
576 | KTextEditor::MovingRange *movingRange = |
577 | m_document->newMovingRange(range: KTextEditor::Range(line, rangeStart + translatedStart, line, rangeStart + translatedEnd)); |
578 | movingRange->setFeedback(this); |
579 | KTextEditor::Attribute *attribute = new KTextEditor::Attribute(); |
580 | attribute->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); |
581 | attribute->setUnderlineColor(KateRendererConfig::global()->spellingMistakeLineColor()); |
582 | |
583 | // don't print this range |
584 | movingRange->setAttributeOnlyForViews(true); |
585 | |
586 | movingRange->setAttribute(KTextEditor::Attribute::Ptr(attribute)); |
587 | m_misspelledList.push_back(t: MisspelledItem(movingRange, m_currentlyCheckedItem.second)); |
588 | |
589 | if (m_backgroundChecker) { |
590 | m_backgroundChecker->continueChecking(); |
591 | } |
592 | } |
593 | |
594 | void KateOnTheFlyChecker::spellCheckDone() |
595 | { |
596 | ON_THE_FLY_DEBUG << "on-the-fly spell check done, queue length " << m_spellCheckQueue.size(); |
597 | if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) { |
598 | return; |
599 | } |
600 | KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first; |
601 | stopCurrentSpellCheck(); |
602 | deleteMovingRangeQuickly(range: movingRange); |
603 | |
604 | if (!m_spellCheckQueue.empty()) { |
605 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
606 | } |
607 | } |
608 | |
609 | QList<KTextEditor::MovingRange *> KateOnTheFlyChecker::installedMovingRanges(KTextEditor::Range range) const |
610 | { |
611 | ON_THE_FLY_DEBUG << range; |
612 | MovingRangeList toReturn; |
613 | |
614 | for (QList<SpellCheckItem>::const_iterator i = m_misspelledList.begin(); i != m_misspelledList.end(); ++i) { |
615 | KTextEditor::MovingRange *movingRange = (*i).first; |
616 | if (movingRange->overlaps(range)) { |
617 | toReturn.push_back(t: movingRange); |
618 | } |
619 | } |
620 | return toReturn; |
621 | } |
622 | |
623 | void KateOnTheFlyChecker::updateConfig() |
624 | { |
625 | ON_THE_FLY_DEBUG; |
626 | // m_speller.restore(); |
627 | } |
628 | |
629 | void KateOnTheFlyChecker::refreshSpellCheck(KTextEditor::Range range) |
630 | { |
631 | if (range.isValid()) { |
632 | textInserted(document: m_document, range); |
633 | } else { |
634 | freeDocument(); |
635 | textInserted(document: m_document, range: m_document->documentRange()); |
636 | } |
637 | } |
638 | |
639 | void KateOnTheFlyChecker::addView(KTextEditor::Document *document, KTextEditor::View *view) |
640 | { |
641 | Q_ASSERT(document == m_document); |
642 | Q_UNUSED(document); |
643 | ON_THE_FLY_DEBUG; |
644 | auto *viewPrivate = static_cast<KTextEditor::ViewPrivate *>(view); |
645 | connect(sender: viewPrivate, signal: &KTextEditor::ViewPrivate::destroyed, context: this, slot: &KateOnTheFlyChecker::viewDestroyed); |
646 | connect(sender: viewPrivate, signal: &KTextEditor::ViewPrivate::displayRangeChanged, context: this, slot: &KateOnTheFlyChecker::restartViewRefreshTimer); |
647 | updateInstalledMovingRanges(view: static_cast<KTextEditor::ViewPrivate *>(view)); |
648 | } |
649 | |
650 | void KateOnTheFlyChecker::viewDestroyed(QObject *obj) |
651 | { |
652 | ON_THE_FLY_DEBUG; |
653 | KTextEditor::View *view = static_cast<KTextEditor::View *>(obj); |
654 | m_displayRangeMap.erase(x: view); |
655 | } |
656 | |
657 | void KateOnTheFlyChecker::removeView(KTextEditor::View *view) |
658 | { |
659 | ON_THE_FLY_DEBUG; |
660 | m_displayRangeMap.erase(x: view); |
661 | } |
662 | |
663 | void KateOnTheFlyChecker::updateInstalledMovingRanges(KTextEditor::ViewPrivate *view) |
664 | { |
665 | Q_ASSERT(m_document == view->document()); |
666 | ON_THE_FLY_DEBUG; |
667 | KTextEditor::Range oldDisplayRange = m_displayRangeMap[view]; |
668 | |
669 | KTextEditor::Range newDisplayRange = view->visibleRange(); |
670 | ON_THE_FLY_DEBUG << "new range: " << newDisplayRange; |
671 | ON_THE_FLY_DEBUG << "old range: " << oldDisplayRange; |
672 | QList<KTextEditor::MovingRange *> toDelete; |
673 | for (const MisspelledItem &item : std::as_const(t&: m_misspelledList)) { |
674 | KTextEditor::MovingRange *movingRange = item.first; |
675 | if (!movingRange->overlaps(range: newDisplayRange)) { |
676 | bool stillVisible = false; |
677 | const auto views = m_document->views(); |
678 | for (KTextEditor::View *it2 : views) { |
679 | KTextEditor::ViewPrivate *view2 = static_cast<KTextEditor::ViewPrivate *>(it2); |
680 | if (view != view2 && movingRange->overlaps(range: view2->visibleRange())) { |
681 | stillVisible = true; |
682 | break; |
683 | } |
684 | } |
685 | if (!stillVisible) { |
686 | toDelete.push_back(t: movingRange); |
687 | } |
688 | } |
689 | } |
690 | deleteMovingRanges(list: toDelete); |
691 | m_displayRangeMap[view] = newDisplayRange; |
692 | if (oldDisplayRange.isValid()) { |
693 | bool emptyAtStart = m_spellCheckQueue.empty(); |
694 | for (int line = newDisplayRange.end().line(); line >= newDisplayRange.start().line(); --line) { |
695 | if (!oldDisplayRange.containsLine(line)) { |
696 | bool visible = false; |
697 | const auto views = m_document->views(); |
698 | for (KTextEditor::View *it2 : views) { |
699 | KTextEditor::ViewPrivate *view2 = static_cast<KTextEditor::ViewPrivate *>(it2); |
700 | if (view != view2 && view2->visibleRange().containsLine(line)) { |
701 | visible = true; |
702 | break; |
703 | } |
704 | } |
705 | if (!visible) { |
706 | queueLineSpellCheck(document: m_document, line); |
707 | } |
708 | } |
709 | } |
710 | if (emptyAtStart && !m_spellCheckQueue.isEmpty()) { |
711 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
712 | } |
713 | } |
714 | } |
715 | |
716 | void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::Range range) |
717 | { |
718 | const QList<KTextEditor::View *> &viewList = m_document->views(); |
719 | for (QList<KTextEditor::View *>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) { |
720 | queueSpellCheckVisibleRange(view: static_cast<KTextEditor::ViewPrivate *>(*i), range); |
721 | } |
722 | } |
723 | |
724 | void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::ViewPrivate *view, KTextEditor::Range range) |
725 | { |
726 | Q_ASSERT(m_document == view->doc()); |
727 | KTextEditor::Range visibleRange = view->visibleRange(); |
728 | KTextEditor::Range intersection = visibleRange.intersect(range); |
729 | if (intersection.isEmpty()) { |
730 | return; |
731 | } |
732 | |
733 | // clear all the highlights that are currently present in the range that |
734 | // is supposed to be checked, necessary due to highlighting |
735 | const MovingRangeList highlightsList = installedMovingRanges(range: intersection); |
736 | deleteMovingRanges(list: highlightsList); |
737 | |
738 | QList<QPair<KTextEditor::Range, QString>> spellCheckRanges = |
739 | KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(doc: m_document, range: intersection, singleLine: true); |
740 | // we queue them up in reverse |
741 | QListIterator<QPair<KTextEditor::Range, QString>> i(spellCheckRanges); |
742 | i.toBack(); |
743 | while (i.hasPrevious()) { |
744 | QPair<KTextEditor::Range, QString> p = i.previous(); |
745 | queueLineSpellCheck(range: p.first, dictionary: p.second); |
746 | } |
747 | } |
748 | |
749 | void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::DocumentPrivate *kateDocument, int line) |
750 | { |
751 | const KTextEditor::Range range = KTextEditor::Range(line, 0, line, kateDocument->lineLength(line)); |
752 | // clear all the highlights that are currently present in the range that |
753 | // is supposed to be checked, necessary due to highlighting |
754 | |
755 | const MovingRangeList highlightsList = installedMovingRanges(range); |
756 | deleteMovingRanges(list: highlightsList); |
757 | |
758 | QList<QPair<KTextEditor::Range, QString>> spellCheckRanges = |
759 | KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(doc: kateDocument, range, singleLine: true); |
760 | // we queue them up in reverse |
761 | QListIterator<QPair<KTextEditor::Range, QString>> i(spellCheckRanges); |
762 | i.toBack(); |
763 | while (i.hasPrevious()) { |
764 | QPair<KTextEditor::Range, QString> p = i.previous(); |
765 | queueLineSpellCheck(range: p.first, dictionary: p.second); |
766 | } |
767 | } |
768 | |
769 | void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::Range range, const QString &dictionary) |
770 | { |
771 | ON_THE_FLY_DEBUG << m_document << range; |
772 | |
773 | Q_ASSERT(range.onSingleLine()); |
774 | |
775 | if (range.isEmpty()) { |
776 | return; |
777 | } |
778 | |
779 | addToSpellCheckQueue(range, dictionary); |
780 | } |
781 | |
782 | void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::Range range, const QString &dictionary) |
783 | { |
784 | addToSpellCheckQueue(range: m_document->newMovingRange(range), dictionary); |
785 | } |
786 | |
787 | void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::MovingRange *range, const QString &dictionary) |
788 | { |
789 | ON_THE_FLY_DEBUG << m_document << *range << dictionary; |
790 | |
791 | range->setFeedback(this); |
792 | |
793 | // if the queue contains a subrange of 'range', we remove that one |
794 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
795 | KTextEditor::MovingRange *spellCheckRange = (*i).first; |
796 | if (range->contains(range: *spellCheckRange)) { |
797 | deleteMovingRangeQuickly(range: spellCheckRange); |
798 | i = m_spellCheckQueue.erase(pos: i); |
799 | } else { |
800 | ++i; |
801 | } |
802 | } |
803 | // leave 'push_front' here as it is a LIFO queue, i.e. a stack |
804 | m_spellCheckQueue.push_front(t: SpellCheckItem(range, dictionary)); |
805 | ON_THE_FLY_DEBUG << "added" << *range << dictionary << "to the queue, which has a length of" << m_spellCheckQueue.size(); |
806 | } |
807 | |
808 | void KateOnTheFlyChecker::viewRefreshTimeout() |
809 | { |
810 | if (m_refreshView) { |
811 | updateInstalledMovingRanges(view: m_refreshView); |
812 | } |
813 | m_refreshView = nullptr; |
814 | } |
815 | |
816 | void KateOnTheFlyChecker::restartViewRefreshTimer(KTextEditor::ViewPrivate *view) |
817 | { |
818 | if (m_refreshView && view != m_refreshView) { // a new view should be refreshed |
819 | updateInstalledMovingRanges(view: m_refreshView); // so refresh the old one first |
820 | } |
821 | m_refreshView = view; |
822 | m_viewRefreshTimer->start(msec: 100); |
823 | } |
824 | |
825 | void KateOnTheFlyChecker::deleteMovingRangeQuickly(KTextEditor::MovingRange *range) |
826 | { |
827 | range->setFeedback(nullptr); |
828 | const auto views = m_document->views(); |
829 | for (KTextEditor::View *view : views) { |
830 | static_cast<KTextEditor::ViewPrivate *>(view)->spellingMenu()->rangeDeleted(range); |
831 | } |
832 | delete (range); |
833 | } |
834 | |
835 | void KateOnTheFlyChecker::handleModifiedRanges() |
836 | { |
837 | for (const ModificationItem &item : std::as_const(t&: m_modificationList)) { |
838 | KTextEditor::MovingRange *movingRange = item.second; |
839 | KTextEditor::Range range = *movingRange; |
840 | deleteMovingRangeQuickly(range: movingRange); |
841 | if (item.first == TEXT_INSERTED) { |
842 | handleInsertedText(range); |
843 | } else { |
844 | handleRemovedText(range); |
845 | } |
846 | } |
847 | m_modificationList.clear(); |
848 | } |
849 | |
850 | bool KateOnTheFlyChecker::removeRangeFromModificationList(KTextEditor::MovingRange *range) |
851 | { |
852 | bool found = false; |
853 | for (ModificationList::iterator i = m_modificationList.begin(); i != m_modificationList.end();) { |
854 | ModificationItem item = *i; |
855 | KTextEditor::MovingRange *movingRange = item.second; |
856 | if (movingRange == range) { |
857 | found = true; |
858 | i = m_modificationList.erase(pos: i); |
859 | } else { |
860 | ++i; |
861 | } |
862 | } |
863 | return found; |
864 | } |
865 | |
866 | void KateOnTheFlyChecker::clearModificationList() |
867 | { |
868 | for (const ModificationItem &item : std::as_const(t&: m_modificationList)) { |
869 | KTextEditor::MovingRange *movingRange = item.second; |
870 | deleteMovingRangeQuickly(range: movingRange); |
871 | } |
872 | m_modificationList.clear(); |
873 | } |
874 | |