Warning, file /frameworks/ktexteditor/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2013-2016 Simon St James <kdedevel@etotheipiplusone.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include <vimode/emulatedcommandbar/emulatedcommandbar.h>
0008 
0009 #include "../commandrangeexpressionparser.h"
0010 #include "../globalstate.h"
0011 #include "commandmode.h"
0012 #include "interactivesedreplacemode.h"
0013 #include "katedocument.h"
0014 #include "kateglobal.h"
0015 #include "kateview.h"
0016 #include "matchhighlighter.h"
0017 #include "searchmode.h"
0018 #include <inputmode/kateviinputmode.h>
0019 #include <vimode/inputmodemanager.h>
0020 #include <vimode/keyparser.h>
0021 #include <vimode/modes/normalvimode.h>
0022 
0023 #include "../history.h"
0024 
0025 #include "../registers.h"
0026 
0027 #include <QApplication>
0028 #include <QLabel>
0029 #include <QLineEdit>
0030 #include <QVBoxLayout>
0031 
0032 #include <algorithm>
0033 
0034 using namespace KateVi;
0035 
0036 namespace
0037 {
0038 /**
0039  * @return \a originalRegex but escaped in such a way that a Qt regex search for
0040  * the resulting string will match the string \a originalRegex.
0041  */
0042 QString escapedForSearchingAsLiteral(const QString &originalQtRegex)
0043 {
0044     QString escapedForSearchingAsLiteral = originalQtRegex;
0045     escapedForSearchingAsLiteral.replace(QLatin1Char('\\'), QLatin1String("\\\\"));
0046     escapedForSearchingAsLiteral.replace(QLatin1Char('$'), QLatin1String("\\$"));
0047     escapedForSearchingAsLiteral.replace(QLatin1Char('^'), QLatin1String("\\^"));
0048     escapedForSearchingAsLiteral.replace(QLatin1Char('.'), QLatin1String("\\."));
0049     escapedForSearchingAsLiteral.replace(QLatin1Char('*'), QLatin1String("\\*"));
0050     escapedForSearchingAsLiteral.replace(QLatin1Char('/'), QLatin1String("\\/"));
0051     escapedForSearchingAsLiteral.replace(QLatin1Char('['), QLatin1String("\\["));
0052     escapedForSearchingAsLiteral.replace(QLatin1Char(']'), QLatin1String("\\]"));
0053     escapedForSearchingAsLiteral.replace(QLatin1Char('\n'), QLatin1String("\\n"));
0054     return escapedForSearchingAsLiteral;
0055 }
0056 }
0057 
0058 EmulatedCommandBar::EmulatedCommandBar(KateViInputMode *viInputMode, InputModeManager *viInputModeManager, QWidget *parent)
0059     : KateViewBarWidget(false, parent)
0060     , m_viInputMode(viInputMode)
0061     , m_viInputModeManager(viInputModeManager)
0062     , m_view(viInputModeManager->view())
0063 {
0064     QHBoxLayout *layout = new QHBoxLayout(centralWidget());
0065     layout->setContentsMargins(0, 0, 0, 0);
0066 
0067     createAndAddBarTypeIndicator(layout);
0068     createAndAddEditWidget(layout);
0069     createAndAddExitStatusMessageDisplay(layout);
0070     createAndInitExitStatusMessageDisplayTimer();
0071     createAndAddWaitingForRegisterIndicator(layout);
0072 
0073     m_matchHighligher.reset(new MatchHighlighter(m_view));
0074 
0075     m_completer.reset(new Completer(this, m_view, m_edit));
0076 
0077     m_interactiveSedReplaceMode.reset(new InteractiveSedReplaceMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view));
0078     layout->addWidget(m_interactiveSedReplaceMode->label());
0079     m_searchMode.reset(new SearchMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view, m_edit));
0080     m_commandMode.reset(
0081         new CommandMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view, m_edit, m_interactiveSedReplaceMode.data(), m_completer.data()));
0082 
0083     m_edit->installEventFilter(this);
0084     connect(m_edit, &QLineEdit::textChanged, this, &EmulatedCommandBar::editTextChanged);
0085 }
0086 
0087 EmulatedCommandBar::~EmulatedCommandBar() = default;
0088 
0089 void EmulatedCommandBar::init(EmulatedCommandBar::Mode mode, const QString &initialText)
0090 {
0091     m_mode = mode;
0092     m_isActive = true;
0093     m_wasAborted = true;
0094 
0095     showBarTypeIndicator(mode);
0096 
0097     if (mode == KateVi::EmulatedCommandBar::SearchBackward || mode == SearchForward) {
0098         switchToMode(m_searchMode.data());
0099         m_searchMode->init(mode == SearchBackward ? SearchMode::SearchDirection::Backward : SearchMode::SearchDirection::Forward);
0100     } else {
0101         switchToMode(m_commandMode.data());
0102     }
0103 
0104     m_edit->setFocus();
0105     m_edit->setText(initialText);
0106     m_edit->show();
0107 
0108     m_exitStatusMessageDisplay->hide();
0109     m_exitStatusMessageDisplayHideTimer->stop();
0110 
0111     // A change in focus will have occurred: make sure we process it now, instead of having it
0112     // occur later and stop() m_commandResponseMessageDisplayHide.
0113     // This is generally only a problem when feeding a sequence of keys without human intervention,
0114     // as when we execute a mapping, macro, or test case.
0115     QApplication::processEvents();
0116 }
0117 
0118 bool EmulatedCommandBar::isActive() const
0119 {
0120     return m_isActive;
0121 }
0122 
0123 void EmulatedCommandBar::setCommandResponseMessageTimeout(long int commandResponseMessageTimeOutMS)
0124 {
0125     m_exitStatusMessageHideTimeOutMS = commandResponseMessageTimeOutMS;
0126 }
0127 
0128 void EmulatedCommandBar::closed()
0129 {
0130     m_matchHighligher->updateMatchHighlight(KTextEditor::Range::invalid());
0131     m_completer->deactivateCompletion();
0132     m_isActive = false;
0133 
0134     if (m_currentMode) {
0135         m_currentMode->deactivate(m_wasAborted);
0136         m_currentMode = nullptr;
0137     }
0138 }
0139 
0140 void EmulatedCommandBar::switchToMode(ActiveMode *newMode)
0141 {
0142     if (newMode == m_currentMode) {
0143         return;
0144     }
0145     if (m_currentMode) {
0146         m_currentMode->deactivate(false);
0147     }
0148     m_currentMode = newMode;
0149     m_completer->setCurrentMode(newMode);
0150 }
0151 
0152 bool EmulatedCommandBar::barHandledKeypress(const QKeyEvent *keyEvent)
0153 {
0154     if ((keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_H) || keyEvent->key() == Qt::Key_Backspace) {
0155         if (m_edit->text().isEmpty()) {
0156             Q_EMIT hideMe();
0157         }
0158         m_edit->backspace();
0159         return true;
0160     }
0161     if (keyEvent->modifiers() != CONTROL_MODIFIER) {
0162         return false;
0163     }
0164     if (keyEvent->key() == Qt::Key_B) {
0165         m_edit->setCursorPosition(0);
0166         return true;
0167     } else if (keyEvent->key() == Qt::Key_E) {
0168         m_edit->setCursorPosition(m_edit->text().length());
0169         return true;
0170     } else if (keyEvent->key() == Qt::Key_W) {
0171         deleteSpacesToLeftOfCursor();
0172         if (!deleteNonWordCharsToLeftOfCursor()) {
0173             deleteWordCharsToLeftOfCursor();
0174         }
0175         return true;
0176     } else if (keyEvent->key() == Qt::Key_R || keyEvent->key() == Qt::Key_G) {
0177         m_waitingForRegister = true;
0178         m_waitingForRegisterIndicator->setVisible(true);
0179         if (keyEvent->key() == Qt::Key_G) {
0180             m_insertedTextShouldBeEscapedForSearchingAsLiteral = true;
0181         }
0182         return true;
0183     }
0184     return false;
0185 }
0186 
0187 void EmulatedCommandBar::insertRegisterContents(const QKeyEvent *keyEvent)
0188 {
0189     if (keyEvent->key() != Qt::Key_Shift && keyEvent->key() != Qt::Key_Control) {
0190         const QChar key = KeyParser::self()->KeyEventToQChar(*keyEvent).toLower();
0191 
0192         const int oldCursorPosition = m_edit->cursorPosition();
0193         QString textToInsert;
0194         if (keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_W) {
0195             textToInsert = m_view->doc()->wordAt(m_view->cursorPosition());
0196         } else {
0197             textToInsert = m_viInputModeManager->globalState()->registers()->getContent(key);
0198         }
0199         if (m_insertedTextShouldBeEscapedForSearchingAsLiteral) {
0200             textToInsert = escapedForSearchingAsLiteral(textToInsert);
0201             m_insertedTextShouldBeEscapedForSearchingAsLiteral = false;
0202         }
0203         m_edit->setText(m_edit->text().insert(m_edit->cursorPosition(), textToInsert));
0204         m_edit->setCursorPosition(oldCursorPosition + textToInsert.length());
0205         m_waitingForRegister = false;
0206         m_waitingForRegisterIndicator->setVisible(false);
0207     }
0208 }
0209 
0210 bool EmulatedCommandBar::eventFilter(QObject *object, QEvent *event)
0211 {
0212     // The "object" will be either m_edit or m_completer's popup.
0213     if (m_suspendEditEventFiltering) {
0214         return false;
0215     }
0216     Q_UNUSED(object);
0217     if (event->type() == QEvent::KeyPress) {
0218         // Re-route this keypress through Vim's central keypress handling area, so that we can use the keypress in e.g.
0219         // mappings and macros.
0220         return m_viInputMode->keyPress(static_cast<QKeyEvent *>(event));
0221     }
0222     return false;
0223 }
0224 
0225 void EmulatedCommandBar::deleteSpacesToLeftOfCursor()
0226 {
0227     while (m_edit->cursorPosition() != 0 && m_edit->text().at(m_edit->cursorPosition() - 1) == QLatin1Char(' ')) {
0228         m_edit->backspace();
0229     }
0230 }
0231 
0232 void EmulatedCommandBar::deleteWordCharsToLeftOfCursor()
0233 {
0234     while (m_edit->cursorPosition() != 0) {
0235         const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1);
0236         if (!charToTheLeftOfCursor.isLetterOrNumber() && charToTheLeftOfCursor != QLatin1Char('_')) {
0237             break;
0238         }
0239 
0240         m_edit->backspace();
0241     }
0242 }
0243 
0244 bool EmulatedCommandBar::deleteNonWordCharsToLeftOfCursor()
0245 {
0246     bool deletionsMade = false;
0247     while (m_edit->cursorPosition() != 0) {
0248         const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1);
0249         if (charToTheLeftOfCursor.isLetterOrNumber() || charToTheLeftOfCursor == QLatin1Char('_') || charToTheLeftOfCursor == QLatin1Char(' ')) {
0250             break;
0251         }
0252 
0253         m_edit->backspace();
0254         deletionsMade = true;
0255     }
0256     return deletionsMade;
0257 }
0258 
0259 bool EmulatedCommandBar::handleKeyPress(const QKeyEvent *keyEvent)
0260 {
0261     if (m_waitingForRegister) {
0262         insertRegisterContents(keyEvent);
0263         return true;
0264     }
0265     const bool completerHandled = m_completer->completerHandledKeypress(keyEvent);
0266     if (completerHandled) {
0267         return true;
0268     }
0269 
0270     if (keyEvent->modifiers() == CONTROL_MODIFIER && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) {
0271         Q_EMIT hideMe();
0272         return true;
0273     }
0274 
0275     // Is this a built-in Emulated Command Bar keypress e.g. insert from register, ctrl-h, etc?
0276     const bool barHandled = barHandledKeypress(keyEvent);
0277     if (barHandled) {
0278         return true;
0279     }
0280 
0281     // Can the current mode handle it?
0282     const bool currentModeHandled = m_currentMode->handleKeyPress(keyEvent);
0283     if (currentModeHandled) {
0284         return true;
0285     }
0286 
0287     // Couldn't handle this key event.
0288     // Send the keypress back to the QLineEdit.  Ideally, instead of doing this, we would simply return "false"
0289     // and let Qt re-dispatch the event itself; however, there is a corner case in that if the selection
0290     // changes (as a result of e.g. incremental searches during Visual Mode), and the keypress that causes it
0291     // is not dispatched from within KateViInputModeHandler::handleKeypress(...)
0292     // (so KateViInputModeManager::isHandlingKeypress() returns false), we lose information about whether we are
0293     // in Visual Mode, Visual Line Mode, etc.  See VisualViMode::updateSelection( ).
0294     if (m_edit->isVisible()) {
0295         if (m_suspendEditEventFiltering) {
0296             return false;
0297         }
0298         m_suspendEditEventFiltering = true;
0299         QKeyEvent keyEventCopy(keyEvent->type(), keyEvent->key(), keyEvent->modifiers(), keyEvent->text(), keyEvent->isAutoRepeat(), keyEvent->count());
0300         qApp->notify(m_edit, &keyEventCopy);
0301         m_suspendEditEventFiltering = false;
0302     }
0303     return true;
0304 }
0305 
0306 bool EmulatedCommandBar::isSendingSyntheticSearchCompletedKeypress()
0307 {
0308     return m_searchMode->isSendingSyntheticSearchCompletedKeypress();
0309 }
0310 
0311 void EmulatedCommandBar::startInteractiveSearchAndReplace(QSharedPointer<SedReplace::InteractiveSedReplacer> interactiveSedReplace)
0312 {
0313     Q_ASSERT_X(interactiveSedReplace->currentMatch().isValid(),
0314                "startInteractiveSearchAndReplace",
0315                "KateCommands shouldn't initiate an interactive sed replace with no initial match");
0316     switchToMode(m_interactiveSedReplaceMode.data());
0317     m_interactiveSedReplaceMode->activate(interactiveSedReplace);
0318 }
0319 
0320 void EmulatedCommandBar::showBarTypeIndicator(EmulatedCommandBar::Mode mode)
0321 {
0322     QChar barTypeIndicator = QChar::Null;
0323     switch (mode) {
0324     case SearchForward:
0325         barTypeIndicator = QLatin1Char('/');
0326         break;
0327     case SearchBackward:
0328         barTypeIndicator = QLatin1Char('?');
0329         break;
0330     case Command:
0331         barTypeIndicator = QLatin1Char(':');
0332         break;
0333     default:
0334         Q_ASSERT(false && "Unknown mode!");
0335     }
0336     m_barTypeIndicator->setText(barTypeIndicator);
0337     m_barTypeIndicator->show();
0338 }
0339 
0340 QString EmulatedCommandBar::executeCommand(const QString &commandToExecute)
0341 {
0342     return m_commandMode->executeCommand(commandToExecute);
0343 }
0344 
0345 void EmulatedCommandBar::closeWithStatusMessage(const QString &exitStatusMessage)
0346 {
0347     // Display the message for a while.  Become inactive, so we don't steal keys in the meantime.
0348     m_isActive = false;
0349 
0350     m_exitStatusMessageDisplay->show();
0351     m_exitStatusMessageDisplay->setText(exitStatusMessage);
0352     hideAllWidgetsExcept(m_exitStatusMessageDisplay);
0353 
0354     m_exitStatusMessageDisplayHideTimer->start(m_exitStatusMessageHideTimeOutMS);
0355 }
0356 
0357 void EmulatedCommandBar::editTextChanged(const QString &newText)
0358 {
0359     Q_ASSERT(!m_interactiveSedReplaceMode->isActive());
0360     m_currentMode->editTextChanged(newText);
0361     m_completer->editTextChanged(newText);
0362 }
0363 
0364 void EmulatedCommandBar::startHideExitStatusMessageTimer()
0365 {
0366     if (m_exitStatusMessageDisplay->isVisible() && !m_exitStatusMessageDisplayHideTimer->isActive()) {
0367         m_exitStatusMessageDisplayHideTimer->start(m_exitStatusMessageHideTimeOutMS);
0368     }
0369 }
0370 
0371 void EmulatedCommandBar::setViInputModeManager(InputModeManager *viInputModeManager)
0372 {
0373     m_viInputModeManager = viInputModeManager;
0374     m_searchMode->setViInputModeManager(viInputModeManager);
0375     m_commandMode->setViInputModeManager(viInputModeManager);
0376     m_interactiveSedReplaceMode->setViInputModeManager(viInputModeManager);
0377 }
0378 
0379 void EmulatedCommandBar::hideAllWidgetsExcept(QWidget *widgetToKeepVisible)
0380 {
0381     const QList<QWidget *> widgets = centralWidget()->findChildren<QWidget *>();
0382     for (QWidget *widget : widgets) {
0383         if (widget != widgetToKeepVisible) {
0384             widget->hide();
0385         }
0386     }
0387 }
0388 
0389 void EmulatedCommandBar::createAndAddBarTypeIndicator(QLayout *layout)
0390 {
0391     m_barTypeIndicator = new QLabel(this);
0392     m_barTypeIndicator->setObjectName(QStringLiteral("bartypeindicator"));
0393     layout->addWidget(m_barTypeIndicator);
0394 }
0395 
0396 void EmulatedCommandBar::createAndAddEditWidget(QLayout *layout)
0397 {
0398     m_edit = new QLineEdit(this);
0399     m_edit->setObjectName(QStringLiteral("commandtext"));
0400     layout->addWidget(m_edit);
0401 }
0402 
0403 void EmulatedCommandBar::createAndAddExitStatusMessageDisplay(QLayout *layout)
0404 {
0405     m_exitStatusMessageDisplay = new QLabel(this);
0406     m_exitStatusMessageDisplay->setObjectName(QStringLiteral("commandresponsemessage"));
0407     m_exitStatusMessageDisplay->setAlignment(Qt::AlignLeft);
0408     layout->addWidget(m_exitStatusMessageDisplay);
0409 }
0410 
0411 void EmulatedCommandBar::createAndInitExitStatusMessageDisplayTimer()
0412 {
0413     m_exitStatusMessageDisplayHideTimer = new QTimer(this);
0414     m_exitStatusMessageDisplayHideTimer->setSingleShot(true);
0415     connect(m_exitStatusMessageDisplayHideTimer, &QTimer::timeout, this, &EmulatedCommandBar::hideMe);
0416     // Make sure the timer is stopped when the user switches views. If not, focus will be given to the
0417     // wrong view when KateViewBar::hideCurrentBarWidget() is called as a result of m_commandResponseMessageDisplayHide
0418     // timing out.
0419     connect(m_view, &KTextEditor::ViewPrivate::focusOut, m_exitStatusMessageDisplayHideTimer, &QTimer::stop);
0420     // We can restart the timer once the view has focus again, though.
0421     connect(m_view, &KTextEditor::ViewPrivate::focusIn, this, &EmulatedCommandBar::startHideExitStatusMessageTimer);
0422 }
0423 
0424 void EmulatedCommandBar::createAndAddWaitingForRegisterIndicator(QLayout *layout)
0425 {
0426     m_waitingForRegisterIndicator = new QLabel(this);
0427     m_waitingForRegisterIndicator->setObjectName(QStringLiteral("waitingforregisterindicator"));
0428     m_waitingForRegisterIndicator->setVisible(false);
0429     m_waitingForRegisterIndicator->setText(QStringLiteral("\""));
0430     layout->addWidget(m_waitingForRegisterIndicator);
0431 }
0432 
0433 #include "moc_emulatedcommandbar.cpp"