1 /*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2014 Miquel Sabaté Solà <mikisabate@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8 #include "fakecodecompletiontestmodel.h"
9 #include <QRegularExpression>
10 #include <katecompletionwidget.h>
11 #include <katedocument.h>
12 #include <kateglobal.h>
13 #include <kateview.h>
14 #include <katewordcompletion.h>
15
16 using namespace KTextEditor;
17
FakeCodeCompletionTestModel(KTextEditor::View * parent)18 FakeCodeCompletionTestModel::FakeCodeCompletionTestModel(KTextEditor::View *parent)
19 : KTextEditor::CodeCompletionModel(parent)
20 , m_kateView(qobject_cast<KTextEditor::ViewPrivate *>(parent))
21 , m_kateDoc(parent->document())
22 , m_removeTailOnCompletion(false)
23 , m_failTestOnInvocation(false)
24 , m_wasInvoked(false)
25 {
26 Q_ASSERT(m_kateView);
27 setRowCount(3);
28 cc()->setAutomaticInvocationEnabled(false);
29 cc()->unregisterCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); // would add additional items, we don't want that in tests
30 connect(static_cast<KTextEditor::DocumentPrivate *>(parent->document()),
31 &KTextEditor::DocumentPrivate::textInsertedRange,
32 this,
33 &FakeCodeCompletionTestModel::textInserted);
34 connect(static_cast<KTextEditor::DocumentPrivate *>(parent->document()),
35 &KTextEditor::DocumentPrivate::textRemoved,
36 this,
37 &FakeCodeCompletionTestModel::textRemoved);
38 }
39
setCompletions(const QStringList & completions)40 void FakeCodeCompletionTestModel::setCompletions(const QStringList &completions)
41 {
42 QStringList sortedCompletions = completions;
43 std::sort(sortedCompletions.begin(), sortedCompletions.end());
44 Q_ASSERT(completions == sortedCompletions
45 && "QCompleter seems to sort the items, so it's best to provide them pre-sorted so it's easier to predict the order");
46 setRowCount(sortedCompletions.length());
47 m_completions = completions;
48 }
49
setRemoveTailOnComplete(bool removeTailOnCompletion)50 void FakeCodeCompletionTestModel::setRemoveTailOnComplete(bool removeTailOnCompletion)
51 {
52 m_removeTailOnCompletion = removeTailOnCompletion;
53 }
54
setFailTestOnInvocation(bool failTestOnInvocation)55 void FakeCodeCompletionTestModel::setFailTestOnInvocation(bool failTestOnInvocation)
56 {
57 m_failTestOnInvocation = failTestOnInvocation;
58 }
59
wasInvoked()60 bool FakeCodeCompletionTestModel::wasInvoked()
61 {
62 return m_wasInvoked;
63 }
64
clearWasInvoked()65 void FakeCodeCompletionTestModel::clearWasInvoked()
66 {
67 m_wasInvoked = false;
68 }
69
forceInvocationIfDocTextIs(const QString & desiredDocText)70 void FakeCodeCompletionTestModel::forceInvocationIfDocTextIs(const QString &desiredDocText)
71 {
72 m_forceInvocationIfDocTextIs = desiredDocText;
73 }
74
doNotForceInvocation()75 void FakeCodeCompletionTestModel::doNotForceInvocation()
76 {
77 m_forceInvocationIfDocTextIs.clear();
78 }
79
data(const QModelIndex & index,int role) const80 QVariant FakeCodeCompletionTestModel::data(const QModelIndex &index, int role) const
81 {
82 m_wasInvoked = true;
83 if (m_failTestOnInvocation) {
84 failTest();
85 return QVariant();
86 }
87 // Order is important, here, as the completion widget seems to do its own sorting.
88 if (role == Qt::DisplayRole) {
89 if (index.column() == Name) {
90 return QString(m_completions[index.row()]);
91 }
92 }
93 return QVariant();
94 }
95
executeCompletionItem(KTextEditor::View * view,const KTextEditor::Range & word,const QModelIndex & index) const96 void FakeCodeCompletionTestModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
97 {
98 qDebug() << "word: " << word << "(" << view->document()->text(word) << ")";
99 const Cursor origCursorPos = m_kateView->cursorPosition();
100 const QString textToInsert = m_completions[index.row()];
101 const QString textAfterCursor = view->document()->text(Range(word.end(), Cursor(word.end().line(), view->document()->lineLength(word.end().line()))));
102 view->document()->removeText(Range(word.start(), origCursorPos));
103 const int lengthStillToRemove = word.end().column() - origCursorPos.column();
104 QString actualTextInserted = textToInsert;
105 // Merge brackets?
106 const QString noArgFunctionCallMarker = "()";
107 const QString withArgFunctionCallMarker = "(...)";
108 const bool endedWithSemiColon = textToInsert.endsWith(';');
109 if (textToInsert.contains(noArgFunctionCallMarker) || textToInsert.contains(withArgFunctionCallMarker)) {
110 Q_ASSERT(m_removeTailOnCompletion && "Function completion items without removing tail is not yet supported!");
111 const bool takesArgument = textToInsert.contains(withArgFunctionCallMarker);
112 // The code for a function call to a function taking no arguments.
113 const QString justFunctionName = textToInsert.left(textToInsert.indexOf("("));
114
115 static const QRegularExpression whitespaceThenOpeningBracket("^\\s*(\\()", QRegularExpression::UseUnicodePropertiesOption);
116 const QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(textAfterCursor);
117 int openingBracketPos = -1;
118 if (match.hasMatch()) {
119 openingBracketPos = match.capturedStart(1) + word.start().column() + justFunctionName.length() + 1 + lengthStillToRemove;
120 }
121 const bool mergeOpenBracketWithExisting = (openingBracketPos != -1) && !endedWithSemiColon;
122 // Add the function name, for now: we don't yet know if we'll be adding the "()", too.
123 view->document()->insertText(word.start(), justFunctionName);
124 if (mergeOpenBracketWithExisting) {
125 // Merge with opening bracket.
126 actualTextInserted = justFunctionName;
127 m_kateView->setCursorPosition(Cursor(word.start().line(), openingBracketPos));
128 } else {
129 // Don't merge.
130 const QString afterFunctionName = endedWithSemiColon ? "();" : "()";
131 view->document()->insertText(Cursor(word.start().line(), word.start().column() + justFunctionName.length()), afterFunctionName);
132 if (takesArgument) {
133 // Place the cursor immediately after the opening "(" we just added.
134 m_kateView->setCursorPosition(Cursor(word.start().line(), word.start().column() + justFunctionName.length() + 1));
135 }
136 }
137 } else {
138 // Plain text.
139 view->document()->insertText(word.start(), textToInsert);
140 }
141 if (m_removeTailOnCompletion) {
142 const int tailLength = word.end().column() - origCursorPos.column();
143 const Cursor tailStart = Cursor(word.start().line(), word.start().column() + actualTextInserted.length());
144 const Cursor tailEnd = Cursor(tailStart.line(), tailStart.column() + tailLength);
145 view->document()->removeText(Range(tailStart, tailEnd));
146 }
147 }
148
cc() const149 KTextEditor::CodeCompletionInterface *FakeCodeCompletionTestModel::cc() const
150 {
151 return dynamic_cast<KTextEditor::CodeCompletionInterface *>(const_cast<QObject *>(QObject::parent()));
152 }
153
failTest() const154 void FakeCodeCompletionTestModel::failTest() const
155 {
156 QFAIL("Shouldn't be invoking me!");
157 }
158
textInserted(Document * document,Range range)159 void FakeCodeCompletionTestModel::textInserted(Document *document, Range range)
160 {
161 Q_UNUSED(document);
162 Q_UNUSED(range);
163 checkIfShouldForceInvocation();
164 }
165
textRemoved(Document * document,Range range)166 void FakeCodeCompletionTestModel::textRemoved(Document *document, Range range)
167 {
168 Q_UNUSED(document);
169 Q_UNUSED(range);
170 checkIfShouldForceInvocation();
171 }
172
checkIfShouldForceInvocation()173 void FakeCodeCompletionTestModel::checkIfShouldForceInvocation()
174 {
175 if (m_forceInvocationIfDocTextIs.isEmpty()) {
176 return;
177 }
178
179 if (m_kateDoc->text() == m_forceInvocationIfDocTextIs) {
180 m_kateView->completionWidget()->userInvokedCompletion();
181 BaseTest::waitForCompletionWidgetToActivate(m_kateView);
182 }
183 }
184