1/*
2 SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>
3 SPDX-FileCopyrightText: 2007-2008 David Nolden <david.nolden.kdevelop@art-master.de>
4 SPDX-FileCopyrightText: 2022-2024 Waqar Ahmed <waqar.17a@gmail.com>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "katecompletionwidget.h"
10
11#include <ktexteditor/codecompletionmodelcontrollerinterface.h>
12
13#include "kateconfig.h"
14#include "katedocument.h"
15#include "kateglobal.h"
16#include "katerenderer.h"
17#include "kateview.h"
18
19#include "documentation_tip.h"
20#include "kateargumenthintmodel.h"
21#include "kateargumenthinttree.h"
22#include "katecompletionmodel.h"
23#include "katecompletiontree.h"
24#include "katepartdebug.h"
25
26#include <QAbstractScrollArea>
27#include <QApplication>
28#include <QBoxLayout>
29#include <QHeaderView>
30#include <QLabel>
31#include <QPushButton>
32#include <QScreen>
33#include <QScrollBar>
34#include <QSizeGrip>
35#include <QTimer>
36#include <QToolButton>
37
38const bool hideAutomaticCompletionOnExactMatch = true;
39
40#define CALLCI(WHAT, WHATELSE, WHAT2, model, FUNC) \
41 { \
42 static KTextEditor::CodeCompletionModelControllerInterface defaultIf; \
43 KTextEditor::CodeCompletionModelControllerInterface *ret = qobject_cast<KTextEditor::CodeCompletionModelControllerInterface *>(model); \
44 if (!ret) { \
45 WHAT2 defaultIf.FUNC; \
46 } else \
47 WHAT2 ret->FUNC; \
48 }
49
50static KTextEditor::Range _completionRange(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, KTextEditor::Cursor cursor)
51{
52 CALLCI(return, , return, model, completionRange(view, cursor));
53}
54
55static KTextEditor::Range _updateRange(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, KTextEditor::Range &range)
56{
57 CALLCI(, return range, return, model, updateCompletionRange(view, range));
58}
59
60static QString _filterString(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, const KTextEditor::Range &range, KTextEditor::Cursor cursor)
61{
62 CALLCI(return, , return, model, filterString(view, range, cursor));
63}
64
65static bool
66_shouldAbortCompletion(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, const KTextEditor::Range &range, const QString &currentCompletion)
67{
68 CALLCI(return, , return, model, shouldAbortCompletion(view, range, currentCompletion));
69}
70
71static void _aborted(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view)
72{
73 CALLCI(return, , return, model, aborted(view));
74}
75
76static bool _shouldStartCompletion(KTextEditor::CodeCompletionModel *model,
77 KTextEditor::View *view,
78 const QString &automaticInvocationLine,
79 bool m_lastInsertionByUser,
80 KTextEditor::Cursor cursor)
81{
82 CALLCI(return, , return, model, shouldStartCompletion(view, automaticInvocationLine, m_lastInsertionByUser, cursor));
83}
84
85KateCompletionWidget::KateCompletionWidget(KTextEditor::ViewPrivate *parent)
86 : QFrame(parent)
87 , m_presentationModel(new KateCompletionModel(this))
88 , m_view(parent)
89 , m_entryList(new KateCompletionTree(this))
90 , m_argumentHintModel(new KateArgumentHintModel(m_presentationModel))
91 , m_argumentHintWidget(new ArgumentHintWidget(m_argumentHintModel, parent->renderer()->currentFont(), this, this))
92 , m_docTip(new DocTip(this))
93 , m_automaticInvocationDelay(100)
94 , m_lastInsertionByUser(false)
95 , m_isSuspended(false)
96 , m_dontShowArgumentHints(false)
97 , m_needShow(false)
98 , m_hadCompletionNavigation(false)
99 , m_noAutoHide(false)
100 , m_completionEditRunning(false)
101 , m_expandedAddedHeightBase(0)
102 , m_lastInvocationType(KTextEditor::CodeCompletionModel::AutomaticInvocation)
103{
104 if (parent->mainWindow() != KTextEditor::EditorPrivate::self()->dummyMainWindow() && parent->mainWindow()->window()) {
105 setParent(parent->mainWindow()->window());
106 } else if (auto w = m_view->window()) {
107 setParent(w);
108 } else if (auto w = QApplication::activeWindow()) {
109 setParent(w);
110 } else {
111 setParent(parent);
112 }
113 m_docTip->setParent(this->parentWidget());
114 parentWidget()->installEventFilter(filterObj: this);
115
116 setFrameStyle(QFrame::Box | QFrame::Raised);
117 setLineWidth(1);
118
119 m_entryList->setModel(m_presentationModel);
120 m_entryList->setColumnWidth(column: 0, width: 0); // These will be determined automatically in KateCompletionTree::resizeColumns
121 m_entryList->setColumnWidth(column: 1, width: 0);
122 m_entryList->setColumnWidth(column: 2, width: 0);
123
124 m_argumentHintWidget->setParent(this->parentWidget());
125
126 // trigger completion on double click on completion list
127 connect(sender: m_entryList, signal: &KateCompletionTree::doubleClicked, context: this, slot: &KateCompletionWidget::execute);
128
129 connect(sender: view(), signal: &KTextEditor::ViewPrivate::focusOut, context: this, slot: &KateCompletionWidget::viewFocusOut);
130
131 m_automaticInvocationTimer = new QTimer(this);
132 m_automaticInvocationTimer->setSingleShot(true);
133 connect(sender: m_automaticInvocationTimer, signal: &QTimer::timeout, context: this, slot: &KateCompletionWidget::automaticInvocation);
134
135 // Keep branches expanded
136 connect(sender: m_presentationModel, signal: &KateCompletionModel::modelReset, context: this, slot: &KateCompletionWidget::modelReset);
137 connect(sender: m_presentationModel, signal: &KateCompletionModel::rowsInserted, context: this, slot: &KateCompletionWidget::rowsInserted);
138 connect(sender: m_argumentHintModel, signal: &KateArgumentHintModel::contentStateChanged, context: this, slot: &KateCompletionWidget::argumentHintsChanged);
139
140 // No smart lock, no queued connects
141 connect(sender: view(), signal: &KTextEditor::ViewPrivate::cursorPositionChanged, context: this, slot: &KateCompletionWidget::cursorPositionChanged);
142 connect(sender: view(), signal: &KTextEditor::ViewPrivate::verticalScrollPositionChanged, context: this, slot: [this] {
143 abortCompletion();
144 });
145
146 // connect to all possible editing primitives
147 connect(sender: view()->doc(), signal: &KTextEditor::Document::lineWrapped, context: this, slot: &KateCompletionWidget::wrapLine);
148 connect(sender: view()->doc(), signal: &KTextEditor::Document::lineUnwrapped, context: this, slot: &KateCompletionWidget::unwrapLine);
149 connect(sender: view()->doc(), signal: &KTextEditor::Document::textInserted, context: this, slot: &KateCompletionWidget::insertText);
150 connect(sender: view()->doc(), signal: &KTextEditor::Document::textRemoved, context: this, slot: &KateCompletionWidget::removeText);
151
152 // This is a non-focus widget, it is passed keyboard input from the view
153
154 // We need to do this, because else the focus goes to nirvana without any control when the completion-widget is clicked.
155 setFocusPolicy(Qt::ClickFocus);
156
157 const auto children = findChildren<QWidget *>();
158 for (QWidget *childWidget : children) {
159 childWidget->setFocusPolicy(Qt::NoFocus);
160 }
161
162 // Position the entry-list so a frame can be drawn around it
163 m_entryList->move(ax: frameWidth(), ay: frameWidth());
164
165 hide();
166 m_docTip->setVisible(false);
167}
168
169KateCompletionWidget::~KateCompletionWidget()
170{
171 // ensure no slot triggered during destruction => else we access already invalidated stuff
172 m_presentationModel->disconnect(receiver: this);
173 m_argumentHintModel->disconnect(receiver: this);
174
175 delete m_docTip;
176}
177
178void KateCompletionWidget::viewFocusOut()
179{
180 QWidget *toplevels[4] = {this, m_entryList, m_docTip, m_argumentHintWidget};
181 if (!std::any_of(first: std::begin(arr&: toplevels), last: std::end(arr&: toplevels), pred: [](QWidget *w) {
182 auto fw = QApplication::focusWidget();
183 return fw == w || w->isAncestorOf(child: fw);
184 })) {
185 abortCompletion();
186 }
187}
188
189void KateCompletionWidget::focusOutEvent(QFocusEvent *)
190{
191 abortCompletion();
192}
193
194void KateCompletionWidget::modelContentChanged()
195{
196 ////qCDebug(LOG_KTE)<<">>>>>>>>>>>>>>>>";
197 if (m_completionRanges.isEmpty()) {
198 // qCDebug(LOG_KTE) << "content changed, but no completion active";
199 abortCompletion();
200 return;
201 }
202
203 if (!view()->hasFocus()) {
204 // qCDebug(LOG_KTE) << "view does not have focus";
205 return;
206 }
207
208 if (!m_waitingForReset.isEmpty()) {
209 // qCDebug(LOG_KTE) << "waiting for" << m_waitingForReset.size() << "completion-models to reset";
210 return;
211 }
212
213 int realItemCount = 0;
214 const auto completionModels = m_presentationModel->completionModels();
215 for (KTextEditor::CodeCompletionModel *model : completionModels) {
216 realItemCount += model->rowCount();
217 }
218 if (!m_isSuspended && ((isHidden() && m_argumentHintWidget->isHidden()) || m_needShow) && realItemCount != 0) {
219 m_needShow = false;
220 updateAndShow();
221 }
222
223 if (m_argumentHintModel->rowCount(parent: QModelIndex()) == 0) {
224 m_argumentHintWidget->hide();
225 }
226
227 if (m_presentationModel->rowCount(parent: QModelIndex()) == 0) {
228 hide();
229 }
230
231 // For automatic invocations, only autoselect first completion entry when enabled in the config
232 if (m_lastInvocationType != KTextEditor::CodeCompletionModel::AutomaticInvocation || view()->config()->automaticCompletionPreselectFirst()) {
233 m_entryList->setCurrentIndex(model()->index(row: 0, column: 0));
234 }
235 // With each filtering items can be added or removed, so we have to reset the current index here so we always have a selected item
236 if (!model()->indexIsItem(index: m_entryList->currentIndex())) {
237 QModelIndex firstIndex = model()->index(row: 0, column: 0, parent: m_entryList->currentIndex());
238 m_entryList->setCurrentIndex(firstIndex);
239 // m_entryList->scrollTo(firstIndex, QAbstractItemView::PositionAtTop);
240 }
241
242 updateHeight();
243
244 // New items for the argument-hint tree may have arrived, so check whether it needs to be shown
245 if (m_argumentHintWidget->isHidden() && !m_dontShowArgumentHints && m_argumentHintModel->rowCount(parent: QModelIndex()) != 0) {
246 m_argumentHintWidget->positionAndShow();
247 }
248
249 if (!m_noAutoHide && hideAutomaticCompletionOnExactMatch && !isHidden() && m_lastInvocationType == KTextEditor::CodeCompletionModel::AutomaticInvocation
250 && m_presentationModel->shouldMatchHideCompletionList()) {
251 hide();
252 } else if (isHidden() && !m_presentationModel->shouldMatchHideCompletionList() && m_presentationModel->rowCount(parent: QModelIndex())) {
253 show();
254 }
255}
256
257KateArgumentHintModel *KateCompletionWidget::argumentHintModel() const
258{
259 return m_argumentHintModel;
260}
261
262const KateCompletionModel *KateCompletionWidget::model() const
263{
264 return m_presentationModel;
265}
266
267KateCompletionModel *KateCompletionWidget::model()
268{
269 return m_presentationModel;
270}
271
272void KateCompletionWidget::rowsInserted(const QModelIndex &parent, int rowFrom, int rowEnd)
273{
274 m_entryList->setAnimated(false);
275
276 if (!parent.isValid()) {
277 for (int i = rowFrom; i <= rowEnd; ++i) {
278 m_entryList->expand(index: m_presentationModel->index(row: i, column: 0, parent));
279 }
280 }
281}
282
283KTextEditor::ViewPrivate *KateCompletionWidget::view() const
284{
285 return m_view;
286}
287
288void KateCompletionWidget::argumentHintsChanged(bool hasContent)
289{
290 m_dontShowArgumentHints = !hasContent;
291
292 if (m_dontShowArgumentHints) {
293 m_argumentHintWidget->hide();
294 } else {
295 updateArgumentHintGeometry();
296 }
297}
298
299void KateCompletionWidget::startCompletion(KTextEditor::CodeCompletionModel::InvocationType invocationType,
300 const QList<KTextEditor::CodeCompletionModel *> &models)
301{
302 if (invocationType == KTextEditor::CodeCompletionModel::UserInvocation) {
303 abortCompletion();
304 }
305 startCompletion(word: KTextEditor::Range(KTextEditor::Cursor(-1, -1), KTextEditor::Cursor(-1, -1)), models, invocationType);
306}
307
308void KateCompletionWidget::deleteCompletionRanges()
309{
310 for (const CompletionRange &r : std::as_const(t&: m_completionRanges)) {
311 delete r.range;
312 }
313 m_completionRanges.clear();
314}
315
316void KateCompletionWidget::startCompletion(KTextEditor::Range word,
317 KTextEditor::CodeCompletionModel *model,
318 KTextEditor::CodeCompletionModel::InvocationType invocationType)
319{
320 QList<KTextEditor::CodeCompletionModel *> models;
321 if (model) {
322 models << model;
323 } else {
324 models = m_sourceModels;
325 }
326 startCompletion(word, models, invocationType);
327}
328
329void KateCompletionWidget::startCompletion(KTextEditor::Range word,
330 const QList<KTextEditor::CodeCompletionModel *> &modelsToStart,
331 KTextEditor::CodeCompletionModel::InvocationType invocationType)
332{
333 ////qCDebug(LOG_KTE)<<"============";
334
335 m_isSuspended = false;
336 m_needShow = true;
337
338 if (m_completionRanges.isEmpty()) {
339 m_noAutoHide = false; // Re-enable auto-hide on every clean restart of the completion
340 }
341
342 m_lastInvocationType = invocationType;
343
344 disconnect(sender: this->model(), signal: &KateCompletionModel::layoutChanged, receiver: this, slot: &KateCompletionWidget::modelContentChanged);
345 disconnect(sender: this->model(), signal: &KateCompletionModel::modelReset, receiver: this, slot: &KateCompletionWidget::modelContentChanged);
346
347 m_dontShowArgumentHints = true;
348
349 QList<KTextEditor::CodeCompletionModel *> models = (modelsToStart.isEmpty() ? m_sourceModels : modelsToStart);
350
351 for (auto it = m_completionRanges.keyBegin(), end = m_completionRanges.keyEnd(); it != end; ++it) {
352 KTextEditor::CodeCompletionModel *model = *it;
353 if (!models.contains(t: model)) {
354 models << model;
355 }
356 }
357
358 m_presentationModel->clearCompletionModels();
359
360 if (invocationType == KTextEditor::CodeCompletionModel::UserInvocation) {
361 deleteCompletionRanges();
362 }
363
364 for (KTextEditor::CodeCompletionModel *model : std::as_const(t&: models)) {
365 KTextEditor::Range range;
366 if (word.isValid()) {
367 range = word;
368 // qCDebug(LOG_KTE)<<"word is used";
369 } else {
370 range = _completionRange(model, view: view(), cursor: view()->cursorPosition());
371 // qCDebug(LOG_KTE)<<"completionRange has been called, cursor pos is"<<view()->cursorPosition();
372 }
373 // qCDebug(LOG_KTE)<<"range is"<<range;
374 if (!range.isValid()) {
375 if (m_completionRanges.contains(key: model)) {
376 KTextEditor::MovingRange *oldRange = m_completionRanges[model].range;
377 // qCDebug(LOG_KTE)<<"removing completion range 1";
378 m_completionRanges.remove(key: model);
379 delete oldRange;
380 }
381 models.removeAll(t: model);
382 continue;
383 }
384 if (m_completionRanges.contains(key: model)) {
385 if (*m_completionRanges[model].range == range) {
386 continue; // Leave it running as it is
387 } else { // delete the range that was used previously
388 KTextEditor::MovingRange *oldRange = m_completionRanges[model].range;
389 // qCDebug(LOG_KTE)<<"removing completion range 2";
390 m_completionRanges.remove(key: model);
391 delete oldRange;
392 }
393 }
394
395 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::waitForReset, context: this, slot: &KateCompletionWidget::waitForModelReset);
396
397 // qCDebug(LOG_KTE)<<"Before completion invoke: range:"<<range;
398 model->completionInvoked(view: view(), range, invocationType);
399
400 disconnect(sender: model, signal: &KTextEditor::CodeCompletionModel::waitForReset, receiver: this, slot: &KateCompletionWidget::waitForModelReset);
401
402 m_completionRanges[model] =
403 CompletionRange(view()->doc()->newMovingRange(range, insertBehaviors: KTextEditor::MovingRange::ExpandRight | KTextEditor::MovingRange::ExpandLeft));
404
405 // In automatic invocation mode, hide the completion widget as soon as the position where the completion was started is passed to the left
406 m_completionRanges[model].leftBoundary = view()->cursorPosition();
407
408 // In manual invocation mode, bound the activity either the point from where completion was invoked, or to the start of the range
409 if (invocationType != KTextEditor::CodeCompletionModel::AutomaticInvocation) {
410 if (range.start() < m_completionRanges[model].leftBoundary) {
411 m_completionRanges[model].leftBoundary = range.start();
412 }
413 }
414
415 if (!m_completionRanges[model].range->toRange().isValid()) {
416 qCWarning(LOG_KTE) << "Could not construct valid smart-range from" << range << "instead got" << *m_completionRanges[model].range;
417 abortCompletion();
418 return;
419 }
420 }
421
422 m_presentationModel->setCompletionModels(models);
423
424 cursorPositionChanged();
425
426 if (!m_completionRanges.isEmpty()) {
427 connect(sender: this->model(), signal: &KateCompletionModel::layoutChanged, context: this, slot: &KateCompletionWidget::modelContentChanged);
428 connect(sender: this->model(), signal: &KateCompletionModel::modelReset, context: this, slot: &KateCompletionWidget::modelContentChanged);
429 // Now that all models have been notified, check whether the widget should be displayed instantly
430 modelContentChanged();
431 } else {
432 abortCompletion();
433 }
434}
435
436QString KateCompletionWidget::tailString() const
437{
438 if (!KateViewConfig::global()->wordCompletionRemoveTail()) {
439 return QString();
440 }
441
442 const int line = view()->cursorPosition().line();
443 const int column = view()->cursorPosition().column();
444
445 const QString text = view()->document()->line(line);
446
447 static constexpr auto options = QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::DontCaptureOption;
448 static const QRegularExpression findWordEnd(QStringLiteral("^[_\\w]*\\b"), options);
449
450 QRegularExpressionMatch match = findWordEnd.match(subject: text.mid(position: column));
451 if (match.hasMatch()) {
452 return match.captured(nth: 0);
453 }
454 return QString();
455}
456
457void KateCompletionWidget::waitForModelReset()
458{
459 KTextEditor::CodeCompletionModel *senderModel = qobject_cast<KTextEditor::CodeCompletionModel *>(object: sender());
460 if (!senderModel) {
461 qCWarning(LOG_KTE) << "waitForReset signal from bad model";
462 return;
463 }
464 m_waitingForReset.insert(value: senderModel);
465}
466
467void KateCompletionWidget::updateAndShow()
468{
469 // qCDebug(LOG_KTE)<<"*******************************************";
470 if (!view()->hasFocus()) {
471 qCDebug(LOG_KTE) << "view does not have focus";
472 return;
473 }
474
475 setUpdatesEnabled(false);
476
477 modelReset();
478
479 m_argumentHintModel->buildRows();
480 if (m_argumentHintModel->rowCount(parent: QModelIndex()) != 0) {
481 argumentHintsChanged(hasContent: true);
482 }
483
484 // update height first
485 updateHeight();
486 // then resize columns afterwards because we need height information
487 m_entryList->resizeColumns(firstShow: true, forceResize: true);
488 // lastly update position as now we have height and width
489 updatePosition(force: true);
490
491 setUpdatesEnabled(true);
492
493 if (m_argumentHintModel->rowCount(parent: QModelIndex())) {
494 updateArgumentHintGeometry();
495 m_argumentHintWidget->positionAndShow();
496 } else {
497 m_argumentHintWidget->hide();
498 }
499
500 if (m_presentationModel->rowCount()
501 && (!m_presentationModel->shouldMatchHideCompletionList() || !hideAutomaticCompletionOnExactMatch
502 || m_lastInvocationType != KTextEditor::CodeCompletionModel::AutomaticInvocation)) {
503 show();
504 } else {
505 hide();
506 }
507}
508
509void KateCompletionWidget::updatePosition(bool force)
510{
511 if (!force && !isCompletionActive()) {
512 return;
513 }
514
515 if (!completionRange()) {
516 return;
517 }
518 const QPoint localCursorCoord = view()->cursorToCoordinate(cursor: completionRange()->start());
519 if (localCursorCoord == QPoint(-1, -1)) {
520 // Start of completion range is now off-screen -> abort
521 abortCompletion();
522 return;
523 }
524
525 const QPoint cursorCoordinate = view()->mapToGlobal(localCursorCoord);
526 QPoint p = cursorCoordinate;
527 int x = p.x();
528 int y = p.y();
529
530 y += view()->renderer()->currentFontMetrics().height() + 2;
531
532 const auto windowGeometry = parentWidget()->geometry();
533 if (x + width() > windowGeometry.right()) {
534 // crossing right edge
535 x = windowGeometry.right() - width();
536 }
537 if (x < windowGeometry.left()) {
538 x = windowGeometry.left();
539 }
540
541 if (y + height() > windowGeometry.bottom()) {
542 // move above cursor if we are crossing the bottom
543 y -= height();
544 if (y + height() > cursorCoordinate.y()) {
545 y -= (y + height()) - cursorCoordinate.y();
546 y -= 2;
547 }
548 }
549
550 move(parentWidget()->mapFromGlobal(QPoint(x, y)));
551}
552
553void KateCompletionWidget::updateArgumentHintGeometry()
554{
555 if (!m_dontShowArgumentHints) {
556 // Now place the argument-hint widget
557 m_argumentHintWidget->updateGeometry();
558 }
559}
560
561// Checks whether the given model has at least "rows" rows, also searching the second level of the tree.
562bool hasAtLeastNRows(int rows, QAbstractItemModel *model)
563{
564 int count = 0;
565 for (int row = 0; row < model->rowCount(); ++row) {
566 ++count;
567
568 QModelIndex index(model->index(row, column: 0));
569 if (index.isValid()) {
570 count += model->rowCount(parent: index);
571 }
572
573 if (count > rows) {
574 return true;
575 }
576 }
577 return false;
578}
579
580void KateCompletionWidget::updateHeight()
581{
582 QRect geom = geometry();
583
584 constexpr int minBaseHeight = 10;
585 constexpr int maxBaseHeight = 300;
586
587 int baseHeight = 0;
588 int calculatedCustomHeight = 0;
589
590 if (hasAtLeastNRows(rows: 15, model: m_presentationModel)) {
591 // If we know there is enough rows, always use max-height, we don't need to calculate size-hints
592 baseHeight = maxBaseHeight;
593 } else {
594 // Calculate size-hints to determine the best height
595 for (int row = 0; row < m_presentationModel->rowCount(); ++row) {
596 baseHeight += treeView()->sizeHintForRow(row);
597
598 QModelIndex index(m_presentationModel->index(row, column: 0));
599 if (index.isValid()) {
600 for (int row2 = 0; row2 < m_presentationModel->rowCount(parent: index); ++row2) {
601 int h = 0;
602 for (int a = 0; a < m_presentationModel->columnCount(parent: index); ++a) {
603 const QModelIndex child = m_presentationModel->index(row: row2, column: a, parent: index);
604 int localHeight = treeView()->sizeHintForIndex(index: child).height();
605 if (localHeight > h) {
606 h = localHeight;
607 }
608 }
609 baseHeight += h;
610 if (baseHeight > maxBaseHeight) {
611 break;
612 }
613 }
614
615 if (baseHeight > maxBaseHeight) {
616 break;
617 }
618 }
619 }
620
621 calculatedCustomHeight = baseHeight;
622 }
623
624 baseHeight += 2 * frameWidth();
625
626 if (m_entryList->horizontalScrollBar()->isVisible()) {
627 baseHeight += m_entryList->horizontalScrollBar()->height();
628 }
629
630 if (baseHeight < minBaseHeight) {
631 baseHeight = minBaseHeight;
632 }
633 if (baseHeight > maxBaseHeight) {
634 baseHeight = maxBaseHeight;
635 m_entryList->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
636 } else {
637 // Somewhere there seems to be a bug that makes QTreeView add a scroll-bar
638 // even if the content exactly fits in. So forcefully disable the scroll-bar in that case
639 m_entryList->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
640 }
641
642 int newExpandingAddedHeight = 0;
643
644 if (baseHeight == maxBaseHeight) {
645 // Eventually add some more height
646 if (calculatedCustomHeight && calculatedCustomHeight > baseHeight && calculatedCustomHeight < maxBaseHeight) {
647 newExpandingAddedHeight = calculatedCustomHeight - baseHeight;
648 }
649 }
650
651 if (m_expandedAddedHeightBase != baseHeight && m_expandedAddedHeightBase - baseHeight > -2 && m_expandedAddedHeightBase - baseHeight < 2) {
652 // Re-use the stored base-height if it only slightly differs from the current one.
653 // Reason: Qt seems to apply slightly wrong sizes when the completion-widget is moved out of the screen at the bottom,
654 // which completely breaks this algorithm. Solution: re-use the old base-size if it only slightly differs from the computed one.
655 baseHeight = m_expandedAddedHeightBase;
656 }
657
658 int finalHeight = baseHeight + newExpandingAddedHeight;
659
660 if (finalHeight < 10) {
661 m_entryList->resize(w: m_entryList->width(), h: height() - 2 * frameWidth());
662 return;
663 }
664
665 m_expandedAddedHeightBase = geometry().height();
666
667 geom.setHeight(finalHeight);
668
669 // Work around a crash deep within the Qt 4.5 raster engine
670 m_entryList->setScrollingEnabled(false);
671
672 if (geometry() != geom) {
673 setGeometry(geom);
674 }
675
676 QSize entryListSize = QSize(m_entryList->width(), finalHeight - 2 * frameWidth());
677 if (m_entryList->size() != entryListSize) {
678 m_entryList->resize(entryListSize);
679 }
680
681 m_entryList->setScrollingEnabled(true);
682}
683
684void KateCompletionWidget::cursorPositionChanged()
685{
686 ////qCDebug(LOG_KTE);
687 if (m_completionRanges.isEmpty()) {
688 return;
689 }
690
691 QModelIndex oldCurrentSourceIndex;
692 if (m_entryList->currentIndex().isValid()) {
693 oldCurrentSourceIndex = m_presentationModel->mapToSource(proxyIndex: m_entryList->currentIndex());
694 }
695
696 QMap<KTextEditor::CodeCompletionModel *, QString> filterStringByModel;
697
698 disconnect(sender: this->model(), signal: &KateCompletionModel::layoutChanged, receiver: this, slot: &KateCompletionWidget::modelContentChanged);
699 disconnect(sender: this->model(), signal: &KateCompletionModel::modelReset, receiver: this, slot: &KateCompletionWidget::modelContentChanged);
700
701 // Check the models and eventually abort some
702 const QList<KTextEditor::CodeCompletionModel *> checkCompletionRanges = m_completionRanges.keys();
703 for (auto model : checkCompletionRanges) {
704 if (!m_completionRanges.contains(key: model)) {
705 continue;
706 }
707
708 // qCDebug(LOG_KTE)<<"range before _updateRange:"<< *range;
709
710 // this might invalidate the range, therefore re-check afterwards
711 KTextEditor::Range rangeTE = m_completionRanges[model].range->toRange();
712 KTextEditor::Range newRange = _updateRange(model, view: view(), range&: rangeTE);
713 if (!m_completionRanges.contains(key: model)) {
714 continue;
715 }
716
717 // update value
718 m_completionRanges[model].range->setRange(newRange);
719
720 // qCDebug(LOG_KTE)<<"range after _updateRange:"<< *range;
721 QString currentCompletion = _filterString(model, view: view(), range: *m_completionRanges[model].range, cursor: view()->cursorPosition());
722 if (!m_completionRanges.contains(key: model)) {
723 continue;
724 }
725
726 // qCDebug(LOG_KTE)<<"after _filterString, currentCompletion="<< currentCompletion;
727 bool abort = _shouldAbortCompletion(model, view: view(), range: *m_completionRanges[model].range, currentCompletion);
728 if (!m_completionRanges.contains(key: model)) {
729 continue;
730 }
731
732 // qCDebug(LOG_KTE)<<"after _shouldAbortCompletion:abort="<<abort;
733 if (view()->cursorPosition() < m_completionRanges[model].leftBoundary) {
734 // qCDebug(LOG_KTE) << "aborting because of boundary:
735 // cursor:"<<view()->cursorPosition()<<"completion_Range_left_boundary:"<<m_completionRanges[*it].leftBoundary;
736 abort = true;
737 }
738
739 if (!m_completionRanges.contains(key: model)) {
740 continue;
741 }
742
743 if (abort) {
744 if (m_completionRanges.count() == 1) {
745 // last model - abort whole completion
746 abortCompletion();
747 return;
748 } else {
749 {
750 delete m_completionRanges[model].range;
751 // qCDebug(LOG_KTE)<<"removing completion range 3";
752 m_completionRanges.remove(key: model);
753 }
754
755 _aborted(model, view: view());
756 m_presentationModel->removeCompletionModel(model);
757 }
758 } else {
759 filterStringByModel[model] = currentCompletion;
760 }
761 }
762
763 connect(sender: this->model(), signal: &KateCompletionModel::layoutChanged, context: this, slot: &KateCompletionWidget::modelContentChanged);
764 connect(sender: this->model(), signal: &KateCompletionModel::modelReset, context: this, slot: &KateCompletionWidget::modelContentChanged);
765
766 m_presentationModel->setCurrentCompletion(filterStringByModel);
767
768 if (oldCurrentSourceIndex.isValid()) {
769 QModelIndex idx = m_presentationModel->mapFromSource(sourceIndex: oldCurrentSourceIndex);
770 // We only want to reselect this if it is still the first item
771 if (idx.isValid() && idx.row() == 0) {
772 // qCDebug(LOG_KTE) << "setting" << idx;
773 m_entryList->setCurrentIndex(idx.sibling(arow: idx.row(), acolumn: 0));
774 // m_entryList->nextCompletion();
775 // m_entryList->previousCompletion();
776 } else {
777 // qCDebug(LOG_KTE) << "failed to map from source";
778 }
779 }
780
781 m_entryList->scheduleUpdate();
782}
783
784bool KateCompletionWidget::isCompletionActive() const
785{
786 return !m_completionRanges.isEmpty() && ((!isHidden() && isVisible()) || (!m_argumentHintWidget->isHidden() && m_argumentHintWidget->isVisible()));
787}
788
789void KateCompletionWidget::abortCompletion()
790{
791 // qCDebug(LOG_KTE) ;
792
793 m_isSuspended = false;
794
795 if (!docTip()->isHidden()) {
796 docTip()->hide();
797 }
798
799 bool wasActive = isCompletionActive();
800
801 clear();
802
803 if (!isHidden()) {
804 hide();
805 }
806
807 if (!m_argumentHintWidget->isHidden()) {
808 m_argumentHintWidget->hide();
809 }
810
811 if (wasActive) {
812 view()->sendCompletionAborted();
813 }
814}
815
816void KateCompletionWidget::clear()
817{
818 m_presentationModel->clearCompletionModels();
819 m_argumentHintModel->clear();
820 m_docTip->clearWidgets();
821
822 const auto keys = m_completionRanges.keys();
823 for (KTextEditor::CodeCompletionModel *model : keys) {
824 _aborted(model, view: view());
825 }
826
827 deleteCompletionRanges();
828}
829
830bool KateCompletionWidget::navigateAccept()
831{
832 m_hadCompletionNavigation = true;
833
834 if (currentEmbeddedWidget()) {
835 QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetAccept");
836 }
837
838 QModelIndex index = selectedIndex();
839 if (index.isValid()) {
840 index.data(arole: KTextEditor::CodeCompletionModel::AccessibilityAccept);
841 return true;
842 }
843 return false;
844}
845
846bool KateCompletionWidget::execute()
847{
848 // qCDebug(LOG_KTE) ;
849
850 if (!isCompletionActive()) {
851 return false;
852 }
853
854 QModelIndex index = selectedIndex();
855
856 if (!index.isValid()) {
857 abortCompletion();
858 return false;
859 }
860
861 QModelIndex toExecute;
862
863 if (index.model() == m_presentationModel) {
864 toExecute = m_presentationModel->mapToSource(proxyIndex: index);
865 } else {
866 toExecute = m_argumentHintModel->mapToSource(proxyIndex: index);
867 }
868
869 if (!toExecute.isValid()) {
870 qCWarning(LOG_KTE) << "Could not map index" << m_entryList->selectionModel()->currentIndex() << "to source index.";
871 abortCompletion();
872 return false;
873 }
874
875 // encapsulate all editing as being from the code completion, and undo-able in one step.
876 view()->doc()->editStart();
877 m_completionEditRunning = true;
878
879 // create scoped pointer, to ensure deletion of cursor
880 std::unique_ptr<KTextEditor::MovingCursor> oldPos(view()->doc()->newMovingCursor(position: view()->cursorPosition(), insertBehavior: KTextEditor::MovingCursor::StayOnInsert));
881
882 KTextEditor::CodeCompletionModel *model = static_cast<KTextEditor::CodeCompletionModel *>(const_cast<QAbstractItemModel *>(toExecute.model()));
883 Q_ASSERT(model);
884
885 Q_ASSERT(m_completionRanges.contains(model));
886
887 KTextEditor::Cursor start = m_completionRanges[model].range->start();
888
889 // Save the "tail"
890 QString tailStr = tailString();
891 std::unique_ptr<KTextEditor::MovingCursor> afterTailMCursor(view()->doc()->newMovingCursor(position: view()->cursorPosition()));
892 afterTailMCursor->move(chars: tailStr.size());
893
894 // Handle completion for multi cursors
895 std::shared_ptr<QMetaObject::Connection> connection(new QMetaObject::Connection());
896 auto autoCompleteMulticursors = [connection, this](KTextEditor::Document *document, const KTextEditor::Range &range) {
897 disconnect(*connection);
898 const QString text = document->text(range);
899 if (text.isEmpty()) {
900 return;
901 }
902 const auto &multicursors = view()->secondaryCursors();
903 for (const auto &c : multicursors) {
904 const KTextEditor::Cursor pos = c.cursor();
905 KTextEditor::Range wordToReplace = view()->doc()->wordRangeAt(cursor: pos);
906 wordToReplace.setEnd(pos); // limit the word to the current cursor position
907 view()->doc()->replaceText(range: wordToReplace, s: text);
908 }
909 };
910 *connection = connect(sender: view()->doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot&: autoCompleteMulticursors);
911
912 model->executeCompletionItem(view: view(), word: *m_completionRanges[model].range, index: toExecute);
913 // NOTE the CompletionRange is now removed from m_completionRanges
914
915 // There are situations where keeping the tail is beneficial, but with the "Remove tail on complete" option is enabled,
916 // the tail is removed. For these situations we convert the completion into two edits:
917 // 1) Insert the completion
918 // 2) Remove the tail
919 //
920 // When we encounter one of these situations we can just do _one_ undo to have the tail back.
921 //
922 // Technically the tail is already removed by "executeCompletionItem()", so before this call we save the possible tail
923 // and re-add the tail before we end the first grouped "edit". Then immediately after that we add a second edit that
924 // removes the tail again.
925 // NOTE: The ViInputMode makes assumptions about the edit actions in a completion and breaks if we insert extra
926 // edits here, so we just disable this feature for ViInputMode
927 if (!tailStr.isEmpty() && view()->viewInputMode() != KTextEditor::View::ViInputMode) {
928 KTextEditor::Cursor currentPos = view()->cursorPosition();
929 KTextEditor::Cursor afterPos = afterTailMCursor->toCursor();
930 // Re add the tail for a possible undo to bring the tail back
931 view()->document()->insertText(position: afterPos, text: tailStr);
932 view()->setCursorPosition(currentPos);
933 view()->doc()->editEnd();
934
935 // Now remove the tail in a separate edit
936 KTextEditor::Cursor endPos = afterPos;
937 endPos.setColumn(afterPos.column() + tailStr.size());
938 view()->doc()->editStart();
939 view()->document()->removeText(range: KTextEditor::Range(afterPos, endPos));
940 }
941
942 view()->doc()->editEnd();
943 m_completionEditRunning = false;
944
945 abortCompletion();
946
947 view()->sendCompletionExecuted(position: start, model, index: toExecute);
948
949 KTextEditor::Cursor newPos = view()->cursorPosition();
950
951 if (newPos > *oldPos) {
952 m_automaticInvocationAt = newPos;
953 m_automaticInvocationLine = view()->doc()->text(range: KTextEditor::Range(*oldPos, newPos));
954 // qCDebug(LOG_KTE) << "executed, starting automatic invocation with line" << m_automaticInvocationLine;
955 m_lastInsertionByUser = false;
956 m_automaticInvocationTimer->start();
957 }
958
959 return true;
960}
961
962void KateCompletionWidget::resizeEvent(QResizeEvent *event)
963{
964 QFrame::resizeEvent(event);
965
966 // keep argument hint geometry in sync
967 if (m_argumentHintWidget->isVisible()) {
968 updateArgumentHintGeometry();
969 }
970}
971
972void KateCompletionWidget::moveEvent(QMoveEvent *event)
973{
974 QFrame::moveEvent(event);
975
976 // keep argument hint geometry in sync
977 if (m_argumentHintWidget->isVisible()) {
978 updateArgumentHintGeometry();
979 }
980}
981
982void KateCompletionWidget::showEvent(QShowEvent *event)
983{
984 m_isSuspended = false;
985
986 QFrame::showEvent(event);
987
988 if (!m_dontShowArgumentHints && m_argumentHintModel->rowCount(parent: QModelIndex()) != 0) {
989 m_argumentHintWidget->positionAndShow();
990 }
991}
992
993KTextEditor::MovingRange *KateCompletionWidget::completionRange(KTextEditor::CodeCompletionModel *model) const
994{
995 if (!model) {
996 if (m_completionRanges.isEmpty()) {
997 return nullptr;
998 }
999
1000 KTextEditor::MovingRange *ret = m_completionRanges.begin()->range;
1001
1002 for (const CompletionRange &range : m_completionRanges) {
1003 if (range.range->start() > ret->start()) {
1004 ret = range.range;
1005 }
1006 }
1007 return ret;
1008 }
1009 if (m_completionRanges.contains(key: model)) {
1010 return m_completionRanges[model].range;
1011 } else {
1012 return nullptr;
1013 }
1014}
1015
1016QMap<KTextEditor::CodeCompletionModel *, KateCompletionWidget::CompletionRange> KateCompletionWidget::completionRanges() const
1017{
1018 return m_completionRanges;
1019}
1020
1021void KateCompletionWidget::modelReset()
1022{
1023 setUpdatesEnabled(false);
1024 m_entryList->setAnimated(false);
1025
1026 for (int row = 0; row < m_entryList->model()->rowCount(parent: QModelIndex()); ++row) {
1027 QModelIndex index(m_entryList->model()->index(row, column: 0, parent: QModelIndex()));
1028 if (!m_entryList->isExpanded(index)) {
1029 m_entryList->expand(index);
1030 }
1031 }
1032 setUpdatesEnabled(true);
1033}
1034
1035KateCompletionTree *KateCompletionWidget::treeView() const
1036{
1037 return m_entryList;
1038}
1039
1040QModelIndex KateCompletionWidget::selectedIndex() const
1041{
1042 if (!isCompletionActive()) {
1043 return QModelIndex();
1044 }
1045
1046 return m_entryList->currentIndex();
1047}
1048
1049bool KateCompletionWidget::navigateLeft()
1050{
1051 m_hadCompletionNavigation = true;
1052 if (currentEmbeddedWidget()) {
1053 QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetLeft");
1054 }
1055
1056 QModelIndex index = selectedIndex();
1057
1058 if (index.isValid()) {
1059 index.data(arole: KTextEditor::CodeCompletionModel::AccessibilityPrevious);
1060
1061 return true;
1062 }
1063 return false;
1064}
1065
1066bool KateCompletionWidget::navigateRight()
1067{
1068 m_hadCompletionNavigation = true;
1069 if (currentEmbeddedWidget()) { ///@todo post 4.2: Make these slots public interface, or create an interface using virtual functions
1070 QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetRight");
1071 }
1072
1073 QModelIndex index = selectedIndex();
1074
1075 if (index.isValid()) {
1076 index.data(arole: KTextEditor::CodeCompletionModel::AccessibilityNext);
1077 return true;
1078 }
1079
1080 return false;
1081}
1082
1083bool KateCompletionWidget::navigateBack()
1084{
1085 m_hadCompletionNavigation = true;
1086 if (currentEmbeddedWidget()) {
1087 QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetBack");
1088 }
1089 return false;
1090}
1091
1092void KateCompletionWidget::toggleDocumentation()
1093{
1094 // user has configured the doc to be always visible
1095 // whenever its available.
1096 if (view()->config()->showDocWithCompletion()) {
1097 return;
1098 }
1099
1100 if (m_docTip->isVisible()) {
1101 m_hadCompletionNavigation = false;
1102 QTimer::singleShot(interval: 400, receiver: this, slot: [this] {
1103 // if 400ms later this is not false, it means
1104 // that the user navigated inside the active
1105 // widget in doc tip
1106 if (!m_hadCompletionNavigation) {
1107 m_docTip->hide();
1108 }
1109 });
1110 } else {
1111 showDocTip(idx: m_entryList->currentIndex());
1112 }
1113}
1114
1115void KateCompletionWidget::showDocTip(const QModelIndex &idx)
1116{
1117 auto data = idx.data(arole: KTextEditor::CodeCompletionModel::ExpandingWidget);
1118 // No data => hide
1119 if (!data.isValid()) {
1120 m_docTip->hide();
1121 return;
1122 } else if (data.canConvert<QWidget *>()) {
1123 m_docTip->setWidget(data.value<QWidget *>());
1124 } else if (data.canConvert<QString>()) {
1125 QString text = data.toString();
1126 if (text.isEmpty()) {
1127 m_docTip->hide();
1128 return;
1129 }
1130 m_docTip->setText(text);
1131 }
1132
1133 m_docTip->updatePosition(completionWidget: this);
1134 if (!m_docTip->isVisible()) {
1135 m_docTip->show();
1136 }
1137}
1138
1139bool KateCompletionWidget::eventFilter(QObject *watched, QEvent *event)
1140{
1141 if (watched != this && event->type() == QEvent::Resize && isCompletionActive()) {
1142 abortCompletion();
1143 } else if (event->type() == QEvent::KeyRelease && isCompletionActive()) {
1144 auto e = static_cast<QKeyEvent *>(event);
1145 if (e->key() == Qt::Key_Left && e->modifiers() == Qt::AltModifier) {
1146 if (navigateLeft()) {
1147 return true;
1148 }
1149 }
1150 if (e->key() == Qt::Key_Right && e->modifiers() == Qt::AltModifier) {
1151 if (navigateRight()) {
1152 return true;
1153 }
1154 }
1155 if (e->key() == Qt::Key_Up && e->modifiers() == Qt::AltModifier) {
1156 if (navigateUp()) {
1157 return true;
1158 }
1159 }
1160 if (e->key() == Qt::Key_Down && e->modifiers() == Qt::AltModifier) {
1161 if (navigateDown()) {
1162 return true;
1163 }
1164 }
1165 if (e->key() == Qt::Key_Return && e->modifiers() == Qt::AltModifier) {
1166 if (navigateAccept()) {
1167 return true;
1168 }
1169 }
1170 if (e->key() == Qt::Key_Backspace && e->modifiers() == Qt::AltModifier) {
1171 if (navigateBack()) {
1172 return true;
1173 }
1174 }
1175 }
1176 return QFrame::eventFilter(watched, event);
1177}
1178
1179bool KateCompletionWidget::navigateDown()
1180{
1181 m_hadCompletionNavigation = true;
1182 if (m_argumentHintModel->rowCount() > 0) {
1183 m_argumentHintWidget->selectNext();
1184 return true;
1185 } else if (currentEmbeddedWidget()) {
1186 QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetDown");
1187 }
1188 return false;
1189}
1190
1191bool KateCompletionWidget::navigateUp()
1192{
1193 m_hadCompletionNavigation = true;
1194 if (m_argumentHintModel->rowCount() > 0) {
1195 m_argumentHintWidget->selectPrevious();
1196 return true;
1197 } else if (currentEmbeddedWidget()) {
1198 QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetUp");
1199 }
1200 return false;
1201}
1202
1203QWidget *KateCompletionWidget::currentEmbeddedWidget()
1204{
1205 return m_docTip->currentWidget();
1206}
1207
1208void KateCompletionWidget::cursorDown()
1209{
1210 m_entryList->nextCompletion();
1211}
1212
1213void KateCompletionWidget::cursorUp()
1214{
1215 m_entryList->previousCompletion();
1216}
1217
1218void KateCompletionWidget::pageDown()
1219{
1220 m_entryList->pageDown();
1221}
1222
1223void KateCompletionWidget::pageUp()
1224{
1225 m_entryList->pageUp();
1226}
1227
1228void KateCompletionWidget::top()
1229{
1230 m_entryList->top();
1231}
1232
1233void KateCompletionWidget::bottom()
1234{
1235 m_entryList->bottom();
1236}
1237
1238void KateCompletionWidget::completionModelReset()
1239{
1240 KTextEditor::CodeCompletionModel *model = qobject_cast<KTextEditor::CodeCompletionModel *>(object: sender());
1241 if (!model) {
1242 qCWarning(LOG_KTE) << "bad sender";
1243 return;
1244 }
1245
1246 if (!m_waitingForReset.contains(value: model)) {
1247 return;
1248 }
1249
1250 m_waitingForReset.remove(value: model);
1251
1252 if (m_waitingForReset.isEmpty()) {
1253 if (!isCompletionActive()) {
1254 // qCDebug(LOG_KTE) << "all completion-models we waited for are ready. Last one: " << model->objectName();
1255 // Eventually show the completion-list if this was the last model we were waiting for
1256 // Use a queued connection once again to make sure that KateCompletionModel is notified before we are
1257 QMetaObject::invokeMethod(obj: this, member: "modelContentChanged", c: Qt::QueuedConnection);
1258 }
1259 }
1260}
1261
1262void KateCompletionWidget::modelDestroyed(QObject *model)
1263{
1264 m_sourceModels.removeAll(t: model);
1265 abortCompletion();
1266}
1267
1268void KateCompletionWidget::registerCompletionModel(KTextEditor::CodeCompletionModel *model)
1269{
1270 if (m_sourceModels.contains(t: model)) {
1271 return;
1272 }
1273
1274 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::destroyed, context: this, slot: &KateCompletionWidget::modelDestroyed);
1275 // This connection must not be queued
1276 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::modelReset, context: this, slot: &KateCompletionWidget::completionModelReset);
1277
1278 m_sourceModels.append(t: model);
1279
1280 if (isCompletionActive()) {
1281 m_presentationModel->addCompletionModel(model);
1282 }
1283}
1284
1285void KateCompletionWidget::unregisterCompletionModel(KTextEditor::CodeCompletionModel *model)
1286{
1287 disconnect(sender: model, signal: &KTextEditor::CodeCompletionModel::destroyed, receiver: this, slot: &KateCompletionWidget::modelDestroyed);
1288 disconnect(sender: model, signal: &KTextEditor::CodeCompletionModel::modelReset, receiver: this, slot: &KateCompletionWidget::completionModelReset);
1289
1290 m_sourceModels.removeAll(t: model);
1291 abortCompletion();
1292}
1293
1294bool KateCompletionWidget::isCompletionModelRegistered(KTextEditor::CodeCompletionModel *model) const
1295{
1296 return m_sourceModels.contains(t: model);
1297}
1298
1299QList<KTextEditor::CodeCompletionModel *> KateCompletionWidget::codeCompletionModels() const
1300{
1301 return m_sourceModels;
1302}
1303
1304int KateCompletionWidget::automaticInvocationDelay() const
1305{
1306 return m_automaticInvocationDelay;
1307}
1308
1309void KateCompletionWidget::setIgnoreBufferSignals(bool ignore) const
1310{
1311 if (ignore) {
1312 disconnect(sender: view()->doc(), signal: &KTextEditor::Document::lineWrapped, receiver: this, slot: &KateCompletionWidget::wrapLine);
1313 disconnect(sender: view()->doc(), signal: &KTextEditor::Document::lineUnwrapped, receiver: this, slot: &KateCompletionWidget::unwrapLine);
1314 disconnect(sender: view()->doc(), signal: &KTextEditor::Document::textInserted, receiver: this, slot: &KateCompletionWidget::insertText);
1315 disconnect(sender: view()->doc(), signal: &KTextEditor::Document::textRemoved, receiver: this, slot: &KateCompletionWidget::removeText);
1316 } else {
1317 connect(sender: view()->doc(), signal: &KTextEditor::Document::lineWrapped, context: this, slot: &KateCompletionWidget::wrapLine);
1318 connect(sender: view()->doc(), signal: &KTextEditor::Document::lineUnwrapped, context: this, slot: &KateCompletionWidget::unwrapLine);
1319 connect(sender: view()->doc(), signal: &KTextEditor::Document::textInserted, context: this, slot: &KateCompletionWidget::insertText);
1320 connect(sender: view()->doc(), signal: &KTextEditor::Document::textRemoved, context: this, slot: &KateCompletionWidget::removeText);
1321 }
1322}
1323
1324void KateCompletionWidget::setAutomaticInvocationDelay(int delay)
1325{
1326 m_automaticInvocationDelay = delay;
1327}
1328
1329void KateCompletionWidget::wrapLine(KTextEditor::Document *, KTextEditor::Cursor)
1330{
1331 m_lastInsertionByUser = !m_completionEditRunning;
1332
1333 // wrap line, be done
1334 m_automaticInvocationLine.clear();
1335 m_automaticInvocationTimer->stop();
1336}
1337
1338void KateCompletionWidget::unwrapLine(KTextEditor::Document *, int)
1339{
1340 m_lastInsertionByUser = !m_completionEditRunning;
1341
1342 // just removal
1343 m_automaticInvocationLine.clear();
1344 m_automaticInvocationTimer->stop();
1345}
1346
1347void KateCompletionWidget::insertText(KTextEditor::Document *, KTextEditor::Cursor position, const QString &text)
1348{
1349 m_lastInsertionByUser = !m_completionEditRunning;
1350
1351 // no invoke?
1352 if (!view()->isAutomaticInvocationEnabled()) {
1353 m_automaticInvocationLine.clear();
1354 m_automaticInvocationTimer->stop();
1355 return;
1356 }
1357
1358 if (m_automaticInvocationAt != position) {
1359 m_automaticInvocationLine.clear();
1360 m_lastInsertionByUser = !m_completionEditRunning;
1361 }
1362
1363 m_automaticInvocationLine += text;
1364 m_automaticInvocationAt = position;
1365 m_automaticInvocationAt.setColumn(position.column() + text.length());
1366
1367 if (m_automaticInvocationLine.isEmpty()) {
1368 m_automaticInvocationTimer->stop();
1369 return;
1370 }
1371
1372 m_automaticInvocationTimer->start(msec: m_automaticInvocationDelay);
1373}
1374
1375void KateCompletionWidget::removeText(KTextEditor::Document *, KTextEditor::Range, const QString &)
1376{
1377 m_lastInsertionByUser = !m_completionEditRunning;
1378
1379 // just removal
1380 m_automaticInvocationLine.clear();
1381 m_automaticInvocationTimer->stop();
1382}
1383
1384void KateCompletionWidget::automaticInvocation()
1385{
1386 // qCDebug(LOG_KTE)<<"m_automaticInvocationAt:"<<m_automaticInvocationAt;
1387 // qCDebug(LOG_KTE)<<view()->cursorPosition();
1388 if (m_automaticInvocationAt != view()->cursorPosition()) {
1389 return;
1390 }
1391
1392 bool start = false;
1393 QList<KTextEditor::CodeCompletionModel *> models;
1394
1395 // qCDebug(LOG_KTE)<<"checking models";
1396 for (KTextEditor::CodeCompletionModel *model : std::as_const(t&: m_sourceModels)) {
1397 // qCDebug(LOG_KTE)<<"m_completionRanges contains model?:"<<m_completionRanges.contains(model);
1398 if (m_completionRanges.contains(key: model)) {
1399 continue;
1400 }
1401
1402 start = _shouldStartCompletion(model, view: view(), automaticInvocationLine: m_automaticInvocationLine, m_lastInsertionByUser, cursor: view()->cursorPosition());
1403 // qCDebug(LOG_KTE)<<"start="<<start;
1404 if (start) {
1405 models << model;
1406 }
1407 }
1408 // qCDebug(LOG_KTE)<<"models found:"<<!models.isEmpty();
1409 if (!models.isEmpty()) {
1410 // Start automatic code completion
1411 startCompletion(invocationType: KTextEditor::CodeCompletionModel::AutomaticInvocation, models);
1412 }
1413}
1414
1415void KateCompletionWidget::userInvokedCompletion()
1416{
1417 startCompletion(invocationType: KTextEditor::CodeCompletionModel::UserInvocation);
1418}
1419
1420void KateCompletionWidget::tabCompletion(Direction direction)
1421{
1422 m_noAutoHide = true;
1423
1424 // Not using cursorDown/Up() as we don't want to go into the argument-hint list
1425 if (direction == Down) {
1426 const bool res = m_entryList->nextCompletion();
1427 if (!res) {
1428 m_entryList->top();
1429 }
1430 } else { // direction == Up
1431 const bool res = m_entryList->previousCompletion();
1432 if (!res) {
1433 m_entryList->bottom();
1434 }
1435 }
1436}
1437
1438#include "moc_katecompletionwidget.cpp"
1439

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