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 | |