File indexing completed on 2024-04-14 03:54:53

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2010-2018 Dominik Haumann <dhaumann@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "katedocument_test.h"
0009 #include "moc_katedocument_test.cpp"
0010 
0011 #include <kateconfig.h>
0012 #include <katedocument.h>
0013 #include <kateglobal.h>
0014 #include <kateview.h>
0015 
0016 #include <QRegularExpression>
0017 #include <QSignalSpy>
0018 #include <QTemporaryFile>
0019 
0020 #include <stdio.h>
0021 
0022 /// TODO: is there a FindValgrind cmake command we could use to
0023 ///      define this automatically?
0024 // comment this out and run the test case with:
0025 //   valgrind --tool=callgrind --instr-atstart=no ./katedocument_test testSetTextPerformance
0026 // or similar
0027 //
0028 // #define USE_VALGRIND
0029 
0030 #ifdef USE_VALGRIND
0031 #include <valgrind/callgrind.h>
0032 #endif
0033 
0034 using namespace KTextEditor;
0035 
0036 QTEST_MAIN(KateDocumentTest)
0037 
0038 KateDocumentTest::KateDocumentTest()
0039     : QObject()
0040 {
0041 }
0042 
0043 KateDocumentTest::~KateDocumentTest()
0044 {
0045 }
0046 
0047 void KateDocumentTest::initTestCase()
0048 {
0049     KTextEditor::EditorPrivate::enableUnitTestMode();
0050 }
0051 
0052 // tests:
0053 // KTextEditor::DocumentPrivate::insertText with word wrap enabled. It is checked whether the
0054 // text is correctly wrapped and whether the moving cursors maintain the correct
0055 // position.
0056 // see also: https://bugs.kde.org/show_bug.cgi?id=168534
0057 void KateDocumentTest::testWordWrap()
0058 {
0059     KTextEditor::DocumentPrivate doc(false, false);
0060     doc.setWordWrap(true);
0061     doc.setWordWrapAt(80);
0062 
0063     const QString content = QLatin1String(".........1.........2.........3.........4.........5.........6 ........7 ........8");
0064 
0065     // space after 7 is now kept
0066     // else we kill indentation...
0067     const QString firstWrap = QLatin1String(".........1.........2.........3.........4.........5.........6 ........7 \n....x....8");
0068 
0069     // space after 6 is now kept
0070     // else we kill indentation...
0071     const QString secondWrap = QLatin1String(".........1.........2.........3.........4.........5.........6 \n....ooooooooooo....7 ....x....8");
0072 
0073     doc.setText(content);
0074     MovingCursor *c = doc.newMovingCursor(Cursor(0, 75), MovingCursor::MoveOnInsert);
0075 
0076     QCOMPARE(doc.text(), content);
0077     QCOMPARE(c->toCursor(), Cursor(0, 75));
0078 
0079     // type a character at (0, 75)
0080     doc.insertText(c->toCursor(), QLatin1String("x"));
0081     QCOMPARE(doc.text(), firstWrap);
0082     QCOMPARE(c->toCursor(), Cursor(1, 5));
0083 
0084     // set cursor to (0, 65) and type "ooooooooooo"
0085     c->setPosition(Cursor(0, 65));
0086     doc.insertText(c->toCursor(), QLatin1String("ooooooooooo"));
0087     QCOMPARE(doc.text(), secondWrap);
0088     QCOMPARE(c->toCursor(), Cursor(1, 15));
0089 }
0090 
0091 void KateDocumentTest::testWrapParagraph()
0092 {
0093     // Each paragraph must be kept as an own but re-wrapped nicely
0094     KTextEditor::DocumentPrivate doc(false, false);
0095     doc.setWordWrapAt(30); // Keep needed test data small
0096 
0097     const QString before =
0098         QLatin1String("aaaaa a aaaa\naaaaa aaa aa aaaa aaaa \naaaa a aaa aaaaaaa a aaaa\n\nxxxxx x\nxxxx xxxxx\nxxx xx xxxx \nxxxx xxxx x xxx xxxxxxx x xxxx");
0099     const QString after =
0100         QLatin1String("aaaaa a aaaa aaaaa aaa aa aaaa \naaaa aaaa a aaa aaaaaaa a aaaa\n\nxxxxx x xxxx xxxxx xxx xx xxxx \nxxxx xxxx x xxx xxxxxxx x xxxx");
0101 
0102     doc.setWordWrap(false); // First we try with disabled hard wrap
0103     doc.setText(before);
0104     doc.wrapParagraph(0, doc.lines() - 1);
0105     QCOMPARE(doc.text(), after);
0106 
0107     doc.setWordWrap(true); // Test again with enabled hard wrap, that had cause trouble due to twice wrapping
0108     doc.setText(before);
0109     doc.wrapParagraph(0, doc.lines() - 1);
0110     QCOMPARE(doc.text(), after);
0111 }
0112 
0113 void KateDocumentTest::testReplaceQStringList()
0114 {
0115     KTextEditor::DocumentPrivate doc(false, false);
0116     doc.setWordWrap(false);
0117     doc.setText(
0118         QLatin1String("asdf\n"
0119                       "foo\n"
0120                       "foo\n"
0121                       "bar\n"));
0122     doc.replaceText(Range(1, 0, 3, 0), {QStringLiteral("new"), QStringLiteral("text"), QLatin1String("")}, false);
0123     QCOMPARE(doc.text(),
0124              QLatin1String("asdf\n"
0125                            "new\n"
0126                            "text\n"
0127                            "bar\n"));
0128 }
0129 
0130 void KateDocumentTest::testMovingInterfaceSignals()
0131 {
0132     KTextEditor::DocumentPrivate *doc = new KTextEditor::DocumentPrivate;
0133     QSignalSpy aboutToDeleteSpy(doc, &KTextEditor::DocumentPrivate::aboutToDeleteMovingInterfaceContent);
0134     QSignalSpy aboutToInvalidateSpy(doc, &KTextEditor::DocumentPrivate::aboutToInvalidateMovingInterfaceContent);
0135 
0136     QCOMPARE(doc->revision(), qint64(0));
0137 
0138     QCOMPARE(aboutToInvalidateSpy.count(), 0);
0139     QCOMPARE(aboutToDeleteSpy.count(), 0);
0140 
0141     QTemporaryFile f;
0142     f.open();
0143     doc->openUrl(QUrl::fromLocalFile(f.fileName()));
0144     QCOMPARE(doc->revision(), qint64(0));
0145     // TODO: gets emitted once in closeFile and once in openFile - is that OK?
0146     QCOMPARE(aboutToInvalidateSpy.count(), 2);
0147     QCOMPARE(aboutToDeleteSpy.count(), 0);
0148 
0149     doc->documentReload();
0150     QCOMPARE(doc->revision(), qint64(0));
0151     QCOMPARE(aboutToInvalidateSpy.count(), 4);
0152     // TODO: gets emitted once in closeFile and once in openFile - is that OK?
0153     QCOMPARE(aboutToDeleteSpy.count(), 0);
0154 
0155     delete doc;
0156     QCOMPARE(aboutToInvalidateSpy.count(), 4);
0157     QCOMPARE(aboutToDeleteSpy.count(), 1);
0158 }
0159 
0160 void KateDocumentTest::testSetTextPerformance()
0161 {
0162     const int lines = 150;
0163     const int columns = 80;
0164     const int rangeLength = 4;
0165     const int rangeGap = 1;
0166 
0167     Q_ASSERT(columns % (rangeLength + rangeGap) == 0);
0168 
0169     KTextEditor::DocumentPrivate doc;
0170     MovingRangeInvalidator invalidator;
0171     connect(&doc,
0172             &KTextEditor::DocumentPrivate::aboutToInvalidateMovingInterfaceContent,
0173             &invalidator,
0174             &MovingRangeInvalidator::aboutToInvalidateMovingInterfaceContent);
0175 
0176     QString text;
0177     QList<Range> ranges;
0178     ranges.reserve(lines * columns / (rangeLength + rangeGap));
0179     const QString line = QString().fill(QLatin1Char('a'), columns);
0180     for (int l = 0; l < lines; ++l) {
0181         text.append(line);
0182         text.append(QLatin1Char('\n'));
0183         for (int c = 0; c < columns; c += rangeLength + rangeGap) {
0184             ranges << Range(l, c, l, c + rangeLength);
0185         }
0186     }
0187 
0188     // replace
0189     QBENCHMARK {
0190         // init
0191         doc.setText(text);
0192         for (const Range &range : std::as_const(ranges)) {
0193             invalidator.addRange(doc.newMovingRange(range));
0194         }
0195         QCOMPARE(invalidator.ranges().size(), ranges.size());
0196 
0197 #ifdef USE_VALGRIND
0198         CALLGRIND_START_INSTRUMENTATION
0199 #endif
0200 
0201         doc.setText(text);
0202 
0203 #ifdef USE_VALGRIND
0204         CALLGRIND_STOP_INSTRUMENTATION
0205 #endif
0206 
0207         QCOMPARE(doc.text(), text);
0208         QVERIFY(invalidator.ranges().isEmpty());
0209     }
0210 }
0211 
0212 void KateDocumentTest::testRemoveTextPerformance()
0213 {
0214     const int lines = 5000;
0215     const int columns = 80;
0216 
0217     KTextEditor::DocumentPrivate doc;
0218 
0219     QString text;
0220     const QString line = QString().fill(QLatin1Char('a'), columns);
0221     for (int l = 0; l < lines; ++l) {
0222         text.append(line);
0223         text.append(QLatin1Char('\n'));
0224     }
0225 
0226     doc.setText(text);
0227 
0228     // replace
0229     QBENCHMARK_ONCE {
0230 #ifdef USE_VALGRIND
0231         CALLGRIND_START_INSTRUMENTATION
0232 #endif
0233 
0234         doc.editStart();
0235 
0236         doc.removeText(doc.documentRange());
0237 
0238         doc.editEnd();
0239 
0240 #ifdef USE_VALGRIND
0241         CALLGRIND_STOP_INSTRUMENTATION
0242 #endif
0243     }
0244 }
0245 
0246 void KateDocumentTest::testForgivingApiUsage()
0247 {
0248     KTextEditor::DocumentPrivate doc;
0249 
0250     QVERIFY(doc.isEmpty());
0251     QVERIFY(doc.replaceText(Range(0, 0, 100, 100), QStringLiteral("asdf")));
0252     QCOMPARE(doc.text(), QStringLiteral("asdf"));
0253     QCOMPARE(doc.lines(), 1);
0254     QVERIFY(doc.replaceText(Range(2, 5, 2, 100), QStringLiteral("asdf")));
0255     QCOMPARE(doc.lines(), 3);
0256     QCOMPARE(doc.text(), QLatin1String("asdf\n\n     asdf"));
0257 
0258     QVERIFY(doc.removeText(Range(0, 0, 1000, 1000)));
0259     QVERIFY(doc.removeText(Range(0, 0, 0, 100)));
0260     QVERIFY(doc.isEmpty());
0261     doc.insertText(Cursor(100, 0), QStringLiteral("foobar"));
0262     QCOMPARE(doc.line(100), QStringLiteral("foobar"));
0263 
0264     doc.setText(QStringLiteral("nY\nnYY\n"));
0265     QVERIFY(doc.removeText(Range(0, 0, 0, 1000)));
0266 }
0267 
0268 void KateDocumentTest::testAutoBrackets()
0269 {
0270     KTextEditor::DocumentPrivate doc;
0271     auto view = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0272 
0273     auto reset = [&]() {
0274         doc.setText(QLatin1String(""));
0275         view->setCursorPosition(Cursor(0, 0));
0276     };
0277 
0278     auto typeText = [&](const QString &text) {
0279         for (int i = 0; i < text.size(); ++i) {
0280             doc.typeChars(view, text.at(i));
0281         }
0282     };
0283 
0284     doc.setHighlightingMode(QStringLiteral("Normal")); // Just to be sure
0285     view->config()->setValue(KateViewConfig::AutoBrackets, true);
0286 
0287     QString testInput;
0288 
0289     testInput = QStringLiteral("(");
0290     typeText(testInput);
0291     QCOMPARE(doc.text(), QStringLiteral("()"));
0292 
0293     reset();
0294     testInput = QStringLiteral("\"");
0295     typeText(testInput);
0296     QCOMPARE(doc.text(), QStringLiteral("\"\""));
0297 
0298     reset();
0299     testInput = QStringLiteral("'");
0300     typeText(testInput);
0301     QCOMPARE(doc.text(), QStringLiteral("'")); // In Normal mode there is only one quote to expect
0302 
0303     //
0304     // Switch over to some other mode
0305     //
0306     doc.setHighlightingMode(QStringLiteral("C++"));
0307 
0308     reset();
0309     typeText(testInput);
0310     QCOMPARE(doc.text(), QStringLiteral("''")); // Now it must be two
0311 
0312     reset();
0313     testInput = QStringLiteral("('')");
0314     typeText(testInput);
0315     // Known bad behaviour in case of nested brackets
0316     QCOMPARE(doc.text(), testInput);
0317 
0318     reset();
0319     testInput = QStringLiteral("foo \"bar\" haz");
0320     typeText(testInput);
0321     QCOMPARE(doc.text(), testInput);
0322     // Simulate afterwards to add quotes, bug 405089
0323     doc.setText(QStringLiteral("foo \"bar"));
0324     typeText(QStringLiteral("\" haz"));
0325     QCOMPARE(doc.text(), testInput);
0326 
0327     // Let's check to add brackets to a selection...
0328     view->setBlockSelection(false);
0329     doc.setText(QStringLiteral("012xxx678"));
0330     view->setSelection(Range(0, 3, 0, 6));
0331     typeText(QStringLiteral("["));
0332     QCOMPARE(doc.text(), QStringLiteral("012[xxx]678"));
0333     QCOMPARE(view->selectionRange(), Range(0, 4, 0, 7));
0334 
0335     // ...over multiple lines..
0336     doc.setText(QStringLiteral("012xxx678\n012xxx678"));
0337     view->setSelection(Range(0, 3, 1, 6));
0338     typeText(QStringLiteral("["));
0339     QCOMPARE(doc.text(), QStringLiteral("012[xxx678\n012xxx]678"));
0340     QCOMPARE(view->selectionRange(), Range(0, 4, 1, 6));
0341 
0342     // ..once again in in block mode with increased complexity..
0343     view->setBlockSelection(true);
0344     doc.setText(QStringLiteral("012xxx678\n012xxx678"));
0345     view->setSelection(Range(0, 3, 1, 6));
0346     typeText(QStringLiteral("[("));
0347     QCOMPARE(doc.text(), QStringLiteral("012[(xxx)]678\n012[(xxx)]678"));
0348     QCOMPARE(view->selectionRange(), Range(0, 5, 1, 8));
0349 
0350     // ..and the same with right->left selection
0351     view->setBlockSelection(true);
0352     doc.setText(QStringLiteral("012xxx678\n012xxx678"));
0353     view->setSelection(Range(0, 6, 1, 3));
0354     typeText(QStringLiteral("[("));
0355     QCOMPARE(doc.text(), QStringLiteral("012[(xxx)]678\n012[(xxx)]678"));
0356     QCOMPARE(view->selectionRange(), Range(0, 8, 1, 5));
0357 }
0358 
0359 void KateDocumentTest::testReplaceTabs()
0360 {
0361     KTextEditor::DocumentPrivate doc;
0362     auto view = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0363 
0364     auto reset = [&]() {
0365         doc.setText(QStringLiteral("  Hi!"));
0366         view->setCursorPosition(Cursor(0, 0));
0367     };
0368 
0369     doc.setHighlightingMode(QStringLiteral("C++"));
0370     doc.config()->setTabWidth(4);
0371     doc.config()->setIndentationMode(QStringLiteral("cppstyle"));
0372 
0373     // no replace tabs, no indent pasted text
0374     doc.setConfigValue(QStringLiteral("replace-tabs"), false);
0375     doc.setConfigValue(QStringLiteral("indent-pasted-text"), false);
0376 
0377     reset();
0378     doc.insertText(Cursor(0, 0), QStringLiteral("\t"));
0379     QCOMPARE(doc.text(), QStringLiteral("\t  Hi!"));
0380 
0381     reset();
0382     doc.typeChars(view, QStringLiteral("\t"));
0383     QCOMPARE(doc.text(), QStringLiteral("\t  Hi!"));
0384 
0385     reset();
0386     doc.paste(view, QStringLiteral("some\ncode\n  3\nhi\n"));
0387     QCOMPARE(doc.text(), QStringLiteral("some\ncode\n  3\nhi\n  Hi!"));
0388 
0389     // now, enable replace tabs
0390     doc.setConfigValue(QStringLiteral("replace-tabs"), true);
0391 
0392     reset();
0393     doc.insertText(Cursor(0, 0), QStringLiteral("\t"));
0394     // calling insertText does not replace tabs
0395     QCOMPARE(doc.text(), QStringLiteral("\t  Hi!"));
0396 
0397     reset();
0398     doc.typeChars(view, QStringLiteral("\t"));
0399     // typing text replaces tabs
0400     QCOMPARE(doc.text(), QStringLiteral("      Hi!"));
0401 
0402     reset();
0403     doc.paste(view, QStringLiteral("\t"));
0404     // pasting text does not with indent-pasted off
0405     QCOMPARE(doc.text(), QStringLiteral("\t  Hi!"));
0406 
0407     doc.setConfigValue(QStringLiteral("indent-pasted-text"), true);
0408     doc.setText(QStringLiteral("int main() {\n    \n}"));
0409     view->setCursorPosition(Cursor(1, 4));
0410     doc.paste(view, QStringLiteral("\tHi"));
0411     // ... and it still does not with indent-pasted on.
0412     // This behaviour is up to discussion.
0413     // \t survives as we don't indent in the given case anymore, see 077dfe954699c674d2c34caf380199a4af7d184a
0414     QCOMPARE(doc.text(), QStringLiteral("int main() {\n    \tHi\n}"));
0415 
0416     reset();
0417     doc.paste(view, QStringLiteral("some\ncode\n  3\nhi"));
0418     QCOMPARE(doc.text(), QStringLiteral("some\ncode\n3\nhi  Hi!"));
0419 }
0420 
0421 void KateDocumentTest::testRemoveMultipleLines()
0422 {
0423     KTextEditor::DocumentPrivate doc;
0424 
0425     doc.setText(
0426         QStringLiteral("line1\n"
0427                        "line2\n"
0428                        "line3\n"
0429                        "line4\n"));
0430 
0431     SignalHandler handler;
0432     connect(&doc, &KTextEditor::DocumentPrivate::textRemoved, &handler, &SignalHandler::slotMultipleLinesRemoved);
0433     doc.removeText(Range(1, 0, 3, 0));
0434 }
0435 
0436 void KateDocumentTest::testInsertNewline()
0437 {
0438     KTextEditor::DocumentPrivate doc;
0439 
0440     doc.setText(
0441         QStringLiteral("this is line\n"
0442                        "this is line2\n"));
0443 
0444     SignalHandler handler;
0445     connect(&doc, &KTextEditor::DocumentPrivate::textInsertedRange, &handler, &SignalHandler::slotNewlineInserted);
0446     doc.editWrapLine(1, 4);
0447 }
0448 
0449 void KateDocumentTest::testInsertAfterEOF()
0450 {
0451     KTextEditor::DocumentPrivate doc;
0452 
0453     doc.setText(
0454         QStringLiteral("line0\n"
0455                        "line1"));
0456 
0457     const QString input = QLatin1String(
0458         "line3\n"
0459         "line4");
0460 
0461     const QString expected = QLatin1String(
0462         "line0\n"
0463         "line1\n"
0464         "\n"
0465         "line3\n"
0466         "line4");
0467 
0468     doc.insertText(KTextEditor::Cursor(3, 0), input);
0469     QCOMPARE(doc.text(), expected);
0470 }
0471 
0472 // we have two different ways of creating the checksum:
0473 // in KateFileLoader and KTextEditor::DocumentPrivate::createDigest. Make
0474 // sure, these two implementations result in the same checksum.
0475 void KateDocumentTest::testDigest()
0476 {
0477     // we will write the test file here to avoid that any line ending conversion for git will break it
0478     const QByteArray fileDigest = "aa22605da164a4e4e55f4c9738cfe1e53d4467f9";
0479     QTemporaryFile file(QStringLiteral("testDigest"));
0480     file.open();
0481     file.write("974d9ab0860c755a4f5686b3b6b429e1efd48a96\ntest\ntest\n\r\n\r\n\r\n");
0482     file.flush();
0483 
0484     // make sure, Kate::TextBuffer and KTextEditor::DocumentPrivate::createDigest() equal
0485     KTextEditor::DocumentPrivate doc;
0486     doc.openUrl(QUrl::fromLocalFile(file.fileName()));
0487     const QByteArray bufferDigest(doc.checksum().toHex());
0488     QVERIFY(doc.createDigest());
0489     const QByteArray docDigest(doc.checksum().toHex());
0490 
0491     QCOMPARE(bufferDigest, fileDigest);
0492     QCOMPARE(docDigest, fileDigest);
0493 }
0494 
0495 void KateDocumentTest::testModelines()
0496 {
0497     // honor document variable indent-width
0498     {
0499         KTextEditor::DocumentPrivate doc;
0500         QCOMPARE(doc.config()->indentationWidth(), 4);
0501         doc.readVariableLine(QStringLiteral("kate: indent-width 3;"));
0502         QCOMPARE(doc.config()->indentationWidth(), 3);
0503     }
0504 
0505     // honor document variable indent-width with * wildcard
0506     {
0507         KTextEditor::DocumentPrivate doc;
0508         QCOMPARE(doc.config()->indentationWidth(), 4);
0509         doc.readVariableLine(QStringLiteral("kate-wildcard(*): indent-width 3;"));
0510         QCOMPARE(doc.config()->indentationWidth(), 3);
0511     }
0512 
0513     // ignore document variable indent-width, since the wildcard does not match
0514     {
0515         KTextEditor::DocumentPrivate doc;
0516         QCOMPARE(doc.config()->indentationWidth(), 4);
0517         doc.readVariableLine(QStringLiteral("kate-wildcard(*.txt): indent-width 3;"));
0518         QCOMPARE(doc.config()->indentationWidth(), 4);
0519     }
0520 
0521     // document variable indent-width, since the wildcard does not match
0522     {
0523         KTextEditor::DocumentPrivate doc;
0524         doc.openUrl(QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "modelines.txt")));
0525         QVERIFY(!doc.isEmpty());
0526 
0527         // ignore wrong wildcard
0528         QCOMPARE(doc.config()->indentationWidth(), 4);
0529         doc.readVariableLine(QStringLiteral("kate-wildcard(*.bar): indent-width 3;"));
0530         QCOMPARE(doc.config()->indentationWidth(), 4);
0531 
0532         // read correct wildcard
0533         QCOMPARE(doc.config()->indentationWidth(), 4);
0534         doc.readVariableLine(QStringLiteral("kate-wildcard(*.txt): indent-width 5;"));
0535         QCOMPARE(doc.config()->indentationWidth(), 5);
0536 
0537         // honor correct wildcard
0538         QCOMPARE(doc.config()->indentationWidth(), 5);
0539         doc.readVariableLine(QStringLiteral("kate-wildcard(*.foo;*.txt;*.bar): indent-width 6;"));
0540         QCOMPARE(doc.config()->indentationWidth(), 6);
0541 
0542         // ignore incorrect mimetype
0543         QCOMPARE(doc.config()->indentationWidth(), 6);
0544         doc.readVariableLine(QStringLiteral("kate-mimetype(text/unknown): indent-width 7;"));
0545         QCOMPARE(doc.config()->indentationWidth(), 6);
0546 
0547         // honor correct mimetype
0548         QCOMPARE(doc.config()->indentationWidth(), 6);
0549         doc.readVariableLine(QStringLiteral("kate-mimetype(text/plain): indent-width 8;"));
0550         QCOMPARE(doc.config()->indentationWidth(), 8);
0551 
0552         // honor correct mimetype
0553         QCOMPARE(doc.config()->indentationWidth(), 8);
0554         doc.readVariableLine(QStringLiteral("kate-mimetype(text/foo;text/plain;text/bar): indent-width 9;"));
0555         QCOMPARE(doc.config()->indentationWidth(), 9);
0556     }
0557 
0558     // wildcard with path match, bug 453541
0559     {
0560         KTextEditor::DocumentPrivate doc;
0561         doc.openUrl(QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "encoding/utf8.txt")));
0562         QVERIFY(!doc.isEmpty());
0563 
0564         // read wrong wildcard
0565         QCOMPARE(doc.config()->indentationWidth(), 4);
0566         doc.readVariableLine(QStringLiteral("kate-wildcard(encodings/*.txt): indent-width 5;"));
0567         QCOMPARE(doc.config()->indentationWidth(), 4);
0568 
0569         // read correct wildcard
0570         QCOMPARE(doc.config()->indentationWidth(), 4);
0571         doc.readVariableLine(QStringLiteral("kate-wildcard(encoding/*.txt): indent-width 5;"));
0572         QCOMPARE(doc.config()->indentationWidth(), 5);
0573     }
0574 }
0575 
0576 void KateDocumentTest::testDefStyleNum()
0577 {
0578     KTextEditor::DocumentPrivate doc;
0579     doc.setText(QStringLiteral("foo\nbar\nasdf"));
0580     QCOMPARE(doc.defStyleNum(0, 0), 0);
0581 }
0582 
0583 void KateDocumentTest::testTypeCharsWithSurrogateAndNewLine()
0584 {
0585     KTextEditor::DocumentPrivate doc;
0586     auto view = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0587     const char32_t surrogateUcs4String[] = {0x1f346, '\n', 0x1f346, 0};
0588     const auto surrogateString = QString::fromUcs4(surrogateUcs4String);
0589     doc.typeChars(view, surrogateString);
0590 
0591     QCOMPARE(doc.text(), surrogateString);
0592 }
0593 
0594 void KateDocumentTest::testRemoveComposedCharacters()
0595 {
0596     KTextEditor::DocumentPrivate doc;
0597     auto view = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0598     view->config()->setValue(KateViewConfig::BackspaceRemoveComposedCharacters, true);
0599     doc.setText(QString::fromUtf8("व्यक्तियों"));
0600     doc.del(view, Cursor(0, 0));
0601 
0602     QCOMPARE(doc.text(), QString::fromUtf8(("क्तियों")));
0603 
0604     view->setCursorPosition({0, 7});
0605     doc.backspace(view);
0606 
0607     QCOMPARE(doc.text(), QString::fromUtf8(("क्ति")));
0608 }
0609 
0610 void KateDocumentTest::testAutoReload()
0611 {
0612     // ATM fails on Windows, mark as such to be able to enforce test success in CI
0613 #ifdef Q_OS_WIN
0614     QSKIP("Fails ATM, please fix");
0615 #endif
0616 
0617     QTemporaryFile file(QStringLiteral("AutoReloadTestFile"));
0618     file.open();
0619     file.write("Hello");
0620     file.flush();
0621 
0622     KTextEditor::DocumentPrivate doc;
0623     auto view = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0624     QVERIFY(doc.openUrl(QUrl::fromLocalFile(file.fileName())));
0625     QCOMPARE(doc.text(), QStringLiteral("Hello"));
0626     // The cursor should be and stay in the last line...
0627     QCOMPARE(view->cursorPosition().line(), doc.documentEnd().line());
0628 
0629     doc.autoReloadToggled(true);
0630 
0631     // Some magic value. You can wait as long as you like after write to file,
0632     // without to wait before it doesn't work here. It mostly fails with 500,
0633     // it tend to work with 600, but is not guarantied to work even with 800
0634     QTest::qWait(1000);
0635 
0636     file.write("\nTest");
0637     file.flush();
0638 
0639     // Hardcoded delay m_modOnHdTimer in DocumentPrivate
0640     // + min val with which it looks working reliable here
0641     QTest::qWait(1000);
0642     QCOMPARE(doc.text(), QStringLiteral("Hello\nTest"));
0643     // ...stay in the last line after reload!
0644     QCOMPARE(view->cursorPosition().line(), doc.documentEnd().line());
0645 
0646     // Now we have more then one line, set the cursor to some line != last line...
0647     Cursor c(0, 3);
0648     view->setCursorPosition(c);
0649 
0650     file.write("\nWorld!");
0651     file.flush();
0652 
0653     QTest::qWait(1000);
0654     QCOMPARE(doc.text(), QStringLiteral("Hello\nTest\nWorld!"));
0655     // ...and ensure we have not move around
0656     QCOMPARE(view->cursorPosition(), c);
0657 }
0658 
0659 void KateDocumentTest::testSearch()
0660 {
0661     /**
0662      * this is the start of some new implementation of searchText that can handle multi-line regex stuff
0663      * just naturally and without always concatenating the full document.
0664      */
0665 
0666     KTextEditor::DocumentPrivate doc;
0667     doc.setText(QStringLiteral("foo\nbar\nzonk"));
0668 
0669     const QRegularExpression pattern(QStringLiteral("ar\nzonk$"), QRegularExpression::MultilineOption | QRegularExpression::UseUnicodePropertiesOption);
0670     int startLine = 0;
0671     int endLine = 2;
0672     QString textToMatch;
0673     int partialMatchLine = -1;
0674     for (int currentLine = startLine; currentLine <= endLine; ++currentLine) {
0675         // if we had a partial match before, we need to keep the old text and append our new line
0676         int matchStartLine = currentLine;
0677         if (partialMatchLine >= 0) {
0678             textToMatch += doc.line(currentLine);
0679             textToMatch += QLatin1Char('\n');
0680             matchStartLine = partialMatchLine;
0681         } else {
0682             textToMatch = doc.line(currentLine);
0683             textToMatch += QLatin1Char('\n');
0684         }
0685 
0686         auto result = pattern.match(textToMatch, 0, QRegularExpression::PartialPreferFirstMatch);
0687         qDebug() << "match" << result.hasMatch() << result.hasPartialMatch() << result.capturedStart() << result.capturedLength();
0688 
0689         if (result.hasMatch()) {
0690             printf("found stuff starting in line %d\n", matchStartLine);
0691             break;
0692         }
0693 
0694         // else: remember if we need to keep text!
0695         if (result.hasPartialMatch()) {
0696             // if we had already a partial match before, keep the line!
0697             if (partialMatchLine >= 0) {
0698             } else {
0699                 partialMatchLine = currentLine;
0700             }
0701         } else {
0702             // we can forget the old text
0703             partialMatchLine = -1;
0704         }
0705     }
0706 }
0707 
0708 void KateDocumentTest::testMatchingBracket_data()
0709 {
0710     QTest::addColumn<QString>("text");
0711     QTest::addColumn<KTextEditor::Cursor>("cursor");
0712     QTest::addColumn<KTextEditor::Range>("match");
0713     QTest::addColumn<int>("maxLines");
0714 
0715     QTest::addRow("invalid") << QStringLiteral("asdf\nasdf") << KTextEditor::Cursor(1, 0) << KTextEditor::Range::invalid() << 10;
0716     QTest::addRow("]-before") << QStringLiteral("[\n]") << KTextEditor::Cursor(1, 0) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0717     QTest::addRow("]-after") << QStringLiteral("[\n]") << KTextEditor::Cursor(1, 1) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0718     QTest::addRow("[-before") << QStringLiteral("[\n]") << KTextEditor::Cursor(0, 0) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0719     QTest::addRow("[-after") << QStringLiteral("[\n]") << KTextEditor::Cursor(0, 1) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0720     QTest::addRow("}-before") << QStringLiteral("{\n}") << KTextEditor::Cursor(1, 0) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0721     QTest::addRow("}-after") << QStringLiteral("{\n}") << KTextEditor::Cursor(1, 1) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0722     QTest::addRow("{-before") << QStringLiteral("{\n}") << KTextEditor::Cursor(0, 0) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0723     QTest::addRow("{-after") << QStringLiteral("{\n}") << KTextEditor::Cursor(0, 1) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0724     QTest::addRow(")-before") << QStringLiteral("(\n)") << KTextEditor::Cursor(1, 0) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0725     QTest::addRow(")-after") << QStringLiteral("(\n)") << KTextEditor::Cursor(1, 1) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0726     QTest::addRow("(-before") << QStringLiteral("(\n)") << KTextEditor::Cursor(0, 0) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0727     QTest::addRow("(-after") << QStringLiteral("(\n)") << KTextEditor::Cursor(0, 1) << KTextEditor::Range({0, 0}, {1, 0}) << 10;
0728     QTest::addRow("]-maxlines") << QStringLiteral("[\n\n]") << KTextEditor::Cursor(1, 0) << KTextEditor::Range::invalid() << 1;
0729 }
0730 
0731 void KateDocumentTest::testMatchingBracket()
0732 {
0733     QFETCH(QString, text);
0734     QFETCH(KTextEditor::Cursor, cursor);
0735     QFETCH(KTextEditor::Range, match);
0736     QFETCH(int, maxLines);
0737 
0738     KTextEditor::DocumentPrivate doc;
0739     doc.setText(text);
0740     QCOMPARE(doc.findMatchingBracket(cursor, maxLines), match);
0741 }
0742 
0743 void KateDocumentTest::testIndentOnPaste()
0744 {
0745     KTextEditor::DocumentPrivate doc;
0746     auto view = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0747 
0748     doc.setHighlightingMode(QStringLiteral("C++"));
0749     doc.config()->setTabWidth(4);
0750     doc.config()->setIndentationMode(QStringLiteral("cppstyle"));
0751 
0752     doc.setConfigValue(QStringLiteral("indent-pasted-text"), true);
0753 
0754     /**
0755      * namespace ns
0756      * {
0757      * class MyClass
0758      */
0759     doc.setText(QStringLiteral("namespace ns\n{\nclass MyClass"));
0760     view->setCursorPosition({2, 5});
0761     doc.paste(view, QStringLiteral(" SOME_MACRO"));
0762     // We have text in the line we are pasting in so the existing indentation shouldn't be disturbed
0763     QCOMPARE(doc.text(), QStringLiteral("namespace ns\n{\nclass SOME_MACRO MyClass"));
0764 
0765     /**
0766      * namespace ns
0767      * {
0768      */
0769     doc.setText(QStringLiteral("namespace ns\n{\n"));
0770     view->setCursorPosition({2, 0});
0771     doc.paste(view, QStringLiteral("class MyClass"));
0772     // We have no text in the line we are pasting in so indentation will be adjusted
0773     QCOMPARE(doc.text(), QStringLiteral("namespace ns\n{\n    class MyClass"));
0774 }
0775 
0776 void KateDocumentTest::testAboutToSave()
0777 {
0778     KTextEditor::DocumentPrivate doc;
0779     const QString thisFile = QString::fromUtf8(__FILE__);
0780     bool opened = doc.openUrl(QUrl::fromLocalFile(thisFile));
0781 
0782     QVERIFY(opened);
0783 
0784     QSignalSpy spy(&doc, &KTextEditor::DocumentPrivate::aboutToSave);
0785     QSignalSpy savedSpy(&doc, &KTextEditor::DocumentPrivate::documentSavedOrUploaded);
0786 
0787     doc.documentSave();
0788 
0789     QVERIFY(spy.count() == 1 || spy.wait());
0790     QVERIFY(savedSpy.count() == 1 || savedSpy.wait());
0791 }
0792 
0793 void KateDocumentTest::testKeepUndoOverReload()
0794 {
0795     // create test document with some simple text
0796     KTextEditor::DocumentPrivate doc;
0797     const QString initialText = QStringLiteral("lala\ntest\nfoobar\n");
0798     doc.setText(initialText);
0799     QCOMPARE(doc.text(), initialText);
0800 
0801     // now: do some editing
0802     const QString insertedText = QStringLiteral("newfirstline");
0803     doc.insertText(KTextEditor::Cursor(0, 0), insertedText);
0804     QCOMPARE(doc.text(), insertedText + initialText);
0805 
0806     // test undo/redo
0807     doc.undo();
0808     QCOMPARE(doc.text(), initialText);
0809     doc.redo();
0810     QCOMPARE(doc.text(), insertedText + initialText);
0811 
0812     // save it to some local temporary file, for later reload
0813     QTemporaryFile tmpFile;
0814     tmpFile.open();
0815     QVERIFY(doc.saveAs(QUrl::fromLocalFile(tmpFile.fileName())));
0816 
0817     // first: try if normal reload works
0818     QVERIFY(doc.documentReload());
0819     QCOMPARE(doc.text(), insertedText + initialText);
0820 
0821     // test undo/redo AFTER reload
0822     doc.undo();
0823     QCOMPARE(doc.text(), initialText);
0824     doc.redo();
0825     QCOMPARE(doc.text(), insertedText + initialText);
0826 }
0827 
0828 void KateDocumentTest::testToggleComment()
0829 {
0830     { // BUG: 451471
0831         KTextEditor::DocumentPrivate doc;
0832         QVERIFY(doc.highlightingModes().contains(QStringLiteral("Python")));
0833         doc.setHighlightingMode(QStringLiteral("Python"));
0834         const QString original = QStringLiteral("import hello;\ndef method():");
0835         doc.setText(original);
0836         QVERIFY(doc.lines() == 2);
0837 
0838         doc.commentSelection(doc.documentRange(), {1, 2}, false, DocumentPrivate::ToggleComment);
0839         QCOMPARE(doc.text(), QStringLiteral("# import hello;\n# def method():"));
0840 
0841         doc.commentSelection(doc.documentRange(), {1, 2}, false, DocumentPrivate::ToggleComment);
0842         QCOMPARE(doc.text(), original);
0843     }
0844 
0845     { // Comment C++;
0846         KTextEditor::DocumentPrivate doc;
0847         QVERIFY(doc.highlightingModes().contains(QStringLiteral("C++")));
0848         doc.setHighlightingMode(QStringLiteral("C++"));
0849         QString original = QStringLiteral("#include<iostream>\nint main()\n{\nreturn 0;\n}\n");
0850         doc.setText(original);
0851         QVERIFY(doc.lines() == 6);
0852 
0853         doc.commentSelection(doc.documentRange(), {5, 0}, false, DocumentPrivate::ToggleComment);
0854         QCOMPARE(doc.text(), QStringLiteral("// #include<iostream>\n// int main()\n// {\n// return 0;\n// }\n"));
0855 
0856         doc.commentSelection(doc.documentRange(), {5, 0}, false, DocumentPrivate::ToggleComment);
0857         QCOMPARE(doc.text(), original);
0858 
0859         // Comment just a portion
0860         doc.commentSelection(Range(1, 0, 1, 3), Cursor(1, 3), false, DocumentPrivate::ToggleComment);
0861         QCOMPARE(doc.text(), QStringLiteral("#include<iostream>\n/*int*/ main()\n{\nreturn 0;\n}\n"));
0862         doc.commentSelection(Range(1, 0, 1, 7), Cursor(1, 3), false, DocumentPrivate::ToggleComment);
0863         QCOMPARE(doc.text(), original);
0864 
0865         // mixed, one line commented, one not => both get commented
0866         original = QStringLiteral(" // int main()\n{}");
0867         doc.setText(original);
0868         doc.commentSelection(doc.documentRange(), {1, 2}, false, DocumentPrivate::ToggleComment);
0869         QCOMPARE(doc.text(), QStringLiteral("//  // int main()\n// {}"));
0870         doc.commentSelection(doc.documentRange(), {1, 2}, false, DocumentPrivate::ToggleComment);
0871         // after uncommenting, we get original text back with one line commented
0872         QCOMPARE(doc.text(), original);
0873     }
0874 
0875     { // BUG: 458126
0876         KTextEditor::DocumentPrivate doc;
0877         doc.setText(QStringLiteral("another:\n\nanother2: hello"));
0878         QVERIFY(doc.highlightingModes().contains(QStringLiteral("YAML")));
0879         doc.setHighlightingMode(QStringLiteral("YAML"));
0880         const QString original = doc.text();
0881 
0882         doc.commentSelection(doc.documentRange(), {2, 0}, false, DocumentPrivate::ToggleComment);
0883         QCOMPARE(doc.text(), QStringLiteral("# another:\n# \n# another2: hello"));
0884 
0885         doc.commentSelection(doc.documentRange(), {2, 0}, false, DocumentPrivate::ToggleComment);
0886         QCOMPARE(doc.text(), original);
0887     }
0888 }
0889 
0890 void KateDocumentTest::testInsertTextTooLargeColumn()
0891 {
0892     KTextEditor::DocumentPrivate doc;
0893     const QString original = QStringLiteral("01234567\n01234567");
0894     doc.setText(original);
0895     QVERIFY(doc.lines() == 2);
0896     QCOMPARE(doc.text(), original);
0897 
0898     // try to insert text with initial \n at wrong column, did trigger invalid call to editWrapLine
0899     doc.insertText(KTextEditor::Cursor(0, 10), QStringLiteral("\nxxxx"));
0900     QCOMPARE(doc.text(), QStringLiteral("01234567  \nxxxx\n01234567"));
0901 }
0902 
0903 void KateDocumentTest::testBug468495()
0904 {
0905     // original
0906     const char o[] = R"(                0123456789abcdefghijkl
0907                 0123456789abcdefghijkl
0908                 012345678901234567890123456789)";
0909     // expected
0910     const char e[] = R"(                0123456789abcdefghijkl
0911 
0912                 0123456789abcdefghijkl
0913                 012345678901234567890123456789)";
0914 
0915     QString original = QString::fromLatin1(o);
0916     KTextEditor::DocumentPrivate doc;
0917     doc.setText(original);
0918     doc.config()->setIndentationMode(QStringLiteral("cstyle"));
0919     KTextEditor::ViewPrivate *v = static_cast<KTextEditor::ViewPrivate *>(doc.createView(nullptr));
0920     v->setCursorPosition({1, 0});
0921     v->keyReturn();
0922 
0923     QString afterIndent = doc.text();
0924     QCOMPARE(QString::fromLatin1(e), afterIndent);
0925 }
0926 
0927 void KateDocumentTest::testCursorToOffset()
0928 {
0929     KTextEditor::DocumentPrivate doc;
0930     doc.setText(QStringLiteral("123\n456"));
0931 
0932     // Simple
0933     {
0934         QCOMPARE(doc.lines(), 2);
0935         QCOMPARE(doc.cursorToOffset({0, 0}), 0);
0936         QCOMPARE(doc.offsetToCursor(0), Cursor(0, 0));
0937 
0938         QCOMPARE(doc.cursorToOffset({0, 1}), 1);
0939         QCOMPARE(doc.offsetToCursor(1), Cursor(0, 1));
0940 
0941         QCOMPARE(doc.cursorToOffset({0, 2}), 2);
0942         QCOMPARE(doc.offsetToCursor(2), Cursor(0, 2));
0943 
0944         QCOMPARE(doc.cursorToOffset(doc.documentEnd()), 7);
0945         QCOMPARE(doc.offsetToCursor(7), doc.documentEnd());
0946 
0947         // out of range column snaps to the last column in line
0948         QCOMPARE(doc.cursorToOffset({0, 7}), 3);
0949 
0950         // invalid / out of range cursor returns -1
0951         QCOMPARE(doc.cursorToOffset({2, 4}), -1);
0952         QCOMPARE(doc.offsetToCursor(99), KTextEditor::Cursor::invalid());
0953         QCOMPARE(doc.cursorToOffset(KTextEditor::Cursor::invalid()), -1);
0954         QCOMPARE(doc.offsetToCursor(-1), KTextEditor::Cursor::invalid());
0955     }
0956 
0957     // After insert/remove
0958     doc.insertText(doc.documentEnd(), QStringLiteral("\n123"));
0959     {
0960         QCOMPARE(doc.lines(), 3);
0961         QCOMPARE(doc.cursorToOffset(doc.documentEnd()), 11);
0962         QCOMPARE(doc.offsetToCursor(11), doc.documentEnd());
0963 
0964         doc.removeLine(0);
0965         QCOMPARE(doc.lines(), 2);
0966         QCOMPARE(doc.cursorToOffset(doc.documentEnd()), 7);
0967         QCOMPARE(doc.offsetToCursor(7), doc.documentEnd());
0968 
0969         QString s(128, QLatin1Char('\n'));
0970         doc.insertText(doc.documentEnd(), s);
0971         QCOMPARE(doc.lines(), 128 + 2);
0972         QCOMPARE(doc.cursorToOffset(doc.documentEnd()), 7 + 128);
0973         QCOMPARE(doc.offsetToCursor(7 + 128), doc.documentEnd());
0974     }
0975 }
0976 
0977 void KateDocumentTest::testBug329247()
0978 {
0979     // This bug fires when the end of selection
0980     // is inside the indentation of the last line of the selection
0981 
0982     const Cursor start_selection = Cursor(0, 2);
0983 
0984     const int end_selection_line = 2;
0985 
0986     const QString document = QString::fromUtf8(
0987         "        AAAAAAAA\n" // <- will be selected
0988         "        AAAAAAAA\n" // <- will be selected
0989         "        AAAAAAAA\n" // <- will be selected
0990         "        BBBBBBBB\n"
0991         "        BBBBBBBB\n\n");
0992 
0993     const QString expected_document_after_1_indent = QString::fromUtf8(
0994         "            AAAAAAAA\n"
0995         "            AAAAAAAA\n"
0996         "            AAAAAAAA\n"
0997         "        BBBBBBBB\n"
0998         "        BBBBBBBB\n\n");
0999 
1000     const QString expected_document_after_2_indent = QString::fromUtf8(
1001         "                AAAAAAAA\n"
1002         "                AAAAAAAA\n"
1003         "                AAAAAAAA\n"
1004         "        BBBBBBBB\n"
1005         "        BBBBBBBB\n\n");
1006 
1007     KTextEditor::DocumentPrivate doc(false, false);
1008     KTextEditor::View *view = doc.createView(nullptr);
1009     view->resize(400, 300);
1010 
1011     // Selection starts before "AAAAA" bloc
1012     // and ends on the last line of "AAAAA" bloc
1013     // Notes :
1014     // - for a wider coverage, all possible end_selection values are tested
1015     // - end_selection_col = 0 is not tested, it's a specific case where
1016     //   last line must not be indented (see KateAutoIndent::changeIndent internal comment)
1017     for (int end_selection_col = 1; end_selection_col < 12; end_selection_col++) {
1018         doc.setText(document);
1019 
1020         Cursor end_selection(end_selection_line, end_selection_col);
1021         Range selection(start_selection, end_selection);
1022 
1023         // First indent : select the "AAAAA" bloc and indent it
1024         // (the first indent works as expected)
1025         view->setSelection(selection);
1026         doc.indent(selection, 1);
1027         QCOMPARE(doc.text(), expected_document_after_1_indent);
1028 
1029         // The bug is related to the selection being altered by the first indent action.
1030         // To reproduce it, we indent the selection again.
1031         selection = view->selectionRange();
1032         doc.indent(selection, 1);
1033         QCOMPARE(doc.text(), expected_document_after_2_indent);
1034     }
1035 }