File indexing completed on 2024-04-21 03:57:14

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;