1/*
2 SPDX-FileCopyrightText: 2009-2010 Michel Ludwig <michel.ludwig@kdemail.net>
3 SPDX-FileCopyrightText: 2008 Mirko Stocker <me@misto.ch>
4 SPDX-FileCopyrightText: 2004-2005 Anders Lund <anders@alweb.dk>
5 SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org>
6 SPDX-FileCopyrightText: 2001-2004 Christoph Cullmann <cullmann@kde.org>
7 SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
8 SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
9
10 SPDX-License-Identifier: LGPL-2.0-or-later
11*/
12
13#include "spellcheckdialog.h"
14
15#include "katedocument.h"
16#include "kateglobal.h"
17#include "kateview.h"
18#include "spellcheck/spellcheck.h"
19#include "spellcheck/spellcheckbar.h"
20
21#include <KActionCollection>
22#include <KLocalizedString>
23#include <KStandardAction>
24
25#include <sonnet/backgroundchecker.h>
26#include <sonnet/speller.h>
27
28KateSpellCheckDialog::KateSpellCheckDialog(KTextEditor::ViewPrivate *view)
29 : QObject(view)
30 , m_view(view)
31 , m_speller(nullptr)
32 , m_backgroundChecker(nullptr)
33 , m_sonnetDialog(nullptr)
34 , m_globalSpellCheckRange(nullptr)
35 , m_spellCheckCancelledByUser(false)
36{
37}
38
39KateSpellCheckDialog::~KateSpellCheckDialog()
40{
41 delete m_globalSpellCheckRange;
42 delete m_sonnetDialog;
43 delete m_backgroundChecker;
44 delete m_speller;
45}
46
47void KateSpellCheckDialog::createActions(KActionCollection *ac)
48{
49 ac->addAction(actionType: KStandardAction::Spelling, receiver: this, SLOT(spellcheck()));
50
51 QAction *a = new QAction(i18n("Spelling (from Cursor)..."), this);
52 ac->addAction(QStringLiteral("tools_spelling_from_cursor"), action: a);
53 a->setIcon(QIcon::fromTheme(QStringLiteral("tools-check-spelling")));
54 a->setWhatsThis(i18n("Check the document's spelling from the cursor and forward"));
55 connect(sender: a, signal: &QAction::triggered, context: this, slot: &KateSpellCheckDialog::spellcheckFromCursor);
56}
57
58void KateSpellCheckDialog::spellcheckFromCursor()
59{
60 if (m_view->selection()) {
61 spellcheckSelection();
62 } else {
63 spellcheck(from: m_view->cursorPosition());
64 }
65}
66
67void KateSpellCheckDialog::spellcheckSelection()
68{
69 spellcheck(from: m_view->selectionRange().start(), to: m_view->selectionRange().end());
70}
71
72void KateSpellCheckDialog::spellcheck()
73{
74 if (m_view->selection()) {
75 spellcheckSelection();
76 } else {
77 spellcheck(from: KTextEditor::Cursor(0, 0));
78 }
79}
80
81void KateSpellCheckDialog::spellcheck(const KTextEditor::Cursor from, const KTextEditor::Cursor to)
82{
83 KTextEditor::Cursor start = from;
84 KTextEditor::Cursor end = to;
85
86 if (end.line() == 0 && end.column() == 0) {
87 end = m_view->doc()->documentEnd();
88 }
89
90 if (!m_speller) {
91 m_speller = new Sonnet::Speller();
92 }
93 m_speller->restore();
94
95 if (!m_backgroundChecker) {
96 m_backgroundChecker = new Sonnet::BackgroundChecker(*m_speller);
97 }
98
99 if (!m_sonnetDialog) {
100 m_sonnetDialog = new SpellCheckBar(m_backgroundChecker, m_view);
101 m_sonnetDialog->showProgressDialog(timeout: 200);
102 m_sonnetDialog->showSpellCheckCompletionMessage();
103 m_sonnetDialog->setSpellCheckContinuedAfterReplacement(false);
104
105 connect(sender: m_sonnetDialog, signal: &SpellCheckBar::done, context: this, slot: &KateSpellCheckDialog::installNextSpellCheckRange);
106
107 connect(sender: m_sonnetDialog, signal: &SpellCheckBar::replace, context: this, slot: &KateSpellCheckDialog::corrected);
108
109 connect(sender: m_sonnetDialog, signal: &SpellCheckBar::misspelling, context: this, slot: &KateSpellCheckDialog::misspelling);
110
111 connect(sender: m_sonnetDialog, signal: &SpellCheckBar::cancel, context: this, slot: &KateSpellCheckDialog::cancelClicked);
112
113 connect(sender: m_sonnetDialog, signal: &SpellCheckBar::destroyed, context: this, slot: &KateSpellCheckDialog::objectDestroyed);
114
115 connect(sender: m_sonnetDialog, signal: &SpellCheckBar::languageChanged, context: this, slot: &KateSpellCheckDialog::languageChanged);
116 }
117
118 m_view->bottomViewBar()->addBarWidget(newBarWidget: m_sonnetDialog);
119
120 m_userSpellCheckLanguage.clear();
121 m_previousGivenSpellCheckLanguage.clear();
122 delete m_globalSpellCheckRange;
123 // we expand to handle the situation when the last word in the range is replace by a new one
124 m_globalSpellCheckRange =
125 m_view->doc()->newMovingRange(range: KTextEditor::Range(start, end), insertBehaviors: KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
126 m_spellCheckCancelledByUser = false;
127 performSpellCheck(range: *m_globalSpellCheckRange);
128}
129
130KTextEditor::Cursor KateSpellCheckDialog::locatePosition(int pos)
131{
132 uint remains;
133
134 while (m_spellLastPos < (uint)pos) {
135 remains = pos - m_spellLastPos;
136 uint l = m_view->doc()->lineLength(line: m_spellPosCursor.line()) - m_spellPosCursor.column();
137 if (l > remains) {
138 m_spellPosCursor.setColumn(m_spellPosCursor.column() + remains);
139 m_spellLastPos = pos;
140 } else {
141 m_spellPosCursor.setLine(m_spellPosCursor.line() + 1);
142 m_spellPosCursor.setColumn(0);
143 m_spellLastPos += l + 1;
144 }
145 }
146
147 return m_spellPosCursor;
148}
149
150void KateSpellCheckDialog::misspelling(const QString &word, int pos)
151{
152 KTextEditor::Cursor cursor;
153 int length;
154 int origPos = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos);
155 cursor = locatePosition(pos: origPos);
156 length = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: pos + word.length()) - origPos;
157
158 m_view->setCursorPositionInternal(position: cursor, tabwidth: 1);
159 m_view->setSelection(KTextEditor::Range(cursor, length));
160}
161
162void KateSpellCheckDialog::corrected(const QString &word, int pos, const QString &newWord)
163{
164 int origPos = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos);
165
166 int length = m_view->doc()->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: pos + word.length()) - origPos;
167
168 KTextEditor::Cursor replacementStartCursor = locatePosition(pos: origPos);
169 KTextEditor::Range replacementRange = KTextEditor::Range(replacementStartCursor, length);
170 KTextEditor::DocumentPrivate *doc = m_view->doc();
171 KTextEditor::EditorPrivate::self()->spellCheckManager()->replaceCharactersEncodedIfNecessary(newWord, doc, replacementRange);
172
173 // we have to be careful here: due to static word wrapping the text might change in addition to simply
174 // the misspelled word being replaced, i.e. new line breaks might be inserted as well. As such, the text
175 // in the 'Sonnet::Dialog' might be eventually out of sync with the visible text. Therefore, we 'restart'
176 // spell checking from the current position.
177 performSpellCheck(range: KTextEditor::Range(replacementStartCursor, m_globalSpellCheckRange->end()));
178}
179
180void KateSpellCheckDialog::performSpellCheck(KTextEditor::Range range)
181{
182 if (range.isEmpty()) {
183 spellCheckDone();
184 m_sonnetDialog->closed();
185 return;
186 }
187 m_languagesInSpellCheckRange = KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckLanguageRanges(doc: m_view->doc(), range);
188 m_currentLanguageRangeIterator = m_languagesInSpellCheckRange.begin();
189 m_currentSpellCheckRange = KTextEditor::Range::invalid();
190 installNextSpellCheckRange();
191 // first check if there is really something to spell check
192 if (m_currentSpellCheckRange.isValid()) {
193 m_view->bottomViewBar()->showBarWidget(barWidget: m_sonnetDialog);
194 m_sonnetDialog->show();
195 m_sonnetDialog->setFocus();
196 } else {
197 m_sonnetDialog->closed();
198 }
199}
200
201void KateSpellCheckDialog::installNextSpellCheckRange()
202{
203 if (m_spellCheckCancelledByUser || m_currentLanguageRangeIterator == m_languagesInSpellCheckRange.end()) {
204 spellCheckDone();
205 return;
206 }
207 KateSpellCheckManager *spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager();
208 KTextEditor::Cursor nextRangeBegin = (m_currentSpellCheckRange.isValid() ? m_currentSpellCheckRange.end() : KTextEditor::Cursor::invalid());
209 m_currentSpellCheckRange = KTextEditor::Range::invalid();
210 m_currentDecToEncOffsetList.clear();
211 QList<QPair<KTextEditor::Range, QString>> rangeDictionaryPairList;
212 while (m_currentLanguageRangeIterator != m_languagesInSpellCheckRange.end()) {
213 KTextEditor::Range currentLanguageRange = (*m_currentLanguageRangeIterator).first;
214 const QString &dictionary = (*m_currentLanguageRangeIterator).second;
215 KTextEditor::Range languageSubRange =
216 (nextRangeBegin.isValid() ? KTextEditor::Range(nextRangeBegin, currentLanguageRange.end()) : currentLanguageRange);
217 rangeDictionaryPairList = spellCheckManager->spellCheckWrtHighlightingRanges(doc: m_view->doc(), range: languageSubRange, dictionary, singleLine: false, returnSingleRange: true);
218 Q_ASSERT(rangeDictionaryPairList.size() <= 1);
219 if (rangeDictionaryPairList.size() == 0) {
220 ++m_currentLanguageRangeIterator;
221 if (m_currentLanguageRangeIterator != m_languagesInSpellCheckRange.end()) {
222 nextRangeBegin = (*m_currentLanguageRangeIterator).first.start();
223 }
224 } else {
225 m_currentSpellCheckRange = rangeDictionaryPairList.first().first;
226 QString dictionary = rangeDictionaryPairList.first().second;
227 const bool languageChanged = (dictionary != m_previousGivenSpellCheckLanguage);
228 m_previousGivenSpellCheckLanguage = dictionary;
229
230 // if there was no change of dictionary stemming from the document language ranges and
231 // the user has set a dictionary in the dialog, we use that one
232 if (!languageChanged && !m_userSpellCheckLanguage.isEmpty()) {
233 dictionary = m_userSpellCheckLanguage;
234 }
235 // we only allow the user to override the preset dictionary within a language range
236 // given by the document
237 else if (languageChanged) {
238 m_userSpellCheckLanguage.clear();
239 }
240
241 m_spellPosCursor = m_currentSpellCheckRange.start();
242 m_spellLastPos = 0;
243
244 m_currentDecToEncOffsetList.clear();
245 KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList;
246 QString text = m_view->doc()->decodeCharacters(range: m_currentSpellCheckRange, decToEncOffsetList&: m_currentDecToEncOffsetList, encToDecOffsetList);
247 // ensure that no empty string is passed on to Sonnet as this can lead to a crash
248 // (bug 228789)
249 if (text.isEmpty()) {
250 nextRangeBegin = m_currentSpellCheckRange.end();
251 continue;
252 }
253
254 if (m_speller->language() != dictionary) {
255 m_speller->setLanguage(dictionary);
256 m_backgroundChecker->setSpeller(*m_speller);
257 }
258
259 m_sonnetDialog->setBuffer(text);
260 break;
261 }
262 }
263 if (m_currentLanguageRangeIterator == m_languagesInSpellCheckRange.end()) {
264 spellCheckDone();
265 return;
266 }
267}
268
269void KateSpellCheckDialog::cancelClicked()
270{
271 m_spellCheckCancelledByUser = true;
272 spellCheckDone();
273}
274
275void KateSpellCheckDialog::spellCheckDone()
276{
277 m_currentSpellCheckRange = KTextEditor::Range::invalid();
278 m_currentDecToEncOffsetList.clear();
279 m_view->clearSelection();
280}
281
282void KateSpellCheckDialog::objectDestroyed(QObject *object)
283{
284 Q_UNUSED(object);
285 m_sonnetDialog = nullptr;
286}
287
288void KateSpellCheckDialog::languageChanged(const QString &language)
289{
290 m_userSpellCheckLanguage = language;
291}
292
293// END
294
295#include "moc_spellcheckdialog.cpp"
296

source code of ktexteditor/src/spellcheck/spellcheckdialog.cpp