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: index.parent());
80 }
81 if (m_model->rowCount(parent: index) != 0) {
82 takeRole(index);
83 }
84}
85
86int HierarchicalModelHandler::inheritanceDepth(const QModelIndex &i) const
87{
88 return getData(role: CodeCompletionModel::InheritanceDepth, index: i).toInt();
89}
90
91void HierarchicalModelHandler::takeRole(const QModelIndex &index)
92{
93 QVariant v = index.data(arole: CodeCompletionModel::GroupRole);
94 if (v.isValid() && v.canConvert<int>()) {
95 QVariant value = index.data(arole: v.toInt());
96 if (v.toInt() == Qt::DisplayRole) {
97 m_customGroup = index.data(arole: Qt::DisplayRole).toString();
98 QVariant sortingKey = index.data(arole: CodeCompletionModel::InheritanceDepth);
99 if (sortingKey.canConvert<int>()) {
100 m_groupSortingKey = sortingKey.toInt();
101 }
102 } else {
103 auto role = (CodeCompletionModel::ExtraItemDataRoles)v.toInt();
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(first: m_roleValues.begin(), last: m_roleValues.end(), pred: [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(arole: 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(first: m_roleValues.begin(), last: m_roleValues.end(), pred: [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(x: {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(sender: m_updateBestMatchesTimer, signal: &QTimer::timeout, context: this, slot: &KateCompletionModel::updateBestMatches);
151
152 m_groupHash.insert(key: 0, value: m_ungrouped);
153 m_groupHash.insert(key: -1, value: m_argumentHints);
154 m_groupHash.insert(key: BestMatchesProperty, value: 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(child: 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(proxyIndex: createIndex(arow: index.row(), acolumn: column, adata: index.internalPointer()));
204 text.append(s: sourceIndex.data(arole: role).toString());
205 }
206
207 return text;
208 }
209
210 if (role == CodeCompletionModel::HighlightingMethod) {
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(proxyIndex: createIndex(arow: index.row(), acolumn: column, adata: index.internalPointer()));
215 QVariant method = sourceIndex.data(arole: CodeCompletionModel::HighlightingMethod);
216 if (method.userType() == QMetaType::Int && method.toInt() == CodeCompletionModel::CustomHighlighting) {
217 return QVariant(CodeCompletionModel::CustomHighlighting);
218 }
219 }
220 return QVariant();
221 }
222 if (role == CodeCompletionModel::CustomHighlight) {
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(asize: columns.size());
229 for (int column : columns) {
230 strings << mapToSource(proxyIndex: createIndex(arow: index.row(), acolumn: column, adata: index.internalPointer())).data(arole: Qt::DisplayRole).toString();
231 }
232
233 QList<QVariantList> highlights;
234
235 // Collect custom-highlightings
236 highlights.reserve(asize: columns.size());
237 for (int column : columns) {
238 highlights << mapToSource(proxyIndex: createIndex(arow: index.row(), acolumn: column, adata: index.internalPointer())).data(arole: CodeCompletionModel::CustomHighlight).toList();
239 }
240
241 return mergeCustomHighlighting(strings, highlights, gapBetweenStrings: 0);
242 }
243
244 QVariant v = mapToSource(proxyIndex: index).data(arole: 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
271 case Qt::ForegroundRole:
272 return QApplication::palette().toolTipText().color();
273 case Qt::BackgroundRole:
274 return QApplication::palette().toolTipBase().color();
275 }
276 }
277
278 return QVariant();
279}
280
281int KateCompletionModel::contextMatchQuality(const QModelIndex &index) const
282{
283 if (!index.isValid()) {
284 return 0;
285 }
286 Group *g = groupOfParent(child: index);
287 if (!g || g->filtered.size() < (size_t)index.row()) {
288 return 0;
289 }
290
291 return contextMatchQuality(sourceRow: 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 : std::as_const(t&: 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
308 QVariant depth = hintIndex.data(arole: CodeCompletionModel::ArgumentHintDepth);
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
313 hintIndex.data(arole: CodeCompletionModel::SetMatchContext);
314
315 QVariant matchQuality = realIndex.data(arole: 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(arole: 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(child: index)) {
344 return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
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(value1: static_cast<CodeCompletionModel *>(const_cast<QAbstractItemModel *>(index.model())), value2: 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(index: 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(index: 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(arow: row, acolumn: column, adata: 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(arow: row, acolumn: column, aid: 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(index: 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(arow: row, acolumn: 0, adata: g);
468}
469
470QModelIndex KateCompletionModel::indexForGroup(Group *g) const
471{
472 if (!hasGroups()) {
473 return QModelIndex();
474 }
475
476 auto it = std::find(first: m_rowTable.begin(), last: m_rowTable.end(), val: g);
477 if (it == m_rowTable.end()) {
478 return QModelIndex();
479 }
480 int row = std::distance(first: m_rowTable.begin(), last: it);
481 return createIndex(arow: row, acolumn: 0, aid: 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(first: std::remove_if(first: m_rowTable.begin(),
492 last: m_rowTable.end(),
493 pred: [this](Group *g) {
494 return (g == m_ungrouped) || (g == m_argumentHints) || (g == m_bestMatches);
495 }),
496 last: m_rowTable.end());
497
498 m_emptyGroups.erase(first: std::remove_if(first: m_emptyGroups.begin(),
499 last: m_emptyGroups.end(),
500 pred: [this](Group *g) {
501 return (g == m_ungrouped) || (g == m_argumentHints) || (g == m_bestMatches);
502 }),
503 last: m_emptyGroups.end());
504
505 qDeleteAll(c: m_rowTable);
506 qDeleteAll(c: m_emptyGroups);
507 m_rowTable.clear();
508 m_emptyGroups.clear();
509 m_groupHash.clear();
510 m_customGroupHash.clear();
511
512 m_emptyGroups.insert(position: m_emptyGroups.end(), l: {m_ungrouped, m_argumentHints, m_bestMatches});
513
514 m_groupHash.insert(key: 0, value: m_ungrouped);
515 m_groupHash.insert(key: -1, value: m_argumentHints);
516 m_groupHash.insert(key: BestMatchesProperty, value: 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(parent: i) == 0) {
526 // Leaf node, create an item
527 ret.insert(x: 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(index: i);
531 for (int a = 0; a < model->rowCount(parent: i); a++) {
532 ret.merge(source: createItems(handler: handler, i: model->index(row: a, column: 0, parent: 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(parent: i) == 0) {
544 // Leaf node, delete the item
545 Group *g = groupForIndex(index: mapFromSource(sourceIndex: i));
546 ret.insert(x: g);
547 g->removeItem(row: 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(parent: i); a++) {
551 ret.merge(source: deleteItems(i: i.model()->index(row: a, column: 0, parent: i)));
552 }
553 }
554
555 return ret;
556}
557
558void KateCompletionModel::createGroups()
559{
560 beginResetModel();
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(t&: m_completionModels)) {
568 has_groups |= sourceModel->hasGroups();
569 for (int i = 0; i < sourceModel->rowCount(); ++i) {
570 groups.merge(source: createItems(handler: HierarchicalModelHandler(sourceModel), i: sourceModel->index(row: i, column: 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(first: g->filtered.begin(), last: g->filtered.end(), comp: [this](const Item &l, const Item &r) {
581 return l.lessThan(model: this, rhs: r);
582 });
583 }
584
585 m_hasGroups = has_groups;
586
587 // debugStats();
588
589 for (Group *g : std::as_const(t&: m_rowTable)) {
590 hideOrShowGroup(g);
591 }
592
593 for (Group *g : std::as_const(t&: m_emptyGroups)) {
594 hideOrShowGroup(g);
595 }
596
597 makeGroupItemsUnique();
598
599 updateBestMatches();
600 endResetModel();
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(role: CodeCompletionModel::CompletionRole, index: sourceIndex).toInt();
608
609 int argumentHintDepth = handler.getData(role: CodeCompletionModel::ArgumentHintDepth, index: 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(key: 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(x: g);
623 m_customGroupHash.insert(key: customGroup, value: g);
624 }
625 } else {
626 g = fetchGroup(attribute: completionFlags, forceGrouping: 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(model: this);
634 }
635
636 g->addItem(i: 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(index: parent);
646 }
647
648 GroupSet affectedGroups;
649 for (int i = start; i <= end; ++i) {
650 affectedGroups.merge(source: createItems(handler: handler, i: handler.model()->index(row: i, column: 0, parent), /* notifyModel= */ true));
651 }
652
653 for (auto g : affectedGroups) {
654 hideOrShowGroup(g, notifyModel: 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(row: i, column: 0, parent);
665 affectedGroups.merge(source: deleteItems(i: index));
666 }
667
668 for (auto g : affectedGroups) {
669 hideOrShowGroup(g, notifyModel: 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(key: groupingAttribute)) {
686 return m_groupHash.value(key: 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(s: QLatin1String(", "));
715 }
716
717 title.append(s: at);
718 }
719
720 Group *ret = new Group(title, attribute, this);
721 ret->scope = QString();
722
723 m_emptyGroups.push_back(x: ret);
724 m_groupHash.insert(key: groupingAttribute, value: 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(child: 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
750QModelIndex KateCompletionModel::parent(const QModelIndex &index) const
751{
752 if (!index.isValid()) {
753 return QModelIndex();
754 }
755
756 if (Group *g = groupOfParent(child: index)) {
757 if (!hasGroups()) {
758 Q_ASSERT(g == m_ungrouped);
759 return QModelIndex();
760 }
761
762 auto it = std::find(first: m_rowTable.begin(), last: m_rowTable.end(), val: 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(first: m_rowTable.begin(), last: it);
768 return createIndex(arow: row, acolumn: 0, aid: 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(index: 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
802QModelIndex KateCompletionModel::mapToSource(const QModelIndex &proxyIndex) const
803{
804 if (!proxyIndex.isValid()) {
805 return QModelIndex();
806 }
807
808 if (Group *g = groupOfParent(child: proxyIndex)) {
809 if (std::find(first: m_rowTable.begin(), last: m_rowTable.end(), val: 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(arow: source.second.row(), acolumn: proxyIndex.column());
817 } else {
818 qCDebug(LOG_KTE) << "Invalid proxy-index";
819 }
820 }
821
822 return QModelIndex();
823}
824
825QModelIndex KateCompletionModel::mapFromSource(const QModelIndex &sourceIndex) const
826{
827 if (!sourceIndex.isValid()) {
828 return QModelIndex();
829 }
830
831 if (!hasGroups()) {
832 return index(row: m_ungrouped->rowOf(item: modelRowPair(index: sourceIndex)), column: sourceIndex.column(), parent: QModelIndex());
833 }
834
835 for (Group *g : std::as_const(t: m_rowTable)) {
836 int row = g->rowOf(item: modelRowPair(index: sourceIndex));
837 if (row != -1) {
838 return index(row, column: sourceIndex.column(), parent: indexForGroup(g));
839 }
840 }
841
842 // Copied from above
843 for (Group *g : std::as_const(t: m_emptyGroups)) {
844 int row = g->rowOf(item: modelRowPair(index: sourceIndex));
845 if (row != -1) {
846 return index(row, column: sourceIndex.column(), parent: indexForGroup(g));
847 }
848 }
849
850 return QModelIndex();
851}
852
853void KateCompletionModel::setCurrentCompletion(QMap<KTextEditor::CodeCompletionModel *, QString> currentMatch)
854{
855 beginResetModel();
856
857 m_currentMatch = currentMatch;
858
859 if (!hasGroups()) {
860 changeCompletions(g: m_ungrouped);
861 } else {
862 for (Group *g : std::as_const(t&: m_rowTable)) {
863 if (g != m_argumentHints) {
864 changeCompletions(g);
865 }
866 }
867 for (Group *g : std::as_const(t&: 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
877 endResetModel();
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(x: m_ungrouped);
886
887 for (Group *g : std::as_const(t&: groups)) {
888 for (const Item &item : std::as_const(t&: g->filtered)) {
889 uint startPos = currentCompletion(model: item.sourceRow().first).length();
890 const QString candidate = item.name().mid(position: startPos);
891
892 if (!candidate.startsWith(s: 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(pos: candidate.length());
905
906 for (int a = 0; a < commonPrefix.length(); ++a) {
907 if (commonPrefix[a] != candidate[a]) {
908 commonPrefix.truncate(pos: a);
909 break;
910 }
911 }
912 }
913 }
914 }
915
916 return commonPrefix;
917}
918
919QString KateCompletionModel::commonPrefix(QModelIndex selectedIndex) const
920{
921 QString commonPrefix = commonPrefixInternal(forcePrefix: QString());
922
923 if (commonPrefix.isEmpty() && selectedIndex.isValid()) {
924 Group *g = m_ungrouped;
925 if (hasGroups()) {
926 g = groupOfParent(child: 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(model: item.sourceRow().first).length();
933 commonPrefix = commonPrefixInternal(forcePrefix: item.name().mid(position: matchLength).left(n: 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(first: g->prefilter.begin(), last: g->prefilter.end(), result: std::back_inserter(x&: g->filtered), pred: [this](Item &item) {
946 return !item.match(model: 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(msec: 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(first: m_rowTable.begin(), last: m_rowTable.end(), val: g);
1001
1002 if (it != m_rowTable.end()) {
1003 int row = std::distance(first: m_rowTable.begin(), last: it);
1004 if (hasGroups() && notifyModel) {
1005 beginRemoveRows(parent: QModelIndex(), first: row, last: row);
1006 }
1007 m_rowTable.erase(position: it);
1008 if (hasGroups() && notifyModel) {
1009 endRemoveRows();
1010 }
1011 m_emptyGroups.push_back(x: 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(other: m_rowTable[a])) {
1025 row = a;
1026 break;
1027 }
1028 row = a + 1;
1029 }
1030
1031 if (notifyModel) {
1032 if (hasGroups()) {
1033 beginInsertRows(parent: QModelIndex(), first: row, last: row);
1034 } else {
1035 beginInsertRows(parent: QModelIndex(), first: 0, last: g->filtered.size());
1036 }
1037 }
1038 m_rowTable.insert(position: m_rowTable.begin() + row, x: g);
1039 if (notifyModel) {
1040 endInsertRows();
1041 }
1042 m_emptyGroups.erase(first: std::remove(first: m_emptyGroups.begin(), last: m_emptyGroups.end(), value: g), last: m_emptyGroups.end());
1043 }
1044 }
1045}
1046
1047bool KateCompletionModel::indexIsItem(const QModelIndex &index) const
1048{
1049 if (!hasGroups()) {
1050 return true;
1051 }
1052
1053 if (groupOfParent(child: 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 : std::as_const(t&: 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(value: 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(value: 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(role: CodeCompletionModel::InheritanceDepth, index: m_sourceRow.second).toInt();
1162 m_unimportant = handler.getData(role: CodeCompletionModel::UnimportantItemRole, index: m_sourceRow.second).toBool();
1163
1164 QModelIndex nameSibling = sr.second.sibling(arow: sr.second.row(), acolumn: CodeCompletionModel::Name);
1165 m_nameColumn = nameSibling.data(arole: Qt::DisplayRole).toString();
1166
1167 if (doInitialMatch) {
1168 match(model: 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(key: rhs.m_sourceRow.first);
1198 if (it != model->m_currentMatch.cend()) {
1199 const QString &filter = it.value();
1200 bool thisStartWithFilter = m_nameColumn.startsWith(s: filter, cs: Qt::CaseSensitive);
1201 bool rhsStartsWithFilter = rhs.m_nameColumn.startsWith(s: filter, cs: 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(s1: m_nameColumn, s2: rhs.m_nameColumn, cs: 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(g: this);
1234 }
1235
1236 if (notifyModel) {
1237 auto comp = [this](const Item &left, const Item &right) {
1238 return left.lessThan(model, rhs: right);
1239 };
1240 prefilter.insert(position: std::upper_bound(first: prefilter.begin(), last: prefilter.end(), val: i, comp: comp), x: i);
1241 } else {
1242 prefilter.push_back(x: 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, rhs: right);
1249 };
1250 auto it = std::upper_bound(first: filtered.begin(), last: filtered.end(), val: i, comp: comp);
1251 const auto rowNumber = it - filtered.begin();
1252 model->beginInsertRows(parent: groupIndex, first: rowNumber, last: rowNumber);
1253 filtered.insert(position: it, x: i);
1254 } else {
1255 // we will sort it later
1256 filtered.push_back(x: 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(item: row);
1270 if (index != -1) {
1271 model->beginRemoveRows(parent: model->indexForGroup(g: this), first: index, last: index);
1272 filtered.erase(position: filtered.begin() + index);
1273 }
1274
1275 prefilter.erase(position: 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, rhs: right);
1304 };
1305 std::stable_sort(first: filtered.begin(), last: filtered.end(), comp: comp);
1306 model->hideOrShowGroup(g: this);
1307}
1308
1309void KateCompletionModel::resort()
1310{
1311 for (Group *g : std::as_const(t&: m_rowTable)) {
1312 g->resort();
1313 }
1314
1315 for (Group *g : std::as_const(t&: 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
1340bool KateCompletionModel::shouldMatchHideCompletionList() const
1341{
1342 // @todo Make this faster
1343
1344 bool doHide = false;
1345 CodeCompletionModel *hideModel = nullptr;
1346
1347 for (Group *group : std::as_const(t: m_rowTable)) {
1348 for (const Item &item : std::as_const(t&: group->filtered)) {
1349 if (item.haveExactMatch()) {
1350 KTextEditor::CodeCompletionModelControllerInterface *iface3 =
1351 qobject_cast<KTextEditor::CodeCompletionModelControllerInterface *>(object: item.sourceRow().first);
1352 bool hide = false;
1353 if (!iface3) {
1354 hide = true;
1355 }
1356 if (iface3
1357 && iface3->matchingItem(matched: 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 : std::as_const(t: m_rowTable)) {
1371 for (const Item &item : std::as_const(t&: 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(pos: firstLetter);
1406
1407 if (toLower(c: wordView.at(n: 0)) != toLower(c: typed.at(i: 0))) {
1408 return false;
1409 }
1410
1411 const auto res = KFuzzyMatcher::match(pattern: typed, str: 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: i - 1);
1428 if (!(prev == QLatin1Char('_') || (c.isUpper() && !prev.isUpper()))) {
1429 continue;
1430 }
1431 if (QStringView(word).mid(pos: i).startsWith(s: typed, cs: 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(model: 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(s: 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(word: m_nameColumn, typed: 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(word: m_nameColumn, typed: 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(key: model);
1499}
1500
1501void KateCompletionModel::addCompletionModel(KTextEditor::CodeCompletionModel *model)
1502{
1503 if (m_completionModels.contains(t: model)) {
1504 return;
1505 }
1506
1507 m_completionModels.push_back(t: model);
1508
1509 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::rowsInserted, context: this, slot: &KateCompletionModel::slotRowsInserted);
1510 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::rowsRemoved, context: this, slot: &KateCompletionModel::slotRowsRemoved);
1511 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::modelReset, context: this, slot: &KateCompletionModel::slotModelReset);
1512
1513 // This performs the reset
1514 createGroups();
1515}
1516
1517void KateCompletionModel::setCompletionModel(KTextEditor::CodeCompletionModel *model)
1518{
1519 clearCompletionModels();
1520 addCompletionModel(model);
1521}
1522
1523void KateCompletionModel::setCompletionModels(const QList<KTextEditor::CodeCompletionModel *> &models)
1524{
1525 // if (m_completionModels == models)
1526 // return;
1527
1528 clearCompletionModels();
1529
1530 m_completionModels = models;
1531
1532 for (KTextEditor::CodeCompletionModel *model : models) {
1533 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::rowsInserted, context: this, slot: &KateCompletionModel::slotRowsInserted);
1534 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::rowsRemoved, context: this, slot: &KateCompletionModel::slotRowsRemoved);
1535 connect(sender: model, signal: &KTextEditor::CodeCompletionModel::modelReset, context: this, slot: &KateCompletionModel::slotModelReset);
1536 }
1537
1538 // This performs the reset
1539 createGroups();
1540}
1541
1542QList<KTextEditor::CodeCompletionModel *> KateCompletionModel::completionModels() const
1543{
1544 return m_completionModels;
1545}
1546
1547void KateCompletionModel::removeCompletionModel(CodeCompletionModel *model)
1548{
1549 if (!model || !m_completionModels.contains(t: model)) {
1550 return;
1551 }
1552
1553 bool willCreateGroups = (m_completionModels.size() - 1) > 0;
1554
1555 if (!willCreateGroups) {
1556 beginResetModel();
1557 }
1558 m_currentMatch.remove(key: model);
1559
1560 clearGroups();
1561
1562 model->disconnect(receiver: this);
1563
1564 m_completionModels.removeAll(t: model);
1565 if (!willCreateGroups) {
1566 endResetModel();
1567 }
1568
1569 if (willCreateGroups) {
1570 // This performs the reset
1571 createGroups();
1572 }
1573}
1574
1575void KateCompletionModel::makeGroupItemsUnique(bool onlyFiltered)
1576{
1577 struct FilterItems {
1578 FilterItems(KateCompletionModel &model, const QList<KTextEditor::CodeCompletionModel *> &needShadowing)
1579 : m_model(model)
1580 , m_needShadowing(needShadowing)
1581 {
1582 }
1583
1584 QHash<QString, CodeCompletionModel *> had;
1585 KateCompletionModel &m_model;
1586 const QList<KTextEditor::CodeCompletionModel *> &m_needShadowing;
1587
1588 void filter(std::vector<Item> &items)
1589 {
1590 std::vector<Item> temp;
1591 temp.reserve(n: items.size());
1592 for (const Item &item : items) {
1593 auto it = had.constFind(key: item.name());
1594 if (it != had.constEnd() && *it != item.sourceRow().first && m_needShadowing.contains(t: item.sourceRow().first)) {
1595 continue;
1596 }
1597
1598 had.insert(key: item.name(), value: item.sourceRow().first);
1599 temp.push_back(x: item);
1600 }
1601 items.swap(x&: temp);
1602 }
1603
1604 void filter(Group *group, bool onlyFiltered)
1605 {
1606 if (group->prefilter.size() == group->filtered.size()) {
1607 // Filter only once
1608 filter(items&: group->filtered);
1609 if (!onlyFiltered) {
1610 group->prefilter = group->filtered;
1611 }
1612 } else {
1613 // Must filter twice
1614 filter(items&: group->filtered);
1615 if (!onlyFiltered) {
1616 filter(items&: group->prefilter);
1617 }
1618 }
1619
1620 if (group->filtered.empty()) {
1621 m_model.hideOrShowGroup(g: group);
1622 }
1623 }
1624 };
1625
1626 QList<KTextEditor::CodeCompletionModel *> needShadowing;
1627 for (KTextEditor::CodeCompletionModel *model : std::as_const(t&: m_completionModels)) {
1628 KTextEditor::CodeCompletionModelControllerInterface *v4 = qobject_cast<KTextEditor::CodeCompletionModelControllerInterface *>(object: model);
1629 if (v4 && v4->shouldHideItemsWithEqualNames()) {
1630 needShadowing.push_back(t: model);
1631 }
1632 }
1633
1634 if (needShadowing.isEmpty()) {
1635 return;
1636 }
1637
1638 FilterItems filter(*this, needShadowing);
1639
1640 filter.filter(group: m_ungrouped, onlyFiltered);
1641
1642 for (Group *group : std::as_const(t&: m_rowTable)) {
1643 filter.filter(group, onlyFiltered);
1644 }
1645}
1646
1647// Updates the best-matches group
1648void KateCompletionModel::updateBestMatches()
1649{
1650 // We cannot do too many operations here, because they are all executed
1651 // whenever a character is added. Would be nice if we could split the
1652 // operations up somewhat using a timer.
1653 int maxMatches = 300;
1654
1655 m_updateBestMatchesTimer->stop();
1656 // Maps match-qualities to ModelRows paired together with the BestMatchesCount returned by the items.
1657 typedef QMultiMap<int, QPair<int, ModelRow>> BestMatchMap;
1658 BestMatchMap matches;
1659
1660 if (!hasGroups()) {
1661 // If there is no grouping, just change the order of the items, moving the best matching ones to the front
1662 QMultiMap<int, int> rowsForQuality;
1663
1664 int row = 0;
1665 for (const Item &item : m_ungrouped->filtered) {
1666 ModelRow source = item.sourceRow();
1667
1668 QVariant v = source.second.data(arole: CodeCompletionModel::BestMatchesCount);
1669
1670 if (v.userType() == QMetaType::Int && v.toInt() > 0) {
1671 int quality = contextMatchQuality(source);
1672 if (quality > 0) {
1673 rowsForQuality.insert(key: quality, value: row);
1674 }
1675 }
1676
1677 ++row;
1678 --maxMatches;
1679 if (maxMatches < 0) {
1680 break;
1681 }
1682 }
1683
1684 if (!rowsForQuality.isEmpty()) {
1685 // Rewrite m_ungrouped->filtered in a new order
1686 QSet<int> movedToFront;
1687 std::vector<Item> newFiltered;
1688 newFiltered.reserve(n: rowsForQuality.size());
1689 movedToFront.reserve(asize: rowsForQuality.size());
1690 for (auto it = rowsForQuality.constBegin(); it != rowsForQuality.constEnd(); ++it) {
1691 newFiltered.push_back(x: m_ungrouped->filtered[it.value()]);
1692 movedToFront.insert(value: it.value());
1693 }
1694 std::reverse(first: newFiltered.begin(), last: newFiltered.end());
1695
1696 int size = m_ungrouped->filtered.size();
1697 for (int a = 0; a < size; ++a) {
1698 if (!movedToFront.contains(value: a)) {
1699 newFiltered.push_back(x: m_ungrouped->filtered[a]);
1700 }
1701 }
1702 m_ungrouped->filtered.swap(x&: newFiltered);
1703 }
1704 return;
1705 }
1706
1707 ///@todo Cache the CodeCompletionModel::BestMatchesCount
1708 for (Group *g : std::as_const(t&: m_rowTable)) {
1709 if (g == m_bestMatches) {
1710 continue;
1711 }
1712 for (int a = 0; a < (int)g->filtered.size(); a++) {
1713 ModelRow source = g->filtered[a].sourceRow();
1714
1715 QVariant v = source.second.data(arole: CodeCompletionModel::BestMatchesCount);
1716
1717 if (v.userType() == QMetaType::Int && v.toInt() > 0) {
1718 // Return the best match with any of the argument-hints
1719
1720 int quality = contextMatchQuality(source);
1721 if (quality > 0) {
1722 matches.insert(key: quality, value: qMakePair(value1: v.toInt(), value2: g->filtered[a].sourceRow()));
1723 }
1724 --maxMatches;
1725 }
1726
1727 if (maxMatches < 0) {
1728 break;
1729 }
1730 }
1731 if (maxMatches < 0) {
1732 break;
1733 }
1734 }
1735
1736 // Now choose how many of the matches will be taken. This is done with the rule:
1737 // The count of shown best-matches should equal the average count of their BestMatchesCounts
1738 int cnt = 0;
1739 int matchesSum = 0;
1740 BestMatchMap::const_iterator it = matches.constEnd();
1741 while (it != matches.constBegin()) {
1742 --it;
1743 ++cnt;
1744 matchesSum += (*it).first;
1745 if (cnt > matchesSum / cnt) {
1746 break;
1747 }
1748 }
1749
1750 m_bestMatches->filtered.clear();
1751
1752 it = matches.constEnd();
1753
1754 while (it != matches.constBegin() && cnt > 0) {
1755 --it;
1756 --cnt;
1757
1758 m_bestMatches->filtered.push_back(x: Item(true, this, HierarchicalModelHandler((*it).second.first), (*it).second));
1759 }
1760
1761 hideOrShowGroup(g: m_bestMatches);
1762}
1763
1764void KateCompletionModel::rowSelected(const QModelIndex & /*row*/) const
1765{
1766 ///@todo delay this
1767 int rc = widget()->argumentHintModel()->rowCount(parent: QModelIndex());
1768 if (rc == 0) {
1769 return;
1770 }
1771
1772 // For now, simply update the whole column 0
1773 QModelIndex start = widget()->argumentHintModel()->index(row: 0, column: 0);
1774 QModelIndex end = widget()->argumentHintModel()->index(row: rc - 1, column: 0);
1775
1776 widget()->argumentHintModel()->emitDataChanged(start, end);
1777}
1778
1779void KateCompletionModel::clearCompletionModels()
1780{
1781 if (m_completionModels.empty()) {
1782 return;
1783 }
1784
1785 beginResetModel();
1786 for (CodeCompletionModel *model : std::as_const(t&: m_completionModels)) {
1787 model->disconnect(receiver: this);
1788 }
1789
1790 m_completionModels.clear();
1791
1792 m_currentMatch.clear();
1793
1794 clearGroups();
1795 endResetModel();
1796}
1797
1798#include "moc_katecompletionmodel.cpp"
1799

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