KTextEditor

katecompletionmodel.cpp
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
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "katecompletionmodel.h"
9
10#include "kateargumenthintmodel.h"
11#include "katecompletiontree.h"
12#include "katecompletionwidget.h"
13#include "katepartdebug.h"
14#include "katerenderer.h"
15#include "kateview.h"
16#include <ktexteditor/codecompletionmodelcontrollerinterface.h>
17
18#include <KFuzzyMatcher>
19#include <KLocalizedString>
20
21#include <QApplication>
22#include <QMultiMap>
23#include <QTimer>
24#include <QVarLengthArray>
25
26using namespace KTextEditor;
27
28/// A helper-class for handling completion-models with hierarchical grouping/optimization
29class HierarchicalModelHandler
30{
31public:
32 explicit HierarchicalModelHandler(CodeCompletionModel *model);
33 void addValue(CodeCompletionModel::ExtraItemDataRoles role, const QVariant &value);
34 // Walks the index upwards and collects all defined completion-roles on the way
35 void collectRoles(const QModelIndex &index);
36 void takeRole(const QModelIndex &index);
37
38 CodeCompletionModel *model() const;
39
40 // Assumes that index is a sub-index of the indices where role-values were taken
41 QVariant getData(CodeCompletionModel::ExtraItemDataRoles role, const QModelIndex &index) const;
42
43 bool hasHierarchicalRoles() const;
44
45 int inheritanceDepth(const QModelIndex &i) const;
46
47 QString customGroup() const
48 {
49 return m_customGroup;
50 }
51
52 int customGroupingKey() const
53 {
54 return m_groupSortingKey;
55 }
56
57private:
58 typedef std::pair<CodeCompletionModel::ExtraItemDataRoles, QVariant> RoleAndValue;
59 typedef std::vector<std::pair<CodeCompletionModel::ExtraItemDataRoles, QVariant>> RoleMap;
60 RoleMap m_roleValues;
61 QString m_customGroup;
62 int m_groupSortingKey;
63 CodeCompletionModel *m_model;
64};
65
66CodeCompletionModel *HierarchicalModelHandler::model() const
67{
68 return m_model;
69}
70
71bool HierarchicalModelHandler::hasHierarchicalRoles() const
72{
73 return !m_roleValues.empty();
74}
75
76void HierarchicalModelHandler::collectRoles(const QModelIndex &index)
77{
78 if (index.parent().isValid()) {
79 collectRoles(index.parent());
80 }
81 if (m_model->rowCount(index) != 0) {
82 takeRole(index);
83 }
84}
85
86int HierarchicalModelHandler::inheritanceDepth(const QModelIndex &i) const
87{
89}
90
91void HierarchicalModelHandler::takeRole(const QModelIndex &index)
92{
94 if (v.isValid() && v.canConvert<int>()) {
95 QVariant value = index.data(v.toInt());
96 if (v.toInt() == Qt::DisplayRole) {
97 m_customGroup = index.data(Qt::DisplayRole).toString();
99 if (sortingKey.canConvert<int>()) {
100 m_groupSortingKey = sortingKey.toInt();
101 }
102 } else {
104 addValue(role, value);
105 }
106 } else {
107 qCDebug(LOG_KTE) << "Did not return valid GroupRole in hierarchical completion-model";
108 }
109}
110
111QVariant HierarchicalModelHandler::getData(CodeCompletionModel::ExtraItemDataRoles role, const QModelIndex &index) const
112{
113 auto it = std::find_if(m_roleValues.begin(), m_roleValues.end(), [role](const RoleAndValue &v) {
114 return v.first == role;
115 });
116 if (it != m_roleValues.end()) {
117 return it->second;
118 } else {
119 return index.data(role);
120 }
121}
122
123HierarchicalModelHandler::HierarchicalModelHandler(CodeCompletionModel *model)
124 : m_groupSortingKey(-1)
125 , m_model(model)
126{
127}
128
129void HierarchicalModelHandler::addValue(CodeCompletionModel::ExtraItemDataRoles role, const QVariant &value)
130{
131 auto it = std::find_if(m_roleValues.begin(), m_roleValues.end(), [role](const RoleAndValue &v) {
132 return v.first == role;
133 });
134 if (it != m_roleValues.end()) {
135 it->second = value;
136 } else {
137 m_roleValues.push_back({role, value});
138 }
139}
140
141KateCompletionModel::KateCompletionModel(KateCompletionWidget *parent)
142 : ExpandingWidgetModel(parent)
143 , m_ungrouped(new Group({}, 0, this))
144 , m_argumentHints(new Group(i18n("Argument-hints"), -1, this))
145 , m_bestMatches(new Group(i18n("Best matches"), BestMatchesProperty, this))
146 , m_emptyGroups({m_ungrouped, m_argumentHints, m_bestMatches})
147{
148 m_updateBestMatchesTimer = new QTimer(this);
149 m_updateBestMatchesTimer->setSingleShot(true);
150 connect(m_updateBestMatchesTimer, &QTimer::timeout, this, &KateCompletionModel::updateBestMatches);
151
152 m_groupHash.insert(0, m_ungrouped);
153 m_groupHash.insert(-1, m_argumentHints);
154 m_groupHash.insert(BestMatchesProperty, m_argumentHints);
155
156 createGroups();
157}
158
159KateCompletionModel::~KateCompletionModel()
160{
161 clearCompletionModels();
162 delete m_argumentHints;
163 delete m_ungrouped;
164 delete m_bestMatches;
165}
166
167QTreeView *KateCompletionModel::treeView() const
168{
169 return view()->completionWidget()->treeView();
170}
171
172QVariant KateCompletionModel::data(const QModelIndex &index, int role) const
173{
174 if (!hasCompletionModel() || !index.isValid()) {
175 return QVariant();
176 }
177
178 if (role == InternalRole::IsNonEmptyGroup) {
179 auto group = groupForIndex(index);
180 return group && !group->isEmpty;
181 }
182
183 // groupOfParent returns a group when the index is a member of that group, but not the group head/label.
184 if (!hasGroups() || groupOfParent(index)) {
185 if (role == Qt::TextAlignmentRole) {
186 int c = 0;
187 for (const auto &list : m_columnMerges) {
188 if (size_t(index.column()) < c + list.size()) {
189 c += list.size();
190 continue;
191 } else if (list.size() == 1 && list.front() == CodeCompletionModel::Scope) {
192 return Qt::AlignRight;
193 } else {
194 return QVariant();
195 }
196 }
197 }
198
199 // Merge text for column merging
200 if (role == Qt::DisplayRole) {
201 QString text;
202 for (int column : m_columnMerges[index.column()]) {
203 QModelIndex sourceIndex = mapToSource(createIndex(index.row(), column, index.internalPointer()));
204 text.append(sourceIndex.data(role).toString());
205 }
206
207 return text;
208 }
209
211 // Return that we are doing custom-highlighting of one of the sub-strings does it. Unfortunately internal highlighting does not work for the other
212 // substrings.
213 for (int column : m_columnMerges[index.column()]) {
214 QModelIndex sourceIndex = mapToSource(createIndex(index.row(), column, index.internalPointer()));
216 if (method.userType() == QMetaType::Int && method.toInt() == CodeCompletionModel::CustomHighlighting) {
217 return QVariant(CodeCompletionModel::CustomHighlighting);
218 }
219 }
220 return QVariant();
221 }
223 // Merge custom highlighting if multiple columns were merged
224 QStringList strings;
225
226 // Collect strings
227 const auto &columns = m_columnMerges[index.column()];
228 strings.reserve(columns.size());
229 for (int column : columns) {
230 strings << mapToSource(createIndex(index.row(), column, index.internalPointer())).data(Qt::DisplayRole).toString();
231 }
232
233 QList<QVariantList> highlights;
234
235 // Collect custom-highlightings
236 highlights.reserve(columns.size());
237 for (int column : columns) {
239 }
240
241 return mergeCustomHighlighting(strings, highlights, 0);
242 }
243
244 QVariant v = mapToSource(index).data(role);
245 if (v.isValid()) {
246 return v;
247 } else {
248 return ExpandingWidgetModel::data(index, role);
249 }
250 }
251
252 // Returns a nonzero group if this index is the head of a group(A Label in the list)
253 Group *g = groupForIndex(index);
254
255 if (g && (!g->isEmpty)) {
256 switch (role) {
257 case Qt::DisplayRole:
258 if (!index.column()) {
259 return g->title;
260 }
261 break;
262
263 case Qt::FontRole:
264 if (!index.column()) {
265 QFont f = view()->renderer()->currentFont();
266 f.setBold(true);
267 return f;
268 }
269 break;
270
275 }
276 }
277
278 return QVariant();
279}
280
282{
283 if (!index.isValid()) {
284 return 0;
285 }
286 Group *g = groupOfParent(index);
287 if (!g || g->filtered.size() < (size_t)index.row()) {
288 return 0;
289 }
290
291 return contextMatchQuality(g->filtered[index.row()].sourceRow());
292}
293
294int KateCompletionModel::contextMatchQuality(const ModelRow &source) const
295{
296 QModelIndex realIndex = source.second;
297
298 int bestMatch = -1;
299 // Iterate through all argument-hints and find the best match-quality
300 for (const Item &item : m_argumentHints->filtered) {
301 const ModelRow &row(item.sourceRow());
302 if (realIndex.model() != row.first) {
303 continue; // We can only match within the same source-model
304 }
305
306 QModelIndex hintIndex = row.second;
307
309 if (!depth.isValid() || depth.userType() != QMetaType::Int || depth.toInt() != 1) {
310 continue; // Only match completion-items to argument-hints of depth 1(the ones the item will be given to as argument)
311 }
312
314
315 QVariant matchQuality = realIndex.data(CodeCompletionModel::MatchQuality);
316 if (matchQuality.isValid() && matchQuality.userType() == QMetaType::Int) {
317 int m = matchQuality.toInt();
318 if (m > bestMatch) {
319 bestMatch = m;
320 }
321 }
322 }
323
324 if (m_argumentHints->filtered.empty()) {
325 QVariant matchQuality = realIndex.data(CodeCompletionModel::MatchQuality);
326 if (matchQuality.isValid() && matchQuality.userType() == QMetaType::Int) {
327 int m = matchQuality.toInt();
328 if (m > bestMatch) {
329 bestMatch = m;
330 }
331 }
332 }
333
334 return bestMatch;
335}
336
337Qt::ItemFlags KateCompletionModel::flags(const QModelIndex &index) const
338{
339 if (!hasCompletionModel() || !index.isValid()) {
340 return Qt::NoItemFlags;
341 }
342
343 if (!hasGroups() || groupOfParent(index)) {
345 }
346
347 return Qt::ItemIsEnabled;
348}
349
350KateCompletionWidget *KateCompletionModel::widget() const
351{
352 return static_cast<KateCompletionWidget *>(QObject::parent());
353}
354
355KTextEditor::ViewPrivate *KateCompletionModel::view() const
356{
357 return widget()->view();
358}
359
360int KateCompletionModel::columnCount(const QModelIndex &) const
361{
362 return 3;
363}
364
365KateCompletionModel::ModelRow KateCompletionModel::modelRowPair(const QModelIndex &index)
366{
367 return qMakePair(static_cast<CodeCompletionModel *>(const_cast<QAbstractItemModel *>(index.model())), index);
368}
369
370bool KateCompletionModel::hasChildren(const QModelIndex &parent) const
371{
372 if (!hasCompletionModel()) {
373 return false;
374 }
375
376 if (!parent.isValid()) {
377 if (hasGroups()) {
378 return true;
379 }
380
381 return !m_ungrouped->filtered.empty();
382 }
383
384 if (parent.column() != 0) {
385 return false;
386 }
387
388 if (!hasGroups()) {
389 return false;
390 }
391
392 if (Group *g = groupForIndex(parent)) {
393 return !g->filtered.empty();
394 }
395
396 return false;
397}
398
399QModelIndex KateCompletionModel::index(int row, int column, const QModelIndex &parent) const
400{
401 if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) {
402 return QModelIndex();
403 }
404
405 if (parent.isValid() || !hasGroups()) {
406 if (parent.isValid() && parent.column() != 0) {
407 return QModelIndex();
408 }
409
410 Group *g = groupForIndex(parent);
411
412 if (!g) {
413 return QModelIndex();
414 }
415
416 if (row >= (int)g->filtered.size()) {
417 // qCWarning(LOG_KTE) << "Invalid index requested: row " << row << " beyond individual range in group " << g;
418 return QModelIndex();
419 }
420
421 // qCDebug(LOG_KTE) << "Returning index for child " << row << " of group " << g;
422 return createIndex(row, column, g);
423 }
424
425 if (size_t(row) >= m_rowTable.size()) {
426 // qCWarning(LOG_KTE) << "Invalid index requested: row " << row << " beyond group range.";
427 return QModelIndex();
428 }
429
430 // qCDebug(LOG_KTE) << "Returning index for group " << m_rowTable[row];
431 return createIndex(row, column, quintptr(0));
432}
433
434bool KateCompletionModel::hasIndex(int row, int column, const QModelIndex &parent) const
435{
436 if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) {
437 return false;
438 }
439
440 if (parent.isValid() || !hasGroups()) {
441 if (parent.isValid() && parent.column() != 0) {
442 return false;
443 }
444
445 Group *g = groupForIndex(parent);
446
447 if (row >= (int)g->filtered.size()) {
448 return false;
449 }
450
451 return true;
452 }
453
454 if (size_t(row) >= m_rowTable.size()) {
455 return false;
456 }
457
458 return true;
459}
460
461QModelIndex KateCompletionModel::indexForRow(Group *g, int row) const
462{
463 if (row < 0 || row >= (int)g->filtered.size()) {
464 return QModelIndex();
465 }
466
467 return createIndex(row, 0, g);
468}
469
470QModelIndex KateCompletionModel::indexForGroup(Group *g) const
471{
472 if (!hasGroups()) {
473 return QModelIndex();
474 }
475
476 auto it = std::find(m_rowTable.begin(), m_rowTable.end(), g);
477 if (it == m_rowTable.end()) {
478 return QModelIndex();
479 }
480 int row = std::distance(m_rowTable.begin(), it);
481 return createIndex(row, 0, quintptr(0));
482}
483
484void KateCompletionModel::clearGroups()
485{
486 m_ungrouped->clear();
487 m_argumentHints->clear();
488 m_bestMatches->clear();
489
490 // Don't bother trying to work out where it is
491 m_rowTable.erase(std::remove_if(m_rowTable.begin(),
492 m_rowTable.end(),
493 [this](Group *g) {
494 return (g == m_ungrouped) || (g == m_argumentHints) || (g == m_bestMatches);
495 }),
496 m_rowTable.end());
497
498 m_emptyGroups.erase(std::remove_if(m_emptyGroups.begin(),
499 m_emptyGroups.end(),
500 [this](Group *g) {
501 return (g == m_ungrouped) || (g == m_argumentHints) || (g == m_bestMatches);
502 }),
503 m_emptyGroups.end());
504
505 qDeleteAll(m_rowTable);
506 qDeleteAll(m_emptyGroups);
507 m_rowTable.clear();
508 m_emptyGroups.clear();
509 m_groupHash.clear();
510 m_customGroupHash.clear();
511
512 m_emptyGroups.insert(m_emptyGroups.end(), {m_ungrouped, m_argumentHints, m_bestMatches});
513
514 m_groupHash.insert(0, m_ungrouped);
515 m_groupHash.insert(-1, m_argumentHints);
516 m_groupHash.insert(BestMatchesProperty, m_bestMatches);
517}
518
519KateCompletionModel::GroupSet KateCompletionModel::createItems(const HierarchicalModelHandler &_handler, const QModelIndex &i, bool notifyModel)
520{
521 HierarchicalModelHandler handler(_handler);
522 GroupSet ret;
523 QAbstractItemModel *model = handler.model();
524
525 if (model->rowCount(i) == 0) {
526 // Leaf node, create an item
527 ret.insert(createItem(handler, i, notifyModel));
528 } else {
529 // Non-leaf node, take the role from the node, and recurse to the sub-nodes
530 handler.takeRole(i);
531 for (int a = 0; a < model->rowCount(i); a++) {
532 ret.merge(createItems(handler, model->index(a, 0, i), notifyModel));
533 }
534 }
535
536 return ret;
537}
538
539KateCompletionModel::GroupSet KateCompletionModel::deleteItems(const QModelIndex &i)
540{
541 GroupSet ret;
542
543 if (i.model()->rowCount(i) == 0) {
544 // Leaf node, delete the item
545 Group *g = groupForIndex(mapFromSource(i));
546 ret.insert(g);
547 g->removeItem(ModelRow(const_cast<CodeCompletionModel *>(static_cast<const CodeCompletionModel *>(i.model())), i));
548 } else {
549 // Non-leaf node
550 for (int a = 0; a < i.model()->rowCount(i); a++) {
551 ret.merge(deleteItems(i.model()->index(a, 0, i)));
552 }
553 }
554
555 return ret;
556}
557
558void KateCompletionModel::createGroups()
559{
561 // After clearing the model, it has to be reset, else we will be in an invalid state while inserting
562 // new groups.
563 clearGroups();
564
565 bool has_groups = false;
566 GroupSet groups;
567 for (CodeCompletionModel *sourceModel : std::as_const(m_completionModels)) {
568 has_groups |= sourceModel->hasGroups();
569 for (int i = 0; i < sourceModel->rowCount(); ++i) {
570 groups.merge(createItems(HierarchicalModelHandler(sourceModel), sourceModel->index(i, 0)));
571 }
572 }
573
574 // since notifyModel = false above, we just appended the data as is,
575 // we sort it now
576 for (auto g : groups) {
577 // no need to sort prefiltered, it is just the raw dump of everything
578 // filtered is what gets displayed
579 // std::sort(g->prefilter.begin(), g->prefilter.end());
580 std::sort(g->filtered.begin(), g->filtered.end(), [this](const Item &l, const Item &r) {
581 return l.lessThan(this, r);
582 });
583 }
584
585 m_hasGroups = has_groups;
586
587 // debugStats();
588
589 for (Group *g : m_rowTable) {
590 hideOrShowGroup(g);
591 }
592
593 for (Group *g : m_emptyGroups) {
594 hideOrShowGroup(g);
595 }
596
597 makeGroupItemsUnique();
598
599 updateBestMatches();
601}
602
603KateCompletionModel::Group *KateCompletionModel::createItem(const HierarchicalModelHandler &handler, const QModelIndex &sourceIndex, bool notifyModel)
604{
605 // QModelIndex sourceIndex = sourceModel->index(row, CodeCompletionModel::Name, QModelIndex());
606
607 int completionFlags = handler.getData(CodeCompletionModel::CompletionRole, sourceIndex).toInt();
608
609 int argumentHintDepth = handler.getData(CodeCompletionModel::ArgumentHintDepth, sourceIndex).toInt();
610
611 Group *g;
612 if (argumentHintDepth) {
613 g = m_argumentHints;
614 } else {
615 QString customGroup = handler.customGroup();
616 if (!customGroup.isNull() && m_hasGroups) {
617 if (m_customGroupHash.contains(customGroup)) {
618 g = m_customGroupHash[customGroup];
619 } else {
620 g = new Group(customGroup, 0, this);
621 g->customSortingKey = handler.customGroupingKey();
622 m_emptyGroups.push_back(g);
623 m_customGroupHash.insert(customGroup, g);
624 }
625 } else {
626 g = fetchGroup(completionFlags, handler.hasHierarchicalRoles());
627 }
628 }
629
630 Item item = Item(g != m_argumentHints, this, handler, ModelRow(handler.model(), sourceIndex));
631
632 if (g != m_argumentHints) {
633 item.match(this);
634 }
635
636 g->addItem(item, notifyModel);
637
638 return g;
639}
640
641void KateCompletionModel::slotRowsInserted(const QModelIndex &parent, int start, int end)
642{
643 HierarchicalModelHandler handler(static_cast<CodeCompletionModel *>(sender()));
644 if (parent.isValid()) {
645 handler.collectRoles(parent);
646 }
647
648 GroupSet affectedGroups;
649 for (int i = start; i <= end; ++i) {
650 affectedGroups.merge(createItems(handler, handler.model()->index(i, 0, parent), /* notifyModel= */ true));
651 }
652
653 for (auto g : affectedGroups) {
654 hideOrShowGroup(g, true);
655 }
656}
657
658void KateCompletionModel::slotRowsRemoved(const QModelIndex &parent, int start, int end)
659{
660 CodeCompletionModel *source = static_cast<CodeCompletionModel *>(sender());
661
662 GroupSet affectedGroups;
663 for (int i = start; i <= end; ++i) {
664 QModelIndex index = source->index(i, 0, parent);
665 affectedGroups.merge(deleteItems(index));
666 }
667
668 for (auto g : affectedGroups) {
669 hideOrShowGroup(g, true);
670 }
671}
672
673KateCompletionModel::Group *KateCompletionModel::fetchGroup(int attribute, bool forceGrouping)
674{
675 Q_UNUSED(forceGrouping);
676
677 ///@todo use forceGrouping
678 if (!hasGroups()) {
679 return m_ungrouped;
680 }
681
682 int groupingAttribute = groupingAttributes(attribute);
683 // qCDebug(LOG_KTE) << attribute << " " << groupingAttribute;
684
685 if (m_groupHash.contains(groupingAttribute)) {
686 return m_groupHash.value(groupingAttribute);
687 }
688
689 QString st;
690 QString at;
691 QString it;
692 QString title;
693
694 if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) {
695 st = QStringLiteral("Global");
696 } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) {
697 st = QStringLiteral("Namespace");
698 } else if (attribute & KTextEditor::CodeCompletionModel::LocalScope) {
699 st = QStringLiteral("Local");
700 }
701
702 title = st;
703
704 if (attribute & KTextEditor::CodeCompletionModel::Public) {
705 at = QStringLiteral("Public");
706 } else if (attribute & KTextEditor::CodeCompletionModel::Protected) {
707 at = QStringLiteral("Protected");
708 } else if (attribute & KTextEditor::CodeCompletionModel::Private) {
709 at = QStringLiteral("Private");
710 }
711
712 if (!at.isEmpty()) {
713 if (!title.isEmpty()) {
714 title.append(QLatin1String(", "));
715 }
716
717 title.append(at);
718 }
719
720 Group *ret = new Group(title, attribute, this);
721 ret->scope = QString();
722
723 m_emptyGroups.push_back(ret);
724 m_groupHash.insert(groupingAttribute, ret);
725
726 return ret;
727}
728
729KateCompletionModel::Group *KateCompletionModel::groupForIndex(const QModelIndex &index) const
730{
731 if (!index.isValid()) {
732 if (!hasGroups()) {
733 return m_ungrouped;
734 } else {
735 return nullptr;
736 }
737 }
738
739 if (groupOfParent(index)) {
740 return nullptr;
741 }
742
743 if (size_t(index.row()) >= m_rowTable.size()) {
744 return m_ungrouped;
745 }
746
747 return m_rowTable[index.row()];
748}
749
751{
752 if (!index.isValid()) {
753 return QModelIndex();
754 }
755
756 if (Group *g = groupOfParent(index)) {
757 if (!hasGroups()) {
758 Q_ASSERT(g == m_ungrouped);
759 return QModelIndex();
760 }
761
762 auto it = std::find(m_rowTable.begin(), m_rowTable.end(), g);
763 if (it == m_rowTable.end()) {
764 qCWarning(LOG_KTE) << "Couldn't find parent for index" << index;
765 return QModelIndex();
766 }
767 int row = std::distance(m_rowTable.begin(), it);
768 return createIndex(row, 0, quintptr(0));
769 }
770
771 return QModelIndex();
772}
773
774int KateCompletionModel::rowCount(const QModelIndex &parent) const
775{
776 if (!parent.isValid()) {
777 if (hasGroups()) {
778 // qCDebug(LOG_KTE) << "Returning row count for toplevel " << m_rowTable.count();
779 return m_rowTable.size();
780 } else {
781 // qCDebug(LOG_KTE) << "Returning ungrouped row count for toplevel " << m_ungrouped->filtered.count();
782 return m_ungrouped->filtered.size();
783 }
784 }
785
786 if (parent.column() > 0) {
787 // only the first column has children
788 return 0;
789 }
790
791 Group *g = groupForIndex(parent);
792
793 // This is not an error, seems you don't have to check hasChildren()
794 if (!g) {
795 return 0;
796 }
797
798 // qCDebug(LOG_KTE) << "Returning row count for group " << g << " as " << g->filtered.count();
799 return g->filtered.size();
800}
801
803{
804 if (!proxyIndex.isValid()) {
805 return QModelIndex();
806 }
807
808 if (Group *g = groupOfParent(proxyIndex)) {
809 if (std::find(m_rowTable.begin(), m_rowTable.end(), g) == m_rowTable.end()) {
810 qWarning() << Q_FUNC_INFO << "Stale proxy index for which there is no group";
811 return {};
812 }
813
814 if (proxyIndex.row() >= 0 && proxyIndex.row() < (int)g->filtered.size()) {
815 ModelRow source = g->filtered[proxyIndex.row()].sourceRow();
816 return source.second.sibling(source.second.row(), proxyIndex.column());
817 } else {
818 qCDebug(LOG_KTE) << "Invalid proxy-index";
819 }
820 }
821
822 return QModelIndex();
823}
824
826{
827 if (!sourceIndex.isValid()) {
828 return QModelIndex();
829 }
830
831 if (!hasGroups()) {
832 return index(m_ungrouped->rowOf(modelRowPair(sourceIndex)), sourceIndex.column(), QModelIndex());
833 }
834
835 for (Group *g : m_rowTable) {
836 int row = g->rowOf(modelRowPair(sourceIndex));
837 if (row != -1) {
838 return index(row, sourceIndex.column(), indexForGroup(g));
839 }
840 }
841
842 // Copied from above
843 for (Group *g : m_emptyGroups) {
844 int row = g->rowOf(modelRowPair(sourceIndex));
845 if (row != -1) {
846 return index(row, sourceIndex.column(), indexForGroup(g));
847 }
848 }
849
850 return QModelIndex();
851}
852
853void KateCompletionModel::setCurrentCompletion(QMap<KTextEditor::CodeCompletionModel *, QString> currentMatch)
854{
856
857 m_currentMatch = currentMatch;
858
859 if (!hasGroups()) {
860 changeCompletions(m_ungrouped);
861 } else {
862 for (Group *g : m_rowTable) {
863 if (g != m_argumentHints) {
864 changeCompletions(g);
865 }
866 }
867 for (Group *g : m_emptyGroups) {
868 if (g != m_argumentHints) {
869 changeCompletions(g);
870 }
871 }
872 }
873
874 // NOTE: best matches are also updated in resort
875 resort();
876
878}
879
880QString KateCompletionModel::commonPrefixInternal(const QString &forcePrefix) const
881{
882 QString commonPrefix; // isNull() = true
883
884 std::vector<Group *> groups = m_rowTable;
885 groups.push_back(m_ungrouped);
886
887 for (Group *g : groups) {
888 for (const Item &item : g->filtered) {
889 uint startPos = currentCompletion(item.sourceRow().first).length();
890 const QString candidate = item.name().mid(startPos);
891
892 if (!candidate.startsWith(forcePrefix)) {
893 continue;
894 }
895
896 if (commonPrefix.isNull()) {
897 commonPrefix = candidate;
898
899 // Replace QString() prefix with QString(), so we won't initialize it again
900 if (commonPrefix.isNull()) {
901 commonPrefix = QString(); // isEmpty() = true, isNull() = false
902 }
903 } else {
904 commonPrefix.truncate(candidate.length());
905
906 for (int a = 0; a < commonPrefix.length(); ++a) {
907 if (commonPrefix[a] != candidate[a]) {
909 break;
910 }
911 }
912 }
913 }
914 }
915
916 return commonPrefix;
917}
918
920{
921 QString commonPrefix = commonPrefixInternal(QString());
922
923 if (commonPrefix.isEmpty() && selectedIndex.isValid()) {
924 Group *g = m_ungrouped;
925 if (hasGroups()) {
926 g = groupOfParent(selectedIndex);
927 }
928
929 if (g && selectedIndex.row() < (int)g->filtered.size()) {
930 // Follow the path of the selected item, finding the next non-empty common prefix
931 Item item = g->filtered[selectedIndex.row()];
932 int matchLength = currentCompletion(item.sourceRow().first).length();
933 commonPrefix = commonPrefixInternal(item.name().mid(matchLength).left(1));
934 }
935 }
936
937 return commonPrefix;
938}
939
940void KateCompletionModel::changeCompletions(Group *g)
941{
942 // This code determines what of the filtered items still fit
943 // don't notify the model. The model is notified afterwards through a reset().
944 g->filtered.clear();
945 std::remove_copy_if(g->prefilter.begin(), g->prefilter.end(), std::back_inserter(g->filtered), [this](Item &item) {
946 return !item.match(this);
947 });
948
949 hideOrShowGroup(g, /*notifyModel=*/false);
950}
951
952int KateCompletionModel::Group::orderNumber() const
953{
954 if (this == model->m_ungrouped) {
955 return 700;
956 }
957
958 if (customSortingKey != -1) {
959 return customSortingKey;
960 }
961
962 if (attribute & BestMatchesProperty) {
963 return 1;
964 }
965
966 if (attribute & KTextEditor::CodeCompletionModel::LocalScope) {
967 return 100;
968 } else if (attribute & KTextEditor::CodeCompletionModel::Public) {
969 return 200;
970 } else if (attribute & KTextEditor::CodeCompletionModel::Protected) {
971 return 300;
972 } else if (attribute & KTextEditor::CodeCompletionModel::Private) {
973 return 400;
974 } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) {
975 return 500;
976 } else if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) {
977 return 600;
978 }
979
980 return 700;
981}
982
983bool KateCompletionModel::Group::orderBefore(Group *other) const
984{
985 return orderNumber() < other->orderNumber();
986}
987
988void KateCompletionModel::hideOrShowGroup(Group *g, bool notifyModel)
989{
990 if (g == m_argumentHints) {
991 Q_EMIT argumentHintsChanged();
992 m_updateBestMatchesTimer->start(200); // We have new argument-hints, so we have new best matches
993 return; // Never show argument-hints in the normal completion-list
994 }
995
996 if (!g->isEmpty) {
997 if (g->filtered.empty()) {
998 // Move to empty group list
999 g->isEmpty = true;
1000 auto it = std::find(m_rowTable.begin(), m_rowTable.end(), g);
1001
1002 if (it != m_rowTable.end()) {
1003 int row = std::distance(m_rowTable.begin(), it);
1004 if (hasGroups() && notifyModel) {
1005 beginRemoveRows(QModelIndex(), row, row);
1006 }
1007 m_rowTable.erase(it);
1008 if (hasGroups() && notifyModel) {
1009 endRemoveRows();
1010 }
1011 m_emptyGroups.push_back(g);
1012 } else {
1013 qCWarning(LOG_KTE) << "Group " << g << " not found in row table!!";
1014 }
1015 }
1016
1017 } else {
1018 if (!g->filtered.empty()) {
1019 // Move off empty group list
1020 g->isEmpty = false;
1021
1022 int row = 0; // Find row where to insert
1023 for (size_t a = 0; a < m_rowTable.size(); a++) {
1024 if (g->orderBefore(m_rowTable[a])) {
1025 row = a;
1026 break;
1027 }
1028 row = a + 1;
1029 }
1030
1031 if (notifyModel) {
1032 if (hasGroups()) {
1033 beginInsertRows(QModelIndex(), row, row);
1034 } else {
1035 beginInsertRows(QModelIndex(), 0, g->filtered.size());
1036 }
1037 }
1038 m_rowTable.insert(m_rowTable.begin() + row, g);
1039 if (notifyModel) {
1040 endInsertRows();
1041 }
1042 m_emptyGroups.erase(std::remove(m_emptyGroups.begin(), m_emptyGroups.end(), g), m_emptyGroups.end());
1043 }
1044 }
1045}
1046
1048{
1049 if (!hasGroups()) {
1050 return true;
1051 }
1052
1053 if (groupOfParent(index)) {
1054 return true;
1055 }
1056
1057 return false;
1058}
1059
1060void KateCompletionModel::slotModelReset()
1061{
1062 createGroups();
1063
1064 // debugStats();
1065}
1066
1067void KateCompletionModel::debugStats()
1068{
1069 if (!hasGroups()) {
1070 qCDebug(LOG_KTE) << "Model groupless, " << m_ungrouped->filtered.size() << " items.";
1071 } else {
1072 qCDebug(LOG_KTE) << "Model grouped (" << m_rowTable.size() << " groups):";
1073 for (Group *g : m_rowTable) {
1074 qCDebug(LOG_KTE) << "Group" << g << "count" << g->filtered.size();
1075 }
1076 }
1077}
1078
1079bool KateCompletionModel::hasCompletionModel() const
1080{
1081 return !m_completionModels.empty();
1082}
1083
1084int KateCompletionModel::translateColumn(int sourceColumn) const
1085{
1086 if (m_columnMerges.empty()) {
1087 return sourceColumn;
1088 }
1089
1090 /* Debugging - dump column merge list
1091
1092 QString columnMerge;
1093 for (const QList<int> &list : m_columnMerges) {
1094 columnMerge += '[';
1095 for (int column : list) {
1096 columnMerge += QString::number(column) + QLatin1Char(' ');
1097 }
1098 columnMerge += "] ";
1099 }
1100
1101 qCDebug(LOG_KTE) << k_funcinfo << columnMerge;*/
1102
1103 int c = 0;
1104 for (const auto &list : m_columnMerges) {
1105 for (int column : list) {
1106 if (column == sourceColumn) {
1107 return c;
1108 }
1109 }
1110 c++;
1111 }
1112 return -1;
1113}
1114
1115int KateCompletionModel::groupingAttributes(int attribute) const
1116{
1117 int ret = 0;
1118
1119 if (countBits(attribute & ScopeTypeMask) > 1) {
1120 qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one scope type modifier provided.";
1121 }
1122 if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) {
1123 ret |= KTextEditor::CodeCompletionModel::GlobalScope;
1124 } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) {
1125 ret |= KTextEditor::CodeCompletionModel::NamespaceScope;
1126 } else if (attribute & KTextEditor::CodeCompletionModel::LocalScope) {
1127 ret |= KTextEditor::CodeCompletionModel::LocalScope;
1128 }
1129
1130 if (countBits(attribute & AccessTypeMask) > 1) {
1131 qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one access type modifier provided.";
1132 }
1133 if (attribute & KTextEditor::CodeCompletionModel::Public) {
1134 ret |= KTextEditor::CodeCompletionModel::Public;
1135 } else if (attribute & KTextEditor::CodeCompletionModel::Protected) {
1136 ret |= KTextEditor::CodeCompletionModel::Protected;
1137 } else if (attribute & KTextEditor::CodeCompletionModel::Private) {
1138 ret |= KTextEditor::CodeCompletionModel::Private;
1139 }
1140
1141 return ret;
1142}
1143
1144int KateCompletionModel::countBits(int value)
1145{
1146 int count = 0;
1147 for (int i = 1; i; i <<= 1) {
1148 if (i & value) {
1149 count++;
1150 }
1151 }
1152
1153 return count;
1154}
1155
1156KateCompletionModel::Item::Item(bool doInitialMatch, KateCompletionModel *m, const HierarchicalModelHandler &handler, ModelRow sr)
1157 : m_sourceRow(sr)
1158 , matchCompletion(StartsWithMatch)
1159 , m_haveExactMatch(false)
1160{
1161 inheritanceDepth = handler.getData(CodeCompletionModel::InheritanceDepth, m_sourceRow.second).toInt();
1162 m_unimportant = handler.getData(CodeCompletionModel::UnimportantItemRole, m_sourceRow.second).toBool();
1163
1164 QModelIndex nameSibling = sr.second.sibling(sr.second.row(), CodeCompletionModel::Name);
1165 m_nameColumn = nameSibling.data(Qt::DisplayRole).toString();
1166
1167 if (doInitialMatch) {
1168 match(m);
1169 }
1170}
1171
1172bool KateCompletionModel::Item::lessThan(KateCompletionModel *model, const Item &rhs) const
1173{
1174 int ret = 0;
1175
1176 // qCDebug(LOG_KTE) << c1 << " c/w " << c2 << " -> " << (model->isSortingReverse() ? ret > 0 : ret < 0) << " (" << ret << ")";
1177
1178 if (m_unimportant && !rhs.m_unimportant) {
1179 return false;
1180 }
1181
1182 if (!m_unimportant && rhs.m_unimportant) {
1183 return true;
1184 }
1185
1186 if (matchCompletion < rhs.matchCompletion) {
1187 // enums are ordered in the order items should be displayed
1188 return true;
1189 }
1190 if (matchCompletion > rhs.matchCompletion) {
1191 return false;
1192 }
1193
1194 ret = inheritanceDepth - rhs.inheritanceDepth;
1195
1196 if (ret == 0) {
1197 auto it = model->m_currentMatch.constFind(rhs.m_sourceRow.first);
1198 if (it != model->m_currentMatch.cend()) {
1199 const QString &filter = it.value();
1200 bool thisStartWithFilter = m_nameColumn.startsWith(filter, Qt::CaseSensitive);
1201 bool rhsStartsWithFilter = rhs.m_nameColumn.startsWith(filter, Qt::CaseSensitive);
1202
1203 if (thisStartWithFilter && !rhsStartsWithFilter) {
1204 return true;
1205 }
1206 if (rhsStartsWithFilter && !thisStartWithFilter) {
1207 return false;
1208 }
1209 }
1210 }
1211
1212 if (ret == 0) {
1213 // Do not use localeAwareCompare, because it is simply too slow for a list of about 1000 items
1214 ret = QString::compare(m_nameColumn, rhs.m_nameColumn, Qt::CaseInsensitive);
1215 }
1216
1217 if (ret == 0) {
1218 // FIXME need to define a better default ordering for multiple model display
1219 ret = m_sourceRow.second.row() - rhs.m_sourceRow.second.row();
1220 }
1221
1222 return ret < 0;
1223}
1224
1225void KateCompletionModel::Group::addItem(const Item &i, bool notifyModel)
1226{
1227 if (isEmpty) {
1228 notifyModel = false;
1229 }
1230
1231 QModelIndex groupIndex;
1232 if (notifyModel) {
1233 groupIndex = model->indexForGroup(this);
1234 }
1235
1236 if (notifyModel) {
1237 auto comp = [this](const Item &left, const Item &right) {
1238 return left.lessThan(model, right);
1239 };
1240 prefilter.insert(std::upper_bound(prefilter.begin(), prefilter.end(), i, comp), i);
1241 } else {
1242 prefilter.push_back(i);
1243 }
1244
1245 if (i.isVisible()) {
1246 if (notifyModel) {
1247 auto comp = [this](const Item &left, const Item &right) {
1248 return left.lessThan(model, right);
1249 };
1250 auto it = std::upper_bound(filtered.begin(), filtered.end(), i, comp);
1251 const auto rowNumber = it - filtered.begin();
1252 model->beginInsertRows(groupIndex, rowNumber, rowNumber);
1253 filtered.insert(it, i);
1254 } else {
1255 // we will sort it later
1256 filtered.push_back(i);
1257 }
1258 }
1259
1260 if (notifyModel) {
1261 model->endInsertRows();
1262 }
1263}
1264
1265bool KateCompletionModel::Group::removeItem(const ModelRow &row)
1266{
1267 for (size_t pi = 0; pi < prefilter.size(); ++pi) {
1268 if (prefilter[pi].sourceRow() == row) {
1269 int index = rowOf(row);
1270 if (index != -1) {
1271 model->beginRemoveRows(model->indexForGroup(this), index, index);
1272 filtered.erase(filtered.begin() + index);
1273 }
1274
1275 prefilter.erase(prefilter.begin() + pi);
1276
1277 if (index != -1) {
1278 model->endRemoveRows();
1279 }
1280
1281 return index != -1;
1282 }
1283 }
1284
1285 Q_ASSERT(false);
1286 return false;
1287}
1288
1289KateCompletionModel::Group::Group(const QString &title, int attribute, KateCompletionModel *m)
1290 : model(m)
1291 , attribute(attribute)
1292 // ugly hack to add some left margin
1293 , title(QLatin1Char(' ') + title)
1294 , isEmpty(true)
1295 , customSortingKey(-1)
1296{
1297 Q_ASSERT(model);
1298}
1299
1300void KateCompletionModel::Group::resort()
1301{
1302 auto comp = [this](const Item &left, const Item &right) {
1303 return left.lessThan(model, right);
1304 };
1305 std::stable_sort(filtered.begin(), filtered.end(), comp);
1306 model->hideOrShowGroup(this);
1307}
1308
1309void KateCompletionModel::resort()
1310{
1311 for (Group *g : m_rowTable) {
1312 g->resort();
1313 }
1314
1315 for (Group *g : m_emptyGroups) {
1316 g->resort();
1317 }
1318
1319 // call updateBestMatches here, so they are moved to the top again.
1320 updateBestMatches();
1321}
1322
1323void KateCompletionModel::Group::clear()
1324{
1325 prefilter.clear();
1326 filtered.clear();
1327 isEmpty = true;
1328}
1329
1330uint KateCompletionModel::filteredItemCount() const
1331{
1332 uint ret = 0;
1333 for (Group *group : m_rowTable) {
1334 ret += group->filtered.size();
1335 }
1336
1337 return ret;
1338}
1339
1341{
1342 // @todo Make this faster
1343
1344 bool doHide = false;
1345 CodeCompletionModel *hideModel = nullptr;
1346
1347 for (Group *group : m_rowTable) {
1348 for (const Item &item : group->filtered) {
1349 if (item.haveExactMatch()) {
1352 bool hide = false;
1353 if (!iface3) {
1354 hide = true;
1355 }
1356 if (iface3
1357 && iface3->matchingItem(item.sourceRow().second) == KTextEditor::CodeCompletionModelControllerInterface::HideListIfAutomaticInvocation) {
1358 hide = true;
1359 }
1360 if (hide) {
1361 doHide = true;
1362 hideModel = item.sourceRow().first;
1363 }
1364 }
1365 }
1366 }
1367
1368 if (doHide) {
1369 // Check if all other visible items are from the same model
1370 for (Group *group : m_rowTable) {
1371 for (const Item &item : group->filtered) {
1372 if (item.sourceRow().first != hideModel) {
1373 return false;
1374 }
1375 }
1376 }
1377 }
1378
1379 return doHide;
1380}
1381
1382static inline QChar toLower(QChar c)
1383{
1384 return c.isLower() ? c : c.toLower();
1385}
1386
1387bool KateCompletionModel::matchesAbbreviation(const QString &word, const QString &typed, int &score)
1388{
1389 // A mismatch is very likely for random even for the first letter,
1390 // thus this optimization makes sense.
1391
1392 // We require that first letter must match before we do fuzzy matching.
1393 // Not sure how well this well it works in practice, but seems ok so far.
1394 // Also, 0 might not be the first letter. Some sources add a space or a marker
1395 // at the beginning. So look for first letter
1396 const int firstLetter = [&word] {
1397 for (auto it = word.cbegin(); it != word.cend(); ++it) {
1398 if (it->isLetter())
1399 return int(it - word.cbegin());
1400 }
1401 return 0;
1402 }();
1403
1404 QStringView wordView = word;
1405 wordView = wordView.mid(firstLetter);
1406
1407 if (toLower(wordView.at(0)) != toLower(typed.at(0))) {
1408 return false;
1409 }
1410
1411 const auto res = KFuzzyMatcher::match(typed, wordView);
1412 score = res.score;
1413 return res.matched;
1414}
1415
1416static inline bool containsAtWordBeginning(const QString &word, const QString &typed)
1417{
1418 if (typed.size() > word.size()) {
1419 return false;
1420 }
1421
1422 for (int i = 1; i < word.size(); i++) {
1423 // The current position is a word beginning if the previous character was an underscore
1424 // or if the current character is uppercase. Subsequent uppercase characters do not count,
1425 // to handle the special case of UPPER_CASE_VARS properly.
1426 const QChar c = word.at(i);
1427 const QChar prev = word.at(i - 1);
1428 if (!(prev == QLatin1Char('_') || (c.isUpper() && !prev.isUpper()))) {
1429 continue;
1430 }
1431 if (QStringView(word).mid(i).startsWith(typed, Qt::CaseInsensitive)) {
1432 return true;
1433 }
1434
1435 // If we do not have enough string left, return early
1436 if (word.size() - i < typed.size()) {
1437 return false;
1438 }
1439 }
1440 return false;
1441}
1442
1443KateCompletionModel::Item::MatchType KateCompletionModel::Item::match(KateCompletionModel *model)
1444{
1445 const QString match = model->currentCompletion(m_sourceRow.first);
1446
1447 m_haveExactMatch = false;
1448
1449 // Hehe, everything matches nothing! (ie. everything matches a blank string)
1450 if (match.isEmpty()) {
1451 return PerfectMatch;
1452 }
1453 if (m_nameColumn.isEmpty()) {
1454 return NoMatch;
1455 }
1456
1457 matchCompletion = (m_nameColumn.startsWith(match) ? StartsWithMatch : NoMatch);
1458
1459 if (matchCompletion == NoMatch && !m_nameColumn.isEmpty() && !match.isEmpty()) {
1460 // if still no match, try abbreviation matching
1461 int score = 0;
1462 if (matchesAbbreviation(m_nameColumn, match, score)) {
1463 inheritanceDepth -= score;
1464 matchCompletion = AbbreviationMatch;
1465 }
1466 }
1467
1468 if (matchCompletion == NoMatch) {
1469 // if no match, try for "contains"
1470 // Only match when the occurrence is at a "word" beginning, marked by
1471 // an underscore or a capital. So Foo matches BarFoo and Bar_Foo, but not barfoo.
1472 // Starting at 1 saves looking at the beginning of the word, that was already checked above.
1473 if (containsAtWordBeginning(m_nameColumn, match)) {
1474 matchCompletion = ContainsMatch;
1475 }
1476 }
1477
1478 if (matchCompletion && match.length() == m_nameColumn.length()) {
1479 matchCompletion = PerfectMatch;
1480 m_haveExactMatch = true;
1481 }
1482
1483 return matchCompletion;
1484}
1485
1486bool KateCompletionModel::Item::isVisible() const
1487{
1488 return matchCompletion;
1489}
1490
1491const KateCompletionModel::ModelRow &KateCompletionModel::Item::sourceRow() const
1492{
1493 return m_sourceRow;
1494}
1495
1496QString KateCompletionModel::currentCompletion(KTextEditor::CodeCompletionModel *model) const
1497{
1498 return m_currentMatch.value(model);
1499}
1500
1501void KateCompletionModel::addCompletionModel(KTextEditor::CodeCompletionModel *model)
1502{
1503 if (m_completionModels.contains(model)) {
1504 return;
1505 }
1506
1507 m_completionModels.push_back(model);
1508
1509 connect(model, &KTextEditor::CodeCompletionModel::rowsInserted, this, &KateCompletionModel::slotRowsInserted);
1510 connect(model, &KTextEditor::CodeCompletionModel::rowsRemoved, this, &KateCompletionModel::slotRowsRemoved);
1511 connect(model, &KTextEditor::CodeCompletionModel::modelReset, this, &KateCompletionModel::slotModelReset);
1512 connect(model,
1514 this,
1515 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
1516 Q_EMIT dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles);
1517 });
1518
1519 // This performs the reset
1520 createGroups();
1521}
1522
1523void KateCompletionModel::setCompletionModel(KTextEditor::CodeCompletionModel *model)
1524{
1525 clearCompletionModels();
1526 addCompletionModel(model);
1527}
1528
1529void KateCompletionModel::setCompletionModels(const QList<KTextEditor::CodeCompletionModel *> &models)
1530{
1531 // if (m_completionModels == models)
1532 // return;
1533
1534 clearCompletionModels();
1535
1536 m_completionModels = models;
1537
1538 for (KTextEditor::CodeCompletionModel *model : models) {
1539 connect(model, &KTextEditor::CodeCompletionModel::rowsInserted, this, &KateCompletionModel::slotRowsInserted);
1540 connect(model, &KTextEditor::CodeCompletionModel::rowsRemoved, this, &KateCompletionModel::slotRowsRemoved);
1541 connect(model, &KTextEditor::CodeCompletionModel::modelReset, this, &KateCompletionModel::slotModelReset);
1542 connect(model,
1544 this,
1545 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
1546 Q_EMIT dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles);
1547 });
1548 }
1549
1550 // This performs the reset
1551 createGroups();
1552}
1553
1554QList<KTextEditor::CodeCompletionModel *> KateCompletionModel::completionModels() const
1555{
1556 return m_completionModels;
1557}
1558
1559void KateCompletionModel::removeCompletionModel(CodeCompletionModel *model)
1560{
1561 if (!model || !m_completionModels.contains(model)) {
1562 return;
1563 }
1564
1565 bool willCreateGroups = (m_completionModels.size() - 1) > 0;
1566
1567 if (!willCreateGroups) {
1569 }
1570 m_currentMatch.remove(model);
1571
1572 clearGroups();
1573
1574 model->disconnect(this);
1575
1576 m_completionModels.removeAll(model);
1577 if (!willCreateGroups) {
1578 endResetModel();
1579 }
1580
1581 if (willCreateGroups) {
1582 // This performs the reset
1583 createGroups();
1584 }
1585}
1586
1587void KateCompletionModel::makeGroupItemsUnique(bool onlyFiltered)
1588{
1589 struct FilterItems {
1590 FilterItems(KateCompletionModel &model, const QList<KTextEditor::CodeCompletionModel *> &needShadowing)
1591 : m_model(model)
1592 , m_needShadowing(needShadowing)
1593 {
1594 }
1595
1597 KateCompletionModel &m_model;
1598 const QList<KTextEditor::CodeCompletionModel *> &m_needShadowing;
1599
1600 void filter(std::vector<Item> &items)
1601 {
1602 std::vector<Item> temp;
1603 temp.reserve(items.size());
1604 for (const Item &item : items) {
1605 auto it = had.constFind(item.name());
1606 if (it != had.constEnd() && *it != item.sourceRow().first && m_needShadowing.contains(item.sourceRow().first)) {
1607 continue;
1608 }
1609
1610 had.insert(item.name(), item.sourceRow().first);
1611 temp.push_back(item);
1612 }
1613 items.swap(temp);
1614 }
1615
1616 void filter(Group *group, bool onlyFiltered)
1617 {
1618 if (group->prefilter.size() == group->filtered.size()) {
1619 // Filter only once
1620 filter(group->filtered);
1621 if (!onlyFiltered) {
1622 group->prefilter = group->filtered;
1623 }
1624 } else {
1625 // Must filter twice
1626 filter(group->filtered);
1627 if (!onlyFiltered) {
1628 filter(group->prefilter);
1629 }
1630 }
1631
1632 if (group->filtered.empty()) {
1633 m_model.hideOrShowGroup(group);
1634 }
1635 }
1636 };
1637
1639 for (KTextEditor::CodeCompletionModel *model : std::as_const(m_completionModels)) {
1641 if (v4 && v4->shouldHideItemsWithEqualNames()) {
1642 needShadowing.push_back(model);
1643 }
1644 }
1645
1646 if (needShadowing.isEmpty()) {
1647 return;
1648 }
1649
1650 FilterItems filter(*this, needShadowing);
1651
1652 filter.filter(m_ungrouped, onlyFiltered);
1653
1654 for (Group *group : m_rowTable) {
1655 filter.filter(group, onlyFiltered);
1656 }
1657}
1658
1659// Updates the best-matches group
1660void KateCompletionModel::updateBestMatches()
1661{
1662 // We cannot do too many operations here, because they are all executed
1663 // whenever a character is added. Would be nice if we could split the
1664 // operations up somewhat using a timer.
1665 int maxMatches = 300;
1666
1667 m_updateBestMatchesTimer->stop();
1668 // Maps match-qualities to ModelRows paired together with the BestMatchesCount returned by the items.
1669 typedef QMultiMap<int, QPair<int, ModelRow>> BestMatchMap;
1670 BestMatchMap matches;
1671
1672 if (!hasGroups()) {
1673 // If there is no grouping, just change the order of the items, moving the best matching ones to the front
1674 QMultiMap<int, int> rowsForQuality;
1675
1676 int row = 0;
1677 for (const Item &item : m_ungrouped->filtered) {
1678 ModelRow source = item.sourceRow();
1679
1681
1682 if (v.userType() == QMetaType::Int && v.toInt() > 0) {
1683 int quality = contextMatchQuality(source);
1684 if (quality > 0) {
1685 rowsForQuality.insert(quality, row);
1686 }
1687 }
1688
1689 ++row;
1690 --maxMatches;
1691 if (maxMatches < 0) {
1692 break;
1693 }
1694 }
1695
1696 if (!rowsForQuality.isEmpty()) {
1697 // Rewrite m_ungrouped->filtered in a new order
1698 QSet<int> movedToFront;
1699 std::vector<Item> newFiltered;
1700 newFiltered.reserve(rowsForQuality.size());
1701 movedToFront.reserve(rowsForQuality.size());
1702 for (auto it = rowsForQuality.constBegin(); it != rowsForQuality.constEnd(); ++it) {
1703 newFiltered.push_back(m_ungrouped->filtered[it.value()]);
1704 movedToFront.insert(it.value());
1705 }
1706 std::reverse(newFiltered.begin(), newFiltered.end());
1707
1708 int size = m_ungrouped->filtered.size();
1709 for (int a = 0; a < size; ++a) {
1710 if (!movedToFront.contains(a)) {
1711 newFiltered.push_back(m_ungrouped->filtered[a]);
1712 }
1713 }
1714 m_ungrouped->filtered.swap(newFiltered);
1715 }
1716 return;
1717 }
1718
1719 ///@todo Cache the CodeCompletionModel::BestMatchesCount
1720 for (Group *g : m_rowTable) {
1721 if (g == m_bestMatches) {
1722 continue;
1723 }
1724 for (int a = 0; a < (int)g->filtered.size(); a++) {
1725 ModelRow source = g->filtered[a].sourceRow();
1726
1728
1729 if (v.userType() == QMetaType::Int && v.toInt() > 0) {
1730 // Return the best match with any of the argument-hints
1731
1732 int quality = contextMatchQuality(source);
1733 if (quality > 0) {
1734 matches.insert(quality, qMakePair(v.toInt(), g->filtered[a].sourceRow()));
1735 }
1736 --maxMatches;
1737 }
1738
1739 if (maxMatches < 0) {
1740 break;
1741 }
1742 }
1743 if (maxMatches < 0) {
1744 break;
1745 }
1746 }
1747
1748 // Now choose how many of the matches will be taken. This is done with the rule:
1749 // The count of shown best-matches should equal the average count of their BestMatchesCounts
1750 int cnt = 0;
1751 int matchesSum = 0;
1752 BestMatchMap::const_iterator it = matches.constEnd();
1753 while (it != matches.constBegin()) {
1754 --it;
1755 ++cnt;
1756 matchesSum += (*it).first;
1757 if (cnt > matchesSum / cnt) {
1758 break;
1759 }
1760 }
1761
1762 m_bestMatches->filtered.clear();
1763
1764 it = matches.constEnd();
1765
1766 while (it != matches.constBegin() && cnt > 0) {
1767 --it;
1768 --cnt;
1769
1770 m_bestMatches->filtered.push_back(Item(true, this, HierarchicalModelHandler((*it).second.first), (*it).second));
1771 }
1772
1773 hideOrShowGroup(m_bestMatches);
1774}
1775
1777{
1778 ///@todo delay this
1779 int rc = widget()->argumentHintModel()->rowCount(QModelIndex());
1780 if (rc == 0) {
1781 return;
1782 }
1783
1784 // For now, simply update the whole column 0
1785 QModelIndex start = widget()->argumentHintModel()->index(0, 0);
1786 QModelIndex end = widget()->argumentHintModel()->index(rc - 1, 0);
1787
1788 widget()->argumentHintModel()->emitDataChanged(start, end);
1789}
1790
1791void KateCompletionModel::clearCompletionModels()
1792{
1793 if (m_completionModels.empty()) {
1794 return;
1795 }
1796
1798 for (CodeCompletionModel *model : std::as_const(m_completionModels)) {
1799 model->disconnect(this);
1800 }
1801
1802 m_completionModels.clear();
1803
1804 m_currentMatch.clear();
1805
1806 clearGroups();
1807 endResetModel();
1808}
1809
1810#include "moc_katecompletionmodel.cpp"
Cares about expanding/un-expanding items in a tree-view together with ExpandingDelegate.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Does not request data from index, this only returns local data like highlighting for expanded rows an...
virtual MatchReaction matchingItem(const QModelIndex &matched)
Called whenever an item in the completion-list perfectly matches the current filter text.
virtual bool shouldHideItemsWithEqualNames() const
When multiple completion models are used at the same time, it may happen that multiple models add ite...
An item model for providing code completion, and meta information for enhanced presentation.
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Reimplemented from QAbstractItemModel::rowCount().
ExtraItemDataRoles
Meta information is passed through extra {Qt::ItemDataRole}s.
@ InheritanceDepth
Returns the inheritance depth of the completion.
@ CompletionRole
The model should return a set of CompletionProperties.
@ GroupRole
Using this Role, it is possible to greatly optimize the time needed to process very long completion-l...
@ SetMatchContext
Is requested before MatchQuality(..) is requested.
@ HighlightingMethod
Define which highlighting method will be used:
@ UnimportantItemRole
Return a nonzero value here to enforce sorting the item at the end of the list.
@ BestMatchesCount
This will be requested for each item to ask whether it should be included in computing a best-matches...
@ ArgumentHintDepth
Is this completion-item an argument-hint? The model should return an integral positive number if the ...
@ MatchQuality
If requested, your model should try to determine whether the completion in question is a suitable mat...
@ CustomHighlight
Allows an item to provide custom highlighting.
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
Reimplemented from QAbstractItemModel::index().
This class has the responsibility for filtering, sorting, and manipulating code completion data provi...
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Does not request data from index, this only returns local data like highlighting for expanded rows an...
virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const
Maps from this display-model into the appropriate source code-completion model.
bool indexIsItem(const QModelIndex &index) const override
Should return true if the given row should be painted like a contained item(as opposed to label-rows ...
bool shouldMatchHideCompletionList() const
Returns whether one of the filtered items exactly matches its completion string.
QString commonPrefix(QModelIndex selectedIndex) const
Returns a common prefix for all current visible completion entries If there is no common prefix,...
int contextMatchQuality(const QModelIndex &index) const override
virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const
Maps from an index in a source-model to the index of the item in this display-model.
void rowSelected(const QModelIndex &row) const
This is the code completion's main widget, and also contains the core interface logic.
const QFont & currentFont() const
Access currently used font.
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18n(const char *text, const TYPE &arg...)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
const QList< QKeySequence > & end()
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
void beginInsertRows(const QModelIndex &parent, int first, int last)
void beginRemoveRows(const QModelIndex &parent, int first, int last)
QModelIndex createIndex(int row, int column, const void *ptr) const const
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
virtual QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const const
virtual int rowCount(const QModelIndex &parent) const const=0
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
const QColor & color() const const
bool isLower(char32_t ucs4)
bool isUpper(char32_t ucs4)
char32_t toLower(char32_t ucs4)
void setBold(bool enable)
QPalette palette()
void clear()
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
bool contains(const Key &key) const const
iterator insert(const Key &key, const T &value)
void clear()
bool contains(const AT &value) const const
bool empty() const const
reference front()
bool isEmpty() const const
void push_back(parameter_type value)
qsizetype removeAll(const AT &t)
void reserve(qsizetype size)
qsizetype size() const const
const_iterator cend() const const
void clear()
const_iterator constFind(const Key &key) const const
size_type remove(const Key &key)
T value(const Key &key, const T &defaultValue) const const
int column() const const
QVariant data(int role) const const
void * internalPointer() const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
int row() const const
QModelIndex sibling(int row, int column) const const
void clear()
bool contains(const Key &key, const T &value) const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) const const
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
size_type size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
T qobject_cast(QObject *object)
QObject * sender() const const
const QBrush & toolTipBase() const const
const QBrush & toolTipText() const const
bool isVisible() const const
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
void reserve(qsizetype size)
QString & append(QChar ch)
const QChar at(qsizetype position) const const
const_iterator cbegin() const const
const_iterator cend() const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
const_iterator constEnd() const const
bool isEmpty() const const
bool isNull() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
void push_back(QChar ch)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
void truncate(qsizetype position)
QStringView mid(qsizetype start, qsizetype length) const const
QChar at(qsizetype n) const const
AlignRight
CaseSensitive
DisplayRole
typedef ItemFlags
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QFuture< typename qValueType< Iterator >::value_type > filtered(Iterator begin, Iterator end, KeepFunctor &&filterFunction)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void start()
void stop()
void timeout()
bool canConvert() const const
void * data()
bool isValid() const const
bool toBool() const const
int toInt(bool *ok) const const
QList< QVariant > toList() const const
QString toString() const const
int userType() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:11:26 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.