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