diff --git a/autotests/src/vimode/CMakeLists.txt b/autotests/src/vimode/CMakeLists.txt --- a/autotests/src/vimode/CMakeLists.txt +++ b/autotests/src/vimode/CMakeLists.txt @@ -19,6 +19,7 @@ vimode_unit_test(modes modes.cpp base.cpp) vimode_unit_test(view view.cpp base.cpp) vimode_unit_test(completion completion.cpp base.cpp fakecodecompletiontestmodel.cpp) -vimode_unit_test(keys keys.cpp base.cpp fakecodecompletiontestmodel.cpp) +vimode_unit_test(keys keys.cpp base.cpp fakecodecompletiontestmodel.cpp emulatedcommandbarsetupandteardown.cpp) +vimode_unit_test(emulatedcommandbar emulatedcommandbar.cpp base.cpp fakecodecompletiontestmodel.cpp emulatedcommandbarsetupandteardown.cpp) add_custom_target(vimode_tests ${CMAKE_CTEST_COMMAND} --force-new-ctest-process -R "^vimode_") diff --git a/autotests/src/vimode/base.cpp b/autotests/src/vimode/base.cpp --- a/autotests/src/vimode/base.cpp +++ b/autotests/src/vimode/base.cpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include "base.h" #include "vimode/macros.h" @@ -318,12 +318,12 @@ vi_global->macros()->clear(); } -void BaseTest::textInserted(Document *document, Range range) +void BaseTest::textInserted(Document *document, KTextEditor::Range range) { m_docChanges.append(DocChange(DocChange::TextInserted, range, document->text(range))); } -void BaseTest::textRemoved(Document *document, Range range) +void BaseTest::textRemoved(Document *document, KTextEditor::Range range) { Q_UNUSED(document); m_docChanges.append(DocChange(DocChange::TextRemoved, range)); diff --git a/autotests/src/vimode/completion.cpp b/autotests/src/vimode/completion.cpp --- a/autotests/src/vimode/completion.cpp +++ b/autotests/src/vimode/completion.cpp @@ -22,15 +22,15 @@ #include #include #include -#include +#include #include #include "completion.h" #include "fakecodecompletiontestmodel.h" #include "vimode/mappings.h" #include "vimode/globalstate.h" -using namespace KateVi; using namespace KTextEditor; +using KateVi::Mappings; QTEST_MAIN(CompletionTest) diff --git a/autotests/src/vimode/emulatedcommandbar.h b/autotests/src/vimode/emulatedcommandbar.h new file mode 100644 --- /dev/null +++ b/autotests/src/vimode/emulatedcommandbar.h @@ -0,0 +1,87 @@ +/* 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. +*/ + +#ifndef EMULATEDCOMMANDBAR_TEST_H +#define EMULATEDCOMMANDBAR_TEST_H + +#include "base.h" + +class QCompleter; +class QLabel; +class QColor; + +class EmulatedCommandBarTest : public BaseTest +{ + Q_OBJECT + +private Q_SLOTS: + void EmulatedCommandBarTests(); + +private: + QCompleter *emulatedCommandBarCompleter(); + + void verifyCommandBarCompletionVisible(); + void verifyCommandBarCompletionsMatches(const QStringList& expectedCompletionList); + void verifyCommandBarCompletionContains(const QStringList& expectedCompletionList); + QLabel *emulatedCommandTypeIndicator(); + void verifyCursorAt(const KTextEditor::Cursor& expectedCursorPos); + + void clearSearchHistory(); + QStringList searchHistory(); + void clearCommandHistory(); + QStringList commandHistory(); + void clearReplaceHistory(); + QStringList replaceHistory(); + + QList rangesOnFirstLine(); + void verifyTextEditBackgroundColour(const QColor& expectedBackgroundColour); + QLabel* commandResponseMessageDisplay(); + void waitForEmulatedCommandBarToHide(long int timeout); + void verifyShowsNumberOfReplacementsAcrossNumberOfLines(int numReplacements, int acrossNumLines); +}; + +class FailsIfSlotNotCalled : public QObject +{ + Q_OBJECT +public: + FailsIfSlotNotCalled(); + ~FailsIfSlotNotCalled(); +public Q_SLOTS: + void slot(); +private: + bool m_slotWasCalled; +}; + +class FailsIfSlotCalled : public QObject +{ + Q_OBJECT +public: + FailsIfSlotCalled(const QString& failureMessage); +public Q_SLOTS: + void slot(); +private: + const QString m_failureMessage; +}; + +#endif + +// kate: space-indent on; indent-width 2; replace-tabs on; + + diff --git a/autotests/src/vimode/emulatedcommandbar.cpp b/autotests/src/vimode/emulatedcommandbar.cpp new file mode 100644 --- /dev/null +++ b/autotests/src/vimode/emulatedcommandbar.cpp @@ -0,0 +1,3377 @@ +/* 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 indentify 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() != NULL); + 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() != NULL); + 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() != NULL); + 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() != NULL); + 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: initally, 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("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"); + // 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 localisation 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 = NULL; + 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. localisation. + // 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); + QCOMPARE(actualNumReplacementsAsString, expectedNumReplacementsAsString); + QCOMPARE(actualAcrossNumLinesAsString, expectedAcrossNumLinesAsString); +} + +FailsIfSlotNotCalled::FailsIfSlotNotCalled(): QObject(), m_slotWasCalled(false) +{ + +} + +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/emulatedcommandbarsetupandteardown.h b/autotests/src/vimode/emulatedcommandbarsetupandteardown.h new file mode 100644 --- /dev/null +++ b/autotests/src/vimode/emulatedcommandbarsetupandteardown.h @@ -0,0 +1,48 @@ +#include + +namespace KTextEditor +{ + class ViewPrivate; +} + +class KateViInputMode; +class QMainWindow; + +/** + * This class is used by the EmulatedCommandBarSetUpAndTearDown class so + * the main window is active all the time. + */ +class WindowKeepActive : public QObject +{ + Q_OBJECT + + public: + WindowKeepActive(QMainWindow *mainWindow); + + public Q_SLOTS: + bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; + + private: + QMainWindow *m_mainWindow; +}; + +/** + * Helper class that is used to setup and tear down tests affecting + * the command bar in any way. + */ +class EmulatedCommandBarSetUpAndTearDown +{ + public: + EmulatedCommandBarSetUpAndTearDown(KateViInputMode *inputMode, + KTextEditor::ViewPrivate *view, + QMainWindow *window); + + ~EmulatedCommandBarSetUpAndTearDown(); + + private: + KTextEditor::ViewPrivate *m_view; + QMainWindow *m_window; + WindowKeepActive m_windowKeepActive; + KateViInputMode *m_viInputMode; +}; + diff --git a/autotests/src/vimode/emulatedcommandbarsetupandteardown.cpp b/autotests/src/vimode/emulatedcommandbarsetupandteardown.cpp new file mode 100644 --- /dev/null +++ b/autotests/src/vimode/emulatedcommandbarsetupandteardown.cpp @@ -0,0 +1,68 @@ +#include "emulatedcommandbarsetupandteardown.h" + +#include +#include +#include +#include + +#include +#include +#include + +//BEGIN: WindowKeepActive + +WindowKeepActive::WindowKeepActive(QMainWindow *mainWindow) +: m_mainWindow(mainWindow) +{ + /* There's nothing to do here. */ +} + +bool WindowKeepActive::eventFilter(QObject *object, QEvent *event) +{ + Q_UNUSED(object); + + if (event->type() == QEvent::WindowDeactivate) { + // With some combinations of Qt and Xvfb, invoking/ dismissing a popup + // will deactiveate the m_mainWindow, preventing it from receiving shortcuts. + // If we detect this, set it back to being the active window again. + event->ignore(); + QApplication::setActiveWindow(m_mainWindow); + return true; + } + return false; +} + +//END: WindowKeepActive + +//BEGIN: EmulatedCommandBarSetUpAndTearDown + +EmulatedCommandBarSetUpAndTearDown::EmulatedCommandBarSetUpAndTearDown(KateViInputMode *inputMode, + KTextEditor::ViewPrivate *view, + QMainWindow *window) +: m_view(view), m_window(window), m_windowKeepActive(window), m_viInputMode(inputMode) +{ + m_window->show(); + m_view->show(); + QApplication::setActiveWindow(m_window); + m_view->setFocus(); + while (QApplication::hasPendingEvents()) { + QApplication::processEvents(); + } + KateViewConfig::global()->setViInputModeStealKeys(true); + m_window->installEventFilter(&m_windowKeepActive); +} +EmulatedCommandBarSetUpAndTearDown::~EmulatedCommandBarSetUpAndTearDown() +{ + m_window->removeEventFilter(&m_windowKeepActive); + // Use invokeMethod to avoid having to export KateViewBar for testing. + QMetaObject::invokeMethod(m_viInputMode->viModeEmulatedCommandBar(), "hideMe"); + m_view->hide(); + m_window->hide(); + KateViewConfig::global()->setViInputModeStealKeys(false); + while (QApplication::hasPendingEvents()) { + QApplication::processEvents(); + } +} + +//END: EmulatedCommandBarSetUpAndTearDown + diff --git a/autotests/src/vimode/keys.h b/autotests/src/vimode/keys.h --- a/autotests/src/vimode/keys.h +++ b/autotests/src/vimode/keys.h @@ -25,46 +25,6 @@ #include "base.h" - -/** - * This class is used by the VimStyleCommandBarTestsSetUpAndTearDown class so - * the main window is active all the time. - */ -class WindowKeepActive : public QObject -{ - Q_OBJECT - -public: - WindowKeepActive(QMainWindow *mainWindow); - -public Q_SLOTS: - bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; - -private: - QMainWindow *m_mainWindow; -}; - -/** - * Helper class that is used to setup and tear down tests affecting - * the command bar in any way. - */ -class VimStyleCommandBarTestsSetUpAndTearDown -{ -public: - VimStyleCommandBarTestsSetUpAndTearDown(KateViInputMode *inputMode, - KTextEditor::ViewPrivate *view, - QMainWindow *window); - - ~VimStyleCommandBarTestsSetUpAndTearDown(); - -private: - KTextEditor::ViewPrivate *m_view; - QMainWindow *m_window; - WindowKeepActive m_windowKeepActive; - KateViInputMode *m_viInputMode; -}; - - class KeysTest : public BaseTest { Q_OBJECT diff --git a/autotests/src/vimode/keys.cpp b/autotests/src/vimode/keys.cpp --- a/autotests/src/vimode/keys.cpp +++ b/autotests/src/vimode/keys.cpp @@ -23,77 +23,21 @@ #include #include #include -#include +#include #include #include "keys.h" +#include "emulatedcommandbarsetupandteardown.h" #include "fakecodecompletiontestmodel.h" #include "vimode/mappings.h" #include "vimode/globalstate.h" -using namespace KateVi; using namespace KTextEditor; +using KateVi::Mappings; +using KateVi::KeyParser; QTEST_MAIN(KeysTest) - -//BEGIN: WindowKeepActive - -WindowKeepActive::WindowKeepActive(QMainWindow *mainWindow) - : m_mainWindow(mainWindow) -{ - /* There's nothing to do here. */ -} - -bool WindowKeepActive::eventFilter(QObject *object, QEvent *event) -{ - Q_UNUSED(object); - - if (event->type() == QEvent::WindowDeactivate) { - // With some combinations of Qt and Xvfb, invoking/ dismissing a popup - // will deactiveate the m_mainWindow, preventing it from receiving shortcuts. - // If we detect this, set it back to being the active window again. - event->ignore(); - QApplication::setActiveWindow(m_mainWindow); - return true; - } - return false; -} - -//END: WindowKeepActive - -//BEGIN: VimStyleCommandBarTestsSetUpAndTearDown - -VimStyleCommandBarTestsSetUpAndTearDown::VimStyleCommandBarTestsSetUpAndTearDown(KateViInputMode *inputMode, - ViewPrivate *view, - QMainWindow *window) - : m_view(view), m_window(window), m_windowKeepActive(window), m_viInputMode(inputMode) -{ - m_window->show(); - m_view->show(); - QApplication::setActiveWindow(m_window); - m_view->setFocus(); - while (QApplication::hasPendingEvents()) { - QApplication::processEvents(); - } - KateViewConfig::global()->setViInputModeStealKeys(true); - m_window->installEventFilter(&m_windowKeepActive); -} -VimStyleCommandBarTestsSetUpAndTearDown::~VimStyleCommandBarTestsSetUpAndTearDown() -{ - m_window->removeEventFilter(&m_windowKeepActive); - // Use invokeMethod to avoid having to export KateViewBar for testing. - QMetaObject::invokeMethod(m_viInputMode->viModeEmulatedCommandBar(), "hideMe"); - m_view->hide(); - m_window->hide(); - KateViewConfig::global()->setViInputModeStealKeys(false); - while (QApplication::hasPendingEvents()) { - QApplication::processEvents(); - } -} - -//END: VimStyleCommandBarTestsSetUpAndTearDown - //BEGIN: KeysTest void KeysTest::MappingTests() @@ -428,7 +372,7 @@ } { - VimStyleCommandBarTestsSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); + EmulatedCommandBarSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); // Can have mappings in Emulated Command Bar. clearAllMappings(); vi_global->mappings()->add(Mappings::CommandModeMapping, "a", "xyz", Mappings::NonRecursive); @@ -568,7 +512,7 @@ DoTest("", "\\:iunmap l\\ilm\\esc", "le"); { - VimStyleCommandBarTestsSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); + EmulatedCommandBarSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); // cmap works in emulated command bar and is recursive. // NOTE: need to do the cmap call using the direct execution (i.e. \\:cmap blah blah\\), *not* using // the emulated command bar (:cmap blah blah\\enter), as this will be subject to mappings, which @@ -839,7 +783,7 @@ { // Ensure that we can call emulated command bar searches, and that we don't record // synthetic keypresses. - VimStyleCommandBarTestsSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); + EmulatedCommandBarSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); clearAllMacros(); DoTest("foo bar\nblank line", "qa/bar\\enterqgg@arX", "foo Xar\nblank line"); // More complex searching stuff. @@ -907,7 +851,7 @@ DoTest("XXXX\nXXXX\nXXXX\nXXXX", "qarOljq3@au", "OXXX\nXXXX\nXXXX\nXXXX"); { - VimStyleCommandBarTestsSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); + EmulatedCommandBarSetUpAndTearDown vimStyleCommandBarTestsSetUpAndTearDown(vi_input_mode, kate_view, mainWindow); // Make sure we can macro-ise an interactive sed replace. clearAllMacros(); DoTest("foo foo foo foo\nfoo foo foo foo", "qa:s/foo/bar/gc\\enteryynyAdone\\escqggj@a", "bar bar foo bardone\nbar bar foo bardone"); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -275,7 +275,13 @@ vimode/range.cpp vimode/keyparser.cpp vimode/globalstate.cpp - vimode/emulatedcommandbar.cpp + vimode/emulatedcommandbar/emulatedcommandbar.cpp + vimode/emulatedcommandbar/matchhighlighter.cpp + vimode/emulatedcommandbar/completer.cpp + vimode/emulatedcommandbar/activemode.cpp + vimode/emulatedcommandbar/interactivesedreplacemode.cpp + vimode/emulatedcommandbar/searchmode.cpp + vimode/emulatedcommandbar/commandmode.cpp vimode/commandrangeexpressionparser.cpp vimode/keymapper.cpp vimode/marks.cpp diff --git a/src/include/ktexteditor/command.h b/src/include/ktexteditor/command.h --- a/src/include/ktexteditor/command.h +++ b/src/include/ktexteditor/command.h @@ -24,6 +24,8 @@ #include #include +#include + #include class QStringList; @@ -112,7 +114,7 @@ { return m_cmds; } - + /** * Find out if a given command can act on a range. This is used for checking * if a command should be called when the user also gave a range or if an @@ -130,7 +132,7 @@ * of replaced strings as \p msg, like "16 replacements made." If an error * occurred in the usage it would return \e false and set the \p msg to * something like "missing argument." or such. - * + * * If a non-invalid range is given, the command shall be executed on that range. * supportsRange() tells if the command supports that. * @@ -198,7 +200,7 @@ * the command list this command got constructed with */ const QStringList m_cmds; - + /** * Private d-pointer */ diff --git a/src/inputmode/kateviinputmode.cpp b/src/inputmode/kateviinputmode.cpp --- a/src/inputmode/kateviinputmode.cpp +++ b/src/inputmode/kateviinputmode.cpp @@ -20,7 +20,7 @@ #include "kateviewinternal.h" #include "kateconfig.h" #include -#include +#include #include #include #include diff --git a/src/utils/katesedcmd.cpp b/src/utils/katesedcmd.cpp --- a/src/utils/katesedcmd.cpp +++ b/src/utils/katesedcmd.cpp @@ -129,7 +129,15 @@ QSharedPointer interactiveSedReplacer(new InteractiveSedReplacer(doc, find, replace, !noCase, !repeat, startLine, endLine)); if (interactive) { - return interactiveSedReplace(kateView, interactiveSedReplacer); + const bool hasInitialMatch = interactiveSedReplacer->currentMatch().isValid(); + if (!hasInitialMatch) + { + // Can't start an interactive sed replace if there is no initial match! + msg = interactiveSedReplacer->finalStatusReportMessage(); + return false; + } + interactiveSedReplace(kateView, interactiveSedReplacer); + return true; } interactiveSedReplacer->replaceAllRemaining(); diff --git a/src/vimode/cmds.cpp b/src/vimode/cmds.cpp --- a/src/vimode/cmds.cpp +++ b/src/vimode/cmds.cpp @@ -26,7 +26,7 @@ #include "kateview.h" #include "kateglobal.h" #include -#include +#include #include "katecmd.h" #include "katepartdebug.h" #include "kateviinputmode.h" diff --git a/src/vimode/emulatedcommandbar.h b/src/vimode/emulatedcommandbar.h deleted file mode 100644 --- a/src/vimode/emulatedcommandbar.h +++ /dev/null @@ -1,178 +0,0 @@ -/* This file is part of the KDE libraries and the Kate part. - * - * Copyright (C) 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. - */ - -#ifndef KATEVI_EMULATED_COMMAND_BAR_H -#define KATEVI_EMULATED_COMMAND_BAR_H - -#include "kateviewhelpers.h" -#include - -#include -#include -#include - -namespace KTextEditor { - class ViewPrivate; - class Command; -} - -class QLabel; -class QCompleter; -class QStringListModel; - -namespace KateVi -{ - -/** - * A KateViewBarWidget that attempts to emulate some of the features of Vim's own command bar, - * including insertion of register contents via ctr-r; dismissal via - * ctrl-c and ctrl-[; bi-directional incremental searching, with SmartCase; interactive sed-replace; - * plus a few extensions such as completion from document and navigable sed search and sed replace history. - */ -class KTEXTEDITOR_EXPORT EmulatedCommandBar : public KateViewBarWidget -{ - Q_OBJECT - -public: - enum Mode { NoMode, SearchForward, SearchBackward, Command }; - explicit EmulatedCommandBar(InputModeManager *viInputModeManager, QWidget *parent = 0); - virtual ~EmulatedCommandBar(); - void init(Mode mode, const QString &initialText = QString()); - bool isActive(); - void setCommandResponseMessageTimeout(long commandResponseMessageTimeOutMS); - void closed() Q_DECL_OVERRIDE; - bool handleKeyPress(const QKeyEvent *keyEvent); - bool isSendingSyntheticSearchCompletedKeypress(); - - void startInteractiveSearchAndReplace(QSharedPointer interactiveSedReplace); - QString executeCommand(const QString &commandToExecute); - - void setViInputModeManager(InputModeManager *viInputModeManager); - -private: - InputModeManager *m_viInputModeManager; - bool m_isActive; - Mode m_mode; - KTextEditor::ViewPrivate *m_view; - QLineEdit *m_edit; - QLabel *m_barTypeIndicator; - void showBarTypeIndicator(Mode mode); - KTextEditor::Cursor m_startingCursorPos; - bool m_wasAborted; - bool m_suspendEditEventFiltering; - bool m_waitingForRegister; - QLabel *m_waitingForRegisterIndicator; - bool m_insertedTextShouldBeEscapedForSearchingAsLiteral; - - QTimer *m_commandResponseMessageDisplayHide; - QLabel *m_commandResponseMessageDisplay; - long m_commandResponseMessageTimeOutMS; - void switchToCommandResponseDisplay(const QString &commandResponseMessage); - - QLabel *m_interactiveSedReplaceLabel; - bool m_interactiveSedReplaceActive; - void updateInteractiveSedReplaceLabelText(); - QSharedPointer m_interactiveSedReplacer; - void finishInteractiveSedReplace(); - - void moveCursorTo(const KTextEditor::Cursor &cursorPos); - - QCompleter *m_completer; - QStringListModel *m_completionModel; - bool m_isNextTextChangeDueToCompletionChange; - enum CompletionType { None, SearchHistory, WordFromDocument, Commands, CommandHistory, SedFindHistory, SedReplaceHistory }; - CompletionType m_currentCompletionType; - void updateCompletionPrefix(); - void currentCompletionChanged(); - bool m_completionActive; - QString m_textToRevertToIfCompletionAborted; - int m_cursorPosToRevertToIfCompletionAborted; - - KTextEditor::Attribute::Ptr m_highlightMatchAttribute; - KTextEditor::MovingRange *m_highlightedMatch; - void updateMatchHighlight(const KTextEditor::Range &matchRange); - enum BarBackgroundStatus { Normal, MatchFound, NoMatchFound }; - void setBarBackground(BarBackgroundStatus status); - - bool m_isSendingSyntheticSearchCompletedKeypress; - - bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; - void deleteSpacesToLeftOfCursor(); - void deleteWordCharsToLeftOfCursor(); - bool deleteNonWordCharsToLeftOfCursor(); - QString wordBeforeCursor(); - QString commandBeforeCursor(); - void replaceWordBeforeCursorWith(const QString &newWord); - void replaceCommandBeforeCursorWith(const QString &newCommand); - - void activateSearchHistoryCompletion(); - void activateWordFromDocumentCompletion(); - void activateCommandCompletion(); - void activateCommandHistoryCompletion(); - void activateSedFindHistoryCompletion(); - void activateSedReplaceHistoryCompletion(); - void deactivateCompletion(); - void abortCompletionAndResetToPreCompletion(); - void setCompletionIndex(int index); - - /** - * Stuff to do with expressions of the form: - * - * s/find/replace/ - */ - struct ParsedSedExpression { - bool parsedSuccessfully; - int findBeginPos; - int findEndPos; - int replaceBeginPos; - int replaceEndPos; - QChar delimiter; - }; - ParsedSedExpression parseAsSedExpression(); - QString withSedFindTermReplacedWith(const QString &newFindTerm); - QString withSedReplaceTermReplacedWith(const QString &newReplaceTerm); - QString sedFindTerm(); - QString sedReplaceTerm(); - QString withSedDelimiterEscaped(const QString &text); - - bool isCursorInFindTermOfSed(); - bool isCursorInReplaceTermOfSed(); - - /** - * The "range expression" is the (optional) expression before the command that describes - * the range over which the command should be run e.g. '<,'>. @see CommandRangeExpressionParser - */ - QString withoutRangeExpression(); - QString rangeExpression(); - -private: - KCompletion m_cmdCompletion; - QHash m_cmdDict; - KTextEditor::Command *queryCommand(const QString &cmd) const; - -private Q_SLOTS: - void editTextChanged(const QString &newText); - void updateMatchHighlightAttrib(); - void startHideCommandResponseTimer(); -}; - -} - -#endif /* KATEVI_EMULATED_COMMAND_BAR_H */ diff --git a/src/vimode/emulatedcommandbar.cpp b/src/vimode/emulatedcommandbar.cpp deleted file mode 100644 --- a/src/vimode/emulatedcommandbar.cpp +++ /dev/null @@ -1,1321 +0,0 @@ -/* This file is part of the KDE libraries and the Kate part. - * - * Copyright (C) 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 "kateconfig.h" -#include "katedocument.h" -#include "kateglobal.h" -#include "commandrangeexpressionparser.h" -#include "kateview.h" -#include "globalstate.h" -#include -#include -#include - -#include -#include -#include -#include "history.h" - -#include "katecmds.h" -#include "katescriptmanager.h" -#include "registers.h" -#include "searcher.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace KateVi; - -namespace -{ -bool caseInsensitiveLessThan(const QString &s1, const QString &s2) -{ - return s1.toLower() < s2.toLower(); -} - -bool isCharEscaped(const QString &string, int charPos) -{ - if (charPos == 0) { - return false; - } - int numContiguousBackslashesToLeft = 0; - charPos--; - while (charPos >= 0 && string[charPos] == QLatin1Char('\\')) { - numContiguousBackslashesToLeft++; - charPos--; - } - return ((numContiguousBackslashesToLeft % 2) == 1); -} - -QString toggledEscaped(const QString &originalString, QChar escapeChar) -{ - int searchFrom = 0; - QString toggledEscapedString = originalString; - do { - const int indexOfEscapeChar = toggledEscapedString.indexOf(escapeChar, searchFrom); - if (indexOfEscapeChar == -1) { - break; - } - if (!isCharEscaped(toggledEscapedString, indexOfEscapeChar)) { - // Escape. - toggledEscapedString.replace(indexOfEscapeChar, 1, QLatin1String("\\") + escapeChar); - searchFrom = indexOfEscapeChar + 2; - } else { - // Unescape. - toggledEscapedString.remove(indexOfEscapeChar - 1, 1); - searchFrom = indexOfEscapeChar; - } - } while (true); - - return toggledEscapedString; -} - -QString ensuredCharEscaped(const QString &originalString, QChar charToEscape) -{ - QString escapedString = originalString; - for (int i = 0; i < escapedString.length(); i++) { - if (escapedString[i] == charToEscape && !isCharEscaped(escapedString, i)) { - escapedString.replace(i, 1, QLatin1String("\\") + charToEscape); - } - } - return escapedString; -} - -QString vimRegexToQtRegexPattern(const QString &vimRegexPattern) -{ - QString qtRegexPattern = vimRegexPattern; - qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('(')); - qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char(')')); - qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('+')); - qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('|')); - qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('?')); - { - // All curly brackets, except the closing curly bracket of a matching pair where the opening bracket is escaped, - // must have their escaping toggled. - bool lookingForMatchingCloseBracket = false; - QList matchingClosedCurlyBracketPositions; - for (int i = 0; i < qtRegexPattern.length(); i++) { - if (qtRegexPattern[i] == QLatin1Char('{') && isCharEscaped(qtRegexPattern, i)) { - lookingForMatchingCloseBracket = true; - } - if (qtRegexPattern[i] == QLatin1Char('}') && lookingForMatchingCloseBracket && qtRegexPattern[i - 1] != QLatin1Char('\\')) { - matchingClosedCurlyBracketPositions.append(i); - } - } - if (matchingClosedCurlyBracketPositions.isEmpty()) { - // Escape all {'s and }'s - there are no matching pairs. - qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('{')); - qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('}')); - } else { - // Ensure that every chunk of qtRegexPattern that does *not* contain a curly closing bracket - // that is matched have their { and } escaping toggled. - QString qtRegexPatternNonMatchingCurliesToggled; - int previousNonMatchingClosedCurlyPos = 0; // i.e. the position of the last character which is either - // not a curly closing bracket, or is a curly closing bracket - // that is not matched. - foreach (int matchingClosedCurlyPos, matchingClosedCurlyBracketPositions) { - QString chunkExcludingMatchingCurlyClosed = qtRegexPattern.mid(previousNonMatchingClosedCurlyPos, matchingClosedCurlyPos - previousNonMatchingClosedCurlyPos); - chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('{')); - chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('}')); - qtRegexPatternNonMatchingCurliesToggled += chunkExcludingMatchingCurlyClosed + - qtRegexPattern[matchingClosedCurlyPos]; - previousNonMatchingClosedCurlyPos = matchingClosedCurlyPos + 1; - } - QString chunkAfterLastMatchingClosedCurly = qtRegexPattern.mid(matchingClosedCurlyBracketPositions.last() + 1); - chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('{')); - chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('}')); - qtRegexPatternNonMatchingCurliesToggled += chunkAfterLastMatchingClosedCurly; - - qtRegexPattern = qtRegexPatternNonMatchingCurliesToggled; - } - - } - - // All square brackets, *except* for those that are a) unescaped; and b) form a matching pair, must be - // escaped. - bool lookingForMatchingCloseBracket = false; - int openingBracketPos = -1; - QList matchingSquareBracketPositions; - for (int i = 0; i < qtRegexPattern.length(); i++) { - if (qtRegexPattern[i] == QLatin1Char('[') && !isCharEscaped(qtRegexPattern, i) && !lookingForMatchingCloseBracket) { - lookingForMatchingCloseBracket = true; - openingBracketPos = i; - } - if (qtRegexPattern[i] == QLatin1Char(']') && lookingForMatchingCloseBracket && !isCharEscaped(qtRegexPattern, i)) { - lookingForMatchingCloseBracket = false; - matchingSquareBracketPositions.append(openingBracketPos); - matchingSquareBracketPositions.append(i); - } - } - - if (matchingSquareBracketPositions.isEmpty()) { - // Escape all ['s and ]'s - there are no matching pairs. - qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('[')); - qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char(']')); - } else { - // Ensure that every chunk of qtRegexPattern that does *not* contain one of the matching pairs of - // square brackets have their square brackets escaped. - QString qtRegexPatternNonMatchingSquaresMadeLiteral; - int previousNonMatchingSquareBracketPos = 0; // i.e. the position of the last character which is - // either not a square bracket, or is a square bracket but - // which is not matched. - foreach (int matchingSquareBracketPos, matchingSquareBracketPositions) { - QString chunkExcludingMatchingSquareBrackets = qtRegexPattern.mid(previousNonMatchingSquareBracketPos, matchingSquareBracketPos - previousNonMatchingSquareBracketPos); - chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char('[')); - chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char(']')); - qtRegexPatternNonMatchingSquaresMadeLiteral += chunkExcludingMatchingSquareBrackets + qtRegexPattern[matchingSquareBracketPos]; - previousNonMatchingSquareBracketPos = matchingSquareBracketPos + 1; - } - QString chunkAfterLastMatchingSquareBracket = qtRegexPattern.mid(matchingSquareBracketPositions.last() + 1); - chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char('[')); - chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char(']')); - qtRegexPatternNonMatchingSquaresMadeLiteral += chunkAfterLastMatchingSquareBracket; - - qtRegexPattern = qtRegexPatternNonMatchingSquaresMadeLiteral; - } - - qtRegexPattern = qtRegexPattern.replace(QLatin1String("\\>"), QLatin1String("\\b")); - qtRegexPattern = qtRegexPattern.replace(QLatin1String("\\<"), QLatin1String("\\b")); - - return qtRegexPattern; -} - -/** - * @return \a originalRegex but escaped in such a way that a Qt regex search for - * the resulting string will match the string \a originalRegex. - */ -QString escapedForSearchingAsLiteral(const QString &originalQtRegex) -{ - QString escapedForSearchingAsLiteral = originalQtRegex; - escapedForSearchingAsLiteral.replace(QLatin1Char('\\'), QLatin1String("\\\\")); - escapedForSearchingAsLiteral.replace(QLatin1Char('$'), QLatin1String("\\$")); - escapedForSearchingAsLiteral.replace(QLatin1Char('^'), QLatin1String("\\^")); - escapedForSearchingAsLiteral.replace(QLatin1Char('.'), QLatin1String("\\.")); - escapedForSearchingAsLiteral.replace(QLatin1Char('*'), QLatin1String("\\*")); - escapedForSearchingAsLiteral.replace(QLatin1Char('/'), QLatin1String("\\/")); - escapedForSearchingAsLiteral.replace(QLatin1Char('['), QLatin1String("\\[")); - escapedForSearchingAsLiteral.replace(QLatin1Char(']'), QLatin1String("\\]")); - escapedForSearchingAsLiteral.replace(QLatin1Char('\n'), QLatin1String("\\n")); - return escapedForSearchingAsLiteral; -} - -QStringList reversed(const QStringList &originalList) -{ - QStringList reversedList = originalList; - std::reverse(reversedList.begin(), reversedList.end()); - return reversedList; -} - -QString withCaseSensitivityMarkersStripped(const QString &originalSearchTerm) -{ - // Only \C is handled, for now - I'll implement \c if someone asks for it. - int pos = 0; - QString caseSensitivityMarkersStripped = originalSearchTerm; - while (pos < caseSensitivityMarkersStripped.length()) { - if (caseSensitivityMarkersStripped.at(pos) == QLatin1Char('C') && isCharEscaped(caseSensitivityMarkersStripped, pos)) { - caseSensitivityMarkersStripped.replace(pos - 1, 2, QString()); - pos--; - } - pos++; - } - return caseSensitivityMarkersStripped; -} - -int findPosOfSearchConfigMarker(const QString &searchText, const bool isSearchBackwards) -{ - const QChar searchConfigMarkerChar = (isSearchBackwards ? QLatin1Char('?') : QLatin1Char('/')); - for (int pos = 0; pos < searchText.length(); pos++) { - if (searchText.at(pos) == searchConfigMarkerChar) { - if (!isCharEscaped(searchText, pos)) { - return pos; - } - } - } - return -1; -} - -bool isRepeatLastSearch(const QString &searchText, const bool isSearchBackwards) -{ - const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards); - if (posOfSearchConfigMarker != -1) { - if (searchText.leftRef(posOfSearchConfigMarker).isEmpty()) { - return true; - } - } - return false; -} - -bool shouldPlaceCursorAtEndOfMatch(const QString &searchText, const bool isSearchBackwards) -{ - const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards); - if (posOfSearchConfigMarker != -1) { - if (searchText.length() > posOfSearchConfigMarker + 1 && searchText.at(posOfSearchConfigMarker + 1) == QLatin1Char('e')) { - return true; - } - } - return false; -} - -QString withSearchConfigRemoved(const QString &originalSearchText, const bool isSearchBackwards) -{ - const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(originalSearchText, isSearchBackwards); - if (posOfSearchConfigMarker == -1) { - return originalSearchText; - } else { - return originalSearchText.left(posOfSearchConfigMarker); - } -} -} - -EmulatedCommandBar::EmulatedCommandBar(InputModeManager *viInputModeManager, QWidget *parent) - : KateViewBarWidget(false, parent) - , m_viInputModeManager(viInputModeManager) - , m_isActive(false) - , m_mode(NoMode) - , m_view(viInputModeManager->view()) - , m_wasAborted(true) - , m_suspendEditEventFiltering(false) - , m_waitingForRegister(false) - , m_insertedTextShouldBeEscapedForSearchingAsLiteral(false) - , m_commandResponseMessageTimeOutMS(4000) - , m_isNextTextChangeDueToCompletionChange(false) - , m_currentCompletionType(None) - , m_isSendingSyntheticSearchCompletedKeypress(false) -{ - QHBoxLayout *layout = new QHBoxLayout(); - layout->setMargin(0); - centralWidget()->setLayout(layout); - m_barTypeIndicator = new QLabel(this); - m_barTypeIndicator->setObjectName(QStringLiteral("bartypeindicator")); - layout->addWidget(m_barTypeIndicator); - - m_edit = new QLineEdit(this); - m_edit->setObjectName(QStringLiteral("commandtext")); - layout->addWidget(m_edit); - - m_commandResponseMessageDisplay = new QLabel(this); - m_commandResponseMessageDisplay->setObjectName(QStringLiteral("commandresponsemessage")); - m_commandResponseMessageDisplay->setAlignment(Qt::AlignLeft); - layout->addWidget(m_commandResponseMessageDisplay); - - m_waitingForRegisterIndicator = new QLabel(this); - m_waitingForRegisterIndicator->setObjectName(QStringLiteral("waitingforregisterindicator")); - m_waitingForRegisterIndicator->setVisible(false); - m_waitingForRegisterIndicator->setText(QStringLiteral("\"")); - layout->addWidget(m_waitingForRegisterIndicator); - - m_interactiveSedReplaceLabel = new QLabel(this); - m_interactiveSedReplaceLabel->setObjectName(QStringLiteral("interactivesedreplace")); - m_interactiveSedReplaceActive = false; - layout->addWidget(m_interactiveSedReplaceLabel); - - updateMatchHighlightAttrib(); - m_highlightedMatch = m_view->doc()->newMovingRange(KTextEditor::Range::invalid(), Kate::TextRange::DoNotExpand); - m_highlightedMatch->setView(m_view); // Show only in this view. - m_highlightedMatch->setAttributeOnlyForViews(true); - // Use z depth defined in moving ranges interface. - m_highlightedMatch->setZDepth(-10000.0); - m_highlightedMatch->setAttribute(m_highlightMatchAttribute); - connect(m_view, SIGNAL(configChanged()), - this, SLOT(updateMatchHighlightAttrib())); - - m_edit->installEventFilter(this); - connect(m_edit, SIGNAL(textChanged(QString)), this, SLOT(editTextChanged(QString))); - - m_completer = new QCompleter(QStringList(), m_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(m_edit); - m_completer->setObjectName(QStringLiteral("completer")); - m_completionModel = new QStringListModel(this); - m_completer->setModel(m_completionModel); - m_completer->setCaseSensitivity(Qt::CaseInsensitive); - m_completer->popup()->installEventFilter(this); - - m_commandResponseMessageDisplayHide = new QTimer(this); - m_commandResponseMessageDisplayHide->setSingleShot(true); - connect(m_commandResponseMessageDisplayHide, SIGNAL(timeout()), - this, SIGNAL(hideMe())); - // 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 as a result of m_commandResponseMessageDisplayHide - // timing out. - connect(m_view, SIGNAL(focusOut(KTextEditor::View*)), m_commandResponseMessageDisplayHide, SLOT(stop())); - // We can restart the timer once the view has focus again, though. - connect(m_view, SIGNAL(focusIn(KTextEditor::View*)), this, SLOT(startHideCommandResponseTimer())); - - 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); - } -} - -EmulatedCommandBar::~EmulatedCommandBar() -{ - delete m_highlightedMatch; -} - -void EmulatedCommandBar::init(EmulatedCommandBar::Mode mode, const QString &initialText) -{ - m_currentCompletionType = None; - m_mode = mode; - m_isActive = true; - m_wasAborted = true; - - showBarTypeIndicator(mode); - - setBarBackground(Normal); - - m_startingCursorPos = m_view->cursorPosition(); - - m_interactiveSedReplaceActive = false; - m_interactiveSedReplaceLabel->hide(); - - m_edit->setFocus(); - m_edit->setText(initialText); - m_edit->show(); - - m_commandResponseMessageDisplay->hide(); - m_commandResponseMessageDisplayHide->stop(); - - // A change in focus will have occurred: make sure we process it now, instead of having it - // occur later and stop() m_commandResponseMessageDisplayHide. - // This is generally only a problem when feeding a sequence of keys without human intervention, - // as when we execute a mapping, macro, or test case. - while (QApplication::hasPendingEvents()) { - QApplication::processEvents(); - } -} - -bool EmulatedCommandBar::isActive() -{ - return m_isActive; -} - -void EmulatedCommandBar::setCommandResponseMessageTimeout(long int commandResponseMessageTimeOutMS) -{ - m_commandResponseMessageTimeOutMS = commandResponseMessageTimeOutMS; -} - -void EmulatedCommandBar::closed() -{ - // Close can be called multiple times between init()'s, so only reset the cursor once! - if (m_startingCursorPos.isValid()) { - if (m_wasAborted) { - moveCursorTo(m_startingCursorPos); - } - } - m_startingCursorPos = KTextEditor::Cursor::invalid(); - updateMatchHighlight(KTextEditor::Range()); - m_completer->popup()->hide(); - m_isActive = false; - - if (m_mode == SearchForward || m_mode == SearchBackward) { - // Send a synthetic keypress through the system that signals whether the search was aborted or - // not. If not, the keypress will "complete" the search motion, thus triggering it. - // We send to KateViewInternal as it updates the status bar and removes the "?". - const Qt::Key syntheticSearchCompletedKey = (m_wasAborted ? static_cast(0) : Qt::Key_Enter); - QKeyEvent syntheticSearchCompletedKeyPress(QEvent::KeyPress, syntheticSearchCompletedKey, Qt::NoModifier); - m_isSendingSyntheticSearchCompletedKeypress = true; - QApplication::sendEvent(m_view->focusProxy(), &syntheticSearchCompletedKeyPress); - m_isSendingSyntheticSearchCompletedKeypress = false; - } else { - if (m_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. - m_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. - m_view->clearSelection(); - } - } -} - -void EmulatedCommandBar::updateMatchHighlightAttrib() -{ - const QColor &matchColour = m_view->renderer()->config()->searchHighlightColor(); - if (!m_highlightMatchAttribute) { - m_highlightMatchAttribute = new KTextEditor::Attribute; - } - m_highlightMatchAttribute->setBackground(matchColour); - KTextEditor::Attribute::Ptr mouseInAttribute(new KTextEditor::Attribute()); - m_highlightMatchAttribute->setDynamicAttribute(KTextEditor::Attribute::ActivateMouseIn, mouseInAttribute); - m_highlightMatchAttribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)->setBackground(matchColour); -} - -void EmulatedCommandBar::updateMatchHighlight(const KTextEditor::Range &matchRange) -{ - // Note that if matchRange is invalid, the highlight will not be shown, so we - // don't need to check for that explicitly. - m_highlightedMatch->setRange(matchRange); -} - -void EmulatedCommandBar::setBarBackground(EmulatedCommandBar::BarBackgroundStatus status) -{ - QPalette barBackground(m_edit->palette()); - switch (status) { - case MatchFound: { - KColorScheme::adjustBackground(barBackground, KColorScheme::PositiveBackground); - break; - } - case NoMatchFound: { - KColorScheme::adjustBackground(barBackground, KColorScheme::NegativeBackground); - break; - } - case Normal: { - barBackground = QPalette(); - break; - } - } - m_edit->setPalette(barBackground); -} - -bool EmulatedCommandBar::eventFilter(QObject *object, QEvent *event) -{ - Q_ASSERT(object == m_edit || object == m_completer->popup()); - if (m_suspendEditEventFiltering) { - return false; - } - Q_UNUSED(object); - if (event->type() == QEvent::KeyPress) { - // Re-route this keypress through Vim's central keypress handling area, so that we can use the keypress in e.g. - // mappings and macros. - return m_viInputModeManager->handleKeypress(static_cast(event)); - } - return false; -} - -void EmulatedCommandBar::deleteSpacesToLeftOfCursor() -{ - while (m_edit->cursorPosition() != 0 && m_edit->text().at(m_edit->cursorPosition() - 1) == QLatin1Char(' ')) { - m_edit->backspace(); - } -} - -void EmulatedCommandBar::deleteWordCharsToLeftOfCursor() -{ - while (m_edit->cursorPosition() != 0) { - const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1); - if (!charToTheLeftOfCursor.isLetterOrNumber() && charToTheLeftOfCursor != QLatin1Char('_')) { - break; - } - - m_edit->backspace(); - } -} - -bool EmulatedCommandBar::deleteNonWordCharsToLeftOfCursor() -{ - bool deletionsMade = false; - while (m_edit->cursorPosition() != 0) { - const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1); - if (charToTheLeftOfCursor.isLetterOrNumber() || charToTheLeftOfCursor == QLatin1Char('_') || charToTheLeftOfCursor == QLatin1Char(' ')) { - break; - } - - m_edit->backspace(); - deletionsMade = true; - } - return deletionsMade; -} - -QString EmulatedCommandBar::wordBeforeCursor() -{ - int wordBeforeCursorBegin = m_edit->cursorPosition() - 1; - while (wordBeforeCursorBegin >= 0 && (m_edit->text()[wordBeforeCursorBegin].isLetterOrNumber() || m_edit->text()[wordBeforeCursorBegin] == QLatin1Char('_'))) { - wordBeforeCursorBegin--; - } - wordBeforeCursorBegin++; - return m_edit->text().mid(wordBeforeCursorBegin, m_edit->cursorPosition() - wordBeforeCursorBegin); -} - -QString EmulatedCommandBar::commandBeforeCursor() -{ - 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++; - return textWithoutRangeExpression.mid(commandBeforeCursorBegin, cursorPositionWithoutRangeExpression - commandBeforeCursorBegin); - -} - -void EmulatedCommandBar::replaceWordBeforeCursorWith(const QString &newWord) -{ - const int wordBeforeCursorStart = m_edit->cursorPosition() - wordBeforeCursor().length(); - const QString newText = m_edit->text().left(m_edit->cursorPosition() - wordBeforeCursor().length()) + - newWord + - m_edit->text().mid(m_edit->cursorPosition()); - m_edit->setText(newText); - m_edit->setCursorPosition(wordBeforeCursorStart + newWord.length()); -} - -void EmulatedCommandBar::replaceCommandBeforeCursorWith(const QString &newCommand) -{ - const QString newText = m_edit->text().left(m_edit->cursorPosition() - commandBeforeCursor().length()) + - newCommand + - m_edit->text().mid(m_edit->cursorPosition()); - m_edit->setText(newText); -} - -void EmulatedCommandBar::activateSearchHistoryCompletion() -{ - m_currentCompletionType = SearchHistory; - m_completionModel->setStringList(reversed(m_viInputModeManager->globalState()->searchHistory()->items())); - updateCompletionPrefix(); - m_completer->complete(); -} - -void EmulatedCommandBar::activateWordFromDocumentCompletion() -{ - m_currentCompletionType = WordFromDocument; - QRegExp wordRegEx(QLatin1String("\\w{1,}")); - 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); - foundWords << foundWord; - wordSearchBeginPos = wordRegEx.indexIn(line, wordSearchBeginPos) + wordRegEx.matchedLength(); - } - } - foundWords = QSet::fromList(foundWords).toList(); - qSort(foundWords.begin(), foundWords.end(), caseInsensitiveLessThan); - m_completionModel->setStringList(foundWords); - updateCompletionPrefix(); - m_completer->complete(); -} - -void EmulatedCommandBar::activateCommandCompletion() -{ - m_completionModel->setStringList(m_cmdCompletion.items()); - m_currentCompletionType = Commands; -} - -void EmulatedCommandBar::activateCommandHistoryCompletion() -{ - m_currentCompletionType = CommandHistory; - m_completionModel->setStringList(reversed(m_viInputModeManager->globalState()->commandHistory()->items())); - updateCompletionPrefix(); - m_completer->complete(); -} - -void EmulatedCommandBar::activateSedFindHistoryCompletion() -{ - if (!m_viInputModeManager->globalState()->searchHistory()->isEmpty()) { - m_currentCompletionType = SedFindHistory; - m_completionModel->setStringList(reversed(m_viInputModeManager->globalState()->searchHistory()->items())); - m_completer->setCompletionPrefix(sedFindTerm()); - m_completer->complete(); - } -} - -void EmulatedCommandBar::activateSedReplaceHistoryCompletion() -{ - if (!m_viInputModeManager->globalState()->replaceHistory()->isEmpty()) { - m_currentCompletionType = SedReplaceHistory; - m_completionModel->setStringList(reversed(m_viInputModeManager->globalState()->replaceHistory()->items())); - m_completer->setCompletionPrefix(sedReplaceTerm()); - m_completer->complete(); - } -} - -void EmulatedCommandBar::deactivateCompletion() -{ - m_completer->popup()->hide(); - m_currentCompletionType = None; -} - -void EmulatedCommandBar::abortCompletionAndResetToPreCompletion() -{ - deactivateCompletion(); - m_isNextTextChangeDueToCompletionChange = true; - m_edit->setText(m_textToRevertToIfCompletionAborted); - m_edit->setCursorPosition(m_cursorPosToRevertToIfCompletionAborted); - m_isNextTextChangeDueToCompletionChange = false; -} - -void EmulatedCommandBar::updateCompletionPrefix() -{ - // TODO - switch on type is not very OO - consider making a polymorphic "completion" class. - if (m_currentCompletionType == WordFromDocument) { - m_completer->setCompletionPrefix(wordBeforeCursor()); - } else if (m_currentCompletionType == SearchHistory) { - m_completer->setCompletionPrefix(m_edit->text()); - } else if (m_currentCompletionType == CommandHistory) { - m_completer->setCompletionPrefix(m_edit->text()); - } else if (m_currentCompletionType == Commands) { - m_completer->setCompletionPrefix(commandBeforeCursor()); - } else { - Q_ASSERT(false && "Unhandled completion type"); - } - // Seem to need a call to complete() else the size of the popup box is not altered appropriately. - m_completer->complete(); -} - -void EmulatedCommandBar::currentCompletionChanged() -{ - // TODO - switch on type is not very OO - consider making a polymorphic "completion" class. - const QString newCompletion = m_completer->currentCompletion(); - if (newCompletion.isEmpty()) { - return; - } - m_isNextTextChangeDueToCompletionChange = true; - if (m_currentCompletionType == WordFromDocument) { - replaceWordBeforeCursorWith(newCompletion); - } else if (m_currentCompletionType == SearchHistory) { - m_edit->setText(newCompletion); - } else if (m_currentCompletionType == CommandHistory) { - m_edit->setText(newCompletion); - } else if (m_currentCompletionType == Commands) { - const int newCursorPosition = m_edit->cursorPosition() + (newCompletion.length() - commandBeforeCursor().length()); - replaceCommandBeforeCursorWith(newCompletion); - m_edit->setCursorPosition(newCursorPosition); - } else if (m_currentCompletionType == SedFindHistory) { - m_edit->setText(withSedFindTermReplacedWith(withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(newCompletion)))); - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - m_edit->setCursorPosition(parsedSedExpression.findEndPos + 1); - } else if (m_currentCompletionType == SedReplaceHistory) { - m_edit->setText(withSedReplaceTermReplacedWith(withSedDelimiterEscaped(newCompletion))); - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - m_edit->setCursorPosition(parsedSedExpression.replaceEndPos + 1); - } else { - Q_ASSERT(false && "Something went wrong, here - completion with unrecognised completion type"); - } - m_isNextTextChangeDueToCompletionChange = false; -} - -void EmulatedCommandBar::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(); -} - -EmulatedCommandBar::ParsedSedExpression EmulatedCommandBar::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 EmulatedCommandBar::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 EmulatedCommandBar::withSedReplaceTermReplacedWith(const QString &newReplaceTerm) -{ - const QString command = m_edit->text(); - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - Q_ASSERT(parsedSedExpression.parsedSuccessfully); - return command.mid(0, parsedSedExpression.replaceBeginPos) + - newReplaceTerm + - command.mid(parsedSedExpression.replaceEndPos + 1); -} - -QString EmulatedCommandBar::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 EmulatedCommandBar::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 EmulatedCommandBar::withSedDelimiterEscaped(const QString &text) -{ - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - QString delimiterEscaped = ensuredCharEscaped(text, parsedSedExpression.delimiter); - return delimiterEscaped; -} - -bool EmulatedCommandBar::isCursorInFindTermOfSed() -{ - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - return parsedSedExpression.parsedSuccessfully && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1); -} - -bool EmulatedCommandBar::isCursorInReplaceTermOfSed() -{ - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1; -} - -QString EmulatedCommandBar::withoutRangeExpression() -{ - const QString originalCommand = m_edit->text(); - return originalCommand.mid(rangeExpression().length()); -} - -QString EmulatedCommandBar::rangeExpression() -{ - const QString command = m_edit->text(); - return CommandRangeExpressionParser(m_viInputModeManager).parseRangeString(command); -} - -bool EmulatedCommandBar::handleKeyPress(const QKeyEvent *keyEvent) -{ - if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft) && !m_waitingForRegister) { - if (m_currentCompletionType == None || !m_completer->popup()->isVisible()) { - emit hideMe(); - } else { - abortCompletionAndResetToPreCompletion(); - } - return true; - } - if (m_interactiveSedReplaceActive) { - // TODO - it would be better to use e.g. keyEvent->key() == Qt::Key_Y instead of keyEvent->text() == "y", - // but this would require some slightly dicey changes to the "feed key press" code in order to make it work - // with mappings and macros. - if (keyEvent->text() == QLatin1String("y") || keyEvent->text() == QLatin1String("n")) { - const KTextEditor::Cursor cursorPosIfFinalMatch = m_interactiveSedReplacer->currentMatch().start(); - if (keyEvent->text() == QLatin1String("y")) { - m_interactiveSedReplacer->replaceCurrentMatch(); - } else { - m_interactiveSedReplacer->skipCurrentMatch(); - } - updateMatchHighlight(m_interactiveSedReplacer->currentMatch()); - updateInteractiveSedReplaceLabelText(); - moveCursorTo(m_interactiveSedReplacer->currentMatch().start()); - - if (!m_interactiveSedReplacer->currentMatch().isValid()) { - moveCursorTo(cursorPosIfFinalMatch); - finishInteractiveSedReplace(); - } - return true; - } else if (keyEvent->text() == QLatin1String("l")) { - m_interactiveSedReplacer->replaceCurrentMatch(); - finishInteractiveSedReplace(); - return true; - } else if (keyEvent->text() == QLatin1String("q")) { - finishInteractiveSedReplace(); - return true; - } else if (keyEvent->text() == QLatin1String("a")) { - m_interactiveSedReplacer->replaceAllRemaining(); - finishInteractiveSedReplace(); - return true; - } - return false; - } - if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_Space) { - activateWordFromDocumentCompletion(); - return true; - } - if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_P) || keyEvent->key() == Qt::Key_Down) { - if (!m_completer->popup()->isVisible()) { - if (m_mode == Command) { - if (isCursorInFindTermOfSed()) { - activateSedFindHistoryCompletion(); - } else if (isCursorInReplaceTermOfSed()) { - activateSedReplaceHistoryCompletion(); - } else { - activateCommandHistoryCompletion(); - } - } else { - activateSearchHistoryCompletion(); - } - if (m_currentCompletionType != 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()) { - if (m_mode == Command) { - activateCommandHistoryCompletion(); - } else { - activateSearchHistoryCompletion(); - } - 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 (m_waitingForRegister) { - if (keyEvent->key() != Qt::Key_Shift && keyEvent->key() != Qt::Key_Control) { - const QChar key = KeyParser::self()->KeyEventToQChar(*keyEvent).toLower(); - - const int oldCursorPosition = m_edit->cursorPosition(); - QString textToInsert; - if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_W) { - textToInsert = m_view->doc()->wordAt(m_view->cursorPosition()); - } else { - textToInsert = m_viInputModeManager->globalState()->registers()->getContent(key); - } - if (m_insertedTextShouldBeEscapedForSearchingAsLiteral) { - textToInsert = escapedForSearchingAsLiteral(textToInsert); - m_insertedTextShouldBeEscapedForSearchingAsLiteral = false; - } - m_edit->setText(m_edit->text().insert(m_edit->cursorPosition(), textToInsert)); - m_edit->setCursorPosition(oldCursorPosition + textToInsert.length()); - m_waitingForRegister = false; - m_waitingForRegisterIndicator->setVisible(false); - } - } else if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_H) || keyEvent->key() == Qt::Key_Backspace) { - if (m_edit->text().isEmpty()) { - emit hideMe(); - } - m_edit->backspace(); - return true; - } else if (keyEvent->modifiers() == Qt::ControlModifier) { - if (keyEvent->key() == Qt::Key_B) { - m_edit->setCursorPosition(0); - return true; - } else if (keyEvent->key() == Qt::Key_E) { - m_edit->setCursorPosition(m_edit->text().length()); - return true; - } else if (keyEvent->key() == Qt::Key_W) { - deleteSpacesToLeftOfCursor(); - if (!deleteNonWordCharsToLeftOfCursor()) { - deleteWordCharsToLeftOfCursor(); - } - return true; - } else if (keyEvent->key() == Qt::Key_R || keyEvent->key() == Qt::Key_G) { - m_waitingForRegister = true; - m_waitingForRegisterIndicator->setVisible(true); - if (keyEvent->key() == Qt::Key_G) { - m_insertedTextShouldBeEscapedForSearchingAsLiteral = true; - } - return true; - } else if (keyEvent->key() == Qt::Key_D || keyEvent->key() == Qt::Key_F) { - if (m_mode == Command) { - 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; - } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { - if (m_completer->popup()->isVisible() && m_currentCompletionType == EmulatedCommandBar::WordFromDocument) { - deactivateCompletion(); - } else { - m_wasAborted = false; - deactivateCompletion(); - if (m_mode == Command) { - QString commandToExecute = m_edit->text(); - ParsedSedExpression parsedSedExpression = parseAsSedExpression(); - if (parsedSedExpression.parsedSuccessfully) { - const QString originalFindTerm = sedFindTerm(); - const QString convertedFindTerm = vimRegexToQtRegexPattern(originalFindTerm); - const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(convertedFindTerm); - m_viInputModeManager->globalState()->searchHistory()->append(originalFindTerm); - const QString replaceTerm = sedReplaceTerm(); - m_viInputModeManager->globalState()->replaceHistory()->append(replaceTerm); - commandToExecute = commandWithSedSearchRegexConverted; - } - - const QString commandResponseMessage = executeCommand(commandToExecute); - if (!m_interactiveSedReplaceActive) { - if (commandResponseMessage.isEmpty() && !m_interactiveSedReplaceActive) { - emit hideMe(); - } else { - switchToCommandResponseDisplay(commandResponseMessage); - } - } - m_viInputModeManager->globalState()->commandHistory()->append(m_edit->text()); - } else { - emit hideMe(); - } - } - return true; - } else { - m_suspendEditEventFiltering = true; - // Send the keypress back to the QLineEdit. Ideally, instead of doing this, we would simply return "false" - // and let Qt re-dispatch the event itself; however, there is a corner case in that if the selection - // changes (as a result of e.g. incremental searches during Visual Mode), and the keypress that causes it - // is not dispatched from within KateViInputModeHandler::handleKeypress(...) - // (so KateViInputModeManager::isHandlingKeypress() returns false), we lose information about whether we are - // in Visual Mode, Visual Line Mode, etc. See VisualViMode::updateSelection( ). - QKeyEvent keyEventCopy(keyEvent->type(), keyEvent->key(), keyEvent->modifiers(), keyEvent->text(), keyEvent->isAutoRepeat(), keyEvent->count()); - if (!m_interactiveSedReplaceActive) { - qApp->notify(m_edit, &keyEventCopy); - } - m_suspendEditEventFiltering = false; - } - return true; -} - -bool EmulatedCommandBar::isSendingSyntheticSearchCompletedKeypress() -{ - return m_isSendingSyntheticSearchCompletedKeypress; -} - -void EmulatedCommandBar::startInteractiveSearchAndReplace(QSharedPointer interactiveSedReplace) -{ - m_interactiveSedReplaceActive = true; - m_interactiveSedReplacer = interactiveSedReplace; - if (!interactiveSedReplace->currentMatch().isValid()) { - // Bit of a hack, but we leave m_incrementalSearchAndReplaceActive true, here, else - // the bar would be immediately hidden and the "0 replacements made" message not shown. - finishInteractiveSedReplace(); - return; - } - m_commandResponseMessageDisplay->hide(); - m_edit->hide(); - m_barTypeIndicator->hide(); - m_interactiveSedReplaceLabel->show(); - updateMatchHighlight(interactiveSedReplace->currentMatch()); - updateInteractiveSedReplaceLabelText(); - moveCursorTo(interactiveSedReplace->currentMatch().start()); -} - -void EmulatedCommandBar::showBarTypeIndicator(EmulatedCommandBar::Mode mode) -{ - QChar barTypeIndicator = QChar::Null; - switch (mode) { - case SearchForward: - barTypeIndicator = QLatin1Char('/'); - break; - case SearchBackward: - barTypeIndicator = QLatin1Char('?'); - break; - case Command: - barTypeIndicator = QLatin1Char(':'); - break; - default: - Q_ASSERT(false && "Unknown mode!"); - } - m_barTypeIndicator->setText(barTypeIndicator); - m_barTypeIndicator->show(); -} - -QString EmulatedCommandBar::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(m_viInputModeManager).parseRange(cmd, cmd); - - if (cmd.length() > 0) { - KTextEditor::Command *p = queryCommand(cmd); - - if (p) { - KateViCommandInterface *ci = dynamic_cast(p); - if (ci) { - ci->setViInputModeManager(m_viInputModeManager); - ci->setViGlobal(m_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 inherit the RangeCommand - // extension. bail out. - if (range.isValid() && !p->supportsRange(cmd)) { - commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd); - } else { - - if (p->exec(m_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(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|bn|bnext|bf|bfirst|bl|blast|edit|e")).exactMatch(cmd.split(QLatin1String(" ")).at(0))) { - m_view->setFocus(); - } - - m_viInputModeManager->reset(); - return commandResponseMessage; -} - -void EmulatedCommandBar::switchToCommandResponseDisplay(const QString &commandResponseMessage) -{ - // Display the message for a while. Become inactive, so we don't steal keys in the meantime. - m_isActive = false; - m_edit->hide(); - m_interactiveSedReplaceLabel->hide(); - m_barTypeIndicator->hide(); - m_commandResponseMessageDisplay->show(); - m_commandResponseMessageDisplay->setText(commandResponseMessage); - m_commandResponseMessageDisplayHide->start(m_commandResponseMessageTimeOutMS); -} - -void EmulatedCommandBar::updateInteractiveSedReplaceLabelText() -{ - m_interactiveSedReplaceLabel->setText(m_interactiveSedReplacer->currentMatchReplacementConfirmationMessage() + QLatin1String(" (y/n/a/q/l)")); -} - -void EmulatedCommandBar::finishInteractiveSedReplace() -{ - switchToCommandResponseDisplay(m_interactiveSedReplacer->finalStatusReportMessage()); - m_interactiveSedReplacer.clear(); -} - -void EmulatedCommandBar::moveCursorTo(const KTextEditor::Cursor &cursorPos) -{ - m_view->setCursorPosition(cursorPos); - if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode || - m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { - - m_viInputModeManager->getViVisualMode()->goToPos(cursorPos); - } -} - -void EmulatedCommandBar::editTextChanged(const QString &newText) -{ - Q_ASSERT(!m_interactiveSedReplaceActive); - if (!m_isNextTextChangeDueToCompletionChange) { - m_textToRevertToIfCompletionAborted = newText; - m_cursorPosToRevertToIfCompletionAborted = m_edit->cursorPosition(); - } - if (m_mode == SearchForward || m_mode == SearchBackward) { - QString qtRegexPattern = newText; - const bool searchBackwards = (m_mode == SearchBackward); - const bool placeCursorAtEndOfMatch = shouldPlaceCursorAtEndOfMatch(qtRegexPattern, searchBackwards); - if (isRepeatLastSearch(qtRegexPattern, searchBackwards)) { - qtRegexPattern = m_viInputModeManager->searcher()->getLastSearchPattern(); - } else { - qtRegexPattern = withSearchConfigRemoved(qtRegexPattern, searchBackwards); - qtRegexPattern = vimRegexToQtRegexPattern(qtRegexPattern); - } - - // Decide case-sensitivity via SmartCase (note: if the expression contains \C, the "case-sensitive" marker, then - // we will be case-sensitive "by coincidence", as it were.). - bool caseSensitive = true; - if (qtRegexPattern.toLower() == qtRegexPattern) { - caseSensitive = false; - } - - qtRegexPattern = withCaseSensitivityMarkersStripped(qtRegexPattern); - - // The "count" for the current search is not shared between Visual & Normal mode, so we need to pick - // the right one to handle the counted search. - int c = m_viInputModeManager->getCurrentViModeHandler()->getCount(); - KTextEditor::Range match = m_viInputModeManager->searcher()->findPattern(qtRegexPattern, searchBackwards, caseSensitive, placeCursorAtEndOfMatch, m_startingCursorPos, c); - - if (match.isValid()) { - // The returned range ends one past the last character of the match, so adjust. - KTextEditor::Cursor realMatchEnd = KTextEditor::Cursor(match.end().line(), match.end().column() - 1); - if (realMatchEnd.column() == -1) { - realMatchEnd = KTextEditor::Cursor(realMatchEnd.line() - 1, m_view->doc()->lineLength(realMatchEnd.line() - 1)); - } - moveCursorTo(placeCursorAtEndOfMatch ? realMatchEnd : match.start()); - setBarBackground(MatchFound); - } else { - moveCursorTo(m_startingCursorPos); - if (!m_edit->text().isEmpty()) { - setBarBackground(NoMatchFound); - } else { - setBarBackground(Normal); - } - } - - updateMatchHighlight(match); - } - - // Command completion doesn't need to be manually invoked. - if (m_mode == Command && m_currentCompletionType == None && !withoutRangeExpression().isEmpty()) { - activateCommandCompletion(); - } - - // Command completion mode should be automatically invoked if we are in Command mode, but - // only if this is the leading word in the text edit (it gets annoying if completion pops up - // after ":s/se" etc). - const bool commandBeforeCursorIsLeading = (m_edit->cursorPosition() - commandBeforeCursor().length() == rangeExpression().length()); - if (m_mode == Command && !commandBeforeCursorIsLeading && m_currentCompletionType == Commands && !m_isNextTextChangeDueToCompletionChange) { - deactivateCompletion(); - } - - // 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 != None && !m_isNextTextChangeDueToCompletionChange) { - updateCompletionPrefix(); - } -} - -void EmulatedCommandBar::startHideCommandResponseTimer() -{ - if (m_commandResponseMessageDisplay->isVisible() && !m_commandResponseMessageDisplayHide->isActive()) { - m_commandResponseMessageDisplayHide->start(m_commandResponseMessageTimeOutMS); - } -} - -KTextEditor::Command *EmulatedCommandBar::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)); -} - -void EmulatedCommandBar::setViInputModeManager(InputModeManager *viInputModeManager) -{ - m_viInputModeManager = viInputModeManager; -} diff --git a/src/vimode/emulatedcommandbar/activemode.h b/src/vimode/emulatedcommandbar/activemode.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/activemode.h @@ -0,0 +1,64 @@ +#ifndef KATEVI_EMULATED_COMMAND_BAR_ACTIVEMODE_H +#define KATEVI_EMULATED_COMMAND_BAR_ACTIVEMODE_H + +#include "completer.h" + +class QKeyEvent; +class QString; +class QWidget; + +namespace KTextEditor +{ + class Cursor; + class Range; + class ViewPrivate; +} + +namespace KateVi +{ +class EmulatedCommandBar; +class CompletionStartParams; +class MatchHighlighter; +class InputModeManager; + +class ActiveMode +{ +public: + ActiveMode(EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view) + : m_emulatedCommandBar(emulatedCommandBar), + m_viInputModeManager(viInputModeManager), + m_view(view), + m_matchHighligher(matchHighlighter) + { + } + virtual ~ActiveMode() = 0; + virtual bool handleKeyPress(const QKeyEvent *keyEvent) = 0; + virtual void editTextChanged(const QString &newText) + { + Q_UNUSED(newText); + } + virtual KateVi::CompletionStartParams completionInvoked(Completer::CompletionInvocation invocationType); + virtual void completionChosen() + { + } + virtual void deactivate(bool wasAborted) = 0; + void setViInputModeManager(InputModeManager *viInputModeManager); +protected: + // Helper methods. + void hideAllWidgetsExcept(QWidget* widgetToKeepVisible); + void updateMatchHighlight(const KTextEditor::Range &matchRange); + void close(bool wasAborted); + void closeWithStatusMessage(const QString& exitStatusMessage); + void startCompletion(const CompletionStartParams& completionStartParams); + void moveCursorTo(const KTextEditor::Cursor &cursorPos); + EmulatedCommandBar *emulatedCommandBar(); + KTextEditor::ViewPrivate* view(); + InputModeManager* viInputModeManager(); +private: + EmulatedCommandBar *m_emulatedCommandBar = nullptr; + InputModeManager* m_viInputModeManager = nullptr; + KTextEditor::ViewPrivate* m_view = nullptr; + MatchHighlighter *m_matchHighligher = nullptr; +}; +} +#endif diff --git a/src/vimode/emulatedcommandbar/activemode.cpp b/src/vimode/emulatedcommandbar/activemode.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/activemode.cpp @@ -0,0 +1,78 @@ +#include "activemode.h" +#include "completer.h" +#include "emulatedcommandbar.h" +#include "matchhighlighter.h" + +#include +#include + +#include "kateview.h" + +using namespace KateVi; + +CompletionStartParams ActiveMode::completionInvoked(Completer::CompletionInvocation invocationType) +{ + Q_UNUSED(invocationType); + return CompletionStartParams(); +} + +void ActiveMode::setViInputModeManager(InputModeManager* viInputModeManager) +{ + m_viInputModeManager = viInputModeManager; +} + +ActiveMode::~ActiveMode() +{ + +} + +void ActiveMode::hideAllWidgetsExcept(QWidget* widgetToKeepVisible) +{ + m_emulatedCommandBar->hideAllWidgetsExcept(widgetToKeepVisible); +} + +void ActiveMode::updateMatchHighlight(const KTextEditor::Range& matchRange) +{ + m_matchHighligher->updateMatchHighlight(matchRange); +} + +void ActiveMode::close( bool wasAborted ) +{ + m_emulatedCommandBar->m_wasAborted = wasAborted; + m_emulatedCommandBar->hideMe(); +} + +void ActiveMode::closeWithStatusMessage(const QString& exitStatusMessage) +{ + m_emulatedCommandBar->closeWithStatusMessage(exitStatusMessage); +} + +void ActiveMode::startCompletion ( const CompletionStartParams& completionStartParams ) +{ + m_emulatedCommandBar->m_completer->startCompletion(completionStartParams); +} + +void ActiveMode::moveCursorTo(const KTextEditor::Cursor &cursorPos) +{ + m_view->setCursorPosition(cursorPos); + if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode || + m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { + + m_viInputModeManager->getViVisualMode()->goToPos(cursorPos); + } +} + +EmulatedCommandBar *ActiveMode::emulatedCommandBar() +{ + return m_emulatedCommandBar; +} + +KTextEditor::ViewPrivate* ActiveMode::view() +{ + return m_view; +} + +InputModeManager* ActiveMode::viInputModeManager() +{ + return m_viInputModeManager; +} diff --git a/src/vimode/emulatedcommandbar/commandmode.h b/src/vimode/emulatedcommandbar/commandmode.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/commandmode.h @@ -0,0 +1,77 @@ +#ifndef KATEVI_EMULATED_COMMAND_BAR_COMMANDMODE_H +#define KATEVI_EMULATED_COMMAND_BAR_COMMANDMODE_H + +#include "activemode.h" + +#include + +#include + +namespace KTextEditor { + class ViewPrivate; +} + +namespace KateVi +{ +class EmulatedCommandBar; +class MatchHighlighter; +class InteractiveSedReplaceMode; +class Completer; +class InputModeManager; + +class CommandMode : public ActiveMode +{ +public: + CommandMode(EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view, QLineEdit* edit, InteractiveSedReplaceMode *interactiveSedReplaceMode, Completer* completer); + virtual ~CommandMode() + { + } + virtual bool handleKeyPress ( const QKeyEvent* keyEvent ); + virtual void editTextChanged(const QString &newText); + virtual CompletionStartParams completionInvoked(Completer::CompletionInvocation invocationType); + virtual void completionChosen(); + void deactivate(bool wasAborted); + QString executeCommand(const QString &commandToExecute); +private: + CompletionStartParams activateCommandCompletion(); + CompletionStartParams activateCommandHistoryCompletion(); + CompletionStartParams activateSedFindHistoryCompletion(); + CompletionStartParams activateSedReplaceHistoryCompletion(); + QString withoutRangeExpression(); + QString rangeExpression(); + QString withSedFindTermReplacedWith(const QString &newFindTerm); + QString withSedDelimiterEscaped(const QString &text); + bool isCursorInFindTermOfSed(); + bool isCursorInReplaceTermOfSed(); + QString sedFindTerm(); + QString sedReplaceTerm(); + /** + * Stuff to do with expressions of the form: + * + * s/find/replace/ + */ + struct ParsedSedExpression { + bool parsedSuccessfully; + int findBeginPos; + int findEndPos; + int replaceBeginPos; + int replaceEndPos; + QChar delimiter; + }; + /** + * The "range expression" is the (optional) expression before the command that describes + * the range over which the command should be run e.g. '<,'>. @see CommandRangeExpressionParser + */ + CommandMode::ParsedSedExpression parseAsSedExpression(); + void replaceCommandBeforeCursorWith(const QString &newCommand); + int commandBeforeCursorBegin(); + QLineEdit *m_edit; + InteractiveSedReplaceMode *m_interactiveSedReplaceMode; + Completer *m_completer; + KCompletion m_cmdCompletion; + QHash m_cmdDict; + KTextEditor::Command *queryCommand(const QString &cmd) const; +}; +} + +#endif diff --git a/src/vimode/emulatedcommandbar/commandmode.cpp b/src/vimode/emulatedcommandbar/commandmode.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/commandmode.cpp @@ -0,0 +1,387 @@ +#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 + +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 inherit the RangeCommand + // extension. Bail out. + 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|bn|bnext|bf|bfirst|bl|blast|edit|e")).exactMatch(cmd.split(QLatin1String(" ")).at(0))) { + 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.h b/src/vimode/emulatedcommandbar/completer.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/completer.h @@ -0,0 +1,82 @@ +#ifndef KATEVI_EMULATED_COMMAND_BAR_COMPLETER_H +#define KATEVI_EMULATED_COMMAND_BAR_COMPLETER_H + +#include + +#include + +namespace KTextEditor +{ + class ViewPrivate; +} + +class QLineEdit; +class QCompleter; +class QStringListModel; +class QKeyEvent; + +namespace KateVi +{ + class ActiveMode; + class EmulatedCommandBar; + + struct CompletionStartParams + { + static CompletionStartParams createModeSpecific(const QStringList& completions, int wordStartPos, std::function completionTransform = std::function()) + { + CompletionStartParams completionStartParams; + completionStartParams.completionType = ModeSpecific; + completionStartParams.completions = completions; + completionStartParams.wordStartPos = wordStartPos; + completionStartParams.completionTransform = completionTransform; + return completionStartParams; + } + static CompletionStartParams invalid() + { + CompletionStartParams completionStartParams; + completionStartParams.completionType = None; + return completionStartParams; + } + enum CompletionType { None, ModeSpecific, WordFromDocument }; + CompletionType completionType = None; + int wordStartPos = -1; + QStringList completions; + std::function completionTransform; + }; + + class Completer + { + public: + enum class CompletionInvocation { ExtraContext, NormalContext }; + Completer(EmulatedCommandBar* emulatedCommandBar, KTextEditor::ViewPrivate* view, QLineEdit* edit); + void startCompletion(const CompletionStartParams& completionStartParams); + void deactivateCompletion(); + bool isCompletionActive() const; + bool isNextTextChangeDueToCompletionChange() const; + bool completerHandledKeypress(const QKeyEvent* keyEvent); + void editTextChanged(const QString &newText); + void setCurrentMode(ActiveMode* currentMode); + private: + QLineEdit *m_edit; + KTextEditor::ViewPrivate *m_view; + ActiveMode *m_currentMode = nullptr; + + void setCompletionIndex(int index); + void currentCompletionChanged(); + void updateCompletionPrefix(); + CompletionStartParams activateWordFromDocumentCompletion(); + QString wordBeforeCursor(); + int wordBeforeCursorBegin(); + void abortCompletionAndResetToPreCompletion(); + + QCompleter *m_completer; + QStringListModel *m_completionModel; + QString m_textToRevertToIfCompletionAborted; + int m_cursorPosToRevertToIfCompletionAborted; + bool m_isNextTextChangeDueToCompletionChange = false; + CompletionStartParams m_currentCompletionStartParams; + CompletionStartParams::CompletionType m_currentCompletionType = CompletionStartParams::None; + }; +} +#endif + diff --git a/src/vimode/emulatedcommandbar/completer.cpp b/src/vimode/emulatedcommandbar/completer.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/completer.cpp @@ -0,0 +1,236 @@ +#include "completer.h" +#include "emulatedcommandbar.h" + +using namespace KateVi; + +#include "kateview.h" + +#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,}")); + 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); + foundWords << foundWord; + wordSearchBeginPos = wordRegEx.indexIn(line, wordSearchBeginPos) + wordRegEx.matchedLength(); + } + } + foundWords = QSet::fromList(foundWords).toList(); + qSort(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; +} + diff --git a/src/vimode/emulatedcommandbar/emulatedcommandbar.h b/src/vimode/emulatedcommandbar/emulatedcommandbar.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/emulatedcommandbar.h @@ -0,0 +1,130 @@ +/* This file is part of the KDE libraries and the Kate part. + * + * Copyright (C) 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. + */ + +#ifndef KATEVI_EMULATED_COMMAND_BAR_H +#define KATEVI_EMULATED_COMMAND_BAR_H + +#include "kateviewhelpers.h" +#include + +#include +#include +#include "../searcher.h" +#include "activemode.h" + +namespace KTextEditor { + class ViewPrivate; + class Command; +} + +class QLabel; +class QLayout; + +namespace KateVi +{ +class MatchHighlighter; +class InteractiveSedReplaceMode; +class SearchMode; +class CommandMode; + +/** + * A KateViewBarWidget that attempts to emulate some of the features of Vim's own command bar, + * including insertion of register contents via ctr-r; dismissal via + * ctrl-c and ctrl-[; bi-directional incremental searching, with SmartCase; interactive sed-replace; + * plus a few extensions such as completion from document and navigable sed search and sed replace history. + */ +class KTEXTEDITOR_EXPORT EmulatedCommandBar : public KateViewBarWidget +{ + Q_OBJECT + +public: + enum Mode { NoMode, SearchForward, SearchBackward, Command }; + explicit EmulatedCommandBar(InputModeManager *viInputModeManager, QWidget *parent = 0); + virtual ~EmulatedCommandBar(); + void init(Mode mode, const QString &initialText = QString()); + bool isActive(); + void setCommandResponseMessageTimeout(long commandResponseMessageTimeOutMS); + bool handleKeyPress(const QKeyEvent *keyEvent); + bool isSendingSyntheticSearchCompletedKeypress(); + + void startInteractiveSearchAndReplace(QSharedPointer interactiveSedReplace); + QString executeCommand(const QString &commandToExecute); + + void setViInputModeManager(InputModeManager *viInputModeManager); + +private: + + InputModeManager *m_viInputModeManager; + bool m_isActive = false; + bool m_wasAborted = true; + Mode m_mode = NoMode; + KTextEditor::ViewPrivate *m_view = nullptr; + QLineEdit *m_edit = nullptr; + + QLabel *m_barTypeIndicator = nullptr; + void showBarTypeIndicator(Mode mode); + + bool m_suspendEditEventFiltering = false; + + bool m_waitingForRegister = false ; + QLabel *m_waitingForRegisterIndicator; + bool m_insertedTextShouldBeEscapedForSearchingAsLiteral = false; + + void hideAllWidgetsExcept(QWidget* widgetToKeepVisible); + + friend class ActiveMode; + QScopedPointer m_matchHighligher; + + QScopedPointer m_completer; + + QScopedPointer m_interactiveSedReplaceMode; + QScopedPointer m_searchMode; + QScopedPointer m_commandMode; + + void switchToMode(ActiveMode *newMode); + ActiveMode *m_currentMode = nullptr; + + bool barHandledKeypress(const QKeyEvent* keyEvent); + void insertRegisterContents(const QKeyEvent *keyEvent); + bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; + void deleteSpacesToLeftOfCursor(); + void deleteWordCharsToLeftOfCursor(); + bool deleteNonWordCharsToLeftOfCursor(); + + void closed() Q_DECL_OVERRIDE; + void closeWithStatusMessage(const QString& exitStatusMessage); + QTimer *m_exitStatusMessageDisplayHideTimer; + QLabel *m_exitStatusMessageDisplay; + long m_exitStatusMessageHideTimeOutMS = 4000; + + void createAndAddBarTypeIndicator(QLayout* layout); + void createAndAddEditWidget(QLayout* layout); + void createAndAddExitStatusMessageDisplay(QLayout* layout); + void createAndInitExitStatusMessageDisplayTimer(); + void createAndAddWaitingForRegisterIndicator(QLayout* layout); + +private Q_SLOTS: + void editTextChanged(const QString &newText); + void startHideExitStatusMessageTimer(); +}; + +} + +#endif /* KATEVI_EMULATED_COMMAND_BAR_H */ diff --git a/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp b/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp @@ -0,0 +1,438 @@ +/* This file is part of the KDE libraries and the Kate part. + * + * Copyright (C) 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 "katedocument.h" +#include "kateglobal.h" +#include "../commandrangeexpressionparser.h" +#include "kateview.h" +#include "../globalstate.h" +#include +#include +#include +#include "matchhighlighter.h" +#include "interactivesedreplacemode.h" +#include "searchmode.h" +#include "commandmode.h" + +#include "../history.h" + +#include "../registers.h" +#include "../searcher.h" + +#include +#include +#include +#include + +#include + +using namespace KateVi; + +namespace +{ +/** + * @return \a originalRegex but escaped in such a way that a Qt regex search for + * the resulting string will match the string \a originalRegex. + */ +QString escapedForSearchingAsLiteral(const QString &originalQtRegex) +{ + QString escapedForSearchingAsLiteral = originalQtRegex; + escapedForSearchingAsLiteral.replace(QLatin1Char('\\'), QLatin1String("\\\\")); + escapedForSearchingAsLiteral.replace(QLatin1Char('$'), QLatin1String("\\$")); + escapedForSearchingAsLiteral.replace(QLatin1Char('^'), QLatin1String("\\^")); + escapedForSearchingAsLiteral.replace(QLatin1Char('.'), QLatin1String("\\.")); + escapedForSearchingAsLiteral.replace(QLatin1Char('*'), QLatin1String("\\*")); + escapedForSearchingAsLiteral.replace(QLatin1Char('/'), QLatin1String("\\/")); + escapedForSearchingAsLiteral.replace(QLatin1Char('['), QLatin1String("\\[")); + escapedForSearchingAsLiteral.replace(QLatin1Char(']'), QLatin1String("\\]")); + escapedForSearchingAsLiteral.replace(QLatin1Char('\n'), QLatin1String("\\n")); + return escapedForSearchingAsLiteral; +} +} + +EmulatedCommandBar::EmulatedCommandBar(InputModeManager *viInputModeManager, QWidget *parent) + : KateViewBarWidget(false, parent), + m_viInputModeManager(viInputModeManager), + m_view(viInputModeManager->view()) +{ + QHBoxLayout *layout = new QHBoxLayout(); + layout->setMargin(0); + centralWidget()->setLayout(layout); + + createAndAddBarTypeIndicator(layout); + createAndAddEditWidget(layout); + createAndAddExitStatusMessageDisplay(layout); + createAndInitExitStatusMessageDisplayTimer(); + createAndAddWaitingForRegisterIndicator(layout); + + m_matchHighligher.reset(new MatchHighlighter(m_view)); + + m_completer.reset(new Completer(this, m_view, m_edit)); + + m_interactiveSedReplaceMode.reset(new InteractiveSedReplaceMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view)); + layout->addWidget(m_interactiveSedReplaceMode->label()); + m_searchMode.reset(new SearchMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view, m_edit)); + m_commandMode.reset(new CommandMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view, m_edit, m_interactiveSedReplaceMode.data(), m_completer.data())); + + m_edit->installEventFilter(this); + connect(m_edit, SIGNAL(textChanged(QString)), this, SLOT(editTextChanged(QString))); +} + +EmulatedCommandBar::~EmulatedCommandBar() +{ +} + +void EmulatedCommandBar::init(EmulatedCommandBar::Mode mode, const QString &initialText) +{ + m_mode = mode; + m_isActive = true; + m_wasAborted = true; + + showBarTypeIndicator(mode); + + if (mode == KateVi::EmulatedCommandBar::SearchBackward || mode == SearchForward) + { + switchToMode(m_searchMode.data()); + m_searchMode->init(mode == SearchBackward ? SearchMode::SearchDirection::Backward : SearchMode::SearchDirection::Forward); + } + else + { + switchToMode(m_commandMode.data()); + } + + m_edit->setFocus(); + m_edit->setText(initialText); + m_edit->show(); + + m_exitStatusMessageDisplay->hide(); + m_exitStatusMessageDisplayHideTimer->stop(); + + // A change in focus will have occurred: make sure we process it now, instead of having it + // occur later and stop() m_commandResponseMessageDisplayHide. + // This is generally only a problem when feeding a sequence of keys without human intervention, + // as when we execute a mapping, macro, or test case. + while (QApplication::hasPendingEvents()) { + QApplication::processEvents(); + } +} + +bool EmulatedCommandBar::isActive() +{ + return m_isActive; +} + +void EmulatedCommandBar::setCommandResponseMessageTimeout(long int commandResponseMessageTimeOutMS) +{ + m_exitStatusMessageHideTimeOutMS = commandResponseMessageTimeOutMS; +} + +void EmulatedCommandBar::closed() +{ + m_matchHighligher->updateMatchHighlight(KTextEditor::Range::invalid()); + m_completer->deactivateCompletion(); + m_isActive = false; + + m_currentMode->deactivate(m_wasAborted); + m_currentMode = nullptr; +} + +void EmulatedCommandBar::switchToMode ( ActiveMode* newMode ) +{ + if (m_currentMode) + m_currentMode->deactivate(false); + m_currentMode = newMode; + m_completer->setCurrentMode(newMode); +} + +bool EmulatedCommandBar::barHandledKeypress ( const QKeyEvent* keyEvent ) +{ + if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_H) || keyEvent->key() == Qt::Key_Backspace) { + if (m_edit->text().isEmpty()) { + emit hideMe(); + } + m_edit->backspace(); + return true; + } + if (keyEvent->modifiers() != Qt::ControlModifier) + return false; + if (keyEvent->key() == Qt::Key_B) { + m_edit->setCursorPosition(0); + return true; + } else if (keyEvent->key() == Qt::Key_E) { + m_edit->setCursorPosition(m_edit->text().length()); + return true; + } else if (keyEvent->key() == Qt::Key_W) { + deleteSpacesToLeftOfCursor(); + if (!deleteNonWordCharsToLeftOfCursor()) { + deleteWordCharsToLeftOfCursor(); + } + return true; + } else if (keyEvent->key() == Qt::Key_R || keyEvent->key() == Qt::Key_G) { + m_waitingForRegister = true; + m_waitingForRegisterIndicator->setVisible(true); + if (keyEvent->key() == Qt::Key_G) { + m_insertedTextShouldBeEscapedForSearchingAsLiteral = true; + } + return true; + } + return false; +} + +void EmulatedCommandBar::insertRegisterContents(const QKeyEvent *keyEvent) +{ + if (keyEvent->key() != Qt::Key_Shift && keyEvent->key() != Qt::Key_Control) { + const QChar key = KeyParser::self()->KeyEventToQChar(*keyEvent).toLower(); + + const int oldCursorPosition = m_edit->cursorPosition(); + QString textToInsert; + if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_W) { + textToInsert = m_view->doc()->wordAt(m_view->cursorPosition()); + } else { + textToInsert = m_viInputModeManager->globalState()->registers()->getContent(key); + } + if (m_insertedTextShouldBeEscapedForSearchingAsLiteral) { + textToInsert = escapedForSearchingAsLiteral(textToInsert); + m_insertedTextShouldBeEscapedForSearchingAsLiteral = false; + } + m_edit->setText(m_edit->text().insert(m_edit->cursorPosition(), textToInsert)); + m_edit->setCursorPosition(oldCursorPosition + textToInsert.length()); + m_waitingForRegister = false; + m_waitingForRegisterIndicator->setVisible(false); + } +} + +bool EmulatedCommandBar::eventFilter(QObject *object, QEvent *event) +{ + // The "object" will be either m_edit or m_completer's popup. + if (m_suspendEditEventFiltering) { + return false; + } + Q_UNUSED(object); + if (event->type() == QEvent::KeyPress) { + // Re-route this keypress through Vim's central keypress handling area, so that we can use the keypress in e.g. + // mappings and macros. + return m_viInputModeManager->handleKeypress(static_cast(event)); + } + return false; +} + +void EmulatedCommandBar::deleteSpacesToLeftOfCursor() +{ + while (m_edit->cursorPosition() != 0 && m_edit->text().at(m_edit->cursorPosition() - 1) == QLatin1Char(' ')) { + m_edit->backspace(); + } +} + +void EmulatedCommandBar::deleteWordCharsToLeftOfCursor() +{ + while (m_edit->cursorPosition() != 0) { + const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1); + if (!charToTheLeftOfCursor.isLetterOrNumber() && charToTheLeftOfCursor != QLatin1Char('_')) { + break; + } + + m_edit->backspace(); + } +} + +bool EmulatedCommandBar::deleteNonWordCharsToLeftOfCursor() +{ + bool deletionsMade = false; + while (m_edit->cursorPosition() != 0) { + const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1); + if (charToTheLeftOfCursor.isLetterOrNumber() || charToTheLeftOfCursor == QLatin1Char('_') || charToTheLeftOfCursor == QLatin1Char(' ')) { + break; + } + + m_edit->backspace(); + deletionsMade = true; + } + return deletionsMade; +} + +bool EmulatedCommandBar::handleKeyPress(const QKeyEvent *keyEvent) +{ + if (m_waitingForRegister) { + insertRegisterContents(keyEvent); + return true; + } + const bool completerHandled = m_completer->completerHandledKeypress(keyEvent); + if (completerHandled) + return true; + + if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) { + emit hideMe(); + return true; + } + + // Is this a built-in Emulated Command Bar keypress e.g. insert from register, ctrl-h, etc? + const bool barHandled = barHandledKeypress(keyEvent); + if (barHandled) + return true; + + // Can the current mode handle it? + const bool currentModeHandled = m_currentMode->handleKeyPress(keyEvent); + if (currentModeHandled) + return true; + + // Couldn't handle this key event. + // Send the keypress back to the QLineEdit. Ideally, instead of doing this, we would simply return "false" + // and let Qt re-dispatch the event itself; however, there is a corner case in that if the selection + // changes (as a result of e.g. incremental searches during Visual Mode), and the keypress that causes it + // is not dispatched from within KateViInputModeHandler::handleKeypress(...) + // (so KateViInputModeManager::isHandlingKeypress() returns false), we lose information about whether we are + // in Visual Mode, Visual Line Mode, etc. See VisualViMode::updateSelection( ). + if (m_edit->isVisible()) + { + m_suspendEditEventFiltering = true; + QKeyEvent keyEventCopy(keyEvent->type(), keyEvent->key(), keyEvent->modifiers(), keyEvent->text(), keyEvent->isAutoRepeat(), keyEvent->count()); + qApp->notify(m_edit, &keyEventCopy); + m_suspendEditEventFiltering = false; + } + return true; +} + +bool EmulatedCommandBar::isSendingSyntheticSearchCompletedKeypress() +{ + return m_searchMode->isSendingSyntheticSearchCompletedKeypress(); +} + +void EmulatedCommandBar::startInteractiveSearchAndReplace(QSharedPointer interactiveSedReplace) +{ + Q_ASSERT_X(interactiveSedReplace->currentMatch().isValid(), "startInteractiveSearchAndReplace", "KateCommands shouldn't initiate an interactive sed replace with no initial match"); + switchToMode(m_interactiveSedReplaceMode.data()); + m_interactiveSedReplaceMode->activate(interactiveSedReplace); +} + +void EmulatedCommandBar::showBarTypeIndicator(EmulatedCommandBar::Mode mode) +{ + QChar barTypeIndicator = QChar::Null; + switch (mode) { + case SearchForward: + barTypeIndicator = QLatin1Char('/'); + break; + case SearchBackward: + barTypeIndicator = QLatin1Char('?'); + break; + case Command: + barTypeIndicator = QLatin1Char(':'); + break; + default: + Q_ASSERT(false && "Unknown mode!"); + } + m_barTypeIndicator->setText(barTypeIndicator); + m_barTypeIndicator->show(); +} + +QString EmulatedCommandBar::executeCommand(const QString &commandToExecute) +{ + return m_commandMode->executeCommand(commandToExecute); +} + +void EmulatedCommandBar::closeWithStatusMessage(const QString &exitStatusMessage) +{ + // Display the message for a while. Become inactive, so we don't steal keys in the meantime. + m_isActive = false; + + m_exitStatusMessageDisplay->show(); + m_exitStatusMessageDisplay->setText(exitStatusMessage); + hideAllWidgetsExcept(m_exitStatusMessageDisplay); + + m_exitStatusMessageDisplayHideTimer->start(m_exitStatusMessageHideTimeOutMS); +} + +void EmulatedCommandBar::editTextChanged(const QString &newText) +{ + Q_ASSERT(!m_interactiveSedReplaceMode->isActive()); + m_currentMode->editTextChanged(newText); + m_completer->editTextChanged(newText); +} + +void EmulatedCommandBar::startHideExitStatusMessageTimer() +{ + if (m_exitStatusMessageDisplay->isVisible() && !m_exitStatusMessageDisplayHideTimer->isActive()) { + m_exitStatusMessageDisplayHideTimer->start(m_exitStatusMessageHideTimeOutMS); + } +} + +void EmulatedCommandBar::setViInputModeManager(InputModeManager *viInputModeManager) +{ + m_viInputModeManager = viInputModeManager; + m_searchMode->setViInputModeManager(viInputModeManager); + m_commandMode->setViInputModeManager(viInputModeManager); +} + +void EmulatedCommandBar::hideAllWidgetsExcept(QWidget* widgetToKeepVisible) +{ + QList widgets = centralWidget()->findChildren(); + foreach(QWidget* widget, widgets) + { + if (widget != widgetToKeepVisible) + widget->hide(); + } + +} + +void EmulatedCommandBar::createAndAddBarTypeIndicator(QLayout* layout) +{ + m_barTypeIndicator = new QLabel(this); + m_barTypeIndicator->setObjectName(QStringLiteral("bartypeindicator")); + layout->addWidget(m_barTypeIndicator); +} + +void EmulatedCommandBar::createAndAddEditWidget(QLayout* layout) +{ + m_edit = new QLineEdit(this); + m_edit->setObjectName(QStringLiteral("commandtext")); + layout->addWidget(m_edit); +} + +void EmulatedCommandBar::createAndAddExitStatusMessageDisplay(QLayout* layout) +{ + m_exitStatusMessageDisplay = new QLabel(this); + m_exitStatusMessageDisplay->setObjectName(QStringLiteral("commandresponsemessage")); + m_exitStatusMessageDisplay->setAlignment(Qt::AlignLeft); + layout->addWidget(m_exitStatusMessageDisplay); +} + +void EmulatedCommandBar::createAndInitExitStatusMessageDisplayTimer() +{ + m_exitStatusMessageDisplayHideTimer = new QTimer(this); + m_exitStatusMessageDisplayHideTimer->setSingleShot(true); + connect(m_exitStatusMessageDisplayHideTimer, SIGNAL(timeout()), + this, SIGNAL(hideMe())); + // 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 as a result of m_commandResponseMessageDisplayHide + // timing out. + connect(m_view, SIGNAL(focusOut(KTextEditor::View*)), m_exitStatusMessageDisplayHideTimer, SLOT(stop())); + // We can restart the timer once the view has focus again, though. + connect(m_view, SIGNAL(focusIn(KTextEditor::View*)), this, SLOT(startHideExitStatusMessageTimer())); +} + +void EmulatedCommandBar::createAndAddWaitingForRegisterIndicator(QLayout* layout) +{ + m_waitingForRegisterIndicator = new QLabel(this); + m_waitingForRegisterIndicator->setObjectName(QStringLiteral("waitingforregisterindicator")); + m_waitingForRegisterIndicator->setVisible(false); + m_waitingForRegisterIndicator->setText(QStringLiteral("\"")); + layout->addWidget(m_waitingForRegisterIndicator); +} diff --git a/src/vimode/emulatedcommandbar/interactivesedreplacemode.h b/src/vimode/emulatedcommandbar/interactivesedreplacemode.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/interactivesedreplacemode.h @@ -0,0 +1,42 @@ +#ifndef KATEVI_EMULATED_COMMAND_BAR_INTERACTIVESEDREPLACEMODE_H +#define KATEVI_EMULATED_COMMAND_BAR_INTERACTIVESEDREPLACEMODE_H + +#include "activemode.h" + +#include "../cmds.h" + +#include + +class QKeyEvent; +class QLabel; + +namespace KateVi +{ +class EmulatedCommandBar; +class MatchHighlighter; + +class InteractiveSedReplaceMode : public ActiveMode +{ +public: + InteractiveSedReplaceMode(EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view); + virtual ~InteractiveSedReplaceMode() + { + }; + void activate(QSharedPointer interactiveSedReplace); + bool isActive() const + { + return m_isActive; + } + virtual bool handleKeyPress(const QKeyEvent* keyEvent); + virtual void deactivate(bool wasAborted); + QWidget *label(); +private: + void updateInteractiveSedReplaceLabelText(); + void finishInteractiveSedReplace(); + QSharedPointer m_interactiveSedReplacer; + bool m_isActive; + QLabel *m_interactiveSedReplaceLabel; +}; +} + +#endif diff --git a/src/vimode/emulatedcommandbar/interactivesedreplacemode.cpp b/src/vimode/emulatedcommandbar/interactivesedreplacemode.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/interactivesedreplacemode.cpp @@ -0,0 +1,89 @@ +#include "interactivesedreplacemode.h" + +#include +#include + +using namespace KateVi; + +InteractiveSedReplaceMode::InteractiveSedReplaceMode(EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view) + : ActiveMode(emulatedCommandBar, matchHighlighter, viInputModeManager, view), + m_isActive(false) +{ + m_interactiveSedReplaceLabel = new QLabel(); + m_interactiveSedReplaceLabel->setObjectName(QStringLiteral("interactivesedreplace")); +} + +void InteractiveSedReplaceMode::activate(QSharedPointer interactiveSedReplace) +{ + Q_ASSERT_X(interactiveSedReplace->currentMatch().isValid(), "startInteractiveSearchAndReplace", "KateCommands shouldn't initiate an interactive sed replace with no initial match"); + + m_isActive = true; + m_interactiveSedReplacer = interactiveSedReplace; + + hideAllWidgetsExcept(m_interactiveSedReplaceLabel); + m_interactiveSedReplaceLabel->show(); + updateInteractiveSedReplaceLabelText(); + + updateMatchHighlight(interactiveSedReplace->currentMatch()); + moveCursorTo(interactiveSedReplace->currentMatch().start()); +} + +bool InteractiveSedReplaceMode::handleKeyPress(const QKeyEvent* keyEvent) +{ + // TODO - it would be better to use e.g. keyEvent->key() == Qt::Key_Y instead of keyEvent->text() == "y", + // but this would require some slightly dicey changes to the "feed key press" code in order to make it work + // with mappings and macros. + if (keyEvent->text() == QLatin1String("y") || keyEvent->text() == QLatin1String("n")) { + const KTextEditor::Cursor cursorPosIfFinalMatch = m_interactiveSedReplacer->currentMatch().start(); + if (keyEvent->text() == QLatin1String("y")) { + m_interactiveSedReplacer->replaceCurrentMatch(); + } else { + m_interactiveSedReplacer->skipCurrentMatch(); + } + updateMatchHighlight(m_interactiveSedReplacer->currentMatch()); + updateInteractiveSedReplaceLabelText(); + moveCursorTo(m_interactiveSedReplacer->currentMatch().start()); + + if (!m_interactiveSedReplacer->currentMatch().isValid()) { + moveCursorTo(cursorPosIfFinalMatch); + finishInteractiveSedReplace(); + } + return true; + } else if (keyEvent->text() == QLatin1String("l")) { + m_interactiveSedReplacer->replaceCurrentMatch(); + finishInteractiveSedReplace(); + return true; + } else if (keyEvent->text() == QLatin1String("q")) { + finishInteractiveSedReplace(); + return true; + } else if (keyEvent->text() == QLatin1String("a")) { + m_interactiveSedReplacer->replaceAllRemaining(); + finishInteractiveSedReplace(); + return true; + } + return false; +} + +void InteractiveSedReplaceMode::deactivate( bool wasAborted ) +{ + Q_UNUSED(wasAborted); + m_isActive = false; + m_interactiveSedReplaceLabel->hide(); +} + +QWidget* InteractiveSedReplaceMode::label() +{ + return m_interactiveSedReplaceLabel; +} + +void InteractiveSedReplaceMode::updateInteractiveSedReplaceLabelText() +{ + m_interactiveSedReplaceLabel->setText(m_interactiveSedReplacer->currentMatchReplacementConfirmationMessage() + QLatin1String(" (y/n/a/q/l)")); +} + +void InteractiveSedReplaceMode::finishInteractiveSedReplace() +{ + deactivate(false); + closeWithStatusMessage(m_interactiveSedReplacer->finalStatusReportMessage()); + m_interactiveSedReplacer.clear(); +} diff --git a/src/vimode/emulatedcommandbar/matchhighlighter.h b/src/vimode/emulatedcommandbar/matchhighlighter.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/matchhighlighter.h @@ -0,0 +1,32 @@ +#ifndef KATEVI_EMULATED_COMMAND_BAR_MATCHHIGHLIGHTER_H +#define KATEVI_EMULATED_COMMAND_BAR_MATCHHIGHLIGHTER_H + +#include + +#include + +namespace KTextEditor +{ + class ViewPrivate; + class Range; + class MovingRange; +} + +namespace KateVi +{ +class MatchHighlighter : public QObject +{ + Q_OBJECT +public: + MatchHighlighter(KTextEditor::ViewPrivate* view); + ~MatchHighlighter(); + void updateMatchHighlight(const KTextEditor::Range &matchRange); +private Q_SLOTS: + void updateMatchHighlightAttrib(); +private: + KTextEditor::ViewPrivate *m_view = nullptr; + KTextEditor::Attribute::Ptr m_highlightMatchAttribute; + KTextEditor::MovingRange *m_highlightedMatch; +}; +} +#endif diff --git a/src/vimode/emulatedcommandbar/matchhighlighter.cpp b/src/vimode/emulatedcommandbar/matchhighlighter.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/matchhighlighter.cpp @@ -0,0 +1,44 @@ +#include "matchhighlighter.h" + +#include "kateview.h" +#include "katedocument.h" +#include "kateconfig.h" + +using namespace KateVi; + +MatchHighlighter::MatchHighlighter ( KTextEditor::ViewPrivate* view ) + : m_view(view) +{ updateMatchHighlightAttrib(); + m_highlightedMatch = m_view->doc()->newMovingRange(KTextEditor::Range::invalid(), Kate::TextRange::DoNotExpand); + m_highlightedMatch->setView(m_view); // Show only in this view. + m_highlightedMatch->setAttributeOnlyForViews(true); + // Use z depth defined in moving ranges interface. + m_highlightedMatch->setZDepth(-10000.0); + m_highlightedMatch->setAttribute(m_highlightMatchAttribute); + connect(m_view, SIGNAL(configChanged()), + this, SLOT(updateMatchHighlightAttrib())); +} + +MatchHighlighter::~MatchHighlighter() +{ + delete m_highlightedMatch; +} + +void MatchHighlighter::updateMatchHighlight ( const KTextEditor::Range& matchRange ) +{ + // Note that if matchRange is invalid, the highlight will not be shown, so we + // don't need to check for that explicitly. + m_highlightedMatch->setRange(matchRange); +} + +void MatchHighlighter::updateMatchHighlightAttrib() +{ + const QColor &matchColour = m_view->renderer()->config()->searchHighlightColor(); + if (!m_highlightMatchAttribute) { + m_highlightMatchAttribute = new KTextEditor::Attribute; + } + m_highlightMatchAttribute->setBackground(matchColour); + KTextEditor::Attribute::Ptr mouseInAttribute(new KTextEditor::Attribute()); + m_highlightMatchAttribute->setDynamicAttribute(KTextEditor::Attribute::ActivateMouseIn, mouseInAttribute); + m_highlightMatchAttribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)->setBackground(matchColour); +} diff --git a/src/vimode/emulatedcommandbar/searchmode.h b/src/vimode/emulatedcommandbar/searchmode.h new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/searchmode.h @@ -0,0 +1,52 @@ +#ifndef KATEVI_EMULATED_COMMAND_BAR_SEARCHMODE_H +#define KATEVI_EMULATED_COMMAND_BAR_SEARCHMODE_H + +#include "activemode.h" +#include "../searcher.h" + +namespace KTextEditor { + class ViewPrivate; +} + +#include + +namespace KateVi +{ +class EmulatedCommandBar; +QString vimRegexToQtRegexPattern(const QString &vimRegexPattern); // TODO - move these generic helper functions into their own file? +QString withCaseSensitivityMarkersStripped(const QString &originalSearchTerm); +QString ensuredCharEscaped(const QString &originalString, QChar charToEscape); +QStringList reversed(const QStringList &originalList); + +class SearchMode : public ActiveMode +{ +public: + SearchMode(EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view, QLineEdit* edit); + virtual ~SearchMode() + { + }; + enum class SearchDirection { Forward, Backward }; + void init(SearchDirection); + virtual bool handleKeyPress ( const QKeyEvent* keyEvent ); + virtual void editTextChanged(const QString &newText); + virtual CompletionStartParams completionInvoked(Completer::CompletionInvocation invocationType); + virtual void completionChosen(); + virtual void deactivate(bool wasAborted); + bool isSendingSyntheticSearchCompletedKeypress() const + { + return m_isSendingSyntheticSearchCompletedKeypress; + } +private: + EmulatedCommandBar *m_emulatedCommandBar = nullptr; + QLineEdit *m_edit = nullptr; + SearchDirection m_searchDirection; + KTextEditor::Cursor m_startingCursorPos; + KateVi::Searcher::SearchParams m_currentSearchParams; + CompletionStartParams activateSearchHistoryCompletion(); + enum BarBackgroundStatus { Normal, MatchFound, NoMatchFound }; + void setBarBackground(BarBackgroundStatus status); + bool m_isSendingSyntheticSearchCompletedKeypress = false; +}; +} + +#endif diff --git a/src/vimode/emulatedcommandbar/searchmode.cpp b/src/vimode/emulatedcommandbar/searchmode.cpp new file mode 100644 --- /dev/null +++ b/src/vimode/emulatedcommandbar/searchmode.cpp @@ -0,0 +1,367 @@ +#include "searchmode.h" + +#include +#include +#include "../globalstate.h" +#include "../history.h" +#include "kateview.h" +#include "katedocument.h" + +#include + +#include +#include + +using namespace KateVi; + +namespace +{ + bool isCharEscaped(const QString &string, int charPos) + { + if (charPos == 0) { + return false; + } + int numContiguousBackslashesToLeft = 0; + charPos--; + while (charPos >= 0 && string[charPos] == QLatin1Char('\\')) { + numContiguousBackslashesToLeft++; + charPos--; + } + return ((numContiguousBackslashesToLeft % 2) == 1); + } + + QString toggledEscaped(const QString &originalString, QChar escapeChar) + { + int searchFrom = 0; + QString toggledEscapedString = originalString; + do { + const int indexOfEscapeChar = toggledEscapedString.indexOf(escapeChar, searchFrom); + if (indexOfEscapeChar == -1) { + break; + } + if (!isCharEscaped(toggledEscapedString, indexOfEscapeChar)) { + // Escape. + toggledEscapedString.replace(indexOfEscapeChar, 1, QLatin1String("\\") + escapeChar); + searchFrom = indexOfEscapeChar + 2; + } else { + // Unescape. + toggledEscapedString.remove(indexOfEscapeChar - 1, 1); + searchFrom = indexOfEscapeChar; + } + } while (true); + + return toggledEscapedString; + } + + int findPosOfSearchConfigMarker(const QString &searchText, const bool isSearchBackwards) + { + const QChar searchConfigMarkerChar = (isSearchBackwards ? QLatin1Char('?') : QLatin1Char('/')); + for (int pos = 0; pos < searchText.length(); pos++) { + if (searchText.at(pos) == searchConfigMarkerChar) { + if (!isCharEscaped(searchText, pos)) { + return pos; + } + } + } + return -1; + } + + bool isRepeatLastSearch(const QString &searchText, const bool isSearchBackwards) + { + const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards); + if (posOfSearchConfigMarker != -1) { + if (searchText.leftRef(posOfSearchConfigMarker).isEmpty()) { + return true; + } + } + return false; + } + + bool shouldPlaceCursorAtEndOfMatch(const QString &searchText, const bool isSearchBackwards) + { + const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards); + if (posOfSearchConfigMarker != -1) { + if (searchText.length() > posOfSearchConfigMarker + 1 && searchText.at(posOfSearchConfigMarker + 1) == QLatin1Char('e')) { + return true; + } + } + return false; + } + + QString withSearchConfigRemoved(const QString &originalSearchText, const bool isSearchBackwards) + { + const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(originalSearchText, isSearchBackwards); + if (posOfSearchConfigMarker == -1) { + return originalSearchText; + } else { + return originalSearchText.left(posOfSearchConfigMarker); + } + } +} + +QString KateVi::vimRegexToQtRegexPattern(const QString &vimRegexPattern) +{ + QString qtRegexPattern = vimRegexPattern; + qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('(')); + qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char(')')); + qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('+')); + qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('|')); + qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('?')); + { + // All curly brackets, except the closing curly bracket of a matching pair where the opening bracket is escaped, + // must have their escaping toggled. + bool lookingForMatchingCloseBracket = false; + QList matchingClosedCurlyBracketPositions; + for (int i = 0; i < qtRegexPattern.length(); i++) { + if (qtRegexPattern[i] == QLatin1Char('{') && isCharEscaped(qtRegexPattern, i)) { + lookingForMatchingCloseBracket = true; + } + if (qtRegexPattern[i] == QLatin1Char('}') && lookingForMatchingCloseBracket && qtRegexPattern[i - 1] != QLatin1Char('\\')) { + matchingClosedCurlyBracketPositions.append(i); + } + } + if (matchingClosedCurlyBracketPositions.isEmpty()) { + // Escape all {'s and }'s - there are no matching pairs. + qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('{')); + qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('}')); + } else { + // Ensure that every chunk of qtRegexPattern that does *not* contain a curly closing bracket + // that is matched have their { and } escaping toggled. + QString qtRegexPatternNonMatchingCurliesToggled; + int previousNonMatchingClosedCurlyPos = 0; // i.e. the position of the last character which is either + // not a curly closing bracket, or is a curly closing bracket + // that is not matched. + foreach (int matchingClosedCurlyPos, matchingClosedCurlyBracketPositions) { + QString chunkExcludingMatchingCurlyClosed = qtRegexPattern.mid(previousNonMatchingClosedCurlyPos, matchingClosedCurlyPos - previousNonMatchingClosedCurlyPos); + chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('{')); + chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('}')); + qtRegexPatternNonMatchingCurliesToggled += chunkExcludingMatchingCurlyClosed + + qtRegexPattern[matchingClosedCurlyPos]; + previousNonMatchingClosedCurlyPos = matchingClosedCurlyPos + 1; + } + QString chunkAfterLastMatchingClosedCurly = qtRegexPattern.mid(matchingClosedCurlyBracketPositions.last() + 1); + chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('{')); + chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('}')); + qtRegexPatternNonMatchingCurliesToggled += chunkAfterLastMatchingClosedCurly; + + qtRegexPattern = qtRegexPatternNonMatchingCurliesToggled; + } + + } + + // All square brackets, *except* for those that are a) unescaped; and b) form a matching pair, must be + // escaped. + bool lookingForMatchingCloseBracket = false; + int openingBracketPos = -1; + QList matchingSquareBracketPositions; + for (int i = 0; i < qtRegexPattern.length(); i++) { + if (qtRegexPattern[i] == QLatin1Char('[') && !isCharEscaped(qtRegexPattern, i) && !lookingForMatchingCloseBracket) { + lookingForMatchingCloseBracket = true; + openingBracketPos = i; + } + if (qtRegexPattern[i] == QLatin1Char(']') && lookingForMatchingCloseBracket && !isCharEscaped(qtRegexPattern, i)) { + lookingForMatchingCloseBracket = false; + matchingSquareBracketPositions.append(openingBracketPos); + matchingSquareBracketPositions.append(i); + } + } + + if (matchingSquareBracketPositions.isEmpty()) { + // Escape all ['s and ]'s - there are no matching pairs. + qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('[')); + qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char(']')); + } else { + // Ensure that every chunk of qtRegexPattern that does *not* contain one of the matching pairs of + // square brackets have their square brackets escaped. + QString qtRegexPatternNonMatchingSquaresMadeLiteral; + int previousNonMatchingSquareBracketPos = 0; // i.e. the position of the last character which is + // either not a square bracket, or is a square bracket but + // which is not matched. + foreach (int matchingSquareBracketPos, matchingSquareBracketPositions) { + QString chunkExcludingMatchingSquareBrackets = qtRegexPattern.mid(previousNonMatchingSquareBracketPos, matchingSquareBracketPos - previousNonMatchingSquareBracketPos); + chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char('[')); + chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char(']')); + qtRegexPatternNonMatchingSquaresMadeLiteral += chunkExcludingMatchingSquareBrackets + qtRegexPattern[matchingSquareBracketPos]; + previousNonMatchingSquareBracketPos = matchingSquareBracketPos + 1; + } + QString chunkAfterLastMatchingSquareBracket = qtRegexPattern.mid(matchingSquareBracketPositions.last() + 1); + chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char('[')); + chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char(']')); + qtRegexPatternNonMatchingSquaresMadeLiteral += chunkAfterLastMatchingSquareBracket; + + qtRegexPattern = qtRegexPatternNonMatchingSquaresMadeLiteral; + } + + qtRegexPattern = qtRegexPattern.replace(QLatin1String("\\>"), QLatin1String("\\b")); + qtRegexPattern = qtRegexPattern.replace(QLatin1String("\\<"), QLatin1String("\\b")); + + return qtRegexPattern; +} + +QString KateVi::ensuredCharEscaped(const QString &originalString, QChar charToEscape) +{ + QString escapedString = originalString; + for (int i = 0; i < escapedString.length(); i++) { + if (escapedString[i] == charToEscape && !isCharEscaped(escapedString, i)) { + escapedString.replace(i, 1, QLatin1String("\\") + charToEscape); + } + } + return escapedString; +} + +QString KateVi::withCaseSensitivityMarkersStripped(const QString &originalSearchTerm) +{ + // Only \C is handled, for now - I'll implement \c if someone asks for it. + int pos = 0; + QString caseSensitivityMarkersStripped = originalSearchTerm; + while (pos < caseSensitivityMarkersStripped.length()) { + if (caseSensitivityMarkersStripped.at(pos) == QLatin1Char('C') && isCharEscaped(caseSensitivityMarkersStripped, pos)) { + caseSensitivityMarkersStripped.replace(pos - 1, 2, QString()); + pos--; + } + pos++; + } + return caseSensitivityMarkersStripped; +} + +QStringList KateVi::reversed(const QStringList &originalList) +{ + QStringList reversedList = originalList; + std::reverse(reversedList.begin(), reversedList.end()); + return reversedList; +} + +SearchMode::SearchMode(EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view, QLineEdit* edit) + : ActiveMode ( emulatedCommandBar, matchHighlighter, viInputModeManager, view), + m_emulatedCommandBar(emulatedCommandBar), + m_edit(edit) +{ +} + +void SearchMode::init ( SearchMode::SearchDirection searchDirection) +{ + m_searchDirection = searchDirection; + m_startingCursorPos = view()->cursorPosition(); +} + +bool SearchMode::handleKeyPress ( const QKeyEvent* keyEvent ) +{ + Q_UNUSED(keyEvent); + return false; +} + +void SearchMode::editTextChanged ( const QString& newText ) +{ + QString qtRegexPattern = newText; + const bool searchBackwards = (m_searchDirection == SearchDirection::Backward); + const bool placeCursorAtEndOfMatch = shouldPlaceCursorAtEndOfMatch(qtRegexPattern, searchBackwards); + if (isRepeatLastSearch(qtRegexPattern, searchBackwards)) { + qtRegexPattern = viInputModeManager()->searcher()->getLastSearchPattern(); + } else { + qtRegexPattern = withSearchConfigRemoved(qtRegexPattern, searchBackwards); + qtRegexPattern = vimRegexToQtRegexPattern(qtRegexPattern); + } + + // Decide case-sensitivity via SmartCase (note: if the expression contains \C, the "case-sensitive" marker, then + // we will be case-sensitive "by coincidence", as it were.). + bool caseSensitive = true; + if (qtRegexPattern.toLower() == qtRegexPattern) { + caseSensitive = false; + } + + qtRegexPattern = withCaseSensitivityMarkersStripped(qtRegexPattern); + + m_currentSearchParams.pattern = qtRegexPattern; + m_currentSearchParams.isCaseSensitive = caseSensitive; + m_currentSearchParams.isBackwards = searchBackwards; + m_currentSearchParams.shouldPlaceCursorAtEndOfMatch = placeCursorAtEndOfMatch; + + // The "count" for the current search is not shared between Visual & Normal mode, so we need to pick + // the right one to handle the counted search. + int c = viInputModeManager()->getCurrentViModeHandler()->getCount(); + KTextEditor::Range match = viInputModeManager()->searcher()->findPattern(m_currentSearchParams, m_startingCursorPos, c, false /* Don't add incremental searches to search history */); + + if (match.isValid()) { + // The returned range ends one past the last character of the match, so adjust. + KTextEditor::Cursor realMatchEnd = KTextEditor::Cursor(match.end().line(), match.end().column() - 1); + if (realMatchEnd.column() == -1) { + realMatchEnd = KTextEditor::Cursor(realMatchEnd.line() - 1, view()->doc()->lineLength(realMatchEnd.line() - 1)); + } + moveCursorTo(placeCursorAtEndOfMatch ? realMatchEnd : match.start()); + setBarBackground(SearchMode::MatchFound); + } else { + moveCursorTo(m_startingCursorPos); + if (!m_edit->text().isEmpty()) { + setBarBackground(SearchMode::NoMatchFound); + } else { + setBarBackground(SearchMode::Normal); + } + } + + updateMatchHighlight(match); +} + +void SearchMode::deactivate(bool wasAborted) +{ + // "Deactivate" can be called multiple times between init()'s, so only reset the cursor once! + if (m_startingCursorPos.isValid()) { + if (wasAborted) { + moveCursorTo(m_startingCursorPos); + } + } + m_startingCursorPos = KTextEditor::Cursor::invalid(); + setBarBackground(SearchMode::Normal); + // Send a synthetic keypress through the system that signals whether the search was aborted or + // not. If not, the keypress will "complete" the search motion, thus triggering it. + // We send to KateViewInternal as it updates the status bar and removes the "?". + const Qt::Key syntheticSearchCompletedKey = (wasAborted ? static_cast(0) : Qt::Key_Enter); + QKeyEvent syntheticSearchCompletedKeyPress(QEvent::KeyPress, syntheticSearchCompletedKey, Qt::NoModifier); + m_isSendingSyntheticSearchCompletedKeypress = true; + QApplication::sendEvent(view()->focusProxy(), &syntheticSearchCompletedKeyPress); + m_isSendingSyntheticSearchCompletedKeypress = false; + if (!wasAborted) { + // Search was actually executed, so store it as the last search. + viInputModeManager()->searcher()->setLastSearchParams(m_currentSearchParams); + } + // Append the raw text of the search to the search history (i.e. without conversion + // from Vim-style regex; without case-sensitivity markers stripped; etc. + // Vim does this even if the search was aborted, so we follow suit. + viInputModeManager()->globalState()->searchHistory()->append(m_edit->text()); +} + +CompletionStartParams SearchMode::completionInvoked ( Completer::CompletionInvocation invocationType ) +{ + Q_UNUSED(invocationType); + return activateSearchHistoryCompletion(); +} + +void SearchMode::completionChosen() +{ + // Choose completion with Enter/ Return -> close bar (the search will have already taken effect at this point), marking as not aborted . + close(false); +} + +CompletionStartParams SearchMode::activateSearchHistoryCompletion() +{ + return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), 0); +} + +void SearchMode::setBarBackground ( SearchMode::BarBackgroundStatus status ) +{ + QPalette barBackground(m_edit->palette()); + switch (status) { + case MatchFound: { + KColorScheme::adjustBackground(barBackground, KColorScheme::PositiveBackground); + break; + } + case NoMatchFound: { + KColorScheme::adjustBackground(barBackground, KColorScheme::NegativeBackground); + break; + } + case Normal: { + barBackground = QPalette(); + break; + } + } + m_edit->setPalette(barBackground); +} diff --git a/src/vimode/history.h b/src/vimode/history.h --- a/src/vimode/history.h +++ b/src/vimode/history.h @@ -20,12 +20,13 @@ #ifndef KATEVI_HISTORY_H #define KATEVI_HISTORY_H +#include #include namespace KateVi { -class History +class KTEXTEDITOR_EXPORT History { public: diff --git a/src/vimode/inputmodemanager.cpp b/src/vimode/inputmodemanager.cpp --- a/src/vimode/inputmodemanager.cpp +++ b/src/vimode/inputmodemanager.cpp @@ -41,7 +41,7 @@ #include #include #include -#include +#include #include "kateviinputmode.h" #include "marks.h" #include "jumps.h" diff --git a/src/vimode/mappings.cpp b/src/vimode/mappings.cpp --- a/src/vimode/mappings.cpp +++ b/src/vimode/mappings.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include using namespace KateVi; diff --git a/src/vimode/modes/normalvimode.cpp b/src/vimode/modes/normalvimode.cpp --- a/src/vimode/modes/normalvimode.cpp +++ b/src/vimode/modes/normalvimode.cpp @@ -29,7 +29,7 @@ #include "katepartdebug.h" #include "kateundomanager.h" #include -#include +#include #include "kateviewhelpers.h" #include "kateviewinternal.h" #include diff --git a/src/vimode/searcher.h b/src/vimode/searcher.h --- a/src/vimode/searcher.h +++ b/src/vimode/searcher.h @@ -50,23 +50,28 @@ Range motionFindPrev(int count = 1); Range findWordForMotion(const QString &pattern, bool backwards, const KTextEditor::Cursor &startFrom, int count); - /** Extended searcher for ECB **/ - KTextEditor::Range findPattern(const QString &pattern, bool backwards, bool caseSensitive, bool placedCursorAtEndOfmatch, const KTextEditor::Cursor &startFrom, int count); + /** Extended searcher for Emulated Command Bar. **/ + struct SearchParams + { + QString pattern; + bool isBackwards = false; + bool isCaseSensitive = false; + bool shouldPlaceCursorAtEndOfMatch = false; + }; + KTextEditor::Range findPattern(const SearchParams& searchParams, const KTextEditor::Cursor &startFrom, int count, bool addToSearchHistory = true); const QString getLastSearchPattern() const; + void setLastSearchParams(const SearchParams& searchParams); private: - Range findPatternForMotion(const QString &pattern, bool backwards, bool caseSensitive, const KTextEditor::Cursor &startFrom, int count = 1) const; - KTextEditor::Range findPatternWorker(const QString &pattern, bool backwards, bool caseSensitive, const KTextEditor::Cursor &startFrom, int count) const; + Range findPatternForMotion(const SearchParams& searchParams, const KTextEditor::Cursor &startFrom, int count = 1) const; + KTextEditor::Range findPatternWorker(const SearchParams& searchParams, const KTextEditor::Cursor &startFrom, int count) const; private: InputModeManager *m_viInputModeManager; KTextEditor::ViewPrivate *m_view; - QString m_lastSearchPattern; - bool m_lastSearchBackwards; - bool m_lastSearchCaseSensitive; - bool m_lastSearchPlacedCursorAtEndOfMatch; + SearchParams m_lastSearchConfig; }; } diff --git a/src/vimode/searcher.cpp b/src/vimode/searcher.cpp --- a/src/vimode/searcher.cpp +++ b/src/vimode/searcher.cpp @@ -32,9 +32,6 @@ Searcher::Searcher(InputModeManager *manager) : m_viInputModeManager(manager) , m_view(manager->view()) - , m_lastSearchBackwards(false) - , m_lastSearchCaseSensitive(false) - , m_lastSearchPlacedCursorAtEndOfMatch(false) { } @@ -44,7 +41,12 @@ const QString Searcher::getLastSearchPattern() const { - return m_lastSearchPattern; + return m_lastSearchConfig.pattern; +} + +void Searcher::setLastSearchParams(const SearchParams& searchParams) +{ + m_lastSearchConfig = searchParams; } void Searcher::findNext() @@ -66,16 +68,14 @@ Range Searcher::motionFindNext(int count) { Range match = findPatternForMotion( - m_lastSearchPattern, - m_lastSearchBackwards, - m_lastSearchCaseSensitive, + m_lastSearchConfig, m_view->cursorPosition(), count); if (!match.valid) { return match; } - if (!m_lastSearchPlacedCursorAtEndOfMatch) { + if (!m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch) { return Range(match.startLine, match.startColumn, ExclusiveMotion); } return Range(match.endLine, match.endColumn - 1, ExclusiveMotion); @@ -83,71 +83,72 @@ Range Searcher::motionFindPrev(int count) { + SearchParams lastSearchReversed = m_lastSearchConfig; + lastSearchReversed.isBackwards = !lastSearchReversed.isBackwards; Range match = findPatternForMotion( - m_lastSearchPattern, - !m_lastSearchBackwards, - m_lastSearchCaseSensitive, + lastSearchReversed, m_view->cursorPosition(), count); if (!match.valid) { return match; } - if (!m_lastSearchPlacedCursorAtEndOfMatch) { + if (!m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch) { return Range(match.startLine, match.startColumn, ExclusiveMotion); } return Range(match.endLine, match.endColumn - 1, ExclusiveMotion); } -Range Searcher::findPatternForMotion(const QString &pattern, bool backwards, bool caseSensitive, const KTextEditor::Cursor &startFrom, int count) const +Range Searcher::findPatternForMotion(const SearchParams& searchParams, const KTextEditor::Cursor &startFrom, int count) const { - if (pattern.isEmpty()) { + if (searchParams.pattern.isEmpty()) { return Range::invalid(); } - KTextEditor::Range match = findPatternWorker(pattern, backwards, caseSensitive, startFrom, count); + KTextEditor::Range match = findPatternWorker(searchParams, startFrom, count); return Range(match.start(), match.end(), ExclusiveMotion); } Range Searcher::findWordForMotion(const QString &word, bool backwards, const KTextEditor::Cursor &startFrom, int count) { - m_lastSearchBackwards = backwards; - m_lastSearchCaseSensitive = false; - m_lastSearchPlacedCursorAtEndOfMatch = false; + m_lastSearchConfig.isBackwards = backwards; + m_lastSearchConfig.isCaseSensitive = false; + m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch = false; m_viInputModeManager->globalState()->searchHistory()->append(QStringLiteral("\\<%1\\>").arg(word)); QString pattern = QStringLiteral("\\b%1\\b").arg(word); - m_lastSearchPattern = pattern; + m_lastSearchConfig.pattern = pattern; - return findPatternForMotion(pattern, backwards, false, startFrom, count); + return findPatternForMotion(m_lastSearchConfig, startFrom, count); } -KTextEditor::Range Searcher::findPattern(const QString &pattern, bool backwards, bool caseSensitive, bool placedCursorAtEndOfmatch, const KTextEditor::Cursor &startFrom, int count) +KTextEditor::Range Searcher::findPattern(const SearchParams& searchParams, const KTextEditor::Cursor &startFrom, int count, bool addToSearchHistory) { - m_lastSearchPattern = pattern; - m_lastSearchBackwards = backwards; - m_lastSearchCaseSensitive = caseSensitive; - m_lastSearchPlacedCursorAtEndOfMatch = placedCursorAtEndOfmatch; + if (addToSearchHistory) { - m_viInputModeManager->globalState()->searchHistory()->append(pattern); + m_viInputModeManager->globalState()->searchHistory()->append(searchParams.pattern); + m_lastSearchConfig = searchParams; + } - return findPatternWorker(pattern, backwards, caseSensitive, startFrom, count); + return findPatternWorker(searchParams, startFrom, count); } -KTextEditor::Range Searcher::findPatternWorker(const QString &pattern, bool backwards, bool caseSensitive, const KTextEditor::Cursor &startFrom, int count) const +KTextEditor::Range Searcher::findPatternWorker(const SearchParams& searchParams, const KTextEditor::Cursor &startFrom, int count) const { KTextEditor::Cursor searchBegin = startFrom; KTextEditor::SearchOptions flags = KTextEditor::Regex; - if (backwards) { + const QString& pattern = searchParams.pattern; + + if (searchParams.isBackwards) { flags |= KTextEditor::Backwards; } - if (!caseSensitive) { + if (!searchParams.isCaseSensitive) { flags |= KTextEditor::CaseInsensitive; } KTextEditor::Range finalMatch; for (int i = 0; i < count; i++) { - if (!backwards) { + if (!searchParams.isBackwards) { const KTextEditor::Range matchRange = m_view->doc()->searchText(KTextEditor::Range(KTextEditor::Cursor(searchBegin.line(), searchBegin.column() + 1), m_view->doc()->documentEnd()), pattern, flags).first(); if (matchRange.isValid()) {