1/*
2 SPDX-FileCopyrightText: 2003 Anders Lund <anders.lund@lund.tdcadsl.dk>
3 SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8// BEGIN includes
9#include "katewordcompletion.h"
10#include "kateconfig.h"
11#include "katedocument.h"
12#include "kateglobal.h"
13#include "kateview.h"
14
15#include <ktexteditor/movingrange.h>
16#include <ktexteditor/range.h>
17
18#include <KAboutData>
19#include <KActionCollection>
20#include <KConfigGroup>
21#include <KLocalizedString>
22#include <KPageDialog>
23#include <KPageWidgetModel>
24#include <KParts/Part>
25#include <KToggleAction>
26
27#include <Sonnet/Speller>
28
29#include <QAction>
30#include <QCheckBox>
31#include <QLabel>
32#include <QLayout>
33#include <QRegularExpression>
34#include <QSet>
35#include <QSpinBox>
36#include <QString>
37
38// END
39
40/// amount of lines to scan backwards and forwards
41static const int maxLinesToScan = 10000;
42
43// BEGIN KateWordCompletionModel
44KateWordCompletionModel::KateWordCompletionModel(QObject *parent)
45 : CodeCompletionModel(parent)
46 , m_automatic(false)
47{
48 setHasGroups(false);
49}
50
51KateWordCompletionModel::~KateWordCompletionModel()
52{
53}
54
55void KateWordCompletionModel::saveMatches(KTextEditor::View *view, const KTextEditor::Range &range)
56{
57 m_matches = allMatches(view, range);
58 m_matches.sort();
59}
60
61QVariant KateWordCompletionModel::data(const QModelIndex &index, int role) const
62{
63 if (role == UnimportantItemRole) {
64 return QVariant(true);
65 }
66 if (role == InheritanceDepth) {
67 return 10000;
68 }
69
70 if (!index.parent().isValid()) {
71 // It is the group header
72 switch (role) {
73 case Qt::DisplayRole:
74 return i18n("Auto Word Completion");
75 case GroupRole:
76 return Qt::DisplayRole;
77 }
78 }
79
80 if (index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole) {
81 return m_matches.at(i: index.row());
82 }
83
84 if (index.column() == KTextEditor::CodeCompletionModel::Icon && role == Qt::DecorationRole) {
85 static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text")).pixmap(size: QSize(16, 16)));
86 return icon;
87 }
88
89 return QVariant();
90}
91
92QModelIndex KateWordCompletionModel::parent(const QModelIndex &index) const
93{
94 if (index.internalId()) {
95 return createIndex(arow: 0, acolumn: 0, aid: quintptr(0));
96 } else {
97 return QModelIndex();
98 }
99}
100
101QModelIndex KateWordCompletionModel::index(int row, int column, const QModelIndex &parent) const
102{
103 if (!parent.isValid()) {
104 if (row == 0) {
105 return createIndex(arow: row, acolumn: column, aid: quintptr(0));
106 } else {
107 return QModelIndex();
108 }
109
110 } else if (parent.parent().isValid()) {
111 return QModelIndex();
112 }
113
114 if (row < 0 || row >= m_matches.count() || column < 0 || column >= ColumnCount) {
115 return QModelIndex();
116 }
117
118 return createIndex(arow: row, acolumn: column, aid: 1);
119}
120
121int KateWordCompletionModel::rowCount(const QModelIndex &parent) const
122{
123 if (!parent.isValid() && !m_matches.isEmpty()) {
124 return 1; // One root node to define the custom group
125 } else if (parent.parent().isValid()) {
126 return 0; // Completion-items have no children
127 } else {
128 return m_matches.count();
129 }
130}
131
132bool KateWordCompletionModel::shouldStartCompletion(KTextEditor::View *view,
133 const QString &insertedText,
134 bool userInsertion,
135 const KTextEditor::Cursor &position)
136{
137 if (!userInsertion) {
138 return false;
139 }
140 if (insertedText.isEmpty()) {
141 return false;
142 }
143
144 KTextEditor::ViewPrivate *v = qobject_cast<KTextEditor::ViewPrivate *>(object: view);
145
146 const QString &text = view->document()->line(line: position.line()).left(n: position.column());
147 const uint check = v->config()->wordCompletionMinimalWordLength();
148 // Start completion immediately if min. word size is zero
149 if (!check) {
150 return true;
151 }
152 // Otherwise, check if user has typed long enough text...
153 const int start = text.length();
154 const int end = start - check;
155 if (end < 0) {
156 return false;
157 }
158 for (int i = start - 1; i >= end; i--) {
159 const QChar c = text.at(i);
160 if (!(c.isLetter() || (c.isNumber()) || c == QLatin1Char('_'))) {
161 return false;
162 }
163 }
164
165 return true;
166}
167
168bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString &currentCompletion)
169{
170 if (m_automatic) {
171 KTextEditor::ViewPrivate *v = qobject_cast<KTextEditor::ViewPrivate *>(object: view);
172 if (currentCompletion.length() < v->config()->wordCompletionMinimalWordLength()) {
173 return true;
174 }
175 }
176
177 return CodeCompletionModelControllerInterface::shouldAbortCompletion(view, range, currentCompletion);
178}
179
180void KateWordCompletionModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it)
181{
182 m_automatic = it == AutomaticInvocation;
183 saveMatches(view, range);
184}
185
186/**
187 * Scan throughout the entire document for possible completions,
188 * ignoring any dublets and words shorter than configured and/or
189 * reasonable minimum length.
190 */
191QStringList KateWordCompletionModel::allMatches(KTextEditor::View *view, const KTextEditor::Range &range)
192{
193 QSet<QStringView> result;
194 const int minWordSize = qMax(a: 2, b: qobject_cast<KTextEditor::ViewPrivate *>(object: view)->config()->wordCompletionMinimalWordLength());
195 const auto cursorPosition = view->cursorPosition();
196 const auto document = view->document();
197 const int startLine = std::max(a: 0, b: cursorPosition.line() - maxLinesToScan);
198 const int endLine = std::min(a: cursorPosition.line() + maxLinesToScan, b: view->document()->lines());
199 for (int line = startLine; line < endLine; line++) {
200 const QString text = document->line(line);
201 if (text.isEmpty() || text.isNull()) {
202 continue;
203 }
204 QStringView textView = text;
205 int wordBegin = 0;
206 int offset = 0;
207 const int end = text.size();
208 const bool cursorLine = cursorPosition.line() == line;
209 const bool isNotLastLine = line != range.end().line();
210 const QChar *d = text.data();
211 while (offset < end) {
212 const QChar c = d[offset];
213 // increment offset when at line end, so we take the last character too
214 if ((!c.isLetterOrNumber() && c != QChar(u'_')) || (offset == end - 1 && offset++)) {
215 if (offset - wordBegin >= minWordSize && (isNotLastLine || offset != range.end().column())) {
216 // don't add the word we are inside with cursor!
217 if (!cursorLine || (cursorPosition.column() < wordBegin || cursorPosition.column() > offset)) {
218 result.insert(value: textView.mid(pos: wordBegin, n: offset - wordBegin));
219 }
220 }
221 wordBegin = offset + 1;
222 }
223 if (c.isSpace()) {
224 wordBegin = offset + 1;
225 }
226 offset += 1;
227 }
228 }
229
230 // ensure words that are ok spell check wise always end up in the completion, see bug 468705
231 const auto language = static_cast<KTextEditor::DocumentPrivate *>(document)->defaultDictionary();
232 const auto word = view->document()->text(range);
233 Sonnet::Speller speller;
234 QStringList spellerSuggestions;
235 speller.setLanguage(language);
236 if (speller.isValid()) {
237 if (speller.isCorrect(word)) {
238 result.insert(value: word);
239 } else {
240 spellerSuggestions = speller.suggest(word);
241 for (const auto &alternative : std::as_const(t&: spellerSuggestions)) {
242 result.insert(value: alternative);
243 }
244 }
245 }
246
247 m_matches.clear();
248 m_matches.reserve(asize: result.size());
249 for (auto v : std::as_const(t&: result)) {
250 m_matches << v.toString();
251 }
252
253 return m_matches;
254}
255
256void KateWordCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
257{
258 view->document()->replaceText(range: word, text: m_matches.at(i: index.row()));
259}
260
261KTextEditor::CodeCompletionModelControllerInterface::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex & /*matched*/)
262{
263 return HideListIfAutomaticInvocation;
264}
265
266bool KateWordCompletionModel::shouldHideItemsWithEqualNames() const
267{
268 // We don't want word-completion items if the same items
269 // are available through more sophisticated completion models
270 return true;
271}
272
273// END KateWordCompletionModel
274
275// BEGIN KateWordCompletionView
276struct KateWordCompletionViewPrivate {
277 KTextEditor::MovingRange *liRange; // range containing last inserted text
278 KTextEditor::Range dcRange; // current range to be completed by directional completion
279 KTextEditor::Cursor dcCursor; // directional completion search cursor
280 int directionalPos; // be able to insert "" at the correct time
281 bool isCompleting; // true when the directional completion is doing a completion
282};
283
284KateWordCompletionView::KateWordCompletionView(KTextEditor::View *view, KActionCollection *ac)
285 : QObject(view)
286 , m_view(view)
287 , m_dWCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel())
288 , d(new KateWordCompletionViewPrivate)
289{
290 d->isCompleting = false;
291 d->dcRange = KTextEditor::Range::invalid();
292
293 d->liRange =
294 static_cast<KTextEditor::DocumentPrivate *>(m_view->document())->newMovingRange(range: KTextEditor::Range::invalid(), insertBehaviors: KTextEditor::MovingRange::DoNotExpand);
295
296 KTextEditor::Attribute::Ptr a = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
297 a->setBackground(static_cast<KTextEditor::ViewPrivate *>(view)->rendererConfig()->selectionColor());
298 d->liRange->setAttribute(a);
299
300 QAction *action;
301
302 action = new QAction(i18n("Shell Completion"), this);
303 ac->addAction(QStringLiteral("doccomplete_sh"), action);
304 action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
305 connect(sender: action, signal: &QAction::triggered, context: this, slot: &KateWordCompletionView::shellComplete);
306
307 action = new QAction(i18n("Reuse Word Above"), this);
308 ac->addAction(QStringLiteral("doccomplete_bw"), action);
309 ac->setDefaultShortcut(action, shortcut: Qt::CTRL | Qt::Key_8);
310 action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
311 connect(sender: action, signal: &QAction::triggered, context: this, slot: &KateWordCompletionView::completeBackwards);
312
313 action = new QAction(i18n("Reuse Word Below"), this);
314 ac->addAction(QStringLiteral("doccomplete_fw"), action);
315 ac->setDefaultShortcut(action, shortcut: Qt::CTRL | Qt::Key_9);
316 action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
317 connect(sender: action, signal: &QAction::triggered, context: this, slot: &KateWordCompletionView::completeForwards);
318}
319
320KateWordCompletionView::~KateWordCompletionView()
321{
322 delete d;
323}
324
325void KateWordCompletionView::completeBackwards()
326{
327 complete(fw: false);
328}
329
330void KateWordCompletionView::completeForwards()
331{
332 complete();
333}
334
335// Pop up the editors completion list if applicable
336void KateWordCompletionView::popupCompletionList()
337{
338 qCDebug(LOG_KTE) << "entered ...";
339 KTextEditor::Range r = range();
340
341 if (m_view->isCompletionActive()) {
342 return;
343 }
344
345 m_dWCompletionModel->saveMatches(view: m_view, range: r);
346
347 qCDebug(LOG_KTE) << "after save matches ...";
348
349 if (!m_dWCompletionModel->rowCount(parent: QModelIndex())) {
350 return;
351 }
352
353 m_view->startCompletion(word: r, model: m_dWCompletionModel);
354}
355
356// Contributed by <brain@hdsnet.hu>
357void KateWordCompletionView::shellComplete()
358{
359 KTextEditor::Range r = range();
360
361 const QStringList matches = m_dWCompletionModel->allMatches(view: m_view, range: r);
362
363 if (matches.size() == 0) {
364 return;
365 }
366
367 QString partial = findLongestUnique(matches, lead: r.columnWidth());
368
369 if (partial.isEmpty()) {
370 popupCompletionList();
371 }
372
373 else {
374 m_view->document()->insertText(position: r.end(), text: partial.mid(position: r.columnWidth()));
375 d->liRange->setView(m_view);
376 d->liRange->setRange(KTextEditor::Range(r.end(), partial.length() - r.columnWidth()));
377 connect(sender: m_view, signal: &KTextEditor::View::cursorPositionChanged, context: this, slot: &KateWordCompletionView::slotCursorMoved);
378 }
379}
380
381// Do one completion, searching in the desired direction,
382// if possible
383void KateWordCompletionView::complete(bool fw)
384{
385 KTextEditor::Range r = range();
386
387 int inc = fw ? 1 : -1;
388 KTextEditor::Document *doc = m_view->document();
389
390 if (d->dcRange.isValid()) {
391 // qCDebug(LOG_KTE)<<"CONTINUE "<<d->dcRange;
392 // this is a repeated activation
393
394 // if we are back to where we started, reset.
395 if ((fw && d->directionalPos == -1) || (!fw && d->directionalPos == 1)) {
396 const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
397 if (spansColumns > 0) {
398 doc->removeText(range: *d->liRange);
399 }
400
401 d->liRange->setRange(KTextEditor::Range::invalid());
402 d->dcCursor = r.end();
403 d->directionalPos = 0;
404
405 return;
406 }
407
408 if (fw) {
409 const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
410 d->dcCursor.setColumn(d->dcCursor.column() + spansColumns);
411 }
412
413 d->directionalPos += inc;
414 } else { // new completion, reset all
415 // qCDebug(LOG_KTE)<<"RESET FOR NEW";
416 d->dcRange = r;
417 d->liRange->setRange(KTextEditor::Range::invalid());
418 d->dcCursor = r.start();
419 d->directionalPos = inc;
420
421 d->liRange->setView(m_view);
422
423 connect(sender: m_view, signal: &KTextEditor::View::cursorPositionChanged, context: this, slot: &KateWordCompletionView::slotCursorMoved);
424 }
425
426 const QRegularExpression wordRegEx(QLatin1String("\\b") + doc->text(range: d->dcRange) + QLatin1String("(\\w+)"), QRegularExpression::UseUnicodePropertiesOption);
427 int pos(0);
428 QString ln = doc->line(line: d->dcCursor.line());
429
430 while (true) {
431 // qCDebug(LOG_KTE)<<"SEARCHING FOR "<<wordRegEx.pattern()<<" "<<ln<<" at "<<d->dcCursor;
432 QRegularExpressionMatch match;
433 pos = fw ? ln.indexOf(re: wordRegEx, from: d->dcCursor.column(), rmatch: &match) : ln.lastIndexOf(re: wordRegEx, from: d->dcCursor.column(), rmatch: &match);
434
435 if (match.hasMatch()) { // we matched a word
436 // qCDebug(LOG_KTE)<<"USABLE MATCH";
437 const QStringView m = match.capturedView(nth: 1);
438 if (m != doc->text(range: *d->liRange) && (d->dcCursor.line() != d->dcRange.start().line() || pos != d->dcRange.start().column())) {
439 // we got good a match! replace text and return.
440 d->isCompleting = true;
441 KTextEditor::Range replaceRange(d->liRange->toRange());
442 if (!replaceRange.isValid()) {
443 replaceRange.setRange(start: r.end(), end: r.end());
444 }
445 doc->replaceText(range: replaceRange, text: m.toString());
446 d->liRange->setRange(KTextEditor::Range(d->dcRange.end(), m.length()));
447
448 d->dcCursor.setColumn(pos); // for next try
449
450 d->isCompleting = false;
451 return;
452 }
453
454 // equal to last one, continue
455 else {
456 // qCDebug(LOG_KTE)<<"SKIPPING, EQUAL MATCH";
457 d->dcCursor.setColumn(pos); // for next try
458
459 if (fw) {
460 d->dcCursor.setColumn(pos + m.length());
461 }
462
463 else {
464 if (pos == 0) {
465 if (d->dcCursor.line() > 0) {
466 int l = d->dcCursor.line() + inc;
467 ln = doc->line(line: l);
468 d->dcCursor.setPosition(line: l, column: ln.length());
469 } else {
470 return;
471 }
472 }
473
474 else {
475 d->dcCursor.setColumn(d->dcCursor.column() - 1);
476 }
477 }
478 }
479 }
480
481 else { // no match
482 // qCDebug(LOG_KTE)<<"NO MATCH";
483 if ((!fw && d->dcCursor.line() == 0) || (fw && d->dcCursor.line() >= doc->lines())) {
484 return;
485 }
486
487 int l = d->dcCursor.line() + inc;
488 ln = doc->line(line: l);
489 d->dcCursor.setPosition(line: l, column: fw ? 0 : ln.length());
490 }
491 } // while true
492}
493
494void KateWordCompletionView::slotCursorMoved()
495{
496 if (d->isCompleting) {
497 return;
498 }
499
500 d->dcRange = KTextEditor::Range::invalid();
501
502 disconnect(sender: m_view, signal: &KTextEditor::View::cursorPositionChanged, receiver: this, slot: &KateWordCompletionView::slotCursorMoved);
503
504 d->liRange->setView(nullptr);
505 d->liRange->setRange(KTextEditor::Range::invalid());
506}
507
508// Contributed by <brain@hdsnet.hu> FIXME
509QString KateWordCompletionView::findLongestUnique(const QStringList &matches, int lead)
510{
511 QString partial = matches.first();
512
513 for (const QString &current : matches) {
514 if (!current.startsWith(s: partial)) {
515 while (partial.length() > lead) {
516 partial.remove(i: partial.length() - 1, len: 1);
517 if (current.startsWith(s: partial)) {
518 break;
519 }
520 }
521
522 if (partial.length() == lead) {
523 return QString();
524 }
525 }
526 }
527
528 return partial;
529}
530
531// Return the string to complete (the letters behind the cursor)
532QString KateWordCompletionView::word() const
533{
534 return m_view->document()->text(range: range());
535}
536
537// Return the range containing the word behind the cursor
538KTextEditor::Range KateWordCompletionView::range() const
539{
540 return m_dWCompletionModel->completionRange(view: m_view, position: m_view->cursorPosition());
541}
542// END
543
544#include "moc_katewordcompletion.cpp"
545

source code of ktexteditor/src/completion/katewordcompletion.cpp