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

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 "base.h"
0009 #include "vimode/globalstate.h"
0010 #include "vimode/macros.h"
0011 #include "vimode/mappings.h"
0012 #include <inputmode/kateviinputmode.h>
0013 #include <kateconfig.h>
0014 #include <katedocument.h>
0015 #include <kateundomanager.h>
0016 #include <kateview.h>
0017 #include <vimode/emulatedcommandbar/emulatedcommandbar.h>
0018 
0019 #include <QMainWindow>
0020 #include <QTest>
0021 #include <QVBoxLayout>
0022 #include <inputmodemanager.h>
0023 
0024 using namespace KateVi;
0025 using namespace KTextEditor;
0026 
0027 // BEGIN: BaseTest
0028 
0029 BaseTest::BaseTest()
0030 {
0031     kate_view = nullptr;
0032     kate_document = nullptr;
0033 
0034     mainWindow = new QMainWindow;
0035     auto centralWidget = new QWidget();
0036     mainWindowLayout = new QVBoxLayout(centralWidget);
0037     mainWindow->setCentralWidget(centralWidget);
0038     mainWindow->resize(640, 480);
0039 
0040     m_codesToModifiers = {
0041         {QStringLiteral("ctrl"), CONTROL_MODIFIER},
0042         {QStringLiteral("alt"), Qt::AltModifier},
0043         {QStringLiteral("meta"), META_MODIFIER},
0044         {QStringLiteral("keypad"), Qt::KeypadModifier},
0045     };
0046 
0047     m_codesToSpecialKeys = {
0048         {QStringLiteral("backspace"), Qt::Key_Backspace},
0049         {QStringLiteral("esc"), Qt::Key_Escape},
0050         {QStringLiteral("return"), Qt::Key_Return},
0051         {QStringLiteral("enter"), Qt::Key_Enter},
0052         {QStringLiteral("left"), Qt::Key_Left},
0053         {QStringLiteral("right"), Qt::Key_Right},
0054         {QStringLiteral("up"), Qt::Key_Up},
0055         {QStringLiteral("down"), Qt::Key_Down},
0056         {QStringLiteral("home"), Qt::Key_Home},
0057         {QStringLiteral("end"), Qt::Key_End},
0058         {QStringLiteral("delete"), Qt::Key_Delete},
0059         {QStringLiteral("insert"), Qt::Key_Insert},
0060         {QStringLiteral("pageup"), Qt::Key_PageUp},
0061         {QStringLiteral("pagedown"), Qt::Key_PageDown},
0062     };
0063 }
0064 
0065 BaseTest::~BaseTest()
0066 {
0067     delete kate_document;
0068 }
0069 
0070 void BaseTest::waitForCompletionWidgetToActivate(KTextEditor::ViewPrivate *kate_view)
0071 {
0072     const QDateTime start = QDateTime::currentDateTime();
0073     while (start.msecsTo(QDateTime::currentDateTime()) < 1000) {
0074         if (kate_view->isCompletionActive()) {
0075             break;
0076         }
0077         QApplication::processEvents();
0078     }
0079     QVERIFY(kate_view->isCompletionActive());
0080 }
0081 
0082 void BaseTest::init()
0083 {
0084     delete kate_view;
0085     delete kate_document;
0086 
0087     kate_document = new KTextEditor::DocumentPrivate();
0088 
0089     // fixed indentation options
0090     kate_document->config()->setTabWidth(8);
0091     kate_document->config()->setIndentationWidth(2);
0092     kate_document->config()->setReplaceTabsDyn(false);
0093 
0094     // ensure the spellchecking doesn't mess with the expected results
0095     static_cast<KTextEditor::DocumentPrivate *>(kate_document)->setDefaultDictionary(QStringLiteral("notexistinglanguage"));
0096 
0097     kate_view = new KTextEditor::ViewPrivate(kate_document, mainWindow);
0098     mainWindowLayout->addWidget(kate_view);
0099     kate_view->config()->setValue(KateViewConfig::AutoBrackets, false);
0100     kate_view->setInputMode(View::ViInputMode);
0101     Q_ASSERT(kate_view->currentInputMode()->viewInputMode() == KTextEditor::View::ViInputMode);
0102     vi_input_mode = static_cast<KateViInputMode *>(kate_view->currentInputMode());
0103     vi_input_mode_manager = vi_input_mode->viInputModeManager();
0104     Q_ASSERT(vi_input_mode_manager);
0105     vi_global = vi_input_mode->globalState();
0106     Q_ASSERT(vi_global);
0107     kate_document->config()->setShowSpaces(KateDocumentConfig::Trailing); // Flush out some issues in the KateRenderer when rendering spaces.
0108     kate_view->config()->setValue(KateViewConfig::ShowScrollBarMiniMap, false);
0109     kate_view->config()->setValue(KateViewConfig::ShowScrollBarPreview, false);
0110     kate_view->setStatusBarEnabled(false);
0111 
0112     connect(kate_document, &KTextEditor::DocumentPrivate::textInsertedRange, this, &BaseTest::textInserted);
0113     connect(kate_document, &KTextEditor::DocumentPrivate::textRemoved, this, &BaseTest::textRemoved);
0114 }
0115 
0116 void BaseTest::TestPressKey(const QString &str)
0117 {
0118     if (m_firstBatchOfKeypressesForTest) {
0119         qDebug() << "\n\n>>> running command " << str << " on text " << kate_document->text();
0120     } else {
0121         qDebug() << "\n>>> running further keypresses " << str << " on text " << kate_document->text();
0122     }
0123     m_firstBatchOfKeypressesForTest = false;
0124 
0125     for (int i = 0; i < str.length(); i++) {
0126         Qt::KeyboardModifiers keyboard_modifier = Qt::NoModifier;
0127         QString key;
0128         int keyCode = -1;
0129         // Looking for keyboard modifiers
0130         if (str[i] == QLatin1Char('\\')) {
0131             int endOfModifier = -1;
0132             Qt::KeyboardModifier parsedModifier = parseCodedModifier(str, i, &endOfModifier);
0133             int endOfSpecialKey = -1;
0134             Qt::Key parsedSpecialKey = parseCodedSpecialKey(str, i, &endOfSpecialKey);
0135             if (parsedModifier != Qt::NoModifier) {
0136                 keyboard_modifier = parsedModifier;
0137                 // Move to the character after the '-' in the modifier.
0138                 i = endOfModifier + 1;
0139                 // Is this a modifier plus special key?
0140                 int endOfSpecialKeyAfterModifier = -1;
0141                 const Qt::Key parsedCodedSpecialKeyAfterModifier = parseCodedSpecialKey(str, i, &endOfSpecialKeyAfterModifier);
0142                 if (parsedCodedSpecialKeyAfterModifier != Qt::Key_unknown) {
0143                     key = parsedCodedSpecialKeyAfterModifier <= 0xffff ? QString(QChar(parsedCodedSpecialKeyAfterModifier)) : QString();
0144                     keyCode = parsedCodedSpecialKeyAfterModifier;
0145                     i = endOfSpecialKeyAfterModifier;
0146                 }
0147             } else if (parsedSpecialKey != Qt::Key_unknown) {
0148                 key = parsedSpecialKey <= 0xffff ? QString(QChar(parsedSpecialKey)) : QString();
0149                 keyCode = parsedSpecialKey;
0150                 i = endOfSpecialKey;
0151             } else if (str.mid(i, 2) == QStringLiteral("\\:")) {
0152                 int start_cmd = i + 2;
0153                 for (i += 2; true; i++) {
0154                     if (str.at(i) == QLatin1Char('\\')) {
0155                         if (i + 1 < str.length() && str.at(i + 1) == QLatin1Char('\\')) {
0156                             // A backslash within a command; skip.
0157                             i += 2;
0158                         } else {
0159                             // End of command.
0160                             break;
0161                         }
0162                     }
0163                 }
0164                 const QString commandToExecute = str.mid(start_cmd, i - start_cmd).replace(QLatin1String("\\\\"), QLatin1String("\\"));
0165                 qDebug() << "Executing command directly from ViModeTest:\n" << commandToExecute;
0166                 vi_input_mode->viModeEmulatedCommandBar()->executeCommand(commandToExecute);
0167                 // We've handled the command; go back round the loop, avoiding sending
0168                 // the closing \ to vi_input_mode_manager.
0169                 continue;
0170             } else if (str.mid(i, 2) == QStringLiteral("\\\\")) {
0171                 key = QStringLiteral("\\");
0172                 keyCode = Qt::Key_Backslash;
0173                 i++;
0174             } else {
0175                 Q_ASSERT(false); // Do not use "\" in tests except for modifiers, command mode (\\:) and literal backslashes "\\\\")
0176             }
0177         }
0178 
0179         if (keyCode == -1) {
0180             key = str[i];
0181             keyCode = key[0].unicode();
0182             // Kate Vim mode's internals identifier e.g. CTRL-C by Qt::Key_C plus the control modifier,
0183             // so we need to translate e.g. 'c' to Key_C.
0184             if (key[0].isLetter()) {
0185                 if (key[0].toLower() == key[0]) {
0186                     keyCode = keyCode - 'a' + Qt::Key_A;
0187                 } else {
0188                     keyCode = keyCode - 'A' + Qt::Key_A;
0189                     keyboard_modifier |= Qt::ShiftModifier;
0190                 }
0191             }
0192         }
0193 
0194         QKeyEvent key_event(QEvent::KeyPress, keyCode, keyboard_modifier, key);
0195         // Attempt to simulate how Qt usually sends events - typically, we want to send them
0196         // to kate_view->focusProxy() (which is a KateViewInternal).
0197         QWidget *destWidget = nullptr;
0198         if (QApplication::activePopupWidget()) {
0199             // According to the docs, the activePopupWidget, if present, takes all events.
0200             destWidget = QApplication::activePopupWidget();
0201         } else if (QApplication::focusWidget()) {
0202             if (QApplication::focusWidget()->focusProxy()) {
0203                 destWidget = QApplication::focusWidget()->focusProxy();
0204             } else {
0205                 destWidget = QApplication::focusWidget();
0206             }
0207         } else {
0208             destWidget = kate_view->focusProxy();
0209         }
0210         QApplication::sendEvent(destWidget, &key_event);
0211     }
0212 }
0213 
0214 void BaseTest::BeginTest(const QString &original)
0215 {
0216     vi_input_mode->viInputModeManager()->viEnterNormalMode();
0217     vi_input_mode->reset();
0218     vi_input_mode_manager = vi_input_mode->viInputModeManager();
0219     kate_document->setText(original);
0220     kate_document->undoManager()->clearUndo();
0221     kate_document->undoManager()->clearRedo();
0222     kate_view->setCursorPosition(Cursor(0, 0));
0223     m_firstBatchOfKeypressesForTest = true;
0224 }
0225 
0226 void BaseTest::FinishTest_(int line, const char *file, const char *expected, Expectation expectation, const char *failureReason)
0227 {
0228     if (expectation == ShouldFail) {
0229         if (!QTest::qExpectFail("", failureReason, QTest::Continue, file, line)) {
0230             return;
0231         }
0232         qDebug() << "Actual text:\n\t" << kate_document->text() << "\nShould be (for this test to pass):\n\t" << expected;
0233     }
0234     if (!QTest::qCompare(kate_document->text(), QString::fromUtf8(expected), "kate_document->text()", "expected_text", file, line)) {
0235         return;
0236     }
0237     Q_ASSERT(!emulatedCommandBarTextEdit()->isVisible() && "Make sure you close the command bar before the end of a test!");
0238 }
0239 
0240 void BaseTest::DoTest_(int line,
0241                        const char *file,
0242                        const char *original,
0243                        const char *command,
0244                        const char *expected,
0245                        Expectation expectation,
0246                        const char *failureReason)
0247 {
0248     BeginTest(QLatin1String(original));
0249     TestPressKey(QLatin1String(command));
0250     FinishTest_(line, file, expected, expectation, failureReason);
0251 }
0252 
0253 void BaseTest::DoTest2(int line,
0254                        const char *file,
0255                        const QString &original,
0256                        const QString &command,
0257                        const QString &expected,
0258                        Expectation expectation,
0259                        const char *failureReason)
0260 {
0261     BeginTest(original);
0262     TestPressKey(command);
0263     FinishTest_(line, file, expected.toUtf8().constData(), expectation, failureReason);
0264 }
0265 
0266 Qt::KeyboardModifier BaseTest::parseCodedModifier(const QString &string, int startPos, int *destEndOfCodedModifier)
0267 {
0268     for (const auto &[code, modifier] : m_codesToModifiers) {
0269         // The "+2" is from the leading '\' and the trailing '-'
0270         if (string.mid(startPos, code.length() + 2) == QStringLiteral("\\") + code + QStringLiteral("-")) {
0271             if (destEndOfCodedModifier) {
0272                 // destEndOfCodeModifier lies on the trailing '-'.
0273                 *destEndOfCodedModifier = startPos + code.length() + 1;
0274                 Q_ASSERT(string[*destEndOfCodedModifier] == QLatin1Char('-'));
0275             }
0276             return modifier;
0277         }
0278     }
0279     return Qt::NoModifier;
0280 }
0281 
0282 Qt::Key BaseTest::parseCodedSpecialKey(const QString &string, int startPos, int *destEndOfCodedKey)
0283 {
0284     for (const auto &[specialKeyCode, specialKey] : m_codesToSpecialKeys) {
0285         // "+1" is for the leading '\'.
0286         if (string.mid(startPos, specialKeyCode.length() + 1) == QStringLiteral("\\") + specialKeyCode) {
0287             if (destEndOfCodedKey) {
0288                 *destEndOfCodedKey = startPos + specialKeyCode.length();
0289             }
0290             return specialKey;
0291         }
0292     }
0293     return Qt::Key_unknown;
0294 }
0295 
0296 KateVi::EmulatedCommandBar *BaseTest::emulatedCommandBar()
0297 {
0298     KateVi::EmulatedCommandBar *emulatedCommandBar = vi_input_mode->viModeEmulatedCommandBar();
0299     Q_ASSERT(emulatedCommandBar);
0300     return emulatedCommandBar;
0301 }
0302 
0303 QLineEdit *BaseTest::emulatedCommandBarTextEdit()
0304 {
0305     QLineEdit *emulatedCommandBarText = emulatedCommandBar()->findChild<QLineEdit *>(QStringLiteral("commandtext"));
0306     Q_ASSERT(emulatedCommandBarText);
0307     return emulatedCommandBarText;
0308 }
0309 
0310 void BaseTest::ensureKateViewVisible()
0311 {
0312     mainWindow->show();
0313     kate_view->show();
0314     QApplication::setActiveWindow(mainWindow);
0315     kate_view->setFocus();
0316     const QDateTime startTime = QDateTime::currentDateTime();
0317     while (startTime.msecsTo(QDateTime::currentDateTime()) < 3000 && !mainWindow->isActiveWindow()) {
0318         QApplication::processEvents();
0319     }
0320     QVERIFY(kate_view->isVisible());
0321     QVERIFY(mainWindow->isActiveWindow());
0322 }
0323 
0324 void BaseTest::clearAllMappings()
0325 {
0326     vi_global->mappings()->clear(Mappings::NormalModeMapping);
0327     vi_global->mappings()->clear(Mappings::VisualModeMapping);
0328     vi_global->mappings()->clear(Mappings::InsertModeMapping);
0329     vi_global->mappings()->clear(Mappings::CommandModeMapping);
0330 }
0331 
0332 void BaseTest::clearAllMacros()
0333 {
0334     vi_global->macros()->clear();
0335 }
0336 
0337 void BaseTest::textInserted(Document *document, KTextEditor::Range range)
0338 {
0339     m_docChanges.append(DocChange(DocChange::TextInserted, range, document->text(range)));
0340 }
0341 
0342 void BaseTest::textRemoved(Document *document, KTextEditor::Range range)
0343 {
0344     Q_UNUSED(document);
0345     m_docChanges.append(DocChange(DocChange::TextRemoved, range));
0346 }
0347 
0348 #include "moc_base.cpp"
0349 // END: BaseTest