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 | |
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 | |
171 | KateCompletionWidget::~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 | |
180 | void 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 | |
191 | void KateCompletionWidget::focusOutEvent(QFocusEvent *) |
192 | { |
193 | abortCompletion(); |
194 | } |
195 | |
196 | void 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 | |
259 | KateArgumentHintModel *KateCompletionWidget::argumentHintModel() const |
260 | { |
261 | return m_argumentHintModel; |
262 | } |
263 | |
264 | const KateCompletionModel *KateCompletionWidget::model() const |
265 | { |
266 | return m_presentationModel; |
267 | } |
268 | |
269 | KateCompletionModel *KateCompletionWidget::model() |
270 | { |
271 | return m_presentationModel; |
272 | } |
273 | |
274 | void 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 | |
285 | KTextEditor::ViewPrivate *KateCompletionWidget::view() const |
286 | { |
287 | return m_view; |
288 | } |
289 | |
290 | void 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 | |
301 | void 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 | |
310 | void 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 | |
318 | void 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 | |
331 | void 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 | |
438 | QString 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 | |
459 | void 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 | |
469 | void 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 | |
511 | void 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 | |
558 | void 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. |
567 | static 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 | |
586 | void 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 | |
690 | void 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 | |
790 | bool KateCompletionWidget::isCompletionActive() const |
791 | { |
792 | return !m_completionRanges.isEmpty() && ((!isHidden() && isVisible()) || (!m_argumentHintWidget->isHidden() && m_argumentHintWidget->isVisible())); |
793 | } |
794 | |
795 | void 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 | |
822 | void 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 | |
836 | bool 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 | |
852 | bool 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 | |
977 | void 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 | |
987 | void 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 | |
997 | void 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 | |
1009 | KTextEditor::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 | |
1032 | QMap<KTextEditor::CodeCompletionModel *, KateCompletionWidget::CompletionRange> KateCompletionWidget::completionRanges() const |
1033 | { |
1034 | return m_completionRanges; |
1035 | } |
1036 | |
1037 | void 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 | |
1051 | KateCompletionTree *KateCompletionWidget::treeView() const |
1052 | { |
1053 | return m_entryList; |
1054 | } |
1055 | |
1056 | QModelIndex KateCompletionWidget::selectedIndex() const |
1057 | { |
1058 | if (!isCompletionActive()) { |
1059 | return QModelIndex(); |
1060 | } |
1061 | |
1062 | return m_entryList->currentIndex(); |
1063 | } |
1064 | |
1065 | bool 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 | |
1082 | bool 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 | |
1099 | bool KateCompletionWidget::navigateBack() |
1100 | { |
1101 | m_hadCompletionNavigation = true; |
1102 | if (currentEmbeddedWidget()) { |
1103 | QMetaObject::invokeMethod(obj: currentEmbeddedWidget(), member: "embeddedWidgetBack" ); |
1104 | } |
1105 | return false; |
1106 | } |
1107 | |
1108 | void 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 | |
1131 | void 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 | |
1155 | bool 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 | |
1177 | bool 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 | |
1185 | bool 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 | |
1197 | bool 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 | |
1209 | QWidget *KateCompletionWidget::currentEmbeddedWidget() |
1210 | { |
1211 | return m_docTip->currentWidget(); |
1212 | } |
1213 | |
1214 | void KateCompletionWidget::cursorDown() |
1215 | { |
1216 | m_entryList->nextCompletion(); |
1217 | } |
1218 | |
1219 | void KateCompletionWidget::cursorUp() |
1220 | { |
1221 | m_entryList->previousCompletion(); |
1222 | } |
1223 | |
1224 | void KateCompletionWidget::pageDown() |
1225 | { |
1226 | m_entryList->pageDown(); |
1227 | } |
1228 | |
1229 | void KateCompletionWidget::pageUp() |
1230 | { |
1231 | m_entryList->pageUp(); |
1232 | } |
1233 | |
1234 | void KateCompletionWidget::top() |
1235 | { |
1236 | m_entryList->top(); |
1237 | } |
1238 | |
1239 | void KateCompletionWidget::bottom() |
1240 | { |
1241 | m_entryList->bottom(); |
1242 | } |
1243 | |
1244 | void 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 | |
1268 | void KateCompletionWidget::modelDestroyed(QObject *model) |
1269 | { |
1270 | m_sourceModels.removeAll(t: model); |
1271 | abortCompletion(); |
1272 | } |
1273 | |
1274 | void 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 | |
1291 | void 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 | |
1300 | bool KateCompletionWidget::isCompletionModelRegistered(KTextEditor::CodeCompletionModel *model) const |
1301 | { |
1302 | return m_sourceModels.contains(t: model); |
1303 | } |
1304 | |
1305 | QList<KTextEditor::CodeCompletionModel *> KateCompletionWidget::codeCompletionModels() const |
1306 | { |
1307 | return m_sourceModels; |
1308 | } |
1309 | |
1310 | int KateCompletionWidget::automaticInvocationDelay() const |
1311 | { |
1312 | return m_automaticInvocationDelay; |
1313 | } |
1314 | |
1315 | void 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 | |
1330 | void KateCompletionWidget::setAutomaticInvocationDelay(int delay) |
1331 | { |
1332 | m_automaticInvocationDelay = delay; |
1333 | } |
1334 | |
1335 | void 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 | |
1344 | void 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 | |
1353 | void 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 | |
1381 | void 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 | |
1390 | void 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 | |
1421 | void KateCompletionWidget::userInvokedCompletion() |
1422 | { |
1423 | startCompletion(invocationType: KTextEditor::CodeCompletionModel::UserInvocation); |
1424 | } |
1425 | |
1426 | void 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 | |
1444 | void 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 | |