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