File indexing completed on 2024-04-28 03:57:09

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2014 Miquel Sabaté Solà <mikisabate@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "fakecodecompletiontestmodel.h"
0009 #include "base.h"
0010 #include <QRegularExpression>
0011 #include <katecompletionwidget.h>
0012 #include <katedocument.h>
0013 #include <kateglobal.h>
0014 #include <kateview.h>
0015 #include <katewordcompletion.h>
0016 
0017 #include <QTest>
0018 
0019 using namespace KTextEditor;
0020 
0021 FakeCodeCompletionTestModel::FakeCodeCompletionTestModel(KTextEditor::View *parent)
0022     : KTextEditor::CodeCompletionModel(parent)
0023     , m_kateView(qobject_cast<KTextEditor::ViewPrivate *>(parent))
0024     , m_kateDoc(parent->document())
0025     , m_removeTailOnCompletion(false)
0026     , m_failTestOnInvocation(false)
0027     , m_wasInvoked(false)
0028 {
0029     Q_ASSERT(m_kateView);
0030     setRowCount(3);
0031     parent->setAutomaticInvocationEnabled(false);
0032     parent->unregisterCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); // would add additional items, we don't want that in tests
0033     connect(static_cast<KTextEditor::DocumentPrivate *>(parent->document()),
0034             &KTextEditor::DocumentPrivate::textInsertedRange,
0035             this,
0036             &FakeCodeCompletionTestModel::textInserted);
0037     connect(static_cast<KTextEditor::DocumentPrivate *>(parent->document()),
0038             &KTextEditor::DocumentPrivate::textRemoved,
0039             this,
0040             &FakeCodeCompletionTestModel::textRemoved);
0041 }
0042 
0043 void FakeCodeCompletionTestModel::setCompletions(const QStringList &completions)
0044 {
0045     QStringList sortedCompletions = completions;
0046     std::sort(sortedCompletions.begin(), sortedCompletions.end());
0047     Q_ASSERT(completions == sortedCompletions
0048              && "QCompleter seems to sort the items, so it's best to provide them pre-sorted so it's easier to predict the order");
0049     setRowCount(sortedCompletions.length());
0050     m_completions = completions;
0051 }
0052 
0053 void FakeCodeCompletionTestModel::setRemoveTailOnComplete(bool removeTailOnCompletion)
0054 {
0055     m_removeTailOnCompletion = removeTailOnCompletion;
0056 }
0057 
0058 void FakeCodeCompletionTestModel::setFailTestOnInvocation(bool failTestOnInvocation)
0059 {
0060     m_failTestOnInvocation = failTestOnInvocation;
0061 }
0062 
0063 bool FakeCodeCompletionTestModel::wasInvoked()
0064 {
0065     return m_wasInvoked;
0066 }
0067 
0068 void FakeCodeCompletionTestModel::clearWasInvoked()
0069 {
0070     m_wasInvoked = false;
0071 }
0072 
0073 void FakeCodeCompletionTestModel::forceInvocationIfDocTextIs(const QString &desiredDocText)
0074 {
0075     m_forceInvocationIfDocTextIs = desiredDocText;
0076 }
0077 
0078 void FakeCodeCompletionTestModel::doNotForceInvocation()
0079 {
0080     m_forceInvocationIfDocTextIs.clear();
0081 }
0082 
0083 QVariant FakeCodeCompletionTestModel::data(const QModelIndex &index, int role) const
0084 {
0085     m_wasInvoked = true;
0086     if (m_failTestOnInvocation) {
0087         failTest();
0088         return QVariant();
0089     }
0090     // Order is important, here, as the completion widget seems to do its own sorting.
0091     if (role == Qt::DisplayRole) {
0092         if (index.column() == Name) {
0093             return QString(m_completions[index.row()]);
0094         }
0095     }
0096     return QVariant();
0097 }
0098 
0099 void FakeCodeCompletionTestModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
0100 {
0101     qDebug() << "word: " << word << "(" << view->document()->text(word) << ")";
0102     const Cursor origCursorPos = m_kateView->cursorPosition();
0103     const QString textToInsert = m_completions[index.row()];
0104     const QString textAfterCursor = view->document()->text(Range(word.end(), Cursor(word.end().line(), view->document()->lineLength(word.end().line()))));
0105     view->document()->removeText(Range(word.start(), origCursorPos));
0106     const int lengthStillToRemove = word.end().column() - origCursorPos.column();
0107     QString actualTextInserted = textToInsert;
0108     // Merge brackets?
0109     const QString noArgFunctionCallMarker = QStringLiteral("()");
0110     const QString withArgFunctionCallMarker = QStringLiteral("(...)");
0111     const bool endedWithSemiColon = textToInsert.endsWith(u';');
0112     if (textToInsert.contains(noArgFunctionCallMarker) || textToInsert.contains(withArgFunctionCallMarker)) {
0113         Q_ASSERT(m_removeTailOnCompletion && "Function completion items without removing tail is not yet supported!");
0114         const bool takesArgument = textToInsert.contains(withArgFunctionCallMarker);
0115         // The code for a function call to a function taking no arguments.
0116         const QString justFunctionName = textToInsert.left(textToInsert.indexOf(QLatin1String("(")));
0117 
0118         static const QRegularExpression whitespaceThenOpeningBracket(QStringLiteral("^\\s*(\\()"), QRegularExpression::UseUnicodePropertiesOption);
0119         const QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(textAfterCursor);
0120         int openingBracketPos = -1;
0121         if (match.hasMatch()) {
0122             openingBracketPos = match.capturedStart(1) + word.start().column() + justFunctionName.length() + 1 + lengthStillToRemove;
0123         }
0124         const bool mergeOpenBracketWithExisting = (openingBracketPos != -1) && !endedWithSemiColon;
0125         // Add the function name, for now: we don't yet know if we'll be adding the "()", too.
0126         view->document()->insertText(word.start(), justFunctionName);
0127         if (mergeOpenBracketWithExisting) {
0128             // Merge with opening bracket.
0129             actualTextInserted = justFunctionName;
0130             m_kateView->setCursorPosition(Cursor(word.start().line(), openingBracketPos));
0131         } else {
0132             // Don't merge.
0133             const QString afterFunctionName = endedWithSemiColon ? QStringLiteral("();") : QStringLiteral("()");
0134             view->document()->insertText(Cursor(word.start().line(), word.start().column() + justFunctionName.length()), afterFunctionName);
0135             if (takesArgument) {
0136                 // Place the cursor immediately after the opening "(" we just added.
0137                 m_kateView->setCursorPosition(Cursor(word.start().line(), word.start().column() + justFunctionName.length() + 1));
0138             }
0139         }
0140     } else {
0141         // Plain text.
0142         view->document()->insertText(word.start(), textToInsert);
0143     }
0144     if (m_removeTailOnCompletion) {
0145         const int tailLength = word.end().column() - origCursorPos.column();
0146         const Cursor tailStart = Cursor(word.start().line(), word.start().column() + actualTextInserted.length());
0147         const Cursor tailEnd = Cursor(tailStart.line(), tailStart.column() + tailLength);
0148         view->document()->removeText(Range(tailStart, tailEnd));
0149     }
0150 }
0151 
0152 void FakeCodeCompletionTestModel::failTest() const
0153 {
0154     QFAIL("Shouldn't be invoking me!");
0155 }
0156 
0157 void FakeCodeCompletionTestModel::textInserted(Document *document, Range range)
0158 {
0159     Q_UNUSED(document);
0160     Q_UNUSED(range);
0161     checkIfShouldForceInvocation();
0162 }
0163 
0164 void FakeCodeCompletionTestModel::textRemoved(Document *document, Range range)
0165 {
0166     Q_UNUSED(document);
0167     Q_UNUSED(range);
0168     checkIfShouldForceInvocation();
0169 }
0170 
0171 void FakeCodeCompletionTestModel::checkIfShouldForceInvocation()
0172 {
0173     if (m_forceInvocationIfDocTextIs.isEmpty()) {
0174         return;
0175     }
0176 
0177     if (m_kateDoc->text() == m_forceInvocationIfDocTextIs) {
0178         m_kateView->completionWidget()->userInvokedCompletion();
0179         BaseTest::waitForCompletionWidgetToActivate(m_kateView);
0180     }
0181 }
0182 
0183 #include "moc_fakecodecompletiontestmodel.cpp"