File indexing completed on 2024-05-12 11:57:24

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 "view.h"
0009 #include <QClipboard>
0010 #include <inputmode/kateviinputmode.h>
0011 #include <katebuffer.h>
0012 #include <kateconfig.h>
0013 #include <katedocument.h>
0014 #include <kateview.h>
0015 
0016 using namespace KTextEditor;
0017 
0018 QTEST_MAIN(ViewTest)
0019 
0020 void ViewTest::yankHighlightingTests()
0021 {
0022     const QColor yankHighlightColour = kate_view->renderer()->config()->savedLineColor();
0023 
0024     BeginTest("foo bar xyz");
0025     const QVector<Kate::TextRange *> rangesInitial = rangesOnFirstLine();
0026     Q_ASSERT(rangesInitial.isEmpty() && "Assumptions about ranges are wrong - this test is invalid and may need updating!");
0027     TestPressKey("wyiw");
0028     {
0029         const QVector<Kate::TextRange *> rangesAfterYank = rangesOnFirstLine();
0030         QCOMPARE(rangesAfterYank.size(), rangesInitial.size() + 1);
0031         QCOMPARE(rangesAfterYank.first()->attribute()->background().color(), yankHighlightColour);
0032         QCOMPARE(rangesAfterYank.first()->start().line(), 0);
0033         QCOMPARE(rangesAfterYank.first()->start().column(), 4);
0034         QCOMPARE(rangesAfterYank.first()->end().line(), 0);
0035         QCOMPARE(rangesAfterYank.first()->end().column(), 7);
0036     }
0037     FinishTest("foo bar xyz");
0038 
0039     BeginTest("foom bar xyz");
0040     TestPressKey("wY");
0041     {
0042         const QVector<Kate::TextRange *> rangesAfterYank = rangesOnFirstLine();
0043         QCOMPARE(rangesAfterYank.size(), rangesInitial.size() + 1);
0044         QCOMPARE(rangesAfterYank.first()->attribute()->background().color(), yankHighlightColour);
0045         QCOMPARE(rangesAfterYank.first()->start().line(), 0);
0046         QCOMPARE(rangesAfterYank.first()->start().column(), 5);
0047         QCOMPARE(rangesAfterYank.first()->end().line(), 0);
0048         QCOMPARE(rangesAfterYank.first()->end().column(), 12);
0049     }
0050     FinishTest("foom bar xyz");
0051 
0052     // Unhighlight on keypress.
0053     DoTest("foo bar xyz", "yiww", "foo bar xyz");
0054     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size());
0055 
0056     // Update colour on config change.
0057     DoTest("foo bar xyz", "yiw", "foo bar xyz");
0058     const QColor newYankHighlightColour = QColor(255, 0, 0);
0059     kate_view->renderer()->config()->setSavedLineColor(newYankHighlightColour);
0060     QCOMPARE(rangesOnFirstLine().first()->attribute()->background().color(), newYankHighlightColour);
0061 
0062     // Visual Mode.
0063     DoTest("foo", "viwy", "foo");
0064     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1);
0065 
0066     // Unhighlight on keypress in Visual Mode
0067     DoTest("foo", "viwyw", "foo");
0068     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size());
0069 
0070     // Add a yank highlight and directly (i.e. without using Vim commands,
0071     // which would clear the highlight) delete all text; if this deletes the yank highlight behind our back
0072     // and we don't respond correctly to this, it will be double-deleted by KateViNormalMode.
0073     // Currently, this seems like it doesn't occur, but better safe than sorry :)
0074     BeginTest("foo bar xyz");
0075     TestPressKey("yiw");
0076     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1);
0077     kate_document->documentReload();
0078     kate_document->clear();
0079     vi_input_mode->reset();
0080     vi_input_mode_manager = vi_input_mode->viInputModeManager();
0081     FinishTest("");
0082 }
0083 
0084 void ViewTest::visualLineUpDownTests()
0085 {
0086     // Need to ensure we have dynamic wrap, a fixed width font, and a decent size kate_view.
0087     ensureKateViewVisible();
0088     const QFont oldFont = kate_view->renderer()->config()->baseFont();
0089     QFont fixedWidthFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
0090     kate_view->renderer()->config()->setFont(fixedWidthFont);
0091     const bool oldDynWordWrap = KateViewConfig::global()->dynWordWrap();
0092     KateViewConfig::global()->setDynWordWrap(true);
0093     const bool oldReplaceTabsDyn = kate_document->config()->replaceTabsDyn();
0094     kate_document->config()->setReplaceTabsDyn(false);
0095     const int oldTabWidth = kate_document->config()->tabWidth();
0096     const int tabWidth = 5;
0097     kate_document->config()->setTabWidth(tabWidth);
0098     KateViewConfig::global()->setValue(KateViewConfig::ShowScrollbars, KateViewConfig::ScrollbarMode::AlwaysOn);
0099 
0100     // Compute the maximum width of text before line-wrapping sets it.
0101     int textWrappingLength = 1;
0102     while (true) {
0103         QString text = QString("X").repeated(textWrappingLength) + ' ' + 'O';
0104         const int posOfO = text.length() - 1;
0105         kate_document->setText(text);
0106         if (kate_view->cursorToCoordinate(Cursor(0, posOfO)).y() != kate_view->cursorToCoordinate(Cursor(0, 0)).y()) {
0107             textWrappingLength++; // Number of x's, plus space.
0108             break;
0109         }
0110         textWrappingLength++;
0111     }
0112     const QString fillsLineAndEndsOnSpace = QString("X").repeated(textWrappingLength - 1) + ' ';
0113 
0114     // Create a QString consisting of enough concatenated fillsLineAndEndsOnSpace to completely
0115     // fill the viewport of the kate View.
0116     QString fillsView = fillsLineAndEndsOnSpace;
0117     while (true) {
0118         kate_document->setText(fillsView);
0119         const QString visibleText = kate_document->text(kate_view->visibleRange());
0120         if (fillsView.length() > visibleText.length() * 2) { // Overkill.
0121             break;
0122         }
0123         fillsView += fillsLineAndEndsOnSpace;
0124     }
0125     const int numVisibleLinesToFillView = fillsView.length() / fillsLineAndEndsOnSpace.length();
0126 
0127     {
0128         // gk/ gj when there is only one line.
0129         DoTest("foo", "lgkr.", "f.o");
0130         DoTest("foo", "lgjr.", "f.o");
0131     }
0132 
0133     {
0134         // gk when sticky bit is set to the end.
0135         const QString originalText = fillsLineAndEndsOnSpace.repeated(2);
0136         QString expectedText = originalText;
0137         kate_document->setText(originalText);
0138         Q_ASSERT(expectedText[textWrappingLength - 1] == ' ');
0139         expectedText[textWrappingLength - 1] = '.';
0140         DoTest(originalText, "$gkr.", expectedText);
0141     }
0142 
0143     {
0144         // Regression test: more than fill the view up, go to end, and do gk on wrapped text (used to crash).
0145         // First work out the text that will fill up the view.
0146         QString expectedText = fillsView;
0147         Q_ASSERT(expectedText[expectedText.length() - textWrappingLength - 1] == ' ');
0148         expectedText[expectedText.length() - textWrappingLength - 1] = '.';
0149 
0150         DoTest(fillsView, "$gkr.", expectedText);
0151     }
0152 
0153     {
0154         // Jump down a few lines all in one go, where we have some variable length lines to navigate.
0155         const int numVisualLinesOnLine[] = {3, 5, 2, 3};
0156         const int numLines = sizeof(numVisualLinesOnLine) / sizeof(int);
0157         const int startVisualLine = 2;
0158         const int numberLinesToGoDownInOneGo = 10;
0159 
0160         int totalVisualLines = 0;
0161         for (int i = 0; i < numLines; i++) {
0162             totalVisualLines += numVisualLinesOnLine[i];
0163         }
0164 
0165         QString startText;
0166         for (int i = 0; i < numLines; i++) {
0167             QString thisLine = fillsLineAndEndsOnSpace.repeated(numVisualLinesOnLine[i]);
0168             // Replace trailing space with carriage return.
0169             thisLine.chop(1);
0170             thisLine.append('\n');
0171             startText += thisLine;
0172         }
0173         QString expectedText = startText;
0174         expectedText[((startVisualLine - 1) + numberLinesToGoDownInOneGo) * fillsLineAndEndsOnSpace.length()] = '.';
0175 
0176         Q_ASSERT(numberLinesToGoDownInOneGo + startVisualLine < totalVisualLines);
0177         Q_ASSERT(numberLinesToGoDownInOneGo + startVisualLine < numVisibleLinesToFillView);
0178         DoTest(startText, QString("gj").repeated(startVisualLine - 1) + QString::number(numberLinesToGoDownInOneGo) + "gjr.", expectedText);
0179         // Now go up a few lines.
0180         const int numLinesToGoBackUp = 7;
0181         expectedText = startText;
0182         expectedText[((startVisualLine - 1) + numberLinesToGoDownInOneGo - numLinesToGoBackUp) * fillsLineAndEndsOnSpace.length()] = '.';
0183         DoTest(startText,
0184                QString("gj").repeated(startVisualLine - 1) + QString::number(numberLinesToGoDownInOneGo) + "gj" + QString::number(numLinesToGoBackUp) + "gkr.",
0185                expectedText);
0186     }
0187 
0188     {
0189         // Move down enough lines in one go to disappear off the view.
0190         // About half-a-viewport past the end of the current viewport.
0191         const int numberLinesToGoDown = numVisibleLinesToFillView * 3 / 2;
0192         const int visualColumnNumber = 7;
0193         Q_ASSERT(fillsLineAndEndsOnSpace.length() > visualColumnNumber);
0194         QString expectedText = fillsView.repeated(2);
0195         Q_ASSERT(expectedText[expectedText.length() - textWrappingLength - 1] == ' ');
0196         expectedText[visualColumnNumber + fillsLineAndEndsOnSpace.length() * numberLinesToGoDown] = '.';
0197 
0198         DoTest(fillsView.repeated(2), QString("l").repeated(visualColumnNumber) + QString::number(numberLinesToGoDown) + "gjr.", expectedText);
0199     }
0200 
0201     {
0202         // Deal with dynamic wrapping and indented blocks - continuations of a line are "invisibly" idented by
0203         // the same amount as the beginning of the line, and we have to subtract this indentation.
0204         const QString unindentedFirstLine = "stickyhelper\n";
0205         const int numIndentationSpaces = 5;
0206         Q_ASSERT(textWrappingLength > numIndentationSpaces * 2 /* keep some wriggle room */);
0207         const QString indentedFillsLineEndsOnSpace =
0208             QString(" ").repeated(numIndentationSpaces) + QString("X").repeated(textWrappingLength - 1 - numIndentationSpaces) + ' ';
0209         DoTest(unindentedFirstLine + indentedFillsLineEndsOnSpace + "LINE3",
0210                QString("l").repeated(numIndentationSpaces) + "jgjr.",
0211                unindentedFirstLine + indentedFillsLineEndsOnSpace + ".INE3");
0212 
0213         // The first, non-wrapped portion of the line is not invisibly indented, though, so ensure we don't mess that up.
0214         QString expectedSecondLine = indentedFillsLineEndsOnSpace;
0215         expectedSecondLine[numIndentationSpaces] = '.';
0216         DoTest(unindentedFirstLine + indentedFillsLineEndsOnSpace + "LINE3",
0217                QString("l").repeated(numIndentationSpaces) + "jgjgkr.",
0218                unindentedFirstLine + expectedSecondLine + "LINE3");
0219     }
0220 
0221     {
0222         // Take into account any invisible indentation when setting the sticky column.
0223         const int numIndentationSpaces = 5;
0224         Q_ASSERT(textWrappingLength > numIndentationSpaces * 2 /* keep some wriggle room */);
0225         const QString indentedFillsLineEndsOnSpace =
0226             QString(" ").repeated(numIndentationSpaces) + QString("X").repeated(textWrappingLength - 1 - numIndentationSpaces) + ' ';
0227         const int posInSecondWrappedLineToChange = 3;
0228         QString expectedText = indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace;
0229         expectedText[textWrappingLength + posInSecondWrappedLineToChange] = '.';
0230         DoTest(indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
0231                QString::number(textWrappingLength + posInSecondWrappedLineToChange) + "lgkgjr.",
0232                expectedText);
0233         // Make sure we can do this more than once (i.e. clear any flags that need clearing).
0234         DoTest(indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
0235                QString::number(textWrappingLength + posInSecondWrappedLineToChange) + "lgkgjr.",
0236                expectedText);
0237     }
0238 
0239     {
0240         // Take into account any invisible indentation when setting the sticky column as above, but use tabs.
0241         const QString indentedFillsLineEndsOnSpace = QString("\t") + QString("X").repeated(textWrappingLength - 1 - tabWidth) + ' ';
0242         const int posInSecondWrappedLineToChange = 3;
0243         QString expectedText = indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace;
0244         expectedText[textWrappingLength - tabWidth + posInSecondWrappedLineToChange] = '.';
0245         DoTest(indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
0246                QString("fXf ") + QString::number(posInSecondWrappedLineToChange) + "lgkgjr.",
0247                expectedText);
0248     }
0249 
0250     {
0251         // Deal with the fact that j/ k may set a sticky column that is impossible to adhere to in visual mode because
0252         // it is too high.
0253         // Here, we have one dummy line and one wrapped line.  We start from the beginning of the wrapped line and
0254         // move right until we wrap and end up at posInWrappedLineToChange one the second line of the wrapped line.
0255         // We then move up and down with j and k to set the sticky column to a value to large to adhere to in a
0256         // visual line, and try to move a visual line up.
0257         const QString dummyLineForUseWithK("dummylineforusewithk\n");
0258         QString startText = dummyLineForUseWithK + fillsLineAndEndsOnSpace.repeated(2);
0259         const int posInWrappedLineToChange = 3;
0260         QString expectedText = startText;
0261         expectedText[dummyLineForUseWithK.length() + posInWrappedLineToChange] = '.';
0262         DoTest(startText, 'j' + QString::number(textWrappingLength + posInWrappedLineToChange) + "lkjgkr.", expectedText);
0263     }
0264 
0265     {
0266         // Ensure gj works in Visual mode.
0267         Q_ASSERT(fillsLineAndEndsOnSpace.toLower() != fillsLineAndEndsOnSpace);
0268         QString expectedText = fillsLineAndEndsOnSpace.toLower() + fillsLineAndEndsOnSpace;
0269         expectedText[textWrappingLength] = expectedText[textWrappingLength].toLower();
0270         DoTest(fillsLineAndEndsOnSpace.repeated(2), "vgjgu", expectedText);
0271     }
0272 
0273     {
0274         // Ensure gk works in Visual mode.
0275         Q_ASSERT(fillsLineAndEndsOnSpace.toLower() != fillsLineAndEndsOnSpace);
0276         DoTest(fillsLineAndEndsOnSpace.repeated(2), "$vgkgu", fillsLineAndEndsOnSpace + fillsLineAndEndsOnSpace.toLower());
0277     }
0278 
0279     {
0280         // Some tests for how well we handle things with real tabs.
0281         QString beginsWithTabFillsLineEndsOnSpace = "\t";
0282         while (beginsWithTabFillsLineEndsOnSpace.length() + (tabWidth - 1) < textWrappingLength - 1) {
0283             beginsWithTabFillsLineEndsOnSpace += 'X';
0284         }
0285         beginsWithTabFillsLineEndsOnSpace += ' ';
0286         const QString unindentedFirstLine = "stockyhelper\n";
0287         const int posOnThirdLineToChange = 3;
0288         QString expectedThirdLine = fillsLineAndEndsOnSpace;
0289         expectedThirdLine[posOnThirdLineToChange] = '.';
0290         DoTest(unindentedFirstLine + beginsWithTabFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
0291                QString("l").repeated(tabWidth + posOnThirdLineToChange) + "gjgjr.",
0292                unindentedFirstLine + beginsWithTabFillsLineEndsOnSpace + expectedThirdLine);
0293 
0294         // As above, but go down twice and return to the middle line.
0295         const int posOnSecondLineToChange = 2;
0296         QString expectedSecondLine = beginsWithTabFillsLineEndsOnSpace;
0297         expectedSecondLine[posOnSecondLineToChange + 1 /* "+1" as we're not counting the leading tab as a pos */] = '.';
0298         DoTest(unindentedFirstLine + beginsWithTabFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
0299                QString("l").repeated(tabWidth + posOnSecondLineToChange) + "gjgjgkr.",
0300                unindentedFirstLine + expectedSecondLine + fillsLineAndEndsOnSpace);
0301     }
0302 
0303     // Restore back to how we were before.
0304     kate_view->renderer()->config()->setFont(oldFont);
0305     KateViewConfig::global()->setDynWordWrap(oldDynWordWrap);
0306     kate_document->config()->setReplaceTabsDyn(oldReplaceTabsDyn);
0307     kate_document->config()->setTabWidth(oldTabWidth);
0308 }
0309 
0310 void ViewTest::ScrollViewTests()
0311 {
0312     QSKIP("This is too unstable in Jenkins", SkipAll);
0313 
0314     // First of all, we have to initialize some sizes and fonts.
0315     ensureKateViewVisible();
0316     const QFont oldFont = kate_view->renderer()->config()->baseFont();
0317     QFont fixedWidthFont("Monospace");
0318     fixedWidthFont.setStyleHint(QFont::TypeWriter);
0319     fixedWidthFont.setPixelSize(14);
0320     Q_ASSERT_X(QFontInfo(fixedWidthFont).fixedPitch(), "setting up ScrollViewTests", "Need a fixed pitch font!");
0321     kate_view->renderer()->config()->setFont(fixedWidthFont);
0322 
0323     // Generating our text here.
0324     QString text;
0325     for (int i = 0; i < 20; i++) {
0326         text += "    aaaaaaaaaaaaaaaa\n";
0327     }
0328 
0329     // TODO: fix the visibleRange's tests.
0330 
0331     // zz
0332     BeginTest(text);
0333     TestPressKey("10l9jzz");
0334     QCOMPARE(kate_view->cursorPosition().line(), 9);
0335     QCOMPARE(kate_view->cursorPosition().column(), 10);
0336     QCOMPARE(kate_view->visibleRange(), Range(4, 0, 13, 20));
0337     FinishTest(text);
0338 
0339     // z.
0340     BeginTest(text);
0341     TestPressKey("10l9jz.");
0342     QCOMPARE(kate_view->cursorPosition().line(), 9);
0343     QCOMPARE(kate_view->cursorPosition().column(), 4);
0344     QCOMPARE(kate_view->visibleRange(), Range(4, 0, 13, 20));
0345     FinishTest(text);
0346 
0347     // zt
0348     BeginTest(text);
0349     TestPressKey("10l9jzt");
0350     QCOMPARE(kate_view->cursorPosition().line(), 9);
0351     QCOMPARE(kate_view->cursorPosition().column(), 10);
0352     QCOMPARE(kate_view->visibleRange(), Range(9, 0, 18, 20));
0353     FinishTest(text);
0354 
0355     // z<cr>
0356     BeginTest(text);
0357     TestPressKey("10l9jz\\return");
0358     QCOMPARE(kate_view->cursorPosition().line(), 9);
0359     QCOMPARE(kate_view->cursorPosition().column(), 4);
0360     QCOMPARE(kate_view->visibleRange(), Range(9, 0, 18, 20));
0361     FinishTest(text);
0362 
0363     // zb
0364     BeginTest(text);
0365     TestPressKey("10l9jzb");
0366     QCOMPARE(kate_view->cursorPosition().line(), 9);
0367     QCOMPARE(kate_view->cursorPosition().column(), 10);
0368     QCOMPARE(kate_view->visibleRange(), Range(0, 0, 9, 20));
0369     FinishTest(text);
0370 
0371     // z-
0372     BeginTest(text);
0373     TestPressKey("10l9jz-");
0374     QCOMPARE(kate_view->cursorPosition().line(), 9);
0375     QCOMPARE(kate_view->cursorPosition().column(), 4);
0376     QCOMPARE(kate_view->visibleRange(), Range(0, 0, 9, 20));
0377     FinishTest(text);
0378 
0379     // Restore back to how we were before.
0380     kate_view->renderer()->config()->setFont(oldFont);
0381 }
0382 
0383 void ViewTest::clipboardTests_data()
0384 {
0385     QTest::addColumn<QString>("text");
0386     QTest::addColumn<QString>("commands");
0387     QTest::addColumn<QString>("clipboard");
0388 
0389     QTest::newRow("yank") << "yyfoo\nbar"
0390                           << "yy"
0391                           << "yyfoo\n";
0392     QTest::newRow("delete") << "ddfoo\nbar"
0393                             << "dd"
0394                             << "ddfoo\n";
0395     QTest::newRow("yank empty line") << "\nbar"
0396                                      << "yy" << QString();
0397     QTest::newRow("delete word") << "word foo"
0398                                  << "dw"
0399                                  << "word ";
0400     QTest::newRow("delete onechar word") << "w foo"
0401                                          << "dw"
0402                                          << "w ";
0403     QTest::newRow("delete onechar") << "word foo"
0404                                     << "dc" << QString();
0405     QTest::newRow("delete empty lines") << " \t\n\n  \nfoo"
0406                                         << "d3d" << QString();
0407 }
0408 
0409 void ViewTest::clipboardTests()
0410 {
0411     QFETCH(QString, text);
0412     QFETCH(QString, commands);
0413     QFETCH(QString, clipboard);
0414 
0415     QApplication::clipboard()->clear();
0416     BeginTest(text);
0417     TestPressKey(commands);
0418     QCOMPARE(QApplication::clipboard()->text(), clipboard);
0419 }
0420 
0421 QVector<Kate::TextRange *> ViewTest::rangesOnFirstLine()
0422 {
0423     return kate_document->buffer().rangesForLine(0, kate_view, true);
0424 }
0425 
0426 #include "moc_view.cpp"