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"