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 "commandmode.h"
8 
9 #include "../commandrangeexpressionparser.h"
10 #include "emulatedcommandbar.h"
11 #include "interactivesedreplacemode.h"
12 #include "searchmode.h"
13 
14 #include "../globalstate.h"
15 #include "../history.h"
16 #include <vimode/appcommands.h>
17 #include <vimode/cmds.h>
18 #include <vimode/inputmodemanager.h>
19 
20 #include "katecmds.h"
21 #include "katecommandlinescript.h"
22 #include "katescriptmanager.h"
23 #include "kateview.h"
24 
25 #include <KLocalizedString>
26 
27 #include <QLineEdit>
28 #include <QRegularExpression>
29 #include <QWhatsThis>
30 
31 using namespace KateVi;
32 
CommandMode(EmulatedCommandBar * emulatedCommandBar,MatchHighlighter * matchHighlighter,InputModeManager * viInputModeManager,KTextEditor::ViewPrivate * view,QLineEdit * edit,InteractiveSedReplaceMode * interactiveSedReplaceMode,Completer * completer)33 CommandMode::CommandMode(EmulatedCommandBar *emulatedCommandBar,
34                          MatchHighlighter *matchHighlighter,
35                          InputModeManager *viInputModeManager,
36                          KTextEditor::ViewPrivate *view,
37                          QLineEdit *edit,
38                          InteractiveSedReplaceMode *interactiveSedReplaceMode,
39                          Completer *completer)
40     : ActiveMode(emulatedCommandBar, matchHighlighter, viInputModeManager, view)
41     , m_edit(edit)
42     , m_interactiveSedReplaceMode(interactiveSedReplaceMode)
43     , m_completer(completer)
44 {
45     QVector<KTextEditor::Command *> cmds;
46     cmds.push_back(KateCommands::CoreCommands::self());
47     cmds.push_back(Commands::self());
48     cmds.push_back(AppCommands::self());
49     cmds.push_back(SedReplace::self());
50     cmds.push_back(BufferCommands::self());
51 
52     for (KTextEditor::Command *cmd : KateScriptManager::self()->commandLineScripts()) {
53         cmds.push_back(cmd);
54     }
55 
56     for (KTextEditor::Command *cmd : std::as_const(cmds)) {
57         QStringList l = cmd->cmds();
58 
59         for (int z = 0; z < l.count(); z++) {
60             m_cmdDict.insert(l[z], cmd);
61         }
62 
63         m_cmdCompletion.insertItems(l);
64     }
65 }
66 
handleKeyPress(const QKeyEvent * keyEvent)67 bool CommandMode::handleKeyPress(const QKeyEvent *keyEvent)
68 {
69     if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_D || keyEvent->key() == Qt::Key_F)) {
70         CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
71         if (parsedSedExpression.parsedSuccessfully) {
72             const bool clearFindTerm = (keyEvent->key() == Qt::Key_D);
73             if (clearFindTerm) {
74                 m_edit->setSelection(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
75                 m_edit->insert(QString());
76             } else {
77                 // Clear replace term.
78                 m_edit->setSelection(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
79                 m_edit->insert(QString());
80             }
81         }
82         return true;
83     }
84     return false;
85 }
86 
editTextChanged(const QString & newText)87 void CommandMode::editTextChanged(const QString &newText)
88 {
89     Q_UNUSED(newText); // We read the current text from m_edit.
90     if (m_completer->isCompletionActive()) {
91         return;
92     }
93     // Command completion doesn't need to be manually invoked.
94     if (!withoutRangeExpression().isEmpty() && !m_completer->isNextTextChangeDueToCompletionChange()) {
95         // ... However, command completion mode should not be automatically invoked if this is not the current leading
96         // word in the text edit (it gets annoying if completion pops up after ":s/se" etc).
97         const bool commandBeforeCursorIsLeading = (commandBeforeCursorBegin() == rangeExpression().length());
98         if (commandBeforeCursorIsLeading) {
99             CompletionStartParams completionStartParams = activateCommandCompletion();
100             startCompletion(completionStartParams);
101         }
102     }
103 }
104 
deactivate(bool wasAborted)105 void CommandMode::deactivate(bool wasAborted)
106 {
107     if (wasAborted) {
108         // Appending the command to the history when it is executed is handled elsewhere; we can't
109         // do it inside closed() as we may still be showing the command response display.
110         viInputModeManager()->globalState()->commandHistory()->append(m_edit->text());
111         // With Vim, aborting a command returns us to Normal mode, even if we were in Visual Mode.
112         // If we switch from Visual to Normal mode, we need to clear the selection.
113         view()->clearSelection();
114     }
115 }
116 
completionInvoked(Completer::CompletionInvocation invocationType)117 CompletionStartParams CommandMode::completionInvoked(Completer::CompletionInvocation invocationType)
118 {
119     CompletionStartParams completionStartParams;
120     if (invocationType == Completer::CompletionInvocation::ExtraContext) {
121         if (isCursorInFindTermOfSed()) {
122             completionStartParams = activateSedFindHistoryCompletion();
123         } else if (isCursorInReplaceTermOfSed()) {
124             completionStartParams = activateSedReplaceHistoryCompletion();
125         } else {
126             completionStartParams = activateCommandHistoryCompletion();
127         }
128     } else {
129         // Normal context, so boring, ordinary History completion.
130         completionStartParams = activateCommandHistoryCompletion();
131     }
132     return completionStartParams;
133 }
134 
completionChosen()135 void CommandMode::completionChosen()
136 {
137     QString commandToExecute = m_edit->text();
138     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
139     if (parsedSedExpression.parsedSuccessfully) {
140         const QString originalFindTerm = sedFindTerm();
141         const QString convertedFindTerm = vimRegexToQtRegexPattern(originalFindTerm);
142         const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(convertedFindTerm);
143         viInputModeManager()->globalState()->searchHistory()->append(originalFindTerm);
144         const QString replaceTerm = sedReplaceTerm();
145         viInputModeManager()->globalState()->replaceHistory()->append(replaceTerm);
146         commandToExecute = commandWithSedSearchRegexConverted;
147     }
148 
149     const QString commandResponseMessage = executeCommand(commandToExecute);
150     // Don't close the bar if executing the command switched us to Interactive Sed Replace mode.
151     if (!m_interactiveSedReplaceMode->isActive()) {
152         if (commandResponseMessage.isEmpty()) {
153             emulatedCommandBar()->hideMe();
154         } else {
155             closeWithStatusMessage(commandResponseMessage);
156         }
157     }
158     viInputModeManager()->globalState()->commandHistory()->append(m_edit->text());
159 }
160 
executeCommand(const QString & commandToExecute)161 QString CommandMode::executeCommand(const QString &commandToExecute)
162 {
163     // Silently ignore leading space characters and colon characters (for vi-heads).
164     uint n = 0;
165     const uint textlen = commandToExecute.length();
166     while ((n < textlen) && commandToExecute[n].isSpace()) {
167         n++;
168     }
169 
170     if (n >= textlen) {
171         return QString();
172     }
173 
174     QString commandResponseMessage;
175     QString cmd = commandToExecute.mid(n);
176 
177     KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(cmd, cmd);
178 
179     if (cmd.length() > 0) {
180         KTextEditor::Command *p = queryCommand(cmd);
181         if (p) {
182             KateViCommandInterface *ci = dynamic_cast<KateViCommandInterface *>(p);
183             if (ci) {
184                 ci->setViInputModeManager(viInputModeManager());
185                 ci->setViGlobal(viInputModeManager()->globalState());
186             }
187 
188             // The following commands changes the focus themselves, so bar should be hidden before execution.
189 
190             // We got a range and a valid command, but the command does not support ranges.
191             if (range.isValid() && !p->supportsRange(cmd)) {
192                 commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd);
193             } else {
194                 if (p->exec(view(), cmd, commandResponseMessage, range)) {
195                     if (commandResponseMessage.length() > 0) {
196                         commandResponseMessage = i18n("Success: ") + commandResponseMessage;
197                     }
198                 } else {
199                     if (commandResponseMessage.length() > 0) {
200                         if (commandResponseMessage.contains(QLatin1Char('\n'))) {
201                             // multiline error, use widget with more space
202                             QWhatsThis::showText(emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), commandResponseMessage);
203                         }
204                     } else {
205                         commandResponseMessage = i18n("Command \"%1\" failed.", cmd);
206                     }
207                 }
208             }
209         } else {
210             commandResponseMessage = i18n("No such command: \"%1\"", cmd);
211         }
212     }
213 
214     // the following commands change the focus themselves
215     static const QRegularExpression reCmds(
216         QStringLiteral("^(?:buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst"
217                        "|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$"));
218     if (!reCmds.match(QStringView(cmd).left(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) {
219         view()->setFocus();
220     }
221 
222     viInputModeManager()->reset();
223     return commandResponseMessage;
224 }
225 
withoutRangeExpression()226 QString CommandMode::withoutRangeExpression()
227 {
228     const QString originalCommand = m_edit->text();
229     return originalCommand.mid(rangeExpression().length());
230 }
231 
rangeExpression()232 QString CommandMode::rangeExpression()
233 {
234     const QString command = m_edit->text();
235     return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command);
236 }
237 
parseAsSedExpression()238 CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression()
239 {
240     const QString commandWithoutRangeExpression = withoutRangeExpression();
241     ParsedSedExpression parsedSedExpression;
242     QString delimiter;
243     parsedSedExpression.parsedSuccessfully = SedReplace::parse(commandWithoutRangeExpression,
244                                                                delimiter,
245                                                                parsedSedExpression.findBeginPos,
246                                                                parsedSedExpression.findEndPos,
247                                                                parsedSedExpression.replaceBeginPos,
248                                                                parsedSedExpression.replaceEndPos);
249     if (parsedSedExpression.parsedSuccessfully) {
250         parsedSedExpression.delimiter = delimiter.at(0);
251         if (parsedSedExpression.replaceBeginPos == -1) {
252             if (parsedSedExpression.findBeginPos != -1) {
253                 // The replace term was empty, and a quirk of the regex used is that replaceBeginPos will be -1.
254                 // It's actually the position after the first occurrence of the delimiter after the end of the find pos.
255                 parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.findEndPos) + 1;
256                 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
257             } else {
258                 // Both find and replace terms are empty; replace term is at the third occurrence of the delimiter.
259                 parsedSedExpression.replaceBeginPos = 0;
260                 for (int delimiterCount = 1; delimiterCount <= 3; delimiterCount++) {
261                     parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.replaceBeginPos + 1);
262                 }
263                 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
264             }
265         }
266         if (parsedSedExpression.findBeginPos == -1) {
267             // The find term was empty, and a quirk of the regex used is that findBeginPos will be -1.
268             // It's actually the position after the first occurrence of the delimiter.
269             parsedSedExpression.findBeginPos = commandWithoutRangeExpression.indexOf(delimiter) + 1;
270             parsedSedExpression.findEndPos = parsedSedExpression.findBeginPos - 1;
271         }
272     }
273 
274     if (parsedSedExpression.parsedSuccessfully) {
275         parsedSedExpression.findBeginPos += rangeExpression().length();
276         parsedSedExpression.findEndPos += rangeExpression().length();
277         parsedSedExpression.replaceBeginPos += rangeExpression().length();
278         parsedSedExpression.replaceEndPos += rangeExpression().length();
279     }
280     return parsedSedExpression;
281 }
282 
sedFindTerm()283 QString CommandMode::sedFindTerm()
284 {
285     const QString command = m_edit->text();
286     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
287     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
288     return command.mid(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
289 }
290 
sedReplaceTerm()291 QString CommandMode::sedReplaceTerm()
292 {
293     const QString command = m_edit->text();
294     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
295     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
296     return command.mid(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
297 }
298 
withSedFindTermReplacedWith(const QString & newFindTerm)299 QString CommandMode::withSedFindTermReplacedWith(const QString &newFindTerm)
300 {
301     const QString command = m_edit->text();
302     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
303     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
304     const QStringView strView(command);
305     return strView.mid(0, parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(parsedSedExpression.findEndPos + 1);
306 }
307 
withSedDelimiterEscaped(const QString & text)308 QString CommandMode::withSedDelimiterEscaped(const QString &text)
309 {
310     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
311     QString delimiterEscaped = ensuredCharEscaped(text, parsedSedExpression.delimiter);
312     return delimiterEscaped;
313 }
314 
isCursorInFindTermOfSed()315 bool CommandMode::isCursorInFindTermOfSed()
316 {
317     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
318     return parsedSedExpression.parsedSuccessfully
319         && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1);
320 }
321 
isCursorInReplaceTermOfSed()322 bool CommandMode::isCursorInReplaceTermOfSed()
323 {
324     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
325     return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos
326         && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1;
327 }
328 
commandBeforeCursorBegin()329 int CommandMode::commandBeforeCursorBegin()
330 {
331     const QString textWithoutRangeExpression = withoutRangeExpression();
332     const int cursorPositionWithoutRangeExpression = m_edit->cursorPosition() - rangeExpression().length();
333     int commandBeforeCursorBegin = cursorPositionWithoutRangeExpression - 1;
334     while (commandBeforeCursorBegin >= 0
335            && (textWithoutRangeExpression[commandBeforeCursorBegin].isLetterOrNumber()
336                || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('_')
337                || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('-'))) {
338         commandBeforeCursorBegin--;
339     }
340     commandBeforeCursorBegin++;
341     commandBeforeCursorBegin += rangeExpression().length();
342     return commandBeforeCursorBegin;
343 }
344 
activateCommandCompletion()345 CompletionStartParams CommandMode::activateCommandCompletion()
346 {
347     return CompletionStartParams::createModeSpecific(m_cmdCompletion.items(), commandBeforeCursorBegin());
348 }
349 
activateCommandHistoryCompletion()350 CompletionStartParams CommandMode::activateCommandHistoryCompletion()
351 {
352     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->commandHistory()->items()), 0);
353 }
354 
activateSedFindHistoryCompletion()355 CompletionStartParams CommandMode::activateSedFindHistoryCompletion()
356 {
357     if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) {
358         return CompletionStartParams::invalid();
359     }
360     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
361     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()),
362                                                      parsedSedExpression.findBeginPos,
363                                                      [this](const QString &completion) -> QString {
364                                                          return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion));
365                                                      });
366 }
367 
activateSedReplaceHistoryCompletion()368 CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion()
369 {
370     if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) {
371         return CompletionStartParams::invalid();
372     }
373     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
374     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->replaceHistory()->items()),
375                                                      parsedSedExpression.replaceBeginPos,
376                                                      [this](const QString &completion) -> QString {
377                                                          return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion));
378                                                      });
379 }
380 
queryCommand(const QString & cmd) const381 KTextEditor::Command *CommandMode::queryCommand(const QString &cmd) const
382 {
383     // a command can be named ".*[\w\-]+" with the constrain that it must
384     // contain at least one letter.
385     int f = 0;
386     bool b = false;
387 
388     // special case: '-' and '_' can be part of a command name, but if the
389     // command is 's' (substitute), it should be considered the delimiter and
390     // should not be counted as part of the command name
391     if (cmd.length() >= 2 && cmd.at(0) == QLatin1Char('s') && (cmd.at(1) == QLatin1Char('-') || cmd.at(1) == QLatin1Char('_'))) {
392         return m_cmdDict.value(QStringLiteral("s"));
393     }
394 
395     for (; f < cmd.length(); f++) {
396         if (cmd[f].isLetter()) {
397             b = true;
398         }
399         if (b && (!cmd[f].isLetterOrNumber() && cmd[f] != QLatin1Char('-') && cmd[f] != QLatin1Char('_'))) {
400             break;
401         }
402     }
403     return m_cmdDict.value(cmd.left(f));
404 }
405