| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2013-2016 Simon St James <kdedevel@etotheipiplusone.com> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include <vimode/emulatedcommandbar/emulatedcommandbar.h> |
| 8 | |
| 9 | #include "../commandrangeexpressionparser.h" |
| 10 | #include "../globalstate.h" |
| 11 | #include "commandmode.h" |
| 12 | #include "interactivesedreplacemode.h" |
| 13 | #include "katedocument.h" |
| 14 | #include "kateglobal.h" |
| 15 | #include "kateview.h" |
| 16 | #include "matchhighlighter.h" |
| 17 | #include "searchmode.h" |
| 18 | #include <inputmode/kateviinputmode.h> |
| 19 | #include <vimode/inputmodemanager.h> |
| 20 | #include <vimode/keyparser.h> |
| 21 | #include <vimode/modes/normalvimode.h> |
| 22 | |
| 23 | #include "../registers.h" |
| 24 | |
| 25 | #include <QApplication> |
| 26 | #include <QLabel> |
| 27 | #include <QLineEdit> |
| 28 | #include <QVBoxLayout> |
| 29 | |
| 30 | using namespace KateVi; |
| 31 | |
| 32 | namespace |
| 33 | { |
| 34 | /** |
| 35 | * @return \a originalRegex but escaped in such a way that a Qt regex search for |
| 36 | * the resulting string will match the string \a originalRegex. |
| 37 | */ |
| 38 | QString escapedForSearchingAsLiteral(const QString &originalQtRegex) |
| 39 | { |
| 40 | QString escapedForSearchingAsLiteral = originalQtRegex; |
| 41 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('\\'), after: QLatin1String("\\\\" )); |
| 42 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('$'), after: QLatin1String("\\$" )); |
| 43 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('^'), after: QLatin1String("\\^" )); |
| 44 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('.'), after: QLatin1String("\\." )); |
| 45 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('*'), after: QLatin1String("\\*" )); |
| 46 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('/'), after: QLatin1String("\\/" )); |
| 47 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('['), after: QLatin1String("\\[" )); |
| 48 | escapedForSearchingAsLiteral.replace(c: QLatin1Char(']'), after: QLatin1String("\\]" )); |
| 49 | escapedForSearchingAsLiteral.replace(c: QLatin1Char('\n'), after: QLatin1String("\\n" )); |
| 50 | return escapedForSearchingAsLiteral; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | EmulatedCommandBar::EmulatedCommandBar(KateViInputMode *viInputMode, InputModeManager *viInputModeManager, QWidget *parent) |
| 55 | : KateViewBarWidget(false, parent) |
| 56 | , m_viInputMode(viInputMode) |
| 57 | , m_viInputModeManager(viInputModeManager) |
| 58 | , m_view(viInputModeManager->view()) |
| 59 | { |
| 60 | QHBoxLayout *layout = new QHBoxLayout(centralWidget()); |
| 61 | layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0); |
| 62 | |
| 63 | createAndAddBarTypeIndicator(layout); |
| 64 | createAndAddEditWidget(layout); |
| 65 | createAndAddExitStatusMessageDisplay(layout); |
| 66 | createAndInitExitStatusMessageDisplayTimer(); |
| 67 | createAndAddWaitingForRegisterIndicator(layout); |
| 68 | |
| 69 | m_matchHighligher.reset(p: new MatchHighlighter(m_view)); |
| 70 | |
| 71 | m_completer.reset(p: new Completer(this, m_view, m_edit)); |
| 72 | |
| 73 | m_interactiveSedReplaceMode.reset(p: new InteractiveSedReplaceMode(this, m_matchHighligher.get(), m_viInputModeManager, m_view)); |
| 74 | layout->addWidget(m_interactiveSedReplaceMode->label()); |
| 75 | m_searchMode.reset(p: new SearchMode(this, m_matchHighligher.get(), m_viInputModeManager, m_view, m_edit)); |
| 76 | m_commandMode.reset( |
| 77 | p: new CommandMode(this, m_matchHighligher.get(), m_viInputModeManager, m_view, m_edit, m_interactiveSedReplaceMode.get(), m_completer.get())); |
| 78 | |
| 79 | m_edit->installEventFilter(filterObj: this); |
| 80 | connect(sender: m_edit, signal: &QLineEdit::textChanged, context: this, slot: &EmulatedCommandBar::editTextChanged); |
| 81 | } |
| 82 | |
| 83 | EmulatedCommandBar::~EmulatedCommandBar() = default; |
| 84 | |
| 85 | void EmulatedCommandBar::init(EmulatedCommandBar::Mode mode, const QString &initialText) |
| 86 | { |
| 87 | m_mode = mode; |
| 88 | m_isActive = true; |
| 89 | m_wasAborted = true; |
| 90 | |
| 91 | showBarTypeIndicator(mode); |
| 92 | |
| 93 | if (mode == KateVi::EmulatedCommandBar::SearchBackward || mode == SearchForward) { |
| 94 | switchToMode(newMode: m_searchMode.get()); |
| 95 | m_searchMode->init(mode == SearchBackward ? SearchMode::SearchDirection::Backward : SearchMode::SearchDirection::Forward); |
| 96 | } else { |
| 97 | switchToMode(newMode: m_commandMode.get()); |
| 98 | } |
| 99 | |
| 100 | m_edit->setFocus(); |
| 101 | m_edit->setText(initialText); |
| 102 | m_edit->show(); |
| 103 | |
| 104 | m_exitStatusMessageDisplay->hide(); |
| 105 | m_exitStatusMessageDisplayHideTimer->stop(); |
| 106 | |
| 107 | // A change in focus will have occurred: make sure we process it now, instead of having it |
| 108 | // occur later and stop() m_commandResponseMessageDisplayHide. |
| 109 | // This is generally only a problem when feeding a sequence of keys without human intervention, |
| 110 | // as when we execute a mapping, macro, or test case. |
| 111 | QApplication::processEvents(); |
| 112 | } |
| 113 | |
| 114 | bool EmulatedCommandBar::isActive() const |
| 115 | { |
| 116 | return m_isActive; |
| 117 | } |
| 118 | |
| 119 | void EmulatedCommandBar::setCommandResponseMessageTimeout(long int commandResponseMessageTimeOutMS) |
| 120 | { |
| 121 | m_exitStatusMessageHideTimeOutMS = commandResponseMessageTimeOutMS; |
| 122 | } |
| 123 | |
| 124 | void EmulatedCommandBar::closed() |
| 125 | { |
| 126 | m_matchHighligher->updateMatchHighlight(matchRange: KTextEditor::Range::invalid()); |
| 127 | m_completer->deactivateCompletion(); |
| 128 | m_isActive = false; |
| 129 | |
| 130 | if (m_currentMode) { |
| 131 | m_currentMode->deactivate(wasAborted: m_wasAborted); |
| 132 | m_currentMode = nullptr; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | void EmulatedCommandBar::switchToMode(ActiveMode *newMode) |
| 137 | { |
| 138 | if (newMode == m_currentMode) { |
| 139 | return; |
| 140 | } |
| 141 | if (m_currentMode) { |
| 142 | m_currentMode->deactivate(wasAborted: false); |
| 143 | } |
| 144 | m_currentMode = newMode; |
| 145 | m_completer->setCurrentMode(newMode); |
| 146 | } |
| 147 | |
| 148 | bool EmulatedCommandBar::barHandledKeypress(const QKeyEvent *keyEvent) |
| 149 | { |
| 150 | if ((keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_H) || keyEvent->key() == Qt::Key_Backspace) { |
| 151 | if (m_edit->text().isEmpty()) { |
| 152 | Q_EMIT hideMe(); |
| 153 | } |
| 154 | m_edit->backspace(); |
| 155 | return true; |
| 156 | } |
| 157 | if (keyEvent->modifiers() != CONTROL_MODIFIER) { |
| 158 | return false; |
| 159 | } |
| 160 | if (keyEvent->key() == Qt::Key_B) { |
| 161 | m_edit->setCursorPosition(0); |
| 162 | return true; |
| 163 | } else if (keyEvent->key() == Qt::Key_E) { |
| 164 | m_edit->setCursorPosition(m_edit->text().length()); |
| 165 | return true; |
| 166 | } else if (keyEvent->key() == Qt::Key_W) { |
| 167 | deleteSpacesToLeftOfCursor(); |
| 168 | if (!deleteNonWordCharsToLeftOfCursor()) { |
| 169 | deleteWordCharsToLeftOfCursor(); |
| 170 | } |
| 171 | return true; |
| 172 | } else if (keyEvent->key() == Qt::Key_R || keyEvent->key() == Qt::Key_G) { |
| 173 | m_waitingForRegister = true; |
| 174 | m_waitingForRegisterIndicator->setVisible(true); |
| 175 | if (keyEvent->key() == Qt::Key_G) { |
| 176 | m_insertedTextShouldBeEscapedForSearchingAsLiteral = true; |
| 177 | } |
| 178 | return true; |
| 179 | } |
| 180 | return false; |
| 181 | } |
| 182 | |
| 183 | void EmulatedCommandBar::insertRegisterContents(const QKeyEvent *keyEvent) |
| 184 | { |
| 185 | if (keyEvent->key() != Qt::Key_Shift && keyEvent->key() != Qt::Key_Control) { |
| 186 | const QChar key = KeyParser::self()->KeyEventToQChar(keyEvent: *keyEvent).toLower(); |
| 187 | |
| 188 | const int oldCursorPosition = m_edit->cursorPosition(); |
| 189 | QString textToInsert; |
| 190 | if (keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_W) { |
| 191 | textToInsert = m_view->doc()->wordAt(cursor: m_view->cursorPosition()); |
| 192 | } else { |
| 193 | textToInsert = m_viInputModeManager->globalState()->registers()->getContent(reg: key); |
| 194 | } |
| 195 | if (m_insertedTextShouldBeEscapedForSearchingAsLiteral) { |
| 196 | textToInsert = escapedForSearchingAsLiteral(originalQtRegex: textToInsert); |
| 197 | m_insertedTextShouldBeEscapedForSearchingAsLiteral = false; |
| 198 | } |
| 199 | m_edit->setText(m_edit->text().insert(i: m_edit->cursorPosition(), s: textToInsert)); |
| 200 | m_edit->setCursorPosition(oldCursorPosition + textToInsert.length()); |
| 201 | m_waitingForRegister = false; |
| 202 | m_waitingForRegisterIndicator->setVisible(false); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | bool EmulatedCommandBar::eventFilter(QObject *object, QEvent *event) |
| 207 | { |
| 208 | // The "object" will be either m_edit or m_completer's popup. |
| 209 | if (m_suspendEditEventFiltering) { |
| 210 | return false; |
| 211 | } |
| 212 | Q_UNUSED(object); |
| 213 | if (event->type() == QEvent::KeyPress) { |
| 214 | // Re-route this keypress through Vim's central keypress handling area, so that we can use the keypress in e.g. |
| 215 | // mappings and macros. |
| 216 | return m_viInputMode->keyPress(static_cast<QKeyEvent *>(event)); |
| 217 | } |
| 218 | return false; |
| 219 | } |
| 220 | |
| 221 | void EmulatedCommandBar::deleteSpacesToLeftOfCursor() |
| 222 | { |
| 223 | while (m_edit->cursorPosition() != 0 && m_edit->text().at(i: m_edit->cursorPosition() - 1) == QLatin1Char(' ')) { |
| 224 | m_edit->backspace(); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | void EmulatedCommandBar::deleteWordCharsToLeftOfCursor() |
| 229 | { |
| 230 | while (m_edit->cursorPosition() != 0) { |
| 231 | const QChar charToTheLeftOfCursor = m_edit->text().at(i: m_edit->cursorPosition() - 1); |
| 232 | if (!charToTheLeftOfCursor.isLetterOrNumber() && charToTheLeftOfCursor != QLatin1Char('_')) { |
| 233 | break; |
| 234 | } |
| 235 | |
| 236 | m_edit->backspace(); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | bool EmulatedCommandBar::deleteNonWordCharsToLeftOfCursor() |
| 241 | { |
| 242 | bool deletionsMade = false; |
| 243 | while (m_edit->cursorPosition() != 0) { |
| 244 | const QChar charToTheLeftOfCursor = m_edit->text().at(i: m_edit->cursorPosition() - 1); |
| 245 | if (charToTheLeftOfCursor.isLetterOrNumber() || charToTheLeftOfCursor == QLatin1Char('_') || charToTheLeftOfCursor == QLatin1Char(' ')) { |
| 246 | break; |
| 247 | } |
| 248 | |
| 249 | m_edit->backspace(); |
| 250 | deletionsMade = true; |
| 251 | } |
| 252 | return deletionsMade; |
| 253 | } |
| 254 | |
| 255 | bool EmulatedCommandBar::handleKeyPress(const QKeyEvent *keyEvent) |
| 256 | { |
| 257 | if (m_waitingForRegister) { |
| 258 | insertRegisterContents(keyEvent); |
| 259 | return true; |
| 260 | } |
| 261 | const bool completerHandled = m_completer->completerHandledKeypress(keyEvent); |
| 262 | if (completerHandled) { |
| 263 | return true; |
| 264 | } |
| 265 | |
| 266 | if (keyEvent->modifiers() == CONTROL_MODIFIER && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) { |
| 267 | Q_EMIT hideMe(); |
| 268 | return true; |
| 269 | } |
| 270 | |
| 271 | // Is this a built-in Emulated Command Bar keypress e.g. insert from register, ctrl-h, etc? |
| 272 | const bool barHandled = barHandledKeypress(keyEvent); |
| 273 | if (barHandled) { |
| 274 | return true; |
| 275 | } |
| 276 | |
| 277 | // Can the current mode handle it? |
| 278 | const bool currentModeHandled = m_currentMode->handleKeyPress(keyEvent); |
| 279 | if (currentModeHandled) { |
| 280 | return true; |
| 281 | } |
| 282 | |
| 283 | // Couldn't handle this key event. |
| 284 | // Send the keypress back to the QLineEdit. Ideally, instead of doing this, we would simply return "false" |
| 285 | // and let Qt re-dispatch the event itself; however, there is a corner case in that if the selection |
| 286 | // changes (as a result of e.g. incremental searches during Visual Mode), and the keypress that causes it |
| 287 | // is not dispatched from within KateViInputModeHandler::handleKeypress(...) |
| 288 | // (so KateViInputModeManager::isHandlingKeypress() returns false), we lose information about whether we are |
| 289 | // in Visual Mode, Visual Line Mode, etc. See VisualViMode::updateSelection( ). |
| 290 | if (m_edit->isVisible()) { |
| 291 | if (m_suspendEditEventFiltering) { |
| 292 | return false; |
| 293 | } |
| 294 | m_suspendEditEventFiltering = true; |
| 295 | QKeyEvent keyEventCopy(keyEvent->type(), keyEvent->key(), keyEvent->modifiers(), keyEvent->text(), keyEvent->isAutoRepeat(), keyEvent->count()); |
| 296 | qApp->notify(m_edit, &keyEventCopy); |
| 297 | m_suspendEditEventFiltering = false; |
| 298 | } |
| 299 | return true; |
| 300 | } |
| 301 | |
| 302 | bool EmulatedCommandBar::isSendingSyntheticSearchCompletedKeypress() |
| 303 | { |
| 304 | return m_searchMode->isSendingSyntheticSearchCompletedKeypress(); |
| 305 | } |
| 306 | |
| 307 | void EmulatedCommandBar::startInteractiveSearchAndReplace(std::shared_ptr<SedReplace::InteractiveSedReplacer> interactiveSedReplace) |
| 308 | { |
| 309 | Q_ASSERT_X(interactiveSedReplace->currentMatch().isValid(), |
| 310 | "startInteractiveSearchAndReplace" , |
| 311 | "KateCommands shouldn't initiate an interactive sed replace with no initial match" ); |
| 312 | switchToMode(newMode: m_interactiveSedReplaceMode.get()); |
| 313 | m_interactiveSedReplaceMode->activate(interactiveSedReplace); |
| 314 | } |
| 315 | |
| 316 | void EmulatedCommandBar::showBarTypeIndicator(EmulatedCommandBar::Mode mode) |
| 317 | { |
| 318 | QChar barTypeIndicator = QChar::Null; |
| 319 | switch (mode) { |
| 320 | case SearchForward: |
| 321 | barTypeIndicator = QLatin1Char('/'); |
| 322 | break; |
| 323 | case SearchBackward: |
| 324 | barTypeIndicator = QLatin1Char('?'); |
| 325 | break; |
| 326 | case Command: |
| 327 | barTypeIndicator = QLatin1Char(':'); |
| 328 | break; |
| 329 | default: |
| 330 | Q_ASSERT(false && "Unknown mode!" ); |
| 331 | } |
| 332 | m_barTypeIndicator->setText(barTypeIndicator); |
| 333 | m_barTypeIndicator->show(); |
| 334 | } |
| 335 | |
| 336 | QString EmulatedCommandBar::executeCommand(const QString &commandToExecute) |
| 337 | { |
| 338 | return m_commandMode->executeCommand(commandToExecute); |
| 339 | } |
| 340 | |
| 341 | void EmulatedCommandBar::closeWithStatusMessage(const QString &exitStatusMessage) |
| 342 | { |
| 343 | // Display the message for a while. Become inactive, so we don't steal keys in the meantime. |
| 344 | m_isActive = false; |
| 345 | |
| 346 | m_exitStatusMessageDisplay->show(); |
| 347 | m_exitStatusMessageDisplay->setText(exitStatusMessage); |
| 348 | hideAllWidgetsExcept(widgetToKeepVisible: m_exitStatusMessageDisplay); |
| 349 | |
| 350 | m_exitStatusMessageDisplayHideTimer->start(msec: m_exitStatusMessageHideTimeOutMS); |
| 351 | } |
| 352 | |
| 353 | void EmulatedCommandBar::editTextChanged(const QString &newText) |
| 354 | { |
| 355 | Q_ASSERT(!m_interactiveSedReplaceMode->isActive()); |
| 356 | m_currentMode->editTextChanged(newText); |
| 357 | m_completer->editTextChanged(newText); |
| 358 | } |
| 359 | |
| 360 | void EmulatedCommandBar::startHideExitStatusMessageTimer() |
| 361 | { |
| 362 | if (m_exitStatusMessageDisplay->isVisible() && !m_exitStatusMessageDisplayHideTimer->isActive()) { |
| 363 | m_exitStatusMessageDisplayHideTimer->start(msec: m_exitStatusMessageHideTimeOutMS); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | void EmulatedCommandBar::setViInputModeManager(InputModeManager *viInputModeManager) |
| 368 | { |
| 369 | m_viInputModeManager = viInputModeManager; |
| 370 | m_searchMode->setViInputModeManager(viInputModeManager); |
| 371 | m_commandMode->setViInputModeManager(viInputModeManager); |
| 372 | m_interactiveSedReplaceMode->setViInputModeManager(viInputModeManager); |
| 373 | } |
| 374 | |
| 375 | void EmulatedCommandBar::hideAllWidgetsExcept(QWidget *widgetToKeepVisible) |
| 376 | { |
| 377 | const QList<QWidget *> widgets = centralWidget()->findChildren<QWidget *>(); |
| 378 | for (QWidget *widget : widgets) { |
| 379 | if (widget != widgetToKeepVisible) { |
| 380 | widget->hide(); |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | void EmulatedCommandBar::createAndAddBarTypeIndicator(QLayout *layout) |
| 386 | { |
| 387 | m_barTypeIndicator = new QLabel(this); |
| 388 | m_barTypeIndicator->setObjectName(QStringLiteral("bartypeindicator" )); |
| 389 | layout->addWidget(w: m_barTypeIndicator); |
| 390 | } |
| 391 | |
| 392 | void EmulatedCommandBar::createAndAddEditWidget(QLayout *layout) |
| 393 | { |
| 394 | m_edit = new QLineEdit(this); |
| 395 | m_edit->setObjectName(QStringLiteral("commandtext" )); |
| 396 | layout->addWidget(w: m_edit); |
| 397 | } |
| 398 | |
| 399 | void EmulatedCommandBar::createAndAddExitStatusMessageDisplay(QLayout *layout) |
| 400 | { |
| 401 | m_exitStatusMessageDisplay = new QLabel(this); |
| 402 | m_exitStatusMessageDisplay->setObjectName(QStringLiteral("commandresponsemessage" )); |
| 403 | m_exitStatusMessageDisplay->setAlignment(Qt::AlignLeft); |
| 404 | layout->addWidget(w: m_exitStatusMessageDisplay); |
| 405 | } |
| 406 | |
| 407 | void EmulatedCommandBar::createAndInitExitStatusMessageDisplayTimer() |
| 408 | { |
| 409 | m_exitStatusMessageDisplayHideTimer = new QTimer(this); |
| 410 | m_exitStatusMessageDisplayHideTimer->setSingleShot(true); |
| 411 | connect(sender: m_exitStatusMessageDisplayHideTimer, signal: &QTimer::timeout, context: this, slot: &EmulatedCommandBar::hideMe); |
| 412 | // Make sure the timer is stopped when the user switches views. If not, focus will be given to the |
| 413 | // wrong view when KateViewBar::hideCurrentBarWidget() is called as a result of m_commandResponseMessageDisplayHide |
| 414 | // timing out. |
| 415 | connect(sender: m_view, signal: &KTextEditor::ViewPrivate::focusOut, context: m_exitStatusMessageDisplayHideTimer, slot: &QTimer::stop); |
| 416 | // We can restart the timer once the view has focus again, though. |
| 417 | connect(sender: m_view, signal: &KTextEditor::ViewPrivate::focusIn, context: this, slot: &EmulatedCommandBar::startHideExitStatusMessageTimer); |
| 418 | } |
| 419 | |
| 420 | void EmulatedCommandBar::createAndAddWaitingForRegisterIndicator(QLayout *layout) |
| 421 | { |
| 422 | m_waitingForRegisterIndicator = new QLabel(this); |
| 423 | m_waitingForRegisterIndicator->setObjectName(QStringLiteral("waitingforregisterindicator" )); |
| 424 | m_waitingForRegisterIndicator->setVisible(false); |
| 425 | m_waitingForRegisterIndicator->setText(QStringLiteral("\"" )); |
| 426 | layout->addWidget(w: m_waitingForRegisterIndicator); |
| 427 | } |
| 428 | |