File indexing completed on 2024-04-28 11:44:49

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;