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 "view.h"
9 #include <QClipboard>
10 #include <inputmode/kateviinputmode.h>
11 #include <katebuffer.h>
12 #include <kateconfig.h>
13 #include <katedocument.h>
14 #include <kateview.h>
15 
16 using namespace KTextEditor;
17 
QTEST_MAIN(ViewTest)18 QTEST_MAIN(ViewTest)
19 
20 void ViewTest::yankHighlightingTests()
21 {
22     const QColor yankHighlightColour = kate_view->renderer()->config()->savedLineColor();
23 
24     BeginTest("foo bar xyz");
25     const QVector<Kate::TextRange *> rangesInitial = rangesOnFirstLine();
26     Q_ASSERT(rangesInitial.isEmpty() && "Assumptions about ranges are wrong - this test is invalid and may need updating!");
27     TestPressKey("wyiw");
28     {
29         const QVector<Kate::TextRange *> rangesAfterYank = rangesOnFirstLine();
30         QCOMPARE(rangesAfterYank.size(), rangesInitial.size() + 1);
31         QCOMPARE(rangesAfterYank.first()->attribute()->background().color(), yankHighlightColour);
32         QCOMPARE(rangesAfterYank.first()->start().line(), 0);
33         QCOMPARE(rangesAfterYank.first()->start().column(), 4);
34         QCOMPARE(rangesAfterYank.first()->end().line(), 0);
35         QCOMPARE(rangesAfterYank.first()->end().column(), 7);
36     }
37     FinishTest("foo bar xyz");
38 
39     BeginTest("foom bar xyz");
40     TestPressKey("wY");
41     {
42         const QVector<Kate::TextRange *> rangesAfterYank = rangesOnFirstLine();
43         QCOMPARE(rangesAfterYank.size(), rangesInitial.size() + 1);
44         QCOMPARE(rangesAfterYank.first()->attribute()->background().color(), yankHighlightColour);
45         QCOMPARE(rangesAfterYank.first()->start().line(), 0);
46         QCOMPARE(rangesAfterYank.first()->start().column(), 5);
47         QCOMPARE(rangesAfterYank.first()->end().line(), 0);
48         QCOMPARE(rangesAfterYank.first()->end().column(), 12);
49     }
50     FinishTest("foom bar xyz");
51 
52     // Unhighlight on keypress.
53     DoTest("foo bar xyz", "yiww", "foo bar xyz");
54     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size());
55 
56     // Update colour on config change.
57     DoTest("foo bar xyz", "yiw", "foo bar xyz");
58     const QColor newYankHighlightColour = QColor(255, 0, 0);
59     kate_view->renderer()->config()->setSavedLineColor(newYankHighlightColour);
60     QCOMPARE(rangesOnFirstLine().first()->attribute()->background().color(), newYankHighlightColour);
61 
62     // Visual Mode.
63     DoTest("foo", "viwy", "foo");
64     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1);
65 
66     // Unhighlight on keypress in Visual Mode
67     DoTest("foo", "viwyw", "foo");
68     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size());
69 
70     // Add a yank highlight and directly (i.e. without using Vim commands,
71     // which would clear the highlight) delete all text; if this deletes the yank highlight behind our back
72     // and we don't respond correctly to this, it will be double-deleted by KateViNormalMode.
73     // Currently, this seems like it doesn't occur, but better safe than sorry :)
74     BeginTest("foo bar xyz");
75     TestPressKey("yiw");
76     QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1);
77     kate_document->documentReload();
78     kate_document->clear();
79     vi_input_mode->reset();
80     vi_input_mode_manager = vi_input_mode->viInputModeManager();
81     FinishTest("");
82 }
83 
visualLineUpDownTests()84 void ViewTest::visualLineUpDownTests()
85 {
86     // Need to ensure we have dynamic wrap, a fixed width font, and a decent size kate_view.
87     ensureKateViewVisible();
88     const QFont oldFont = kate_view->renderer()->config()->baseFont();
89     QFont fixedWidthFont("Courier");
90     fixedWidthFont.setStyleHint(QFont::TypeWriter);
91     Q_ASSERT_X(QFontInfo(fixedWidthFont).fixedPitch(), "setting up visual line up down tests", "Need a fixed pitch font!");
92     kate_view->renderer()->config()->setFont(fixedWidthFont);
93     const bool oldDynWordWrap = KateViewConfig::global()->dynWordWrap();
94     KateViewConfig::global()->setDynWordWrap(true);
95     const bool oldReplaceTabsDyn = kate_document->config()->replaceTabsDyn();
96     kate_document->config()->setReplaceTabsDyn(false);
97     const int oldTabWidth = kate_document->config()->tabWidth();
98     const int tabWidth = 5;
99     kate_document->config()->setTabWidth(tabWidth);
100     KateViewConfig::global()->setValue(KateViewConfig::ShowScrollbars, KateViewConfig::ScrollbarMode::AlwaysOn);
101 
102     // Compute the maximum width of text before line-wrapping sets it.
103     int textWrappingLength = 1;
104     while (true) {
105         QString text = QString("X").repeated(textWrappingLength) + ' ' + 'O';
106         const int posOfO = text.length() - 1;
107         kate_document->setText(text);
108         if (kate_view->cursorToCoordinate(Cursor(0, posOfO)).y() != kate_view->cursorToCoordinate(Cursor(0, 0)).y()) {
109             textWrappingLength++; // Number of x's, plus space.
110             break;
111         }
112         textWrappingLength++;
113     }
114     const QString fillsLineAndEndsOnSpace = QString("X").repeated(textWrappingLength - 1) + ' ';
115 
116     // Create a QString consisting of enough concatenated fillsLineAndEndsOnSpace to completely
117     // fill the viewport of the kate View.
118     QString fillsView = fillsLineAndEndsOnSpace;
119     while (true) {
120         kate_document->setText(fillsView);
121         const QString visibleText = kate_document->text(kate_view->visibleRange());
122         if (fillsView.length() > visibleText.length() * 2) { // Overkill.
123             break;
124         }
125         fillsView += fillsLineAndEndsOnSpace;
126     }
127     const int numVisibleLinesToFillView = fillsView.length() / fillsLineAndEndsOnSpace.length();
128 
129     {
130         // gk/ gj when there is only one line.
131         DoTest("foo", "lgkr.", "f.o");
132         DoTest("foo", "lgjr.", "f.o");
133     }
134 
135     {
136         // gk when sticky bit is set to the end.
137         const QString originalText = fillsLineAndEndsOnSpace.repeated(2);
138         QString expectedText = originalText;
139         kate_document->setText(originalText);
140         Q_ASSERT(expectedText[textWrappingLength - 1] == ' ');
141         expectedText[textWrappingLength - 1] = '.';
142         DoTest(originalText, "$gkr.", expectedText);
143     }
144 
145     {
146         // Regression test: more than fill the view up, go to end, and do gk on wrapped text (used to crash).
147         // First work out the text that will fill up the view.
148         QString expectedText = fillsView;
149         Q_ASSERT(expectedText[expectedText.length() - textWrappingLength - 1] == ' ');
150         expectedText[expectedText.length() - textWrappingLength - 1] = '.';
151 
152         DoTest(fillsView, "$gkr.", expectedText);
153     }
154 
155     {
156         // Jump down a few lines all in one go, where we have some variable length lines to navigate.
157         const int numVisualLinesOnLine[] = {3, 5, 2, 3};
158         const int numLines = sizeof(numVisualLinesOnLine) / sizeof(int);
159         const int startVisualLine = 2;
160         const int numberLinesToGoDownInOneGo = 10;
161 
162         int totalVisualLines = 0;
163         for (int i = 0; i < numLines; i++) {
164             totalVisualLines += numVisualLinesOnLine[i];
165         }
166 
167         QString startText;
168         for (int i = 0; i < numLines; i++) {
169             QString thisLine = fillsLineAndEndsOnSpace.repeated(numVisualLinesOnLine[i]);
170             // Replace trailing space with carriage return.
171             thisLine.chop(1);
172             thisLine.append('\n');
173             startText += thisLine;
174         }
175         QString expectedText = startText;
176         expectedText[((startVisualLine - 1) + numberLinesToGoDownInOneGo) * fillsLineAndEndsOnSpace.length()] = '.';
177 
178         Q_ASSERT(numberLinesToGoDownInOneGo + startVisualLine < totalVisualLines);
179         Q_ASSERT(numberLinesToGoDownInOneGo + startVisualLine < numVisibleLinesToFillView);
180         DoTest(startText, QString("gj").repeated(startVisualLine - 1) + QString::number(numberLinesToGoDownInOneGo) + "gjr.", expectedText);
181         // Now go up a few lines.
182         const int numLinesToGoBackUp = 7;
183         expectedText = startText;
184         expectedText[((startVisualLine - 1) + numberLinesToGoDownInOneGo - numLinesToGoBackUp) * fillsLineAndEndsOnSpace.length()] = '.';
185         DoTest(startText,
186                QString("gj").repeated(startVisualLine - 1) + QString::number(numberLinesToGoDownInOneGo) + "gj" + QString::number(numLinesToGoBackUp) + "gkr.",
187                expectedText);
188     }
189 
190     {
191         // Move down enough lines in one go to disappear off the view.
192         // About half-a-viewport past the end of the current viewport.
193         const int numberLinesToGoDown = numVisibleLinesToFillView * 3 / 2;
194         const int visualColumnNumber = 7;
195         Q_ASSERT(fillsLineAndEndsOnSpace.length() > visualColumnNumber);
196         QString expectedText = fillsView.repeated(2);
197         Q_ASSERT(expectedText[expectedText.length() - textWrappingLength - 1] == ' ');
198         expectedText[visualColumnNumber + fillsLineAndEndsOnSpace.length() * numberLinesToGoDown] = '.';
199 
200         DoTest(fillsView.repeated(2), QString("l").repeated(visualColumnNumber) + QString::number(numberLinesToGoDown) + "gjr.", expectedText);
201     }
202 
203     {
204         // Deal with dynamic wrapping and indented blocks - continuations of a line are "invisibly" idented by
205         // the same amount as the beginning of the line, and we have to subtract this indentation.
206         const QString unindentedFirstLine = "stickyhelper\n";
207         const int numIndentationSpaces = 5;
208         Q_ASSERT(textWrappingLength > numIndentationSpaces * 2 /* keep some wriggle room */);
209         const QString indentedFillsLineEndsOnSpace =
210             QString(" ").repeated(numIndentationSpaces) + QString("X").repeated(textWrappingLength - 1 - numIndentationSpaces) + ' ';
211         DoTest(unindentedFirstLine + indentedFillsLineEndsOnSpace + "LINE3",
212                QString("l").repeated(numIndentationSpaces) + "jgjr.",
213                unindentedFirstLine + indentedFillsLineEndsOnSpace + ".INE3");
214 
215         // The first, non-wrapped portion of the line is not invisibly indented, though, so ensure we don't mess that up.
216         QString expectedSecondLine = indentedFillsLineEndsOnSpace;
217         expectedSecondLine[numIndentationSpaces] = '.';
218         DoTest(unindentedFirstLine + indentedFillsLineEndsOnSpace + "LINE3",
219                QString("l").repeated(numIndentationSpaces) + "jgjgkr.",
220                unindentedFirstLine + expectedSecondLine + "LINE3");
221     }
222 
223     {
224         // Take into account any invisible indentation when setting the sticky column.
225         const int numIndentationSpaces = 5;
226         Q_ASSERT(textWrappingLength > numIndentationSpaces * 2 /* keep some wriggle room */);
227         const QString indentedFillsLineEndsOnSpace =
228             QString(" ").repeated(numIndentationSpaces) + QString("X").repeated(textWrappingLength - 1 - numIndentationSpaces) + ' ';
229         const int posInSecondWrappedLineToChange = 3;
230         QString expectedText = indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace;
231         expectedText[textWrappingLength + posInSecondWrappedLineToChange] = '.';
232         DoTest(indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
233                QString::number(textWrappingLength + posInSecondWrappedLineToChange) + "lgkgjr.",
234                expectedText);
235         // Make sure we can do this more than once (i.e. clear any flags that need clearing).
236         DoTest(indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
237                QString::number(textWrappingLength + posInSecondWrappedLineToChange) + "lgkgjr.",
238                expectedText);
239     }
240 
241     {
242         // Take into account any invisible indentation when setting the sticky column as above, but use tabs.
243         const QString indentedFillsLineEndsOnSpace = QString("\t") + QString("X").repeated(textWrappingLength - 1 - tabWidth) + ' ';
244         const int posInSecondWrappedLineToChange = 3;
245         QString expectedText = indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace;
246         expectedText[textWrappingLength - tabWidth + posInSecondWrappedLineToChange] = '.';
247         DoTest(indentedFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
248                QString("fXf ") + QString::number(posInSecondWrappedLineToChange) + "lgkgjr.",
249                expectedText);
250     }
251 
252     {
253         // Deal with the fact that j/ k may set a sticky column that is impossible to adhere to in visual mode because
254         // it is too high.
255         // Here, we have one dummy line and one wrapped line.  We start from the beginning of the wrapped line and
256         // move right until we wrap and end up at posInWrappedLineToChange one the second line of the wrapped line.
257         // 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
258         // visual line, and try to move a visual line up.
259         const QString dummyLineForUseWithK("dummylineforusewithk\n");
260         QString startText = dummyLineForUseWithK + fillsLineAndEndsOnSpace.repeated(2);
261         const int posInWrappedLineToChange = 3;
262         QString expectedText = startText;
263         expectedText[dummyLineForUseWithK.length() + posInWrappedLineToChange] = '.';
264         DoTest(startText, 'j' + QString::number(textWrappingLength + posInWrappedLineToChange) + "lkjgkr.", expectedText);
265     }
266 
267     {
268         // Ensure gj works in Visual mode.
269         Q_ASSERT(fillsLineAndEndsOnSpace.toLower() != fillsLineAndEndsOnSpace);
270         QString expectedText = fillsLineAndEndsOnSpace.toLower() + fillsLineAndEndsOnSpace;
271         expectedText[textWrappingLength] = expectedText[textWrappingLength].toLower();
272         DoTest(fillsLineAndEndsOnSpace.repeated(2), "vgjgu", expectedText);
273     }
274 
275     {
276         // Ensure gk works in Visual mode.
277         Q_ASSERT(fillsLineAndEndsOnSpace.toLower() != fillsLineAndEndsOnSpace);
278         DoTest(fillsLineAndEndsOnSpace.repeated(2), "$vgkgu", fillsLineAndEndsOnSpace + fillsLineAndEndsOnSpace.toLower());
279     }
280 
281     {
282         // Some tests for how well we handle things with real tabs.
283         QString beginsWithTabFillsLineEndsOnSpace = "\t";
284         while (beginsWithTabFillsLineEndsOnSpace.length() + (tabWidth - 1) < textWrappingLength - 1) {
285             beginsWithTabFillsLineEndsOnSpace += 'X';
286         }
287         beginsWithTabFillsLineEndsOnSpace += ' ';
288         const QString unindentedFirstLine = "stockyhelper\n";
289         const int posOnThirdLineToChange = 3;
290         QString expectedThirdLine = fillsLineAndEndsOnSpace;
291         expectedThirdLine[posOnThirdLineToChange] = '.';
292         DoTest(unindentedFirstLine + beginsWithTabFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
293                QString("l").repeated(tabWidth + posOnThirdLineToChange) + "gjgjr.",
294                unindentedFirstLine + beginsWithTabFillsLineEndsOnSpace + expectedThirdLine);
295 
296         // As above, but go down twice and return to the middle line.
297         const int posOnSecondLineToChange = 2;
298         QString expectedSecondLine = beginsWithTabFillsLineEndsOnSpace;
299         expectedSecondLine[posOnSecondLineToChange + 1 /* "+1" as we're not counting the leading tab as a pos */] = '.';
300         DoTest(unindentedFirstLine + beginsWithTabFillsLineEndsOnSpace + fillsLineAndEndsOnSpace,
301                QString("l").repeated(tabWidth + posOnSecondLineToChange) + "gjgjgkr.",
302                unindentedFirstLine + expectedSecondLine + fillsLineAndEndsOnSpace);
303     }
304 
305     // Restore back to how we were before.
306     kate_view->renderer()->config()->setFont(oldFont);
307     KateViewConfig::global()->setDynWordWrap(oldDynWordWrap);
308     kate_document->config()->setReplaceTabsDyn(oldReplaceTabsDyn);
309     kate_document->config()->setTabWidth(oldTabWidth);
310 }
311 
ScrollViewTests()312 void ViewTest::ScrollViewTests()
313 {
314     QSKIP("This is too unstable in Jenkins", SkipAll);
315 
316     // First of all, we have to initialize some sizes and fonts.
317     ensureKateViewVisible();
318     const QFont oldFont = kate_view->renderer()->config()->baseFont();
319     QFont fixedWidthFont("Monospace");
320     fixedWidthFont.setStyleHint(QFont::TypeWriter);
321     fixedWidthFont.setPixelSize(14);
322     Q_ASSERT_X(QFontInfo(fixedWidthFont).fixedPitch(), "setting up ScrollViewTests", "Need a fixed pitch font!");
323     kate_view->renderer()->config()->setFont(fixedWidthFont);
324 
325     // Generating our text here.
326     QString text;
327     for (int i = 0; i < 20; i++) {
328         text += "    aaaaaaaaaaaaaaaa\n";
329     }
330 
331     // TODO: fix the visibleRange's tests.
332 
333     // zz
334     BeginTest(text);
335     TestPressKey("10l9jzz");
336     QCOMPARE(kate_view->cursorPosition().line(), 9);
337     QCOMPARE(kate_view->cursorPosition().column(), 10);
338     QCOMPARE(kate_view->visibleRange(), Range(4, 0, 13, 20));
339     FinishTest(text);
340 
341     // z.
342     BeginTest(text);
343     TestPressKey("10l9jz.");
344     QCOMPARE(kate_view->cursorPosition().line(), 9);
345     QCOMPARE(kate_view->cursorPosition().column(), 4);
346     QCOMPARE(kate_view->visibleRange(), Range(4, 0, 13, 20));
347     FinishTest(text);
348 
349     // zt
350     BeginTest(text);
351     TestPressKey("10l9jzt");
352     QCOMPARE(kate_view->cursorPosition().line(), 9);
353     QCOMPARE(kate_view->cursorPosition().column(), 10);
354     QCOMPARE(kate_view->visibleRange(), Range(9, 0, 18, 20));
355     FinishTest(text);
356 
357     // z<cr>
358     BeginTest(text);
359     TestPressKey("10l9jz\\return");
360     QCOMPARE(kate_view->cursorPosition().line(), 9);
361     QCOMPARE(kate_view->cursorPosition().column(), 4);
362     QCOMPARE(kate_view->visibleRange(), Range(9, 0, 18, 20));
363     FinishTest(text);
364 
365     // zb
366     BeginTest(text);
367     TestPressKey("10l9jzb");
368     QCOMPARE(kate_view->cursorPosition().line(), 9);
369     QCOMPARE(kate_view->cursorPosition().column(), 10);
370     QCOMPARE(kate_view->visibleRange(), Range(0, 0, 9, 20));
371     FinishTest(text);
372 
373     // z-
374     BeginTest(text);
375     TestPressKey("10l9jz-");
376     QCOMPARE(kate_view->cursorPosition().line(), 9);
377     QCOMPARE(kate_view->cursorPosition().column(), 4);
378     QCOMPARE(kate_view->visibleRange(), Range(0, 0, 9, 20));
379     FinishTest(text);
380 
381     // Restore back to how we were before.
382     kate_view->renderer()->config()->setFont(oldFont);
383 }
384 
clipboardTests_data()385 void ViewTest::clipboardTests_data()
386 {
387     QTest::addColumn<QString>("text");
388     QTest::addColumn<QString>("commands");
389     QTest::addColumn<QString>("clipboard");
390 
391     QTest::newRow("yank") << "yyfoo\nbar"
392                           << "yy"
393                           << "yyfoo\n";
394     QTest::newRow("delete") << "ddfoo\nbar"
395                             << "dd"
396                             << "ddfoo\n";
397     QTest::newRow("yank empty line") << "\nbar"
398                                      << "yy" << QString();
399     QTest::newRow("delete word") << "word foo"
400                                  << "dw"
401                                  << "word ";
402     QTest::newRow("delete onechar word") << "w foo"
403                                          << "dw"
404                                          << "w ";
405     QTest::newRow("delete onechar") << "word foo"
406                                     << "dc" << QString();
407     QTest::newRow("delete empty lines") << " \t\n\n  \nfoo"
408                                         << "d3d" << QString();
409 }
410 
clipboardTests()411 void ViewTest::clipboardTests()
412 {
413     QFETCH(QString, text);
414     QFETCH(QString, commands);
415     QFETCH(QString, clipboard);
416 
417     QApplication::clipboard()->clear();
418     BeginTest(text);
419     TestPressKey(commands);
420     QCOMPARE(QApplication::clipboard()->text(), clipboard);
421 }
422 
rangesOnFirstLine()423 QVector<Kate::TextRange *> ViewTest::rangesOnFirstLine()
424 {
425     return kate_document->buffer().rangesForLine(0, kate_view, true);
426 }
427