diff --git a/autotests/src/codecompletiontestmodels.h b/autotests/src/codecompletiontestmodels.h index e185e399..677b758c 100644 --- a/autotests/src/codecompletiontestmodels.h +++ b/autotests/src/codecompletiontestmodels.h @@ -1,166 +1,168 @@ /* This file is part of the KDE libraries Copyright (C) 2008 Niko Sams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KATE_COMPLETIONTESTMODELS_H #define KATE_COMPLETIONTESTMODELS_H #include "codecompletiontestmodel.h" #include #include #include +#include + using namespace KTextEditor; class CustomRangeModel : public CodeCompletionTestModel, public CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit CustomRangeModel(KTextEditor::View *parent = nullptr, const QString &startText = QString()) : CodeCompletionTestModel(parent, startText) {} Range completionRange(View *view, const Cursor &position) override { Range range = CodeCompletionModelControllerInterface::completionRange(view, position); if (range.start().column() > 0) { KTextEditor::Range preRange(Cursor(range.start().line(), range.start().column() - 1), Cursor(range.start().line(), range.start().column())); qDebug() << preRange << view->document()->text(preRange); if (view->document()->text(preRange) == "$") { range.expandToRange(preRange); qDebug() << "using custom completion range" << range; } } return range; } bool shouldAbortCompletion(View *view, const Range &range, const QString ¤tCompletion) override { Q_UNUSED(view); Q_UNUSED(range); - static const QRegExp allowedText("^\\$?(\\w*)"); - return !allowedText.exactMatch(currentCompletion); + static const QRegularExpression allowedText("^\\$?(\\w*)$"); + return !allowedText.match(currentCompletion).hasMatch(); } }; class CustomAbortModel : public CodeCompletionTestModel, public CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit CustomAbortModel(KTextEditor::View *parent = nullptr, const QString &startText = QString()) : CodeCompletionTestModel(parent, startText) {} bool shouldAbortCompletion(View *view, const Range &range, const QString ¤tCompletion) override { Q_UNUSED(view); Q_UNUSED(range); - static const QRegExp allowedText("^([\\w-]*)"); - return !allowedText.exactMatch(currentCompletion); + static const QRegularExpression allowedText("^([\\w-]*)"); + return !allowedText.match(currentCompletion).hasMatch(); } }; class EmptyFilterStringModel : public CodeCompletionTestModel, public CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit EmptyFilterStringModel(KTextEditor::View *parent = nullptr, const QString &startText = QString()) : CodeCompletionTestModel(parent, startText) {} QString filterString(View *, const Range &, const Cursor &) override { return QString(); } }; class UpdateCompletionRangeModel : public CodeCompletionTestModel, public CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit UpdateCompletionRangeModel(KTextEditor::View *parent = nullptr, const QString &startText = QString()) : CodeCompletionTestModel(parent, startText) {} Range updateCompletionRange(View *view, const Range &range) override { Q_UNUSED(view); if (view->document()->text(range) == QString("ab")) { return Range(Cursor(range.start().line(), 0), range.end()); } return range; } bool shouldAbortCompletion(View *view, const Range &range, const QString ¤tCompletion) override { Q_UNUSED(view); Q_UNUSED(range); Q_UNUSED(currentCompletion); return false; } }; class StartCompletionModel : public CodeCompletionTestModel, public CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit StartCompletionModel(KTextEditor::View *parent = nullptr, const QString &startText = QString()) : CodeCompletionTestModel(parent, startText) {} bool shouldStartCompletion(View *view, const QString &insertedText, bool userInsertion, const Cursor &position) override { Q_UNUSED(view); Q_UNUSED(userInsertion); Q_UNUSED(position); if (insertedText.isEmpty()) { return false; } QChar lastChar = insertedText.at(insertedText.count() - 1); if (lastChar == '%') { return true; } return false; } }; class ImmideatelyAbortCompletionModel : public CodeCompletionTestModel, public CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit ImmideatelyAbortCompletionModel(KTextEditor::View *parent = nullptr, const QString &startText = QString()) : CodeCompletionTestModel(parent, startText) {} bool shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString ¤tCompletion) override { Q_UNUSED(view); Q_UNUSED(range); Q_UNUSED(currentCompletion); return true; } }; #endif diff --git a/autotests/src/vimode/emulatedcommandbar.cpp b/autotests/src/vimode/emulatedcommandbar.cpp index 946f840b..0a3e79dc 100644 --- a/autotests/src/vimode/emulatedcommandbar.cpp +++ b/autotests/src/vimode/emulatedcommandbar.cpp @@ -1,3383 +1,3384 @@ /* This file is part of the KDE libraries Copyright (C) 2011 Kuzmich Svyatoslav Copyright (C) 2012 - 2013 Simon St James This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "emulatedcommandbar.h" #include #include #include #include "keys.h" #include "view.h" #include "emulatedcommandbarsetupandteardown.h" #include "vimode/mappings.h" #include "vimode/globalstate.h" #include #include #include #include #include #include #include QTEST_MAIN(EmulatedCommandBarTest) using namespace KTextEditor; using namespace KateVi; void EmulatedCommandBarTest::EmulatedCommandBarTests() { // Ensure that some preconditions for these tests are setup, and (more importantly) // ensure that they are reverted no matter how these tests end. EmulatedCommandBarSetUpAndTearDown emulatedCommandBarSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); // Verify that we can get a non-null pointer to the emulated command bar. EmulatedCommandBar *emulatedCommandBar = vi_input_mode->viModeEmulatedCommandBar(); QVERIFY(emulatedCommandBar); // Should initially be hidden. QVERIFY(!emulatedCommandBar->isVisible()); // Test that "/" invokes the emulated command bar (if we are configured to use it) BeginTest(""); TestPressKey("/"); QVERIFY(emulatedCommandBar->isVisible()); QCOMPARE(emulatedCommandTypeIndicator()->text(), QString("/")); QVERIFY(emulatedCommandTypeIndicator()->isVisible()); QVERIFY(emulatedCommandBarTextEdit()); QVERIFY(emulatedCommandBarTextEdit()->text().isEmpty()); // Make sure the keypresses end up changing the text. QVERIFY(emulatedCommandBarTextEdit()->isVisible()); TestPressKey("foo"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); // Make sure ctrl-c dismisses it (assuming we allow Vim to steal the ctrl-c shortcut). TestPressKey("\\ctrl-c"); QVERIFY(!emulatedCommandBar->isVisible()); // Ensure that ESC dismisses it, too. BeginTest(""); TestPressKey("/"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\esc"); QVERIFY(!emulatedCommandBar->isVisible()); FinishTest(""); // Ensure that Ctrl-[ dismisses it, too. BeginTest(""); TestPressKey("/"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\ctrl-["); QVERIFY(!emulatedCommandBar->isVisible()); FinishTest(""); // Ensure that Enter dismisses it, too. BeginTest(""); TestPressKey("/"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); QVERIFY(!emulatedCommandBar->isVisible()); FinishTest(""); // Ensure that Return dismisses it, too. BeginTest(""); TestPressKey("/"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\return"); QVERIFY(!emulatedCommandBar->isVisible()); FinishTest(""); // Ensure that text is always initially empty. BeginTest(""); TestPressKey("/a\\enter"); TestPressKey("/"); QVERIFY(emulatedCommandBarTextEdit()->text().isEmpty()); TestPressKey("\\enter"); FinishTest(""); // Check backspace works. BeginTest(""); TestPressKey("/foo\\backspace"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("fo")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-h works. BeginTest(""); TestPressKey("/bar\\ctrl-h"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("ba")); TestPressKey("\\enter"); FinishTest(""); // ctrl-h should dismiss bar when empty. BeginTest(""); TestPressKey("/\\ctrl-h"); QVERIFY(!emulatedCommandBar->isVisible()); FinishTest(""); // ctrl-h should not dismiss bar when there is stuff to the left of cursor. BeginTest(""); TestPressKey("/a\\ctrl-h"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); FinishTest(""); // ctrl-h should not dismiss bar when bar is not empty, even if there is nothing to the left of cursor. BeginTest(""); TestPressKey("/a\\left\\ctrl-h"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); FinishTest(""); // Same for backspace. BeginTest(""); TestPressKey("/bar\\backspace"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("ba")); TestPressKey("\\enter"); FinishTest(""); BeginTest(""); TestPressKey("/\\backspace"); QVERIFY(!emulatedCommandBar->isVisible()); FinishTest(""); BeginTest(""); TestPressKey("/a\\backspace"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); FinishTest(""); BeginTest(""); TestPressKey("/a\\left\\backspace"); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-b works. BeginTest(""); TestPressKey("/bar foo xyz\\ctrl-bX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("Xbar foo xyz")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-e works. BeginTest(""); TestPressKey("/bar foo xyz\\ctrl-b\\ctrl-eX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("bar foo xyzX")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works. BeginTest(""); TestPressKey("/foo bar\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo ")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works on empty command bar. BeginTest(""); TestPressKey("/\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works in middle of word. BeginTest(""); TestPressKey("/foo bar\\left\\left\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo ar")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w leaves the cursor in the right place when in the middle of word. BeginTest(""); TestPressKey("/foo bar\\left\\left\\ctrl-wX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo Xar")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works when at the beginning of the text. BeginTest(""); TestPressKey("/foo\\left\\left\\left\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works when the character to the left is a space. BeginTest(""); TestPressKey("/foo bar \\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo ")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works when all characters to the left of the cursor are spaces. BeginTest(""); TestPressKey("/ \\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w works when all characters to the left of the cursor are non-spaces. BeginTest(""); TestPressKey("/foo\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w does not continue to delete subsequent alphanumerics if the characters to the left of the cursor // are non-space, non-alphanumerics. BeginTest(""); TestPressKey("/foo!!!\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w does not continue to delete subsequent alphanumerics if the characters to the left of the cursor // are non-space, non-alphanumerics. BeginTest(""); TestPressKey("/foo!!!\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w deletes underscores and alphanumerics to the left of the cursor, but stops when it reaches a // character that is none of these. BeginTest(""); TestPressKey("/foo!!!_d1\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo!!!")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w doesn't swallow the spaces preceding the block of non-word chars. BeginTest(""); TestPressKey("/foo !!!\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo ")); TestPressKey("\\enter"); FinishTest(""); // Check ctrl-w doesn't swallow the spaces preceding the word. BeginTest(""); TestPressKey("/foo 1d_\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo ")); TestPressKey("\\enter"); FinishTest(""); // Check there is a "waiting for register" indicator, initially hidden. BeginTest(""); TestPressKey("/"); QLabel* waitingForRegisterIndicator = emulatedCommandBar->findChild("waitingforregisterindicator"); QVERIFY(waitingForRegisterIndicator); QVERIFY(!waitingForRegisterIndicator->isVisible()); QCOMPARE(waitingForRegisterIndicator->text(), QString("\"")); TestPressKey("\\enter"); FinishTest(""); // Test that ctrl-r causes it to become visible. It is displayed to the right of the text edit. BeginTest(""); TestPressKey("/\\ctrl-r"); QVERIFY(waitingForRegisterIndicator->isVisible()); QVERIFY(waitingForRegisterIndicator->x() >= emulatedCommandBarTextEdit()->x() + emulatedCommandBarTextEdit()->width()); TestPressKey("\\ctrl-c"); TestPressKey("\\ctrl-c"); FinishTest(""); // The first ctrl-c after ctrl-r (when no register entered) hides the waiting for register // indicator, but not the bar. BeginTest(""); TestPressKey("/\\ctrl-r"); QVERIFY(waitingForRegisterIndicator->isVisible()); TestPressKey("\\ctrl-c"); QVERIFY(!waitingForRegisterIndicator->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss the bar. FinishTest(""); // The first ctrl-c after ctrl-r (when no register entered) aborts waiting for register. BeginTest("foo"); TestPressKey("\"cyiw/\\ctrl-r\\ctrl-ca"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("a")); TestPressKey("\\ctrl-c"); // Dismiss the bar. FinishTest("foo"); // Same as above, but for ctrl-[ instead of ctrl-c. BeginTest(""); TestPressKey("/\\ctrl-r"); QVERIFY(waitingForRegisterIndicator->isVisible()); TestPressKey("\\ctrl-["); QVERIFY(!waitingForRegisterIndicator->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss the bar. FinishTest(""); BeginTest("foo"); TestPressKey("\"cyiw/\\ctrl-r\\ctrl-[a"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("a")); TestPressKey("\\ctrl-c"); // Dismiss the bar. FinishTest("foo"); // Check ctrl-r works with registers, and hides the "waiting for register" indicator. BeginTest("xyz"); TestPressKey("\"ayiw/foo\\ctrl-ra"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("fooxyz")); QVERIFY(!waitingForRegisterIndicator->isVisible()); TestPressKey("\\enter"); FinishTest("xyz"); // Check ctrl-r inserts text at the current cursor position. BeginTest("xyz"); TestPressKey("\"ayiw/foo\\left\\ctrl-ra"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foxyzo")); TestPressKey("\\enter"); FinishTest("xyz"); // Check ctrl-r ctrl-w inserts word under the cursor, and hides the "waiting for register" indicator. BeginTest("foo bar xyz"); TestPressKey("w/\\left\\ctrl-r\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("bar")); QVERIFY(!waitingForRegisterIndicator->isVisible()); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Check ctrl-r ctrl-w doesn't insert the contents of register w! BeginTest("foo baz xyz"); TestPressKey("\"wyiww/\\left\\ctrl-r\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("baz")); TestPressKey("\\enter"); FinishTest("foo baz xyz"); // Check ctrl-r ctrl-w inserts at the current cursor position. BeginTest("foo nose xyz"); TestPressKey("w/bar\\left\\ctrl-r\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("banoser")); TestPressKey("\\enter"); FinishTest("foo nose xyz"); // Cursor position is at the end of the inserted text after ctrl-r ctrl-w. BeginTest("foo nose xyz"); TestPressKey("w/bar\\left\\ctrl-r\\ctrl-wX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("banoseXr")); TestPressKey("\\enter"); FinishTest("foo nose xyz"); // Cursor position is at the end of the inserted register contents after ctrl-r. BeginTest("xyz"); TestPressKey("\"ayiw/foo\\left\\ctrl-raX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foxyzXo")); TestPressKey("\\enter"); FinishTest("xyz"); // Insert clipboard contents on ctrl-r +. We implicitly need to test the ability to handle // shift key key events when waiting for register (they should be ignored). BeginTest("xyz"); QApplication::clipboard()->setText("vimodetestclipboardtext"); TestPressKey("/\\ctrl-r"); QKeyEvent *shiftKeyDown = new QKeyEvent(QEvent::KeyPress, Qt::Key_Shift, Qt::NoModifier); QApplication::postEvent(emulatedCommandBarTextEdit(), shiftKeyDown); QApplication::sendPostedEvents(); TestPressKey("+"); QKeyEvent *shiftKeyUp = new QKeyEvent(QEvent::KeyPress, Qt::Key_Shift, Qt::NoModifier); QApplication::postEvent(emulatedCommandBarTextEdit(), shiftKeyUp); QApplication::sendPostedEvents(); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("vimodetestclipboardtext")); TestPressKey("\\enter"); FinishTest("xyz"); // Similarly, test that we can press "ctrl" after ctrl-r without it being taken for a register. BeginTest("wordundercursor"); TestPressKey("/\\ctrl-r"); QKeyEvent *ctrlKeyDown = new QKeyEvent(QEvent::KeyPress, Qt::Key_Control, Qt::NoModifier); QApplication::postEvent(emulatedCommandBarTextEdit(), ctrlKeyDown); QApplication::sendPostedEvents(); QKeyEvent *ctrlKeyUp = new QKeyEvent(QEvent::KeyRelease, Qt::Key_Control, Qt::NoModifier); QApplication::postEvent(emulatedCommandBarTextEdit(), ctrlKeyUp); QApplication::sendPostedEvents(); QVERIFY(waitingForRegisterIndicator->isVisible()); TestPressKey("\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("wordundercursor")); TestPressKey("\\ctrl-c"); // Dismiss the bar. FinishTest("wordundercursor"); // Begin tests for ctrl-g, which is almost identical to ctrl-r save that the contents, when added, // are escaped for searching. // Normal register contents/ word under cursor are added as normal. BeginTest("wordinregisterb wordundercursor"); TestPressKey("\"byiw"); TestPressKey("/\\ctrl-g"); QVERIFY(waitingForRegisterIndicator->isVisible()); QVERIFY(waitingForRegisterIndicator->x() >= emulatedCommandBarTextEdit()->x() + emulatedCommandBarTextEdit()->width()); TestPressKey("b"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("wordinregisterb")); QVERIFY(!waitingForRegisterIndicator->isVisible()); TestPressKey("\\ctrl-c\\ctrl-cw/\\ctrl-g\\ctrl-w"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("wordundercursor")); QVERIFY(!waitingForRegisterIndicator->isVisible()); TestPressKey("\\ctrl-c"); TestPressKey("\\ctrl-c"); FinishTest("wordinregisterb wordundercursor"); // \'s must be escaped when inserted via ctrl-g. DoTest("foo a\\b\\\\c\\\\\\d", "wYb/\\ctrl-g0\\enterrX", "foo X\\b\\\\c\\\\\\d"); // $'s must be escaped when inserted via ctrl-g. DoTest("foo a$b", "wYb/\\ctrl-g0\\enterrX", "foo X$b"); DoTest("foo a$b$c", "wYb/\\ctrl-g0\\enterrX", "foo X$b$c"); DoTest("foo a\\$b\\$c", "wYb/\\ctrl-g0\\enterrX", "foo X\\$b\\$c"); // ^'s must be escaped when inserted via ctrl-g. DoTest("foo a^b", "wYb/\\ctrl-g0\\enterrX", "foo X^b"); DoTest("foo a^b^c", "wYb/\\ctrl-g0\\enterrX", "foo X^b^c"); DoTest("foo a\\^b\\^c", "wYb/\\ctrl-g0\\enterrX", "foo X\\^b\\^c"); // .'s must be escaped when inserted via ctrl-g. DoTest("foo axb a.b", "wwYgg/\\ctrl-g0\\enterrX", "foo axb X.b"); DoTest("foo a\\xb Na\\.b", "fNlYgg/\\ctrl-g0\\enterrX", "foo a\\xb NX\\.b"); // *'s must be escaped when inserted via ctrl-g DoTest("foo axxxxb ax*b", "wwYgg/\\ctrl-g0\\enterrX", "foo axxxxb Xx*b"); DoTest("foo a\\xxxxb Na\\x*X", "fNlYgg/\\ctrl-g0\\enterrX", "foo a\\xxxxb NX\\x*X"); // /'s must be escaped when inserted via ctrl-g. DoTest("foo a a/b", "wwYgg/\\ctrl-g0\\enterrX", "foo a X/b"); DoTest("foo a a/b/c", "wwYgg/\\ctrl-g0\\enterrX", "foo a X/b/c"); DoTest("foo a a\\/b\\/c", "wwYgg/\\ctrl-g0\\enterrX", "foo a X\\/b\\/c"); // ['s and ]'s must be escaped when inserted via ctrl-g. DoTest("foo axb a[xyz]b", "wwYgg/\\ctrl-g0\\enterrX", "foo axb X[xyz]b"); DoTest("foo a[b", "wYb/\\ctrl-g0\\enterrX", "foo X[b"); DoTest("foo a[b[c", "wYb/\\ctrl-g0\\enterrX", "foo X[b[c"); DoTest("foo a\\[b\\[c", "wYb/\\ctrl-g0\\enterrX", "foo X\\[b\\[c"); DoTest("foo a]b", "wYb/\\ctrl-g0\\enterrX", "foo X]b"); DoTest("foo a]b]c", "wYb/\\ctrl-g0\\enterrX", "foo X]b]c"); DoTest("foo a\\]b\\]c", "wYb/\\ctrl-g0\\enterrX", "foo X\\]b\\]c"); // Test that expressions involving {'s and }'s work when inserted via ctrl-g. DoTest("foo {", "wYgg/\\ctrl-g0\\enterrX", "foo X"); DoTest("foo }", "wYgg/\\ctrl-g0\\enterrX", "foo X"); DoTest("foo aaaaa \\aaaaa a\\{5}", "WWWYgg/\\ctrl-g0\\enterrX", "foo aaaaa \\aaaaa X\\{5}"); DoTest("foo }", "wYgg/\\ctrl-g0\\enterrX", "foo X"); // Transform newlines into "\\n" when inserted via ctrl-g. DoTest(" \nfoo\nfoo\nxyz\nbar\n123", "jjvjjllygg/\\ctrl-g0\\enterrX", " \nfoo\nXoo\nxyz\nbar\n123"); DoTest(" \nfoo\nfoo\nxyz\nbar\n123", "jjvjjllygg/\\ctrl-g0/e\\enterrX", " \nfoo\nfoo\nxyz\nbaX\n123"); // Don't do any escaping for ctrl-r, though. BeginTest("foo .*$^\\/"); TestPressKey("wY/\\ctrl-r0"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".*$^\\/")); TestPressKey("\\ctrl-c"); TestPressKey("\\ctrl-c"); FinishTest("foo .*$^\\/"); // Ensure that the flag that says "next register insertion should be escaped for searching" // is cleared if we do ctrl-g but then abort with ctrl-c. DoTest("foo a$b", "/\\ctrl-g\\ctrl-c\\ctrl-cwYgg/\\ctrl-r0\\enterrX", "Xoo a$b"); // Ensure that we actually perform a search while typing. BeginTest("abcd"); TestPressKey("/c"); verifyCursorAt(Cursor(0, 2)); TestPressKey("\\enter"); FinishTest("abcd"); // Ensure that the search is from the cursor. BeginTest("acbcd"); TestPressKey("ll/c"); verifyCursorAt(Cursor(0, 3)); TestPressKey("\\enter"); FinishTest("acbcd"); // Reset the cursor to the original position on Ctrl-C BeginTest("acbcd"); TestPressKey("ll/c\\ctrl-crX"); FinishTest("acXcd"); // Reset the cursor to the original position on Ctrl-[ BeginTest("acbcd"); TestPressKey("ll/c\\ctrl-[rX"); FinishTest("acXcd"); // Reset the cursor to the original position on ESC BeginTest("acbcd"); TestPressKey("ll/c\\escrX"); FinishTest("acXcd"); // *Do not* reset the cursor to the original position on Enter. BeginTest("acbcd"); TestPressKey("ll/c\\enterrX"); FinishTest("acbXd"); // *Do not* reset the cursor to the original position on Return. BeginTest("acbcd"); TestPressKey("ll/c\\returnrX"); FinishTest("acbXd"); // Should work with mappings. clearAllMappings(); vi_global->mappings()->add(Mappings::NormalModeMapping, "'testmapping", "/crX", Mappings::Recursive); BeginTest("acbcd"); TestPressKey("'testmapping"); FinishTest("aXbcd"); clearAllMappings(); // Don't send keys that were part of a mapping to the emulated command bar. vi_global->mappings()->add(Mappings::NormalModeMapping, "H", "/a", Mappings::Recursive); BeginTest("foo a aH"); TestPressKey("H\\enterrX"); FinishTest("foo X aH"); clearAllMappings(); // Incremental searching from the original position. BeginTest("foo bar foop fool food"); TestPressKey("ll/foo"); verifyCursorAt(Cursor(0, 8)); TestPressKey("l"); verifyCursorAt(Cursor(0, 13)); TestPressKey("\\backspace"); verifyCursorAt(Cursor(0, 8)); TestPressKey("\\enter"); FinishTest("foo bar foop fool food"); // End up back at the start if no match found BeginTest("foo bar foop fool food"); TestPressKey("ll/fool"); verifyCursorAt(Cursor(0, 13)); TestPressKey("\\backspacex"); verifyCursorAt(Cursor(0, 2)); TestPressKey("\\enter"); FinishTest("foo bar foop fool food"); // Wrap around if no match found. BeginTest("afoom bar foop fool food"); TestPressKey("lll/foom"); verifyCursorAt(Cursor(0, 1)); TestPressKey("\\enter"); FinishTest("afoom bar foop fool food"); // SmartCase: match case-insensitively if the search text is all lower-case. DoTest("foo BaR", "ll/bar\\enterrX", "foo XaR"); // SmartCase: match case-sensitively if the search text is mixed case. DoTest("foo BaR bAr", "ll/bAr\\enterrX", "foo BaR XAr"); // Assume regex by default. DoTest("foo bwibblear", "ll/b.*ar\\enterrX", "foo Xwibblear"); // Set the last search pattern. DoTest("foo bar", "ll/bar\\enterggnrX", "foo Xar"); // Make sure the last search pattern is a regex, too. DoTest("foo bwibblear", "ll/b.*ar\\enterggnrX", "foo Xwibblear"); // 'n' should search case-insensitively if the original search was case-insensitive. DoTest("foo bAR", "ll/bar\\enterggnrX", "foo XAR"); // 'n' should search case-sensitively if the original search was case-sensitive. DoTest("foo bar bAR", "ll/bAR\\enterggnrX", "foo bar XAR"); // 'N' should search case-insensitively if the original search was case-insensitive. DoTest("foo bAR xyz", "ll/bar\\enter$NrX", "foo XAR xyz"); // 'N' should search case-sensitively if the original search was case-sensitive. DoTest("foo bAR bar", "ll/bAR\\enter$NrX", "foo XAR bar"); // Don't forget to set the last search to case-insensitive. DoTest("foo bAR bar", "ll/bAR\\enter^/bar\\enter^nrX", "foo XAR bar"); // Usage of \C for manually specifying case sensitivity. // Strip occurrences of "\C" from the pattern to find. DoTest("foo bar", "/\\\\Cba\\\\Cr\\enterrX", "foo Xar"); // Be careful about escaping, though! DoTest("foo \\Cba\\Cr", "/\\\\\\\\Cb\\\\Ca\\\\\\\\C\\\\C\\\\Cr\\enterrX", "foo XCba\\Cr"); // The item added to the search history should contain all the original \C's. clearSearchHistory(); BeginTest("foo \\Cba\\Cr"); TestPressKey("/\\\\\\\\Cb\\\\Ca\\\\\\\\C\\\\C\\\\Cr\\enterrX"); QCOMPARE(searchHistory().first(), QString("\\\\Cb\\Ca\\\\C\\C\\Cr")); FinishTest("foo XCba\\Cr"); // If there is an escaped C, assume case sensitivity. DoTest("foo bAr BAr bar", "/ba\\\\Cr\\enterrX", "foo bAr BAr Xar"); // The last search pattern should be the last search with escaped C's stripped. DoTest("foo \\Cbar\nfoo \\Cbar", "/\\\\\\\\Cba\\\\C\\\\Cr\\enterggjnrX", "foo \\Cbar\nfoo XCbar"); // If the last search pattern had an escaped "\C", then the next search should be case-sensitive. DoTest("foo bar\nfoo bAr BAr bar", "/ba\\\\Cr\\enterggjnrX", "foo bar\nfoo bAr BAr Xar"); // Don't set the last search parameters if we abort, though. DoTest("foo bar xyz", "/bar\\enter/xyz\\ctrl-cggnrX", "foo Xar xyz"); DoTest("foo bar bAr", "/bar\\enter/bA\\ctrl-cggnrX", "foo Xar bAr"); DoTest("foo bar bar", "/bar\\enter?ba\\ctrl-cggnrX", "foo Xar bar"); // Don't let ":" trample all over the search parameters, either. DoTest("foo bar xyz foo", "/bar\\entergg*:yank\\enterggnrX", "foo bar xyz Xoo"); // Some mirror tests for "?" // Test that "?" summons the search bar, with empty text and with the "?" indicator. QVERIFY(!emulatedCommandBar->isVisible()); BeginTest(""); TestPressKey("?"); QVERIFY(emulatedCommandBar->isVisible()); QCOMPARE(emulatedCommandTypeIndicator()->text(), QString("?")); QVERIFY(emulatedCommandTypeIndicator()->isVisible()); QVERIFY(emulatedCommandBarTextEdit()); QVERIFY(emulatedCommandBarTextEdit()->text().isEmpty()); TestPressKey("\\enter"); FinishTest(""); // Search backwards. DoTest("foo foo bar foo foo", "ww?foo\\enterrX", "foo Xoo bar foo foo"); // Reset cursor if we find nothing. BeginTest("foo foo bar foo foo"); TestPressKey("ww?foo"); verifyCursorAt(Cursor(0, 4)); TestPressKey("d"); verifyCursorAt(Cursor(0, 8)); TestPressKey("\\enter"); FinishTest("foo foo bar foo foo"); // Wrap to the end if we find nothing. DoTest("foo foo bar xyz xyz", "ww?xyz\\enterrX", "foo foo bar xyz Xyz"); // Specify that the last was backwards when using '?' DoTest("foo foo bar foo foo", "ww?foo\\enter^wwnrX", "foo Xoo bar foo foo"); // ... and make sure we do the equivalent with "/" BeginTest("foo foo bar foo foo"); TestPressKey("ww?foo\\enter^ww/foo"); QCOMPARE(emulatedCommandTypeIndicator()->text(), QString("/")); TestPressKey("\\enter^wwnrX"); FinishTest("foo foo bar Xoo foo"); // If we are at the beginning of a word, that word is not the first match in a search // for that word. DoTest("foo foo foo", "w/foo\\enterrX", "foo foo Xoo"); DoTest("foo foo foo", "w?foo\\enterrX", "Xoo foo foo"); // When searching backwards, ensure we can find a match whose range includes the starting cursor position, // if we allow it to wrap around. DoTest("foo foofoofoo bar", "wlll?foofoofoo\\enterrX", "foo Xoofoofoo bar"); // When searching backwards, ensure we can find a match whose range includes the starting cursor position, // even if we don't allow it to wrap around. DoTest("foo foofoofoo foofoofoo", "wlll?foofoofoo\\enterrX", "foo Xoofoofoo foofoofoo"); // The same, but where we the match ends at the end of the line or document. DoTest("foo foofoofoo\nfoofoofoo", "wlll?foofoofoo\\enterrX", "foo Xoofoofoo\nfoofoofoo"); DoTest("foo foofoofoo", "wlll?foofoofoo\\enterrX", "foo Xoofoofoo"); // Searching forwards for just "/" repeats last search. DoTest("foo bar", "/bar\\entergg//\\enterrX", "foo Xar"); // The "last search" can be one initiated via e.g. "*". DoTest("foo bar foo", "/bar\\entergg*gg//\\enterrX", "foo bar Xoo"); // Searching backwards for just "?" repeats last search. DoTest("foo bar bar", "/bar\\entergg??\\enterrX", "foo bar Xar"); // Search forwards treats "?" as a literal. DoTest("foo ?ba?r", "/?ba?r\\enterrX", "foo Xba?r"); // As always, be careful with escaping! DoTest("foo ?ba\\?r", "/?ba\\\\\\\\\\\\?r\\enterrX", "foo Xba\\?r"); // Searching forwards for just "?" finds literal question marks. DoTest("foo ??", "/?\\enterrX", "foo X?"); // Searching backwards for just "/" finds literal forward slashes. DoTest("foo //", "?/\\enterrX", "foo /X"); // Searching forwards, stuff after (and including) an unescaped "/" is ignored. DoTest("foo ba bar bar/xyz", "/bar/xyz\\enterrX", "foo ba Xar bar/xyz"); // Needs to be unescaped, though! DoTest("foo bar bar/xyz", "/bar\\\\/xyz\\enterrX", "foo bar Xar/xyz"); DoTest("foo bar bar\\/xyz", "/bar\\\\\\\\/xyz\\enterrX", "foo bar Xar\\/xyz"); // Searching backwards, stuff after (and including) an unescaped "?" is ignored. DoTest("foo bar bar?xyz bar ba", "?bar?xyz\\enterrX", "foo bar bar?xyz Xar ba"); // Needs to be unescaped, though! DoTest("foo bar bar?xyz bar ba", "?bar\\\\?xyz\\enterrX", "foo bar Xar?xyz bar ba"); DoTest("foo bar bar\\?xyz bar ba", "?bar\\\\\\\\?xyz\\enterrX", "foo bar Xar\\?xyz bar ba"); // If, in a forward search, the first character after the first unescaped "/" is an e, then // we place the cursor at the end of the word. DoTest("foo ba bar bar/eyz", "/bar/e\\enterrX", "foo ba baX bar/eyz"); // Needs to be unescaped, though! DoTest("foo bar bar/eyz", "/bar\\\\/e\\enterrX", "foo bar Xar/eyz"); DoTest("foo bar bar\\/xyz", "/bar\\\\\\\\/e\\enterrX", "foo bar barX/xyz"); // If, in a backward search, the first character after the first unescaped "?" is an e, then // we place the cursor at the end of the word. DoTest("foo bar bar?eyz bar ba", "?bar?e\\enterrX", "foo bar bar?eyz baX ba"); // Needs to be unescaped, though! DoTest("foo bar bar?eyz bar ba", "?bar\\\\?e\\enterrX", "foo bar Xar?eyz bar ba"); DoTest("foo bar bar\\?eyz bar ba", "?bar\\\\\\\\?e\\enterrX", "foo bar barX?eyz bar ba"); // Quick check that repeating the last search and placing the cursor at the end of the match works. DoTest("foo bar bar", "/bar\\entergg//e\\enterrX", "foo baX bar"); DoTest("foo bar bar", "?bar\\entergg??e\\enterrX", "foo bar baX"); // When repeating a change, don't try to convert from Vim to Qt regex again. DoTest("foo bar()", "/bar()\\entergg//e\\enterrX", "foo bar(X"); DoTest("foo bar()", "?bar()\\entergg??e\\enterrX", "foo bar(X"); // If the last search said that we should place the cursor at the end of the match, then // do this with n & N. DoTest("foo bar bar foo", "/bar/e\\enterggnrX", "foo baX bar foo"); DoTest("foo bar bar foo", "/bar/e\\enterggNrX", "foo bar baX foo"); // Don't do this if that search was aborted, though. DoTest("foo bar bar foo", "/bar\\enter/bar/e\\ctrl-cggnrX", "foo Xar bar foo"); DoTest("foo bar bar foo", "/bar\\enter/bar/e\\ctrl-cggNrX", "foo bar Xar foo"); // "#" and "*" reset the "place cursor at the end of the match" to false. DoTest("foo bar bar foo", "/bar/e\\enterggw*nrX", "foo Xar bar foo"); DoTest("foo bar bar foo", "/bar/e\\enterggw#nrX", "foo Xar bar foo"); // "/" and "?" should be usable as motions. DoTest("foo bar", "ld/bar\\enter", "fbar"); // They are not linewise. DoTest("foo bar\nxyz", "ld/yz\\enter", "fyz"); DoTest("foo bar\nxyz", "jld?oo\\enter", "fyz"); // Should be usable in Visual Mode without aborting Visual Mode. DoTest("foo bar", "lv/bar\\enterd", "far"); // Same for ?. DoTest("foo bar", "$hd?oo\\enter", "far"); DoTest("foo bar", "$hv?oo\\enterd", "fr"); DoTest("foo bar", "lv?bar\\enterd", "far"); // If we abort the "/" / "?" motion, the command should be aborted, too. DoTest("foo bar", "d/bar\\esc", "foo bar"); DoTest("foo bar", "d/bar\\ctrl-c", "foo bar"); DoTest("foo bar", "d/bar\\ctrl-[", "foo bar"); // We should be able to repeat a command using "/" or "?" as the motion. DoTest("foo bar bar bar", "d/bar\\enter.", "bar bar"); // The "synthetic" Enter keypress should not be logged as part of the command to be repeated. DoTest("foo bar bar bar\nxyz", "d/bar\\enter.rX", "Xar bar\nxyz"); // Counting. DoTest("foo bar bar bar", "2/bar\\enterrX", "foo bar Xar bar"); // Counting with wraparound. DoTest("foo bar bar bar", "4/bar\\enterrX", "foo Xar bar bar"); // Counting in Visual Mode. DoTest("foo bar bar bar", "v2/bar\\enterd", "ar bar"); // Should update the selection in Visual Mode as we search. BeginTest("foo bar bbc"); TestPressKey("vl/b"); QCOMPARE(kate_view->selectionText(), QString("foo b")); TestPressKey("b"); QCOMPARE(kate_view->selectionText(), QString("foo bar b")); TestPressKey("\\ctrl-h"); QCOMPARE(kate_view->selectionText(), QString("foo b")); TestPressKey("notexists"); QCOMPARE(kate_view->selectionText(), QString("fo")); TestPressKey("\\enter"); // Dismiss bar. QCOMPARE(kate_view->selectionText(), QString("fo")); FinishTest("foo bar bbc"); BeginTest("foo\nxyz\nbar\nbbc"); TestPressKey("Vj/b"); QCOMPARE(kate_view->selectionText(), QString("foo\nxyz\nbar")); TestPressKey("b"); QCOMPARE(kate_view->selectionText(), QString("foo\nxyz\nbar\nbbc")); TestPressKey("\\ctrl-h"); QCOMPARE(kate_view->selectionText(), QString("foo\nxyz\nbar")); TestPressKey("notexists"); QCOMPARE(kate_view->selectionText(), QString("foo\nxyz")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo\nxyz\nbar\nbbc"); // Dismissing the search bar in visual mode should leave original selection. BeginTest("foo bar bbc"); TestPressKey("vl/\\ctrl-c"); QCOMPARE(kate_view->selectionText(), QString("fo")); FinishTest("foo bar bbc"); BeginTest("foo bar bbc"); TestPressKey("vl?\\ctrl-c"); QCOMPARE(kate_view->selectionText(), QString("fo")); FinishTest("foo bar bbc"); BeginTest("foo bar bbc"); TestPressKey("vl/b\\ctrl-c"); QCOMPARE(kate_view->selectionText(), QString("fo")); FinishTest("foo bar bbc"); BeginTest("foo\nbar\nbbc"); TestPressKey("Vl/b\\ctrl-c"); QCOMPARE(kate_view->selectionText(), QString("foo")); FinishTest("foo\nbar\nbbc"); // Search-highlighting tests. const QColor searchHighlightColour = kate_view->renderer()->config()->searchHighlightColor(); BeginTest("foo bar xyz"); // Sanity test. const QList rangesInitial = rangesOnFirstLine(); Q_ASSERT(rangesInitial.isEmpty() && "Assumptions about ranges are wrong - this test is invalid and may need updating!"); FinishTest("foo bar xyz"); // Test highlighting single character match. BeginTest("foo bar xyz"); TestPressKey("/b"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->attribute()->background().color(), searchHighlightColour); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 4); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 5); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Test highlighting two character match. BeginTest("foo bar xyz"); TestPressKey("/ba"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 4); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 6); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Test no highlighting if no longer a match. BeginTest("foo bar xyz"); TestPressKey("/baz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Test highlighting on wraparound. BeginTest(" foo bar xyz"); TestPressKey("ww/foo"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 1); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 4); TestPressKey("\\enter"); FinishTest(" foo bar xyz"); // Test highlighting backwards BeginTest("foo bar xyz"); TestPressKey("$?ba"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 4); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 6); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Test no highlighting when no match is found searching backwards BeginTest("foo bar xyz"); TestPressKey("$?baz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Test highlight when wrapping around after searching backwards. BeginTest("foo bar xyz"); TestPressKey("w?xyz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 8); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 11); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Test no highlighting when bar is dismissed. DoTest("foo bar xyz", "/bar\\ctrl-c", "foo bar xyz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); DoTest("foo bar xyz", "/bar\\enter", "foo bar xyz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); DoTest("foo bar xyz", "/bar\\ctrl-[", "foo bar xyz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); DoTest("foo bar xyz", "/bar\\return", "foo bar xyz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); DoTest("foo bar xyz", "/bar\\esc", "foo bar xyz"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); // Update colour on config change. BeginTest("foo bar xyz"); TestPressKey("/xyz"); const QColor newSearchHighlightColour = QColor(255, 0, 0); kate_view->renderer()->config()->setSearchHighlightColor(newSearchHighlightColour); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->attribute()->background().color(), newSearchHighlightColour); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Set the background colour appropriately. KColorScheme currentColorScheme(QPalette::Normal); const QColor normalBackgroundColour = QPalette().brush(QPalette::Base).color(); const QColor matchBackgroundColour = currentColorScheme.background(KColorScheme::PositiveBackground).color(); const QColor noMatchBackgroundColour = currentColorScheme.background(KColorScheme::NegativeBackground).color(); BeginTest("foo bar xyz"); TestPressKey("/xyz"); verifyTextEditBackgroundColour(matchBackgroundColour); TestPressKey("a"); verifyTextEditBackgroundColour(noMatchBackgroundColour); TestPressKey("\\ctrl-w"); verifyTextEditBackgroundColour(normalBackgroundColour); TestPressKey("/xyz\\enter/"); verifyTextEditBackgroundColour(normalBackgroundColour); TestPressKey("\\enter"); FinishTest("foo bar xyz"); // Escape regex's in a Vim-ish style. // Unescaped ( and ) are always literals. DoTest("foo bar( xyz", "/bar(\\enterrX", "foo Xar( xyz"); DoTest("foo bar) xyz", "/bar)\\enterrX", "foo Xar) xyz"); // + is literal, unless it is already escaped. DoTest("foo bar+ xyz", "/bar+ \\enterrX", "foo Xar+ xyz"); DoTest(" foo+AAAAbar", "/foo+A\\\\+bar\\enterrX", " Xoo+AAAAbar"); DoTest(" foo++++bar", "/foo+\\\\+bar\\enterrX", " Xoo++++bar"); DoTest(" foo++++bar", "/+\\enterrX", " fooX+++bar"); // An escaped "\" is a literal, of course. DoTest("foo x\\y", "/x\\\\\\\\y\\enterrX", "foo X\\y"); // ( and ), if escaped, are not literals. DoTest("foo barbarxyz", "/ \\\\(bar\\\\)\\\\+xyz\\enterrX", "foo Xbarbarxyz"); // Handle escaping correctly if we have an escaped and unescaped bracket next to each other. DoTest("foo x(A)y", "/x(\\\\(.\\\\))y\\enterrX", "foo X(A)y"); // |, if unescaped, is literal. DoTest("foo |bar", "/|\\enterrX", "foo Xbar"); // |, if escaped, is not a literal. DoTest("foo xfoo\\y xbary", "/x\\\\(foo\\\\|bar\\\\)y\\enterrX", "foo xfoo\\y Xbary"); // A single [ is a literal. DoTest("foo bar[", "/bar[\\enterrX", "foo Xar["); // A single ] is a literal. DoTest("foo bar]", "/bar]\\enterrX", "foo Xar]"); // A matching [ and ] are *not* literals. DoTest("foo xbcay", "/x[abc]\\\\+y\\enterrX", "foo Xbcay"); DoTest("foo xbcay", "/[abc]\\\\+y\\enterrX", "foo xXcay"); DoTest("foo xbaadcdcy", "/x[ab]\\\\+[cd]\\\\+y\\enterrX", "foo Xbaadcdcy"); // Need to be an unescaped match, though. DoTest("foo xbcay", "/x[abc\\\\]\\\\+y\\enterrX", "Xoo xbcay"); DoTest("foo xbcay", "/x\\\\[abc]\\\\+y\\enterrX", "Xoo xbcay"); DoTest("foo x[abc]]]]]y", "/x\\\\[abc]\\\\+y\\enterrX", "foo X[abc]]]]]y"); // An escaped '[' between matching unescaped '[' and ']' is treated as a literal '[' DoTest("foo xb[cay", "/x[a\\\\[bc]\\\\+y\\enterrX", "foo Xb[cay"); // An escaped ']' between matching unescaped '[' and ']' is treated as a literal ']' DoTest("foo xb]cay", "/x[a\\\\]bc]\\\\+y\\enterrX", "foo Xb]cay"); // An escaped '[' not between other square brackets is a literal. DoTest("foo xb[cay", "/xb\\\\[\\enterrX", "foo Xb[cay"); DoTest("foo xb[cay", "/\\\\[ca\\enterrX", "foo xbXcay"); // An escaped ']' not between other square brackets is a literal. DoTest("foo xb]cay", "/xb\\\\]\\enterrX", "foo Xb]cay"); DoTest("foo xb]cay", "/\\\\]ca\\enterrX", "foo xbXcay"); // An unescaped '[' not between other square brackets is a literal. DoTest("foo xbaba[y", "/x[ab]\\\\+[y\\enterrX", "foo Xbaba[y"); DoTest("foo xbaba[dcdcy", "/x[ab]\\\\+[[cd]\\\\+y\\enterrX", "foo Xbaba[dcdcy"); // An unescaped ']' not between other square brackets is a literal. DoTest("foo xbaba]y", "/x[ab]\\\\+]y\\enterrX", "foo Xbaba]y"); DoTest("foo xbaba]dcdcy", "/x[ab]\\\\+][cd]\\\\+y\\enterrX", "foo Xbaba]dcdcy"); // Be more clever about how we identify escaping: the presence of a preceding // backslash is not always sufficient! DoTest("foo x\\babay", "/x\\\\\\\\[ab]\\\\+y\\enterrX", "foo X\\babay"); DoTest("foo x\\[abc]]]]y", "/x\\\\\\\\\\\\[abc]\\\\+y\\enterrX", "foo X\\[abc]]]]y"); DoTest("foo xa\\b\\c\\y", "/x[abc\\\\\\\\]\\\\+y\\enterrX", "foo Xa\\b\\c\\y"); DoTest("foo x[abc\\]]]]y", "/x[abc\\\\\\\\\\\\]\\\\+y\\enterrX", "foo X[abc\\]]]]y"); DoTest("foo xa[\\b\\[y", "/x[ab\\\\\\\\[]\\\\+y\\enterrX", "foo Xa[\\b\\[y"); DoTest("foo x\\[y", "/x\\\\\\\\[y\\enterrX", "foo X\\[y"); DoTest("foo x\\]y", "/x\\\\\\\\]y\\enterrX", "foo X\\]y"); DoTest("foo x\\+y", "/x\\\\\\\\+y\\enterrX", "foo X\\+y"); // A dot is not a literal, nor is a star. DoTest("foo bar", "/o.*b\\enterrX", "fXo bar"); // Escaped dots and stars are literals, though. DoTest("foo xay x.y", "/x\\\\.y\\enterrX", "foo xay X.y"); DoTest("foo xaaaay xa*y", "/xa\\\\*y\\enterrX", "foo xaaaay Xa*y"); // Unescaped curly braces are literals. DoTest("foo x{}y", "/x{}y\\enterrX", "foo X{}y"); // Escaped curly brackets are quantifers. DoTest("foo xaaaaay", "/xa\\\\{5\\\\}y\\enterrX", "foo Xaaaaay"); // Matching curly brackets where only the first is escaped are also quantifiers. DoTest("foo xaaaaaybbbz", "/xa\\\\{5}yb\\\\{3}z\\enterrX", "foo Xaaaaaybbbz"); // Make sure it really is escaped, though! DoTest("foo xa\\{5}", "/xa\\\\\\\\{5}\\enterrX", "foo Xa\\{5}"); // Don't crash if the first character is a } DoTest("foo aaaaay", "/{\\enterrX", "Xoo aaaaay"); // Vim's '\<' and '\>' map, roughly, to Qt's '\b' DoTest("foo xbar barx bar", "/bar\\\\>\\enterrX", "foo xXar barx bar"); DoTest("foo xbar barx bar", "/\\\\\\enterrX", "foo xbar barx Xar "); DoTest("foo xbar barx bar", "/\\\\\\enterrX", "foo xbar barx Xar"); DoTest("foo xbar barx\nbar", "/\\\\\\enterrX", "foo xbar barx\nXar"); // Escaped "^" and "$" are treated as literals. DoTest("foo x^$y", "/x\\\\^\\\\$y\\enterrX", "foo X^$y"); // Ensure that it is the escaped version of the pattern that is recorded as the last search pattern. DoTest("foo bar( xyz", "/bar(\\enterggnrX", "foo Xar( xyz"); // Don't log keypresses sent to the emulated command bar as commands to be repeated via "."! DoTest("foo", "/diw\\enterciwbar\\ctrl-c.", "bar"); // Don't leave Visual mode on aborting a search. DoTest("foo bar", "vw/\\ctrl-cd", "ar"); DoTest("foo bar", "vw/\\ctrl-[d", "ar"); // Don't crash on leaving Visual Mode on aborting a search. This is perhaps the most opaque regression // test ever; what it's testing for is the situation where the synthetic keypress issue by the emulated // command bar on the "ctrl-[" is sent to the key mapper. This in turn converts it into a weird character // which is then, upon not being recognised as part of a mapping, sent back around the keypress processing, // where it ends up being sent to the emulated command bar's text edit, which in turn issues a "text changed" // event where the text is still empty, which tries to move the cursor to (-1, -1), which causes a crash deep // within Kate. So, in a nutshell: this test ensures that the keymapper never handles the synthetic keypress :) DoTest("", "ifoo\\ctrl-cv/\\ctrl-[", "foo"); // History auto-completion tests. clearSearchHistory(); QVERIFY(searchHistory().isEmpty()); vi_global->searchHistory()->append("foo"); vi_global->searchHistory()->append("bar"); QCOMPARE(searchHistory(), QStringList() << "foo" << "bar"); clearSearchHistory(); QVERIFY(searchHistory().isEmpty()); // Ensure current search bar text is added to the history if we press enter. DoTest("foo bar", "/bar\\enter", "foo bar"); DoTest("foo bar", "/xyz\\enter", "foo bar"); QCOMPARE(searchHistory(), QStringList() << "bar" << "xyz"); // Interesting - Vim adds the search bar text to the history even if we abort via e.g. ctrl-c, ctrl-[, etc. clearSearchHistory(); DoTest("foo bar", "/baz\\ctrl-[", "foo bar"); QCOMPARE(searchHistory(), QStringList() << "baz"); clearSearchHistory(); DoTest("foo bar", "/foo\\esc", "foo bar"); QCOMPARE(searchHistory(), QStringList() << "foo"); clearSearchHistory(); DoTest("foo bar", "/nose\\ctrl-c", "foo bar"); QCOMPARE(searchHistory(), QStringList() << "nose"); clearSearchHistory(); vi_global->searchHistory()->append("foo"); vi_global->searchHistory()->append("bar"); QVERIFY(emulatedCommandBarCompleter() != nullptr); BeginTest("foo bar"); TestPressKey("/\\ctrl-p"); verifyCommandBarCompletionVisible(); // Make sure the completion appears in roughly the correct place: this is a little fragile :/ const QPoint completerRectTopLeft = emulatedCommandBarCompleter()->popup()->mapToGlobal(emulatedCommandBarCompleter()->popup()->rect().topLeft()) ; const QPoint barEditBottomLeft = emulatedCommandBarTextEdit()->mapToGlobal(emulatedCommandBarTextEdit()->rect().bottomLeft()); QCOMPARE(completerRectTopLeft.x(), barEditBottomLeft.x()); QVERIFY(qAbs(completerRectTopLeft.y() - barEditBottomLeft.y()) <= 1); // Will activate the current completion item, activating the search, and dismissing the bar. TestPressKey("\\enter"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); // Close the command bar. FinishTest("foo bar"); // Don't show completion with an empty search bar. clearSearchHistory(); vi_global->searchHistory()->append("foo"); BeginTest("foo bar"); TestPressKey("/"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\enter"); FinishTest("foo bar"); // Don't auto-complete, either. clearSearchHistory(); vi_global->searchHistory()->append("foo"); BeginTest("foo bar"); TestPressKey("/f"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\enter"); FinishTest("foo bar"); clearSearchHistory(); vi_global->searchHistory()->append("xyz"); vi_global->searchHistory()->append("bar"); QVERIFY(emulatedCommandBarCompleter() != nullptr); BeginTest("foo bar"); TestPressKey("/\\ctrl-p"); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("bar")); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo bar"); clearSearchHistory(); vi_global->searchHistory()->append("xyz"); vi_global->searchHistory()->append("bar"); vi_global->searchHistory()->append("foo"); QVERIFY(emulatedCommandBarCompleter() != nullptr); BeginTest("foo bar"); TestPressKey("/\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("foo")); QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 0); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("bar")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("bar")); QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 1); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("xyz")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("xyz")); QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 2); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("foo")); // Wrap-around QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 0); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo bar"); clearSearchHistory(); vi_global->searchHistory()->append("xyz"); vi_global->searchHistory()->append("bar"); vi_global->searchHistory()->append("foo"); QVERIFY(emulatedCommandBarCompleter() != nullptr); BeginTest("foo bar"); TestPressKey("/\\ctrl-n"); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("xyz")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("xyz")); QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 2); TestPressKey("\\ctrl-n"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("bar")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("bar")); QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 1); TestPressKey("\\ctrl-n"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("foo")); QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 0); TestPressKey("\\ctrl-n"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("xyz")); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("xyz")); // Wrap-around. QCOMPARE(emulatedCommandBarCompleter()->popup()->currentIndex().row(), 2); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo bar"); clearSearchHistory(); vi_global->searchHistory()->append("xyz"); vi_global->searchHistory()->append("bar"); vi_global->searchHistory()->append("foo"); BeginTest("foo bar"); TestPressKey("/\\ctrl-n\\ctrl-n"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("bar")); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo bar"); // If we add something to the history, remove any earliest occurrences (this is what Vim appears to do) // and append to the end. clearSearchHistory(); vi_global->searchHistory()->append("bar"); vi_global->searchHistory()->append("xyz"); vi_global->searchHistory()->append("foo"); vi_global->searchHistory()->append("xyz"); QCOMPARE(searchHistory(), QStringList() << "bar" << "foo" << "xyz"); // Push out older entries if we have too many search items in the history. const int HISTORY_SIZE_LIMIT = 100; clearSearchHistory(); for (int i = 1; i <= HISTORY_SIZE_LIMIT; i++) { vi_global->searchHistory()->append(QString("searchhistoryitem %1").arg(i)); } QCOMPARE(searchHistory().size(), HISTORY_SIZE_LIMIT); QCOMPARE(searchHistory().first(), QString("searchhistoryitem 1")); QCOMPARE(searchHistory().last(), QString("searchhistoryitem 100")); vi_global->searchHistory()->append(QString("searchhistoryitem %1").arg(HISTORY_SIZE_LIMIT + 1)); QCOMPARE(searchHistory().size(), HISTORY_SIZE_LIMIT); QCOMPARE(searchHistory().first(), QString("searchhistoryitem 2")); QCOMPARE(searchHistory().last(), QString("searchhistoryitem %1").arg(HISTORY_SIZE_LIMIT + 1)); // Don't add empty searches to the history. clearSearchHistory(); DoTest("foo bar", "/\\enter", "foo bar"); QVERIFY(searchHistory().isEmpty()); // "*" and "#" should add the relevant word to the search history, enclosed between \< and \> clearSearchHistory(); BeginTest("foo bar"); TestPressKey("*"); QVERIFY(!searchHistory().isEmpty()); QCOMPARE(searchHistory().last(), QString("\\")); TestPressKey("w#"); QCOMPARE(searchHistory().size(), 2); QCOMPARE(searchHistory().last(), QString("\\")); // Auto-complete words from the document on ctrl-space. // Test that we can actually find a single word and add it to the list of completions. BeginTest("foo"); TestPressKey("/\\ctrl- "); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("foo")); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo"); // Count digits and underscores as being part of a word. BeginTest("foo_12"); TestPressKey("/\\ctrl- "); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("foo_12")); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo_12"); // This feels a bit better to me, usability-wise: in the special case of completion from document, where // the completion list is manually summoned, allow one to press Enter without the bar being dismissed // (just dismiss the completion list instead). BeginTest("foo_12"); TestPressKey("/\\ctrl- \\ctrl-p\\enter"); QVERIFY(emulatedCommandBar->isVisible()); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo_12"); // Check that we can find multiple words on one line. BeginTest("bar (foo) [xyz]"); TestPressKey("/\\ctrl- "); QStringListModel *completerStringListModel = dynamic_cast(emulatedCommandBarCompleter()->model()); Q_ASSERT(completerStringListModel); QCOMPARE(completerStringListModel->stringList(), QStringList() << "bar" << "foo" << "xyz"); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("bar (foo) [xyz]"); // Check that we arrange the found words in case-insensitive sorted order. BeginTest("D c e a b f"); TestPressKey("/\\ctrl- "); verifyCommandBarCompletionsMatches(QStringList() << "a" << "b" << "c" << "D" << "e" << "f"); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("D c e a b f"); // Check that we don't include the same word multiple times. BeginTest("foo bar bar bar foo"); TestPressKey("/\\ctrl- "); verifyCommandBarCompletionsMatches(QStringList() << "bar" << "foo"); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo bar bar bar foo"); // Check that we search only a narrow portion of the document, around the cursor (4096 lines either // side, say). QStringList manyLines; for (int i = 1; i < 2 * 4096 + 3; i++) { // Pad the digits so that when sorted alphabetically, they are also sorted numerically. manyLines << QString("word%1").arg(i, 5, 10, QChar('0')); } QStringList allButFirstAndLastOfManyLines = manyLines; allButFirstAndLastOfManyLines.removeFirst(); allButFirstAndLastOfManyLines.removeLast(); BeginTest(manyLines.join("\n")); TestPressKey("4097j/\\ctrl- "); verifyCommandBarCompletionsMatches(allButFirstAndLastOfManyLines); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest(manyLines.join("\n")); // "The current word" means the word before the cursor in the command bar, and includes numbers // and underscores. Make sure also that the completion prefix is set when the completion is first invoked. BeginTest("foo fee foa_11 foa_11b"); // Write "bar(foa112$nose" and position cursor before the "2", then invoke completion. TestPressKey("/bar(foa_112$nose\\left\\left\\left\\left\\left\\left\\ctrl- "); verifyCommandBarCompletionsMatches(QStringList() << "foa_11" << "foa_11b"); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo fee foa_11 foa_11b"); // But don't count "-" as being part of the current word. BeginTest("foo_12"); TestPressKey("/bar-foo\\ctrl- "); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("foo_12")); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo_12"); // Be case insensitive. BeginTest("foo Fo12 fOo13 FO45"); TestPressKey("/fo\\ctrl- "); verifyCommandBarCompletionsMatches(QStringList() << "Fo12" << "FO45" << "foo" << "fOo13"); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo Fo12 fOo13 FO45"); // Feed the current word to complete to the completer as we type/ edit. BeginTest("foo fee foa foab"); TestPressKey("/xyz|f\\ctrl- o"); verifyCommandBarCompletionsMatches(QStringList() << "foa" << "foab" << "foo"); TestPressKey("a"); verifyCommandBarCompletionsMatches(QStringList() << "foa" << "foab"); TestPressKey("\\ctrl-h"); verifyCommandBarCompletionsMatches(QStringList() << "foa" << "foab" << "foo"); TestPressKey("o"); verifyCommandBarCompletionsMatches(QStringList() << "foo"); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo fee foa foab"); // Upon selecting a completion with an empty command bar, add the completed text to the command bar. BeginTest("foo fee fob foables"); TestPressKey("/\\ctrl- foa\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foables")); verifyCommandBarCompletionVisible(); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo fee fob foables"); // If bar is non-empty, replace the word under the cursor. BeginTest("foo fee foa foab"); TestPressKey("/xyz|f$nose\\left\\left\\left\\left\\left\\ctrl- oa\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("xyz|foab$nose")); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("foo fee foa foab"); // Place the cursor at the end of the completed text. BeginTest("foo fee foa foab"); TestPressKey("/xyz|f$nose\\left\\left\\left\\left\\left\\ctrl- oa\\ctrl-p\\enterX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("xyz|foabX$nose")); TestPressKey("\\ctrl-c"); // Dismiss completion, then bar. FinishTest("foo fee foa foab"); // If we're completing from history, though, the entire text gets set, and the completion prefix // is the beginning of the entire text, not the current word before the cursor. clearSearchHistory(); vi_global->searchHistory()->append("foo(bar"); BeginTest(""); TestPressKey("/foo(b\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "foo(bar"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo(bar")); TestPressKey("\\enter"); // Dismiss bar. FinishTest(""); // If we're completing from history and we abort the completion via ctrl-c or ctrl-[, we revert the whole // text to the last manually typed text. clearSearchHistory(); vi_global->searchHistory()->append("foo(b|ar"); BeginTest(""); TestPressKey("/foo(b\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "foo(b|ar"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo(b|ar")); TestPressKey("\\ctrl-c"); // Dismiss completion. QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo(b")); TestPressKey("\\enter"); // Dismiss bar. FinishTest(""); // Scroll completion list if necessary so that currently selected completion is visible. BeginTest("a b c d e f g h i j k l m n o p q r s t u v w x y z"); TestPressKey("/\\ctrl- "); const int lastItemRow = 25; const QRect initialLastCompletionItemRect = emulatedCommandBarCompleter()->popup()->visualRect(emulatedCommandBarCompleter()->popup()->model()->index(lastItemRow, 0)); QVERIFY(!emulatedCommandBarCompleter()->popup()->rect().contains(initialLastCompletionItemRect)); // If this fails, then we have an error in the test setup: initially, the last item in the list should be outside of the bounds of the popup. TestPressKey("\\ctrl-n"); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("z")); const QRect lastCompletionItemRect = emulatedCommandBarCompleter()->popup()->visualRect(emulatedCommandBarCompleter()->popup()->model()->index(lastItemRow, 0)); QVERIFY(emulatedCommandBarCompleter()->popup()->rect().contains(lastCompletionItemRect)); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("a b c d e f g h i j k l m n o p q r s t u v w x y z"); // Ensure that the completion list changes size appropriately as the number of candidate completions changes. BeginTest("a ab abc"); TestPressKey("/\\ctrl- "); const int initialPopupHeight = emulatedCommandBarCompleter()->popup()->height(); TestPressKey("ab"); const int popupHeightAfterEliminatingOne = emulatedCommandBarCompleter()->popup()->height(); QVERIFY(popupHeightAfterEliminatingOne < initialPopupHeight); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("a ab abc"); // Ensure that the completion list disappears when no candidate completions are found, but re-appears // when some are found. BeginTest("a ab abc"); TestPressKey("/\\ctrl- "); TestPressKey("abd"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-h"); verifyCommandBarCompletionVisible(); TestPressKey("\\enter\\enter"); // Dismiss completion, then bar. FinishTest("a ab abc"); // ctrl-c and ctrl-[ when the completion list is visible should dismiss the completion list, but *not* // the emulated command bar. TODO - same goes for ESC, but this is harder as KateViewInternal dismisses it // itself. BeginTest("a ab abc"); TestPressKey("/\\ctrl- \\ctrl-cdiw"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); // Dismiss bar. TestPressKey("/\\ctrl- \\ctrl-[diw"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\enter"); // Dismiss bar. FinishTest("a ab abc"); // If we implicitly choose an element from the summoned completion list (by highlighting it, then // continuing to edit the text), the completion box should not re-appear unless explicitly summoned // again, even if the current word has a valid completion. BeginTest("a ab abc"); TestPressKey("/\\ctrl- \\ctrl-p"); TestPressKey(".a"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\enter"); // Dismiss bar. FinishTest("a ab abc"); // If we dismiss the summoned completion list via ctrl-c or ctrl-[, it should not re-appear unless explicitly summoned // again, even if the current word has a valid completion. BeginTest("a ab abc"); TestPressKey("/\\ctrl- \\ctrl-c"); TestPressKey(".a"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\enter"); TestPressKey("/\\ctrl- \\ctrl-["); TestPressKey(".a"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\enter"); // Dismiss bar. FinishTest("a ab abc"); // If we select a completion from an empty bar, but then dismiss it via ctrl-c or ctrl-[, then we // should restore the empty text. BeginTest("foo"); TestPressKey("/\\ctrl- \\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); TestPressKey("\\ctrl-c"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("")); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo"); BeginTest("foo"); TestPressKey("/\\ctrl- \\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("foo")); TestPressKey("\\ctrl-["); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("")); TestPressKey("\\enter"); // Dismiss bar. FinishTest("foo"); // If we select a completion but then dismiss it via ctrl-c or ctrl-[, then we // should restore the last manually typed word. BeginTest("fooabc"); TestPressKey("/f\\ctrl- o\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("fooabc")); TestPressKey("\\ctrl-c"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); QVERIFY(emulatedCommandBar->isVisible()); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("fo")); TestPressKey("\\enter"); // Dismiss bar. FinishTest("fooabc"); // If we select a completion but then dismiss it via ctrl-c or ctrl-[, then we // should restore the word currently being typed to the last manually typed word. BeginTest("fooabc"); TestPressKey("/ab\\ctrl- |fo\\ctrl-p"); TestPressKey("\\ctrl-c"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("ab|fo")); TestPressKey("\\enter"); // Dismiss bar. FinishTest("fooabc"); // Set the completion prefix for the search history completion as soon as it is shown. clearSearchHistory(); vi_global->searchHistory()->append("foo(bar"); vi_global->searchHistory()->append("xyz"); BeginTest(""); TestPressKey("/f\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "foo(bar"); TestPressKey("\\enter"); // Dismiss bar. FinishTest(""); // Command Mode (:) tests. // ":" should summon the command bar, with ":" as the label. BeginTest(""); TestPressKey(":"); QVERIFY(emulatedCommandBar->isVisible()); QCOMPARE(emulatedCommandTypeIndicator()->text(), QString(":")); QVERIFY(emulatedCommandTypeIndicator()->isVisible()); QVERIFY(emulatedCommandBarTextEdit()); QVERIFY(emulatedCommandBarTextEdit()->text().isEmpty()); TestPressKey("\\esc"); FinishTest(""); // If we have a selection, it should be encoded as a range in the text edit. BeginTest("d\nb\na\nc"); TestPressKey("Vjjj:"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("'<,'>")); TestPressKey("\\esc"); FinishTest("d\nb\na\nc"); // If we have a count, it should be encoded as a range in the text edit. BeginTest("d\nb\na\nc"); TestPressKey("7:"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+6")); TestPressKey("\\esc"); FinishTest("d\nb\na\nc"); // Don't go doing an incremental search when we press keys! BeginTest("foo bar xyz"); TestPressKey(":bar"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); TestPressKey("\\esc"); FinishTest("foo bar xyz"); // Execute the command on Enter. DoTest("d\nb\na\nc", "Vjjj:sort\\enter", "a\nb\nc\nd"); // Don't crash if we call a non-existent command with a range. DoTest("123", ":42nonexistentcommand\\enter", "123"); // Bar background should always be normal for command bar. BeginTest("foo"); TestPressKey("/foo\\enter:"); verifyTextEditBackgroundColour(normalBackgroundColour); TestPressKey("\\ctrl-c/bar\\enter:"); verifyTextEditBackgroundColour(normalBackgroundColour); TestPressKey("\\esc"); FinishTest("foo"); const int commandResponseMessageTimeOutMSOverride = QString::fromLatin1(qgetenv("KATE_VIMODE_TEST_COMMANDRESPONSEMESSAGETIMEOUTMS")).toInt(); const long commandResponseMessageTimeOutMS = (commandResponseMessageTimeOutMSOverride > 0) ? commandResponseMessageTimeOutMSOverride : 4000; { // If there is any output from the command, show it in a label for a short amount of time // (make sure the bar type indicator is hidden, here, as it looks messy). emulatedCommandBar->setCommandResponseMessageTimeout(commandResponseMessageTimeOutMS); BeginTest("foo bar xyz"); const QDateTime timeJustBeforeCommandExecuted = QDateTime::currentDateTime(); TestPressKey(":commandthatdoesnotexist\\enter"); QVERIFY(emulatedCommandBar->isVisible()); QVERIFY(commandResponseMessageDisplay()); QVERIFY(commandResponseMessageDisplay()->isVisible()); QVERIFY(!emulatedCommandBarTextEdit()->isVisible()); QVERIFY(!emulatedCommandTypeIndicator()->isVisible()); // Be a bit vague about the exact message, due to i18n, etc. QVERIFY(commandResponseMessageDisplay()->text().contains("commandthatdoesnotexist")); waitForEmulatedCommandBarToHide(4 * commandResponseMessageTimeOutMS); QVERIFY(timeJustBeforeCommandExecuted.msecsTo(QDateTime::currentDateTime()) >= commandResponseMessageTimeOutMS - 500); // "- 500" because coarse timers can fire up to 500ms *prematurely*. QVERIFY(!emulatedCommandBar->isVisible()); // Piggy-back on this test, as the bug we're about to test for would actually make setting // up the conditions again in a separate test impossible ;) // When we next summon the bar, the response message should be invisible; the editor visible & editable; // and the bar type indicator visible again. TestPressKey("/"); QVERIFY(!commandResponseMessageDisplay()->isVisible()); QVERIFY(emulatedCommandBarTextEdit()->isVisible()); QVERIFY(emulatedCommandBarTextEdit()->isEnabled()); QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\esc"); // Dismiss the bar. FinishTest("foo bar xyz"); } { // Show the same message twice in a row. BeginTest("foo bar xyz"); TestPressKey(":othercommandthatdoesnotexist\\enter"); QDateTime startWaitingForMessageToHide = QDateTime::currentDateTime(); waitForEmulatedCommandBarToHide(4 * commandResponseMessageTimeOutMS); TestPressKey(":othercommandthatdoesnotexist\\enter"); QVERIFY(commandResponseMessageDisplay()->isVisible()); // Wait for it to disappear again, as a courtesy for the next test. waitForEmulatedCommandBarToHide(4 * commandResponseMessageTimeOutMS); } { // Emulated command bar should not steal keypresses when it is merely showing the results of an executed command. BeginTest("foo bar"); TestPressKey(":commandthatdoesnotexist\\enterrX"); Q_ASSERT_X(commandResponseMessageDisplay()->isVisible(), "running test", "Need to increase timeJustBeforeCommandExecuted!"); FinishTest("Xoo bar"); } { // Don't send the synthetic "enter" keypress (for making search-as-a-motion work) when we finally hide. BeginTest("foo bar\nbar"); TestPressKey(":commandthatdoesnotexist\\enter"); Q_ASSERT_X(commandResponseMessageDisplay()->isVisible(), "running test", "Need to increase timeJustBeforeCommandExecuted!"); waitForEmulatedCommandBarToHide(commandResponseMessageTimeOutMS * 4); TestPressKey("rX"); FinishTest("Xoo bar\nbar"); } { // The timeout should be cancelled when we invoke the command bar again. BeginTest(""); TestPressKey(":commandthatdoesnotexist\\enter"); const QDateTime waitStartedTime = QDateTime::currentDateTime(); TestPressKey(":"); // Wait ample time for the timeout to fire. Do not use waitForEmulatedCommandBarToHide for this! while(waitStartedTime.msecsTo(QDateTime::currentDateTime()) < commandResponseMessageTimeOutMS * 2) { QApplication::processEvents(); } QVERIFY(emulatedCommandBar->isVisible()); TestPressKey("\\esc"); // Dismiss the bar. FinishTest(""); } { // The timeout should not cause kate_view to regain focus if we have manually taken it away. qDebug()<< " NOTE: this test is weirdly fragile, so if it starts failing, comment it out and e-mail me: it may well be more trouble that it's worth."; BeginTest(""); TestPressKey(":commandthatdoesnotexist\\enter"); while (QApplication::hasPendingEvents()) { // Wait for any focus changes to take effect. QApplication::processEvents(); } const QDateTime waitStartedTime = QDateTime::currentDateTime(); QLineEdit *dummyToFocus = new QLineEdit(QString("Sausage"), mainWindow); // Take focus away from kate_view by giving it to dummyToFocus. QApplication::setActiveWindow(mainWindow); kate_view->setFocus(); mainWindowLayout->addWidget(dummyToFocus); dummyToFocus->show(); dummyToFocus->setEnabled(true); dummyToFocus->setFocus(); // Allow dummyToFocus to receive focus. while(!dummyToFocus->hasFocus()) { QApplication::processEvents(); } QVERIFY(dummyToFocus->hasFocus()); // Wait ample time for the timeout to fire. Do not use waitForEmulatedCommandBarToHide for this - // the bar never actually hides in this instance, and I think it would take some deep changes in // Kate to make it do so (the KateCommandLineBar as the same issue). while(waitStartedTime.msecsTo(QDateTime::currentDateTime()) < commandResponseMessageTimeOutMS * 2) { QApplication::processEvents(); } QVERIFY(dummyToFocus->hasFocus()); QVERIFY(emulatedCommandBar->isVisible()); mainWindowLayout->removeWidget(dummyToFocus); // Restore focus to the kate_view. kate_view->setFocus(); while(!kate_view->hasFocus()) { QApplication::processEvents(); } // *Now* wait for the command bar to disappear - giving kate_view focus should trigger it. waitForEmulatedCommandBarToHide(commandResponseMessageTimeOutMS * 4); FinishTest(""); } { // No completion should be shown when the bar is first shown: this gives us an opportunity // to invoke command history via ctrl-p and ctrl-n. BeginTest(""); TestPressKey(":"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); } { // Should be able to switch to completion from document, even when we have a completion from commands. BeginTest("soggy1 soggy2"); TestPressKey(":so"); verifyCommandBarCompletionContains(QStringList() << "sort"); TestPressKey("\\ctrl- "); verifyCommandBarCompletionsMatches(QStringList() << "soggy1" << "soggy2"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest("soggy1 soggy2"); } { // If we dismiss the command completion then change the text, it should summon the completion // again. BeginTest(""); TestPressKey(":so"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("r"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionContains(QStringList() << "sort"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); } { // Completion should be dismissed when we are showing command response text. BeginTest(""); TestPressKey(":set-au\\enter"); QVERIFY(commandResponseMessageDisplay()->isVisible()); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); waitForEmulatedCommandBarToHide(commandResponseMessageTimeOutMS * 4); FinishTest(""); } // If we abort completion via ctrl-c or ctrl-[, we should revert the current word to the last // manually entered word. BeginTest(""); TestPressKey(":se\\ctrl-p"); verifyCommandBarCompletionVisible(); QVERIFY(emulatedCommandBarTextEdit()->text() != "se"); TestPressKey("\\ctrl-c"); // Dismiss completer QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("se")); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // In practice, it's annoying if, as we enter ":s/se", completions pop up after the "se": // for now, only summon completion if we are on the first word in the text. BeginTest(""); TestPressKey(":s/se"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); BeginTest(""); TestPressKey(":.,.+7s/se"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // Don't blank the text if we activate command history completion with no command history. BeginTest(""); clearCommandHistory(); TestPressKey(":s/se\\ctrl-p"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/se")); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // On completion, only update the command in front of the cursor. BeginTest(""); clearCommandHistory(); TestPressKey(":.,.+6s/se\\left\\left\\leftet-auto-in\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+6set-auto-indent/se")); TestPressKey("\\ctrl-c"); // Dismiss completer. TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // On completion, place the cursor after the new command. BeginTest(""); clearCommandHistory(); TestPressKey(":.,.+6s/fo\\left\\left\\leftet-auto-in\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+6set-auto-indentX/fo")); TestPressKey("\\ctrl-c"); // Dismiss completer. TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // "The current word", for Commands, can contain "-". BeginTest(""); TestPressKey(":set-\\ctrl-p"); verifyCommandBarCompletionVisible(); QVERIFY(emulatedCommandBarTextEdit()->text() != "set-"); QVERIFY(emulatedCommandBarCompleter()->currentCompletion().startsWith(QLatin1String("set-"))); QCOMPARE(emulatedCommandBarTextEdit()->text(), emulatedCommandBarCompleter()->currentCompletion()); TestPressKey("\\ctrl-c"); // Dismiss completion. TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); { // Don't switch from word-from-document to command-completion just because we press a key, though! BeginTest("soggy1 soggy2"); TestPressKey(":\\ctrl- s"); TestPressKey("o"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "soggy1" << "soggy2"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest("soggy1 soggy2"); } { // If we're in a place where there is no command completion allowed, don't go hiding the word // completion as we type. BeginTest("soggy1 soggy2"); TestPressKey(":s/s\\ctrl- o"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "soggy1" << "soggy2"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest("soggy1 soggy2"); } { // Don't show command completion before we start typing a command: we want ctrl-p/n // to go through command history instead (we'll test for that second part later). BeginTest("soggy1 soggy2"); TestPressKey(":"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-cvl:"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest("soggy1 soggy2"); } { // Aborting ":" should leave us in normal mode with no selection. BeginTest("foo bar"); TestPressKey("vw:\\ctrl-["); QVERIFY(kate_view->selectionText().isEmpty()); TestPressKey("wdiw"); BeginTest("foo "); } // Command history tests. clearCommandHistory(); QVERIFY(commandHistory().isEmpty()); vi_global->commandHistory()->append("foo"); vi_global->commandHistory()->append("bar"); QCOMPARE(commandHistory(), QStringList() << "foo" << "bar"); clearCommandHistory(); QVERIFY(commandHistory().isEmpty()); // If we add something to the history, remove any earliest occurrences (this is what Vim appears to do) // and append to the end. clearCommandHistory(); vi_global->commandHistory()->append("bar"); vi_global->commandHistory()->append("xyz"); vi_global->commandHistory()->append("foo"); vi_global->commandHistory()->append("xyz"); QCOMPARE(commandHistory(), QStringList() << "bar" << "foo" << "xyz"); // Push out older entries if we have too many command items in the history. clearCommandHistory(); for (int i = 1; i <= HISTORY_SIZE_LIMIT; i++) { vi_global->commandHistory()->append(QString("commandhistoryitem %1").arg(i)); } QCOMPARE(commandHistory().size(), HISTORY_SIZE_LIMIT); QCOMPARE(commandHistory().first(), QString("commandhistoryitem 1")); QCOMPARE(commandHistory().last(), QString("commandhistoryitem 100")); vi_global->commandHistory()->append(QString("commandhistoryitem %1").arg(HISTORY_SIZE_LIMIT + 1)); QCOMPARE(commandHistory().size(), HISTORY_SIZE_LIMIT); QCOMPARE(commandHistory().first(), QString("commandhistoryitem 2")); QCOMPARE(commandHistory().last(), QString("commandhistoryitem %1").arg(HISTORY_SIZE_LIMIT + 1)); // Don't add empty commands to the history. clearCommandHistory(); DoTest("foo bar", ":\\enter", "foo bar"); QVERIFY(commandHistory().isEmpty()); clearCommandHistory(); BeginTest(""); TestPressKey(":sort\\enter"); QCOMPARE(commandHistory(), QStringList() << "sort"); TestPressKey(":yank\\enter"); QCOMPARE(commandHistory(), QStringList() << "sort" << "yank"); // Add to history immediately: don't wait for the command response display to timeout. TestPressKey(":commandthatdoesnotexist\\enter"); QCOMPARE(commandHistory(), QStringList() << "sort" << "yank" << "commandthatdoesnotexist"); // Vim adds aborted commands to the history too, oddly. TestPressKey(":abortedcommand\\ctrl-c"); QCOMPARE(commandHistory(), QStringList() << "sort" << "yank" << "commandthatdoesnotexist" << "abortedcommand"); // Only add for commands, not searches! TestPressKey("/donotaddme\\enter?donotaddmeeither\\enter/donotaddme\\ctrl-c?donotaddmeeither\\ctrl-c"); QCOMPARE(commandHistory(), QStringList() << "sort" << "yank" << "commandthatdoesnotexist" << "abortedcommand"); FinishTest(""); // Commands should not be added to the search history! clearCommandHistory(); clearSearchHistory(); BeginTest(""); TestPressKey(":sort\\enter"); QVERIFY(searchHistory().isEmpty()); FinishTest(""); // With an empty command bar, ctrl-p / ctrl-n should go through history. clearCommandHistory(); vi_global->commandHistory()->append("command1"); vi_global->commandHistory()->append("command2"); BeginTest(""); TestPressKey(":\\ctrl-p"); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("command2")); QCOMPARE(emulatedCommandBarTextEdit()->text(), emulatedCommandBarCompleter()->currentCompletion()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); clearCommandHistory(); vi_global->commandHistory()->append("command1"); vi_global->commandHistory()->append("command2"); BeginTest(""); TestPressKey(":\\ctrl-n"); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("command1")); QCOMPARE(emulatedCommandBarTextEdit()->text(), emulatedCommandBarCompleter()->currentCompletion()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // If we're at a place where command completions are not allowed, ctrl-p/n should go through history. clearCommandHistory(); vi_global->commandHistory()->append("s/command1"); vi_global->commandHistory()->append("s/command2"); BeginTest(""); TestPressKey(":s/\\ctrl-p"); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("s/command2")); QCOMPARE(emulatedCommandBarTextEdit()->text(), emulatedCommandBarCompleter()->currentCompletion()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); clearCommandHistory(); vi_global->commandHistory()->append("s/command1"); vi_global->commandHistory()->append("s/command2"); BeginTest(""); TestPressKey(":s/\\ctrl-n"); verifyCommandBarCompletionVisible(); QCOMPARE(emulatedCommandBarCompleter()->currentCompletion(), QString("s/command1")); QCOMPARE(emulatedCommandBarTextEdit()->text(), emulatedCommandBarCompleter()->currentCompletion()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest(""); // Cancelling word-from-document completion should revert the whole text to what it was before. BeginTest("sausage bacon"); TestPressKey(":s/b\\ctrl- \\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/bacon")); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/b")); TestPressKey("\\ctrl-c"); // Dismiss bar FinishTest("sausage bacon"); // "Replace" history tests. clearReplaceHistory(); QVERIFY(replaceHistory().isEmpty()); vi_global->replaceHistory()->append("foo"); vi_global->replaceHistory()->append("bar"); QCOMPARE(replaceHistory(), QStringList() << "foo" << "bar"); clearReplaceHistory(); QVERIFY(replaceHistory().isEmpty()); // If we add something to the history, remove any earliest occurrences (this is what Vim appears to do) // and append to the end. clearReplaceHistory(); vi_global->replaceHistory()->append("bar"); vi_global->replaceHistory()->append("xyz"); vi_global->replaceHistory()->append("foo"); vi_global->replaceHistory()->append("xyz"); QCOMPARE(replaceHistory(), QStringList() << "bar" << "foo" << "xyz"); // Push out older entries if we have too many replace items in the history. clearReplaceHistory(); for (int i = 1; i <= HISTORY_SIZE_LIMIT; i++) { vi_global->replaceHistory()->append(QString("replacehistoryitem %1").arg(i)); } QCOMPARE(replaceHistory().size(), HISTORY_SIZE_LIMIT); QCOMPARE(replaceHistory().first(), QString("replacehistoryitem 1")); QCOMPARE(replaceHistory().last(), QString("replacehistoryitem 100")); vi_global->replaceHistory()->append(QString("replacehistoryitem %1").arg(HISTORY_SIZE_LIMIT + 1)); QCOMPARE(replaceHistory().size(), HISTORY_SIZE_LIMIT); QCOMPARE(replaceHistory().first(), QString("replacehistoryitem 2")); QCOMPARE(replaceHistory().last(), QString("replacehistoryitem %1").arg(HISTORY_SIZE_LIMIT + 1)); // Don't add empty replaces to the history. clearReplaceHistory(); vi_global->replaceHistory()->append(""); QVERIFY(replaceHistory().isEmpty()); // Some misc SedReplace tests. DoTest("x\\/y", ":s/\\\\//replace/g\\enter", "x\\replacey"); DoTest("x\\/y", ":s/\\\\\\\\\\\\//replace/g\\enter", "xreplacey"); DoTest("x\\/y", ":s:/:replace:g\\enter", "x\\replacey"); DoTest("foo\nbar\nxyz", ":%delete\\enter", ""); DoTest("foo\nbar\nxyz\nbaz", "jVj:delete\\enter", "foo\nbaz"); DoTest("foo\nbar\nxyz\nbaz", "j2:delete\\enter", "foo\nbaz"); // Test that 0 is accepted as a line index (and treated as 1) in a range specifier DoTest("bar\nbar\nbar", ":0,$s/bar/foo/g\\enter", "foo\nfoo\nfoo"); DoTest("bar\nbar\nbar", ":1,$s/bar/foo/g\\enter", "foo\nfoo\nfoo"); DoTest("bar\nbar\nbar", ":0,2s/bar/foo/g\\enter", "foo\nfoo\nbar"); // On ctrl-d, delete the "search" term in a s/search/replace/xx BeginTest("foo bar"); TestPressKey(":s/x\\\\\\\\\\\\/yz/rep\\\\\\\\\\\\/lace/g\\ctrl-d"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s//rep\\\\\\/lace/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Move cursor to position of deleted search term. BeginTest("foo bar"); TestPressKey(":s/x\\\\\\\\\\\\/yz/rep\\\\\\\\\\\\/lace/g\\ctrl-dX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/X/rep\\\\\\/lace/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Do nothing on ctrl-d in search mode. BeginTest("foo bar"); TestPressKey("/s/search/replace/g\\ctrl-d"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/replace/g")); TestPressKey("\\ctrl-c?s/searchbackwards/replace/g\\ctrl-d"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/searchbackwards/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // On ctrl-f, delete "replace" term in a s/search/replace/xx BeginTest("foo bar"); TestPressKey(":s/a\\\\\\\\\\\\/bc/rep\\\\\\\\\\\\/lace/g\\ctrl-f"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/a\\\\\\/bc//g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Move cursor to position of deleted replace term. BeginTest("foo bar"); TestPressKey(":s:a/bc:replace:g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:a/bc:X:g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Do nothing on ctrl-d in search mode. BeginTest("foo bar"); TestPressKey("/s/search/replace/g\\ctrl-f"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/replace/g")); TestPressKey("\\ctrl-c?s/searchbackwards/replace/g\\ctrl-f"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/searchbackwards/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Do nothing on ctrl-d / ctrl-f if the current expression is not a sed expression. BeginTest("foo bar"); TestPressKey(":s/notasedreplaceexpression::gi\\ctrl-f\\ctrl-dX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/notasedreplaceexpression::giX")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Need to convert Vim-style regex's to Qt one's in Sed Replace. DoTest("foo xbacba(boo)|[y", ":s/x[abc]\\\\+(boo)|[y/boo/g\\enter", "foo boo"); DoTest("foo xbacba(boo)|[y\nfoo xbacba(boo)|[y", "Vj:s/x[abc]\\\\+(boo)|[y/boo/g\\enter", "foo boo\nfoo boo"); // Just convert the search term, please :) DoTest("foo xbacba(boo)|[y", ":s/x[abc]\\\\+(boo)|[y/boo()/g\\enter", "foo boo()"); // With an empty search expression, ctrl-d should still position the cursor correctly. BeginTest("foo bar"); TestPressKey(":s//replace/g\\ctrl-dX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/X/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":s::replace:g\\ctrl-dX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:X:replace:g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // With an empty replace expression, ctrl-f should still position the cursor correctly. BeginTest("foo bar"); TestPressKey(":s/sear\\\\/ch//g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/sear\\/ch/X/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":s:sear\\\\:ch::g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:sear\\:ch:X:g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // With both empty search *and* replace expressions, ctrl-f should still position the cursor correctly. BeginTest("foo bar"); TestPressKey(":s///g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s//X/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":s:::g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s::X:g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Should be able to undo ctrl-f or ctrl-d. BeginTest("foo bar"); TestPressKey(":s/find/replace/g\\ctrl-d"); emulatedCommandBarTextEdit()->undo(); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/find/replace/g")); TestPressKey("\\ctrl-f"); emulatedCommandBarTextEdit()->undo(); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/find/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // ctrl-f / ctrl-d should cleanly finish sed find/ replace history completion. clearReplaceHistory(); clearSearchHistory(); vi_global->searchHistory()->append("searchxyz"); vi_global->replaceHistory()->append("replacexyz"); TestPressKey(":s///g\\ctrl-d\\ctrl-p"); QVERIFY(emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-f"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/searchxyz//g")); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-p"); QVERIFY(emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-d"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s//replacexyz/g")); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Don't hang if we execute a sed replace with empty search term. DoTest("foo bar", ":s//replace/g\\enter", "foo bar"); // ctrl-f & ctrl-d should work even when there is a range expression at the beginning of the sed replace. BeginTest("foo bar"); TestPressKey(":'<,'>s/search/replace/g\\ctrl-d"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("'<,'>s//replace/g")); TestPressKey("\\ctrl-c:.,.+6s/search/replace/g\\ctrl-f"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+6s/search//g")); TestPressKey("\\ctrl-c:%s/search/replace/g\\ctrl-f"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("%s/search//g")); // Place the cursor in the right place even when there is a range expression. TestPressKey("\\ctrl-c:.,.+6s/search/replace/g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+6s/search/X/g")); TestPressKey("\\ctrl-c:%s/search/replace/g\\ctrl-fX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("%s/search/X/g")); TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest("foo bar"); // Don't crash on ctrl-f/d if we have an empty command. DoTest("", ":\\ctrl-f\\ctrl-d\\ctrl-c", ""); // Parser regression test: Don't crash on ctrl-f/d with ".,.+". DoTest("", ":.,.+\\ctrl-f\\ctrl-d\\ctrl-c", ""); // Command-completion should be invoked on the command being typed even when preceded by a range expression. BeginTest(""); TestPressKey(":0,'>so"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Command-completion should ignore the range expression. BeginTest(""); TestPressKey(":.,.+6so"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // A sed-replace should immediately add the search term to the search history. clearSearchHistory(); BeginTest(""); TestPressKey(":s/search/replace/g\\enter"); QCOMPARE(searchHistory(), QStringList() << "search"); FinishTest(""); // An aborted sed-replace should not add the search term to the search history. clearSearchHistory(); BeginTest(""); TestPressKey(":s/search/replace/g\\ctrl-c"); QCOMPARE(searchHistory(), QStringList()); FinishTest(""); // A non-sed-replace should leave the search history unchanged. clearSearchHistory(); BeginTest(""); TestPressKey(":s,search/replace/g\\enter"); QCOMPARE(searchHistory(), QStringList()); FinishTest(""); // A sed-replace should immediately add the replace term to the replace history. clearReplaceHistory(); BeginTest(""); TestPressKey(":s/search/replace/g\\enter"); QCOMPARE(replaceHistory(), QStringList() << "replace"); clearReplaceHistory(); TestPressKey(":'<,'>s/search/replace1/g\\enter"); QCOMPARE(replaceHistory(), QStringList() << "replace1"); FinishTest(""); // An aborted sed-replace should not add the replace term to the replace history. clearReplaceHistory(); BeginTest(""); TestPressKey(":s/search/replace/g\\ctrl-c"); QCOMPARE(replaceHistory(), QStringList()); FinishTest(""); // A non-sed-replace should leave the replace history unchanged. clearReplaceHistory(); BeginTest(""); TestPressKey(":s,search/replace/g\\enter"); QCOMPARE(replaceHistory(), QStringList()); FinishTest(""); // Misc tests for sed replace. These are for the *generic* Kate sed replace; they should all // use EmulatedCommandBarTests' built-in command execution stuff (\\:\\\) rather than // invoking a EmulatedCommandBar and potentially doing some Vim-specific transforms to // the command. DoTest("foo foo foo", "\\:s/foo/bar/\\", "bar foo foo"); DoTest("foo foo xyz foo", "\\:s/foo/bar/g\\", "bar bar xyz bar"); DoTest("foofooxyzfoo", "\\:s/foo/bar/g\\", "barbarxyzbar"); DoTest("foofooxyzfoo", "\\:s/foo/b/g\\", "bbxyzb"); DoTest("ffxyzf", "\\:s/f/b/g\\", "bbxyzb"); DoTest("ffxyzf", "\\:s/f/bar/g\\", "barbarxyzbar"); DoTest("foo Foo fOO FOO foo", "\\:s/foo/bar/\\", "bar Foo fOO FOO foo"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/bar/\\", "Foo bar fOO FOO foo"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/bar/g\\", "Foo bar fOO FOO bar"); DoTest("foo Foo fOO FOO foo", "\\:s/foo/bar/i\\", "bar Foo fOO FOO foo"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/bar/i\\", "bar foo fOO FOO foo"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/bar/gi\\", "bar bar bar bar bar"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/bar/ig\\", "bar bar bar bar bar"); // There are some oddities to do with how EmulatedCommandBarTest's "execute command directly" stuff works with selected ranges: // basically, we need to do our selection in Visual mode, then exit back to Normal mode before running the //command. DoTest("foo foo\nbar foo foo\nxyz foo foo\nfoo bar foo", "jVj\\esc\\:'<,'>s/foo/bar/\\", "foo foo\nbar bar foo\nxyz bar foo\nfoo bar foo"); DoTest("foo foo\nbar foo foo\nxyz foo foo\nfoo bar foo", "jVj\\esc\\:'<,'>s/foo/bar/g\\", "foo foo\nbar bar bar\nxyz bar bar\nfoo bar foo"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/barfoo/g\\", "Foo barfoo fOO FOO barfoo"); DoTest("Foo foo fOO FOO foo", "\\:s/foo/foobar/g\\", "Foo foobar fOO FOO foobar"); DoTest("axyzb", "\\:s/a(.*)b/d\\\\1f/\\", "dxyzf"); DoTest("ayxzzyxzfddeefdb", "\\:s/a([xyz]+)([def]+)b/<\\\\1|\\\\2>/\\", ""); DoTest("foo", "\\:s/.*//g\\", ""); DoTest("foo", "\\:s/.*/f/g\\", "f"); DoTest("foo/bar", "\\:s/foo\\\\/bar/123\\\\/xyz/g\\", "123/xyz"); DoTest("foo:bar", "\\:s:foo\\\\:bar:123\\\\:xyz:g\\", "123:xyz"); const bool oldReplaceTabsDyn = kate_document->config()->replaceTabsDyn(); kate_document->config()->setReplaceTabsDyn(false); DoTest("foo\tbar", "\\:s/foo\\\\tbar/replace/g\\", "replace"); DoTest("foo\tbar", "\\:s/foo\\\\tbar/rep\\\\tlace/g\\", "rep\tlace"); kate_document->config()->setReplaceTabsDyn(oldReplaceTabsDyn); DoTest("foo", "\\:s/foo/replaceline1\\\\nreplaceline2/g\\", "replaceline1\nreplaceline2"); DoTest("foofoo", "\\:s/foo/replaceline1\\\\nreplaceline2/g\\", "replaceline1\nreplaceline2replaceline1\nreplaceline2"); DoTest("foofoo\nfoo", "\\:s/foo/replaceline1\\\\nreplaceline2/g\\", "replaceline1\nreplaceline2replaceline1\nreplaceline2\nfoo"); DoTest("fooafoob\nfooc\nfood", "Vj\\esc\\:'<,'>s/foo/replaceline1\\\\nreplaceline2/g\\", "replaceline1\nreplaceline2areplaceline1\nreplaceline2b\nreplaceline1\nreplaceline2c\nfood"); DoTest("fooafoob\nfooc\nfood", "Vj\\esc\\:'<,'>s/foo/replaceline1\\\\nreplaceline2/\\", "replaceline1\nreplaceline2afoob\nreplaceline1\nreplaceline2c\nfood"); DoTest("fooafoob\nfooc\nfood", "Vj\\esc\\:'<,'>s/foo/replaceline1\\\\nreplaceline2\\\\nreplaceline3/g\\", "replaceline1\nreplaceline2\nreplaceline3areplaceline1\nreplaceline2\nreplaceline3b\nreplaceline1\nreplaceline2\nreplaceline3c\nfood"); DoTest("foofoo", "\\:s/foo/replace\\\\nfoo/g\\", "replace\nfooreplace\nfoo"); DoTest("foofoo", "\\:s/foo/replacefoo\\\\nfoo/g\\", "replacefoo\nfooreplacefoo\nfoo"); DoTest("foofoo", "\\:s/foo/replacefoo\\\\n/g\\", "replacefoo\nreplacefoo\n"); DoTest("ff", "\\:s/f/f\\\\nf/g\\", "f\nff\nf"); DoTest("ff", "\\:s/f/f\\\\n/g\\", "f\nf\n"); DoTest("foo\nbar", "\\:s/foo\\\\n//g\\", "bar"); DoTest("foo\n\n\nbar", "\\:s/foo(\\\\n)*bar//g\\", ""); DoTest("foo\n\n\nbar", "\\:s/foo(\\\\n*)bar/123\\\\1456/g\\", "123\n\n\n456"); DoTest("xAbCy", "\\:s/x(.)(.)(.)y/\\\\L\\\\1\\\\U\\\\2\\\\3/g\\", "aBC"); DoTest("foo", "\\:s/foo/\\\\a/g\\", "\x07"); // End "generic" (i.e. not involving any Vi mode tricks/ transformations) sed replace tests: the remaining // ones should go via the EmulatedCommandBar. BeginTest("foo foo\nxyz\nfoo"); TestPressKey(":%s/foo/bar/g\\enter"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(3, 2); FinishTest("bar bar\nxyz\nbar"); // ctrl-p on the first character of the search term in a sed-replace should // invoke search history completion. clearSearchHistory(); vi_global->searchHistory()->append("search"); BeginTest(""); TestPressKey(":s/search/replace/g\\ctrl-b\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s/search/replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on the last character of the search term in a sed-replace should // invoke search history completion. clearSearchHistory(); vi_global->searchHistory()->append("xyz"); BeginTest(""); TestPressKey(":s/xyz/replace/g\\ctrl-b\\right\\right\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. QVERIFY(!emulatedCommandBar->isVisible()); TestPressKey(":'<,'>s/xyz/replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\right\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on some arbitrary character of the search term in a sed-replace should // invoke search history completion. clearSearchHistory(); vi_global->searchHistory()->append("xyzaaaaaa"); BeginTest(""); TestPressKey(":s/xyzaaaaaa/replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s/xyzaaaaaa/replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\right\\right\\right\\right\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on some character *after" the search term should // *not* invoke search history completion. // Note: in s/xyz/replace/g, the "/" after the "z" is counted as part of the find term; // this allows us to do xyz and get completions. clearSearchHistory(); clearCommandHistory(); clearReplaceHistory(); vi_global->searchHistory()->append("xyz"); BeginTest(""); TestPressKey(":s/xyz/replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\ctrl-p"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. clearSearchHistory(); clearCommandHistory(); TestPressKey(":'<,'>s/xyz/replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\right\\right\\right\\right\\right\\right\\ctrl-p"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. // Make sure it's the search history we're invoking. clearSearchHistory(); vi_global->searchHistory()->append("xyzaaaaaa"); BeginTest(""); TestPressKey(":s//replace/g\\ctrl-b\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "xyzaaaaaa"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":.,.+6s//replace/g\\ctrl-b\\right\\right\\right\\right\\right\\right\\right\\ctrl-p"); verifyCommandBarCompletionsMatches(QStringList() << "xyzaaaaaa"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // (Search history should be reversed). clearSearchHistory(); vi_global->searchHistory()->append("xyzaaaaaa"); vi_global->searchHistory()->append("abc"); vi_global->searchHistory()->append("def"); BeginTest(""); TestPressKey(":s//replace/g\\ctrl-b\\right\\right\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "def" << "abc" << "xyzaaaaaa"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Completion prefix is the current find term. clearSearchHistory(); vi_global->searchHistory()->append("xy:zaaaaaa"); vi_global->searchHistory()->append("abc"); vi_global->searchHistory()->append("def"); vi_global->searchHistory()->append("xy:zbaaaaa"); vi_global->searchHistory()->append("xy:zcaaaaa"); BeginTest(""); TestPressKey(":s//replace/g\\ctrl-dxy:z\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "xy:zcaaaaa" << "xy:zbaaaaa" << "xy:zaaaaaa"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Replace entire search term with completion. clearSearchHistory(); vi_global->searchHistory()->append("ab,cd"); vi_global->searchHistory()->append("ab,xy"); BeginTest(""); TestPressKey(":s//replace/g\\ctrl-dab,\\ctrl-p\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/ab,cd/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s//replace/g\\ctrl-dab,\\ctrl-p\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("'<,'>s/ab,cd/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Place the cursor at the end of find term. clearSearchHistory(); vi_global->searchHistory()->append("ab,xy"); BeginTest(""); TestPressKey(":s//replace/g\\ctrl-dab,\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/ab,xyX/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":.,.+7s//replace/g\\ctrl-dab,\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+7s/ab,xyX/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Leave find term unchanged if there is no search history. clearSearchHistory(); BeginTest(""); TestPressKey(":s/nose/replace/g\\ctrl-b\\right\\right\\right\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/nose/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Leave cursor position unchanged if there is no search history. clearSearchHistory(); BeginTest(""); TestPressKey(":s/nose/replace/g\\ctrl-b\\right\\right\\right\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/nXose/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on the first character of the replace term in a sed-replace should // invoke replace history completion. clearSearchHistory(); clearReplaceHistory(); clearCommandHistory(); vi_global->replaceHistory()->append("replace"); BeginTest(""); TestPressKey(":s/search/replace/g\\left\\left\\left\\left\\left\\left\\left\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s/search/replace/g\\left\\left\\left\\left\\left\\left\\left\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on the last character of the replace term in a sed-replace should // invoke replace history completion. clearReplaceHistory(); vi_global->replaceHistory()->append("replace"); BeginTest(""); TestPressKey(":s/xyz/replace/g\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s/xyz/replace/g\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on some arbitrary character of the search term in a sed-replace should // invoke search history completion. clearReplaceHistory(); vi_global->replaceHistory()->append("replaceaaaaaa"); BeginTest(""); TestPressKey(":s/xyzaaaaaa/replace/g\\left\\left\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s/xyzaaaaaa/replace/g\\left\\left\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // ctrl-p on some character *after" the replace term should // *not* invoke replace history completion. // Note: in s/xyz/replace/g, the "/" after the "e" is counted as part of the replace term; // this allows us to do replace and get completions. clearSearchHistory(); clearCommandHistory(); clearReplaceHistory(); vi_global->replaceHistory()->append("xyz"); BeginTest(""); TestPressKey(":s/xyz/replace/g\\left\\ctrl-p"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. clearSearchHistory(); clearCommandHistory(); TestPressKey(":'<,'>s/xyz/replace/g\\left\\ctrl-p"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. // (Replace history should be reversed). clearReplaceHistory(); vi_global->replaceHistory()->append("xyzaaaaaa"); vi_global->replaceHistory()->append("abc"); vi_global->replaceHistory()->append("def"); BeginTest(""); TestPressKey(":s/search//g\\left\\left\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "def" << "abc" << "xyzaaaaaa"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Completion prefix is the current replace term. clearReplaceHistory(); vi_global->replaceHistory()->append("xy:zaaaaaa"); vi_global->replaceHistory()->append("abc"); vi_global->replaceHistory()->append("def"); vi_global->replaceHistory()->append("xy:zbaaaaa"); vi_global->replaceHistory()->append("xy:zcaaaaa"); BeginTest(""); TestPressKey(":'<,'>s/replace/search/g\\ctrl-fxy:z\\ctrl-p"); verifyCommandBarCompletionVisible(); verifyCommandBarCompletionsMatches(QStringList() << "xy:zcaaaaa" << "xy:zbaaaaa" << "xy:zaaaaaa"); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Replace entire search term with completion. clearReplaceHistory(); clearSearchHistory(); vi_global->replaceHistory()->append("ab,cd"); vi_global->replaceHistory()->append("ab,xy"); BeginTest(""); TestPressKey(":s/search//g\\ctrl-fab,\\ctrl-p\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/ab,cd/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":'<,'>s/search//g\\ctrl-fab,\\ctrl-p\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("'<,'>s/search/ab,cd/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Place the cursor at the end of replace term. clearReplaceHistory(); vi_global->replaceHistory()->append("ab,xy"); BeginTest(""); TestPressKey(":s/search//g\\ctrl-fab,\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/ab,xyX/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. TestPressKey(":.,.+7s/search//g\\ctrl-fab,\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString(".,.+7s/search/ab,xyX/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Leave replace term unchanged if there is no replace history. clearReplaceHistory(); BeginTest(""); TestPressKey(":s/nose/replace/g\\left\\left\\left\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/nose/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Leave cursor position unchanged if there is no replace history. clearSearchHistory(); BeginTest(""); TestPressKey(":s/nose/replace/g\\left\\left\\left\\left\\ctrl-pX"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/nose/replaXce/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Invoke replacement history even when the "find" term is empty. BeginTest(""); clearReplaceHistory(); clearSearchHistory(); vi_global->replaceHistory()->append("ab,xy"); vi_global->searchHistory()->append("whoops"); TestPressKey(":s///g\\ctrl-f\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s//ab,xy/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Move the cursor back to the last manual edit point when aborting completion. BeginTest(""); clearSearchHistory(); vi_global->searchHistory()->append("xyzaaaaa"); TestPressKey(":s/xyz/replace/g\\ctrl-b\\right\\right\\right\\right\\righta\\ctrl-p\\ctrl-[X"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/xyzaX/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Don't blank the "find" term if there is no search history that begins with the // current "find" term. BeginTest(""); clearSearchHistory(); vi_global->searchHistory()->append("doesnothavexyzasaprefix"); TestPressKey(":s//replace/g\\ctrl-dxyz\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/xyz/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Escape the delimiter if it occurs in a search history term - searching for it likely won't // work, but at least it won't crash! BeginTest(""); clearSearchHistory(); vi_global->searchHistory()->append("search"); vi_global->searchHistory()->append("aa/aa\\/a"); vi_global->searchHistory()->append("ss/ss"); TestPressKey(":s//replace/g\\ctrl-d\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/ss\\/ss/replace/g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/aa\\/aa\\/a/replace/g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. clearSearchHistory(); // Now do the same, but with a different delimiter. vi_global->searchHistory()->append("search"); vi_global->searchHistory()->append("aa:aa\\:a"); vi_global->searchHistory()->append("ss:ss"); TestPressKey(":s::replace:g\\ctrl-d\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:ss\\:ss:replace:g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:aa\\:aa\\:a:replace:g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:search:replace:g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Remove \C if occurs in search history. BeginTest(""); clearSearchHistory(); vi_global->searchHistory()->append("s\\Cear\\\\Cch"); TestPressKey(":s::replace:g\\ctrl-d\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:sear\\\\Cch:replace:g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Don't blank the "replace" term if there is no search history that begins with the // current "replace" term. BeginTest(""); clearReplaceHistory(); vi_global->replaceHistory()->append("doesnothavexyzasaprefix"); TestPressKey(":s/search//g\\ctrl-fxyz\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/xyz/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Escape the delimiter if it occurs in a replace history term - searching for it likely won't // work, but at least it won't crash! BeginTest(""); clearReplaceHistory(); vi_global->replaceHistory()->append("replace"); vi_global->replaceHistory()->append("aa/aa\\/a"); vi_global->replaceHistory()->append("ss/ss"); TestPressKey(":s/search//g\\ctrl-f\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/ss\\/ss/g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/aa\\/aa\\/a/g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/search/replace/g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. clearReplaceHistory(); // Now do the same, but with a different delimiter. vi_global->replaceHistory()->append("replace"); vi_global->replaceHistory()->append("aa:aa\\:a"); vi_global->replaceHistory()->append("ss:ss"); TestPressKey(":s:search::g\\ctrl-f\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:search:ss\\:ss:g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:search:aa\\:aa\\:a:g")); TestPressKey("\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s:search:replace:g")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // In search mode, don't blank current text on completion if there is no item in the search history which // has the current text as a prefix. BeginTest(""); clearSearchHistory(); vi_global->searchHistory()->append("doesnothavexyzasaprefix"); TestPressKey("/xyz\\ctrl-p"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("xyz")); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Don't dismiss the command completion just because the cursor ends up *temporarily* at a place where // command completion is disallowed when cycling through completions. BeginTest(""); TestPressKey(":set/se\\left\\left\\left-\\ctrl-p"); verifyCommandBarCompletionVisible(); TestPressKey("\\ctrl-c"); // Dismiss completer TestPressKey("\\ctrl-c"); // Dismiss bar. FinishTest(""); // Don't expand mappings meant for Normal mode in the emulated command bar. clearAllMappings(); vi_global->mappings()->add(Mappings::NormalModeMapping, "foo", "xyz", Mappings::NonRecursive); DoTest("bar foo xyz", "/foo\\enterrX", "bar Xoo xyz"); clearAllMappings(); // Incremental search and replace. QLabel* interactiveSedReplaceLabel = emulatedCommandBar->findChild("interactivesedreplace"); QVERIFY(interactiveSedReplaceLabel); BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); QVERIFY(interactiveSedReplaceLabel->isVisible()); QVERIFY(!commandResponseMessageDisplay()->isVisible()); QVERIFY(!emulatedCommandBarTextEdit()->isVisible()); QVERIFY(!emulatedCommandTypeIndicator()->isVisible()); TestPressKey("\\ctrl-c"); // Dismiss search and replace. QVERIFY(!emulatedCommandBar->isVisible()); FinishTest("foo"); // Clear the flag that stops the command response from being shown after an incremental search and // replace, and also make sure that the edit and bar type indicator are not forcibly hidden. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter\\ctrl-c"); TestPressKey(":s/foo/bar/"); QVERIFY(emulatedCommandBarTextEdit()->isVisible()); QVERIFY(emulatedCommandTypeIndicator()->isVisible()); TestPressKey("\\enter"); QVERIFY(commandResponseMessageDisplay()->isVisible()); FinishTest("bar"); // Hide the incremental search and replace label when we show the bar. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter\\ctrl-c"); TestPressKey(":"); QVERIFY(!interactiveSedReplaceLabel->isVisible()); TestPressKey("\\ctrl-c"); FinishTest("foo"); // The "c" marker can be anywhere in the three chars following the delimiter. BeginTest("foo"); TestPressKey(":s/foo/bar/cgi\\enter"); QVERIFY(interactiveSedReplaceLabel->isVisible()); TestPressKey("\\ctrl-c"); FinishTest("foo"); BeginTest("foo"); TestPressKey(":s/foo/bar/igc\\enter"); QVERIFY(interactiveSedReplaceLabel->isVisible()); TestPressKey("\\ctrl-c"); FinishTest("foo"); BeginTest("foo"); TestPressKey(":s/foo/bar/icg\\enter"); QVERIFY(interactiveSedReplaceLabel->isVisible()); TestPressKey("\\ctrl-c"); FinishTest("foo"); BeginTest("foo"); TestPressKey(":s/foo/bar/ic\\enter"); QVERIFY(interactiveSedReplaceLabel->isVisible()); TestPressKey("\\ctrl-c"); FinishTest("foo"); BeginTest("foo"); TestPressKey(":s/foo/bar/ci\\enter"); QVERIFY(interactiveSedReplaceLabel->isVisible()); TestPressKey("\\ctrl-c"); FinishTest("foo"); // Emulated command bar is still active during an incremental search and replace. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("idef\\esc"); FinishTest("foo"); // Emulated command bar text is not edited during an incremental search and replace. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("def"); QCOMPARE(emulatedCommandBarTextEdit()->text(), QString("s/foo/bar/c")); TestPressKey("\\ctrl-c"); FinishTest("foo"); // Pressing "n" when there is only a single change we can make aborts incremental search // and replace. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("n"); QVERIFY(!interactiveSedReplaceLabel->isVisible()); TestPressKey("ixyz\\esc"); FinishTest("xyzfoo"); // Pressing "n" when there is only a single change we can make aborts incremental search // and replace, and shows the no replacements on no lines. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("n"); QVERIFY(commandResponseMessageDisplay()->isVisible()); verifyShowsNumberOfReplacementsAcrossNumberOfLines(0, 0); FinishTest("foo"); // First possible match is highlighted when we start an incremental search and replace, and // cleared if we press 'n'. BeginTest(" xyz 123 foo bar"); TestPressKey(":s/foo/bar/gc\\enter"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 10); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 13); TestPressKey("n"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); FinishTest(" xyz 123 foo bar"); // Second possible match highlighted if we start incremental search and replace and press 'n', // cleared if we press 'n' again. BeginTest(" xyz 123 foo foo bar"); TestPressKey(":s/foo/bar/gc\\enter"); TestPressKey("n"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 14); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 17); TestPressKey("n"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size()); FinishTest(" xyz 123 foo foo bar"); // Perform replacement if we press 'y' on the first match. BeginTest(" xyz foo 123 foo bar"); TestPressKey(":s/foo/bar/gc\\enter"); TestPressKey("y"); TestPressKey("\\ctrl-c"); FinishTest(" xyz bar 123 foo bar"); // Replacement uses grouping, etc. BeginTest(" xyz def 123 foo bar"); TestPressKey(":s/d\\\\(e\\\\)\\\\(f\\\\)/x\\\\1\\\\U\\\\2/gc\\enter"); TestPressKey("y"); TestPressKey("\\ctrl-c"); FinishTest(" xyz xeF 123 foo bar"); // On replacement, highlight next match. BeginTest(" xyz foo 123 foo bar"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("y"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 14); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 17); TestPressKey("\\ctrl-c"); FinishTest(" xyz bar 123 foo bar"); // On replacement, if there is no further match, abort incremental search and replace. BeginTest(" xyz foo 123 foa bar"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("y"); QVERIFY(commandResponseMessageDisplay()->isVisible()); TestPressKey("ggidone\\esc"); FinishTest("done xyz bar 123 foa bar"); // After replacement, the next match is sought after the end of the replacement text. BeginTest("foofoo"); TestPressKey(":s/foo/barfoo/cg\\enter"); TestPressKey("y"); QCOMPARE(rangesOnFirstLine().size(), rangesInitial.size() + 1); QCOMPARE(rangesOnFirstLine().first()->start().line(), 0); QCOMPARE(rangesOnFirstLine().first()->start().column(), 6); QCOMPARE(rangesOnFirstLine().first()->end().line(), 0); QCOMPARE(rangesOnFirstLine().first()->end().column(), 9); TestPressKey("\\ctrl-c"); FinishTest("barfoofoo"); BeginTest("xffy"); TestPressKey(":s/f/bf/cg\\enter"); TestPressKey("yy"); FinishTest("xbfbfy"); // Make sure the incremental search bar label contains the "instruction" keypresses. const QString interactiveSedReplaceShortcuts = "(y/n/a/q/l)"; BeginTest("foofoo"); TestPressKey(":s/foo/barfoo/cg\\enter"); QVERIFY(interactiveSedReplaceLabel->text().contains(interactiveSedReplaceShortcuts)); TestPressKey("\\ctrl-c"); FinishTest("foofoo"); // Make sure the incremental search bar label contains a reference to the text we're going to // replace with. // We're going to be a bit vague about the precise text due to localization issues. BeginTest("fabababbbar"); TestPressKey(":s/f\\\\([ab]\\\\+\\\\)/1\\\\U\\\\12/c\\enter"); QVERIFY(interactiveSedReplaceLabel->text().contains("1ABABABBBA2")); TestPressKey("\\ctrl-c"); FinishTest("fabababbbar"); // Replace newlines in the "replace?" message with "\\n" BeginTest("foo"); TestPressKey(":s/foo/bar\\\\nxyz\\\\n123/c\\enter"); QVERIFY(interactiveSedReplaceLabel->text().contains("bar\\nxyz\\n123")); TestPressKey("\\ctrl-c"); FinishTest("foo"); // Update the "confirm replace?" message on pressing "y". BeginTest("fabababbbar fabbb"); TestPressKey(":s/f\\\\([ab]\\\\+\\\\)/1\\\\U\\\\12/gc\\enter"); TestPressKey("y"); QVERIFY(interactiveSedReplaceLabel->text().contains("1ABBB2")); QVERIFY(interactiveSedReplaceLabel->text().contains(interactiveSedReplaceShortcuts)); TestPressKey("\\ctrl-c"); FinishTest("1ABABABBBA2r fabbb"); // Update the "confirm replace?" message on pressing "n". BeginTest("fabababbbar fabab"); TestPressKey(":s/f\\\\([ab]\\\\+\\\\)/1\\\\U\\\\12/gc\\enter"); TestPressKey("n"); QVERIFY(interactiveSedReplaceLabel->text().contains("1ABAB2")); QVERIFY(interactiveSedReplaceLabel->text().contains(interactiveSedReplaceShortcuts)); TestPressKey("\\ctrl-c"); FinishTest("fabababbbar fabab"); // Cursor is placed at the beginning of first match. BeginTest(" foo foo foo"); TestPressKey(":s/foo/bar/c\\enter"); verifyCursorAt(Cursor(0, 2)); TestPressKey("\\ctrl-c"); FinishTest(" foo foo foo"); // "y" and "n" update the cursor pos. BeginTest(" foo foo foo"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("y"); verifyCursorAt(Cursor(0, 8)); TestPressKey("n"); verifyCursorAt(Cursor(0, 12)); TestPressKey("\\ctrl-c"); FinishTest(" bar foo foo"); // If we end due to a "y" or "n" on the final match, leave the cursor at the beginning of the final match. BeginTest(" foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("y"); verifyCursorAt(Cursor(0, 2)); FinishTest(" bar"); BeginTest(" foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("n"); verifyCursorAt(Cursor(0, 2)); FinishTest(" foo"); // Respect ranges. BeginTest("foo foo\nfoo foo\nfoo foo\nfoo foo\n"); TestPressKey("jVj:s/foo/bar/gc\\enter"); TestPressKey("ynny"); QVERIFY(commandResponseMessageDisplay()->isVisible()); TestPressKey("ggidone \\ctrl-c"); FinishTest("done foo foo\nbar foo\nfoo bar\nfoo foo\n"); BeginTest("foo foo\nfoo foo\nfoo foo\nfoo foo\n"); TestPressKey("jVj:s/foo/bar/gc\\enter"); TestPressKey("nyyn"); QVERIFY(commandResponseMessageDisplay()->isVisible()); TestPressKey("ggidone \\ctrl-c"); FinishTest("done foo foo\nfoo bar\nbar foo\nfoo foo\n"); BeginTest("foo foo\nfoo foo\nfoo foo\nfoo foo\n"); TestPressKey("j:s/foo/bar/gc\\enter"); TestPressKey("ny"); QVERIFY(commandResponseMessageDisplay()->isVisible()); TestPressKey("ggidone \\ctrl-c"); FinishTest("done foo foo\nfoo bar\nfoo foo\nfoo foo\n"); BeginTest("foo foo\nfoo foo\nfoo foo\nfoo foo\n"); TestPressKey("j:s/foo/bar/gc\\enter"); TestPressKey("yn"); QVERIFY(commandResponseMessageDisplay()->isVisible()); TestPressKey("ggidone \\ctrl-c"); FinishTest("done foo foo\nbar foo\nfoo foo\nfoo foo\n"); // If no initial match can be found, abort and show a "no replacements" message. // The cursor position should remain unnchanged. BeginTest("fab"); TestPressKey("l:s/fee/bar/c\\enter"); QVERIFY(commandResponseMessageDisplay()->isVisible()); verifyShowsNumberOfReplacementsAcrossNumberOfLines(0, 0); QVERIFY(!interactiveSedReplaceLabel->isVisible()); TestPressKey("rX"); BeginTest("fXb"); // Case-sensitive by default. BeginTest("foo Foo FOo foo foO"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar Foo FOo bar foO"); // Case-insensitive if "i" flag is used. BeginTest("foo Foo FOo foo foO"); TestPressKey(":s/foo/bar/icg\\enter"); TestPressKey("yyyyyggidone\\esc"); FinishTest("donebar bar bar bar bar"); // Only one replacement per-line unless "g" flag is used. BeginTest("boo foo 123 foo\nxyz foo foo\nfoo foo foo\nxyz\nfoo foo\nfoo 123 foo"); TestPressKey("jVjjj:s/foo/bar/c\\enter"); TestPressKey("yynggidone\\esc"); FinishTest("doneboo foo 123 foo\nxyz bar foo\nbar foo foo\nxyz\nfoo foo\nfoo 123 foo"); BeginTest("boo foo 123 foo\nxyz foo foo\nfoo foo foo\nxyz\nfoo foo\nfoo 123 foo"); TestPressKey("jVjjj:s/foo/bar/c\\enter"); TestPressKey("nnyggidone\\esc"); FinishTest("doneboo foo 123 foo\nxyz foo foo\nfoo foo foo\nxyz\nbar foo\nfoo 123 foo"); // If replacement contains new lines, adjust the end line down. BeginTest("foo\nfoo1\nfoo2\nfoo3"); TestPressKey("jVj:s/foo/bar\\\\n/gc\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donefoo\nbar\n1\nbar\n2\nfoo3"); BeginTest("foo\nfoo1\nfoo2\nfoo3"); TestPressKey("jVj:s/foo/bar\\\\nboo\\\\n/gc\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donefoo\nbar\nboo\n1\nbar\nboo\n2\nfoo3"); // With "g" and a replacement that involves multiple lines, resume search from the end of the last line added. BeginTest("foofoo"); TestPressKey(":s/foo/bar\\\\n/gc\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar\nbar\n"); BeginTest("foofoo"); TestPressKey(":s/foo/bar\\\\nxyz\\\\nfoo/gc\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar\nxyz\nfoobar\nxyz\nfoo"); // Without "g" and with a replacement that involves multiple lines, resume search from the line after the line just added. BeginTest("foofoo1\nfoo2\nfoo3"); TestPressKey("Vj:s/foo/bar\\\\nxyz\\\\nfoo/c\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar\nxyz\nfoofoo1\nbar\nxyz\nfoo2\nfoo3"); // Regression test: handle 'g' when it occurs before 'i' and 'c'. BeginTest("foo fOo"); TestPressKey(":s/foo/bar/gci\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar bar"); // When the search terms swallows several lines, move the endline up accordingly. BeginTest("foo\nfoo1\nfoo\nfoo2\nfoo\nfoo3"); TestPressKey("V3j:s/foo\\\\nfoo/bar/cg\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar1\nbar2\nfoo\nfoo3"); BeginTest("foo\nfoo\nfoo1\nfoo\nfoo\nfoo2\nfoo\nfoo\nfoo3"); TestPressKey("V5j:s/foo\\\\nfoo\\\\nfoo/bar/cg\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar1\nbar2\nfoo\nfoo\nfoo3"); // Make sure we still adjust endline down if the replacement text has '\n's. BeginTest("foo\nfoo\nfoo1\nfoo\nfoo\nfoo2\nfoo\nfoo\nfoo3"); TestPressKey("V5j:s/foo\\\\nfoo\\\\nfoo/bar\\\\n/cg\\enter"); TestPressKey("yyggidone\\esc"); FinishTest("donebar\n1\nbar\n2\nfoo\nfoo\nfoo3"); // Status reports. BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); TestPressKey("y"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(1, 1); FinishTest("bar"); BeginTest("foo foo foo"); TestPressKey(":s/foo/bar/gc\\enter"); TestPressKey("yyy"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(3, 1); FinishTest("bar bar bar"); BeginTest("foo foo foo"); TestPressKey(":s/foo/bar/gc\\enter"); TestPressKey("yny"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(2, 1); FinishTest("bar foo bar"); BeginTest("foo\nfoo"); TestPressKey(":%s/foo/bar/gc\\enter"); TestPressKey("yy"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(2, 2); FinishTest("bar\nbar"); BeginTest("foo foo\nfoo foo\nfoo foo"); TestPressKey(":%s/foo/bar/gc\\enter"); TestPressKey("yynnyy"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(4, 2); FinishTest("bar bar\nfoo foo\nbar bar"); BeginTest("foofoo"); TestPressKey(":s/foo/bar\\\\nxyz/gc\\enter"); TestPressKey("yy"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(2, 1); FinishTest("bar\nxyzbar\nxyz"); BeginTest("foofoofoo"); TestPressKey(":s/foo/bar\\\\nxyz\\\\nboo/gc\\enter"); TestPressKey("yyy"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(3, 1); FinishTest("bar\nxyz\nboobar\nxyz\nboobar\nxyz\nboo"); // Tricky one: how many lines are "touched" if a single replacement // swallows multiple lines? I'm going to say the number of lines swallowed. BeginTest("foo\nfoo\nfoo"); TestPressKey(":s/foo\\\\nfoo\\\\nfoo/bar/c\\enter"); TestPressKey("y"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(1, 3); FinishTest("bar"); BeginTest("foo\nfoo\nfoo\n"); TestPressKey(":s/foo\\\\nfoo\\\\nfoo\\\\n/bar/c\\enter"); TestPressKey("y"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(1, 4); FinishTest("bar"); // "Undo" undoes last replacement. BeginTest("foo foo foo foo"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("nyynu"); FinishTest("foo bar foo foo"); // "l" does the current replacement then exits. BeginTest("foo foo foo foo foo foo"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("nnl"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(1, 1); FinishTest("foo foo bar foo foo foo"); // "q" just exits. BeginTest("foo foo foo foo foo foo"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("yyq"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(2, 1); FinishTest("bar bar foo foo foo foo"); // "a" replaces all remaining, then exits. BeginTest("foo foo foo foo foo foo"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("nna"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(4, 1); FinishTest("foo foo bar bar bar bar"); // The results of "a" can be undone in one go. BeginTest("foo foo foo foo foo foo"); TestPressKey(":s/foo/bar/cg\\enter"); TestPressKey("ya"); verifyShowsNumberOfReplacementsAcrossNumberOfLines(6, 1); TestPressKey("u"); FinishTest("bar foo foo foo foo foo"); #if 0 // XXX - as of Qt 5.5, simply replaying the correct QKeyEvents does *not* cause shortcuts // to be triggered, so these tests cannot pass. // It's possible that a solution involving QTestLib will be workable in the future, though. { // Test the test suite: ensure that shortcuts are still being sent and received correctly. // The test shortcut chosen should be one that does not conflict with built-in Kate ones. FailsIfSlotNotCalled failsIfActionNotTriggered; QAction *dummyAction = kate_view->actionCollection()->addAction("Woo"); dummyAction->setShortcut(QKeySequence("Ctrl+]")); QVERIFY(connect(dummyAction, SIGNAL(triggered()), &failsIfActionNotTriggered, SLOT(slot()))); DoTest("foo", "\\ctrl-]", "foo"); // Processing shortcuts seems to require events to be processed. while (QApplication::hasPendingEvents()) { QApplication::processEvents(); } delete dummyAction; } { // Test that shortcuts involving ctrl+ work correctly. FailsIfSlotNotCalled failsIfActionNotTriggered; QAction *dummyAction = kate_view->actionCollection()->addAction("Woo"); dummyAction->setShortcut(QKeySequence("Ctrl+1")); QVERIFY(connect(dummyAction, SIGNAL(triggered()), &failsIfActionNotTriggered, SLOT(slot()))); DoTest("foo", "\\ctrl-1", "foo"); // Processing shortcuts seems to require events to be processed. while (QApplication::hasPendingEvents()) { QApplication::processEvents(); } delete dummyAction; } { // Test that shortcuts involving alt+ work correctly. FailsIfSlotNotCalled failsIfActionNotTriggered; QAction *dummyAction = kate_view->actionCollection()->addAction("Woo"); dummyAction->setShortcut(QKeySequence("Alt+1")); QVERIFY(connect(dummyAction, SIGNAL(triggered()), &failsIfActionNotTriggered, SLOT(slot()))); DoTest("foo", "\\alt-1", "foo"); // Processing shortcuts seems to require events to be processed. while (QApplication::hasPendingEvents()) { QApplication::processEvents(); } delete dummyAction; } #endif // Find the "Print" action for later use. QAction *printAction = nullptr; foreach(QAction* action, kate_view->actionCollection()->actions()) { if (action->shortcut() == QKeySequence("Ctrl+p")) { printAction = action; break; } } // Test that we don't inadvertantly trigger shortcuts in kate_view when typing them in the // emulated command bar. Requires the above test for shortcuts to be sent and received correctly // to pass. { QVERIFY(mainWindow->isActiveWindow()); QVERIFY(printAction); FailsIfSlotCalled failsIfActionTriggered("The kate_view shortcut should not be triggered by typing it in emulated command bar!"); // Don't invoke Print on failure, as this hangs instead of failing. //disconnect(printAction, SIGNAL(triggered(bool)), kate_document, SLOT(print())); connect(printAction, SIGNAL(triggered(bool)), &failsIfActionTriggered, SLOT(slot())); DoTest("foo bar foo bar", "/bar\\enterggd/\\ctrl-p\\enter.", "bar"); // Processing shortcuts seems to require events to be processed. while (QApplication::hasPendingEvents()) { QApplication::processEvents(); } } // Test that the interactive search replace does not handle general keypresses like ctrl-p ("invoke // completion in emulated command bar"). // Unfortunately, "ctrl-p" in kate_view, which is what will be triggered if this // test succeeds, hangs due to showing the print dialog, so we need to temporarily // block the Print action. clearCommandHistory(); if (printAction) { printAction->blockSignals(true); } vi_global->commandHistory()->append("s/foo/bar/caa"); BeginTest("foo"); TestPressKey(":s/foo/bar/c\\ctrl-b\\enter\\ctrl-p"); QVERIFY(!emulatedCommandBarCompleter()->popup()->isVisible()); TestPressKey("\\ctrl-c"); if (printAction) { printAction->blockSignals(false); } FinishTest("foo"); // The interactive sed replace command is added to the history straight away. clearCommandHistory(); BeginTest("foo"); TestPressKey(":s/foo/bar/c\\enter"); QCOMPARE(commandHistory(), QStringList() << "s/foo/bar/c"); TestPressKey("\\ctrl-c"); FinishTest("foo"); clearCommandHistory(); BeginTest("foo"); TestPressKey(":s/notfound/bar/c\\enter"); QCOMPARE(commandHistory(), QStringList() << "s/notfound/bar/c"); TestPressKey("\\ctrl-c"); FinishTest("foo"); // Should be usable in mappings. clearAllMappings(); vi_global->mappings()->add(Mappings::NormalModeMapping, "H", ":s/foo/bar/gcnnyyl", Mappings::Recursive); DoTest("foo foo foo foo foo foo", "H", "foo foo bar bar bar foo"); clearAllMappings(); vi_global->mappings()->add(Mappings::NormalModeMapping, "H", ":s/foo/bar/gcnna", Mappings::Recursive); DoTest("foo foo foo foo foo foo", "H", "foo foo bar bar bar bar"); clearAllMappings(); vi_global->mappings()->add(Mappings::NormalModeMapping, "H", ":s/foo/bar/gcnnyqggidone", Mappings::Recursive); DoTest("foo foo foo foo foo foo", "H", "donefoo foo bar foo foo foo"); // Don't swallow "Ctrl+" meant for the text edit. if (QKeySequence::keyBindings(QKeySequence::Undo).contains(QKeySequence("Ctrl+Z"))) { DoTest("foo bar", "/bar\\ctrl-z\\enterrX", "Xoo bar"); } else { qWarning() << "Skipped test: Ctrl+Z is not Undo on this platform"; } // Don't give invalid cursor position to updateCursor in Visual Mode: it will cause a crash! DoTest("xyz\nfoo\nbar\n123", "/foo\\\\nbar\\\\n\\enterggv//e\\enter\\ctrl-crX", "xyz\nfoo\nbaX\n123"); DoTest("\nfooxyz\nbar;\n" , "/foo.*\\\\n.*;\\enterggv//e\\enter\\ctrl-crX", "\nfooxyz\nbarX\n"); } QCompleter* EmulatedCommandBarTest::emulatedCommandBarCompleter() { return vi_input_mode->viModeEmulatedCommandBar()->findChild("completer"); } void EmulatedCommandBarTest::verifyCommandBarCompletionVisible() { if (!emulatedCommandBarCompleter()->popup()->isVisible()) { qDebug() << "Emulated command bar completer not visible."; QStringListModel *completionModel = qobject_cast(emulatedCommandBarCompleter()->model()); Q_ASSERT(completionModel); QStringList allAvailableCompletions = completionModel->stringList(); qDebug() << " Completion list: " << allAvailableCompletions; qDebug() << " Completion prefix: " << emulatedCommandBarCompleter()->completionPrefix(); bool candidateCompletionFound = false; foreach (const QString& availableCompletion, allAvailableCompletions) { if (availableCompletion.startsWith(emulatedCommandBarCompleter()->completionPrefix())) { candidateCompletionFound = true; break; } } if (candidateCompletionFound) { qDebug() << " The current completion prefix is a prefix of one of the available completions, so either complete() was not called, or the popup was manually hidden since then"; } else { qDebug() << " The current completion prefix is not a prefix of one of the available completions; this may or may not be why it is not visible"; } } QVERIFY(emulatedCommandBarCompleter()->popup()->isVisible()); } void EmulatedCommandBarTest::verifyCommandBarCompletionsMatches(const QStringList& expectedCompletionList) { verifyCommandBarCompletionVisible(); QStringList actualCompletionList; for (int i = 0; emulatedCommandBarCompleter()->setCurrentRow(i); i++) actualCompletionList << emulatedCommandBarCompleter()->currentCompletion(); if (expectedCompletionList != actualCompletionList) { qDebug() << "Actual completions:\n " << actualCompletionList << "\n\ndo not match expected:\n" << expectedCompletionList; } QCOMPARE(actualCompletionList, expectedCompletionList); } void EmulatedCommandBarTest::verifyCommandBarCompletionContains(const QStringList& expectedCompletionList) { verifyCommandBarCompletionVisible(); QStringList actualCompletionList; for (int i = 0; emulatedCommandBarCompleter()->setCurrentRow(i); i++) { actualCompletionList << emulatedCommandBarCompleter()->currentCompletion(); } foreach(const QString& expectedCompletion, expectedCompletionList) { if (!actualCompletionList.contains(expectedCompletion)) { qDebug() << "Whoops: " << actualCompletionList << " does not contain " << expectedCompletion; } QVERIFY(actualCompletionList.contains(expectedCompletion)); } } QLabel* EmulatedCommandBarTest::emulatedCommandTypeIndicator() { return emulatedCommandBar()->findChild("bartypeindicator"); } void EmulatedCommandBarTest::verifyCursorAt(const Cursor& expectedCursorPos) { QCOMPARE(kate_view->cursorPosition().line(), expectedCursorPos.line()); QCOMPARE(kate_view->cursorPosition().column(), expectedCursorPos.column()); } void EmulatedCommandBarTest::clearSearchHistory() { vi_global->searchHistory()->clear(); } QStringList EmulatedCommandBarTest::searchHistory() { return vi_global->searchHistory()->items(); } void EmulatedCommandBarTest::clearCommandHistory() { vi_global->commandHistory()->clear(); } QStringList EmulatedCommandBarTest::commandHistory() { return vi_global->commandHistory()->items(); } void EmulatedCommandBarTest::clearReplaceHistory() { vi_global->replaceHistory()->clear(); } QStringList EmulatedCommandBarTest::replaceHistory() { return vi_global->replaceHistory()->items(); } QList EmulatedCommandBarTest::rangesOnFirstLine() { return kate_document->buffer().rangesForLine(0, kate_view, true); } void EmulatedCommandBarTest::verifyTextEditBackgroundColour(const QColor& expectedBackgroundColour) { QCOMPARE(emulatedCommandBarTextEdit()->palette().brush(QPalette::Base).color(), expectedBackgroundColour); } QLabel* EmulatedCommandBarTest::commandResponseMessageDisplay() { QLabel* commandResponseMessageDisplay = emulatedCommandBar()->findChild("commandresponsemessage"); Q_ASSERT(commandResponseMessageDisplay); return commandResponseMessageDisplay; } void EmulatedCommandBarTest::waitForEmulatedCommandBarToHide(long int timeout) { const QDateTime waitStartedTime = QDateTime::currentDateTime(); while(emulatedCommandBar()->isVisible() && waitStartedTime.msecsTo(QDateTime::currentDateTime()) < timeout) { QApplication::processEvents(); } QVERIFY(!emulatedCommandBar()->isVisible()); } void EmulatedCommandBarTest::verifyShowsNumberOfReplacementsAcrossNumberOfLines(int numReplacements, int acrossNumLines) { QVERIFY(commandResponseMessageDisplay()->isVisible()); QVERIFY(!emulatedCommandTypeIndicator()->isVisible()); const QString commandMessageResponseText = commandResponseMessageDisplay()->text(); const QString expectedNumReplacementsAsString = QString::number(numReplacements); const QString expectedAcrossNumLinesAsString = QString::number(acrossNumLines); // Be a bit vague about the actual contents due to e.g. localization. // TODO - see if we can insist that en_US is available on the Kate Jenkins server and // insist that we use it ... ? - QRegExp numReplacementsMessageRegex("^.*(\\d+).*(\\d+).*$"); - QVERIFY(numReplacementsMessageRegex.exactMatch(commandMessageResponseText)); - const QString actualNumReplacementsAsString = numReplacementsMessageRegex.cap(1); - const QString actualAcrossNumLinesAsString = numReplacementsMessageRegex.cap(2); + static const QRegularExpression numReplacementsMessageRegex("^.*(\\d+).*(\\d+).*$"); + const auto match = numReplacementsMessageRegex.match(commandMessageResponseText); + QVERIFY(match.hasMatch()); + const QString actualNumReplacementsAsString = match.captured(1); + const QString actualAcrossNumLinesAsString = match.captured(2); QCOMPARE(actualNumReplacementsAsString, expectedNumReplacementsAsString); QCOMPARE(actualAcrossNumLinesAsString, expectedAcrossNumLinesAsString); } FailsIfSlotNotCalled::FailsIfSlotNotCalled(): QObject() { } FailsIfSlotNotCalled::~FailsIfSlotNotCalled() { QVERIFY(m_slotWasCalled); } void FailsIfSlotNotCalled::slot() { m_slotWasCalled = true; } FailsIfSlotCalled::FailsIfSlotCalled(const QString& failureMessage): QObject(), m_failureMessage(failureMessage) { } void FailsIfSlotCalled::slot() { QFAIL(qPrintable(m_failureMessage.toLatin1())); } diff --git a/autotests/src/vimode/fakecodecompletiontestmodel.cpp b/autotests/src/vimode/fakecodecompletiontestmodel.cpp index 79efd8ea..eee40cb7 100644 --- a/autotests/src/vimode/fakecodecompletiontestmodel.cpp +++ b/autotests/src/vimode/fakecodecompletiontestmodel.cpp @@ -1,190 +1,192 @@ /* * This file is part of the KDE libraries * * Copyright (C) 2014 Miquel Sabaté Solà * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include "base.h" #include "fakecodecompletiontestmodel.h" +#include using namespace KTextEditor; FakeCodeCompletionTestModel::FakeCodeCompletionTestModel(KTextEditor::View *parent) : KTextEditor::CodeCompletionModel(parent), m_kateView(qobject_cast(parent)), m_kateDoc(parent->document()), m_removeTailOnCompletion(false), m_failTestOnInvocation(false), m_wasInvoked(false) { Q_ASSERT(m_kateView); setRowCount(3); cc()->setAutomaticInvocationEnabled(false); cc()->unregisterCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); //would add additional items, we don't want that in tests connect(static_cast(parent->document()), &KTextEditor::DocumentPrivate::textInserted, this, &FakeCodeCompletionTestModel::textInserted); connect(static_cast(parent->document()), &KTextEditor::DocumentPrivate::textRemoved, this, &FakeCodeCompletionTestModel::textRemoved); } void FakeCodeCompletionTestModel::setCompletions(const QStringList &completions) { QStringList sortedCompletions = completions; std::sort(sortedCompletions.begin(), sortedCompletions.end()); Q_ASSERT(completions == sortedCompletions && "QCompleter seems to sort the items, so it's best to provide them pre-sorted so it's easier to predict the order"); setRowCount(sortedCompletions.length()); m_completions = completions; } void FakeCodeCompletionTestModel::setRemoveTailOnComplete(bool removeTailOnCompletion) { m_removeTailOnCompletion = removeTailOnCompletion; } void FakeCodeCompletionTestModel::setFailTestOnInvocation(bool failTestOnInvocation) { m_failTestOnInvocation = failTestOnInvocation; } bool FakeCodeCompletionTestModel::wasInvoked() { return m_wasInvoked; } void FakeCodeCompletionTestModel::clearWasInvoked() { m_wasInvoked = false; } void FakeCodeCompletionTestModel::forceInvocationIfDocTextIs(const QString &desiredDocText) { m_forceInvocationIfDocTextIs = desiredDocText; } void FakeCodeCompletionTestModel::doNotForceInvocation() { m_forceInvocationIfDocTextIs.clear(); } QVariant FakeCodeCompletionTestModel::data(const QModelIndex &index, int role) const { m_wasInvoked = true; if (m_failTestOnInvocation) { failTest(); return QVariant(); } // Order is important, here, as the completion widget seems to do its own sorting. if (role == Qt::DisplayRole) { if (index.column() == Name) { return QString(m_completions[index.row()]); } } return QVariant(); } void FakeCodeCompletionTestModel::executeCompletionItem (KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const { qDebug() << "word: " << word << "(" << view->document()->text(word) << ")"; const Cursor origCursorPos = m_kateView->cursorPosition(); const QString textToInsert = m_completions[index.row()]; const QString textAfterCursor = view->document()->text(Range(word.end(), Cursor(word.end().line(), view->document()->lineLength(word.end().line())))); view->document()->removeText(Range(word.start(), origCursorPos)); const int lengthStillToRemove = word.end().column() - origCursorPos.column(); QString actualTextInserted = textToInsert; // Merge brackets? const QString noArgFunctionCallMarker = "()"; const QString withArgFunctionCallMarker = "(...)"; const bool endedWithSemiColon = textToInsert.endsWith(';'); if (textToInsert.contains(noArgFunctionCallMarker) || textToInsert.contains(withArgFunctionCallMarker)) { Q_ASSERT(m_removeTailOnCompletion && "Function completion items without removing tail is not yet supported!"); const bool takesArgument = textToInsert.contains(withArgFunctionCallMarker); // The code for a function call to a function taking no arguments. const QString justFunctionName = textToInsert.left(textToInsert.indexOf("(")); - QRegExp whitespaceThenOpeningBracket("^\\s*(\\()"); + static const QRegularExpression whitespaceThenOpeningBracket("^\\s*(\\()"); + const QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(textAfterCursor); int openingBracketPos = -1; - if (textAfterCursor.contains(whitespaceThenOpeningBracket)) { - openingBracketPos = whitespaceThenOpeningBracket.pos(1) + word.start().column() + justFunctionName.length() + 1 + lengthStillToRemove; + if (match.hasMatch()) { + openingBracketPos = match.capturedStart(1) + word.start().column() + justFunctionName.length() + 1 + lengthStillToRemove; } const bool mergeOpenBracketWithExisting = (openingBracketPos != -1) && !endedWithSemiColon; // Add the function name, for now: we don't yet know if we'll be adding the "()", too. view->document()->insertText(word.start(), justFunctionName); if (mergeOpenBracketWithExisting) { // Merge with opening bracket. actualTextInserted = justFunctionName; m_kateView->setCursorPosition(Cursor(word.start().line(), openingBracketPos)); } else { // Don't merge. const QString afterFunctionName = endedWithSemiColon ? "();" : "()"; view->document()->insertText(Cursor(word.start().line(), word.start().column() + justFunctionName.length()), afterFunctionName); if (takesArgument) { // Place the cursor immediately after the opening "(" we just added. m_kateView->setCursorPosition(Cursor(word.start().line(), word.start().column() + justFunctionName.length() + 1)); } } } else { // Plain text. view->document()->insertText(word.start(), textToInsert); } if (m_removeTailOnCompletion) { const int tailLength = word.end().column() - origCursorPos.column(); const Cursor tailStart = Cursor(word.start().line(), word.start().column() + actualTextInserted.length()); const Cursor tailEnd = Cursor(tailStart.line(), tailStart.column() + tailLength); view->document()->removeText(Range(tailStart, tailEnd)); } } KTextEditor::CodeCompletionInterface *FakeCodeCompletionTestModel::cc() const { return dynamic_cast(const_cast(QObject::parent())); } void FakeCodeCompletionTestModel::failTest() const { QFAIL("Shouldn't be invoking me!"); } void FakeCodeCompletionTestModel::textInserted(Document *document, Range range) { Q_UNUSED(document); Q_UNUSED(range); checkIfShouldForceInvocation(); } void FakeCodeCompletionTestModel::textRemoved(Document *document, Range range) { Q_UNUSED(document); Q_UNUSED(range); checkIfShouldForceInvocation(); } void FakeCodeCompletionTestModel::checkIfShouldForceInvocation() { if (m_forceInvocationIfDocTextIs.isEmpty()) { return; } if (m_kateDoc->text() == m_forceInvocationIfDocTextIs) { m_kateView->completionWidget()->userInvokedCompletion(); BaseTest::waitForCompletionWidgetToActivate(m_kateView); } } diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp index c6fb4fa0..8215465d 100644 --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -1,6093 +1,6093 @@ /* This file is part of the KDE libraries Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2006 Hamish Rodda Copyright (C) 2007 Mirko Stocker Copyright (C) 2009-2010 Michel Ludwig Copyright (C) 2013 Gerald Senarclens de Grancy Copyright (C) 2013 Andrey Matveyakin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-13020, USA. */ //BEGIN includes #include "config.h" #include "katedocument.h" #include "kateglobal.h" #include "katedialogs.h" #include "katehighlight.h" #include "kateview.h" #include "kateautoindent.h" #include "katetextline.h" #include "katerenderer.h" #include "kateregexp.h" #include "kateplaintextsearch.h" #include "kateregexpsearch.h" #include "kateconfig.h" #include "katemodemanager.h" #include "kateschema.h" #include "katebuffer.h" #include "kateundomanager.h" #include "spellcheck/prefixstore.h" #include "spellcheck/ontheflycheck.h" #include "spellcheck/spellcheck.h" #include "katescriptmanager.h" #include "kateswapfile.h" #include "katepartdebug.h" #include "printing/kateprinter.h" #include "kateabstractinputmode.h" #include "katetemplatehandler.h" #if EDITORCONFIG_FOUND #include "editorconfig.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if LIBGIT2_FOUND #include #include #include #endif //END includes #if 0 #define EDIT_DEBUG qCDebug(LOG_KTE) #else #define EDIT_DEBUG if (0) qCDebug(LOG_KTE) #endif template static int indexOf(const std::initializer_list & list, const E& entry) { auto it = std::find(list.begin(), list.end(), entry); return it == list.end() ? -1 : std::distance(list.begin(), it); } template static bool contains(const std::initializer_list & list, const E& entry) { return indexOf(list, entry) >= 0; } static inline QChar matchingStartBracket(const QChar c) { switch (c.toLatin1()) { case '}': return QLatin1Char('{'); case ']': return QLatin1Char('['); case ')': return QLatin1Char('('); } return QChar(); } static inline QChar matchingEndBracket(const QChar c, bool withQuotes = true) { switch (c.toLatin1()) { case '{': return QLatin1Char('}'); case '[': return QLatin1Char(']'); case '(': return QLatin1Char(')'); case '\'': return withQuotes ? QLatin1Char('\'') : QChar(); case '"': return withQuotes ? QLatin1Char('"') : QChar(); } return QChar(); } static inline QChar matchingBracket(const QChar c) { QChar bracket = matchingStartBracket(c); if (bracket.isNull()) { bracket = matchingEndBracket(c, /*withQuotes=*/false); } return bracket; } static inline bool isStartBracket(const QChar c) { return ! matchingEndBracket(c, /*withQuotes=*/false).isNull(); } static inline bool isEndBracket(const QChar c) { return ! matchingStartBracket(c).isNull(); } static inline bool isBracket(const QChar c) { return isStartBracket(c) || isEndBracket(c); } /** * normalize given url * @param url input url * @return normalized url */ static QUrl normalizeUrl (const QUrl &url) { /** * only normalize local urls */ if (url.isEmpty() || !url.isLocalFile()) return url; /** * don't normalize if not existing! * canonicalFilePath won't work! */ const QString normalizedUrl(QFileInfo(url.toLocalFile()).canonicalFilePath()); if (normalizedUrl.isEmpty()) return url; /** * else: use canonicalFilePath to normalize */ return QUrl::fromLocalFile(normalizedUrl); } //BEGIN d'tor, c'tor // // KTextEditor::DocumentPrivate Constructor // KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode, bool bReadOnly, QWidget *parentWidget, QObject *parent) : KTextEditor::Document (this, parent), m_bSingleViewMode(bSingleViewMode), m_bReadOnly(bReadOnly), m_undoManager(new KateUndoManager(this)), m_buffer(new KateBuffer(this)), m_indenter(new KateAutoIndent(this)), m_docName(QStringLiteral("need init")), m_fileType(QStringLiteral("Normal")), m_config(new KateDocumentConfig(this)) { /** * no plugins from kparts here */ setPluginLoadingMode (DoNotLoadPlugins); /** * pass on our component data, do this after plugin loading is off */ setComponentData(KTextEditor::EditorPrivate::self()->aboutData()); /** * avoid spamming plasma and other window managers with progress dialogs * we show such stuff inline in the views! */ setProgressInfoEnabled(false); // register doc at factory KTextEditor::EditorPrivate::self()->registerDocument(this); // normal hl m_buffer->setHighlight(0); // swap file m_swapfile = (config()->swapFileMode() == KateDocumentConfig::DisableSwapFile) ? nullptr : new Kate::SwapFile(this); // some nice signals from the buffer connect(m_buffer, SIGNAL(tagLines(int,int)), this, SLOT(tagLines(int,int))); // if the user changes the highlight with the dialog, notify the doc connect(KateHlManager::self(), SIGNAL(changed()), SLOT(internalHlChanged())); // signals for mod on hd connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(dirty(QString)), this, SLOT(slotModOnHdDirty(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(created(QString)), this, SLOT(slotModOnHdCreated(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(deleted(QString)), this, SLOT(slotModOnHdDeleted(QString))); /** * singleshot timer to handle updates of mod on hd state delayed */ m_modOnHdTimer.setSingleShot(true); m_modOnHdTimer.setInterval(200); connect(&m_modOnHdTimer, SIGNAL(timeout()), this, SLOT(slotDelayedHandleModOnHd())); // Setup auto reload stuff m_autoReloadMode = new KToggleAction(i18n("Auto Reload Document"), this); m_autoReloadMode->setWhatsThis(i18n("Automatic reload the document when it was changed on disk")); connect(m_autoReloadMode, &KToggleAction::triggered, this, &DocumentPrivate::autoReloadToggled); // Prepare some reload amok protector... m_autoReloadThrottle.setSingleShot(true); //...but keep the value small in unit tests m_autoReloadThrottle.setInterval(KTextEditor::EditorPrivate::self()->unitTestMode() ? 50 : 3000); connect(&m_autoReloadThrottle, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); /** * load handling * this is needed to ensure we signal the user if a file ist still loading * and to disallow him to edit in that time */ connect(this, SIGNAL(started(KIO::Job*)), this, SLOT(slotStarted(KIO::Job*))); connect(this, SIGNAL(completed()), this, SLOT(slotCompleted())); connect(this, SIGNAL(canceled(QString)), this, SLOT(slotCanceled())); connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(slotUrlChanged(QUrl))); // update doc name updateDocName(); // if single view mode, like in the konqui embedding, create a default view ;) // be lazy, only create it now, if any parentWidget is given, otherwise widget() // will create it on demand... if (m_bSingleViewMode && parentWidget) { KTextEditor::View *view = (KTextEditor::View *)createView(parentWidget); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); } connect(m_undoManager, SIGNAL(undoChanged()), this, SIGNAL(undoChanged())); connect(m_undoManager, SIGNAL(undoStart(KTextEditor::Document*)), this, SIGNAL(editingStarted(KTextEditor::Document*))); connect(m_undoManager, SIGNAL(undoEnd(KTextEditor::Document*)), this, SIGNAL(editingFinished(KTextEditor::Document*))); connect(m_undoManager, SIGNAL(redoStart(KTextEditor::Document*)), this, SIGNAL(editingStarted(KTextEditor::Document*))); connect(m_undoManager, SIGNAL(redoEnd(KTextEditor::Document*)), this, SIGNAL(editingFinished(KTextEditor::Document*))); connect(this, SIGNAL(sigQueryClose(bool*,bool*)), this, SLOT(slotQueryClose_save(bool*,bool*))); connect(this, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearEditingPosStack())); onTheFlySpellCheckingEnabled(config()->onTheFlySpellCheck()); // make sure correct defaults are set (indenter, ...) updateConfig(); } // // KTextEditor::DocumentPrivate Destructor // KTextEditor::DocumentPrivate::~DocumentPrivate() { // delete pending mod-on-hd message, if applicable delete m_modOnHdHandler; /** * we are about to delete cursors/ranges/... */ emit aboutToDeleteMovingInterfaceContent(this); // kill it early, it has ranges! delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; clearDictionaryRanges(); // Tell the world that we're about to close (== destruct) // Apps must receive this in a direct signal-slot connection, and prevent // any further use of interfaces once they return. emit aboutToClose(this); // remove file from dirwatch deactivateDirWatch(); // thanks for offering, KPart, but we're already self-destructing setAutoDeleteWidget(false); setAutoDeletePart(false); // clean up remaining views qDeleteAll (m_views.keys()); m_views.clear(); // cu marks for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { delete i.value(); } m_marks.clear(); delete m_config; KTextEditor::EditorPrivate::self()->deregisterDocument(this); } //END void KTextEditor::DocumentPrivate::saveEditingPositions(const KTextEditor::Cursor &cursor) { if (m_editingStackPosition != m_editingStack.size() - 1) { m_editingStack.resize(m_editingStackPosition); } // try to be clever: reuse existing cursors if possible QSharedPointer mc; // we might pop last one: reuse that if (!m_editingStack.isEmpty() && cursor.line() == m_editingStack.top()->line()) { mc = m_editingStack.pop(); } // we might expire oldest one, reuse that one, if not already one there // we prefer the other one for reuse, as already on the right line aka in the right block! const int editingStackSizeLimit = 32; if (m_editingStack.size() >= editingStackSizeLimit) { if (mc) { m_editingStack.removeFirst(); } else { mc = m_editingStack.takeFirst(); } } // new cursor needed? or adjust existing one? if (mc) { mc->setPosition(cursor); } else { mc = QSharedPointer (newMovingCursor(cursor)); } // add new one as top of stack m_editingStack.push(mc); m_editingStackPosition = m_editingStack.size() - 1; } KTextEditor::Cursor KTextEditor::DocumentPrivate::lastEditingPosition(EditingPositionKind nextOrPrev, KTextEditor::Cursor currentCursor) { if (m_editingStack.isEmpty()) { return KTextEditor::Cursor::invalid(); } auto targetPos = m_editingStack.at(m_editingStackPosition)->toCursor(); if (targetPos == currentCursor) { if (nextOrPrev == Previous) { m_editingStackPosition--; } else { m_editingStackPosition++; } m_editingStackPosition = qBound(0, m_editingStackPosition, m_editingStack.size() - 1); } return m_editingStack.at(m_editingStackPosition)->toCursor(); } void KTextEditor::DocumentPrivate::clearEditingPosStack() { m_editingStack.clear(); m_editingStackPosition = -1; } // on-demand view creation QWidget *KTextEditor::DocumentPrivate::widget() { // no singleViewMode -> no widget()... if (!singleViewMode()) { return nullptr; } // does a widget exist already? use it! if (KTextEditor::Document::widget()) { return KTextEditor::Document::widget(); } // create and return one... KTextEditor::View *view = (KTextEditor::View *)createView(nullptr); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); return view; } //BEGIN KTextEditor::Document stuff KTextEditor::View *KTextEditor::DocumentPrivate::createView(QWidget *parent, KTextEditor::MainWindow *mainWindow) { KTextEditor::ViewPrivate *newView = new KTextEditor::ViewPrivate(this, parent, mainWindow); if (m_fileChangedDialogsActivated) { connect(newView, SIGNAL(focusIn(KTextEditor::View*)), this, SLOT(slotModifiedOnDisk())); } emit viewCreated(this, newView); // post existing messages to the new view, if no specific view is given foreach (KTextEditor::Message *message, m_messageHash.keys()) { if (!message->view()) { newView->postMessage(message, m_messageHash[message]); } } return newView; } KTextEditor::Range KTextEditor::DocumentPrivate::rangeOnLine(KTextEditor::Range range, int line) const { const int col1 = toVirtualColumn(range.start()); const int col2 = toVirtualColumn(range.end()); return KTextEditor::Range(line, fromVirtualColumn(line, col1), line, fromVirtualColumn(line, col2)); } //BEGIN KTextEditor::EditInterface stuff bool KTextEditor::DocumentPrivate::isEditingTransactionRunning() const { return editSessionNumber > 0; } QString KTextEditor::DocumentPrivate::text() const { return m_buffer->text(); } QString KTextEditor::DocumentPrivate::text(const KTextEditor::Range &range, bool blockwise) const { if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return QString(); } QString s; if (range.start().line() == range.end().line()) { if (range.start().column() > range.end().column()) { return QString(); } Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return QString(); } return textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { s.append(textLine->string(range.start().column(), textLine->length() - range.start().column())); } else if (i == range.end().line()) { s.append(textLine->string(0, range.end().column())); } else { s.append(textLine->string()); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); s.append(textLine->string(subRange.start().column(), subRange.columnWidth())); } if (i < range.end().line()) { s.append(QLatin1Char('\n')); } } } return s; } QChar KTextEditor::DocumentPrivate::characterAt(const KTextEditor::Cursor &position) const { Kate::TextLine textLine = m_buffer->plainLine(position.line()); if (!textLine) { return QChar(); } return textLine->at(position.column()); } QString KTextEditor::DocumentPrivate::wordAt(const KTextEditor::Cursor &cursor) const { return text(wordRangeAt(cursor)); } KTextEditor::Range KTextEditor::DocumentPrivate::wordRangeAt(const KTextEditor::Cursor &cursor) const { // get text line const int line = cursor.line(); Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { return KTextEditor::Range::invalid(); } // make sure the cursor is const int lineLenth = textLine->length(); if (cursor.column() > lineLenth) { return KTextEditor::Range::invalid(); } int start = cursor.column(); int end = start; while (start > 0 && highlight()->isInWord(textLine->at(start - 1), textLine->attribute(start - 1))) { start--; } while (end < lineLenth && highlight()->isInWord(textLine->at(end), textLine->attribute(end))) { end++; } return KTextEditor::Range(line, start, line, end); } bool KTextEditor::DocumentPrivate::isValidTextPosition(const KTextEditor::Cursor& cursor) const { const int ln = cursor.line(); const int col = cursor.column(); // cursor in document range? if (ln < 0 || col < 0 || ln >= lines() || col > lineLength(ln)) { return false; } const QString str = line(ln); Q_ASSERT(str.length() >= col); // cursor at end of line? const int len = lineLength(ln); if (col == 0 || col == len) { return true; } // cursor in the middle of a valid utf32-surrogate? return (! str.at(col).isLowSurrogate()) || (! str.at(col-1).isHighSurrogate()); } QStringList KTextEditor::DocumentPrivate::textLines(const KTextEditor::Range &range, bool blockwise) const { QStringList ret; if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return ret; } if (blockwise && (range.start().column() > range.end().column())) { return ret; } if (range.start().line() == range.end().line()) { Q_ASSERT(range.start() <= range.end()); Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return ret; } ret << textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { ret << textLine->string(range.start().column(), textLine->length() - range.start().column()); } else if (i == range.end().line()) { ret << textLine->string(0, range.end().column()); } else { ret << textLine->string(); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); ret << textLine->string(subRange.start().column(), subRange.columnWidth()); } } } return ret; } QString KTextEditor::DocumentPrivate::line(int line) const { Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return QString(); } return l->string(); } bool KTextEditor::DocumentPrivate::setText(const QString &s) { if (!isReadWrite()) { return false; } QList msave; foreach (KTextEditor::Mark *mark, m_marks) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor(), s); editEnd(); foreach (KTextEditor::Mark mark, msave) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::setText(const QStringList &text) { if (!isReadWrite()) { return false; } QList msave; foreach (KTextEditor::Mark *mark, m_marks) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor::start(), text); editEnd(); foreach (KTextEditor::Mark mark, msave) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::clear() { if (!isReadWrite()) { return false; } foreach (KTextEditor::ViewPrivate *view, m_views) { view->clear(); view->tagAll(); view->update(); } clearMarks(); emit aboutToInvalidateMovingInterfaceContent(this); m_buffer->invalidateRanges(); emit aboutToRemoveText(documentRange()); return editRemoveLines(0, lastLine()); } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QString &text, bool block) { if (!isReadWrite()) { return false; } if (text.isEmpty()) { return true; } editStart(); int currentLine = position.line(); int currentLineStart = 0; const int totalLength = text.length(); int insertColumn = position.column(); // pad with empty lines, if insert position is after last line if (position.line() > lines()) { int line = lines(); while (line <= position.line()) { editInsertLine(line, QString()); line++; } } // compute expanded column for block mode int positionColumnExpanded = insertColumn; const int tabWidth = config()->tabWidth(); if (block) { if (auto l = plainKateTextLine(currentLine)) { positionColumnExpanded = l->toVirtualColumn(insertColumn, tabWidth); } } int pos = 0; for (; pos < totalLength; pos++) { const QChar &ch = text.at(pos); if (ch == QLatin1Char('\n')) { // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } if (!block) { editWrapLine(currentLine, insertColumn + pos - currentLineStart); insertColumn = 0; } currentLine++; if (block) { auto l = plainKateTextLine(currentLine); if (currentLine == lastLine() + 1) { editInsertLine(currentLine, QString()); } insertColumn = positionColumnExpanded; if (l) { insertColumn = l->fromVirtualColumn(insertColumn, tabWidth); } } currentLineStart = pos + 1; } } // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QStringList &textLines, bool block) { if (!isReadWrite()) { return false; } // just reuse normal function return insertText(position, textLines.join(QStringLiteral("\n")), block); } bool KTextEditor::DocumentPrivate::removeText(const KTextEditor::Range &_range, bool block) { KTextEditor::Range range = _range; if (!isReadWrite()) { return false; } // Should now be impossible to trigger with the new Range class Q_ASSERT(range.start().line() <= range.end().line()); if (range.start().line() > lastLine()) { return false; } if (!block) { emit aboutToRemoveText(range); } editStart(); if (!block) { if (range.end().line() > lastLine()) { range.setEnd(KTextEditor::Cursor(lastLine() + 1, 0)); } if (range.onSingleLine()) { editRemoveText(range.start().line(), range.start().column(), range.columnWidth()); } else { int from = range.start().line(); int to = range.end().line(); // remove last line if (to <= lastLine()) { editRemoveText(to, 0, range.end().column()); } // editRemoveLines() will be called on first line (to remove bookmark) if (range.start().column() == 0 && from > 0) { --from; } // remove middle lines editRemoveLines(from + 1, to - 1); // remove first line if not already removed by editRemoveLines() if (range.start().column() > 0 || range.start().line() == 0) { editRemoveText(from, range.start().column(), m_buffer->plainLine(from)->length() - range.start().column()); editUnWrapLine(from); } } } // if ( ! block ) else { int startLine = qMax(0, range.start().line()); int vc1 = toVirtualColumn(range.start()); int vc2 = toVirtualColumn(range.end()); for (int line = qMin(range.end().line(), lastLine()); line >= startLine; --line) { int col1 = fromVirtualColumn(line, vc1); int col2 = fromVirtualColumn(line, vc2); editRemoveText(line, qMin(col1, col2), qAbs(col2 - col1)); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertLine(int l, const QString &str) { if (!isReadWrite()) { return false; } if (l < 0 || l > lines()) { return false; } return editInsertLine(l, str); } bool KTextEditor::DocumentPrivate::insertLines(int line, const QStringList &text) { if (!isReadWrite()) { return false; } if (line < 0 || line > lines()) { return false; } bool success = true; foreach (const QString &string, text) { success &= editInsertLine(line++, string); } return success; } bool KTextEditor::DocumentPrivate::removeLine(int line) { if (!isReadWrite()) { return false; } if (line < 0 || line > lastLine()) { return false; } return editRemoveLine(line); } int KTextEditor::DocumentPrivate::totalCharacters() const { int l = 0; for (int i = 0; i < m_buffer->count(); ++i) { Kate::TextLine line = m_buffer->plainLine(i); if (line) { l += line->length(); } } return l; } int KTextEditor::DocumentPrivate::lines() const { return m_buffer->count(); } int KTextEditor::DocumentPrivate::lineLength(int line) const { if (line < 0 || line > lastLine()) { return -1; } Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return -1; } return l->length(); } bool KTextEditor::DocumentPrivate::isLineModified(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified(); } bool KTextEditor::DocumentPrivate::isLineSaved(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsSavedOnDisk(); } bool KTextEditor::DocumentPrivate::isLineTouched(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified() || l->markedAsSavedOnDisk(); } //END //BEGIN KTextEditor::EditInterface internal stuff // // Starts an edit session with (or without) undo, update of view disabled during session // bool KTextEditor::DocumentPrivate::editStart() { editSessionNumber++; if (editSessionNumber > 1) { return false; } editIsRunning = true; // no last change cursor at start m_editLastChangeStartCursor = KTextEditor::Cursor::invalid(); m_undoManager->editStart(); foreach (KTextEditor::ViewPrivate *view, m_views) { view->editStart(); } m_buffer->editStart(); return true; } // // End edit session and update Views // bool KTextEditor::DocumentPrivate::editEnd() { if (editSessionNumber == 0) { Q_ASSERT(0); return false; } // wrap the new/changed text, if something really changed! if (m_buffer->editChanged() && (editSessionNumber == 1)) if (m_undoManager->isActive() && config()->wordWrap()) { wrapText(m_buffer->editTagStart(), m_buffer->editTagEnd()); } editSessionNumber--; if (editSessionNumber > 0) { return false; } // end buffer edit, will trigger hl update // this will cause some possible adjustment of tagline start/end m_buffer->editEnd(); m_undoManager->editEnd(); // edit end for all views !!!!!!!!! foreach (KTextEditor::ViewPrivate *view, m_views) { view->editEnd(m_buffer->editTagStart(), m_buffer->editTagEnd(), m_buffer->editTagFrom()); } if (m_buffer->editChanged()) { setModified(true); emit textChanged(this); } // remember last change position in the stack, if any // this avoid costly updates for longer editing transactions // before we did that on textInsert/Removed if (m_editLastChangeStartCursor.isValid()) saveEditingPositions(m_editLastChangeStartCursor); editIsRunning = false; return true; } void KTextEditor::DocumentPrivate::pushEditState() { editStateStack.push(editSessionNumber); } void KTextEditor::DocumentPrivate::popEditState() { if (editStateStack.isEmpty()) { return; } int count = editStateStack.pop() - editSessionNumber; while (count < 0) { ++count; editEnd(); } while (count > 0) { --count; editStart(); } } void KTextEditor::DocumentPrivate::inputMethodStart() { m_undoManager->inputMethodStart(); } void KTextEditor::DocumentPrivate::inputMethodEnd() { m_undoManager->inputMethodEnd(); } bool KTextEditor::DocumentPrivate::wrapText(int startLine, int endLine) { if (startLine < 0 || endLine < 0) { return false; } if (!isReadWrite()) { return false; } int col = config()->wordWrapAt(); if (col == 0) { return false; } editStart(); for (int line = startLine; (line <= endLine) && (line < lines()); line++) { Kate::TextLine l = kateTextLine(line); if (!l) { break; } //qCDebug(LOG_KTE) << "try wrap line: " << line; if (l->virtualLength(m_buffer->tabWidth()) > col) { Kate::TextLine nextl = kateTextLine(line + 1); //qCDebug(LOG_KTE) << "do wrap line: " << line; int eolPosition = l->length() - 1; // take tabs into account here, too int x = 0; const QString &t = l->string(); int z2 = 0; for (; z2 < l->length(); z2++) { static const QChar tabChar(QLatin1Char('\t')); if (t.at(z2) == tabChar) { x += m_buffer->tabWidth() - (x % m_buffer->tabWidth()); } else { x++; } if (x > col) { break; } } const int colInChars = qMin(z2, l->length() - 1); int searchStart = colInChars; // If where we are wrapping is an end of line and is a space we don't // want to wrap there if (searchStart == eolPosition && t.at(searchStart).isSpace()) { searchStart--; } // Scan backwards looking for a place to break the line // We are not interested in breaking at the first char // of the line (if it is a space), but we are at the second // anders: if we can't find a space, try breaking on a word // boundary, using KateHighlight::canBreakAt(). // This could be a priority (setting) in the hl/filetype/document int z = -1; int nw = -1; // alternative position, a non word character for (z = searchStart; z >= 0; z--) { if (t.at(z).isSpace()) { break; } if ((nw < 0) && highlight()->canBreakAt(t.at(z), l->attribute(z))) { nw = z; } } if (z >= 0) { // So why don't we just remove the trailing space right away? // Well, the (view's) cursor may be directly in front of that space // (user typing text before the last word on the line), and if that // happens, the cursor would be moved to the next line, which is not // what we want (bug #106261) z++; } else { // There was no space to break at so break at a nonword character if // found, or at the wrapcolumn ( that needs be configurable ) // Don't try and add any white space for the break if ((nw >= 0) && nw < colInChars) { nw++; // break on the right side of the character } z = (nw >= 0) ? nw : colInChars; } if (nextl && !nextl->isAutoWrapped()) { editWrapLine(line, z, true); editMarkLineAutoWrapped(line + 1, true); endLine++; } else { if (nextl && (nextl->length() > 0) && !nextl->at(0).isSpace() && ((l->length() < 1) || !l->at(l->length() - 1).isSpace())) { editInsertText(line + 1, 0, QLatin1String(" ")); } bool newLineAdded = false; editWrapLine(line, z, false, &newLineAdded); editMarkLineAutoWrapped(line + 1, true); endLine++; } } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::wrapParagraph(int first, int last) { if (first == last) { return wrapText(first, last); } if (first < 0 || last < first) { return false; } if (last >= lines() || first > last) { return false; } if (!isReadWrite()) { return false; } editStart(); // Because we shrink and expand lines, we need to track the working set by powerful "MovingStuff" std::unique_ptr range(newMovingRange(KTextEditor::Range(first, 0, last, 0))); std::unique_ptr curr(newMovingCursor(KTextEditor::Cursor(range->start()))); // Scan the selected range for paragraphs, whereas each empty line trigger a new paragraph for (int line = first; line <= range->end().line(); ++line) { // Is our first line a somehow filled line? if(plainKateTextLine(first)->firstChar() < 0) { // Fast forward to first non empty line ++first; curr->setPosition(curr->line() + 1, 0); continue; } // Is our current line a somehow filled line? If not, wrap the paragraph if (plainKateTextLine(line)->firstChar() < 0) { curr->setPosition(line, 0); // Set on empty line joinLines(first, line - 1); // Don't wrap twice! That may cause a bad result if (!wordWrap()) { wrapText(first, first); } first = curr->line() + 1; line = first; } } // If there was no paragraph, we need to wrap now bool needWrap = (curr->line() != range->end().line()); if (needWrap && plainKateTextLine(first)->firstChar() != -1) { joinLines(first, range->end().line()); // Don't wrap twice! That may cause a bad result if (!wordWrap()) { wrapText(first, first); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertText(int line, int col, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertText" << line << col << s; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (s.isEmpty()) { return true; } editStart(); QString s2 = s; int col2 = col; if (col2 > l->length()) { s2 = QString(col2 - l->length(), QLatin1Char(' ')) + s; col2 = l->length(); } m_undoManager->slotTextInserted(line, col2, s2); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col2); // insert text into line m_buffer->insertText(m_editLastChangeStartCursor, s2); emit textInserted(this, KTextEditor::Range(line, col2, line, col2 + s2.length())); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveText(int line, int col, int len) { // verbose debug EDIT_DEBUG << "editRemoveText" << line << col << len; if (line < 0 || col < 0 || len < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (len == 0) { return true; } // wrong column if (col >= l->text().size()) { return false; } // don't try to remove what's not there len = qMin(len, l->text().size() - col); editStart(); QString oldText = l->string().mid(col, len); m_undoManager->slotTextRemoved(line, col, oldText); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); // remove text from line m_buffer->removeText(KTextEditor::Range(m_editLastChangeStartCursor, KTextEditor::Cursor(line, col + len))); emit textRemoved(this, KTextEditor::Range(line, col, line, col + len), oldText); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editMarkLineAutoWrapped(int line, bool autowrapped) { // verbose debug EDIT_DEBUG << "editMarkLineAutoWrapped" << line << autowrapped; if (line < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); m_undoManager->slotMarkLineAutoWrapped(line, autowrapped); l->setAutoWrapped(autowrapped); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editWrapLine(int line, int col, bool newLine, bool *newLineAdded) { // verbose debug EDIT_DEBUG << "editWrapLine" << line << col << newLine; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); Kate::TextLine nextLine = kateTextLine(line + 1); const int length = l->length(); m_undoManager->slotLineWrapped(line, col, length - col, (!nextLine || newLine)); if (!nextLine || newLine) { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { if ((col == 0) || (i.value()->line > line)) { list.append(i.value()); } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // yes, we added a new line ! if (newLineAdded) { (*newLineAdded) = true; } } else { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); m_buffer->unwrapLine(line + 2); // no, no new line added ! if (newLineAdded) { (*newLineAdded) = false; } } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textInserted(this, KTextEditor::Range(line, col, line + 1, 0)); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editUnWrapLine(int line, bool removeLine, int length) { // verbose debug EDIT_DEBUG << "editUnWrapLine" << line << removeLine << length; if (line < 0 || length < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); Kate::TextLine nextLine = kateTextLine(line + 1); if (!l || !nextLine) { return false; } editStart(); int col = l->length(); m_undoManager->slotLineUnWrapped(line, col, length, removeLine); if (removeLine) { m_buffer->unwrapLine(line + 1); } else { m_buffer->wrapLine(KTextEditor::Cursor(line + 1, length)); m_buffer->unwrapLine(line + 1); } QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line + 1) { list.append(i.value()); } if (i.value()->line == line + 1) { KTextEditor::Mark *mark = m_marks.take(line); if (mark) { i.value()->type |= mark->type; } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line--; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textRemoved(this, KTextEditor::Range(line, col, line + 1, 0), QStringLiteral("\n")); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertLine(int line, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertLine" << line << s; if (line < 0) { return false; } if (!isReadWrite()) { return false; } if (line > lines()) { return false; } editStart(); m_undoManager->slotLineInserted(line, s); // wrap line if (line > 0) { Kate::TextLine previousLine = m_buffer->line(line - 1); m_buffer->wrapLine(KTextEditor::Cursor(line - 1, previousLine->text().size())); } else { m_buffer->wrapLine(KTextEditor::Cursor(0, 0)); } // insert text m_buffer->insertText(KTextEditor::Cursor(line, 0), s); Kate::TextLine tl = m_buffer->line(line); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { list.append(i.value()); } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeInserted(line, 0, line, tl->length()); if (line) { Kate::TextLine prevLine = plainKateTextLine(line - 1); rangeInserted.setStart(KTextEditor::Cursor(line - 1, prevLine->length())); } else { rangeInserted.setEnd(KTextEditor::Cursor(line + 1, 0)); } // remember last change cursor m_editLastChangeStartCursor = rangeInserted.start(); emit textInserted(this, rangeInserted); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveLine(int line) { return editRemoveLines(line, line); } bool KTextEditor::DocumentPrivate::editRemoveLines(int from, int to) { // verbose debug EDIT_DEBUG << "editRemoveLines" << from << to; if (to < from || from < 0 || to > lastLine()) { return false; } if (!isReadWrite()) { return false; } if (lines() == 1) { return editRemoveText(0, 0, kateTextLine(0)->length()); } editStart(); QStringList oldText; /** * first remove text */ for (int line = to; line >= from; --line) { Kate::TextLine tl = m_buffer->line(line); oldText.prepend(this->line(line)); m_undoManager->slotLineRemoved(line, this->line(line)); m_buffer->removeText(KTextEditor::Range(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, tl->text().size()))); } /** * then collapse lines */ for (int line = to; line >= from; --line) { /** * unwrap all lines, prefer to unwrap line behind, skip to wrap line 0 */ if (line + 1 < m_buffer->lines()) { m_buffer->unwrapLine(line + 1); } else if (line) { m_buffer->unwrapLine(line); } } QList rmark; QList list; foreach (KTextEditor::Mark *mark, m_marks) { int line = mark->line; if (line > to) { list << line; } else if (line >= from) { rmark << line; } } foreach (int line, rmark) { delete m_marks.take(line); } foreach (int line, list) { KTextEditor::Mark *mark = m_marks.take(line); mark->line -= to - from + 1; m_marks.insert(mark->line, mark); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeRemoved(from, 0, to + 1, 0); if (to == lastLine() + to - from + 1) { rangeRemoved.setEnd(KTextEditor::Cursor(to, oldText.last().length())); if (from > 0) { Kate::TextLine prevLine = plainKateTextLine(from - 1); rangeRemoved.setStart(KTextEditor::Cursor(from - 1, prevLine->length())); } } // remember last change cursor m_editLastChangeStartCursor = rangeRemoved.start(); emit textRemoved(this, rangeRemoved, oldText.join(QStringLiteral("\n")) + QLatin1Char('\n')); editEnd(); return true; } //END //BEGIN KTextEditor::UndoInterface stuff uint KTextEditor::DocumentPrivate::undoCount() const { return m_undoManager->undoCount(); } uint KTextEditor::DocumentPrivate::redoCount() const { return m_undoManager->redoCount(); } void KTextEditor::DocumentPrivate::undo() { m_undoManager->undo(); } void KTextEditor::DocumentPrivate::redo() { m_undoManager->redo(); } //END //BEGIN KTextEditor::SearchInterface stuff QVector KTextEditor::DocumentPrivate::searchText( const KTextEditor::Range &range, const QString &pattern, const KTextEditor::SearchOptions options) const { const bool escapeSequences = options.testFlag(KTextEditor::EscapeSequences); const bool regexMode = options.testFlag(KTextEditor::Regex); const bool backwards = options.testFlag(KTextEditor::Backwards); const bool wholeWords = options.testFlag(KTextEditor::WholeWords); const Qt::CaseSensitivity caseSensitivity = options.testFlag(KTextEditor::CaseInsensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive; if (regexMode) { // regexp search // escape sequences are supported by definition KateRegExpSearch searcher(this, caseSensitivity); return searcher.search(pattern, range, backwards); } if (escapeSequences) { // escaped search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(KateRegExpSearch::escapePlaintext(pattern), range, backwards); QVector result; result.append(match); return result; } // plaintext search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(pattern, range, backwards); QVector result; result.append(match); return result; } //END QWidget *KTextEditor::DocumentPrivate::dialogParent() { QWidget *w = widget(); if (!w) { w = activeView(); if (!w) { w = QApplication::activeWindow(); } } return w; } //BEGIN KTextEditor::HighlightingInterface stuff bool KTextEditor::DocumentPrivate::setMode(const QString &name) { updateFileType(name); return true; } KTextEditor::DefaultStyle KTextEditor::DocumentPrivate::defaultStyleAt(const KTextEditor::Cursor &position) const { // TODO, FIXME KDE5: in surrogate, use 2 bytes before if (! isValidTextPosition(position)) { return dsNormal; } int ds = const_cast(this)-> defStyleNum(position.line(), position.column()); if (ds < 0 || ds > static_cast(dsError)) { return dsNormal; } return static_cast(ds); } QString KTextEditor::DocumentPrivate::mode() const { return m_fileType; } QStringList KTextEditor::DocumentPrivate::modes() const { QStringList m; const QList &modeList = KTextEditor::EditorPrivate::self()->modeManager()->list(); foreach (KateFileType *type, modeList) { m << type->name; } return m; } bool KTextEditor::DocumentPrivate::setHighlightingMode(const QString &name) { int mode = KateHlManager::self()->nameFind(name); if (mode == -1) { return false; } m_buffer->setHighlight(mode); return true; } QString KTextEditor::DocumentPrivate::highlightingMode() const { return highlight()->name(); } QStringList KTextEditor::DocumentPrivate::highlightingModes() const { QStringList hls; for (const auto &hl : KateHlManager::self()->modeList()) { hls << hl.name(); } return hls; } QString KTextEditor::DocumentPrivate::highlightingModeSection(int index) const { return KateHlManager::self()->modeList().at(index).section(); } QString KTextEditor::DocumentPrivate::modeSection(int index) const { return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section; } void KTextEditor::DocumentPrivate::bufferHlChanged() { // update all views makeAttribs(false); // deactivate indenter if necessary m_indenter->checkRequiredStyle(); emit highlightingModeChanged(this); } void KTextEditor::DocumentPrivate::setDontChangeHlOnSave() { m_hlSetByUser = true; } void KTextEditor::DocumentPrivate::bomSetByUser() { m_bomSetByUser = true; } //END //BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff void KTextEditor::DocumentPrivate::readSessionConfig(const KConfigGroup &kconfig, const QSet &flags) { if (!flags.contains(QStringLiteral("SkipEncoding"))) { // get the encoding QString tmpenc = kconfig.readEntry("Encoding"); if (!tmpenc.isEmpty() && (tmpenc != encoding())) { setEncoding(tmpenc); } } if (!flags.contains(QStringLiteral("SkipUrl"))) { // restore the url QUrl url(kconfig.readEntry("URL")); // open the file if url valid if (!url.isEmpty() && url.isValid()) { openUrl(url); } else { completed(); //perhaps this should be emitted at the end of this function } } else { completed(); //perhaps this should be emitted at the end of this function } if (!flags.contains(QStringLiteral("SkipMode"))) { // restore the filetype if (kconfig.hasKey("Mode")) { updateFileType(kconfig.readEntry("Mode", fileType())); // restore if set by user, too! m_fileTypeSetByUser = kconfig.readEntry("Mode Set By User", false); } } if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // restore the hl stuff if (kconfig.hasKey("Highlighting")) { const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting")); if (mode >= 0) { m_buffer->setHighlight(mode); // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save m_hlSetByUser = kconfig.readEntry("Highlighting Set By User", false); } } } // indent mode config()->setIndentationMode(kconfig.readEntry("Indentation Mode", config()->indentationMode())); // Restore Bookmarks const QList marks = kconfig.readEntry("Bookmarks", QList()); for (int i = 0; i < marks.count(); i++) { addMark(marks.at(i), KTextEditor::DocumentPrivate::markType01); } } void KTextEditor::DocumentPrivate::writeSessionConfig(KConfigGroup &kconfig, const QSet &flags) { if (this->url().isLocalFile()) { const QString path = this->url().toLocalFile(); if (path.startsWith(QDir::tempPath())) { return; // inside tmp resource, do not save } } if (!flags.contains(QStringLiteral("SkipUrl"))) { // save url kconfig.writeEntry("URL", this->url().toString()); } if (!flags.contains(QStringLiteral("SkipEncoding"))) { // save encoding kconfig.writeEntry("Encoding", encoding()); } if (!flags.contains(QStringLiteral("SkipMode"))) { // save file type kconfig.writeEntry("Mode", m_fileType); // save if set by user, too! kconfig.writeEntry("Mode Set By User", m_fileTypeSetByUser); } if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // save hl kconfig.writeEntry("Highlighting", highlight()->name()); // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser); } // indent mode kconfig.writeEntry("Indentation Mode", config()->indentationMode()); // Save Bookmarks QList marks; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) if (i.value()->type & KTextEditor::MarkInterface::markType01) { marks << i.value()->line; } kconfig.writeEntry("Bookmarks", marks); } //END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff uint KTextEditor::DocumentPrivate::mark(int line) { KTextEditor::Mark *m = m_marks.value(line); if (!m) { return 0; } return m->type; } void KTextEditor::DocumentPrivate::setMark(int line, uint markType) { clearMark(line); addMark(line, markType); } void KTextEditor::DocumentPrivate::clearMark(int line) { if (line < 0 || line > lastLine()) { return; } if (!m_marks.value(line)) { return; } KTextEditor::Mark *mark = m_marks.take(line); emit markChanged(this, *mark, MarkRemoved); emit marksChanged(this); delete mark; tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::addMark(int line, uint markType) { KTextEditor::Mark *mark; if (line < 0 || line > lastLine()) { return; } if (markType == 0) { return; } if ((mark = m_marks.value(line))) { // Remove bits already set markType &= ~mark->type; if (markType == 0) { return; } // Add bits mark->type |= markType; } else { mark = new KTextEditor::Mark; mark->line = line; mark->type = markType; m_marks.insert(line, mark); } // Emit with a mark having only the types added. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkAdded); emit marksChanged(this); tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::removeMark(int line, uint markType) { if (line < 0 || line > lastLine()) { return; } KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } // Remove bits not set markType &= mark->type; if (markType == 0) { return; } // Subtract bits mark->type &= ~markType; // Emit with a mark having only the types removed. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkRemoved); if (mark->type == 0) { m_marks.remove(line); delete mark; } emit marksChanged(this); tagLines(line, line); repaintViews(true); } const QHash &KTextEditor::DocumentPrivate::marks() { return m_marks; } void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position) { KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } bool handled = false; emit markToolTipRequested(this, *mark, position, handled); } bool KTextEditor::DocumentPrivate::handleMarkClick(int line) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markClicked(this, KTextEditor::Mark{line, 0}, handled); } else { emit markClicked(this, *mark, handled); } return handled; } bool KTextEditor::DocumentPrivate::handleMarkContextMenu(int line, QPoint position) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markContextMenuRequested(this, KTextEditor::Mark{line, 0}, position, handled); } else { emit markContextMenuRequested(this, *mark, position, handled); } return handled; } void KTextEditor::DocumentPrivate::clearMarks() { while (!m_marks.isEmpty()) { QHash::iterator it = m_marks.begin(); KTextEditor::Mark mark = *it.value(); delete it.value(); m_marks.erase(it); emit markChanged(this, mark, MarkRemoved); tagLines(mark.line, mark.line); } m_marks.clear(); emit marksChanged(this); repaintViews(true); } void KTextEditor::DocumentPrivate::setMarkPixmap(MarkInterface::MarkTypes type, const QPixmap &pixmap) { m_markPixmaps.insert(type, pixmap); } void KTextEditor::DocumentPrivate::setMarkDescription(MarkInterface::MarkTypes type, const QString &description) { m_markDescriptions.insert(type, description); } QPixmap KTextEditor::DocumentPrivate::markPixmap(MarkInterface::MarkTypes type) const { return m_markPixmaps.value(type, QPixmap()); } QColor KTextEditor::DocumentPrivate::markColor(MarkInterface::MarkTypes type) const { uint reserved = (0x1 << KTextEditor::MarkInterface::reservedMarkersCount()) - 1; if ((uint)type >= (uint)markType01 && (uint)type <= reserved) { return KateRendererConfig::global()->lineMarkerColor(type); } else { return QColor(); } } QString KTextEditor::DocumentPrivate::markDescription(MarkInterface::MarkTypes type) const { return m_markDescriptions.value(type, QString()); } void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask) { m_editableMarks = markMask; } uint KTextEditor::DocumentPrivate::editableMarks() const { return m_editableMarks; } //END //BEGIN KTextEditor::PrintInterface stuff bool KTextEditor::DocumentPrivate::print() { return KatePrinter::print(this); } void KTextEditor::DocumentPrivate::printPreview() { KatePrinter::printPreview(this); } //END KTextEditor::PrintInterface stuff //BEGIN KTextEditor::DocumentInfoInterface (### unfinished) QString KTextEditor::DocumentPrivate::mimeType() { /** * collect first 4k of text * only heuristic */ QByteArray buf; for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) { buf.append(line(i).toUtf8()); buf.append('\n'); } // use path of url, too, if set if (!url().path().isEmpty()) { return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name(); } // else only use the content return QMimeDatabase().mimeTypeForData(buf).name(); } //END KTextEditor::DocumentInfoInterface //BEGIN: error void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess() { QPointer message = new KTextEditor::Message(i18n("The file %1 could not be loaded, as it was not possible to read from it.
Check if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)), KTextEditor::Message::Error); message->setWordWrap(true); QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"), nullptr); connect(tryAgainAction, SIGNAL(triggered()), SLOT(documentReload()), Qt::QueuedConnection); QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); // add try again and close actions message->addAction(tryAgainAction); message->addAction(closeAction); // finally post message postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 could not be loaded, as it was not possible to read from it.\n\nCheck if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)); } //END: error void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride() { // raise line length limit to the next power of 2 const int longestLine = m_buffer->longestLineLoaded(); int newLimit = pow(2, ceil(log2(longestLine))); if (newLimit <= longestLine) { newLimit *= 2; } // do the raise config()->setLineLengthLimit(newLimit); // just reload m_buffer->clear(); openFile(); if (!m_openingError) { setReadWrite(true); m_readWriteStateBeforeLoading = true; } } int KTextEditor::DocumentPrivate::lineLengthLimit() const { return config()->lineLengthLimit(); } //BEGIN KParts::ReadWrite stuff bool KTextEditor::DocumentPrivate::openFile() { /** * we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // no open errors until now... m_openingError = false; m_openingErrorMessage.clear (); // add new m_file to dirwatch activateDirWatch(); // remember current encoding QString currentEncoding = encoding(); // // mime type magic to get encoding right // QString mimeType = arguments().mimeType(); int pos = mimeType.indexOf(QLatin1Char(';')); if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) { setEncoding(mimeType.mid(pos + 1)); } // update file type, we do this here PRE-LOAD, therefore pass file name for reading from updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath())); // read dir config (if possible and wanted) // do this PRE-LOAD to get encoding info! readDirConfig(); // perhaps we need to re-set again the user encoding if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) { setEncoding(currentEncoding); } bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload)); // // yeah, success // read variables // if (success) { readVariables(); } // // update views // foreach (KTextEditor::ViewPrivate *view, m_views) { // This is needed here because inserting the text moves the view's start position (it is a MovingCursor) view->setCursorPosition(KTextEditor::Cursor()); view->updateView(true); } // Inform that the text has changed (required as we're not inside the usual editStart/End stuff) emit textChanged(this); emit loaded(this); // // to houston, we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // // display errors // if (!success) { showAndSetOpeningErrorAccess(); } // warn: broken encoding if (m_buffer->brokenEncoding()) { // this file can't be saved again without killing it setReadWrite(false); m_readWriteStateBeforeLoading=false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened with %2 encoding but contained invalid characters.
" "It is set to read-only mode, as saving might destroy its content.
" "Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())), KTextEditor::Message::Warning); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 was opened with %2 encoding but contained invalid characters." " It is set to read-only mode, as saving might destroy its content." " Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())); } // warn: too long lines if (m_buffer->tooLongLinesWrapped()) { // this file can't be saved again without modifications setReadWrite(false); m_readWriteStateBeforeLoading = false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(), m_buffer->longestLineLoaded()), KTextEditor::Message::Warning); QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message); connect(increaseAndReload, SIGNAL(triggered()), this, SLOT(openWithLineLengthLimitOverride())); message->addAction(increaseAndReload, true); message->addAction(new QAction(i18n("Close"), message), true); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(),m_buffer->longestLineLoaded()); } // // return the success // return success; } bool KTextEditor::DocumentPrivate::saveFile() { // delete pending mod-on-hd message if applicable. delete m_modOnHdHandler; // some warnings, if file was changed by the outside! if (!url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { QString str = reasonedMOHString() + QLatin1String("\n\n"); if (!isModified()) { if (KMessageBox::warningContinueCancel(dialogParent(), str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."), i18n("Trying to Save Unmodified File"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } else { if (KMessageBox::warningContinueCancel(dialogParent(), str + i18n("Do you really want to save this file? Both your open file and the file on disk were changed. There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } } } // // can we encode it if we want to save it ? // if (!m_buffer->canEncode() && (KMessageBox::warningContinueCancel(dialogParent(), i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save it? There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue)) { return false; } /** * create a backup file or abort if that fails! * if no backup file wanted, this routine will just return true */ if (!createBackupFile()) return false; // update file type, pass no file path, read file type content from this document QString oldPath = m_dirWatchFile; // only update file type if path has changed so that variables are not overridden on normal save if (oldPath != localFilePath()) { updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString())); if (url().isLocalFile()) { // if file is local then read dir config for new path readDirConfig(); } } // read our vars readVariables(); // remove file from dirwatch deactivateDirWatch(); // remove all trailing spaces in the document (as edit actions) // NOTE: we need this as edit actions, since otherwise the edit actions // in the swap file recovery may happen at invalid cursor positions removeTrailingSpaces(); // // try to save // if (!m_buffer->saveFile(localFilePath())) { // add m_file again to dirwatch activateDirWatch(oldPath); KMessageBox::error(dialogParent(), i18n("The document could not be saved, as it was not possible to write to %1.\nCheck that you have write access to this file or that enough disk space is available.\nThe original file may be lost or damaged. Don't quit the application until the file is successfully written.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // update the checksum createDigest(); // add m_file again to dirwatch activateDirWatch(); // // we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // (dominik) mark last undo group as not mergeable, otherwise the next // edit action might be merged and undo will never stop at the saved state m_undoManager->undoSafePoint(); m_undoManager->updateLineModifications(); // // return success // return true; } bool KTextEditor::DocumentPrivate::createBackupFile() { /** * backup for local or remote files wanted? */ const bool backupLocalFiles = config()->backupOnSaveLocal(); const bool backupRemoteFiles = config()->backupOnSaveRemote(); /** * early out, before mount check: backup wanted at all? * => if not, all fine, just return */ if (!backupLocalFiles && !backupRemoteFiles) { return true; } /** * decide if we need backup based on locality * skip that, if we always want backups, as currentMountPoints is not that fast */ QUrl u(url()); bool needBackup = backupLocalFiles && backupRemoteFiles; if (!needBackup) { bool slowOrRemoteFile = !u.isLocalFile(); if (!slowOrRemoteFile) { // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs) // we have the early out above to skip this, if we want no backup, which is the default KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile()); slowOrRemoteFile = (mountPoint && mountPoint->probablySlow()); } needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles); } /** * no backup needed? be done */ if (!needBackup) { return true; } /** * else: try to backup */ if (config()->backupPrefix().contains(QDir::separator())) { /** * replace complete path, as prefix is a path! */ u.setPath(config()->backupPrefix() + u.fileName() + config()->backupSuffix()); } else { /** * replace filename in url */ const QString fileName = u.fileName(); u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + config()->backupPrefix() + fileName + config()->backupSuffix()); } qCDebug(LOG_KTE) << "backup src file name: " << url(); qCDebug(LOG_KTE) << "backup dst file name: " << u; // handle the backup... bool backupSuccess = false; // local file mode, no kio if (u.isLocalFile()) { if (QFile::exists(url().toLocalFile())) { // first: check if backupFile is already there, if true, unlink it QFile backupFile(u.toLocalFile()); if (backupFile.exists()) { backupFile.remove(); } backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile()); } else { backupSuccess = true; } } else { // remote file mode, kio // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); if (statJob->exec()) { // do a evil copy which will overwrite target if possible KFileItem item(statJob->statResult(), url()); KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); backupSuccess = job->exec(); } else { backupSuccess = true; } } // backup has failed, ask user how to proceed if (!backupSuccess && (KMessageBox::warningContinueCancel(dialogParent() , i18n("For file %1 no backup copy could be created before saving." " If an error occurs while saving, you might lose the data of this file." " A reason could be that the media you write to is full or the directory of the file is read-only for you.", url().toDisplayString(QUrl::PreferLocalFile)) , i18n("Failed to create backup copy.") , KGuiItem(i18n("Try to Save Nevertheless")) , KStandardGuiItem::cancel(), QStringLiteral("Backup Failed Warning")) != KMessageBox::Continue)) { return false; } return true; } void KTextEditor::DocumentPrivate::readDirConfig() { if (!url().isLocalFile()) { return; } /** * first search .kateconfig upwards * with recursion guard */ QSet seenDirectories; QDir dir (QFileInfo(localFilePath()).absolutePath()); while (!seenDirectories.contains (dir.absolutePath ())) { /** * fill recursion guard */ seenDirectories.insert (dir.absolutePath ()); // try to open config file in this dir QFile f(dir.absolutePath () + QLatin1String("/.kateconfig")); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); uint linesRead = 0; QString line = stream.readLine(); while ((linesRead < 32) && !line.isNull()) { readVariableLine(line); line = stream.readLine(); linesRead++; } return; } /** * else: cd up, if possible or abort */ if (!dir.cdUp()) { break; } } #if EDITORCONFIG_FOUND // if there wasn’t any .kateconfig file and KTextEditor was compiled with // EditorConfig support, try to load document config from a .editorconfig // file, if such is provided EditorConfig editorConfig(this); editorConfig.parse(); #endif } void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName) { QString fileToUse = useFileName; if (fileToUse.isEmpty()) { fileToUse = localFilePath(); } QFileInfo fileInfo = QFileInfo(fileToUse); if (fileInfo.isSymLink()) { // Monitor the actual data and not the symlink fileToUse = fileInfo.canonicalFilePath(); } // same file as we are monitoring, return if (fileToUse == m_dirWatchFile) { return; } // remove the old watched file deactivateDirWatch(); // add new file if needed if (url().isLocalFile() && !fileToUse.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->addFile(fileToUse); m_dirWatchFile = fileToUse; } } void KTextEditor::DocumentPrivate::deactivateDirWatch() { if (!m_dirWatchFile.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->removeFile(m_dirWatchFile); } m_dirWatchFile.clear(); } bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url) { if (!m_reloading) { // Reset filetype when opening url m_fileTypeSetByUser = false; } bool res = KTextEditor::Document::openUrl(normalizeUrl(url)); updateDocName(); return res; } bool KTextEditor::DocumentPrivate::closeUrl() { // // file mod on hd // if (!m_reloading && !url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { // make sure to not forget pending mod-on-hd handler delete m_modOnHdHandler; QWidget *parentWidget(dialogParent()); if (!(KMessageBox::warningContinueCancel( parentWidget, reasonedMOHString() + QLatin1String("\n\n") + i18n("Do you really want to continue to close this file? Data loss may occur."), i18n("Possible Data Loss"), KGuiItem(i18n("Close Nevertheless")), KStandardGuiItem::cancel(), QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason)) == KMessageBox::Continue)) { /** * reset reloading */ m_reloading = false; return false; } } } // // first call the normal kparts implementation // if (!KParts::ReadWritePart::closeUrl()) { /** * reset reloading */ m_reloading = false; return false; } // Tell the world that we're about to go ahead with the close if (!m_reloading) { emit aboutToClose(this); } /** * delete all KTE::Messages */ if (!m_messageHash.isEmpty()) { QList keys = m_messageHash.keys(); foreach (KTextEditor::Message *message, keys) { delete message; } } /** * we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // remove file from dirwatch deactivateDirWatch(); // // empty url + fileName // setUrl(QUrl()); setLocalFilePath(QString()); // we are not modified if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // remove all marks clearMarks(); // clear the buffer m_buffer->clear(); // clear undo/redo history m_undoManager->clearUndo(); m_undoManager->clearRedo(); // no, we are no longer modified setModified(false); // we have no longer any hl m_buffer->setHighlight(0); // update all our views foreach (KTextEditor::ViewPrivate *view, m_views) { view->clearSelection(); // fix bug #118588 view->clear(); } // purge swap file if (m_swapfile) { m_swapfile->fileClosed(); } // success return true; } bool KTextEditor::DocumentPrivate::isDataRecoveryAvailable() const { return m_swapfile && m_swapfile->shouldRecover(); } void KTextEditor::DocumentPrivate::recoverData() { if (isDataRecoveryAvailable()) { m_swapfile->recover(); } } void KTextEditor::DocumentPrivate::discardDataRecovery() { if (isDataRecoveryAvailable()) { m_swapfile->discard(); } } void KTextEditor::DocumentPrivate::setReadWrite(bool rw) { if (isReadWrite() == rw) { return; } KParts::ReadWritePart::setReadWrite(rw); foreach (KTextEditor::ViewPrivate *view, m_views) { view->slotUpdateUndo(); view->slotReadWriteChanged(); } emit readWriteChanged(this); } void KTextEditor::DocumentPrivate::setModified(bool m) { if (isModified() != m) { KParts::ReadWritePart::setModified(m); foreach (KTextEditor::ViewPrivate *view, m_views) { view->slotUpdateUndo(); } emit modifiedChanged(this); } m_undoManager->setModified(m); } //END //BEGIN Kate specific stuff ;) void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->renderer()->updateAttributes(); } if (needInvalidate) { m_buffer->invalidateHighlighting(); } foreach (KTextEditor::ViewPrivate *view, m_views) { view->tagAll(); view->updateView(true); } } // the attributes of a hl have changed, update void KTextEditor::DocumentPrivate::internalHlChanged() { makeAttribs(); } void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view) { Q_ASSERT (!m_views.contains(view)); m_views.insert(view, static_cast(view)); m_viewsCache.append(view); // apply the view & renderer vars from the file type if (!m_fileType.isEmpty()) { readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, true); } // apply the view & renderer vars from the file readVariables(true); setActiveView(view); } void KTextEditor::DocumentPrivate::removeView(KTextEditor::View *view) { Q_ASSERT (m_views.contains(view)); m_views.remove(view); m_viewsCache.removeAll(view); if (activeView() == view) { setActiveView(nullptr); } } void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view) { if (m_activeView == view) { return; } m_activeView = static_cast(view); } bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view) { // do we own the given view? return (m_views.contains(view)); } int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->toVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor &cursor) const { return toVirtualColumn(cursor.line(), cursor.column()); } int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->fromVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor &cursor) const { return fromVirtualColumn(cursor.line(), cursor.column()); } bool KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, const QString &realChars) { /** * filter out non-printable chars (convert to utf-32 to support surrogate pairs) */ const auto realUcs4Chars = realChars.toUcs4(); QVector ucs4Chars; Q_FOREACH (auto c, realUcs4Chars) if (QChar::isPrint(c) || c == QChar::fromLatin1('\t') || c == QChar::fromLatin1('\n') || c == QChar::fromLatin1('\r')) { ucs4Chars.append(c); } /** * no printable chars => nothing to insert! */ QString chars = QString::fromUcs4(ucs4Chars.data(), ucs4Chars.size()); if (chars.isEmpty()) { return false; } // auto bracket handling QChar closingBracket; if (view->config()->autoBrackets()) { // Check if entered closing bracket is already balanced const QChar typedChar = chars.at(0); const QChar openBracket = matchingStartBracket(typedChar); if (!openBracket.isNull()) { KTextEditor::Cursor curPos = view->cursorPosition(); if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123/*Which value may best?*/).isValid()) { // Do nothing view->cursorRight(); return true; } } // for newly inserted text: remember if we should auto add some bracket if (chars.size() == 1) { // we inserted a bracket? => remember the matching closing one closingBracket = matchingEndBracket(typedChar); // closing bracket for the autobracket we inserted earlier? if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) { // do nothing m_currentAutobraceRange.reset(nullptr); view->cursorRight(); return true; } } } // Treat some char also as "auto bracket" only when we have a selection if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) { const QChar typedChar = chars.at(0); if (view->config()->charsToEncloseSelection().contains(typedChar)) { // The unconditional mirroring cause no harm, but allows funny brackets closingBracket = typedChar.mirroredChar(); } } editStart(); /** * special handling if we want to add auto brackets to a selection */ if (view->selection() && !closingBracket.isNull()) { std::unique_ptr selectionRange(newMovingRange(view->selectionRange())); const int startLine = qMax(0, selectionRange->start().line()); const int endLine = qMin(selectionRange->end().line(), lastLine()); const bool blockMode = view->blockSelection() && (startLine != endLine); if (blockMode) { if (selectionRange->start().column() > selectionRange->end().column()) { // Selection was done from right->left, requires special setting to ensure the new // added brackets will not be part of the selection selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight); } // Add brackets to each line of the block const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column()); const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column()); const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn); for (int line = startLine; line <= endLine; ++line) { const KTextEditor::Range r(rangeOnLine(workingRange, line)); insertText(r.end(), QString(closingBracket)); view->slotTextInserted(view, r.end(), QString(closingBracket)); insertText(r.start(), chars); view->slotTextInserted(view, r.start(), chars); } } else { // No block, just add to start & end of selection insertText(selectionRange->end(), QString(closingBracket)); view->slotTextInserted(view, selectionRange->end(), QString(closingBracket)); insertText(selectionRange->start(), chars); view->slotTextInserted(view, selectionRange->start(), chars); } // Refesh selection view->setSelection(selectionRange->toRange()); view->setCursorPosition(selectionRange->end()); editEnd(); return true; } /** * normal handling */ if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } const KTextEditor::Cursor oldCur(view->cursorPosition()); const bool multiLineBlockMode = view->blockSelection() && view->selection(); if (view->currentInputMode()->overwrite()) { // blockmode multiline selection case: remove chars in every line const KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line(); const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine; const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition()); for (int line = endLine; line >= startLine; --line) { Kate::TextLine textLine = m_buffer->plainLine(line); Q_ASSERT(textLine); const int column = fromVirtualColumn(line, virtualColumn); KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine->length() - column)); // replace mode needs to know what was removed so it can be restored with backspace if (oldCur.column() < lineLength(line)) { QChar removed = characterAt(KTextEditor::Cursor(line, column)); view->currentInputMode()->overwrittenChar(removed); } removeText(r); } } chars = eventuallyReplaceTabs(view->cursorPosition(), chars); if (multiLineBlockMode) { KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = qMax(0, selectionRange.start().line()); const int endLine = qMin(selectionRange.end().line(), lastLine()); const int column = toVirtualColumn(selectionRange.end()); for (int line = endLine; line >= startLine; --line) { editInsertText(line, fromVirtualColumn(line, column), chars); } int newSelectionColumn = toVirtualColumn(view->cursorPosition()); selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)) , KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn))); view->setSelection(selectionRange); } else { insertText(view->cursorPosition(), chars); } /** * auto bracket handling for newly inserted text * we inserted a bracket? * => add the matching closing one to the view + input chars * try to preserve the cursor position */ bool skipAutobrace = closingBracket == QLatin1Char('\''); if (highlight() && skipAutobrace) { // skip adding ' in spellchecked areas, because those are text skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, view->cursorPosition() - Cursor{0, 1}); } const auto cursorPos(view->cursorPosition()); if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) { // skip auto quotes when these looks already balanced, bug 405089 Kate::TextLine textLine = m_buffer->plainLine(cursorPos.line()); // RegEx match quote, but not excaped quote, thanks to https://stackoverflow.com/a/11819111 const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?plainLine(cursorPos.line()); const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?document()->text({cursorPos, cursorPos + Cursor{0, 1}}).trimmed(); if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) { insertText(view->cursorPosition(), QString(closingBracket)); const auto insertedAt(view->cursorPosition()); view->setCursorPosition(cursorPos); m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor{0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand)); connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection); // add bracket to chars inserted! needed for correct signals + indent chars.append(closingBracket); } m_currentAutobraceClosingChar = closingBracket; } // end edit session here, to have updated HL in userTypedChar! editEnd(); // trigger indentation KTextEditor::Cursor b(view->cursorPosition()); m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1)); /** * inform the view about the original inserted chars */ view->slotTextInserted(view, oldCur, chars); return true; } void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View*, const KTextEditor::Cursor& newPos) { if ( m_currentAutobraceRange && ! m_currentAutobraceRange->toRange().contains(newPos) ) { m_currentAutobraceRange.clear(); } } void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent) { editStart(); if (!v->config()->persistentSelection() && v->selection()) { v->removeSelectedText(); v->clearSelection(); } // query cursor position KTextEditor::Cursor c = v->cursorPosition(); if (c.line() > lastLine()) { c.setLine(lastLine()); } if (c.line() < 0) { c.setLine(0); } int ln = c.line(); Kate::TextLine textLine = plainKateTextLine(ln); if (c.column() > textLine->length()) { c.setColumn(textLine->length()); } // first: wrap line editWrapLine(c.line(), c.column()); // end edit session here, to have updated HL in userTypedChar! editEnd(); // second: if "indent" is true, indent the new line, if needed... if (indent == KTextEditor::DocumentPrivate::Indent) { m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n')); } } void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor &cursor) { Kate::TextLine textLine = m_buffer->plainLine(cursor.line()); if (!textLine || (textLine->length() < 2)) { return; } uint col = cursor.column(); if (col > 0) { col--; } if ((textLine->length() - col) < 2) { return; } uint line = cursor.line(); QString s; //clever swap code if first character on the line swap right&left //otherwise left & right s.append(textLine->at(col + 1)); s.append(textLine->at(col)); //do the swap // do it right, never ever manipulate a textline editStart(); editRemoveText(line, col, 2); editInsertText(line, col, s); editEnd(); } void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character before vertical selection line by expanding the selection range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } uint col = qMax(c.column(), 0); uint line = qMax(c.line(), 0); if ((col == 0) && (line == 0)) { return; } const Kate::TextLine textLine = m_buffer->plainLine(line); // don't forget this check!!!! really!!!! if (!textLine) { return; } if (col > 0) { bool useNextBlock = false; if (config()->backspaceIndents()) { // backspace indents: erase to next indent position int colX = textLine->toVirtualColumn(col, config()->tabWidth()); int pos = textLine->firstChar(); if (pos > 0) { pos = textLine->toVirtualColumn(pos, config()->tabWidth()); } if (pos < 0 || pos >= (int)colX) { // only spaces on left side of cursor indent(KTextEditor::Range(line, 0, line, 0), -1); } else { useNextBlock = true; } } if (!config()->backspaceIndents() || useNextBlock) { KTextEditor::Cursor beginCursor(line, 0); KTextEditor::Cursor endCursor(line, col); if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior beginCursor.setColumn(col - 1); // move to left of surrogate pair if (!isValidTextPosition(beginCursor)) { Q_ASSERT(col >= 2); beginCursor.setColumn(col - 2); } } else { beginCursor.setColumn(view->textLayout(c)->previousCursorPosition(c.column())); } removeText(KTextEditor::Range(beginCursor, endCursor)); // in most cases cursor is moved by removeText, but we should do it manually // for past-end-of-line cursors in block mode view->setCursorPosition(beginCursor); } } else { // col == 0: wrap to previous line const Kate::TextLine textLine = m_buffer->plainLine(line - 1); if (line > 0 && textLine) { if (config()->wordWrap() && textLine->endsWith(QLatin1String(" "))) { // gg: in hard wordwrap mode, backspace must also eat the trailing space removeText(KTextEditor::Range(line - 1, textLine->length() - 1, line, 0)); } else { removeText(KTextEditor::Range(line - 1, textLine->length(), line, 0)); } } } if (m_currentAutobraceRange) { const auto r = m_currentAutobraceRange->toRange(); if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) { // start parenthesis removed and range length is 1, remove end as well del(view, view->cursorPosition()); m_currentAutobraceRange.clear(); } } } void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character after vertical selection line by expanding the selection range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } if (c.column() < (int) m_buffer->plainLine(c.line())->length()) { KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column())); removeText(KTextEditor::Range(c, endCursor)); } else if (c.line() < lastLine()) { removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0)); } } void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text) { static const QChar newLineChar(QLatin1Char('\n')); QString s = text; if (s.isEmpty()) { return; } int lines = s.count(newLineChar); m_undoManager->undoSafePoint(); editStart(); KTextEditor::Cursor pos = view->cursorPosition(); if (!view->config()->persistentSelection() && view->selection()) { pos = view->selectionRange().start(); if (view->blockSelection()) { pos = rangeOnLine(view->selectionRange(), pos.line()).start(); if (lines == 0) { s += newLineChar; s = s.repeated(view->selectionRange().numberOfLines() + 1); s.chop(1); } } view->removeSelectedText(); } if (config()->ovr()) { QStringList pasteLines = s.split(newLineChar); if (!view->blockSelection()) { int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length(); removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn)); } else { int maxi = qMin(pos.line() + pasteLines.count(), this->lines()); for (int i = pos.line(); i < maxi; ++i) { int pasteLength = pasteLines.at(i - pos.line()).length(); removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i)))); } } } insertText(pos, s, view->blockSelection()); editEnd(); // move cursor right for block select, as the user is moved right internal // even in that case, but user expects other behavior in block selection // mode ! // just let cursor stay, that was it before I changed to moving ranges! if (view->blockSelection()) { view->setCursorPositionInternal(pos); } if (config()->indentPastedText()) { KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0)); m_indenter->indent(view, range); } if (!view->blockSelection()) { emit charactersSemiInteractivelyInserted(pos, s); } m_undoManager->undoSafePoint(); } void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change) { if (!isReadWrite()) { return; } editStart(); m_indenter->changeIndent(range, change); editEnd(); } void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { m_indenter->indent(view, range); } void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &) { if (!isReadWrite()) { return; } int lineLen = line(view->cursorPosition().line()).length(); KTextEditor::Cursor c = view->cursorPosition(); editStart(); if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) { KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1); // replace mode needs to know what was removed so it can be restored with backspace QChar removed = line(view->cursorPosition().line()).at(r.start().column()); view->currentInputMode()->overwrittenChar(removed); removeText(r); } c = view->cursorPosition(); editInsertText(c.line(), c.column(), QStringLiteral("\t")); editEnd(); } /* Remove a given string at the beginning of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->startsWith(str); if (!there) { cursor.setColumn(textline->firstChar()); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Remove a given string at the end of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->endsWith(str); if (there) { cursor.setColumn(textline->length() - str.length()); } else { cursor.setColumn(textline->lastChar() - str.length() + 1); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Replace tabs by spaces in the given string, if enabled. */ QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor &cursorPos, const QString &str) const { const bool replacetabs = config()->replaceTabsDyn(); if ( ! replacetabs ) { return str; } const int indentWidth = config()->indentationWidth(); static const QLatin1Char tabChar('\t'); int column = cursorPos.column(); // The result will always be at least as long as the input QString result; result.reserve(str.size()); Q_FOREACH (const QChar ch, str) { if (ch == tabChar) { // Insert only enough spaces to align to the next indentWidth column // This fixes bug #340212 int spacesToInsert = indentWidth - (column % indentWidth); result += QStringLiteral(" ").repeated(spacesToInsert); column += spacesToInsert; } else { // Just keep all other typed characters as-is result += ch; ++column; } } return result; } /* Add to the current line a comment line mark at the beginning. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib) { QString commentLineMark = highlight()->getCommentSingleLineStart(attrib); int pos = -1; if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::StartOfLine) { pos = 0; commentLineMark += QLatin1Char(' '); } else { const Kate::TextLine l = kateTextLine(line); pos = l->firstChar(); } if (pos >= 0) { insertText(KTextEditor::Cursor(line, pos), commentLineMark); } } /* Remove from the current line a comment line mark at the beginning if there is one. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); editStart(); // Try to remove the long comment mark first bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark)); editEnd(); return removed; } /* Add to the current line a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib) { const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' '); const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib); editStart(); // Add the start comment mark insertText(KTextEditor::Cursor(line, 0), startCommentMark); // Go to the end of the line const int col = m_buffer->plainLine(line)->length(); // Add the stop comment mark insertText(KTextEditor::Cursor(line, col), stopCommentMark); editEnd(); } /* Remove from the current line a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib) { const QString shortStartCommentMark = highlight()->getCommentStart(attrib); const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' '); const QString shortStopCommentMark = highlight()->getCommentEnd(attrib); const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark; editStart(); // Try to remove the long start comment mark first const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark)); // Try to remove the long stop comment mark first const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark)); editEnd(); return (removedStart || removedStop); } /* Add to the current selection a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); KTextEditor::Range range = view->selectionRange(); if ((range.end().column() == 0) && (range.end().line() > 0)) { range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1))); } editStart(); if (!view->blockSelection()) { insertText(range.end(), endComment); insertText(range.start(), startComment); } else { for (int line = range.start().line(); line <= range.end().line(); line++) { KTextEditor::Range subRange = rangeOnLine(range, line); insertText(subRange.end(), endComment); insertText(subRange.start(), startComment); } } editEnd(); // selection automatically updated (MovingRange) } /* Add to the current selection a comment line mark at the beginning of each line. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { //const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); // if end of selection is in column 0 in last line, omit the last line if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { //insertText (z, 0, commentLineMark); addStartLineCommentToSingleLine(z, attrib); } editEnd(); // selection automatically updated (MovingRange) } bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col) { for (; line < (int)m_buffer->count(); line++) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->nextNonSpaceChar(col); if (col != -1) { return true; // Next non-space char found } col = 0; } // No non-space char found line = -1; col = -1; return false; } bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col) { while (true) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->previousNonSpaceChar(col); if (col != -1) { return true; } if (line == 0) { return false; } --line; col = textLine->length(); } // No non-space char found line = -1; col = -1; return false; } /* Remove from the selection a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); int sl = qMax (0, view->selectionRange().start().line()); int el = qMin (view->selectionRange().end().line(), lastLine()); int sc = view->selectionRange().start().column(); int ec = view->selectionRange().end().column(); // The selection ends on the char before selectEnd if (ec != 0) { --ec; } else if (el > 0) { --el; ec = m_buffer->plainLine(el)->length() - 1; } const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/ bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl)->matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec) && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el)->matchesAt(ec - endCommentLen + 1, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1)); removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen)); editEnd(); // selection automatically updated (MovingRange) } return remove; } bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor &start, const KTextEditor::Cursor &end, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); const bool remove = m_buffer->plainLine(start.line())->matchesAt(start.column(), startComment) && m_buffer->plainLine(end.line())->matchesAt(end.column() - endCommentLen, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column())); removeText(KTextEditor::Range(start, startCommentLen)); editEnd(); } return remove; } /* Remove from the beginning of each line of the selection a start comment line mark. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } bool removed = false; editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { // Try to remove the long comment mark first removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed); } editEnd(); // selection automatically updated (MovingRange) return removed; } /* Comment or uncomment the selection or the current line if there is no selection. */ void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, int change) { // skip word wrap bug #105373 const bool skipWordWrap = wordWrap(); if (skipWordWrap) { setWordWrap(false); } bool hassel = v->selection(); int c = 0; if (hassel) { c = v->selectionRange().start().column(); } int startAttrib = 0; Kate::TextLine ln = kateTextLine(line); if (c < ln->length()) { startAttrib = ln->attribute(c); } else if (!ln->attributesList().isEmpty()) { startAttrib = ln->attributesList().back().attributeValue; } bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty()); bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty())); if (change > 0) { // comment if (!hassel) { if (hasStartLineCommentMark) { addStartLineCommentToSingleLine(line, startAttrib); } else if (hasStartStopCommentMark) { addStartStopCommentToSingleLine(line, startAttrib); } } else { // anders: prefer single line comment to avoid nesting probs // If the selection starts after first char in the first line // or ends before the last char of the last line, we may use // multiline comment markers. // TODO We should try to detect nesting. // - if selection ends at col 0, most likely she wanted that // line ignored const KTextEditor::Range sel = v->selectionRange(); if (hasStartStopCommentMark && (!hasStartLineCommentMark || ( (sel.start().column() > m_buffer->plainLine(sel.start().line())->firstChar()) || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line())->length())) ))) { addStartStopCommentToSelection(v, startAttrib); } else if (hasStartLineCommentMark) { addStartLineCommentToSelection(v, startAttrib); } } } else { // uncomment bool removed = false; if (!hassel) { removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib)) || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib)); } else { // anders: this seems like it will work with above changes :) removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(v, startAttrib)) || (hasStartLineCommentMark && removeStartLineCommentFromSelection(v, startAttrib)); } // recursive call for toggle comment if (!removed && change == 0) { comment(v, line, column, 1); } } if (skipWordWrap) { setWordWrap(true); // see begin of function ::comment (bug #105373) } } void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor &c, KTextEditor::DocumentPrivate::TextTransform t) { if (v->selection()) { editStart(); // cache the selection and cursor, so we can be sure to restore. KTextEditor::Range selection = v->selectionRange(); KTextEditor::Range range(selection.start(), 0); while (range.start().line() <= selection.end().line()) { int start = 0; int end = lineLength(range.start().line()); if (range.start().line() == selection.start().line() || v->blockSelection()) { start = selection.start().column(); } if (range.start().line() == selection.end().line() || v->blockSelection()) { end = selection.end().column(); } if (start > end) { int swapCol = start; start = end; end = swapCol; } range.setStart(KTextEditor::Cursor(range.start().line(), start)); range.setEnd(KTextEditor::Cursor(range.end().line(), end)); QString s = text(range); QString old = s; if (t == Uppercase) { s = s.toUpper(); } else if (t == Lowercase) { s = s.toLower(); } else { // Capitalize Kate::TextLine l = m_buffer->plainLine(range.start().line()); int p(0); while (p < s.length()) { // If bol or the character before is not in a word, up this one: // 1. if both start and p is 0, upper char. // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper // 3. if p-1 is not in a word, upper. if ((! range.start().column() && ! p) || ((range.start().line() == selection.start().line() || v->blockSelection()) && ! p && ! highlight()->isInWord(l->at(range.start().column() - 1))) || (p && ! highlight()->isInWord(s.at(p - 1))) ) { s[p] = s.at(p).toUpper(); } p++; } } if (s != old) { removeText(range); insertText(range.start(), s); } range.setBothLines(range.start().line() + 1); } editEnd(); // restore selection & cursor v->setSelection(selection); v->setCursorPosition(c); } else { // no selection editStart(); // get cursor KTextEditor::Cursor cursor = c; QString old = text(KTextEditor::Range(cursor, 1)); QString s; switch (t) { case Uppercase: s = old.toUpper(); break; case Lowercase: s = old.toLower(); break; case Capitalize: { Kate::TextLine l = m_buffer->plainLine(cursor.line()); while (cursor.column() > 0 && highlight()->isInWord(l->at(cursor.column() - 1), l->attribute(cursor.column() - 1))) { cursor.setColumn(cursor.column() - 1); } old = text(KTextEditor::Range(cursor, 1)); s = old.toUpper(); } break; default: break; } removeText(KTextEditor::Range(cursor, 1)); insertText(cursor, s); editEnd(); } } void KTextEditor::DocumentPrivate::joinLines(uint first, uint last) { // if ( first == last ) last += 1; editStart(); int line(first); while (first < last) { // Normalize the whitespace in the joined lines by making sure there's // always exactly one space between the joined lines // This cannot be done in editUnwrapLine, because we do NOT want this // behavior when deleting from the start of a line, just when explicitly // calling the join command Kate::TextLine l = kateTextLine(line); Kate::TextLine tl = kateTextLine(line + 1); if (!l || !tl) { editEnd(); return; } int pos = tl->firstChar(); if (pos >= 0) { if (pos != 0) { editRemoveText(line + 1, 0, pos); } if (!(l->length() == 0 || l->at(l->length() - 1).isSpace())) { editInsertText(line + 1, 0, QLatin1String(" ")); } } else { // Just remove the whitespace and let Kate handle the rest editRemoveText(line + 1, 0, tl->length()); } editUnWrapLine(line); first++; } editEnd(); } void KTextEditor::DocumentPrivate::tagLines(int start, int end) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->tagLines(start, end, true); } } void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->repaintText(paintOnlyDirty); } } /* Bracket matching uses the following algorithm: If in overwrite mode, match the bracket currently underneath the cursor. Otherwise, if the character to the left is a bracket, match it. Otherwise if the character to the right of the cursor is a bracket, match it. Otherwise, don't match anything. */ KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor &start, int maxLines) { if (maxLines < 0) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = m_buffer->plainLine(start.line()); if (!textLine) { return KTextEditor::Range::invalid(); } KTextEditor::Range range(start, start); const QChar right = textLine->at(range.start().column()); const QChar left = textLine->at(range.start().column() - 1); QChar bracket; if (config()->ovr()) { if (isBracket(right)) { bracket = right; } else { return KTextEditor::Range::invalid(); } } else if (isBracket(right)) { bracket = right; } else if (isBracket(left)) { range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); bracket = left; } else { return KTextEditor::Range::invalid(); } const QChar opposite = matchingBracket(bracket); if (opposite.isNull()) { return KTextEditor::Range::invalid(); } const int searchDir = isStartBracket(bracket) ? 1 : -1; uint nesting = 0; const int minLine = qMax(range.start().line() - maxLines, 0); const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line()); range.setEnd(range.start()); KTextEditor::DocumentCursor cursor(this); cursor.setPosition(range.start()); int validAttr = kateTextLine(cursor.line())->attribute(cursor.column()); while (cursor.line() >= minLine && cursor.line() <= maxLine) { if (!cursor.move(searchDir)) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = kateTextLine(cursor.line()); if (textLine->attribute(cursor.column()) == validAttr) { // Check for match QChar c = textLine->at(cursor.column()); if (c == opposite) { if (nesting == 0) { if (searchDir > 0) { // forward range.setEnd(cursor.toCursor()); } else { range.setStart(cursor.toCursor()); } return range; } nesting--; } else if (c == bracket) { nesting++; } } } return KTextEditor::Range::invalid(); } // helper: remove \r and \n from visible document name (bug #170876) inline static QString removeNewLines(const QString &str) { QString tmp(str); return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")) .replace(QLatin1Char('\r'), QLatin1Char(' ')) .replace(QLatin1Char('\n'), QLatin1Char(' ')); } void KTextEditor::DocumentPrivate::updateDocName() { // if the name is set, and starts with FILENAME, it should not be changed! if (! url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) { return; } int count = -1; foreach (KTextEditor::DocumentPrivate *doc, KTextEditor::EditorPrivate::self()->kateDocuments()) { if ((doc != this) && (doc->url().fileName() == url().fileName())) if (doc->m_docNameNumber > count) { count = doc->m_docNameNumber; } } m_docNameNumber = count + 1; QString oldName = m_docName; m_docName = removeNewLines(url().fileName()); m_isUntitled = m_docName.isEmpty(); if (m_isUntitled) { m_docName = i18n("Untitled"); } if (m_docNameNumber > 0) { m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1); } /** * avoid to emit this, if name doesn't change! */ if (oldName != m_docName) { emit documentNameChanged(this); } } void KTextEditor::DocumentPrivate::slotModifiedOnDisk(KTextEditor::View * /*v*/) { if (url().isEmpty() || !m_modOnHd) { return; } if (!isModified() && isAutoReload()) { onModOnHdAutoReload(); return; } if (!m_fileChangedDialogsActivated || m_modOnHdHandler) { return; } // don't ask the user again and again the same thing if (m_modOnHdReason == m_prevModOnHdReason) { return; } m_prevModOnHdReason = m_modOnHdReason; m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString()); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore); } void KTextEditor::DocumentPrivate::onModOnHdSaveAs() { m_modOnHd = false; QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File"), url()); if (!res.isEmpty()) { if (! saveAs(res)) { KMessageBox::error(parentWidget, i18n("Save failed")); m_modOnHd = true; } else { delete m_modOnHdHandler; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); } } else { // the save as dialog was canceled, we are still modified on disk m_modOnHd = true; } } void KTextEditor::DocumentPrivate::onModOnHdClose() { // avoid prompt in closeUrl() m_fileChangedDialogsActivated = false; // close the file without prompt confirmation closeUrl(); // Useful for kate only closeDocumentInApplication(); } void KTextEditor::DocumentPrivate::onModOnHdReload() { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::autoReloadToggled(bool b) { m_autoReloadMode->setChecked(b); if (b) { connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } else { disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } } bool KTextEditor::DocumentPrivate::isAutoReload() { return m_autoReloadMode->isChecked(); } void KTextEditor::DocumentPrivate::delayAutoReload() { if (isAutoReload()) { m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdAutoReload() { if (m_modOnHdHandler) { delete m_modOnHdHandler; autoReloadToggled(true); } if (!isAutoReload()) { return; } if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdIgnore() { // ignore as long as m_prevModOnHdReason == m_modOnHdReason delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::setModifiedOnDisk(ModifiedOnDiskReason reason) { m_modOnHdReason = reason; m_modOnHd = (reason != OnDiskUnmodified); emit modifiedOnDisk(this, (reason != OnDiskUnmodified), reason); } class KateDocumentTmpMark { public: QString line; KTextEditor::Mark mark; }; void KTextEditor::DocumentPrivate::setModifiedOnDiskWarning(bool on) { m_fileChangedDialogsActivated = on; } bool KTextEditor::DocumentPrivate::documentReload() { if (url().isEmpty()) { return false; } // typically, the message for externally modified files is visible. Since it // does not make sense showing an additional dialog, just hide the message. delete m_modOnHdHandler; emit aboutToReload(this); QList tmp; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { KateDocumentTmpMark m; m.line = line(i.value()->line); m.mark = *i.value(); tmp.append(m); } const QString oldMode = mode(); const bool byUser = m_fileTypeSetByUser; const QString hl_mode = highlightingMode(); m_storedVariables.clear(); // save cursor positions for all views QHash cursorPositions; for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); cursorPositions.insert(v, v->cursorPosition()); } m_reloading = true; KTextEditor::DocumentPrivate::openUrl(url()); // reset some flags only valid for one reload! m_userSetEncodingForNextReload = false; // restore cursor positions for all views for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); setActiveView(v); v->setCursorPosition(cursorPositions.value(v)); if (v->isVisible()) { v->repaintText(false); } } for (int z = 0; z < tmp.size(); z++) { if (z < lines()) { if (line(tmp.at(z).mark.line) == tmp.at(z).line) { setMark(tmp.at(z).mark.line, tmp.at(z).mark.type); } } } if (byUser) { setMode(oldMode); } setHighlightingMode(hl_mode); emit reloaded(this); return true; } bool KTextEditor::DocumentPrivate::documentSave() { if (!url().isValid() || !isReadWrite()) { return documentSaveAs(); } return save(); } bool KTextEditor::DocumentPrivate::documentSaveAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding) { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } setEncoding(encoding); return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveCopyAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save Copy of File"), url()); if (saveUrl.isEmpty()) { return false; } QTemporaryFile file; if (!file.open()) { return false; } if (!m_buffer->saveFile(file.fileName())) { KMessageBox::error(dialogParent(), i18n("The document could not be saved, as it was not possible to write to %1.\n\nCheck that you have write access to this file or that enough disk space is available.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); int permissions = -1; if (statJob->exec()) { permissions = KFileItem(statJob->statResult(), url()).permissions(); } // KIO move, important: allow overwrite, we checked above! KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file.fileName()), saveUrl, permissions, KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); return job->exec(); } void KTextEditor::DocumentPrivate::setWordWrap(bool on) { config()->setWordWrap(on); } bool KTextEditor::DocumentPrivate::wordWrap() const { return config()->wordWrap(); } void KTextEditor::DocumentPrivate::setWordWrapAt(uint col) { config()->setWordWrapAt(col); } unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const { return config()->wordWrapAt(); } void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on) { config()->setPageUpDownMovesCursor(on); } bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const { return config()->pageUpDownMovesCursor(); } //END bool KTextEditor::DocumentPrivate::setEncoding(const QString &e) { return m_config->setEncoding(e); } QString KTextEditor::DocumentPrivate::encoding() const { return m_config->encoding(); } void KTextEditor::DocumentPrivate::updateConfig() { m_undoManager->updateConfig(); // switch indenter if needed and update config.... m_indenter->setMode(m_config->indentationMode()); m_indenter->updateConfig(); // set tab width there, too m_buffer->setTabWidth(config()->tabWidth()); // update all views, does tagAll and updateView... foreach (KTextEditor::ViewPrivate *view, m_views) { view->updateDocumentConfig(); } // update on-the-fly spell checking as spell checking defaults might have changes if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); } emit configChanged(); } //BEGIN Variable reader // "local variable" feature by anders, 2003 /* TODO add config options (how many lines to read, on/off) add interface for plugins/apps to set/get variables add view stuff */ void KTextEditor::DocumentPrivate::readVariables(bool onlyViewAndRenderer) { if (!onlyViewAndRenderer) { m_config->configStart(); } // views! KTextEditor::ViewPrivate *v; foreach (v, m_views) { v->config()->configStart(); v->renderer()->config()->configStart(); } // read a number of lines in the top/bottom of the document for (int i = 0; i < qMin(9, lines()); ++i) { readVariableLine(line(i), onlyViewAndRenderer); } if (lines() > 10) { for (int i = qMax(10, lines() - 10); i < lines(); i++) { readVariableLine(line(i), onlyViewAndRenderer); } } if (!onlyViewAndRenderer) { m_config->configEnd(); } foreach (v, m_views) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } void KTextEditor::DocumentPrivate::readVariableLine(QString t, bool onlyViewAndRenderer) { static const QRegularExpression kvLine(QStringLiteral("kate:(.*)")); static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)")); static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)")); static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)")); // simple check first, no regex // no kate inside, no vars, simple... if (!t.contains(QLatin1String("kate"))) { return; } // found vars, if any QString s; // now, try first the normal ones auto match = kvLine.match(t); if (match.hasMatch()) { s = match.captured(1); //qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s; } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); const QString nameOfFile = url().fileName(); bool found = false; foreach (const QString &pattern, wildcards) { - QRegExp wildcard(pattern, Qt::CaseSensitive, QRegExp::Wildcard); + QRegularExpression wildcard(QLatin1Char('^') + QRegularExpression::wildcardToRegularExpression(pattern) + QLatin1Char('$')); - found = wildcard.exactMatch(nameOfFile); + found = wildcard.match(nameOfFile).hasMatch(); if (found) { break; } } // nothing usable found... if (!found) { return; } s = match.captured(2); //qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s; } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given const QStringList types(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); // no matching type found if (!types.contains(mimeType())) { return; } s = match.captured(2); //qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s; } else { // nothing found return; } // view variable names static const auto vvl = { QLatin1String("dynamic-word-wrap") , QLatin1String("dynamic-word-wrap-indicators") , QLatin1String("line-numbers") , QLatin1String("icon-border") , QLatin1String("folding-markers") , QLatin1String("folding-preview") , QLatin1String("bookmark-sorting") , QLatin1String("auto-center-lines") , QLatin1String("icon-bar-color") , QLatin1String("scrollbar-minimap") , QLatin1String("scrollbar-preview") // renderer , QLatin1String("background-color") , QLatin1String("selection-color") , QLatin1String("current-line-color") , QLatin1String("bracket-highlight-color") , QLatin1String("word-wrap-marker-color") , QLatin1String("font") , QLatin1String("font-size") , QLatin1String("scheme") }; int spaceIndent = -1; // for backward compatibility; see below bool replaceTabsSet = false; int startPos(0); QString var, val; while ((match = kvVar.match(s, startPos)).hasMatch()) { startPos = match.capturedEnd(0); var = match.captured(1); val = match.captured(2).trimmed(); bool state; // store booleans here int n; // store ints here // only apply view & renderer config stuff if (onlyViewAndRenderer) { if (contains(vvl, var)) { // FIXME define above setViewVariable(var, val); } } else { // BOOL SETTINGS if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) { setWordWrap(state); // ??? FIXME CHECK } // KateConfig::configFlags // FIXME should this be optimized to only a few calls? how? else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) { m_config->setBackspaceIndents(state); } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) { m_config->setIndentPastedText(state); } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) { m_config->setReplaceTabsDyn(state); replaceTabsSet = true; // for backward compatibility; see below } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n("Using deprecated modeline 'remove-trailing-space'. " "Please replace with 'remove-trailing-spaces modified;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 1 : 0); } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n("Using deprecated modeline 'replace-trailing-space-save'. " "Please replace with 'remove-trailing-spaces all;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 2 : 0); } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) { m_config->setOvr(state); } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) { m_config->setKeepExtraSpaces(state); } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) { m_config->setTabIndents(state); } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) { m_config->setShowTabs(state); } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) { m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None); } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) { // this is for backward compatibility; see below spaceIndent = state; } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) { m_config->setSmartHome(state); } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) { m_config->setNewLineAtEof(state); } // INTEGER SETTINGS else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) { m_config->setTabWidth(n); } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) { m_config->setIndentationWidth(n); } else if (var == QLatin1String("indent-mode")) { m_config->setIndentationMode(val); } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;) m_config->setWordWrapAt(n); } // STRING SETTINGS else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) { const auto l = { QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac") }; if ((n = indexOf(l, val.toLower())) != -1) { /** * set eol + avoid that it is overwritten by auto-detection again! * this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705 */ m_config->setEol(n); m_config->setAllowEolDetection(false); } } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) { if (checkBoolValue(val, &state)) { m_config->setBom(state); } } else if (var == QLatin1String("remove-trailing-spaces")) { val = val.toLower(); if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) { m_config->setRemoveSpaces(1); } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) { m_config->setRemoveSpaces(2); } else { m_config->setRemoveSpaces(0); } } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) { setHighlightingMode(val); } else if (var == QLatin1String("mode")) { setMode(val); } else if (var == QLatin1String("encoding")) { setEncoding(val); } else if (var == QLatin1String("default-dictionary")) { setDefaultDictionary(val); } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) { onTheFlySpellCheckingEnabled(state); } // VIEW SETTINGS else if (contains(vvl, var)) { setViewVariable(var, val); } else { m_storedVariables.insert(var, val); } } } // Backward compatibility // If space-indent was set, but replace-tabs was not set, we assume // that the user wants to replace tabulators and set that flag. // If both were set, replace-tabs has precedence. // At this point spaceIndent is -1 if it was never set, // 0 if it was set to off, and 1 if it was set to on. // Note that if onlyViewAndRenderer was requested, spaceIndent is -1. if (!replaceTabsSet && spaceIndent >= 0) { m_config->setReplaceTabsDyn(spaceIndent > 0); } } void KTextEditor::DocumentPrivate::setViewVariable(QString var, QString val) { KTextEditor::ViewPrivate *v; bool state; int n; QColor c; foreach (v, m_views) { // First, try the new config interface QVariant help(val); // Special treatment to catch "on"/"off" if (checkBoolValue(val, &state)) { help = state; } if (v->config()->setValue(var, help)) { } else if (v->renderer()->config()->setValue(var, help)) { // No success? Go the old way } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) { v->config()->setDynWordWrap(state); } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) { v->setBlockSelection(state); //else if ( var = "dynamic-word-wrap-indicators" ) } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) { v->renderer()->config()->setIconBarColor(c); } // RENDERER else if (var == QLatin1String("background-color") && checkColorValue(val, c)) { v->renderer()->config()->setBackgroundColor(c); } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) { v->renderer()->config()->setSelectionColor(c); } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedLineColor(c); } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedBracketColor(c); } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) { v->renderer()->config()->setWordWrapMarkerColor(c); } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && var == QLatin1String("font-size"))) { QFont _f(v->renderer()->config()->font()); if (var == QLatin1String("font")) { _f.setFamily(val); _f.setFixedPitch(QFont(val).fixedPitch()); } else { _f.setPointSize(n); } v->renderer()->config()->setFont(_f); } else if (var == QLatin1String("scheme")) { v->renderer()->config()->setSchema(val); } } } bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result) { val = val.trimmed().toLower(); static const auto trueValues = { QLatin1String("1"), QLatin1String("on"), QLatin1String("true") }; if (contains(trueValues, val)) { *result = true; return true; } static const auto falseValues = { QLatin1String("0"), QLatin1String("off"), QLatin1String("false") }; if (contains(falseValues, val)) { *result = false; return true; } return false; } bool KTextEditor::DocumentPrivate::checkIntValue(QString val, int *result) { bool ret(false); *result = val.toInt(&ret); return ret; } bool KTextEditor::DocumentPrivate::checkColorValue(QString val, QColor &c) { c.setNamedColor(val); return c.isValid(); } // KTextEditor::variable QString KTextEditor::DocumentPrivate::variable(const QString &name) const { return m_storedVariables.value(name, QString()); } void KTextEditor::DocumentPrivate::setVariable(const QString &name, const QString &value) { QString s = QStringLiteral("kate: "); s.append(name); s.append(QLatin1Char(' ')); s.append(value); readVariableLine(s); } //END void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) { m_modOnHd = true; m_modOnHdReason = OnDiskModified; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) { m_modOnHd = true; m_modOnHdReason = OnDiskCreated; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) { m_modOnHd = true; m_modOnHdReason = OnDiskDeleted; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd() { // compare git hash with the one we have (if we have one) const QByteArray oldDigest = checksum(); if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) { /** * if current checksum == checksum of new file => unmodified */ if (m_modOnHdReason != OnDiskDeleted && createDigest() && oldDigest == checksum()) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; } #if LIBGIT2_FOUND /** * if still modified, try to take a look at git * skip that, if document is modified! * only do that, if the file is still there, else reload makes no sense! */ if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())) { /** * try to discover the git repo of this file * libgit2 docs state that UTF-8 is the right encoding, even on windows * I hope that is correct! */ git_repository *repository = nullptr; const QByteArray utf8Path = url().toLocalFile().toUtf8(); if (git_repository_open_ext(&repository, utf8Path.constData(), 0, nullptr) == 0) { /** * if we have repo, convert the git hash to an OID */ git_oid oid; if (git_oid_fromstr(&oid, oldDigest.toHex().data()) == 0) { /** * finally: is there a blob for this git hash? */ git_blob *blob = nullptr; if (git_blob_lookup(&blob, repository, &oid) == 0) { /** * this hash exists still in git => just reload */ m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; documentReload(); } git_blob_free(blob); } } git_repository_free(repository); } #endif } /** * emit our signal to the outside! */ emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } QByteArray KTextEditor::DocumentPrivate::checksum() const { return m_buffer->digest(); } bool KTextEditor::DocumentPrivate::createDigest() { QByteArray digest; if (url().isLocalFile()) { QFile f(url().toLocalFile()); if (f.open(QIODevice::ReadOnly)) { // init the hash with the git header QCryptographicHash crypto(QCryptographicHash::Sha1); const QString header = QStringLiteral("blob %1").arg(f.size()); crypto.addData(header.toLatin1() + '\0'); while (!f.atEnd()) { crypto.addData(f.read(256 * 1024)); } digest = crypto.result(); } } /** * set new digest */ m_buffer->setDigest(digest); return !digest.isEmpty(); } QString KTextEditor::DocumentPrivate::reasonedMOHString() const { // squeeze path const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile)); switch (m_modOnHdReason) { case OnDiskModified: return i18n("The file '%1' was modified by another program.", str); break; case OnDiskCreated: return i18n("The file '%1' was created by another program.", str); break; case OnDiskDeleted: return i18n("The file '%1' was deleted by another program.", str); break; default: return QString(); } Q_UNREACHABLE(); return QString(); } void KTextEditor::DocumentPrivate::removeTrailingSpaces() { const int remove = config()->removeSpaces(); if (remove == 0) { return; } // temporarily disable static word wrap (see bug #328900) const bool wordWrapEnabled = config()->wordWrap(); if (wordWrapEnabled) { setWordWrap(false); } editStart(); for (int line = 0; line < lines(); ++line) { Kate::TextLine textline = plainKateTextLine(line); // remove trailing spaces in entire document, remove = 2 // remove trailing spaces of touched lines, remove = 1 // remove trailing spaces of lines saved on disk, remove = 1 if (remove == 2 || textline->markedAsModified() || textline->markedAsSavedOnDisk()) { const int p = textline->lastChar() + 1; const int l = textline->length() - p; if (l > 0) { editRemoveText(line, p, l); } } } editEnd(); // enable word wrap again, if it was enabled (see bug #328900) if (wordWrapEnabled) { setWordWrap(true); // see begin of this function } } void KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user) { if (user || !m_fileTypeSetByUser) { if (!newType.isEmpty()) { // remember that we got set by user m_fileTypeSetByUser = user; m_fileType = newType; m_config->configStart(); // NOTE: if the user changes the Mode, the Highlighting also changes. // m_hlSetByUser avoids resetting the highlight when saving the document, if // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763) if ((user || !m_hlSetByUser) && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl.isEmpty()) { int hl(KateHlManager::self()->nameFind(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl)); if (hl >= 0) { m_buffer->setHighlight(hl); } } /** * set the indentation mode, if any in the mode... * and user did not set it before! * NOTE: KateBuffer::setHighlight() also sets the indentation. */ if (!m_indenterSetByUser && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter.isEmpty()) { config()->setIndentationMode(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter); } // views! KTextEditor::ViewPrivate *v; foreach (v, m_views) { v->config()->configStart(); v->renderer()->config()->configStart(); } bool bom_settings = false; if (m_bomSetByUser) { bom_settings = m_config->bom(); } readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).varLine); if (m_bomSetByUser) { m_config->setBom(bom_settings); } m_config->configEnd(); foreach (v, m_views) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } } // fixme, make this better... emit modeChanged(this); } void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing) { *handled = true; *abortClosing = true; if (this->url().isEmpty()) { QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File")); if (res.isEmpty()) { *abortClosing = true; return; } saveAs(res); *abortClosing = false; } else { save(); *abortClosing = false; } } //BEGIN KTextEditor::ConfigInterface // BEGIN ConfigInterface stff QStringList KTextEditor::DocumentPrivate::configKeys() const { /** * expose all internally registered keys of the KateDocumentConfig */ return m_config->configKeys(); } QVariant KTextEditor::DocumentPrivate::configValue(const QString &key) { /** * just dispatch to internal key => value lookup */ return m_config->value(key); } void KTextEditor::DocumentPrivate::setConfigValue(const QString &key, const QVariant &value) { /** * just dispatch to internal key + value set */ m_config->setValue(key, value); } //END KTextEditor::ConfigInterface KTextEditor::Cursor KTextEditor::DocumentPrivate::documentEnd() const { return KTextEditor::Cursor(lastLine(), lineLength(lastLine())); } bool KTextEditor::DocumentPrivate::replaceText(const KTextEditor::Range &range, const QString &s, bool block) { // TODO more efficient? editStart(); bool changed = removeText(range, block); changed |= insertText(range.start(), s, block); editEnd(); return changed; } KateHighlighting *KTextEditor::DocumentPrivate::highlight() const { return m_buffer->highlight(); } Kate::TextLine KTextEditor::DocumentPrivate::kateTextLine(int i) { m_buffer->ensureHighlighted(i); return m_buffer->plainLine(i); } Kate::TextLine KTextEditor::DocumentPrivate::plainKateTextLine(int i) { return m_buffer->plainLine(i); } bool KTextEditor::DocumentPrivate::isEditRunning() const { return editIsRunning; } void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge) { if (merge && m_undoMergeAllEdits) { // Don't add another undo safe point: it will override our current one, // meaning we'll need two undo's to get back there - which defeats the object! return; } m_undoManager->undoSafePoint(); m_undoManager->setAllowComplexMerge(merge); m_undoMergeAllEdits = merge; } //BEGIN KTextEditor::MovingInterface KTextEditor::MovingCursor *KTextEditor::DocumentPrivate::newMovingCursor(const KTextEditor::Cursor &position, KTextEditor::MovingCursor::InsertBehavior insertBehavior) { return new Kate::TextCursor(buffer(), position, insertBehavior); } KTextEditor::MovingRange *KTextEditor::DocumentPrivate::newMovingRange(const KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior) { return new Kate::TextRange(buffer(), range, insertBehaviors, emptyBehavior); } qint64 KTextEditor::DocumentPrivate::revision() const { return m_buffer->history().revision(); } qint64 KTextEditor::DocumentPrivate::lastSavedRevision() const { return m_buffer->history().lastSavedRevision(); } void KTextEditor::DocumentPrivate::lockRevision(qint64 revision) { m_buffer->history().lockRevision(revision); } void KTextEditor::DocumentPrivate::unlockRevision(qint64 revision) { m_buffer->history().unlockRevision(revision); } void KTextEditor::DocumentPrivate::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); } void KTextEditor::DocumentPrivate::transformCursor(KTextEditor::Cursor &cursor, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { int line = cursor.line(), column = cursor.column(); m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); cursor.setLine(line); cursor.setColumn(column); } void KTextEditor::DocumentPrivate::transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision); } //END //BEGIN KTextEditor::AnnotationInterface void KTextEditor::DocumentPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model) { KTextEditor::AnnotationModel *oldmodel = m_annotationModel; m_annotationModel = model; emit annotationModelChanged(oldmodel, m_annotationModel); } KTextEditor::AnnotationModel *KTextEditor::DocumentPrivate::annotationModel() const { return m_annotationModel; } //END KTextEditor::AnnotationInterface //TAKEN FROM kparts.h bool KTextEditor::DocumentPrivate::queryClose() { if (!isReadWrite() || !isModified()) { return true; } QString docName = documentName(); int res = KMessageBox::warningYesNoCancel(dialogParent(), i18n("The document \"%1\" has been modified.\n" "Do you want to save your changes or discard them?", docName), i18n("Close Document"), KStandardGuiItem::save(), KStandardGuiItem::discard()); bool abortClose = false; bool handled = false; switch (res) { case KMessageBox::Yes : sigQueryClose(&handled, &abortClose); if (!handled) { if (url().isEmpty()) { QUrl url = QFileDialog::getSaveFileUrl(dialogParent()); if (url.isEmpty()) { return false; } saveAs(url); } else { save(); } } else if (abortClose) { return false; } return waitSaveComplete(); case KMessageBox::No : return true; default : // case KMessageBox::Cancel : return false; } } void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job) { /** * if we are idle before, we are now loading! */ if (m_documentState == DocumentIdle) { m_documentState = DocumentLoading; } /** * if loading: * - remember pre loading read-write mode * if remote load: * - set to read-only * - trigger possible message */ if (m_documentState == DocumentLoading) { /** * remember state */ m_readWriteStateBeforeLoading = isReadWrite(); /** * perhaps show loading message, but wait one second */ if (job) { /** * only read only if really remote file! */ setReadWrite(false); /** * perhaps some message about loading in one second! * remember job pointer, we want to be able to kill it! */ m_loadingJob = job; QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage())); } } } void KTextEditor::DocumentPrivate::slotCompleted() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; } /** * Emit signal that we saved the document, if needed */ if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) { emit documentSavedOrUploaded(this, m_documentState == DocumentSavingAs); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotCanceled() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; showAndSetOpeningErrorAccess(); updateDocName(); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage() { /** * no longer loading? * no message needed! */ if (m_documentState != DocumentLoading) { return; } /** * create message about file loading in progress */ delete m_loadingMessage; m_loadingMessage = new KTextEditor::Message(i18n("The file %2 is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName())); m_loadingMessage->setPosition(KTextEditor::Message::TopInView); /** * if around job: add cancel action */ if (m_loadingJob) { QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr); connect(cancel, SIGNAL(triggered()), this, SLOT(slotAbortLoading())); m_loadingMessage->addAction(cancel); } /** * really post message */ postMessage(m_loadingMessage); } void KTextEditor::DocumentPrivate::slotAbortLoading() { /** * no job, no work */ if (!m_loadingJob) { return; } /** * abort loading if any job * signal results! */ m_loadingJob->kill(KJob::EmitResult); m_loadingJob = nullptr; } void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url) { if (m_reloading) { // the URL is temporarily unset and then reset to the previous URL during reload // we do not want to notify the outside about this return; } Q_UNUSED(url); updateDocName(); emit documentUrlChanged(this); } bool KTextEditor::DocumentPrivate::save() { /** * no double save/load * we need to allow DocumentPreSavingAs here as state, as save is called in saveAs! */ if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) { return false; } /** * if we are idle, we are now saving */ if (m_documentState == DocumentIdle) { m_documentState = DocumentSaving; } else { m_documentState = DocumentSavingAs; } /** * call back implementation for real work */ return KTextEditor::Document::save(); } bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url) { /** * abort on bad URL * that is done in saveAs below, too * but we must check it here already to avoid messing up * as no signals will be send, then */ if (!url.isValid()) { return false; } /** * no double save/load */ if (m_documentState != DocumentIdle) { return false; } /** * we enter the pre save as phase */ m_documentState = DocumentPreSavingAs; /** * call base implementation for real work */ return KTextEditor::Document::saveAs(normalizeUrl(url)); } QString KTextEditor::DocumentPrivate::defaultDictionary() const { return m_defaultDictionary; } QList > KTextEditor::DocumentPrivate::dictionaryRanges() const { return m_dictionaryRanges; } void KTextEditor::DocumentPrivate::clearDictionaryRanges() { for (QList >::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end(); ++i) { delete(*i).first; } m_dictionaryRanges.clear(); if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(); } emit dictionaryRangesPresent(false); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range, bool blockmode) { if (blockmode) { for (int i = range.start().line(); i <= range.end().line(); ++i) { setDictionary(newDictionary, rangeOnLine(range, i)); } } else { setDictionary(newDictionary, range); } emit dictionaryRangesPresent(!m_dictionaryRanges.isEmpty()); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range) { KTextEditor::Range newDictionaryRange = range; if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) { return; } QList > newRanges; // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint for (QList >::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) { qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange; if (newDictionaryRange.isEmpty()) { break; } QPair pair = *i; QString dictionarySet = pair.second; KTextEditor::MovingRange *dictionaryRange = pair.first; qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet; if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) { qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange"; return; } if (newDictionaryRange.contains(*dictionaryRange)) { delete dictionaryRange; i = m_dictionaryRanges.erase(i); qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange"; continue; } KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange); if (!intersection.isEmpty() && intersection.isValid()) { if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection' // except cut off the intersection QList remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection); Q_ASSERT(remainingRanges.size() == 1); newDictionaryRange = remainingRanges.first(); ++i; qCDebug(LOG_KTE) << "dictionarySet == newDictionary"; continue; } QList remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection); for (QList::iterator j = remainingRanges.begin(); j != remainingRanges.end(); ++j) { KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); remainingRange->setFeedback(this); newRanges.push_back(QPair(remainingRange, dictionarySet)); } i = m_dictionaryRanges.erase(i); delete dictionaryRange; } else { ++i; } } m_dictionaryRanges += newRanges; if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary KTextEditor::MovingRange *newDictionaryMovingRange = newMovingRange(newDictionaryRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); newDictionaryMovingRange->setFeedback(this); m_dictionaryRanges.push_back(QPair(newDictionaryMovingRange, newDictionary)); } if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) { m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange); } } void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict) { if (m_defaultDictionary == dict) { return; } m_defaultDictionary = dict; if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); refreshOnTheFlyCheck(); } emit defaultDictionaryChanged(this); } void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable) { if (isOnTheFlySpellCheckingEnabled() == enable) { return; } if (enable) { Q_ASSERT(m_onTheFlyChecker == nullptr); m_onTheFlyChecker = new KateOnTheFlyChecker(this); } else { delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; } foreach (KTextEditor::ViewPrivate *view, m_views) { view->reflectOnTheFlySpellCheckStatus(enable); } } bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const { return m_onTheFlyChecker != nullptr; } QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(const KTextEditor::Range &range) const { if (!m_onTheFlyChecker) { return QString(); } else { return m_onTheFlyChecker->dictionaryForMisspelledRange(range); } } void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word) { if (m_onTheFlyChecker) { m_onTheFlyChecker->clearMisspellingForWord(word); } } void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(const KTextEditor::Range &range) { if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(range); } } void KTextEditor::DocumentPrivate::rangeInvalid(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::rangeEmpty(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange) { qCDebug(LOG_KTE) << "deleting" << movingRange; auto finder = [=] (const QPair& item) -> bool { return item.first == movingRange; }; auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder); if (it != m_dictionaryRanges.end()) { m_dictionaryRanges.erase(it); delete movingRange; } Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end()); } bool KTextEditor::DocumentPrivate::containsCharacterEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn; ++col) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); if (!prefixStore.findPrefix(textLine, col).isEmpty()) { return true; } } } return false; } int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos) { int previousOffset = 0; for (OffsetList::const_iterator i = offsetList.begin(); i != offsetList.end(); ++i) { if ((*i).first > pos) { break; } previousOffset = (*i).second; } return pos + previousOffset; } QString KTextEditor::DocumentPrivate::decodeCharacters(const KTextEditor::Range &range, KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList, KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList) { QString toReturn; KTextEditor::Cursor previous = range.start(); int decToEncCurrentOffset = 0, encToDecCurrentOffset = 0; int i = 0; int newI = 0; KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); const QHash &characterEncodingsHash = highlighting->getCharacterEncodings(attr); QString matchingPrefix = prefixStore.findPrefix(textLine, col); if (!matchingPrefix.isEmpty()) { toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col))); const QChar &c = characterEncodingsHash.value(matchingPrefix); const bool isNullChar = c.isNull(); if (!c.isNull()) { toReturn += c; } i += matchingPrefix.length(); col += matchingPrefix.length(); previous = KTextEditor::Cursor(line, col); decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length(); encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1); newI += (isNullChar ? 0 : 1); decToEncOffsetList.push_back(QPair(newI, decToEncCurrentOffset)); encToDecOffsetList.push_back(QPair(i, encToDecCurrentOffset)); continue; } ++col; ++i; ++newI; } ++i; ++newI; } if (previous < range.end()) { toReturn += text(KTextEditor::Range(previous, range.end())); } return toReturn; } void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const QHash &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr); QHash::const_iterator it = reverseCharacterEncodingsHash.find(textLine->at(col)); if (it != reverseCharacterEncodingsHash.end()) { replaceText(KTextEditor::Range(line, col, line, col + 1), *it); col += (*it).length(); continue; } ++col; } } } // // Highlighting information // KTextEditor::Attribute::Ptr KTextEditor::DocumentPrivate::attributeAt(const KTextEditor::Cursor &position) { KTextEditor::Attribute::Ptr attrib(new KTextEditor::Attribute()); KTextEditor::ViewPrivate *view = m_views.empty() ? nullptr : m_views.begin().value(); if (!view) { qCWarning(LOG_KTE) << "ATTENTION: cannot access lineAttributes() without any View (will be fixed eventually)"; return attrib; } Kate::TextLine kateLine = kateTextLine(position.line()); if (!kateLine) { return attrib; } *attrib = *view->renderer()->attribute(kateLine->attribute(position.column())); return attrib; } QStringList KTextEditor::DocumentPrivate::embeddedHighlightingModes() const { return highlight()->getEmbeddedHighlightingModes(); } QString KTextEditor::DocumentPrivate::highlightingModeAt(const KTextEditor::Cursor &position) { return highlight()->higlightingModeForLocation(this, position); } Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile() { return m_swapfile; } /** * \return \c -1 if \c line or \c column invalid, otherwise one of * standard style attribute number */ int KTextEditor::DocumentPrivate::defStyleNum(int line, int column) { // Validate parameters to prevent out of range access if (line < 0 || line >= lines() || column < 0) { return -1; } // get highlighted line Kate::TextLine tl = kateTextLine(line); // make sure the textline is a valid pointer if (!tl) { return -1; } /** * either get char attribute or attribute of context still active at end of line */ int attribute = 0; if (column < tl->length()) { attribute = tl->attribute(column); } else if (column == tl->length()) { if (!tl->attributesList().isEmpty()) { attribute = tl->attributesList().back().attributeValue; } else { return -1; } } else { return -1; } return highlight()->defaultStyleForAttribute(attribute); } bool KTextEditor::DocumentPrivate::isComment(int line, int column) { const int defaultStyle = defStyleNum(line, column); return defaultStyle == KTextEditor::dsComment; } int KTextEditor::DocumentPrivate::findTouchedLine(int startLine, bool down) { const int offset = down ? 1 : -1; const int lineCount = lines(); while (startLine >= 0 && startLine < lineCount) { Kate::TextLine tl = m_buffer->plainLine(startLine); if (tl && (tl->markedAsModified() || tl->markedAsSavedOnDisk())) { return startLine; } startLine += offset; } return -1; } void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler* handler) { // delete any active template handler delete m_activeTemplateHandler.data(); m_activeTemplateHandler = handler; } //BEGIN KTextEditor::MessageInterface bool KTextEditor::DocumentPrivate::postMessage(KTextEditor::Message *message) { // no message -> cancel if (!message) { return false; } // make sure the desired view belongs to this document if (message->view() && message->view()->document() != this) { qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text(); return false; } message->setParent(this); message->setDocument(this); // if there are no actions, add a close action by default if widget does not auto-hide if (message->actions().count() == 0 && message->autoHide() < 0) { QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); message->addAction(closeAction); } // make sure the message is registered even if no actions and no views exist m_messageHash[message] = QList >(); // reparent actions, as we want full control over when they are deleted foreach (QAction *action, message->actions()) { action->setParent(nullptr); m_messageHash[message].append(QSharedPointer(action)); } // post message to requested view, or to all views if (KTextEditor::ViewPrivate *view = qobject_cast(message->view())) { view->postMessage(message, m_messageHash[message]); } else { foreach (KTextEditor::ViewPrivate *view, m_views) { view->postMessage(message, m_messageHash[message]); } } // also catch if the user manually calls delete message connect(message, SIGNAL(closed(KTextEditor::Message*)), SLOT(messageDestroyed(KTextEditor::Message*))); return true; } void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message) { // KTE:Message is already in destructor Q_ASSERT(m_messageHash.contains(message)); m_messageHash.remove(message); } //END KTextEditor::MessageInterface void KTextEditor::DocumentPrivate::closeDocumentInApplication() { KTextEditor::EditorPrivate::self()->application()->closeDocument(this); } diff --git a/src/script/katescriptmanager.cpp b/src/script/katescriptmanager.cpp index a1bca224..9bb3031f 100644 --- a/src/script/katescriptmanager.cpp +++ b/src/script/katescriptmanager.cpp @@ -1,341 +1,336 @@ // This file is part of the KDE libraries // Copyright (C) 2005 Christoph Cullmann // Copyright (C) 2005 Joseph Wenninger // Copyright (C) 2006-2018 Dominik Haumann // Copyright (C) 2008 Paul Giannaros // Copyright (C) 2010 Joseph Wenninger // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License version 2 as published by the Free Software Foundation. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public License // along with this library; see the file COPYING.LIB. If not, write to // the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, // Boston, MA 02110-1301, USA. #include "katescriptmanager.h" #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include "kateglobal.h" #include "katecmd.h" #include "katepartdebug.h" KateScriptManager *KateScriptManager::m_instance = nullptr; KateScriptManager::KateScriptManager() : KTextEditor::Command({ QStringLiteral("reload-scripts") }) { // use cached info collect(); } KateScriptManager::~KateScriptManager() { qDeleteAll(m_indentationScripts); qDeleteAll(m_commandLineScripts); m_instance = nullptr; } KateIndentScript *KateScriptManager::indenter(const QString &language) { KateIndentScript *highestPriorityIndenter = nullptr; foreach (KateIndentScript *indenter, m_languageToIndenters.value(language.toLower())) { // don't overwrite if there is already a result with a higher priority if (highestPriorityIndenter && indenter->indentHeader().priority() < highestPriorityIndenter->indentHeader().priority()) { #ifdef DEBUG_SCRIPTMANAGER qCDebug(LOG_KTE) << "Not overwriting indenter for" << language << "as the priority isn't big enough (" << indenter->indentHeader().priority() << '<' << highestPriorityIndenter->indentHeader().priority() << ')'; #endif } else { highestPriorityIndenter = indenter; } } #ifdef DEBUG_SCRIPTMANAGER if (highestPriorityIndenter) { qCDebug(LOG_KTE) << "Found indenter" << highestPriorityIndenter->url() << "for" << language; } else { qCDebug(LOG_KTE) << "No indenter for" << language; } #endif return highestPriorityIndenter; } /** * Small helper: QJsonValue to QStringList */ static QStringList jsonToStringList (const QJsonValue &value) { QStringList list; Q_FOREACH (const QJsonValue &value, value.toArray()) { if (value.isString()) { list.append(value.toString()); } } return list; } void KateScriptManager::collect() { // clear out the old scripts and reserve enough space qDeleteAll(m_indentationScripts); qDeleteAll(m_commandLineScripts); m_indentationScripts.clear(); m_commandLineScripts.clear(); m_languageToIndenters.clear(); m_indentationScriptMap.clear(); /** * now, we search all kinds of known scripts */ for (const auto &type : { QLatin1String("indentation"), QLatin1String("commands") }) { // basedir for filesystem lookup const QString basedir = QLatin1String("/katepart5/script/") + type; QStringList dirs; // first writable locations, e.g. stuff the user has provided dirs += QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + basedir; // then resources, e.g. the stuff we ship with us dirs.append(QLatin1String(":/ktexteditor/script/") + type); // then all other locations, this includes global stuff installed by other applications // this will not allow global stuff to overwrite the stuff we ship in our resources to allow to install a more up-to-date ktexteditor lib locally! foreach (const QString &dir, QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) { dirs.append(dir + basedir); } QStringList list; foreach (const QString &dir, dirs) { const QStringList fileNames = QDir(dir).entryList({ QStringLiteral("*.js") }); foreach (const QString &file, fileNames) { list.append(dir + QLatin1Char('/') + file); } } // iterate through the files and read info out of cache or file, no double loading of same scripts QSet unique; foreach (const QString &fileName, list) { /** * get file basename */ const QString baseName = QFileInfo(fileName).baseName(); /** * only load scripts once, even if multiple installed variants found! */ if (unique.contains(baseName)) continue; /** * remember the script */ unique.insert (baseName); /** * open file or skip it */ QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qCDebug(LOG_KTE) << "Script parse error: Cannot open file " << qPrintable(fileName) << '\n'; continue; } /** * search json header or skip this file */ QByteArray fileContent = file.readAll(); int startOfJson = fileContent.indexOf ('{'); if (startOfJson < 0) { qCDebug(LOG_KTE) << "Script parse error: Cannot find start of json header at start of file " << qPrintable(fileName) << '\n'; continue; } int endOfJson = fileContent.indexOf("\n};", startOfJson); if (endOfJson < 0) { // as fallback, check also mac os line ending endOfJson = fileContent.indexOf("\r};", startOfJson); } if (endOfJson < 0) { qCDebug(LOG_KTE) << "Script parse error: Cannot find end of json header at start of file " << qPrintable(fileName) << '\n'; continue; } endOfJson += 2; //we want the end including the } but not the ; /** * parse json header or skip this file */ QJsonParseError error; const QJsonDocument metaInfo (QJsonDocument::fromJson(fileContent.mid(startOfJson, endOfJson-startOfJson), &error)); if (error.error || !metaInfo.isObject()) { qCDebug(LOG_KTE) << "Script parse error: Cannot parse json header at start of file " << qPrintable(fileName) << error.errorString() << endOfJson << fileContent.mid(endOfJson-25, 25).replace('\n', ' '); continue; } /** * remember type */ KateScriptHeader generalHeader; if (type == QLatin1String("indentation")) { generalHeader.setScriptType(Kate::ScriptType::Indentation); } else if (type == QLatin1String("commands")) { generalHeader.setScriptType(Kate::ScriptType::CommandLine); } else { // should never happen, we dictate type by directory Q_ASSERT(false); } const QJsonObject metaInfoObject = metaInfo.object(); generalHeader.setLicense(metaInfoObject.value(QStringLiteral("license")).toString()); generalHeader.setAuthor(metaInfoObject.value(QStringLiteral("author")).toString()); generalHeader.setRevision(metaInfoObject.value(QStringLiteral("revision")).toInt()); generalHeader.setKateVersion(metaInfoObject.value(QStringLiteral("kate-version")).toString()); // now, cast accordingly based on type switch (generalHeader.scriptType()) { case Kate::ScriptType::Indentation: { KateIndentScriptHeader indentHeader; indentHeader.setName(metaInfoObject.value(QStringLiteral("name")).toString()); indentHeader.setBaseName(baseName); if (indentHeader.name().isNull()) { qCDebug(LOG_KTE) << "Script value error: No name specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping indenter" << '\n'; continue; } // required style? indentHeader.setRequiredStyle(metaInfoObject.value(QStringLiteral("required-syntax-style")).toString()); // which languages does this support? QStringList indentLanguages = jsonToStringList(metaInfoObject.value(QStringLiteral("indent-languages"))); if (!indentLanguages.isEmpty()) { indentHeader.setIndentLanguages(indentLanguages); } else { indentHeader.setIndentLanguages(QStringList() << indentHeader.name()); #ifdef DEBUG_SCRIPTMANAGER qCDebug(LOG_KTE) << "Script value warning: No indent-languages specified for indent " << "script " << qPrintable(fileName) << ". Using the name (" << qPrintable(indentHeader.name()) << ")\n"; #endif } // priority indentHeader.setPriority(metaInfoObject.value(QStringLiteral("priority")).toInt()); KateIndentScript *script = new KateIndentScript(fileName, indentHeader); script->setGeneralHeader(generalHeader); foreach (const QString &language, indentHeader.indentLanguages()) { m_languageToIndenters[language.toLower()].push_back(script); } m_indentationScriptMap.insert(indentHeader.baseName(), script); m_indentationScripts.append(script); break; } case Kate::ScriptType::CommandLine: { KateCommandLineScriptHeader commandHeader; commandHeader.setFunctions(jsonToStringList(metaInfoObject.value(QStringLiteral("functions")))); commandHeader.setActions(metaInfoObject.value(QStringLiteral("actions")).toArray()); if (commandHeader.functions().isEmpty()) { qCDebug(LOG_KTE) << "Script value error: No functions specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping script" << '\n'; continue; } KateCommandLineScript *script = new KateCommandLineScript(fileName, commandHeader); script->setGeneralHeader(generalHeader); m_commandLineScripts.push_back(script); break; } case Kate::ScriptType::Unknown: default: qCDebug(LOG_KTE) << "Script value warning: Unknown type ('" << qPrintable(type) << "'): " << qPrintable(fileName) << '\n'; } } } #ifdef DEBUG_SCRIPTMANAGER // XX Test if (indenter("Python")) { qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("triggerCharacters").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("triggerCharacters").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("blafldsjfklas").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("indent").isValid() << "\n"; } if (indenter("C")) { qCDebug(LOG_KTE) << "C: " << qPrintable(indenter("C")->url()) << "\n"; } if (indenter("lisp")) { qCDebug(LOG_KTE) << "LISP: " << qPrintable(indenter("Lisp")->url()) << "\n"; } #endif } void KateScriptManager::reload() { collect(); emit reloaded(); } /// Kate::Command stuff bool KateScriptManager::exec(KTextEditor::View *view, const QString &_cmd, QString &errorMsg, const KTextEditor::Range &) { - QStringList args(_cmd.split(QRegExp(QLatin1String("\\s+")), QString::SkipEmptyParts)); - QString cmd(args.first()); - args.removeFirst(); + Q_UNUSED(view) - if (!view) { - errorMsg = i18n("Could not access view"); - return false; - } + QVector args = _cmd.splitRef(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts); + const QString cmd = args.first().toString(); if (cmd == QLatin1String("reload-scripts")) { reload(); return true; - } else { - errorMsg = i18n("Command not found: %1", cmd); - return false; } + + return false; } bool KateScriptManager::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view) if (cmd == QLatin1String("reload-scripts")) { msg = i18n("Reload all JavaScript files (indenters, command line scripts, etc)."); return true; - } else { - msg = i18n("Command not found: %1", cmd); - return false; } + + return false; } diff --git a/src/view/kateviewhelpers.cpp b/src/view/kateviewhelpers.cpp index f668a466..666b8556 100644 --- a/src/view/kateviewhelpers.cpp +++ b/src/view/kateviewhelpers.cpp @@ -1,3105 +1,3107 @@ /* This file is part of the KDE libraries Copyright (C) 2008, 2009 Matthew Woehlke Copyright (C) 2007 Mirko Stocker Copyright (C) 2002 John Firebaugh Copyright (C) 2001 Anders Lund Copyright (C) 2001 Christoph Cullmann Copyright (C) 2011 Svyatoslav Kuzmich Copyright (C) 2012 Kåre Särs (Minimap) Copyright 2017-2018 Friedrich W. H. Kossebau This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateviewhelpers.h" #include "katecmd.h" #include #include #include #include "kateconfig.h" #include "katedocument.h" #include #include "katerenderer.h" #include "kateannotationitemdelegate.h" #include "kateview.h" #include "kateviewinternal.h" #include "katelayoutcache.h" #include "katetextlayout.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katecommandrangeexpressionparser.h" #include "kateabstractinputmode.h" #include "katetextpreview.h" #include #include #include #include #include -#include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //BEGIN KateMessageLayout KateMessageLayout::KateMessageLayout(QWidget *parent) : QLayout(parent) { } KateMessageLayout::~KateMessageLayout() { while (QLayoutItem *item = takeAt(0)) delete item; } void KateMessageLayout::addItem(QLayoutItem *item) { Q_ASSERT(false); add(item, KTextEditor::Message::CenterInView); } void KateMessageLayout::addWidget(QWidget *widget, KTextEditor::Message::MessagePosition pos) { add(new QWidgetItem(widget), pos); } int KateMessageLayout::count() const { return m_items.size(); } QLayoutItem *KateMessageLayout::itemAt(int index) const { if (index < 0 || index >= m_items.size()) return nullptr; return m_items[index]->item; } void KateMessageLayout::setGeometry(const QRect &rect) { QLayout::setGeometry(rect); const int s = spacing(); const QRect adjustedRect = rect.adjusted(s, s, -s, -s); for (auto wrapper : m_items) { QLayoutItem *item = wrapper->item; auto position = wrapper->position; if (position == KTextEditor::Message::TopInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), s, item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::BottomInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), adjustedRect.height() - item->sizeHint().height(), item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::CenterInView) { QRect r(0, 0, item->sizeHint().width(), item->sizeHint().height()); r.moveCenter(adjustedRect.center()); item->setGeometry(r); } else { Q_ASSERT_X(false, "setGeometry", "Only TopInView, CenterInView, and BottomInView are supported."); } } } QSize KateMessageLayout::sizeHint() const { return QSize(); } QLayoutItem *KateMessageLayout::takeAt(int index) { if (index >= 0 && index < m_items.size()) { ItemWrapper *layoutStruct = m_items.takeAt(index); return layoutStruct->item; } return nullptr; } void KateMessageLayout::add(QLayoutItem *item, KTextEditor::Message::MessagePosition pos) { m_items.push_back(new ItemWrapper(item, pos)); } //END KateMessageLayout //BEGIN KateScrollBar static const int s_lineWidth = 100; static const int s_pixelMargin = 8; static const int s_linePixelIncLimit = 6; const unsigned char KateScrollBar::characterOpacity[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 15 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, // <- 31 0, 125, 41, 221, 138, 195, 218, 21, 142, 142, 137, 137, 97, 87, 87, 140, // <- 47 223, 164, 183, 190, 191, 193, 214, 158, 227, 216, 103, 113, 146, 140, 146, 149, // <- 63 248, 204, 240, 174, 217, 197, 178, 205, 209, 176, 168, 211, 160, 246, 238, 218, // <- 79 195, 229, 227, 196, 167, 212, 188, 238, 197, 169, 189, 158, 21, 151, 115, 90, // <- 95 15, 192, 209, 153, 208, 187, 162, 221, 183, 149, 161, 191, 146, 203, 167, 182, // <- 111 208, 203, 139, 166, 158, 167, 157, 189, 164, 179, 156, 167, 145, 166, 109, 0, // <- 127 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 143 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 159 0, 125, 184, 187, 146, 201, 127, 203, 89, 194, 156, 141, 117, 87, 202, 88, // <- 175 115, 165, 118, 121, 85, 190, 236, 87, 88, 111, 151, 140, 194, 191, 203, 148, // <- 191 215, 215, 222, 224, 223, 234, 230, 192, 208, 208, 216, 217, 187, 187, 194, 195, // <- 207 228, 255, 228, 228, 235, 239, 237, 150, 255, 222, 222, 229, 232, 180, 197, 225, // <- 223 208, 208, 216, 217, 212, 230, 218, 170, 202, 202, 211, 204, 156, 156, 165, 159, // <- 239 214, 194, 197, 197, 206, 206, 201, 132, 214, 183, 183, 192, 187, 195, 227, 198 }; KateScrollBar::KateScrollBar(Qt::Orientation orientation, KateViewInternal *parent) : QScrollBar(orientation, parent->m_view) , m_middleMouseDown(false) , m_leftMouseDown(false) , m_view(parent->m_view) , m_doc(parent->doc()) , m_viewInternal(parent) , m_textPreview(nullptr) , m_showMarks(false) , m_showMiniMap(false) , m_miniMapAll(true) , m_miniMapWidth(40) , m_grooveHeight(height()) , m_linesModified(0) { connect(this, SIGNAL(valueChanged(int)), this, SLOT(sliderMaybeMoved(int))); connect(m_doc, SIGNAL(marksChanged(KTextEditor::Document*)), this, SLOT(marksChanged())); m_updateTimer.setInterval(300); m_updateTimer.setSingleShot(true); QTimer::singleShot(10, this, SLOT(updatePixmap())); // track mouse for text preview widget setMouseTracking(orientation == Qt::Vertical); // setup text preview timer m_delayTextPreviewTimer.setSingleShot(true); m_delayTextPreviewTimer.setInterval(250); connect(&m_delayTextPreviewTimer, SIGNAL(timeout()), this, SLOT(showTextPreview())); } KateScrollBar::~KateScrollBar() { delete m_textPreview; } void KateScrollBar::setShowMiniMap(bool b) { if (b && !m_showMiniMap) { connect(m_view, SIGNAL(selectionChanged(KTextEditor::View*)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_doc, SIGNAL(textChanged(KTextEditor::Document*)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_view, SIGNAL(delayedUpdateOfView()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updatePixmap()), Qt::UniqueConnection); connect(&(m_view->textFolding()), SIGNAL(foldingRangesChanged()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); } else if (!b) { disconnect(&m_updateTimer); } m_showMiniMap = b; updateGeometry(); update(); } QSize KateScrollBar::sizeHint() const { if (m_showMiniMap) { return QSize(m_miniMapWidth, QScrollBar::sizeHint().height()); } return QScrollBar::sizeHint(); } int KateScrollBar::minimapYToStdY(int y) { // Check if the minimap fills the whole scrollbar if (m_stdGroveRect.height() == m_mapGroveRect.height()) { return y; } // check if y is on the step up/down if ((y < m_stdGroveRect.top()) || (y > m_stdGroveRect.bottom())) { return y; } if (y < m_mapGroveRect.top()) { return m_stdGroveRect.top() + 1; } if (y > m_mapGroveRect.bottom()) { return m_stdGroveRect.bottom() - 1; } // check for div/0 if (m_mapGroveRect.height() == 0) { return y; } int newY = (y - m_mapGroveRect.top()) * m_stdGroveRect.height() / m_mapGroveRect.height(); newY += m_stdGroveRect.top(); return newY; } void KateScrollBar::mousePressEvent(QMouseEvent *e) { // delete text preview hideTextPreview(); if (e->button() == Qt::MidButton) { m_middleMouseDown = true; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = true; } if (m_showMiniMap) { if (m_leftMouseDown && e->pos().y() > m_mapGroveRect.top() && e->pos().y() < m_mapGroveRect.bottom()) { // if we show the minimap left-click jumps directly to the selected position int newVal = (e->pos().y()-m_mapGroveRect.top()) / (double)m_mapGroveRect.height() * (double)(maximum()+pageStep()) - pageStep()/2; newVal = qBound(0, newVal, maximum()); setSliderPosition(newVal); } QMouseEvent eMod(QEvent::MouseButtonPress, QPoint(6, minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mousePressEvent(&eMod); } else { QScrollBar::mousePressEvent(e); } m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); redrawMarks(); } void KateScrollBar::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MidButton) { m_middleMouseDown = false; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = false; } redrawMarks(); if (m_leftMouseDown || m_middleMouseDown) { QToolTip::hideText(); } if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseButtonRelease, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseReleaseEvent(&eMod); } else { QScrollBar::mouseReleaseEvent(e); } } void KateScrollBar::mouseMoveEvent(QMouseEvent *e) { if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseMove, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseMoveEvent(&eMod); } else { QScrollBar::mouseMoveEvent(e); } if (e->buttons() & (Qt::LeftButton | Qt::MidButton)) { redrawMarks(); // current line tool tip m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } showTextPreviewDelayed(); } void KateScrollBar::leaveEvent(QEvent *event) { hideTextPreview(); QAbstractSlider::leaveEvent(event); } bool KateScrollBar::eventFilter(QObject *object, QEvent *event) { Q_UNUSED(object) if (m_textPreview && event->type() == QEvent::WindowDeactivate) { // We need hide the scrollbar TextPreview widget hideTextPreview(); } return false; } void KateScrollBar::paintEvent(QPaintEvent *e) { if (m_doc->marks().size() != m_lines.size()) { recomputeMarksPositions(); } if (m_showMiniMap) { miniMapPaintEvent(e); } else { normalPaintEvent(e); } } void KateScrollBar::showTextPreviewDelayed() { if (!m_textPreview) { if (!m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.start(); } } else { showTextPreview(); } } void KateScrollBar::showTextPreview() { if (orientation() != Qt::Vertical || isSliderDown() || (minimum() == maximum()) || !m_view->config()->scrollBarPreview()) { return; } // only show when main window is active (#392396) if (window() && !window()->isActiveWindow()) { return; } QRect grooveRect; if (m_showMiniMap) { // If mini-map is shown, the height of the map might not be the whole height grooveRect = m_mapGroveRect; } else { QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); } if (m_view->config()->scrollPastEnd()) { // Adjust the grove size to accommodate the added pageStep at the bottom int adjust = pageStep()*grooveRect.height() / (maximum() + pageStep() - minimum()); grooveRect.adjust(0,0,0, -adjust); } const QPoint cursorPos = mapFromGlobal(QCursor::pos()); if (grooveRect.contains(cursorPos)) { if (!m_textPreview) { m_textPreview = new KateTextPreview(m_view, this); m_textPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_textPreview->setFrameStyle(QFrame::StyledPanel); // event filter to catch application WindowDeactivate event, to hide the preview window qApp->installEventFilter(this); } const qreal posInPercent = static_cast(cursorPos.y() - grooveRect.top()) / grooveRect.height(); const qreal startLine = posInPercent * m_view->textFolding().visibleLines(); m_textPreview->resize(m_view->width() / 2, m_view->height() / 5); const int xGlobal = mapToGlobal(QPoint(0, 0)).x(); const int yGlobal = qMin(mapToGlobal(QPoint(0, height())).y() - m_textPreview->height(), qMax(mapToGlobal(QPoint(0, 0)).y(), mapToGlobal(cursorPos).y() - m_textPreview->height() / 2)); m_textPreview->move(xGlobal - m_textPreview->width(), yGlobal); m_textPreview->setLine(startLine); m_textPreview->setCenterView(true); m_textPreview->setScaleFactor(0.8); m_textPreview->raise(); m_textPreview->show(); } else { hideTextPreview(); } } void KateScrollBar::hideTextPreview() { if (m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.stop(); } qApp->removeEventFilter(this); delete m_textPreview; } // This function is optimized for bing called in sequence. const QColor KateScrollBar::charColor(const QVector &attributes, int &attributeIndex, const QVector &decorations, const QColor &defaultColor, int x, QChar ch) { QColor color = defaultColor; bool styleFound = false; // Query the decorations, that is, things like search highlighting, or the // KDevelop DUChain highlighting, for a color to use foreach (const QTextLayout::FormatRange &range, decorations) { if (range.start <= x && range.start + range.length > x) { // If there's a different background color set (search markers, ...) // use that, otherwise use the foreground color. if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { color = range.format.background().color(); } else { color = range.format.foreground().color(); } styleFound = true; break; } } // If there's no decoration set for the current character (this will mostly be the case for // plain Kate), query the styles, that is, the default kate syntax highlighting. if (!styleFound) { // go to the block containing x while ((attributeIndex < attributes.size()) && ((attributes[attributeIndex].offset + attributes[attributeIndex].length) < x)) { ++attributeIndex; } if ((attributeIndex < attributes.size()) && (x < attributes[attributeIndex].offset + attributes[attributeIndex].length)) { color = m_view->renderer()->attribute(attributes[attributeIndex].attributeValue)->foreground().color(); } } // Query how much "blackness" the character has. // This causes for example a dot or a dash to appear less intense // than an A or similar. // This gives the pixels created a bit of structure, which makes it look more // like real text. color.setAlpha((ch.unicode() < 256) ? characterOpacity[ch.unicode()] : 222); return color; } void KateScrollBar::updatePixmap() { //QTime time; //time.start(); if (!m_showMiniMap) { // make sure no time is wasted if the option is disabled return; } // For performance reason, only every n-th line will be drawn if the widget is // sufficiently small compared to the amount of lines in the document. int docLineCount = m_view->textFolding().visibleLines(); int pixmapLineCount = docLineCount; if (m_view->config()->scrollPastEnd()) { pixmapLineCount += pageStep(); } int pixmapLinesUnscaled = pixmapLineCount; if (m_grooveHeight < 5) { m_grooveHeight = 5; } int lineDivisor = pixmapLinesUnscaled / m_grooveHeight; if (lineDivisor < 1) { lineDivisor = 1; } int charIncrement = 1; int lineIncrement = 1; if ((m_grooveHeight > 10) && (pixmapLineCount >= m_grooveHeight * 2)) { charIncrement = pixmapLineCount / m_grooveHeight; while (charIncrement > s_linePixelIncLimit) { lineIncrement++; pixmapLineCount = pixmapLinesUnscaled / lineIncrement; charIncrement = pixmapLineCount / m_grooveHeight; } pixmapLineCount /= charIncrement; } int pixmapLineWidth = s_pixelMargin + s_lineWidth / charIncrement; //qCDebug(LOG_KTE) << "l" << lineIncrement << "c" << charIncrement << "d" << lineDivisor; //qCDebug(LOG_KTE) << "pixmap" << pixmapLineCount << pixmapLineWidth << "docLines" << m_view->textFolding().visibleLines() << "height" << m_grooveHeight; const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor defaultTextColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor selectionBgColor = m_view->renderer()->config()->selectionColor(); QColor modifiedLineColor = m_view->renderer()->config()->modifiedLineColor(); QColor savedLineColor = m_view->renderer()->config()->savedLineColor(); // move the modified line color away from the background color modifiedLineColor.setHsv(modifiedLineColor.hue(), 255, 255 - backgroundColor.value() / 3); savedLineColor.setHsv(savedLineColor.hue(), 100, 255 - backgroundColor.value() / 3); // increase dimensions by ratio m_pixmap = QPixmap(pixmapLineWidth * m_view->devicePixelRatioF(), pixmapLineCount * m_view->devicePixelRatioF()); m_pixmap.fill(QColor("transparent")); // The text currently selected in the document, to be drawn later. const KTextEditor::Range &selection = m_view->selectionRange(); QPainter painter; if (painter.begin(&m_pixmap)) { // init pen once, afterwards, only change it if color changes to avoid a lot of allocation for setPen painter.setPen(selectionBgColor); // Do not force updates of the highlighting if the document is very large bool simpleMode = m_doc->lines() > 7500; int pixelY = 0; int drawnLines = 0; // Iterate over all visible lines, drawing them. for (int virtualLine = 0; virtualLine < docLineCount; virtualLine += lineIncrement) { int realLineNumber = m_view->textFolding().visibleLineToLine(virtualLine); QString lineText = m_doc->line(realLineNumber); if (!simpleMode) { m_doc->buffer().ensureHighlighted(realLineNumber); } const Kate::TextLine &kateline = m_doc->plainKateTextLine(realLineNumber); const QVector &attributes = kateline->attributesList(); QVector decorations = m_view->renderer()->decorationsForLine(kateline, realLineNumber); int attributeIndex = 0; // Draw selection if it is on an empty line if (selection.contains(KTextEditor::Cursor(realLineNumber, 0)) && lineText.size() == 0) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(s_pixelMargin, pixelY, s_pixelMargin + s_lineWidth - 1, pixelY); } // Iterate over the line to draw the background int selStartX = -1; int selEndX = -1; int pixelX = s_pixelMargin; // use this to control the offset of the text from the left for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // Query the selection and draw it behind the character if (selection.contains(KTextEditor::Cursor(realLineNumber, x))) { if (selStartX == -1) selStartX = pixelX; selEndX = pixelX; if (lineText.size() - 1 == x) { selEndX = s_lineWidth + s_pixelMargin-1; } } if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { pixelX++; } } if (selStartX != -1) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(selStartX, pixelY, selEndX, pixelY); } // Iterate over all the characters in the current line pixelX = s_pixelMargin; for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // draw the pixels if (lineText[x] == QLatin1Char(' ')) { pixelX++; } else if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { const QColor newPenColor(charColor(attributes, attributeIndex, decorations, defaultTextColor, x, lineText[x])); if (newPenColor != painter.pen().color()) { painter.setPen(newPenColor); } // Actually draw the pixel with the color queried from the renderer. painter.drawPoint(pixelX, pixelY); pixelX++; } } drawnLines++; if (((drawnLines) % charIncrement) == 0) { pixelY++; } } //qCDebug(LOG_KTE) << drawnLines; // Draw line modification marker map. // Disable this if the document is really huge, // since it requires querying every line. if (m_doc->lines() < 50000) { for (int lineno = 0; lineno < docLineCount; lineno++) { int realLineNo = m_view->textFolding().visibleLineToLine(lineno); const Kate::TextLine &line = m_doc->plainKateTextLine(realLineNo); const QColor & col = line->markedAsModified() ? modifiedLineColor : savedLineColor; if (line->markedAsModified() || line->markedAsSavedOnDisk()) { painter.fillRect(2, lineno / lineDivisor, 3, 1, col); } } } // end painting painter.end(); } // set right ratio m_pixmap.setDevicePixelRatio(m_view->devicePixelRatioF()); //qCDebug(LOG_KTE) << time.elapsed(); // Redraw the scrollbar widget with the updated pixmap. update(); } void KateScrollBar::miniMapPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); m_stdGroveRect = grooveRect; if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSubLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.moveTop(alignMargin); grooveRect.setHeight(grooveRect.height() - alignMargin); } if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarAddLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.setHeight(grooveRect.height() - alignMargin); } m_grooveHeight = grooveRect.height(); const int docXMargin = 1; //style()->drawControl(QStyle::CE_ScrollBarAddLine, &opt, &painter, this); //style()->drawControl(QStyle::CE_ScrollBarSubLine, &opt, &painter, this); // calculate the document size and position const int docHeight = qMin(grooveRect.height(), int(m_pixmap.height() / m_pixmap.devicePixelRatio() * 2)) - 2 * docXMargin; const int yoffset = 1; // top-aligned in stead of center-aligned (grooveRect.height() - docHeight) / 2; const QRect docRect(QPoint(grooveRect.left() + docXMargin, yoffset + grooveRect.top()), QSize(grooveRect.width() - docXMargin, docHeight)); m_mapGroveRect = docRect; // calculate the visible area int max = qMax(maximum() + 1, 1); int visibleStart = value() * docHeight / (max + pageStep()) + docRect.top() + 0.5; int visibleEnd = (value() + pageStep()) * docHeight / (max + pageStep()) + docRect.top(); QRect visibleRect = docRect; visibleRect.moveTop(visibleStart); visibleRect.setHeight(visibleEnd - visibleStart); // calculate colors const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor foregroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor highlightColor = palette().link().color(); const int backgroundLightness = backgroundColor.lightness(); const int foregroundLightness = foregroundColor.lightness(); const int lighnessDiff = (foregroundLightness - backgroundLightness); // get a color suited for the color theme QColor darkShieldColor = palette().color(QPalette::Mid); int hue, sat, light; darkShieldColor.getHsl(&hue, &sat, &light); // apply suitable lightness darkShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.35); // gradient for nicer results QLinearGradient gradient(0, 0, width(), 0); gradient.setColorAt(0, darkShieldColor); gradient.setColorAt(0.3, darkShieldColor.lighter(115)); gradient.setColorAt(1, darkShieldColor); QColor lightShieldColor; lightShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.15); QColor outlineColor; outlineColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.5); // draw the grove background in case the document is small painter.setPen(Qt::NoPen); painter.setBrush(backgroundColor); painter.drawRect(grooveRect); // adjust the rectangles QRect sliderRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); sliderRect.setX(docXMargin); sliderRect.setWidth(width() - docXMargin*2); if ((docHeight + 2 * docXMargin >= grooveRect.height()) && (sliderRect.height() > visibleRect.height() + 2)) { visibleRect.adjust(2, 0, -3, 0); } else { visibleRect.adjust(1, 0, -1, 2); sliderRect.setTop(visibleRect.top() - 1); sliderRect.setBottom(visibleRect.bottom() + 1); } // Smooth transform only when squeezing if (grooveRect.height() < m_pixmap.height() / m_pixmap.devicePixelRatio()) { painter.setRenderHint(QPainter::SmoothPixmapTransform); } // draw the modified lines margin QRect pixmapMarginRect(QPoint(0, 0), QSize(s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapMarginRect(QPoint(0, docRect.top()), QSize(s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapMarginRect, m_pixmap, pixmapMarginRect); // calculate the stretch and draw the stretched lines (scrollbar marks) QRect pixmapRect(QPoint(s_pixelMargin, 0), QSize(m_pixmap.width() / m_pixmap.devicePixelRatio() - s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapRect(QPoint(s_pixelMargin, docRect.top()), QSize(docRect.width() - s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapRect, m_pixmap, pixmapRect); // delimit the end of the document const int y = docPixmapRect.height() + grooveRect.y(); if (y+2 < grooveRect.y() + grooveRect.height()) { QColor fg(foregroundColor); fg.setAlpha(30); painter.setBrush(Qt::NoBrush); painter.setPen(QPen(fg, 1)); painter.drawLine(grooveRect.x()+1,y+2,width()-1,y+2); } // fade the invisible sections const QRect top( grooveRect.x(), grooveRect.y(), grooveRect.width(), visibleRect.y()-grooveRect.y() //Pen width ); const QRect bottom( grooveRect.x(), grooveRect.y()+visibleRect.y()+visibleRect.height()-grooveRect.y(), //Pen width grooveRect.width(), grooveRect.height() - (visibleRect.y()-grooveRect.y())-visibleRect.height() ); QColor faded(backgroundColor); faded.setAlpha(110); painter.fillRect(top, faded); painter.fillRect(bottom, faded); // add a thin line to limit the scrollbar QColor c(foregroundColor); c.setAlpha(10); painter.setPen(QPen(c,1)); painter.drawLine(0, 0, 0, height()); if (m_showMarks) { QHashIterator it = m_lines; QPen penBg; penBg.setWidth(4); lightShieldColor.setAlpha(180); penBg.setColor(lightShieldColor); painter.setPen(penBg); while (it.hasNext()) { it.next(); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top();; painter.drawLine(6, y, width() - 6, y); } it = m_lines; QPen pen; pen.setWidth(2); while (it.hasNext()) { it.next(); pen.setColor(it.value()); painter.setPen(pen); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top(); painter.drawLine(6, y, width() - 6, y); } } // slider outline QColor sliderColor(highlightColor); sliderColor.setAlpha(50); painter.fillRect(sliderRect, sliderColor); painter.setPen(QPen(highlightColor, 0)); // rounded rect looks ugly for some reason, so we draw 4 lines. painter.drawLine(sliderRect.left(), sliderRect.top() + 1, sliderRect.left(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.right(), sliderRect.top() + 1, sliderRect.right(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.left() + 1, sliderRect.top(), sliderRect.right() - 1, sliderRect.top()); painter.drawLine(sliderRect.left() + 1, sliderRect.bottom(), sliderRect.right() - 1, sliderRect.bottom()); } void KateScrollBar::normalPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); if (!m_showMarks) { return; } QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect rect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); int sideMargin = width() - rect.width(); if (sideMargin < 4) { sideMargin = 4; } sideMargin /= 2; QHashIterator it = m_lines; while (it.hasNext()) { it.next(); painter.setPen(it.value()); if (it.key() < rect.top() || it.key() > rect.bottom()) { painter.drawLine(0, it.key(), width(), it.key()); } else { painter.drawLine(0, it.key(), sideMargin, it.key()); painter.drawLine(width() - sideMargin, it.key(), width(), it.key()); } } } void KateScrollBar::resizeEvent(QResizeEvent *e) { QScrollBar::resizeEvent(e); m_updateTimer.start(); m_lines.clear(); update(); } void KateScrollBar::sliderChange(SliderChange change) { // call parents implementation QScrollBar::sliderChange(change); if (change == QAbstractSlider::SliderValueChange) { redrawMarks(); } else if (change == QAbstractSlider::SliderRangeChange) { marksChanged(); } if (m_leftMouseDown || m_middleMouseDown) { const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } } void KateScrollBar::marksChanged() { m_lines.clear(); update(); } void KateScrollBar::redrawMarks() { if (!m_showMarks) { return; } update(); } void KateScrollBar::recomputeMarksPositions() { // get the style options to compute the scrollbar pixels QStyleOptionSlider opt; initStyleOption(&opt); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); // cache top margin and groove height const int top = grooveRect.top(); const int h = grooveRect.height() - 1; // make sure we have a sane height if (h <= 0) { return; } // get total visible (=without folded) lines in the document int visibleLines = m_view->textFolding().visibleLines() - 1; if (m_view->config()->scrollPastEnd()) { visibleLines += m_viewInternal->linesDisplayed() - 1; visibleLines -= m_view->config()->autoCenterLines(); } // now repopulate the scrollbar lines list m_lines.clear(); const QHash &marks = m_doc->marks(); for (QHash::const_iterator i = marks.constBegin(); i != marks.constEnd(); ++i) { KTextEditor::Mark *mark = i.value(); const int line = m_view->textFolding().lineToVisibleLine(mark->line); const double ratio = static_cast(line) / visibleLines; m_lines.insert(top + (int)(h * ratio), KateRendererConfig::global()->lineMarkerColor((KTextEditor::MarkInterface::MarkTypes)mark->type)); } } void KateScrollBar::sliderMaybeMoved(int value) { if (m_middleMouseDown) { // we only need to emit this signal once, as for the following slider // movements the signal sliderMoved() is already emitted. // Thus, set m_middleMouseDown to false right away. m_middleMouseDown = false; emit sliderMMBMoved(value); } } //END //BEGIN KateCmdLineEditFlagCompletion /** * This class provide completion of flags. It shows a short description of * each flag, and flags are appended. */ class KateCmdLineEditFlagCompletion : public KCompletion { public: KateCmdLineEditFlagCompletion() { ; } QString makeCompletion(const QString & /*s*/) override { return QString(); } }; //END KateCmdLineEditFlagCompletion //BEGIN KateCmdLineEdit KateCommandLineBar::KateCommandLineBar(KTextEditor::ViewPrivate *view, QWidget *parent) : KateViewBarWidget(true, parent) { QHBoxLayout *topLayout = new QHBoxLayout(); centralWidget()->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); m_lineEdit = new KateCmdLineEdit(this, view); connect(m_lineEdit, SIGNAL(hideRequested()), SIGNAL(hideMe())); topLayout->addWidget(m_lineEdit); QToolButton *helpButton = new QToolButton(this); helpButton->setAutoRaise(true); helpButton->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); topLayout->addWidget(helpButton); connect(helpButton, SIGNAL(clicked()), this, SLOT(showHelpPage())); setFocusProxy(m_lineEdit); } void KateCommandLineBar::showHelpPage() { KHelpClient::invokeHelp(QStringLiteral("advanced-editing-tools-commandline"), QStringLiteral("kate")); } KateCommandLineBar::~KateCommandLineBar() { } // inserts the given string in the command line edit and (if selected = true) selects it so the user // can type over it if they want to void KateCommandLineBar::setText(const QString &text, bool selected) { m_lineEdit->setText(text); if (selected) { m_lineEdit->selectAll(); } } void KateCommandLineBar::execute(const QString &text) { m_lineEdit->slotReturnPressed(text); } KateCmdLineEdit::KateCmdLineEdit(KateCommandLineBar *bar, KTextEditor::ViewPrivate *view) : KLineEdit() , m_view(view) , m_bar(bar) , m_msgMode(false) , m_histpos(0) , m_cmdend(0) , m_command(nullptr) { connect(this, SIGNAL(returnPressed(QString)), this, SLOT(slotReturnPressed(QString))); setCompletionObject(KateCmd::self()->commandCompletionObject()); setAutoDeleteCompletionObject(false); m_hideTimer = new QTimer(this); m_hideTimer->setSingleShot(true); connect(m_hideTimer, SIGNAL(timeout()), this, SLOT(hideLineEdit())); // make sure the timer is stopped when the user switches views. if not, focus will be given to the // wrong view when KateViewBar::hideCurrentBarWidget() is called after 4 seconds. (the timer is // used for showing things like "Success" for four seconds after the user has used the kate // command line) connect(m_view, SIGNAL(focusOut(KTextEditor::View*)), m_hideTimer, SLOT(stop())); } void KateCmdLineEdit::hideEvent(QHideEvent *e) { Q_UNUSED(e); } QString KateCmdLineEdit::helptext(const QPoint &) const { - QString beg = QStringLiteral("
Help: "); - QString mid = QStringLiteral("
"); - QString end = QStringLiteral("
"); + const QString beg = QStringLiteral("
Help: "); + const QString mid = QStringLiteral("
"); + const QString end = QStringLiteral("
"); - QString t = text(); - QRegExp re(QLatin1String("\\s*help\\s+(.*)")); - if (re.indexIn(t) > -1) { + const QString t = text(); + static const QRegularExpression re(QLatin1String("\\s*help\\s+(.*)")); + auto match = re.match(t); + if (match.hasMatch()) { QString s; // get help for command - QString name = re.cap(1); + const QString name = match.captured(1); if (name == QLatin1String("list")) { return beg + i18n("Available Commands") + mid + KateCmd::self()->commandList().join(QLatin1Char(' ')) + i18n("

For help on individual commands, do 'help <command>'

") + end; } else if (! name.isEmpty()) { KTextEditor::Command *cmd = KateCmd::self()->queryCommand(name); if (cmd) { if (cmd->help(m_view, name, s)) { return beg + name + mid + s + end; } else { return beg + name + mid + i18n("No help for '%1'", name) + end; } } else { return beg + mid + i18n("No such command %1", name) + end; } } } return beg + mid + i18n( "

This is the Katepart command line.
" "Syntax: command [ arguments ]
" "For a list of available commands, enter help list
" "For help for individual commands, enter help <command>

") + end; } bool KateCmdLineEdit::event(QEvent *e) { if (e->type() == QEvent::QueryWhatsThis) { setWhatsThis(helptext(QPoint())); e->accept(); return true; } return KLineEdit::event(e); } /** * Parse the text as a command. * * The following is a simple PEG grammar for the syntax of the command. * (A PEG grammar is like a BNF grammar, except that "/" stands for * ordered choice: only the first matching rule is used. In other words, * the parsing is short-circuited in the manner of the "or" operator in * programming languages, and so the grammar is unambiguous.) * * Text <- Range? Command * / Position * Range <- Position ("," Position)? * / "%" * Position <- Base Offset? * Base <- Line * / LastLine * / ThisLine * / Mark * Offset <- [+-] Base * Line <- [0-9]+ * LastLine <- "$" * ThisLine <- "." * Mark <- "'" [a-z] */ void KateCmdLineEdit::slotReturnPressed(const QString &text) { if (text.isEmpty()) { return; } // silently ignore leading space characters uint n = 0; const uint textlen = text.length(); while ((n < textlen) && (text[n].isSpace())) { n++; } if (n >= textlen) { return; } QString cmd = text.mid(n); // Parse any leading range expression, and strip it (and maybe do some other transforms on the command). QString leadingRangeExpression; KTextEditor::Range range = CommandRangeExpressionParser::parseRangeExpression(cmd, m_view, leadingRangeExpression, cmd); // Built in help: if the command starts with "help", [try to] show some help if (cmd.startsWith(QLatin1String("help"))) { QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), helptext(QPoint())); clear(); KateCmd::self()->appendHistory(cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); return; } if (cmd.length() > 0) { KTextEditor::Command *p = KateCmd::self()->queryCommand(cmd); m_oldText = leadingRangeExpression + cmd; m_msgMode = true; // the following commands changes the focus themselves, so bar should be hidden before execution. - if (QRegExp(QLatin1String("buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e")).exactMatch(cmd.split(QLatin1Char(' ')).at(0))) { + if (QRegularExpression(QLatin1String("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { emit hideRequested(); } if (!p) { setText(i18n("No such command: \"%1\"", cmd)); } else if (range.isValid() && !p->supportsRange(cmd)) { // Raise message, when the command does not support ranges. setText(i18n("Error: No range allowed for command \"%1\".", cmd)); } else { QString msg; if (p->exec(m_view, cmd, msg, range)) { // append command along with range (will be empty if none given) to history KateCmd::self()->appendHistory(leadingRangeExpression + cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); if (msg.length() > 0) { setText(i18n("Success: ") + msg); } else if (isVisible()) { // always hide on success without message emit hideRequested(); } } else { if (msg.length() > 0) { if (msg.contains(QLatin1Char('\n'))) { // multiline error, use widget with more space QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), msg); } else { setText(msg); } } else { setText(i18n("Command \"%1\" failed.", cmd)); } } } } // clean up if (completionObject() != KateCmd::self()->commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_command = nullptr; m_cmdend = 0; // the following commands change the focus themselves - if (!QRegExp(QLatin1String("buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e")).exactMatch(cmd.split(QLatin1Char(' ')).at(0))) { + if (!QRegularExpression(QLatin1String("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { m_view->setFocus(); } if (isVisible()) { m_hideTimer->start(4000); } } void KateCmdLineEdit::hideLineEdit() // unless i have focus ;) { if (! hasFocus()) { emit hideRequested(); } } void KateCmdLineEdit::focusInEvent(QFocusEvent *ev) { if (m_msgMode) { m_msgMode = false; setText(m_oldText); selectAll(); } KLineEdit::focusInEvent(ev); } void KateCmdLineEdit::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Escape || (ev->key() == Qt::Key_BracketLeft && ev->modifiers() == Qt::ControlModifier)) { m_view->setFocus(); hideLineEdit(); clear(); } else if (ev->key() == Qt::Key_Up) { fromHistory(true); } else if (ev->key() == Qt::Key_Down) { fromHistory(false); } uint cursorpos = cursorPosition(); KLineEdit::keyPressEvent(ev); // during typing, let us see if we have a valid command if (! m_cmdend || cursorpos <= m_cmdend) { QChar c; if (! ev->text().isEmpty()) { c = ev->text().at(0); } if (! m_cmdend && ! c.isNull()) { // we have no command, so lets see if we got one if (! c.isLetterOrNumber() && c != QLatin1Char('-') && c != QLatin1Char('_')) { m_command = KateCmd::self()->queryCommand(text().trimmed()); if (m_command) { //qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<queryCommand(text().trimmed()); if (m_command) { //qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_cmdend = 0; } } // if we got a command, check if it wants to do something. if (m_command) { KCompletion *cmpl = m_command->completionObject(m_view, text().left(m_cmdend).trimmed()); if (cmpl) { // We need to prepend the current command name + flag string // when completion is done //qCDebug(LOG_KTE)<<"keypress in commandline: Setting completion object!"; setCompletionObject(cmpl); } } } else if (m_command && !ev->text().isEmpty()) { // check if we should call the commands processText() if (m_command->wantsToProcessText(text().left(m_cmdend).trimmed())) { m_command->processText(m_view, text()); } } } void KateCmdLineEdit::fromHistory(bool up) { if (! KateCmd::self()->historyLength()) { return; } QString s; if (up) { if (m_histpos > 0) { m_histpos--; s = KateCmd::self()->fromHistory(m_histpos); } } else { if (m_histpos < (KateCmd::self()->historyLength() - 1)) { m_histpos++; s = KateCmd::self()->fromHistory(m_histpos); } else { m_histpos = KateCmd::self()->historyLength(); setText(m_oldText); } } if (! s.isEmpty()) { // Select the argument part of the command, so that it is easy to overwrite setText(s); - static QRegExp reCmd = QRegExp(QLatin1String(".*[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)")); - if (reCmd.indexIn(text()) == 0) { - setSelection(text().length() - reCmd.cap(1).length(), reCmd.cap(1).length()); + static const QRegularExpression reCmd(QLatin1String("^[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)")); + const auto match = reCmd.match(text()); + if (match.hasMatch()) { + setSelection(text().length() - match.capturedLength(1), match.capturedLength(1)); } } } //END KateCmdLineEdit //BEGIN KateIconBorder using namespace KTextEditor; KateIconBorder::KateIconBorder(KateViewInternal *internalView, QWidget *parent) : QWidget(parent) , m_view(internalView->m_view) , m_doc(internalView->doc()) , m_viewInternal(internalView) , m_iconBorderOn(false) , m_lineNumbersOn(false) , m_relLineNumbersOn(false) , m_updateRelLineNumbers(false) , m_foldingMarkersOn(false) , m_dynWrapIndicatorsOn(false) , m_annotationBorderOn(false) , m_updatePositionToArea(true) , m_annotationItemDelegate(new KateAnnotationItemDelegate(m_viewInternal, this)) { setAcceptDrops(true); setAttribute(Qt::WA_StaticContents); // See: https://doc.qt.io/qt-5/qwidget.html#update. As this widget does not // have a background, there's no need for Qt to erase the widget's area // before repainting. Enabling this prevents flickering when the widget is // repainted. setAttribute(Qt::WA_OpaquePaintEvent); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); setMouseTracking(true); m_doc->setMarkDescription(MarkInterface::markType01, i18n("Bookmark")); m_doc->setMarkPixmap(MarkInterface::markType01, QIcon::fromTheme(QStringLiteral("bookmarks")).pixmap(32, 32)); connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); updateFont(); m_antiFlickerTimer.setSingleShot(true); m_antiFlickerTimer.setInterval(300); connect(&m_antiFlickerTimer, &QTimer::timeout, this, &KateIconBorder::highlightFolding); // user interaction (scrolling) hides e.g. preview connect(m_view, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate*)), this, SLOT(displayRangeChanged())); } KateIconBorder::~KateIconBorder() { delete m_foldingPreview; delete m_foldingRange; } void KateIconBorder::setIconBorderOn(bool enable) { if (enable == m_iconBorderOn) { return; } m_iconBorderOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setAnnotationBorderOn(bool enable) { if (enable == m_annotationBorderOn) { return; } m_annotationBorderOn = enable; // make sure the tooltip is hidden if (!m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } emit m_view->annotationBorderVisibilityChanged(m_view, enable); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::removeAnnotationHovering() { // remove hovering if it's still there if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::setLineNumbersOn(bool enable) { if (enable == m_lineNumbersOn) { return; } m_lineNumbersOn = enable; m_dynWrapIndicatorsOn = (m_dynWrapIndicators == 1) ? enable : m_dynWrapIndicators; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setRelLineNumbersOn(bool enable) { if (enable == m_relLineNumbersOn) { return; } m_relLineNumbersOn = enable; /* * We don't have to touch the m_dynWrapIndicatorsOn because * we already got it right from the m_lineNumbersOn */ m_updatePositionToArea = true; QTimer::singleShot( 0, this, SLOT(update()) ); } void KateIconBorder::updateForCursorLineChange() { if (m_relLineNumbersOn) { m_updateRelLineNumbers = true; } // always do normal update, e.g. for different current line color! update(); } void KateIconBorder::setDynWrapIndicators(int state) { if (state == m_dynWrapIndicators) { return; } m_dynWrapIndicators = state; m_dynWrapIndicatorsOn = (state == 1) ? m_lineNumbersOn : state; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setFoldingMarkersOn(bool enable) { if (enable == m_foldingMarkersOn) { return; } m_foldingMarkersOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } QSize KateIconBorder::sizeHint() const { int w = 1; // Must be any value != 0 or we will never painted! const int i = m_positionToArea.size(); if (i > 0) { w = m_positionToArea.at(i - 1).first; } return QSize(w, 0); } // This function (re)calculates the maximum width of any of the digit characters (0 -> 9) // for graceful handling of variable-width fonts as the linenumber font. void KateIconBorder::updateFont() { const QFontMetricsF &fm = m_view->renderer()->config()->fontMetrics(); m_maxCharWidth = 0.0; // Loop to determine the widest numeric character in the current font. // 48 is ascii '0' for (int i = 48; i < 58; i++) { const qreal charWidth = ceil(fm.width(QChar(i))); m_maxCharWidth = qMax(m_maxCharWidth, charWidth); } // NOTE/TODO(or not) Take size of m_dynWrapIndicatorChar into account. // It's a multi-char and it's reported size is, even with a mono-space font, // bigger than each digit, e.g. 10 vs 12. Currently it seems to work even with // "Line Numbers Off" but all these width calculating looks slightly hacky // the icon pane scales with the font... m_iconAreaWidth = fm.height(); // Only for now, later may that become an own value m_foldingAreaWidth = m_iconAreaWidth; calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } int KateIconBorder::lineNumberWidth() const { int width = 0; // Avoid unneeded expensive calculations ;-) if (m_lineNumbersOn) { // width = (number of digits + 1) * char width const int digits = (int) ceil(log10((double)(m_view->doc()->lines() + 1))); width = (int)ceil((digits + 1) * m_maxCharWidth); } if ((width < 1) && m_dynWrapIndicatorsOn && m_view->config()->dynWordWrap()) { // FIXME Why 2x? because of above (number of digits + 1) // -> looks to me like a hint for bad calculation elsewhere width = 2 * m_maxCharWidth; } return width; } void KateIconBorder::dragMoveEvent(QDragMoveEvent *event) { // FIXME Just calling m_view->m_viewInternal->dragMoveEvent(e) don't work // as intended, we need to set the cursor at column 1 // Is there a way to change the pos of the event? QPoint pos(0, event->pos().y()); // Code copy of KateViewInternal::dragMoveEvent m_view->m_viewInternal->placeCursor(pos, true, false); m_view->m_viewInternal->fixDropEvent(event); } void KateIconBorder::dropEvent(QDropEvent *event) { m_view->m_viewInternal->dropEvent(event); } void KateIconBorder::paintEvent(QPaintEvent *e) { paintBorder(e->rect().x(), e->rect().y(), e->rect().width(), e->rect().height()); } static void paintTriangle(QPainter &painter, QColor c, int xOffset, int yOffset, int width, int height, bool open) { painter.setRenderHint(QPainter::Antialiasing); qreal size = qMin(width, height); if (open) { // Paint unfolded icon less pushy if (KColorUtils::luma(c) < 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } else { // Paint folded icon in contrast to popup highlighting if (KColorUtils::luma(c) > 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } QPen pen; pen.setJoinStyle(Qt::RoundJoin); pen.setColor(c); pen.setWidthF(1.5); painter.setPen(pen); painter.setBrush(c); // let some border, if possible size *= 0.6; qreal halfSize = size / 2; qreal halfSizeP = halfSize * 0.6; QPointF middle(xOffset + (qreal)width / 2, yOffset + (qreal)height / 2); if (open) { QPointF points[3] = { middle + QPointF(-halfSize, -halfSizeP), middle + QPointF(halfSize, -halfSizeP), middle + QPointF(0, halfSizeP) }; painter.drawConvexPolygon(points, 3); } else { QPointF points[3] = { middle + QPointF(-halfSizeP, -halfSize), middle + QPointF(-halfSizeP, halfSize), middle + QPointF(halfSizeP, 0) }; painter.drawConvexPolygon(points, 3); } painter.setRenderHint(QPainter::Antialiasing, false); } /** * Helper class for an identifier which can be an empty or non-empty string or invalid. * Avoids complicated explicit statements in code dealing with the identifier * received as QVariant from a model. */ class KateAnnotationGroupIdentifier { public: KateAnnotationGroupIdentifier(const QVariant &identifier) : m_isValid(identifier.isValid() && identifier.canConvert()) , m_id(m_isValid ? identifier.toString() : QString()) { } KateAnnotationGroupIdentifier() = default; KateAnnotationGroupIdentifier(const KateAnnotationGroupIdentifier &rhs) = default; KateAnnotationGroupIdentifier& operator=(const KateAnnotationGroupIdentifier &rhs) { m_isValid = rhs.m_isValid; m_id = rhs.m_id; return *this; } KateAnnotationGroupIdentifier& operator=(const QVariant &identifier) { m_isValid = (identifier.isValid() && identifier.canConvert()); if (m_isValid) { m_id = identifier.toString(); } else { m_id.clear(); } return *this; } bool operator==(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid == rhs.m_isValid) && (!m_isValid || (m_id == rhs.m_id)); } bool operator!=(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid != rhs.m_isValid) || (m_isValid && (m_id != rhs.m_id)); } void clear() { m_isValid = false; m_id.clear(); } bool isValid() const { return m_isValid; } const QString& id() const { return m_id; } private: bool m_isValid = false; QString m_id; }; /** * Helper class for iterative calculation of data regarding the position * of a line with regard to annotation item grouping. */ class KateAnnotationGroupPositionState { public: /** * @param startz first rendered displayed line * @param isUsed flag whether the KateAnnotationGroupPositionState object will * be used or is just created due to being on the stack */ KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed); /** * @param styleOption option to fill with data for the given line * @param z rendered displayed line * @param realLine real line which is rendered here (passed to avoid another look-up) */ void nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine); private: KateViewInternal *m_viewInternal; const KTextEditor::AnnotationModel * const m_model; const QString m_hoveredAnnotationGroupIdentifier; int m_visibleWrappedLineInAnnotationGroup = -1; KateAnnotationGroupIdentifier m_lastAnnotationGroupIdentifier; KateAnnotationGroupIdentifier m_nextAnnotationGroupIdentifier; bool m_isSameAnnotationGroupsSinceLast = false; }; KateAnnotationGroupPositionState::KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed) : m_viewInternal(viewInternal) , m_model(model) , m_hoveredAnnotationGroupIdentifier(hoveredAnnotationGroupIdentifier) { if (!isUsed) { return; } if (!m_model || (static_cast(startz) >= m_viewInternal->cache()->viewCacheLineCount())) { return; } const auto realLineAtStart = m_viewInternal->cache()->viewLine(startz).line(); m_nextAnnotationGroupIdentifier = m_model->data(realLineAtStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_nextAnnotationGroupIdentifier.isValid()) { // estimate state of annotation group before first rendered line if (startz == 0) { if (realLineAtStart > 0) { // TODO: here we would want to scan until the next line that would be displayed, // to see if there are any group changes until then // for now simply taking neighbour line into account, not a grave bug on the first displayed line m_lastAnnotationGroupIdentifier = m_model->data(realLineAtStart - 1, (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole); m_isSameAnnotationGroupsSinceLast = (m_lastAnnotationGroupIdentifier == m_nextAnnotationGroupIdentifier); } } else { const auto realLineBeforeStart = m_viewInternal->cache()->viewLine(startz-1).line(); m_lastAnnotationGroupIdentifier = m_model->data(realLineBeforeStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_lastAnnotationGroupIdentifier.isValid()) { if (m_lastAnnotationGroupIdentifier.id() == m_nextAnnotationGroupIdentifier.id()) { m_isSameAnnotationGroupsSinceLast = true; // estimate m_visibleWrappedLineInAnnotationGroup from lines before startz for (uint z = startz; z > 0; --z) { const auto realLine = m_viewInternal->cache()->viewLine(z-1).line(); const KateAnnotationGroupIdentifier identifier = m_model->data(realLine, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (identifier != m_lastAnnotationGroupIdentifier) { break; } ++m_visibleWrappedLineInAnnotationGroup; } } } } } } void KateAnnotationGroupPositionState::nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine) { styleOption.wrappedLine = m_viewInternal->cache()->viewLine(z).viewLine(); styleOption.wrappedLineCount = m_viewInternal->cache()->viewLineCount(realLine); // Estimate position in group const KateAnnotationGroupIdentifier annotationGroupIdentifier = m_nextAnnotationGroupIdentifier; bool isSameAnnotationGroupsSinceThis = false; // Calculate next line's group identifier // shortcut: assuming wrapped lines are always displayed together, test is simple if (styleOption.wrappedLine+1 < styleOption.wrappedLineCount) { m_nextAnnotationGroupIdentifier = annotationGroupIdentifier; isSameAnnotationGroupsSinceThis = true; } else { if (static_cast(z+1) < m_viewInternal->cache()->viewCacheLineCount()) { const int realLineAfter = m_viewInternal->cache()->viewLine(z+1).line(); // search for any realLine with a different group id, also the non-displayed int rl = realLine + 1; for (; rl <= realLineAfter; ++rl) { m_nextAnnotationGroupIdentifier = m_model->data(rl, (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole); if (!m_nextAnnotationGroupIdentifier.isValid() || (m_nextAnnotationGroupIdentifier.id() != annotationGroupIdentifier.id())) { break; } } isSameAnnotationGroupsSinceThis = (rl > realLineAfter); if (rl < realLineAfter) { m_nextAnnotationGroupIdentifier = m_model->data(realLineAfter, (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole); } } else { // TODO: check next line after display end m_nextAnnotationGroupIdentifier.clear(); isSameAnnotationGroupsSinceThis = false; } } if (annotationGroupIdentifier.isValid()) { if (m_hoveredAnnotationGroupIdentifier == annotationGroupIdentifier.id()) { styleOption.state |= QStyle::State_MouseOver; } else { styleOption.state &= ~QStyle::State_MouseOver; } if (m_isSameAnnotationGroupsSinceLast) { ++m_visibleWrappedLineInAnnotationGroup; } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.annotationItemGroupingPosition = StyleOptionAnnotationItem::InGroup; if (!m_isSameAnnotationGroupsSinceLast) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupBegin; } if (!isSameAnnotationGroupsSinceThis) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupEnd; } } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.visibleWrappedLineInGroup = m_visibleWrappedLineInAnnotationGroup; m_lastAnnotationGroupIdentifier = m_nextAnnotationGroupIdentifier; m_isSameAnnotationGroupsSinceLast = isSameAnnotationGroupsSinceThis; } void KateIconBorder::paintBorder(int /*x*/, int y, int /*width*/, int height) { const uint h = m_view->renderer()->lineHeight(); const uint startz = (y / h); const uint endz = qMin(startz + 1 + (height / h), static_cast(m_viewInternal->cache()->viewCacheLineCount())); const uint currentLine = m_view->cursorPosition().line(); // center the folding boxes int m_px = (h - 11) / 2; if (m_px < 0) { m_px = 0; } // Ensure we miss no change of the count of line number digits const int newNeededWidth = lineNumberWidth(); if (m_updatePositionToArea || (newNeededWidth != m_lineNumberAreaWidth)) { m_lineNumberAreaWidth = newNeededWidth; m_updatePositionToArea = true; m_positionToArea.clear(); } QPainter p(this); p.setRenderHints(QPainter::TextAntialiasing); p.setFont(m_view->renderer()->config()->font()); // for line numbers KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, m_hoveredAnnotationGroupIdentifier, startz, m_annotationBorderOn); // Fetch often used data only once, improve readability const int w = width(); const QColor iconBarColor = m_view->renderer()->config()->iconBarColor(); // Effective our background const QColor lineNumberColor = m_view->renderer()->config()->lineNumberColor(); const QColor backgroundColor = m_view->renderer()->config()->backgroundColor(); // Of the edit area // Paint the border in chunks line by line for (uint z = startz; z < endz; z++) { // Painting coordinates, lineHeight * lineNumber const uint y = h * z; // Paint the border in chunks left->right, remember used width uint lnX = 0; // Paint background over full width... p.fillRect(lnX, y, w, h, iconBarColor); // ...and overpaint again the end to simulate some margin to the edit area, // so that the text not looks like stuck to the border p.fillRect(w - m_separatorWidth, y, w, h, backgroundColor); const KateTextLayout lineLayout = m_viewInternal->cache()->viewLine(z); int realLine = lineLayout.line(); if (realLine < 0) { // We have reached the end of the document, just paint background continue; } // icon pane if (m_iconBorderOn) { p.setPen(m_view->renderer()->config()->separatorColor()); p.setBrush(m_view->renderer()->config()->separatorColor()); p.drawLine(lnX + m_iconAreaWidth, y, lnX + m_iconAreaWidth, y + h); const uint mrk(m_doc->mark(realLine)); // call only once if (mrk && lineLayout.startCol() == 0) { for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { QPixmap px_mark(m_doc->markPixmap(markType)); px_mark.setDevicePixelRatio(devicePixelRatioF()); if (!px_mark.isNull() && h > 0 && m_iconAreaWidth > 0) { // scale up to a usable size const int s = qMin(m_iconAreaWidth * devicePixelRatioF(), h * devicePixelRatioF()) - 2; px_mark = px_mark.scaled(s, s, Qt::KeepAspectRatio, Qt::SmoothTransformation); // center the mark pixmap int x_px = 0.5 * qMax(m_iconAreaWidth - (s / devicePixelRatioF()), 0.0); int y_px = 0.5 * qMax(h - (s / devicePixelRatioF()), 0.0); p.drawPixmap(lnX + x_px, y + y_px, px_mark); } } } } lnX += m_iconAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, IconBorder)); } } // annotation information if (m_annotationBorderOn) { // Draw a border line between annotations and the line numbers p.setPen(lineNumberColor); p.setBrush(lineNumberColor); const qreal borderX = lnX + m_annotationAreaWidth + 0.5; p.drawLine(QPointF(borderX, y+0.5), QPointF(borderX, y + h - 0.5)); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect.setRect(lnX, y, m_annotationAreaWidth, h); annotationGroupPositionState.nextLine(styleOption, z, realLine); m_annotationItemDelegate->paint(&p, styleOption, model, realLine); } lnX += m_annotationAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, AnnotationBorder)); } } // line number if (m_lineNumbersOn || m_dynWrapIndicatorsOn) { QColor usedLineNumberColor; const int distanceToCurrent = abs(realLine - static_cast(currentLine)); if (distanceToCurrent == 0) { usedLineNumberColor = m_view->renderer()->config()->currentLineNumberColor(); } else { usedLineNumberColor = lineNumberColor; } p.setPen(usedLineNumberColor); p.setBrush(usedLineNumberColor); if (lineLayout.startCol() == 0) { if (m_relLineNumbersOn) { if (distanceToCurrent == 0) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter, QString::number(realLine + 1)); } else { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(distanceToCurrent)); } if (m_updateRelLineNumbers) { m_updateRelLineNumbers = false; update(); } } else if (m_lineNumbersOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(realLine + 1)); } } else if (m_dynWrapIndicatorsOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, m_dynWrapIndicatorChar); } lnX += m_lineNumberAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, LineNumbers)); } } // modified line system if (m_view->config()->lineModification() && !m_doc->url().isEmpty()) { const Kate::TextLine tl = m_doc->plainKateTextLine(realLine); if (tl->markedAsModified()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->modifiedLineColor()); } else if (tl->markedAsSavedOnDisk()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->savedLineColor()); } else { p.fillRect(lnX, y, m_modAreaWidth, h, iconBarColor); } lnX += m_modAreaWidth; // No m_separatorWidth if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, None)); } } // folding markers if (m_foldingMarkersOn) { const QColor foldingColor(m_view->renderer()->config()->foldingColor()); // possible additional folding highlighting if (m_foldingRange && m_foldingRange->overlapsLine(realLine)) { p.fillRect(lnX, y, m_foldingAreaWidth, h, foldingColor); } if (lineLayout.startCol() == 0) { QVector > startingRanges = m_view->textFolding().foldingRangesStartingOnLine(realLine); bool anyFolded = false; for (int i = 0; i < startingRanges.size(); ++i) { if (startingRanges[i].second & Kate::TextFolding::Folded) { anyFolded = true; } } const Kate::TextLine tl = m_doc->kateTextLine(realLine); if (!startingRanges.isEmpty() || tl->markedAsFoldingStart()) { if (anyFolded) { paintTriangle(p, foldingColor, lnX, y, m_foldingAreaWidth, h, false); } else { // Don't try to use currentLineNumberColor, the folded icon gets also not highligted paintTriangle(p, lineNumberColor, lnX, y, m_foldingAreaWidth, h, true); } } } lnX += m_foldingAreaWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, FoldingMarkers)); } } if (m_updatePositionToArea) { m_updatePositionToArea = false; // Don't forget our "text-stuck-to-border" protector lnX += m_separatorWidth; m_positionToArea.append(AreaPosition(lnX, None)); // Now that we know our needed space, ensure we are painted properly updateGeometry(); update(); return; } } } KateIconBorder::BorderArea KateIconBorder::positionToArea(const QPoint &p) const { for (int i = 0; i < m_positionToArea.size(); ++i) { if (p.x() <= m_positionToArea.at(i).first) { return m_positionToArea.at(i).second; } } return None; } void KateIconBorder::mousePressEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (t.isValid()) { m_lastClickedLine = t.line(); const auto area = positionToArea(e->pos()); // IconBorder and AnnotationBorder have their own behavior; don't forward to view if (area != IconBorder && area != AnnotationBorder) { const auto pos = QPoint(0, e->y()); if (area == LineNumbers && e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) { // setup view so the following mousePressEvent will select the line m_viewInternal->beginSelectLine(pos); } QMouseEvent forward(QEvent::MouseButtonPress, pos, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mousePressEvent(&forward); } return e->accept(); } QWidget::mousePressEvent(e); } void KateIconBorder::highlightFoldingDelayed(int line) { if ((line == m_currentLine) || (line >= m_doc->buffer().lines())) { return; } m_currentLine = line; if (m_foldingRange) { // We are for a while in the folding area, no need for delay highlightFolding(); } else if (!m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.start(); } } void KateIconBorder::highlightFolding() { /** * compute to which folding range we belong * FIXME: optimize + perhaps have some better threshold or use timers! */ KTextEditor::Range newRange = KTextEditor::Range::invalid(); for (int line = m_currentLine; line >= qMax(0, m_currentLine - 1024); --line) { /** * try if we have folding range from that line, should be fast per call */ KTextEditor::Range foldingRange = m_doc->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { continue; } /** * does the range reach us? */ if (foldingRange.overlapsLine(m_currentLine)) { newRange = foldingRange; break; } } if (newRange.isValid() && m_foldingRange && *m_foldingRange == newRange) { // new range equals the old one, nothing to do. return; } // the ranges differ, delete the old, if it exists delete m_foldingRange; m_foldingRange = nullptr; // New range, new preview! delete m_foldingPreview; bool showPreview = false; if (newRange.isValid()) { // When next line is not visible we have a folded range, only then we want a preview! showPreview = !m_view->textFolding().isLineVisible(newRange.start().line() + 1); //qCDebug(LOG_KTE) << "new folding hl-range:" << newRange; m_foldingRange = m_doc->newMovingRange(newRange, KTextEditor::MovingRange::ExpandRight); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); /** * create highlighting color * we avoid alpha as overpainting leads to ugly lines (https://bugreports.qt.io/browse/QTBUG-66036) */ attr->setBackground(QBrush(m_view->renderer()->config()->foldingColor())); m_foldingRange->setView(m_view); // use z depth defined in moving ranges interface m_foldingRange->setZDepth(-100.0); m_foldingRange->setAttribute(attr); } // show text preview, if a folded region starts here... // ...but only when main window is active (#392396) const bool isWindowActive = !window() || window()->isActiveWindow(); if (showPreview && m_view->config()->foldingPreview() && isWindowActive) { m_foldingPreview = new KateTextPreview(m_view, this); m_foldingPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_foldingPreview->setFrameStyle(QFrame::StyledPanel); // Calc how many lines can be displayed in the popup const int lineHeight = m_view->renderer()->lineHeight(); const int foldingStartLine = m_foldingRange->start().line(); // FIXME Is there really no easier way to find lineInDisplay? const QPoint pos = m_viewInternal->mapFrom(m_view, m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))); const int lineInDisplay = pos.y() / lineHeight; // Allow slightly overpainting of the view bottom to proper cover all lines const int extra = (m_viewInternal->height() % lineHeight) > (lineHeight * 0.6) ? 1 : 0; const int lineCount = qMin(m_foldingRange->numberOfLines() + 1, m_viewInternal->linesDisplayed() - lineInDisplay + extra); m_foldingPreview->resize(m_viewInternal->width(), lineCount * lineHeight + 2 * m_foldingPreview->frameWidth()); const int xGlobal = mapToGlobal(QPoint(width(), 0)).x(); const int yGlobal = m_view->mapToGlobal(m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))).y(); m_foldingPreview->move(QPoint(xGlobal, yGlobal) - m_foldingPreview->contentsRect().topLeft()); m_foldingPreview->setLine(foldingStartLine); m_foldingPreview->setCenterView(false); m_foldingPreview->setShowFoldedLines(true); m_foldingPreview->raise(); m_foldingPreview->show(); } } void KateIconBorder::hideFolding() { if (m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.stop(); } m_currentLine = -1; delete m_foldingRange; m_foldingRange = nullptr; delete m_foldingPreview; } void KateIconBorder::leaveEvent(QEvent *event) { hideFolding(); removeAnnotationHovering(); QWidget::leaveEvent(event); } void KateIconBorder::mouseMoveEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (!t.isValid()) { // Cleanup everything which may be shown removeAnnotationHovering(); hideFolding(); } else { const BorderArea area = positionToArea(e->pos()); if (area == FoldingMarkers) { highlightFoldingDelayed(t.line()); } else { hideFolding(); } if (area == AnnotationBorder) { KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { m_hoveredAnnotationGroupIdentifier = model->data( t.line(), (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole ).toString(); const QPoint viewRelativePos = m_view->mapFromGlobal(e->globalPos()); QHelpEvent helpEvent(QEvent::ToolTip, viewRelativePos, e->globalPos()); KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect = annotationLineRectInView(t.line()); setStyleOptionLineData(&styleOption, e->y(), t.line(), model, m_hoveredAnnotationGroupIdentifier); m_annotationItemDelegate->helpEvent(&helpEvent, m_view, styleOption, model, t.line()); QTimer::singleShot(0, this, SLOT(update())); } } else { if (area == IconBorder) { m_doc->requestMarkTooltip(t.line(), e->globalPos()); } m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } if (area != IconBorder) { QPoint p = m_viewInternal->mapFromGlobal(e->globalPos()); QMouseEvent forward(QEvent::MouseMove, p, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseMoveEvent(&forward); } } QWidget::mouseMoveEvent(e); } void KateIconBorder::mouseReleaseEvent(QMouseEvent *e) { const int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine >= 0 && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); if (area == IconBorder) { if (e->button() == Qt::LeftButton) { if (!m_doc->handleMarkClick(cursorOnLine)) { KateViewConfig *config = m_view->config(); const uint editBits = m_doc->editableMarks(); // is the default or the only editable mark const uint singleMark = qPopulationCount(editBits) > 1 ? editBits & config->defaultMarkType() : editBits; if (singleMark) { if (m_doc->mark(cursorOnLine) & singleMark) { m_doc->removeMark(cursorOnLine, singleMark); } else { m_doc->addMark(cursorOnLine, singleMark); } } else if (config->allowMarkMenu()) { showMarkMenu(cursorOnLine, QCursor::pos()); } } } else if (e->button() == Qt::RightButton) { showMarkMenu(cursorOnLine, QCursor::pos()); } } if (area == FoldingMarkers) { // Prefer the highlighted range over the exact clicked line const int lineToToggle = m_foldingRange ? m_foldingRange->toRange().start().line() : cursorOnLine; if (e->button() == Qt::LeftButton) { m_view->toggleFoldingOfLine(lineToToggle); } else if (e->button() == Qt::RightButton) { m_view->toggleFoldingsInRange(lineToToggle); } delete m_foldingPreview; } if (area == AnnotationBorder) { const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (e->button() == Qt::LeftButton && singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } else if (e->button() == Qt::RightButton) { showAnnotationMenu(cursorOnLine, e->globalPos()); } } } QMouseEvent forward(QEvent::MouseButtonRelease, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseReleaseEvent(&forward); } void KateIconBorder::mouseDoubleClickEvent(QMouseEvent *e) { int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (area == AnnotationBorder && !singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } } QMouseEvent forward(QEvent::MouseButtonDblClick, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseDoubleClickEvent(&forward); } void KateIconBorder::wheelEvent(QWheelEvent *e) { QCoreApplication::sendEvent(m_viewInternal, e); } void KateIconBorder::showMarkMenu(uint line, const QPoint &pos) { if (m_doc->handleMarkContextMenu(line, pos)) { return; } if (!m_view->config()->allowMarkMenu()) { return; } QMenu markMenu; QMenu selectDefaultMark; auto selectDefaultMarkActionGroup = new QActionGroup(&selectDefaultMark); QVector vec(33); int i = 1; for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (!(m_doc->editableMarks() & markType)) { continue; } QAction *mA; QAction *dMA; const QPixmap icon = m_doc->markPixmap(markType); if (!m_doc->markDescription(markType).isEmpty()) { mA = markMenu.addAction(icon, m_doc->markDescription(markType)); dMA = selectDefaultMark.addAction(icon, m_doc->markDescription(markType)); } else { mA = markMenu.addAction(icon, i18n("Mark Type %1", bit + 1)); dMA = selectDefaultMark.addAction(icon, i18n("Mark Type %1", bit + 1)); } selectDefaultMarkActionGroup->addAction(dMA); mA->setData(i); mA->setCheckable(true); dMA->setData(i + 100); dMA->setCheckable(true); if (m_doc->mark(line) & markType) { mA->setChecked(true); } if (markType & KateViewConfig::global()->defaultMarkType()) { dMA->setChecked(true); } vec[i++] = markType; } if (markMenu.actions().count() == 0) { return; } if (markMenu.actions().count() > 1) { markMenu.addAction(i18n("Set Default Mark Type"))->setMenu(&selectDefaultMark); } QAction *rA = markMenu.exec(pos); if (!rA) { return; } int result = rA->data().toInt(); if (result > 100) { KateViewConfig::global()->setValue(KateViewConfig::DefaultMarkType, vec[result - 100]); } else { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes) vec[result]; if (m_doc->mark(line) & markType) { m_doc->removeMark(line, markType); } else { m_doc->addMark(line, markType); } } } KTextEditor::AbstractAnnotationItemDelegate* KateIconBorder::annotationItemDelegate() const { return m_annotationItemDelegate; } void KateIconBorder::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate) { if (delegate == m_annotationItemDelegate) { return; } // reset to default, but already on that? if (!delegate && m_isDefaultAnnotationItemDelegate) { // nothing to do return; } // make sure the tooltip is hidden if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } disconnect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (!m_isDefaultAnnotationItemDelegate) { disconnect(m_annotationItemDelegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } if (!delegate) { // reset to a default delegate m_annotationItemDelegate = new KateAnnotationItemDelegate(m_viewInternal, this); m_isDefaultAnnotationItemDelegate = true; } else { // drop any default delegate if (m_isDefaultAnnotationItemDelegate) { delete m_annotationItemDelegate; m_isDefaultAnnotationItemDelegate = false; } m_annotationItemDelegate = delegate; // catch delegate being destroyed connect(delegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (m_annotationBorderOn) { updateGeometry(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::handleDestroyedAnnotationItemDelegate() { setAnnotationItemDelegate(nullptr); } void KateIconBorder::initStyleOption(KTextEditor::StyleOptionAnnotationItem* styleOption) const { styleOption->initFrom(this); styleOption->view = m_view; styleOption->decorationSize = QSize(m_iconAreaWidth, m_iconAreaWidth); styleOption->contentFontMetrics = m_view->renderer()->config()->fontMetrics(); } void KateIconBorder::setStyleOptionLineData(KTextEditor::StyleOptionAnnotationItem* styleOption, int y, int realLine, const KTextEditor::AnnotationModel *model, const QString &annotationGroupIdentifier) const { // calculate rendered displayed line const uint h = m_view->renderer()->lineHeight(); const uint z = (y / h); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, annotationGroupIdentifier, z, true); annotationGroupPositionState.nextLine(*styleOption, z, realLine); } QRect KateIconBorder::annotationLineRectInView(int line) const { int x = 0; if (m_iconBorderOn) { x += m_iconAreaWidth + 2; } const int y = m_view->m_viewInternal->lineToY(line); return QRect(x, y, m_annotationAreaWidth, m_view->renderer()->lineHeight()); } void KateIconBorder::updateAnnotationLine(int line) { // TODO: why has the default value been 8, where is that magic number from? int width = 8; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); width = m_annotationItemDelegate->sizeHint(styleOption, model, line).width(); } if (width > m_annotationAreaWidth) { m_annotationAreaWidth = width; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::showAnnotationMenu(int line, const QPoint &pos) { QMenu menu; QAction a(i18n("Disable Annotation Bar"), &menu); a.setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); menu.addAction(&a); emit m_view->annotationContextMenuAboutToShow(m_view, &menu, line); if (menu.exec(pos) == &a) { m_view->setAnnotationBorderVisible(false); } } void KateIconBorder::hideAnnotationTooltip() { m_annotationItemDelegate->hideTooltip(m_view); } void KateIconBorder::updateAnnotationBorderWidth() { calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::calcAnnotationBorderWidth() { // TODO: another magic number, not matching the one in updateAnnotationLine() m_annotationAreaWidth = 6; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); const int lineCount = m_view->doc()->lines(); if (lineCount > 0) { const int checkedLineCount = m_hasUniformAnnotationItemSizes ? 1 : lineCount; for (int i = 0; i < checkedLineCount; ++i) { const int curwidth = m_annotationItemDelegate->sizeHint(styleOption, model, i).width(); if (curwidth > m_annotationAreaWidth) { m_annotationAreaWidth = curwidth; } } } } } void KateIconBorder::annotationModelChanged(KTextEditor::AnnotationModel *oldmodel, KTextEditor::AnnotationModel *newmodel) { if (oldmodel) { oldmodel->disconnect(this); } if (newmodel) { connect(newmodel, SIGNAL(reset()), this, SLOT(updateAnnotationBorderWidth())); connect(newmodel, SIGNAL(lineChanged(int)), this, SLOT(updateAnnotationLine(int))); } updateAnnotationBorderWidth(); } void KateIconBorder::displayRangeChanged() { hideFolding(); removeAnnotationHovering(); } //END KateIconBorder //BEGIN KateViewEncodingAction // According to http://www.iana.org/assignments/ianacharset-mib // the default/unknown mib value is 2. #define MIB_DEFAULT 2 bool lessThanAction(KSelectAction *a, KSelectAction *b) { return a->text() < b->text(); } void KateViewEncodingAction::Private::init() { QList actions; q->setToolBarMode(MenuMode); int i; foreach (const QStringList &encodingsForScript, KCharsets::charsets()->encodingsByScript()) { KSelectAction *tmp = new KSelectAction(encodingsForScript.at(0), q); for (i = 1; i < encodingsForScript.size(); ++i) { tmp->addAction(encodingsForScript.at(i)); } q->connect(tmp, SIGNAL(triggered(QAction*)), q, SLOT(_k_subActionTriggered(QAction*))); //tmp->setCheckable(true); actions << tmp; } std::sort(actions.begin(), actions.end(), lessThanAction); foreach (KSelectAction *action, actions) { q->addAction(action); } } void KateViewEncodingAction::Private::_k_subActionTriggered(QAction *action) { if (currentSubAction == action) { return; } currentSubAction = action; bool ok = false; int mib = q->mibForName(action->text(), &ok); if (ok) { emit q->KSelectAction::triggered(action->text()); emit q->triggered(q->codecForMib(mib)); } } KateViewEncodingAction::KateViewEncodingAction(KTextEditor::DocumentPrivate *_doc, KTextEditor::ViewPrivate *_view, const QString &text, QObject *parent, bool saveAsMode) : KSelectAction(text, parent), doc(_doc), view(_view), d(new Private(this)) , m_saveAsMode(saveAsMode) { d->init(); connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); } KateViewEncodingAction::~KateViewEncodingAction() { delete d; } void KateViewEncodingAction::slotAboutToShow() { setCurrentCodec(doc->config()->encoding()); } void KateViewEncodingAction::setEncoding(const QString &e) { /** * in save as mode => trigger saveAs */ if (m_saveAsMode) { doc->documentSaveAsWithEncoding(e); return; } /** * else switch encoding */ doc->userSetEncodingForNextReload(); doc->setEncoding(e); view->reloadFile(); } int KateViewEncodingAction::mibForName(const QString &codecName, bool *ok) const { // FIXME logic is good but code is ugly bool success = false; int mib = MIB_DEFAULT; KCharsets *charsets = KCharsets::charsets(); QTextCodec *codec = charsets->codecForName(codecName, success); if (!success) { // Maybe we got a description name instead codec = charsets->codecForName(charsets->encodingForName(codecName), success); } if (codec) { mib = codec->mibEnum(); } if (ok) { *ok = success; } if (success) { return mib; } qCWarning(LOG_KTE) << "Invalid codec name: " << codecName; return MIB_DEFAULT; } QTextCodec *KateViewEncodingAction::codecForMib(int mib) const { if (mib == MIB_DEFAULT) { // FIXME offer to change the default codec return QTextCodec::codecForLocale(); } else { return QTextCodec::codecForMib(mib); } } QTextCodec *KateViewEncodingAction::currentCodec() const { return codecForMib(currentCodecMib()); } bool KateViewEncodingAction::setCurrentCodec(QTextCodec *codec) { disconnect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); int i, j; for (i = 0; i < actions().size(); ++i) { if (actions().at(i)->menu()) { for (j = 0; j < actions().at(i)->menu()->actions().size(); ++j) { if (!j && !actions().at(i)->menu()->actions().at(j)->data().isNull()) { continue; } if (actions().at(i)->menu()->actions().at(j)->isSeparator()) { continue; } if (codec == KCharsets::charsets()->codecForName(actions().at(i)->menu()->actions().at(j)->text())) { d->currentSubAction = actions().at(i)->menu()->actions().at(j); d->currentSubAction->setChecked(true); } else { actions().at(i)->menu()->actions().at(j)->setChecked(false); } } } } connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); return true; } QString KateViewEncodingAction::currentCodecName() const { return d->currentSubAction->text(); } bool KateViewEncodingAction::setCurrentCodec(const QString &codecName) { return setCurrentCodec(KCharsets::charsets()->codecForName(codecName)); } int KateViewEncodingAction::currentCodecMib() const { return mibForName(currentCodecName()); } bool KateViewEncodingAction::setCurrentCodec(int mib) { return setCurrentCodec(codecForMib(mib)); } //END KateViewEncodingAction //BEGIN KateViewBar related classes KateViewBarWidget::KateViewBarWidget(bool addCloseButton, QWidget *parent) : QWidget(parent) , m_viewBar(nullptr) { QHBoxLayout *layout = new QHBoxLayout(this); // NOTE: Here be cosmetics. layout->setContentsMargins(0, 0, 0, 0); // hide button if (addCloseButton) { QToolButton *hideButton = new QToolButton(this); hideButton->setAutoRaise(true); hideButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); connect(hideButton, SIGNAL(clicked()), SIGNAL(hideMe())); layout->addWidget(hideButton); layout->setAlignment(hideButton, Qt::AlignLeft | Qt::AlignTop); } // widget to be used as parent for the real content m_centralWidget = new QWidget(this); layout->addWidget(m_centralWidget); setLayout(layout); setFocusProxy(m_centralWidget); } KateViewBar::KateViewBar(bool external, QWidget *parent, KTextEditor::ViewPrivate *view) : QWidget(parent), m_external(external), m_view(view), m_permanentBarWidget(nullptr) { m_layout = new QVBoxLayout(this); m_stack = new QStackedWidget(this); m_layout->addWidget(m_stack); m_layout->setContentsMargins(0, 0, 0, 0); m_stack->hide(); hide(); } void KateViewBar::addBarWidget(KateViewBarWidget *newBarWidget) { // just ignore additional adds for already existing widgets if (hasBarWidget(newBarWidget)) { return; } // add new widget, invisible... newBarWidget->hide(); m_stack->addWidget(newBarWidget); newBarWidget->setAssociatedViewBar(this); connect(newBarWidget, SIGNAL(hideMe()), SLOT(hideCurrentBarWidget())); } void KateViewBar::removeBarWidget(KateViewBarWidget *barWidget) { // remove only if there if (!hasBarWidget(barWidget)) { return; } m_stack->removeWidget(barWidget); barWidget->setAssociatedViewBar(nullptr); barWidget->hide(); disconnect(barWidget, nullptr, this, nullptr); } void KateViewBar::addPermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget); Q_ASSERT(!m_permanentBarWidget); m_stack->addWidget(barWidget); m_stack->setCurrentWidget(barWidget); m_stack->show(); m_permanentBarWidget = barWidget; m_permanentBarWidget->show(); setViewBarVisible(true); } void KateViewBar::removePermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(m_permanentBarWidget == barWidget); Q_UNUSED(barWidget); const bool hideBar = m_stack->currentWidget() == m_permanentBarWidget; m_permanentBarWidget->hide(); m_stack->removeWidget(m_permanentBarWidget); m_permanentBarWidget = nullptr; if (hideBar) { m_stack->hide(); setViewBarVisible(false); } } bool KateViewBar::hasPermanentWidget(KateViewBarWidget *barWidget) const { return (m_permanentBarWidget == barWidget); } void KateViewBar::showBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget != nullptr); if (barWidget != qobject_cast(m_stack->currentWidget())) { hideCurrentBarWidget(); } // raise correct widget m_stack->setCurrentWidget(barWidget); barWidget->show(); barWidget->setFocus(Qt::ShortcutFocusReason); m_stack->show(); setViewBarVisible(true); } bool KateViewBar::hasBarWidget(KateViewBarWidget *barWidget) const { return m_stack->indexOf(barWidget) != -1; } void KateViewBar::hideCurrentBarWidget() { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (current) { current->closed(); } // if we have any permanent widget, make it visible again if (m_permanentBarWidget) { m_stack->setCurrentWidget (m_permanentBarWidget); } else { // else: hide the bar m_stack->hide(); setViewBarVisible(false); } m_view->setFocus(); } void KateViewBar::setViewBarVisible(bool visible) { if (m_external) { if (visible) { m_view->mainWindow()->showViewBar(m_view); } else { m_view->mainWindow()->hideViewBar(m_view); } } else { setVisible(visible); } } bool KateViewBar::hiddenOrPermanent() const { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (!isVisible() || (m_permanentBarWidget && m_permanentBarWidget == current)) { return true; } return false; } void KateViewBar::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { hideCurrentBarWidget(); return; } QWidget::keyPressEvent(event); } void KateViewBar::hideEvent(QHideEvent *event) { Q_UNUSED(event); // if (!event->spontaneous()) // m_view->setFocus(); } //END KateViewBar related classes KatePasteMenu::KatePasteMenu(const QString &text, KTextEditor::ViewPrivate *view) : KActionMenu(text, view) , m_view(view) { connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); } void KatePasteMenu::slotAboutToShow() { menu()->clear(); /** * insert complete paste history */ int i = 0; Q_FOREACH (const QString &text, KTextEditor::EditorPrivate::self()->clipboardHistory()) { /** * get text for the menu ;) */ QString leftPart = (text.size() > 48) ? (text.left(48) + QLatin1String("...")) : text; QAction *a = menu()->addAction(leftPart.replace(QLatin1String("\n"), QLatin1String(" ")), this, SLOT(paste())); a->setData(i++); } } void KatePasteMenu::paste() { if (!sender()) { return; } QAction *action = qobject_cast(sender()); if (!action) { return; } // get index int i = action->data().toInt(); if (i >= KTextEditor::EditorPrivate::self()->clipboardHistory().size()) { return; } // paste m_view->paste(&KTextEditor::EditorPrivate::self()->clipboardHistory()[i]); } diff --git a/src/vimode/appcommands.cpp b/src/vimode/appcommands.cpp index 66502a01..45ac55e2 100644 --- a/src/vimode/appcommands.cpp +++ b/src/vimode/appcommands.cpp @@ -1,523 +1,525 @@ /* This file is part of the KDE libraries Copyright (C) 2009 Erlend Hamberg Copyright (C) 2011 Svyatoslav Kuzmich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include +#include #include #include #include #include #include #include #include #include using namespace KateVi; //BEGIN AppCommands AppCommands *AppCommands::m_instance = nullptr; AppCommands::AppCommands() : KTextEditor::Command({ QStringLiteral("q"), QStringLiteral("qa"), QStringLiteral("qall"), QStringLiteral("q!"), QStringLiteral("qa!"), QStringLiteral("qall!") , QStringLiteral("w"), QStringLiteral("wq"), QStringLiteral("wa"), QStringLiteral("wqa"), QStringLiteral("x"), QStringLiteral("xa"), QStringLiteral("new") , QStringLiteral("vnew"), QStringLiteral("e"), QStringLiteral("edit"), QStringLiteral("enew"), QStringLiteral("sp"), QStringLiteral("split"), QStringLiteral("vs") , QStringLiteral("vsplit"), QStringLiteral("only"), QStringLiteral("tabe"), QStringLiteral("tabedit"), QStringLiteral("tabnew"), QStringLiteral("bd") , QStringLiteral("bdelete"), QStringLiteral("tabc"), QStringLiteral("tabclose"), QStringLiteral("clo"), QStringLiteral("close") }) + , re_write(QStringLiteral("^w(a)?$")) + , re_close(QStringLiteral("^bd(elete)?|tabc(lose)?$")) + , re_quit(QStringLiteral("^(w)?q(a|all)?(!)?$")) + , re_exit(QStringLiteral("^x(a)?$")) + , re_edit(QStringLiteral("^e(dit)?|tabe(dit)?|tabnew$")) + , re_tabedit(QStringLiteral("^tabe(dit)?|tabnew$")) + , re_new(QStringLiteral("^(v)?new$")) + , re_split(QStringLiteral("^sp(lit)?$")) + , re_vsplit(QStringLiteral("^vs(plit)?$")) + , re_vclose(QStringLiteral("^clo(se)?$")) + , re_only(QStringLiteral("^on(ly)?$")) { - re_write.setPattern(QStringLiteral("w(a)?")); - re_close.setPattern(QStringLiteral("bd(elete)?|tabc(lose)?")); - re_quit.setPattern(QStringLiteral("(w)?q(a|all)?(!)?")); - re_exit.setPattern(QStringLiteral("x(a)?")); - re_edit.setPattern(QStringLiteral("e(dit)?|tabe(dit)?|tabnew")); - re_tabedit.setPattern(QStringLiteral("tabe(dit)?|tabnew")); - re_new.setPattern(QStringLiteral("(v)?new")); - re_split.setPattern(QStringLiteral("sp(lit)?")); - re_vsplit.setPattern(QStringLiteral("vs(plit)?")); - re_vclose.setPattern(QStringLiteral("clo(se)?")); - re_only.setPattern(QStringLiteral("on(ly)?")); } AppCommands::~AppCommands() { m_instance = nullptr; } bool AppCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &) { - QStringList args(cmd.split(QRegExp(QLatin1String("\\s+")), QString::SkipEmptyParts)) ; + QStringList args(cmd.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts)) ; QString command(args.takeFirst()); KTextEditor::MainWindow *mainWin = view->mainWindow(); KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); - if (re_write.exactMatch(command)) { //TODO: handle writing to specific file - if (!re_write.cap(1).isEmpty()) { // [a]ll + QRegularExpressionMatch match; + if ((match = re_write.match(command)).hasMatch()) { //TODO: handle writing to specific file + if (!match.captured(1).isEmpty()) { // [a]ll Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } msg = i18n("All documents written to disk"); } else { view->document()->documentSave(); msg = i18n("Document written to disk"); } } // Other buffer commands are implemented by the KateFileTree plugin - else if (re_close.exactMatch(command)) { + else if ((match = re_close.match(command)).hasMatch()) { QTimer::singleShot(0, [app, view](){ app->closeDocument(view->document()); }); - } else if (re_quit.exactMatch(command)) { - const bool save = !re_quit.cap(1).isEmpty(); // :[w]q - const bool allDocuments = !re_quit.cap(2).isEmpty(); // :q[all] - const bool doNotPromptForSave = !re_quit.cap(3).isEmpty(); // :q[!] + } else if ((match = re_quit.match(command)).hasMatch()) { + const bool save = !match.captured(1).isEmpty(); // :[w]q + const bool allDocuments = !match.captured(2).isEmpty(); // :q[all] + const bool doNotPromptForSave = !match.captured(3).isEmpty(); // :q[!] if (allDocuments) { if (save) { Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } } if (doNotPromptForSave) { Q_FOREACH(KTextEditor::Document *doc, app->documents()) { if (doc->isModified()) { doc->setModified(false); } } } QTimer::singleShot(0, this, SLOT(quit())); } else { if (save && view->document()->isModified()) { view->document()->documentSave(); } if (doNotPromptForSave) { view->document()->setModified(false); } if (mainWin->views().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentView())); } else { if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } - } else if (re_exit.exactMatch(command)) { - if (!re_exit.cap(1).isEmpty()) { // a[ll] + } else if ((match = re_exit.match(command)).hasMatch()) { + if (!match.captured(1).isEmpty()) { // a[ll] Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } QTimer::singleShot(0, this, SLOT(quit())); } else { if (view->document()->isModified()) { view->document()->documentSave(); } if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } - } else if (re_edit.exactMatch(command)) { + } else if ((match = re_edit.match(command)).hasMatch()) { QString argument = args.join(QLatin1Char(' ')); if (argument.isEmpty() || argument == QLatin1String("!")) { - if (re_tabedit.exactMatch(command)) { + if ((match = re_tabedit.match(command)).hasMatch()) { if (auto doc = app->openUrl(QUrl())) { QTimer::singleShot(0, [mainWin, doc](){ mainWin->activateView(doc); }); } } else { view->document()->documentReload(); } } else { QUrl base = view->document()->url(); QUrl url; QUrl arg2path(argument); if (base.isValid()) { // first try to use the same path as the current open document has url = QUrl(base.resolved(arg2path)); //resolved handles the case where the args is a relative path, and is the same as using QUrl(args) elsewise } else { // else use the cwd url = QUrl(QUrl(QDir::currentPath() + QLatin1String("/")).resolved(arg2path)); // + "/" is needed because of http://lists.qt.nokia.com/public/qt-interest/2011-May/033913.html } QFileInfo file(url.toLocalFile()); KTextEditor::Document *doc = app->findUrl(url); if (!doc) { if (file.exists()) { doc = app->openUrl(url); } else { if ((doc = app->openUrl(QUrl()))) { doc->saveAs(url); } } } if (doc) { QTimer::singleShot(0, [mainWin, doc](){ mainWin->activateView(doc); }); } } // splitView() orientations are reversed from the usual editor convention. // 'vsplit' and 'vnew' use Qt::Horizontal to match vi and the Kate UI actions. - } else if (re_new.exactMatch(command)) { - if (re_new.cap(1) == QLatin1String("v")) { // vertical split + } else if ((match = re_new.match(command)).hasMatch()) { + if (match.captured(1) == QLatin1String("v")) { // vertical split mainWin->splitView(Qt::Horizontal); } else { // horizontal split mainWin->splitView(Qt::Vertical); } mainWin->openUrl(QUrl()); } else if (command == QLatin1String("enew")) { mainWin->openUrl(QUrl()); - } else if (re_split.exactMatch(command)) { + } else if ((match = re_split.match(command)).hasMatch()) { mainWin->splitView(Qt::Vertical); // see above - } else if (re_vsplit.exactMatch(command)) { + } else if ((match = re_vsplit.match(command)).hasMatch()) { mainWin->splitView(Qt::Horizontal); - } else if (re_vclose.exactMatch(command)) { + } else if ((match = re_vclose.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeCurrentSplitView())); - } else if (re_only.exactMatch(command)) { + } else if ((match = re_only.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeOtherSplitViews())); } return true; } bool AppCommands::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view); - if (re_write.exactMatch(cmd)) { + if (re_write.match(cmd).hasMatch()) { msg = i18n("

w/wa — write document(s) to disk

" "

Usage: w[a]

" "

Writes the current document(s) to disk. " "It can be called in two ways:
" " w — writes the current document to disk
" " wa — writes all documents to disk.

" "

If no file name is associated with the document, " "a file dialog will be shown.

"); return true; - } else if (re_quit.exactMatch(cmd)) { + } else if (re_quit.match(cmd).hasMatch()) { msg = i18n("

q/qa/wq/wqa — [write and] quit

" "

Usage: [w]q[a]

" "

Quits the application. If w is prepended, it also writes" " the document(s) to disk. This command " "can be called in several ways:
" " q — closes the current view.
" " qa — closes all views, effectively quitting the application.
" " wq — writes the current document to disk and closes its view.
" " wqa — writes all documents to disk and quits.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

"); return true; - } else if (re_exit.exactMatch(cmd)) { + } else if (re_exit.match(cmd).hasMatch()) { msg = i18n("

x/xa — write and quit

" "

Usage: x[a]

" "

Saves document(s) and quits (exits). This command " "can be called in two ways:
" " x — closes the current view.
" " xa — closes all views, effectively quitting the application.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

" "

Unlike the 'w' commands, this command only writes the document if it is modified." "

"); return true; - } else if (re_split.exactMatch(cmd)) { + } else if (re_split.match(cmd).hasMatch()) { msg = i18n("

sp,split— Split horizontally the current view into two

" "

Usage: sp[lit]

" "

The result is two views on the same document.

"); return true; - } else if (re_vsplit.exactMatch(cmd)) { + } else if (re_vsplit.match(cmd).hasMatch()) { msg = i18n("

vs,vsplit— Split vertically the current view into two

" "

Usage: vs[plit]

" "

The result is two views on the same document.

"); return true; - } else if (re_vclose.exactMatch(cmd)) { + } else if (re_vclose.match(cmd).hasMatch()) { msg = i18n("

clo[se]— Close the current view

" "

Usage: clo[se]

" "

After executing it, the current view will be closed.

"); return true; - } else if (re_new.exactMatch(cmd)) { + } else if (re_new.match(cmd).hasMatch()) { msg = i18n("

[v]new — split view and create new document

" "

Usage: [v]new

" "

Splits the current view and opens a new document in the new view." " This command can be called in two ways:
" " new — splits the view horizontally and opens a new document.
" " vnew — splits the view vertically and opens a new document.
" "

"); return true; - } else if (re_edit.exactMatch(cmd)) { + } else if (re_edit.match(cmd).hasMatch()) { msg = i18n("

e[dit] — reload current document

" "

Usage: e[dit]

" "

Starts editing the current document again. This is useful to re-edit" " the current file, when it has been changed by another program.

"); return true; } return false; } KTextEditor::View * AppCommands::findViewInDifferentSplitView(KTextEditor::MainWindow *window, KTextEditor::View *view) { Q_FOREACH (KTextEditor::View *it, window->views()) { if (!window->viewsInSameSplitView(it, view)) { return it; } } return nullptr; } void AppCommands::closeCurrentDocument() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::Document *doc = app->activeMainWindow()->activeView()->document(); QTimer::singleShot(0, [app, doc](){ app->closeDocument(doc); }); } void AppCommands::closeCurrentView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeView(mw->activeView()); } void AppCommands::closeCurrentSplitView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeSplitView(mw->activeView()); } void AppCommands::closeOtherSplitViews() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); KTextEditor::View *view = mw->activeView(); KTextEditor::View *viewToRemove = nullptr; while ((viewToRemove = findViewInDifferentSplitView(mw, view))) { mw->closeSplitView(viewToRemove); } } void AppCommands::quit() { KTextEditor::Editor::instance()->application()->quit(); } //END AppCommands //BEGIN KateViBufferCommand BufferCommands *BufferCommands::m_instance = nullptr; BufferCommands::BufferCommands() : KTextEditor::Command({ QStringLiteral("ls") , QStringLiteral("b"), QStringLiteral("buffer") , QStringLiteral("bn"), QStringLiteral("bnext"), QStringLiteral("bp"), QStringLiteral("bprevious") , QStringLiteral("tabn"), QStringLiteral("tabnext"), QStringLiteral("tabp"), QStringLiteral("tabprevious") , QStringLiteral("bf"), QStringLiteral("bfirst"), QStringLiteral("bl"), QStringLiteral("blast") , QStringLiteral("tabf"), QStringLiteral("tabfirst"), QStringLiteral("tabl"), QStringLiteral("tablast")}) { } BufferCommands::~BufferCommands() { m_instance = nullptr; } bool BufferCommands::exec(KTextEditor::View *view, const QString &cmd, QString &, const KTextEditor::Range &) { // create list of args QStringList args(cmd.split(QLatin1Char(' '), QString::KeepEmptyParts)); QString command = args.takeFirst(); // same as cmd if split failed QString argument = args.join(QLatin1Char(' ')); if (command == QLatin1String("ls")) { // TODO: open quickview } else if (command == QLatin1String("b") || command == QLatin1String("buffer")) { switchDocument(view, argument); } else if (command == QLatin1String("bp") || command == QLatin1String("bprevious")) { prevBuffer(view); } else if (command == QLatin1String("bn") || command == QLatin1String("bnext")) { nextBuffer(view); } else if (command == QLatin1String("bf") || command == QLatin1String("bfirst")) { firstBuffer(view); } else if (command == QLatin1String("bl") || command == QLatin1String("blast")) { lastBuffer(view); } else if (command == QLatin1String("tabn") || command == QLatin1String("tabnext")) { nextTab(view); } else if (command == QLatin1String("tabp") || command == QLatin1String("tabprevious")) { prevTab(view); } else if (command == QLatin1String("tabf") || command == QLatin1String("tabfirst")) { firstTab(view); } else if (command == QLatin1String("tabl") || command == QLatin1String("tablast")) { lastTab(view); } return true; } void BufferCommands::switchDocument(KTextEditor::View *view, const QString &address) { if (address.isEmpty()) { // no argument: switch to the previous document prevBuffer(view); return; } const int idx = address.toInt(); QList docs = documents(); if (idx > 0 && idx <= docs.size()) { // numerical argument: switch to the nth document activateDocument(view, docs.at(idx - 1)); } else { // string argument: switch to the given file KTextEditor::Document *doc = nullptr; Q_FOREACH(KTextEditor::Document *it, docs) { if (it->documentName() == address) { doc = it; break; } } if (doc) { activateDocument(view, doc); } } } void BufferCommands::prevBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx > 0) { activateDocument(view, docs.at(idx - 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.last()); } } void BufferCommands::nextBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx + 1 < docs.size()) { activateDocument(view, docs.at(idx + 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.first()); } } void BufferCommands::firstBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().at(0)); } } void BufferCommands::lastBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().last()); } } void BufferCommands::prevTab(KTextEditor::View *view) { prevBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::nextTab(KTextEditor::View *view) { nextBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::firstTab(KTextEditor::View *view) { firstBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::lastTab(KTextEditor::View *view) { lastBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::activateDocument(KTextEditor::View *view, KTextEditor::Document *doc) { KTextEditor::MainWindow *mainWindow = view->mainWindow(); QTimer::singleShot(0, [mainWindow, doc]() { mainWindow->activateView(doc); }); } QList< KTextEditor::Document * > BufferCommands::documents() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); return app->documents(); } bool BufferCommands::help(KTextEditor::View * /*view*/, const QString &cmd, QString &msg) { if (cmd == QLatin1String("b") || cmd == QLatin1String("buffer")) { msg = i18n("

b,buffer — Edit document N from the document list

" "

Usage: b[uffer] [N]

"); return true; } else if (cmd == QLatin1String("bp") || cmd == QLatin1String("bprevious") || cmd == QLatin1String("tabp") || cmd == QLatin1String("tabprevious")) { msg = i18n("

bp,bprev — previous buffer

" "

Usage: bp[revious] [N]

" "

Goes to [N]th previous document (\"buffer\") in document list.

" "

[N] defaults to one.

" "

Wraps around the start of the document list.

"); return true; } else if (cmd == QLatin1String("bn") || cmd == QLatin1String("bnext") || cmd == QLatin1String("tabn") || cmd == QLatin1String("tabnext")) { msg = i18n("

bn,bnext — switch to next document

" "

Usage: bn[ext] [N]

" "

Goes to [N]th next document (\"buffer\") in document list." "[N] defaults to one.

" "

Wraps around the end of the document list.

"); return true; } else if (cmd == QLatin1String("bf") || cmd == QLatin1String("bfirst") || cmd == QLatin1String("tabf") || cmd == QLatin1String("tabfirst")) { msg = i18n("

bf,bfirst — first document

" "

Usage: bf[irst]

" "

Goes to the first document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("bl") || cmd == QLatin1String("blast") || cmd == QLatin1String("tabl") || cmd == QLatin1String("tablast")) { msg = i18n("

bl,blast — last document

" "

Usage: bl[ast]

" "

Goes to the last document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("ls")) { msg = i18n("

ls

" "

list current buffers

"); } return false; } //END KateViBufferCommand diff --git a/src/vimode/appcommands.h b/src/vimode/appcommands.h index a24a6889..7943a83d 100644 --- a/src/vimode/appcommands.h +++ b/src/vimode/appcommands.h @@ -1,119 +1,120 @@ /* This file is part of the KDE libraries Copyright (C) 2009 Erlend Hamberg Copyright (C) 2011 Svyatoslav Kuzmich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KATEVI_APP_COMMANDS_H #define KATEVI_APP_COMMANDS_H #include #include +#include namespace KTextEditor { class MainWindow; } namespace KateVi { class AppCommands : public KTextEditor::Command { Q_OBJECT AppCommands(); static AppCommands* m_instance; public: ~AppCommands() override; bool exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &range = KTextEditor::Range::invalid()) override; bool help(KTextEditor::View *view, const QString &cmd, QString &msg) override; static AppCommands* self() { if (m_instance == nullptr) { m_instance = new AppCommands(); } return m_instance; } private: /** * @returns a view in the given \p window that does not share a split * view with the given \p view. If such view could not be found, then * nullptr is returned. */ KTextEditor::View * findViewInDifferentSplitView(KTextEditor::MainWindow *window, KTextEditor::View *view); private Q_SLOTS: void closeCurrentDocument(); void closeCurrentView(); void closeCurrentSplitView(); void closeOtherSplitViews(); void quit(); private: - QRegExp re_write; - QRegExp re_close; - QRegExp re_quit; - QRegExp re_exit; - QRegExp re_edit; - QRegExp re_tabedit; - QRegExp re_new; - QRegExp re_split; - QRegExp re_vsplit; - QRegExp re_vclose; - QRegExp re_only; + const QRegularExpression re_write; + const QRegularExpression re_close; + const QRegularExpression re_quit; + const QRegularExpression re_exit; + const QRegularExpression re_edit; + const QRegularExpression re_tabedit; + const QRegularExpression re_new; + const QRegularExpression re_split; + const QRegularExpression re_vsplit; + const QRegularExpression re_vclose; + const QRegularExpression re_only; }; class BufferCommands : public KTextEditor::Command { Q_OBJECT BufferCommands(); static BufferCommands* m_instance; public: ~BufferCommands() override; bool exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &range = KTextEditor::Range::invalid()) override; bool help(KTextEditor::View *view, const QString &cmd, QString &msg) override; static BufferCommands* self() { if (m_instance == nullptr) { m_instance = new BufferCommands(); } return m_instance; } private: void switchDocument(KTextEditor::View *, const QString &doc); void prevBuffer(KTextEditor::View *); void nextBuffer(KTextEditor::View *); void firstBuffer(KTextEditor::View *); void lastBuffer(KTextEditor::View *); void prevTab(KTextEditor::View *); void nextTab(KTextEditor::View *); void firstTab(KTextEditor::View *); void lastTab(KTextEditor::View *); void activateDocument(KTextEditor::View *, KTextEditor::Document *); QList documents(); }; } #endif /* KATEVI_APP_COMMANDS_H */ diff --git a/src/vimode/cmds.cpp b/src/vimode/cmds.cpp index c5adb7d2..9f198433 100644 --- a/src/vimode/cmds.cpp +++ b/src/vimode/cmds.cpp @@ -1,279 +1,280 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2003-2005 Anders Lund * Copyright (C) 2001-2010 Christoph Cullmann * Copyright (C) 2001 Charles Samuels * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include "katedocument.h" #include "kateview.h" #include "kateglobal.h" #include #include #include "katecmd.h" #include "katepartdebug.h" #include "kateviinputmode.h" #include #include "globalstate.h" #include "marks.h" #include #include -#include +#include #include using namespace KateVi; //BEGIN ViCommands Commands *Commands::m_instance = nullptr; bool Commands::exec(KTextEditor::View *view, const QString &_cmd, QString &msg, const KTextEditor::Range &range) { Q_UNUSED(range) // cast it hardcore, we know that it is really a kateview :) KTextEditor::ViewPrivate *v = static_cast(view); if (!v) { msg = i18n("Could not access view"); return false; } //create a list of args - QStringList args(_cmd.split(QRegExp(QLatin1String("\\s+")), QString::SkipEmptyParts)); + QStringList args(_cmd.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts)); QString cmd(args.takeFirst()); // ALL commands that takes no arguments. if (mappingCommands().contains(cmd)) { if (cmd.endsWith(QLatin1String("unmap"))) { if (args.count() == 1) { m_viGlobal->mappings()->remove(modeForMapCommand(cmd), args.at(0)); return true; } else { msg = i18n("Missing argument. Usage: %1 ", cmd); return false; } } if (args.count() == 1) { msg = m_viGlobal->mappings()->get(modeForMapCommand(cmd), args.at(0), true); if (msg.isEmpty()) { msg = i18n("No mapping found for \"%1\"", args.at(0)); return false; } else { msg = i18n("\"%1\" is mapped to \"%2\"", args.at(0), msg); } } else if (args.count() == 2) { Mappings::MappingRecursion mappingRecursion = (isMapCommandRecursive(cmd)) ? Mappings::Recursive : Mappings::NonRecursive; m_viGlobal->mappings()->add(modeForMapCommand(cmd), args.at(0), args.at(1), mappingRecursion); } else { msg = i18n("Missing argument(s). Usage: %1 []", cmd); return false; } return true; } NormalViMode *nm = m_viInputModeManager->getViNormalMode(); if (cmd == QLatin1String("d") || cmd == QLatin1String("delete") || cmd == QLatin1String("j") || cmd == QLatin1String("c") || cmd == QLatin1String("change") || cmd == QLatin1String("<") || cmd == QLatin1String(">") || cmd == QLatin1String("y") || cmd == QLatin1String("yank")) { KTextEditor::Cursor start_cursor_position = v->cursorPosition(); int count = 1; if (range.isValid()) { count = qAbs(range.end().line() - range.start().line()) + 1; v->setCursorPosition(KTextEditor::Cursor(qMin(range.start().line(), range.end().line()), 0)); } - QRegExp number(QLatin1String("^(\\d+)$")); + static const QRegularExpression number(QLatin1String("^(\\d+)$")); for (int i = 0; i < args.count(); i++) { - if (number.indexIn(args.at(i)) != -1) { - count += number.cap().toInt() - 1; + auto match = number.match(args.at(i)); + if (match.hasMatch()) { + count += match.captured(0).toInt() - 1; } QChar r = args.at(i).at(0); if (args.at(i).size() == 1 && ((r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*'))) { nm->setRegister(r); } } nm->setCount(count); if (cmd == QLatin1String("d") || cmd == QLatin1String("delete")) { nm->commandDeleteLine(); } if (cmd == QLatin1String("j")) { nm->commandJoinLines(); } if (cmd == QLatin1String("c") || cmd == QLatin1String("change")) { nm->commandChangeLine(); } if (cmd == QLatin1String("<")) { nm->commandUnindentLine(); } if (cmd == QLatin1String(">")) { nm->commandIndentLine(); } if (cmd == QLatin1String("y") || cmd == QLatin1String("yank")) { nm->commandYankLine(); v->setCursorPosition(start_cursor_position); } // TODO - should we resetParser, here? We'd have to make it public, if so. // Or maybe synthesise a KateViCommand to execute instead ... ? nm->setCount(0); return true; } if (cmd == QLatin1String("mark") || cmd == QLatin1String("ma") || cmd == QLatin1String("k")) { if (args.count() == 0) { if (cmd == QLatin1String("mark")) { // TODO: show up mark list; } else { msg = i18n("Wrong arguments"); return false; } } else if (args.count() == 1) { QChar r = args.at(0).at(0); int line; if ((r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*')) { if (range.isValid()) { line = qMax(range.end().line(), range.start().line()); } else { line = v->cursorPosition().line(); } m_viInputModeManager->marks()->setUserMark(r, KTextEditor::Cursor(line, 0)); } } else { msg = i18n("Wrong arguments"); return false; } return true; } // should not happen :) msg = i18n("Unknown command '%1'", cmd); return false; } bool Commands::supportsRange(const QString &range) { static QStringList l; if (l.isEmpty()) l << QStringLiteral("d") << QStringLiteral("delete") << QStringLiteral("j") << QStringLiteral("c") << QStringLiteral("change") << QStringLiteral("<") << QStringLiteral(">") << QStringLiteral("y") << QStringLiteral("yank") << QStringLiteral("ma") << QStringLiteral("mark") << QStringLiteral("k"); return l.contains(range.split(QLatin1String(" ")).at(0)); } KCompletion *Commands::completionObject(KTextEditor::View *view, const QString &cmd) { Q_UNUSED(view) KTextEditor::ViewPrivate *v = static_cast(view); if (v && (cmd == QLatin1String("nn") || cmd == QLatin1String("nnoremap"))) { QStringList l = m_viGlobal->mappings()->getAll(Mappings::NormalModeMapping); KateCmdShellCompletion *co = new KateCmdShellCompletion(); co->setItems(l); co->setIgnoreCase(false); return co; } return nullptr; } const QStringList &Commands::mappingCommands() { static QStringList mappingsCommands; if (mappingsCommands.isEmpty()) { mappingsCommands << QStringLiteral("nmap") << QStringLiteral("nm") << QStringLiteral("noremap") << QStringLiteral("nnoremap") << QStringLiteral("nn") << QStringLiteral("no") << QStringLiteral("vmap") << QStringLiteral("vm") << QStringLiteral("vnoremap") << QStringLiteral("vn") << QStringLiteral("imap") << QStringLiteral("im") << QStringLiteral("inoremap") << QStringLiteral("ino") << QStringLiteral("cmap") << QStringLiteral("cm") << QStringLiteral("cnoremap") << QStringLiteral("cno"); mappingsCommands << QStringLiteral("nunmap") << QStringLiteral("vunmap") << QStringLiteral("iunmap") << QStringLiteral("cunmap"); } return mappingsCommands; } Mappings::MappingMode Commands::modeForMapCommand(const QString &mapCommand) { static QMap modeForMapCommand; if (modeForMapCommand.isEmpty()) { // Normal is the default. modeForMapCommand.insert(QStringLiteral("vmap"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("vm"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("vnoremap"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("vn"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("imap"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("im"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("inoremap"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("ino"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("cmap"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("cm"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("cnoremap"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("cno"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("nunmap"), Mappings::NormalModeMapping); modeForMapCommand.insert(QStringLiteral("vunmap"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("iunmap"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("cunmap"), Mappings::CommandModeMapping); } return modeForMapCommand.value(mapCommand); } bool Commands::isMapCommandRecursive(const QString &mapCommand) { static QMap isMapCommandRecursive; if (isMapCommandRecursive.isEmpty()) { isMapCommandRecursive.insert(QStringLiteral("nmap"), true); isMapCommandRecursive.insert(QStringLiteral("nm"), true); isMapCommandRecursive.insert(QStringLiteral("vmap"), true); isMapCommandRecursive.insert(QStringLiteral("vm"), true); isMapCommandRecursive.insert(QStringLiteral("imap"), true); isMapCommandRecursive.insert(QStringLiteral("im"), true); isMapCommandRecursive.insert(QStringLiteral("cmap"), true); isMapCommandRecursive.insert(QStringLiteral("cm"), true); } return isMapCommandRecursive.value(mapCommand); } //END ViCommands //BEGIN SedReplace SedReplace *SedReplace::m_instance = nullptr; bool SedReplace::interactiveSedReplace(KTextEditor::ViewPrivate *, QSharedPointer interactiveSedReplace) { EmulatedCommandBar *emulatedCommandBar = m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar(); emulatedCommandBar->startInteractiveSearchAndReplace(interactiveSedReplace); return true; } //END SedReplace diff --git a/src/vimode/completionreplayer.cpp b/src/vimode/completionreplayer.cpp index 3e322f20..f388409a 100644 --- a/src/vimode/completionreplayer.cpp +++ b/src/vimode/completionreplayer.cpp @@ -1,153 +1,155 @@ /* * This file is part of the KDE libraries * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "completionreplayer.h" #include #include "katepartdebug.h" #include "kateview.h" #include "katedocument.h" #include "completionrecorder.h" #include "macrorecorder.h" #include "lastchangerecorder.h" #include +#include using namespace KateVi; CompletionReplayer::CompletionReplayer(InputModeManager *viInputModeManager) : m_viInputModeManager(viInputModeManager) { } CompletionReplayer::~CompletionReplayer() { } void CompletionReplayer::start(const CompletionList &completions) { m_nextCompletionIndex.push(0); m_CompletionsToReplay.push(completions); } void CompletionReplayer::stop() { m_CompletionsToReplay.pop(); m_nextCompletionIndex.pop(); } void CompletionReplayer::replay() { const Completion completion = nextCompletion(); KTextEditor::ViewPrivate *m_view = m_viInputModeManager->view(); KTextEditor::DocumentPrivate *doc = m_view->doc(); // Find beginning of the word. KTextEditor::Cursor cursorPos = m_view->cursorPosition(); KTextEditor::Cursor wordStart = KTextEditor::Cursor::invalid(); if (!doc->characterAt(cursorPos).isLetterOrNumber() && doc->characterAt(cursorPos) != QLatin1Char('_')) { cursorPos.setColumn(cursorPos.column() - 1); } while (cursorPos.column() >= 0 && (doc->characterAt(cursorPos).isLetterOrNumber() || doc->characterAt(cursorPos) == QLatin1Char('_'))) { wordStart = cursorPos; cursorPos.setColumn(cursorPos.column() - 1); } // Find end of current word. cursorPos = m_view->cursorPosition(); KTextEditor::Cursor wordEnd = KTextEditor::Cursor(cursorPos.line(), cursorPos.column() - 1); while (cursorPos.column() < doc->lineLength(cursorPos.line()) && (doc->characterAt(cursorPos).isLetterOrNumber() || doc->characterAt(cursorPos) == QLatin1Char('_'))) { wordEnd = cursorPos; cursorPos.setColumn(cursorPos.column() + 1); } QString completionText = completion.completedText(); const KTextEditor::Range currentWord = KTextEditor::Range(wordStart, KTextEditor::Cursor(wordEnd.line(), wordEnd.column() + 1)); // Should we merge opening brackets? Yes, if completion is a function with arguments and after the cursor // there is (optional whitespace) followed by an open bracket. int offsetFinalCursorPosBy = 0; if (completion.completionType() == Completion::FunctionWithArgs) { const int nextMergableBracketAfterCursorPos = findNextMergeableBracketPos(currentWord.end()); if (nextMergableBracketAfterCursorPos != -1) { if (completionText.endsWith(QLatin1String("()"))) { // Strip "()". completionText = completionText.left(completionText.length() - 2); } else if (completionText.endsWith(QLatin1String("();"))) { // Strip "();". completionText = completionText.left(completionText.length() - 3); } // Ensure cursor ends up after the merged open bracket. offsetFinalCursorPosBy = nextMergableBracketAfterCursorPos + 1; } else { if (!completionText.endsWith(QLatin1String("()")) && !completionText.endsWith(QLatin1String("();"))) { // Original completion merged with an opening bracket; we'll have to add our own brackets. completionText.append(QLatin1String("()")); } // Position cursor correctly i.e. we'll have added "functionname()" or "functionname();"; need to step back by // one or two to be after the opening bracket. offsetFinalCursorPosBy = completionText.endsWith(QLatin1Char(';')) ? -2 : -1; } } KTextEditor::Cursor deleteEnd = completion.removeTail() ? currentWord.end() : KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + 0); if (currentWord.isValid()) { doc->removeText(KTextEditor::Range(currentWord.start(), deleteEnd)); doc->insertText(currentWord.start(), completionText); } else { doc->insertText(m_view->cursorPosition(), completionText); } if (offsetFinalCursorPosBy != 0) { m_view->setCursorPosition(KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + offsetFinalCursorPosBy)); } if (!m_viInputModeManager->lastChangeRecorder()->isReplaying()) { Q_ASSERT(m_viInputModeManager->macroRecorder()->isReplaying()); // Post the completion back: it needs to be added to the "last change" list ... m_viInputModeManager->completionRecorder()->logCompletionEvent(completion); // ... but don't log the ctrl-space that led to this call to replayCompletion(), as // a synthetic ctrl-space was just added to the last change keypresses by logCompletionEvent(), and we don't // want to duplicate them! m_viInputModeManager->doNotLogCurrentKeypress(); } } Completion CompletionReplayer::nextCompletion() { Q_ASSERT(m_viInputModeManager->lastChangeRecorder()->isReplaying() || m_viInputModeManager->macroRecorder()->isReplaying()); if (m_nextCompletionIndex.top() >= m_CompletionsToReplay.top().length()) { qCDebug(LOG_KTE) << "Something wrong here: requesting more completions for macro than we actually have. Returning dummy."; return Completion(QString(), false, Completion::PlainText); } return m_CompletionsToReplay.top()[m_nextCompletionIndex.top()++]; } int CompletionReplayer::findNextMergeableBracketPos(const KTextEditor::Cursor &startPos) const { KTextEditor::DocumentPrivate *doc = m_viInputModeManager->view()->doc(); const QString lineAfterCursor = doc->text(KTextEditor::Range(startPos, KTextEditor::Cursor(startPos.line(), doc->lineLength(startPos.line())))); - QRegExp whitespaceThenOpeningBracket(QLatin1String("^\\s*(\\()")); + static const QRegularExpression whitespaceThenOpeningBracket(QLatin1String("^\\s*(\\()")); + QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(lineAfterCursor); int nextMergableBracketAfterCursorPos = -1; - if (lineAfterCursor.contains(whitespaceThenOpeningBracket)) { - nextMergableBracketAfterCursorPos = whitespaceThenOpeningBracket.pos(1); + if (match.hasMatch()) { + nextMergableBracketAfterCursorPos = match.capturedStart(1); } return nextMergableBracketAfterCursorPos; } diff --git a/src/vimode/emulatedcommandbar/commandmode.cpp b/src/vimode/emulatedcommandbar/commandmode.cpp index 3c1f173c..1ccca9b8 100644 --- a/src/vimode/emulatedcommandbar/commandmode.cpp +++ b/src/vimode/emulatedcommandbar/commandmode.cpp @@ -1,406 +1,408 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2013-2016 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "commandmode.h" #include "emulatedcommandbar.h" #include "interactivesedreplacemode.h" #include "searchmode.h" #include "../commandrangeexpressionparser.h" #include #include #include #include "../globalstate.h" #include "../history.h" #include "katescriptmanager.h" #include "katecmds.h" #include #include +#include #include using namespace KateVi; CommandMode::CommandMode ( EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view, QLineEdit* edit, InteractiveSedReplaceMode* interactiveSedReplaceMode, Completer* completer) : ActiveMode ( emulatedCommandBar, matchHighlighter, viInputModeManager, view), m_edit(edit), m_interactiveSedReplaceMode(interactiveSedReplaceMode), m_completer(completer) { QList cmds; cmds.push_back(KateCommands::CoreCommands::self()); cmds.push_back(Commands::self()); cmds.push_back(AppCommands::self()); cmds.push_back(SedReplace::self()); cmds.push_back(BufferCommands::self()); Q_FOREACH (KTextEditor::Command *cmd, KateScriptManager::self()->commandLineScripts()) { cmds.push_back(cmd); } Q_FOREACH (KTextEditor::Command *cmd, cmds) { QStringList l = cmd->cmds(); for (int z = 0; z < l.count(); z++) { m_cmdDict.insert(l[z], cmd); } m_cmdCompletion.insertItems(l); } } bool CommandMode::handleKeyPress ( const QKeyEvent* keyEvent ) { if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_D || keyEvent->key() == Qt::Key_F)) { CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); if (parsedSedExpression.parsedSuccessfully) { const bool clearFindTerm = (keyEvent->key() == Qt::Key_D); if (clearFindTerm) { m_edit->setSelection(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1); m_edit->insert(QString()); } else { // Clear replace term. m_edit->setSelection(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1); m_edit->insert(QString()); } } return true; } return false; } void CommandMode::editTextChanged ( const QString& newText ) { Q_UNUSED(newText); // We read the current text from m_edit. if (m_completer->isCompletionActive()) return; // Command completion doesn't need to be manually invoked. if (!withoutRangeExpression().isEmpty() && !m_completer->isNextTextChangeDueToCompletionChange()) { // ... However, command completion mode should not be automatically invoked if this is not the current leading // word in the text edit (it gets annoying if completion pops up after ":s/se" etc). const bool commandBeforeCursorIsLeading = (commandBeforeCursorBegin() == rangeExpression().length()); if (commandBeforeCursorIsLeading) { CompletionStartParams completionStartParams = activateCommandCompletion(); startCompletion(completionStartParams); } } } void CommandMode::deactivate ( bool wasAborted ) { if (wasAborted) { // Appending the command to the history when it is executed is handled elsewhere; we can't // do it inside closed() as we may still be showing the command response display. viInputModeManager()->globalState()->commandHistory()->append(m_edit->text()); // With Vim, aborting a command returns us to Normal mode, even if we were in Visual Mode. // If we switch from Visual to Normal mode, we need to clear the selection. view()->clearSelection(); } } CompletionStartParams CommandMode::completionInvoked(Completer::CompletionInvocation invocationType) { CompletionStartParams completionStartParams; if (invocationType == Completer::CompletionInvocation::ExtraContext) { if (isCursorInFindTermOfSed()) { completionStartParams = activateSedFindHistoryCompletion(); } else if (isCursorInReplaceTermOfSed()) { completionStartParams = activateSedReplaceHistoryCompletion(); } else { completionStartParams = activateCommandHistoryCompletion(); } } else { // Normal context, so boring, ordinary History completion. completionStartParams = activateCommandHistoryCompletion(); } return completionStartParams; } void CommandMode::completionChosen() { QString commandToExecute = m_edit->text(); CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); if (parsedSedExpression.parsedSuccessfully) { const QString originalFindTerm = sedFindTerm(); const QString convertedFindTerm = vimRegexToQtRegexPattern(originalFindTerm); const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(convertedFindTerm); viInputModeManager()->globalState()->searchHistory()->append(originalFindTerm); const QString replaceTerm = sedReplaceTerm(); viInputModeManager()->globalState()->replaceHistory()->append(replaceTerm); commandToExecute = commandWithSedSearchRegexConverted; } const QString commandResponseMessage = executeCommand(commandToExecute); // Don't close the bar if executing the command switched us to Interactive Sed Replace mode. if (!m_interactiveSedReplaceMode->isActive()) { if (commandResponseMessage.isEmpty()) { emulatedCommandBar()->hideMe(); } else { closeWithStatusMessage(commandResponseMessage); } } viInputModeManager()->globalState()->commandHistory()->append(m_edit->text()); } QString CommandMode::executeCommand ( const QString& commandToExecute ) { // Silently ignore leading space characters and colon characters (for vi-heads). uint n = 0; const uint textlen = commandToExecute.length(); while ((n < textlen) && commandToExecute[n].isSpace()) { n++; } if (n >= textlen) { return QString(); } QString commandResponseMessage; QString cmd = commandToExecute.mid(n); KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(cmd, cmd); if (cmd.length() > 0) { KTextEditor::Command *p = queryCommand(cmd); if (p) { KateViCommandInterface *ci = dynamic_cast(p); if (ci) { ci->setViInputModeManager(viInputModeManager()); ci->setViGlobal(viInputModeManager()->globalState()); } // The following commands changes the focus themselves, so bar should be hidden before execution. // We got a range and a valid command, but the command does not support ranges. if (range.isValid() && !p->supportsRange(cmd)) { commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd); } else { if (p->exec(view(), cmd, commandResponseMessage, range)) { if (commandResponseMessage.length() > 0) { commandResponseMessage = i18n("Success: ") + commandResponseMessage; } } else { if (commandResponseMessage.length() > 0) { if (commandResponseMessage.contains(QLatin1Char('\n'))) { // multiline error, use widget with more space QWhatsThis::showText(emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), commandResponseMessage); } } else { commandResponseMessage = i18n("Command \"%1\" failed.", cmd); } } } } else { commandResponseMessage = i18n("No such command: \"%1\"", cmd); } } // the following commands change the focus themselves - if (!QRegExp(QLatin1String("buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew")).exactMatch(cmd.split(QLatin1Char(' ')).at(0))) { + static const QRegularExpression reCmds(QLatin1String("^(buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$")); + if (!reCmds.match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { view()->setFocus(); } viInputModeManager()->reset(); return commandResponseMessage; } QString CommandMode::withoutRangeExpression() { const QString originalCommand = m_edit->text(); return originalCommand.mid(rangeExpression().length()); } QString CommandMode::rangeExpression() { const QString command = m_edit->text(); return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command); } CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression() { const QString commandWithoutRangeExpression = withoutRangeExpression(); ParsedSedExpression parsedSedExpression; QString delimiter; parsedSedExpression.parsedSuccessfully = SedReplace::parse(commandWithoutRangeExpression, delimiter, parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos, parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos); if (parsedSedExpression.parsedSuccessfully) { parsedSedExpression.delimiter = delimiter.at(0); if (parsedSedExpression.replaceBeginPos == -1) { if (parsedSedExpression.findBeginPos != -1) { // The replace term was empty, and a quirk of the regex used is that replaceBeginPos will be -1. // It's actually the position after the first occurrence of the delimiter after the end of the find pos. parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.findEndPos) + 1; parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1; } else { // Both find and replace terms are empty; replace term is at the third occurrence of the delimiter. parsedSedExpression.replaceBeginPos = 0; for (int delimiterCount = 1; delimiterCount <= 3; delimiterCount++) { parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.replaceBeginPos + 1); } parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1; } } if (parsedSedExpression.findBeginPos == -1) { // The find term was empty, and a quirk of the regex used is that findBeginPos will be -1. // It's actually the position after the first occurrence of the delimiter. parsedSedExpression.findBeginPos = commandWithoutRangeExpression.indexOf(delimiter) + 1; parsedSedExpression.findEndPos = parsedSedExpression.findBeginPos - 1; } } if (parsedSedExpression.parsedSuccessfully) { parsedSedExpression.findBeginPos += rangeExpression().length(); parsedSedExpression.findEndPos += rangeExpression().length(); parsedSedExpression.replaceBeginPos += rangeExpression().length(); parsedSedExpression.replaceEndPos += rangeExpression().length(); } return parsedSedExpression; } QString CommandMode::sedFindTerm() { const QString command = m_edit->text(); ParsedSedExpression parsedSedExpression = parseAsSedExpression(); Q_ASSERT(parsedSedExpression.parsedSuccessfully); return command.mid(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1); } QString CommandMode::sedReplaceTerm() { const QString command = m_edit->text(); ParsedSedExpression parsedSedExpression = parseAsSedExpression(); Q_ASSERT(parsedSedExpression.parsedSuccessfully); return command.mid(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1); } QString CommandMode::withSedFindTermReplacedWith ( const QString& newFindTerm ) { const QString command = m_edit->text(); ParsedSedExpression parsedSedExpression = parseAsSedExpression(); Q_ASSERT(parsedSedExpression.parsedSuccessfully); return command.mid(0, parsedSedExpression.findBeginPos) + newFindTerm + command.mid(parsedSedExpression.findEndPos + 1); } QString CommandMode::withSedDelimiterEscaped ( const QString& text ) { ParsedSedExpression parsedSedExpression = parseAsSedExpression(); QString delimiterEscaped = ensuredCharEscaped(text, parsedSedExpression.delimiter); return delimiterEscaped; } bool CommandMode::isCursorInFindTermOfSed() { ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return parsedSedExpression.parsedSuccessfully && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1); } bool CommandMode::isCursorInReplaceTermOfSed() { ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1; } int CommandMode::commandBeforeCursorBegin() { const QString textWithoutRangeExpression = withoutRangeExpression(); const int cursorPositionWithoutRangeExpression = m_edit->cursorPosition() - rangeExpression().length(); int commandBeforeCursorBegin = cursorPositionWithoutRangeExpression - 1; while (commandBeforeCursorBegin >= 0 && (textWithoutRangeExpression[commandBeforeCursorBegin].isLetterOrNumber() || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('_') || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('-'))) { commandBeforeCursorBegin--; } commandBeforeCursorBegin++; commandBeforeCursorBegin += rangeExpression().length(); return commandBeforeCursorBegin; } CompletionStartParams CommandMode::activateCommandCompletion() { return CompletionStartParams::createModeSpecific(m_cmdCompletion.items(), commandBeforeCursorBegin()); } CompletionStartParams CommandMode::activateCommandHistoryCompletion() { return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->commandHistory()->items()), 0); } CompletionStartParams CommandMode::activateSedFindHistoryCompletion() { if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) { return CompletionStartParams::invalid(); } CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), parsedSedExpression.findBeginPos, [this] (const QString& completion) -> QString { return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion)); }); } CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion() { if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) { return CompletionStartParams::invalid(); } CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->replaceHistory()->items()), parsedSedExpression.replaceBeginPos, [this] (const QString& completion) -> QString { return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion)); }); } KTextEditor::Command* CommandMode::queryCommand ( const QString& cmd ) const { // a command can be named ".*[\w\-]+" with the constrain that it must // contain at least one letter. int f = 0; bool b = false; // special case: '-' and '_' can be part of a command name, but if the // command is 's' (substitute), it should be considered the delimiter and // should not be counted as part of the command name if (cmd.length() >= 2 && cmd.at(0) == QLatin1Char('s') && (cmd.at(1) == QLatin1Char('-') || cmd.at(1) == QLatin1Char('_'))) { return m_cmdDict.value(QStringLiteral("s")); } for (; f < cmd.length(); f++) { if (cmd[f].isLetter()) { b = true; } if (b && (! cmd[f].isLetterOrNumber() && cmd[f] != QLatin1Char('-') && cmd[f] != QLatin1Char('_'))) { break; } } return m_cmdDict.value(cmd.left(f)); } diff --git a/src/vimode/emulatedcommandbar/completer.cpp b/src/vimode/emulatedcommandbar/completer.cpp index 1bb6840f..85dd2d32 100644 --- a/src/vimode/emulatedcommandbar/completer.cpp +++ b/src/vimode/emulatedcommandbar/completer.cpp @@ -1,256 +1,260 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2013-2016 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "completer.h" #include "emulatedcommandbar.h" using namespace KateVi; #include "kateview.h" #include #include #include #include +#include namespace { bool caseInsensitiveLessThan(const QString &s1, const QString &s2) { return s1.toLower() < s2.toLower(); } } Completer::Completer ( EmulatedCommandBar* emulatedCommandBar, KTextEditor::ViewPrivate* view, QLineEdit* edit ) : m_edit(edit) , m_view(view) { m_completer = new QCompleter(QStringList(), edit); // Can't find a way to stop the QCompleter from auto-completing when attached to a QLineEdit, // so don't actually set it as the QLineEdit's completer. m_completer->setWidget(edit); m_completer->setObjectName(QStringLiteral("completer")); m_completionModel = new QStringListModel(emulatedCommandBar); m_completer->setModel(m_completionModel); m_completer->setCaseSensitivity(Qt::CaseInsensitive); m_completer->popup()->installEventFilter(emulatedCommandBar); } void Completer::startCompletion ( const CompletionStartParams& completionStartParams ) { if (completionStartParams.completionType != CompletionStartParams::None) { m_completionModel->setStringList(completionStartParams.completions); const QString completionPrefix = m_edit->text().mid(completionStartParams.wordStartPos, m_edit->cursorPosition() - completionStartParams.wordStartPos); m_completer->setCompletionPrefix(completionPrefix); m_completer->complete(); m_currentCompletionStartParams = completionStartParams; m_currentCompletionType = completionStartParams.completionType; } } void Completer::deactivateCompletion() { m_completer->popup()->hide(); m_currentCompletionType = CompletionStartParams::None; } bool Completer::isCompletionActive() const { return m_currentCompletionType != CompletionStartParams::None; } bool Completer::isNextTextChangeDueToCompletionChange() const { return m_isNextTextChangeDueToCompletionChange; } bool Completer::completerHandledKeypress ( const QKeyEvent* keyEvent ) { if (!m_edit->isVisible()) return false; if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) { if (m_currentCompletionType != CompletionStartParams::None && m_completer->popup()->isVisible()) { abortCompletionAndResetToPreCompletion(); return true; } } if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_Space) { CompletionStartParams completionStartParams = activateWordFromDocumentCompletion(); startCompletion(completionStartParams); return true; } if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_P) || keyEvent->key() == Qt::Key_Down) { if (!m_completer->popup()->isVisible()) { const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::ExtraContext); startCompletion(completionStartParams); if (m_currentCompletionType != CompletionStartParams::None) { setCompletionIndex(0); } } else { // Descend to next row, wrapping around if necessary. if (m_completer->currentRow() + 1 == m_completer->completionCount()) { setCompletionIndex(0); } else { setCompletionIndex(m_completer->currentRow() + 1); } } return true; } if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_N) || keyEvent->key() == Qt::Key_Up) { if (!m_completer->popup()->isVisible()) { const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::NormalContext); startCompletion(completionStartParams); setCompletionIndex(m_completer->completionCount() - 1); } else { // Ascend to previous row, wrapping around if necessary. if (m_completer->currentRow() == 0) { setCompletionIndex(m_completer->completionCount() - 1); } else { setCompletionIndex(m_completer->currentRow() - 1); } } return true; } if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { if (!m_completer->popup()->isVisible() || m_currentCompletionType != CompletionStartParams::WordFromDocument) { m_currentMode->completionChosen(); } deactivateCompletion(); return true; } return false; } void Completer::editTextChanged ( const QString& newText ) { if (!m_isNextTextChangeDueToCompletionChange) { m_textToRevertToIfCompletionAborted = newText; m_cursorPosToRevertToIfCompletionAborted = m_edit->cursorPosition(); } // If we edit the text after having selected a completion, this means we implicitly accept it, // and so we should dismiss it. if (!m_isNextTextChangeDueToCompletionChange && m_completer->popup()->currentIndex().row() != -1) { deactivateCompletion(); } if (m_currentCompletionType != CompletionStartParams::None && !m_isNextTextChangeDueToCompletionChange) { updateCompletionPrefix(); } } void Completer::setCurrentMode ( ActiveMode* currentMode ) { m_currentMode = currentMode; } void Completer::setCompletionIndex ( int index ) { const QModelIndex modelIndex = m_completer->popup()->model()->index(index, 0); // Need to set both of these, for some reason. m_completer->popup()->setCurrentIndex(modelIndex); m_completer->setCurrentRow(index); m_completer->popup()->scrollTo(modelIndex); currentCompletionChanged(); } void Completer::currentCompletionChanged() { const QString newCompletion = m_completer->currentCompletion(); if (newCompletion.isEmpty()) { return; } QString transformedCompletion = newCompletion; if (m_currentCompletionStartParams.completionTransform) { transformedCompletion = m_currentCompletionStartParams.completionTransform(newCompletion); } m_isNextTextChangeDueToCompletionChange = true; m_edit->setSelection(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos); m_edit->insert(transformedCompletion); m_isNextTextChangeDueToCompletionChange = false; } void Completer::updateCompletionPrefix() { const QString completionPrefix = m_edit->text().mid(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos); m_completer->setCompletionPrefix(completionPrefix); // Seem to need a call to complete() else the size of the popup box is not altered appropriately. m_completer->complete(); } CompletionStartParams Completer::activateWordFromDocumentCompletion() { - QRegExp wordRegEx(QLatin1String("\\w{1,}")); + static const QRegularExpression wordRegEx(QLatin1String("\\w{1,}")); + QRegularExpressionMatch match; + QStringList foundWords; // Narrow the range of lines we search around the cursor so that we don't die on huge files. const int startLine = qMax(0, m_view->cursorPosition().line() - 4096); const int endLine = qMin(m_view->document()->lines(), m_view->cursorPosition().line() + 4096); for (int lineNum = startLine; lineNum < endLine; lineNum++) { - const QString line = m_view->document()->line(lineNum); int wordSearchBeginPos = 0; - while (wordRegEx.indexIn(line, wordSearchBeginPos) != -1) { - const QString foundWord = wordRegEx.cap(0); + const QString line = m_view->document()->line(lineNum); + int wordSearchBeginPos = 0; + while ((match = wordRegEx.match(line, wordSearchBeginPos)).hasMatch()) { + const QString foundWord = match.captured(); foundWords << foundWord; - wordSearchBeginPos = wordRegEx.indexIn(line, wordSearchBeginPos) + wordRegEx.matchedLength(); + wordSearchBeginPos = match.capturedEnd(); } } foundWords = QSet::fromList(foundWords).toList(); std::sort(foundWords.begin(), foundWords.end(), caseInsensitiveLessThan); CompletionStartParams completionStartParams; completionStartParams.completionType = CompletionStartParams::WordFromDocument; completionStartParams.completions = foundWords; completionStartParams.wordStartPos = wordBeforeCursorBegin(); return completionStartParams; } QString Completer::wordBeforeCursor() { const int wordBeforeCursorBegin = this->wordBeforeCursorBegin(); return m_edit->text().mid(wordBeforeCursorBegin, m_edit->cursorPosition() - wordBeforeCursorBegin); } int Completer::wordBeforeCursorBegin() { int wordBeforeCursorBegin = m_edit->cursorPosition() - 1; while (wordBeforeCursorBegin >= 0 && (m_edit->text()[wordBeforeCursorBegin].isLetterOrNumber() || m_edit->text()[wordBeforeCursorBegin] == QLatin1Char('_'))) { wordBeforeCursorBegin--; } wordBeforeCursorBegin++; return wordBeforeCursorBegin; } void Completer::abortCompletionAndResetToPreCompletion() { deactivateCompletion(); m_isNextTextChangeDueToCompletionChange = true; m_edit->setText(m_textToRevertToIfCompletionAborted); m_edit->setCursorPosition(m_cursorPosToRevertToIfCompletionAborted); m_isNextTextChangeDueToCompletionChange = false; }