File indexing completed on 2024-09-15 12:02:36
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "multicursortest.h" 0009 0010 #include <QClipboard> 0011 #include <katebuffer.h> 0012 #include <kateconfig.h> 0013 #include <katedocument.h> 0014 #include <kateglobal.h> 0015 #include <kateundomanager.h> 0016 #include <kateview.h> 0017 #include <kateviewinternal.h> 0018 #include <ktexteditor/message.h> 0019 #include <ktexteditor/movingcursor.h> 0020 0021 #include <QTest> 0022 0023 using namespace KTextEditor; 0024 0025 QTEST_MAIN(MulticursorTest) 0026 0027 struct DocAndView { 0028 DocumentPrivate *doc; 0029 ViewPrivate *view; 0030 0031 ~DocAndView() 0032 { 0033 delete view; 0034 delete doc; 0035 } 0036 }; 0037 0038 DocAndView createDocAndView(const QString &text, int line, int column) 0039 { 0040 auto doc = new DocumentPrivate(); 0041 doc->setText(text); 0042 auto view = new ViewPrivate(doc, nullptr); 0043 view->setCursorPosition({line, column}); 0044 return {doc, view}; 0045 } 0046 0047 template<typename Cont> 0048 bool isSorted(const Cont &c) 0049 { 0050 return std::is_sorted(c.begin(), c.end()); 0051 } 0052 0053 MulticursorTest::MulticursorTest() 0054 : QObject() 0055 { 0056 KTextEditor::EditorPrivate::enableUnitTestMode(); 0057 } 0058 0059 MulticursorTest::~MulticursorTest() 0060 { 0061 } 0062 0063 static QWidget *findViewInternal(KTextEditor::ViewPrivate *view) 0064 { 0065 for (QObject *child : view->children()) { 0066 if (child->metaObject()->className() == QByteArrayLiteral("KateViewInternal")) { 0067 return qobject_cast<QWidget *>(child); 0068 } 0069 } 0070 return nullptr; 0071 } 0072 0073 static void clickAtPosition(ViewPrivate *view, QObject *internalView, Cursor pos, Qt::KeyboardModifiers m) 0074 { 0075 QPoint p = view->cursorToCoordinate(pos); 0076 QVERIFY(p.x() >= 0 && p.y() >= 0); 0077 auto me = QMouseEvent(QEvent::MouseButtonPress, p, Qt::LeftButton, Qt::LeftButton, m); 0078 QCoreApplication::sendEvent(internalView, &me); 0079 } 0080 0081 void MulticursorTest::testKillline() 0082 { 0083 KTextEditor::DocumentPrivate doc; 0084 doc.insertLines(0, {"foo", "bar", "baz"}); 0085 KTextEditor::ViewPrivate *view = new KTextEditor::ViewPrivate(&doc, nullptr); 0086 view->setCursorPositionInternal(KTextEditor::Cursor(0, 0)); 0087 view->addSecondaryCursor(KTextEditor::Cursor(1, 0)); 0088 view->addSecondaryCursor(KTextEditor::Cursor(2, 0)); 0089 QVERIFY(isSorted(view->secondaryCursors())); 0090 0091 view->killLine(); 0092 0093 QCOMPARE(doc.text(), QString()); 0094 } 0095 0096 void MulticursorTest::insertRemoveText() 0097 { 0098 auto [doc, view] = createDocAndView("foo\nbar\nfoo\n", 0, 0); 0099 QObject *internalView = findViewInternal(view); 0100 QVERIFY(internalView); 0101 0102 { // Same line 0103 view->addSecondaryCursor({0, 1}); 0104 view->addSecondaryCursor({0, 2}); 0105 view->addSecondaryCursor({0, 3}); 0106 QVERIFY(isSorted(view->secondaryCursors())); 0107 QCOMPARE(view->secondaryCursors().size(), 3); 0108 QKeyEvent ke(QKeyEvent::KeyPress, Qt::Key_L, Qt::NoModifier, QStringLiteral("L")); 0109 QCoreApplication::sendEvent(internalView, &ke); 0110 0111 QCOMPARE(doc->line(0), QStringLiteral("LfLoLoL")); 0112 0113 // Removal 0114 view->backspace(); 0115 QCOMPARE(doc->line(0), QStringLiteral("foo")); 0116 0117 view->clearSecondaryCursors(); 0118 } 0119 0120 { // Different lines 0121 view->setCursorPosition(Cursor(0, 0)); 0122 view->addSecondaryCursor({1, 0}); 0123 view->addSecondaryCursor({2, 0}); 0124 QKeyEvent ke(QKeyEvent::KeyPress, Qt::Key_L, Qt::NoModifier, QStringLiteral("L")); 0125 QCoreApplication::sendEvent(internalView, &ke); 0126 0127 QCOMPARE(doc->line(0), QStringLiteral("Lfoo")); 0128 QCOMPARE(doc->line(1), QStringLiteral("Lbar")); 0129 QCOMPARE(doc->line(2), QStringLiteral("Lfoo")); 0130 0131 view->backspace(); 0132 QVERIFY(isSorted(view->secondaryCursors())); 0133 0134 QCOMPARE(doc->line(0), QStringLiteral("foo")); 0135 QCOMPARE(doc->line(1), QStringLiteral("bar")); 0136 QCOMPARE(doc->line(2), QStringLiteral("foo")); 0137 0138 QVERIFY(isSorted(view->secondaryCursors())); 0139 view->clearSecondaryCursors(); 0140 } 0141 0142 // Three empty lines 0143 doc->setText(QStringLiteral("\n\n\n")); 0144 view->setCursorPosition({0, 0}); 0145 view->addSecondaryCursor({1, 0}); 0146 view->addSecondaryCursor({2, 0}); 0147 QVERIFY(isSorted(view->secondaryCursors())); 0148 0149 // cursors should merge 0150 view->backspace(); 0151 QCOMPARE(view->secondaryCursors().size(), 0); 0152 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0153 } 0154 0155 void MulticursorTest::backspace() 0156 { 0157 auto [doc, view] = createDocAndView("foo\nbar\nbaz", 0, 3); 0158 QVector<KTextEditor::ViewPrivate::PlainSecondaryCursor> cursors; 0159 0160 { 0161 // Mixed cursors, one doesn't have selection 0162 // the two below have selection 0163 cursors.append({Cursor(1, 3), Range(1, 0, 1, 3)}); 0164 cursors.append({Cursor(2, 3), Range(2, 0, 2, 3)}); 0165 view->addSecondaryCursorsWithSelection(cursors); 0166 0167 // Pressing backspace should only remove selected text 0168 view->backspace(); 0169 QVERIFY(isSorted(view->secondaryCursors())); 0170 QCOMPARE(doc->text(), QStringLiteral("foo\n\n")); 0171 0172 // Pressing backspace again 0173 view->backspace(); 0174 QVERIFY(view->secondaryCursors().empty()); 0175 QCOMPARE(doc->text(), QStringLiteral("fo")); 0176 } 0177 0178 { 0179 // No selection 0180 doc->setText("foo\nbar\nbaz"); 0181 view->setCursors({Cursor(0, 3), Cursor(1, 3), Cursor(2, 3)}); 0182 view->backspace(); 0183 QCOMPARE(doc->text(), QStringLiteral("fo\nba\nba")); 0184 view->backspace(); 0185 view->backspace(); 0186 QCOMPARE(doc->text(), QStringLiteral("\n\n")); 0187 QCOMPARE(view->cursors().size(), 3); 0188 } 0189 } 0190 0191 void MulticursorTest::keyDelete() 0192 { 0193 auto [doc, view] = createDocAndView("foo\nbar\nbaz", 0, 0); 0194 QVector<KTextEditor::ViewPrivate::PlainSecondaryCursor> cursors; 0195 0196 { 0197 // Mixed cursors, one doesn't have selection 0198 // the two below have selection 0199 cursors.append({Cursor(1, 0), Range(1, 0, 1, 3)}); 0200 cursors.append({Cursor(2, 0), Range(2, 0, 2, 3)}); 0201 view->addSecondaryCursorsWithSelection(cursors); 0202 0203 // Pressing del should only remove selected text 0204 view->keyDelete(); 0205 QVERIFY(isSorted(view->secondaryCursors())); 0206 QCOMPARE(doc->text(), QStringLiteral("foo\n\n")); 0207 0208 // Pressing del again 0209 view->keyDelete(); 0210 QCOMPARE(view->secondaryCursors().size(), 1); 0211 QCOMPARE(doc->text(), QStringLiteral("oo\n")); 0212 } 0213 0214 { 0215 // No selection 0216 doc->setText("foo\nbar\nbaz"); 0217 view->setCursors({Cursor(0, 0), Cursor(1, 0), Cursor(2, 0)}); 0218 view->keyDelete(); 0219 QCOMPARE(doc->text(), QStringLiteral("oo\nar\naz")); 0220 view->keyDelete(); 0221 view->keyDelete(); 0222 QCOMPARE(doc->text(), QStringLiteral("\n\n")); 0223 QCOMPARE(view->cursors().size(), 3); 0224 } 0225 } 0226 0227 void MulticursorTest::testUndoRedo() 0228 { 0229 auto [doc, view] = createDocAndView("foo\nfoo", 0, 3); 0230 0231 // single cursor backspace 0232 view->backspace(); 0233 QCOMPARE(doc->text(), QStringLiteral("fo\nfoo")); 0234 doc->undoManager()->undoSafePoint(); 0235 0236 // backspace with 2 cursors 0237 view->setCursors({view->cursorPosition(), Cursor{1, 3}}); 0238 view->backspace(); 0239 QCOMPARE(doc->text(), QStringLiteral("f\nfo")); 0240 0241 view->doc()->undo(); 0242 QCOMPARE(doc->text(), QStringLiteral("fo\nfoo")); 0243 QCOMPARE(view->secondaryCursors().size(), 1); 0244 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 3)); 0245 0246 // Another undo, multicursor should be gone 0247 view->doc()->undo(); 0248 QCOMPARE(doc->text(), QStringLiteral("foo\nfoo")); 0249 QCOMPARE(view->secondaryCursors().size(), 0); 0250 0251 // One redo 0252 view->doc()->redo(); 0253 QCOMPARE(doc->text(), QStringLiteral("fo\nfoo")); 0254 0255 // Second redo, multicursor should be back 0256 view->doc()->redo(); 0257 QCOMPARE(doc->text(), QStringLiteral("f\nfo")); 0258 QCOMPARE(view->secondaryCursors().size(), 1); 0259 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 2)); 0260 } 0261 0262 void MulticursorTest::testUndoRedoWithSelection() 0263 { 0264 auto [doc, view] = createDocAndView("foo\nfoo", 0, 3); 0265 view->setCursors({Cursor(0, 3), Cursor(1, 3)}); 0266 0267 // select a word & remove it 0268 view->shiftWordLeft(); 0269 view->backspace(); 0270 0271 QCOMPARE(doc->text(), QStringLiteral("\n")); 0272 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0273 QCOMPARE(view->secondaryCursors().size(), 1); 0274 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 0)); 0275 0276 view->doc()->undo(); 0277 0278 QCOMPARE(doc->text(), QStringLiteral("foo\nfoo")); 0279 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0280 QCOMPARE(view->secondaryCursors().size(), 1); 0281 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 0)); 0282 QCOMPARE(*view->secondaryCursors().at(0).range, Range(1, 0, 1, 3)); 0283 QCOMPARE(view->secondaryCursors().at(0).anchor, Cursor(1, 3)); 0284 } 0285 0286 void MulticursorTest::keyReturnIndentTest() 0287 { 0288 auto [doc, view] = createDocAndView("\n\n", 0, 0); 0289 QCOMPARE(doc->lines(), 3); 0290 doc->setMode(QStringLiteral("C++")); 0291 view->config()->setValue(KateViewConfig::AutoBrackets, true); 0292 0293 view->addSecondaryCursorDown(); 0294 view->addSecondaryCursorDown(); 0295 QCOMPARE(view->secondaryCursors().size(), 2); 0296 QVERIFY(isSorted(view->secondaryCursors())); 0297 0298 doc->typeChars(view, QStringLiteral("{")); 0299 QCOMPARE(doc->text(), QStringLiteral("{}\n{}\n{}")); 0300 QCOMPARE(view->secondaryCursors().size(), 2); 0301 QVERIFY(isSorted(view->secondaryCursors())); 0302 0303 view->keyReturn(); 0304 QCOMPARE(doc->text(), QStringLiteral("{\n \n}\n{\n \n}\n{\n \n}")); 0305 } 0306 0307 void MulticursorTest::wrapSelectionWithCharsTest() 0308 { 0309 auto [doc, view] = createDocAndView("foo\nfoo\nfoo", 0, 3); 0310 0311 view->addSecondaryCursorDown(); 0312 view->addSecondaryCursorDown(); 0313 QCOMPARE(view->secondaryCursors().size(), 2); 0314 0315 view->shiftWordLeft(); 0316 doc->typeChars(view, QStringLiteral("{")); 0317 QCOMPARE(doc->text(), QStringLiteral("{foo}\n{foo}\n{foo}")); 0318 } 0319 0320 void MulticursorTest::insertAutoBrackets() 0321 { 0322 auto [doc, view] = createDocAndView("hello\nhello", 0, 0); 0323 QCOMPARE(doc->lines(), 2); 0324 doc->setMode(QStringLiteral("C++")); 0325 view->config()->setValue(KateViewConfig::AutoBrackets, true); 0326 view->setSecondaryCursors({Cursor(0, 0), Cursor(1, 0)}); 0327 QCOMPARE(view->cursors().size(), 2); 0328 0329 doc->typeChars(view, QStringLiteral("(")); 0330 QCOMPARE(doc->text(), QStringLiteral("(hello\n(hello")); 0331 } 0332 0333 void MulticursorTest::testCreateMultiCursor() 0334 { 0335 auto [doc, view] = createDocAndView("foo\nbar\nfoo\n", 0, 0); 0336 0337 QObject *internalView = findViewInternal(view); 0338 QVERIFY(internalView); 0339 0340 // Alt + click should add a cursor 0341 auto primary = view->cursorPosition(); 0342 clickAtPosition(view, internalView, {1, 0}, Qt::AltModifier); 0343 QCOMPARE(view->secondaryCursors().size(), 1); 0344 // primary cursor moved to the position which is clicked 0345 QCOMPARE(view->cursorPosition(), Cursor(1, 0)); 0346 // secondary was created where primary cursor was 0347 QCOMPARE(view->secondaryCursors().at(0).cursor(), primary); 0348 0349 // Alt + click at the same point should remove the cursor 0350 clickAtPosition(view, internalView, {1, 0}, Qt::AltModifier); 0351 QCOMPARE(view->secondaryCursors().size(), 0); 0352 0353 // Create two cursors using alt+click 0354 clickAtPosition(view, internalView, {1, 0}, Qt::AltModifier); 0355 clickAtPosition(view, internalView, {1, 1}, Qt::AltModifier); 0356 QCOMPARE(view->secondaryCursors().size(), 2); 0357 QVERIFY(isSorted(view->secondaryCursors())); 0358 0359 // now simple click => should remove all secondary cursors 0360 clickAtPosition(view, internalView, {1, 0}, Qt::NoModifier); 0361 QCOMPARE(view->secondaryCursors().size(), 0); 0362 QCOMPARE(view->cursorPosition(), Cursor(1, 0)); 0363 } 0364 0365 void MulticursorTest::testCreateMultiCursorFromSelection() 0366 { 0367 auto [doc, view] = createDocAndView("foo\nbar\nfoo", 2, 3); 0368 view->setSelection(KTextEditor::Range(0, 0, 2, 3)); 0369 // move primary cursor to beginning of line, so we can check whether it is moved to end of line 0370 view->setCursorPosition({view->cursorPosition().line(), 0}); 0371 view->createMultiCursorsFromSelection(); 0372 QVERIFY(isSorted(view->secondaryCursors())); 0373 QCOMPARE(view->cursorPosition().column(), 3); 0374 0375 const auto &cursors = view->secondaryCursors(); 0376 QCOMPARE(cursors.size(), doc->lines() - 1); // 1 cursor is primary, not included 0377 0378 int i = 0; 0379 for (const auto &c : cursors) { 0380 QCOMPARE(c.cursor(), KTextEditor::Cursor(i, 3)); 0381 i++; 0382 } 0383 } 0384 0385 void MulticursorTest::testMulticursorToggling() 0386 { 0387 auto [doc, view] = createDocAndView("foo\nbar\nfoo", 0, 0); 0388 view->setSelections({Range(0, 0, 0, 3), Range(1, 0, 1, 3)}); 0389 QCOMPARE(view->selectionRanges().size(), 2); 0390 0391 // Trying to add a cursor in one of the selection region 0392 // will remove it 0393 view->addSecondaryCursor(Cursor(0, 2)); 0394 QCOMPARE(view->selectionRanges().size(), 1); 0395 0396 // Trying to toggle last remaining selection will do nothing 0397 view->addSecondaryCursor(Cursor(1, 2)); 0398 QCOMPARE(view->selectionRanges().size(), 1); 0399 } 0400 0401 void MulticursorTest::moveCharTest() 0402 { 0403 auto [doc, view] = createDocAndView("foo\nbar\nfoo\n", 0, 0); 0404 view->setCursors({Cursor(0, 0), Cursor(1, 0)}); 0405 0406 // Simple left right 0407 view->cursorRight(); 0408 QCOMPARE(view->cursorPosition(), Cursor(0, 1)); 0409 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(1, 1)); 0410 0411 view->cursorLeft(); 0412 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0413 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(1, 0)); 0414 0415 // Shift pressed 0416 view->shiftCursorRight(); 0417 QCOMPARE(view->cursorPosition(), Cursor(0, 1)); 0418 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(1, 1)); 0419 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(1, 0, 1, 1)); 0420 0421 view->shiftCursorLeft(); 0422 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0423 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(1, 0)); 0424 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(1, 0, 1, 0)); 0425 0426 view->clearSecondaryCursors(); 0427 0428 // Selection merge test => merge into primary cursor 0429 view->setCursors({Cursor(0, 2), Cursor(0, 3)}); // fo|o| 0430 // Two shift left should result in one cursor 0431 view->shiftCursorLeft(); 0432 view->shiftCursorLeft(); 0433 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0434 QCOMPARE(view->secondaryCursors().size(), 0); 0435 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0436 0437 view->clearSelection(); 0438 0439 // Selection merge test => merge primary into multi => multi becomes primary 0440 view->setCursorPosition({0, 0}); // fo|o 0441 view->addSecondaryCursor({0, 1}); // foo| 0442 // Two shift left should result in one cursor 0443 view->shiftCursorRight(); 0444 view->shiftCursorRight(); 0445 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0446 QCOMPARE(view->secondaryCursors().size(), 0); 0447 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0448 } 0449 0450 void MulticursorTest::moveCharInFirstOrLastLineTest() 0451 { 0452 auto [doc, view] = createDocAndView("foo", 0, 0); 0453 view->addSecondaryCursor({0, 1}); 0454 // |f|oo 0455 0456 view->cursorLeft(); 0457 QCOMPARE(view->secondaryCursors().size(), 0); 0458 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0459 0460 view->setCursorPosition({0, 2}); 0461 view->addSecondaryCursor({0, 3}); 0462 view->cursorRight(); 0463 QCOMPARE(view->secondaryCursors().size(), 0); 0464 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0465 } 0466 0467 void MulticursorTest::moveWordTest() 0468 { 0469 auto [doc, view] = createDocAndView("foo\nbar\nfoo\n", 0, 0); 0470 view->setCursors({Cursor(0, 0), Cursor(1, 0)}); 0471 0472 // Simple left right 0473 view->wordRight(); 0474 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0475 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 3)); 0476 0477 view->wordLeft(); 0478 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0479 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 0)); 0480 0481 // Shift pressed 0482 view->shiftWordRight(); 0483 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0484 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 3)); 0485 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(1, 0, 1, 3)); 0486 0487 view->shiftWordLeft(); 0488 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0489 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 0)); 0490 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(1, 0, 1, 0)); 0491 0492 view->clearSecondaryCursors(); 0493 0494 // Two cursors in same word, => word movement should merge them (sel) 0495 view->setCursorPosition({0, 0}); // |foo 0496 view->addSecondaryCursor({0, 1}); // f|oo 0497 view->shiftWordRight(); // foo| 0498 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0499 QCOMPARE(view->secondaryCursors().size(), 0); 0500 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0501 0502 // Three cursors in same word, => word movement should merge them (no sel) 0503 view->setCursorPosition({0, 3}); // foo| 0504 view->addSecondaryCursor({0, 2}); // fo|o 0505 view->addSecondaryCursor({0, 1}); // f|oo 0506 view->wordLeft(); // foo| 0507 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0508 QCOMPARE(view->secondaryCursors().size(), 0); 0509 } 0510 0511 void MulticursorTest::homeEndKeyTest() 0512 { 0513 auto [doc, view] = createDocAndView("foo\nbar\nfoo\n", 0, 0); 0514 view->setCursors({Cursor(0, 0), Cursor(0, 1)}); 0515 0516 // Two cursor in same line => home should merge them 0517 view->home(); 0518 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0519 QCOMPARE(view->secondaryCursors().size(), 0); 0520 0521 // Two cursor in same line => end should merge them 0522 view->setCursors({Cursor(0, 0), Cursor(0, 1)}); 0523 view->end(); 0524 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0525 QCOMPARE(view->secondaryCursors().size(), 0); 0526 0527 view->setCursors({Cursor(0, 3), Cursor(1, 0)}); 0528 view->end(); 0529 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0530 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 3)); 0531 0532 view->clearSecondaryCursors(); 0533 0534 view->setCursors({Cursor(0, 3), Cursor(1, 3)}); 0535 view->home(); 0536 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0537 QCOMPARE(*view->secondaryCursors().at(0).pos, Cursor(1, 0)); 0538 } 0539 0540 void MulticursorTest::moveUpDown() 0541 { 0542 /** TEST UP **/ 0543 auto [doc, view] = createDocAndView("foo\nbar\nfoo", 0, 0); 0544 0545 view->setSecondaryCursors({Cursor(1, 0), Cursor(2, 0)}); 0546 QCOMPARE(view->secondaryCursors().size(), 2); 0547 QVERIFY(isSorted(view->secondaryCursors())); 0548 0549 view->up(); 0550 QCOMPARE(view->secondaryCursors().size(), 1); 0551 0552 view->up(); 0553 QCOMPARE(view->secondaryCursors().size(), 0); 0554 0555 /** TEST DOWN **/ 0556 0557 view->setSecondaryCursors({Cursor(1, 0), Cursor(2, 0)}); 0558 QCOMPARE(view->secondaryCursors().size(), 2); 0559 0560 view->down(); 0561 QCOMPARE(view->secondaryCursors().size(), 2); // last cursor moves to end of line 0562 QCOMPARE(*view->secondaryCursors().at(1).pos, Cursor(2, 3)); 0563 QVERIFY(isSorted(view->secondaryCursors())); 0564 0565 view->down(); 0566 QCOMPARE(view->secondaryCursors().size(), 1); 0567 0568 view->down(); 0569 QCOMPARE(view->secondaryCursors().size(), 0); 0570 QCOMPARE(view->cursorPosition(), Cursor(2, 3)); 0571 } 0572 0573 void MulticursorTest::testSelectionMerge() 0574 { 0575 // 8 lines 0576 { 0577 // Left movement, cursor at top 0578 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 0, 3); 0579 0580 view->selectAll(); 0581 view->createMultiCursorsFromSelection(); 0582 QVERIFY(isSorted(view->secondaryCursors())); 0583 0584 QCOMPARE(view->secondaryCursors().size(), 6); 0585 0586 view->shiftWordLeft(); 0587 QVERIFY(isSorted(view->secondaryCursors())); 0588 view->shiftWordLeft(); 0589 QVERIFY(isSorted(view->secondaryCursors())); 0590 view->shiftWordLeft(); 0591 0592 QCOMPARE(view->secondaryCursors().size(), 0); 0593 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0594 QCOMPARE(view->selectionRange(), Range(0, 0, 6, 3)); 0595 } 0596 0597 { 0598 // Left movement, cursor at bottom 0599 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 6, 3); 0600 0601 view->selectAll(); 0602 view->createMultiCursorsFromSelection(); 0603 QVERIFY(isSorted(view->secondaryCursors())); 0604 0605 QCOMPARE(view->secondaryCursors().size(), 6); 0606 QCOMPARE(view->cursorPosition(), Cursor(6, 3)); 0607 0608 view->shiftWordLeft(); 0609 QVERIFY(isSorted(view->secondaryCursors())); 0610 view->shiftWordLeft(); 0611 QVERIFY(isSorted(view->secondaryCursors())); 0612 view->shiftWordLeft(); 0613 0614 QCOMPARE(view->secondaryCursors().size(), 0); 0615 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0616 QCOMPARE(view->selectionRange(), Range(0, 0, 6, 3)); 0617 } 0618 0619 { 0620 // Left word movement, cursor in the middle 0621 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 3, 3); 0622 0623 for (int i = 0; i < 10; ++i) { 0624 view->addSecondaryCursorUp(); 0625 view->addSecondaryCursorDown(); 0626 } 0627 0628 QCOMPARE(view->secondaryCursors().size(), 6); 0629 0630 view->shiftWordLeft(); 0631 view->shiftWordLeft(); 0632 view->shiftWordLeft(); 0633 0634 QCOMPARE(view->secondaryCursors().size(), 0); 0635 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0636 QCOMPARE(view->selectionRange(), Range(0, 0, 6, 3)); 0637 } 0638 0639 { 0640 // Left word + char movement, cursor in the middle 0641 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 3, 3); 0642 0643 view->addSecondaryCursorUp(); 0644 view->addSecondaryCursorUp(); 0645 view->addSecondaryCursorDown(); 0646 view->addSecondaryCursorDown(); 0647 QVERIFY(isSorted(view->secondaryCursors())); 0648 0649 QCOMPARE(view->secondaryCursors().size(), 4); 0650 0651 view->shiftWordLeft(); 0652 view->shiftCursorLeft(); 0653 view->shiftCursorLeft(); 0654 view->shiftCursorLeft(); 0655 0656 QCOMPARE(view->secondaryCursors().size(), 0); 0657 QCOMPARE(view->cursorPosition(), Cursor(0, 1)); 0658 QCOMPARE(view->selectionRange(), Range(0, 1, 5, 3)); 0659 } 0660 0661 { 0662 // Right movement, cursor at bottom line 0663 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 6, 0); 0664 0665 for (int i = 0; i < 10; ++i) { 0666 view->addSecondaryCursorUp(); 0667 } 0668 0669 QCOMPARE(view->secondaryCursors().size(), 6); 0670 0671 view->shiftWordRight(); 0672 QVERIFY(isSorted(view->secondaryCursors())); 0673 view->shiftWordRight(); 0674 QVERIFY(isSorted(view->secondaryCursors())); 0675 view->shiftWordRight(); 0676 0677 QCOMPARE(view->secondaryCursors().size(), 0); 0678 QCOMPARE(view->cursorPosition(), Cursor(6, 3)); 0679 QCOMPARE(view->selectionRange(), Range(0, 0, 6, 3)); 0680 } 0681 0682 { 0683 // Right movement, cursor at top line 0684 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 0, 0); 0685 0686 for (int i = 0; i < 10; ++i) { 0687 view->addSecondaryCursorDown(); 0688 } 0689 0690 QCOMPARE(view->secondaryCursors().size(), 6); 0691 0692 view->shiftWordRight(); 0693 view->shiftWordRight(); 0694 view->shiftWordRight(); 0695 0696 QCOMPARE(view->secondaryCursors().size(), 0); 0697 QCOMPARE(view->cursorPosition(), Cursor(6, 3)); 0698 QCOMPARE(view->selectionRange(), Range(0, 0, 6, 3)); 0699 } 0700 0701 { 0702 // Right word + char movement, cursor in the middle 0703 auto [doc, view] = createDocAndView("foo\nfoo\nfoo\nfoo\nfoo\nfoo\nfoo", 3, 0); 0704 0705 view->addSecondaryCursorUp(); 0706 view->addSecondaryCursorUp(); 0707 view->addSecondaryCursorDown(); 0708 view->addSecondaryCursorDown(); 0709 QVERIFY(isSorted(view->secondaryCursors())); 0710 0711 QCOMPARE(view->secondaryCursors().size(), 4); 0712 0713 view->shiftWordRight(); 0714 QVERIFY(isSorted(view->secondaryCursors())); 0715 view->shiftCursorRight(); 0716 QVERIFY(isSorted(view->secondaryCursors())); 0717 view->shiftCursorRight(); 0718 QVERIFY(isSorted(view->secondaryCursors())); 0719 view->shiftCursorRight(); 0720 0721 QCOMPARE(view->secondaryCursors().size(), 0); 0722 QCOMPARE(view->cursorPosition(), Cursor(6, 2)); 0723 QCOMPARE(view->selectionRange(), Range(1, 0, 6, 2)); 0724 } 0725 } 0726 0727 void MulticursorTest::findNextOccurenceTest() 0728 { 0729 auto [doc, view] = createDocAndView("foo\nbar\nfoo\nfoo", 0, 0); 0730 0731 // No selection 0732 view->findNextOccurunceAndSelect(); 0733 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0734 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0735 QCOMPARE(view->secondaryCursors().size(), 0); 0736 0737 view->clearSelection(); 0738 // with selection 0739 view->setSelection(Range(0, 0, 0, 3)); 0740 view->findNextOccurunceAndSelect(); 0741 QCOMPARE(view->secondaryCursors().size(), 1); 0742 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(0, 3)); 0743 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(0, 0, 0, 3)); 0744 // primary cursor has the last selection 0745 QCOMPARE(view->cursorPosition(), Cursor(2, 3)); 0746 QCOMPARE(view->selectionRange(), Range(2, 0, 2, 3)); 0747 0748 // find another 0749 view->findNextOccurunceAndSelect(); 0750 QCOMPARE(view->secondaryCursors().size(), 2); 0751 QVERIFY(isSorted(view->secondaryCursors())); 0752 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(0, 3)); 0753 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(0, 0, 0, 3)); 0754 QCOMPARE(view->secondaryCursors().at(1).cursor(), Cursor(2, 3)); 0755 QCOMPARE(view->secondaryCursors().at(1).range->toRange(), Range(2, 0, 2, 3)); 0756 // primary cursor has the last selection 0757 QCOMPARE(view->cursorPosition(), Cursor(3, 3)); 0758 QCOMPARE(view->selectionRange(), Range(3, 0, 3, 3)); 0759 0760 // Try to find another, there is none so nothing should change 0761 // except that the primary cursor position is moved to newest found 0762 view->findNextOccurunceAndSelect(); 0763 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0764 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0765 QVERIFY(isSorted(view->secondaryCursors())); 0766 } 0767 0768 void MulticursorTest::findAllOccurenceTest() 0769 { 0770 auto [doc, view] = createDocAndView("foo\nbar\nfoo\nfoo", 0, 0); 0771 0772 // No selection 0773 view->findAllOccuruncesAndSelect(); 0774 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0775 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0776 QCOMPARE(view->secondaryCursors().size(), 2); 0777 // first 0778 QCOMPARE(view->secondaryCursors().at(0).cursor(), Cursor(2, 3)); 0779 QCOMPARE(view->secondaryCursors().at(0).range->toRange(), Range(2, 0, 2, 3)); 0780 // second 0781 QCOMPARE(view->secondaryCursors().at(1).cursor(), Cursor(3, 3)); 0782 QCOMPARE(view->secondaryCursors().at(1).range->toRange(), Range(3, 0, 3, 3)); 0783 0784 // Try to find another, there is none so nothing should change 0785 view->findAllOccuruncesAndSelect(); 0786 QCOMPARE(view->cursorPosition(), Cursor(0, 3)); 0787 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0788 } 0789 0790 void MulticursorTest::testMultiCopyPaste() 0791 { 0792 // Create two docs, copy from one to the other 0793 { 0794 auto [doc, view] = createDocAndView("foo\nbar\nfoo\nfoo", 0, 0); 0795 view->addSecondaryCursor({1, 0}); 0796 view->addSecondaryCursor({2, 0}); 0797 view->addSecondaryCursor({3, 0}); 0798 view->shiftWordRight(); 0799 view->copy(); 0800 } 0801 0802 // Same number of cursors when pasting => each line gets pasted into matching cursor postion 0803 { 0804 KTextEditor::DocumentPrivate doc; 0805 doc.setText("\n\n\n\n"); 0806 KTextEditor::ViewPrivate *v = new KTextEditor::ViewPrivate(&doc, nullptr); 0807 v->setCursorPosition({0, 0}); 0808 v->addSecondaryCursor({1, 0}); 0809 v->addSecondaryCursor({2, 0}); 0810 v->addSecondaryCursor({3, 0}); 0811 v->paste(); 0812 QCOMPARE(doc.text(), QStringLiteral("foo\nbar\nfoo\nfoo\n")); 0813 0814 // Different number of cursors 0815 v->clear(); 0816 QVERIFY(doc.clear()); 0817 doc.setText(QStringLiteral("\n\n")); 0818 v->setCursorPosition({0, 0}); 0819 v->addSecondaryCursor({1, 0}); 0820 QCOMPARE(v->secondaryCursors().size(), 1); 0821 0822 v->paste(); 0823 QString text = doc.text(); 0824 QCOMPARE(text, QStringLiteral("foo\nbar\nfoo\nfoo\nfoo\nbar\nfoo\nfoo\n")); 0825 } 0826 } 0827 0828 void MulticursorTest::testSelectionTextOrdering() 0829 { 0830 auto [doc, view] = createDocAndView("foo\nbar\nfoo\nfoo", 0, 0); 0831 view->addSecondaryCursor({1, 0}); 0832 view->addSecondaryCursor({2, 0}); 0833 view->shiftWordRight(); 0834 QVERIFY(isSorted(view->secondaryCursors())); 0835 0836 QString selText = view->selectionText(); 0837 QCOMPARE(selText, QStringLiteral("foo\nbar\nfoo")); 0838 0839 view->copy(); 0840 QCOMPARE(QApplication::clipboard()->text(QClipboard::Clipboard), selText); 0841 } 0842 0843 void MulticursorTest::testViewClear() 0844 { 0845 auto [doc, view] = createDocAndView("foo\nbar", 0, 0); 0846 view->addSecondaryCursor({1, 0}); 0847 QCOMPARE(view->secondaryCursors().size(), 1); 0848 view->clear(); 0849 QCOMPARE(view->secondaryCursors().size(), 0); 0850 } 0851 0852 void MulticursorTest::testSetGetCursors() 0853 { 0854 using Cursors = QVector<Cursor>; 0855 // Simple check 0856 { 0857 auto [doc, view] = createDocAndView("foo\nbar\nfoo\nfoo", 0, 0); 0858 0859 // primary included 0860 QCOMPARE(view->cursors(), Cursors{Cursor(0, 0)}); 0861 0862 const Cursors cursors = {{0, 1}, {1, 1}, {2, 1}, {3, 1}}; 0863 view->setCursors(cursors); 0864 QCOMPARE(view->cursors(), cursors); 0865 QVERIFY(isSorted(view->cursors())); 0866 QCOMPARE(view->cursorPosition(), Cursor(0, 1)); 0867 // We have no selection 0868 QVERIFY(!view->selection()); 0869 QCOMPARE(view->selectionRanges(), QVector<Range>{}); 0870 } 0871 0872 // Test duplicate cursor positions 0873 { 0874 auto [doc, view] = createDocAndView("foo\nbar", 0, 0); 0875 0876 QCOMPARE(view->cursors(), Cursors{Cursor(0, 0)}); 0877 const Cursors cursors = {{0, 0}, {1, 1}, {0, 0}, {1, 1}}; 0878 view->setCursors(cursors); 0879 auto expectedCursors = Cursors{Cursor(0, 0), Cursor(1, 1)}; 0880 QCOMPARE(view->cursors(), expectedCursors); 0881 QVERIFY(isSorted(view->cursors())); 0882 QCOMPARE(view->cursorPosition(), Cursor(0, 0)); 0883 0884 QVERIFY(view->cursors().size() > 1); 0885 view->setCursors({}); 0886 QVERIFY(view->cursors().size() == 1); 0887 } 0888 } 0889 0890 void MulticursorTest::testSetGetSelections() 0891 { 0892 // Set cursors => press shift+right 0893 { 0894 auto [doc, view] = createDocAndView("foo\nbar\nfoo", 0, 0); 0895 QCOMPARE(view->cursors(), QVector<Cursor>{Cursor(0, 0)}); 0896 QVector<Cursor> cursors = {{0, 1}, {1, 1}, {2, 1}}; 0897 view->setCursors(cursors); 0898 QCOMPARE(view->cursors(), cursors); 0899 QVERIFY(isSorted(view->cursors())); 0900 view->shiftCursorRight(); 0901 QVERIFY(view->selection()); 0902 cursors = {{0, 2}, {1, 2}, {2, 2}}; 0903 QCOMPARE(view->cursors(), cursors); 0904 QVector<Range> selections = {Range(0, 1, 0, 2), Range(1, 1, 1, 2), Range(2, 1, 2, 2)}; 0905 QCOMPARE(view->selectionRanges(), selections); 0906 QVERIFY(isSorted(view->selectionRanges())); 0907 QCOMPARE(view->selectionRange(), selections.front()); 0908 } 0909 0910 // Set cursors including an invalid position cursor 0911 // - primary already has selection 0912 // - try to get selection 0913 { 0914 auto [doc, view] = createDocAndView("foo\nbar", 0, 0); 0915 view->shiftWordRight(); 0916 QVERIFY(view->selection()); 0917 QCOMPARE(view->selectionRange(), Range(0, 0, 0, 3)); 0918 0919 QVector<Cursor> cursors = {{0, 1}, {1, 1}, {2, 1}}; 0920 view->setCursors(cursors); 0921 QVERIFY(!view->selection()); // selection is lost 0922 auto expectedCursors = QVector<Cursor>{Cursor(0, 1), Cursor(1, 1)}; 0923 QCOMPARE(view->cursors(), expectedCursors); 0924 } 0925 0926 // Set selections 0927 { 0928 auto [doc, view] = createDocAndView("foo\nbar", 0, 0); 0929 0930 QVERIFY(!view->selection()); 0931 QVector<Range> selections = {Range(0, 0, 0, 1), Range(1, 0, 1, 1)}; 0932 view->setSelections(selections); 0933 QVERIFY(view->selection()); 0934 QCOMPARE(view->selectionRanges(), selections); 0935 } 0936 0937 // Set overlapping selections 0938 { 0939 auto [doc, view] = createDocAndView("foo\nbar", 0, 0); 0940 0941 QVERIFY(!view->selection()); 0942 QVector<Range> selections = {Range(0, 0, 0, 3), Range(0, 1, 0, 2), Range(0, 0, 0, 1)}; 0943 view->setSelections(selections); 0944 QVERIFY(view->selection()); 0945 QVector<Range> expectedSelections = {Range(0, 0, 0, 3)}; 0946 QCOMPARE(view->selectionRanges(), expectedSelections); 0947 0948 view->setSelections({}); 0949 QVERIFY(!view->selection()); 0950 } 0951 0952 // Set selections with invalid range 0953 { 0954 auto [doc, view] = createDocAndView("foo\nbar", 0, 0); 0955 0956 QVERIFY(!view->selection()); 0957 QVector<Range> selections = {Range(0, 0, 0, 3), Range(1, 0, 1, 1), Range(2, 0, 2, 1)}; 0958 view->setSelections(selections); 0959 QVERIFY(view->selection()); 0960 QVector<Range> expectedSelections = {Range(0, 0, 0, 3), Range(1, 0, 1, 1)}; 0961 QCOMPARE(view->selectionRanges(), expectedSelections); 0962 } 0963 } 0964 0965 #include "moc_multicursortest.cpp" 0966 0967 // kate: indent-mode cstyle; indent-width 4; replace-tabs on;