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 }