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