File indexing completed on 2024-05-05 03:58:35

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