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