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 | |
38 | const 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 | |
50 | static KTextEditor::Range _completionRange(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, KTextEditor::Cursor cursor) |
51 | { |
52 | CALLCI(return, , return, model, completionRange(view, cursor)); |
53 | } |
54 | |
55 | static KTextEditor::Range _updateRange(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, KTextEditor::Range &range) |
56 | { |
57 | CALLCI(, return range, return, model, updateCompletionRange(view, range)); |
58 | } |
59 | |
60 | static 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 | |
65 | static bool |
66 | _shouldAbortCompletion(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view, const KTextEditor::Range &range, const QString ¤tCompletion) |
67 | { |
68 | CALLCI(return, , return, model, shouldAbortCompletion(view, range, currentCompletion)); |
69 | } |
70 | |
71 | static void _aborted(KTextEditor::CodeCompletionModel *model, KTextEditor::View *view) |
72 | { |
73 | CALLCI(return, , return, model, aborted(view)); |
74 | } |
75 | |
76 | static 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 | |
85 | KateCompletionWidget::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 | |
169 | KateCompletionWidget::~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 | |
178 | void 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 | |
189 | void KateCompletionWidget::focusOutEvent(QFocusEvent *) |
190 | { |
191 | abortCompletion(); |
192 | } |
193 | |
194 | void 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 | |
257 | KateArgumentHintModel *KateCompletionWidget::argumentHintModel() const |
258 | { |
259 | return m_argumentHintModel; |
260 | } |
261 | |
262 | const KateCompletionModel *KateCompletionWidget::model() const |
263 | { |
264 | return m_presentationModel; |
265 | } |
266 | |
267 | KateCompletionModel *KateCompletionWidget::model() |
268 | { |
269 | return m_presentationModel; |
270 | } |
271 | |
272 | void 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 | |
283 | KTextEditor::ViewPrivate *KateCompletionWidget::view() const |
284 | { |
285 | return m_view; |
286 | } |
287 | |
288 | void 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 | |
299 | void 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 | |
308 | void 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 | |
316 | void 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 | |
329 | void 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 | |
436 | QString 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 | |
457 | void 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 | |
467 | void 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 | |
509 | void 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 | |
553 | void 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. |
562 | bool 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 | |
580 | void 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 | |
684 | void 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 | |
784 | bool KateCompletionWidget::isCompletionActive() const |
785 | { |
786 | return !m_completionRanges.isEmpty() && ((!isHidden() && isVisible()) || (!m_argumentHintWidget->isHidden() && m_argumentHintWidget->isVisible())); |
787 | } |
788 | |
789 | void 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 | |
816 | void 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 | |
830 | bool 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 | |
846 | bool 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 | |
962 | void 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 | |
972 | void 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 | |
982 | void 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 | |
993 | KTextEditor::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 | |
1016 | QMap<KTextEditor::CodeCompletionModel *, KateCompletionWidget::CompletionRange> KateCompletionWidget::completionRanges() const |
1017 | { |
1018 | return m_completionRanges; |
1019 | } |
1020 | |
1021 | void 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 | |
1035 | KateCompletionTree *KateCompletionWidget::treeView() const |
1036 | { |
1037 | return m_entryList; |
1038 | } |
1039 | |
1040 | QModelIndex KateCompletionWidget::selectedIndex() const |
1041 | { |
1042 | if (!isCompletionActive()) { |
1043 | return QModelIndex(); |
1044 | } |
1045 | |
1046 | return m_entryList->currentIndex(); |
1047 | } |
1048 | |
1049 | bool 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 | |
1066 | bool 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 | |
1083 | bool KateCompletionWidget::navigateBack() |
1084 | { |
1085 | m_hadCompletionNavigation = true; |
1086 | if (currentEmbeddedWidget()) { |
1087 | QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetBack" ); |
1088 | } |
1089 | return false; |
1090 | } |
1091 | |
1092 | void 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 | |
1115 | void 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 | |
1139 | bool 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 | |
1179 | bool 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 | |
1191 | bool 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 | |
1203 | QWidget *KateCompletionWidget::currentEmbeddedWidget() |
1204 | { |
1205 | return m_docTip->currentWidget(); |
1206 | } |
1207 | |
1208 | void KateCompletionWidget::cursorDown() |
1209 | { |
1210 | m_entryList->nextCompletion(); |
1211 | } |
1212 | |
1213 | void KateCompletionWidget::cursorUp() |
1214 | { |
1215 | m_entryList->previousCompletion(); |
1216 | } |
1217 | |
1218 | void KateCompletionWidget::pageDown() |
1219 | { |
1220 | m_entryList->pageDown(); |
1221 | } |
1222 | |
1223 | void KateCompletionWidget::pageUp() |
1224 | { |
1225 | m_entryList->pageUp(); |
1226 | } |
1227 | |
1228 | void KateCompletionWidget::top() |
1229 | { |
1230 | m_entryList->top(); |
1231 | } |
1232 | |
1233 | void KateCompletionWidget::bottom() |
1234 | { |
1235 | m_entryList->bottom(); |
1236 | } |
1237 | |
1238 | void 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 | |
1262 | void KateCompletionWidget::modelDestroyed(QObject *model) |
1263 | { |
1264 | m_sourceModels.removeAll(t: model); |
1265 | abortCompletion(); |
1266 | } |
1267 | |
1268 | void 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 | |
1285 | void 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 | |
1294 | bool KateCompletionWidget::isCompletionModelRegistered(KTextEditor::CodeCompletionModel *model) const |
1295 | { |
1296 | return m_sourceModels.contains(t: model); |
1297 | } |
1298 | |
1299 | QList<KTextEditor::CodeCompletionModel *> KateCompletionWidget::codeCompletionModels() const |
1300 | { |
1301 | return m_sourceModels; |
1302 | } |
1303 | |
1304 | int KateCompletionWidget::automaticInvocationDelay() const |
1305 | { |
1306 | return m_automaticInvocationDelay; |
1307 | } |
1308 | |
1309 | void 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 | |
1324 | void KateCompletionWidget::setAutomaticInvocationDelay(int delay) |
1325 | { |
1326 | m_automaticInvocationDelay = delay; |
1327 | } |
1328 | |
1329 | void 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 | |
1338 | void 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 | |
1347 | void 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 | |
1375 | void 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 | |
1384 | void 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 | |
1415 | void KateCompletionWidget::userInvokedCompletion() |
1416 | { |
1417 | startCompletion(invocationType: KTextEditor::CodeCompletionModel::UserInvocation); |
1418 | } |
1419 | |
1420 | void 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 | |