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

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