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
30using namespace KateVi;
31
32namespace
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 */
38QString 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
54EmulatedCommandBar::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
83EmulatedCommandBar::~EmulatedCommandBar() = default;
84
85void 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
114bool EmulatedCommandBar::isActive() const
115{
116 return m_isActive;
117}
118
119void EmulatedCommandBar::setCommandResponseMessageTimeout(long int commandResponseMessageTimeOutMS)
120{
121 m_exitStatusMessageHideTimeOutMS = commandResponseMessageTimeOutMS;
122}
123
124void 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
136void 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
148bool 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
183void 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
206bool 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
221void 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
228void 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
240bool 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
255bool 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
302bool EmulatedCommandBar::isSendingSyntheticSearchCompletedKeypress()
303{
304 return m_searchMode->isSendingSyntheticSearchCompletedKeypress();
305}
306
307void 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
316void 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
336QString EmulatedCommandBar::executeCommand(const QString &commandToExecute)
337{
338 return m_commandMode->executeCommand(commandToExecute);
339}
340
341void 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
353void EmulatedCommandBar::editTextChanged(const QString &newText)
354{
355 Q_ASSERT(!m_interactiveSedReplaceMode->isActive());
356 m_currentMode->editTextChanged(newText);
357 m_completer->editTextChanged(newText);
358}
359
360void EmulatedCommandBar::startHideExitStatusMessageTimer()
361{
362 if (m_exitStatusMessageDisplay->isVisible() && !m_exitStatusMessageDisplayHideTimer->isActive()) {
363 m_exitStatusMessageDisplayHideTimer->start(msec: m_exitStatusMessageHideTimeOutMS);
364 }
365}
366
367void 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
375void 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
385void 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
392void 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
399void 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
407void 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
420void 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

source code of ktexteditor/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp