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 ¤tCompletion)
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 ¤t : 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