Warning, file /frameworks/ktexteditor/src/vimode/emulatedcommandbar/commandmode.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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 "commandmode.h"
0008 
0009 #include "../commandrangeexpressionparser.h"
0010 #include "emulatedcommandbar.h"
0011 #include "interactivesedreplacemode.h"
0012 #include "searchmode.h"
0013 
0014 #include "../globalstate.h"
0015 #include "../history.h"
0016 #include <vimode/appcommands.h>
0017 #include <vimode/cmds.h>
0018 #include <vimode/inputmodemanager.h>
0019 
0020 #include "katecmds.h"
0021 #include "katecommandlinescript.h"
0022 #include "katescriptmanager.h"
0023 #include "kateview.h"
0024 
0025 #include <KLocalizedString>
0026 
0027 #include <QLineEdit>
0028 #include <QRegularExpression>
0029 #include <QWhatsThis>
0030 
0031 using namespace KateVi;
0032 
0033 CommandMode::CommandMode(EmulatedCommandBar *emulatedCommandBar,
0034                          MatchHighlighter *matchHighlighter,
0035                          InputModeManager *viInputModeManager,
0036                          KTextEditor::ViewPrivate *view,
0037                          QLineEdit *edit,
0038                          InteractiveSedReplaceMode *interactiveSedReplaceMode,
0039                          Completer *completer)
0040     : ActiveMode(emulatedCommandBar, matchHighlighter, viInputModeManager, view)
0041     , m_edit(edit)
0042     , m_interactiveSedReplaceMode(interactiveSedReplaceMode)
0043     , m_completer(completer)
0044 {
0045     QVector<KTextEditor::Command *> cmds;
0046     cmds.push_back(KateCommands::CoreCommands::self());
0047     cmds.push_back(Commands::self());
0048     cmds.push_back(AppCommands::self());
0049     cmds.push_back(SedReplace::self());
0050     cmds.push_back(BufferCommands::self());
0051 
0052     for (KTextEditor::Command *cmd : KateScriptManager::self()->commandLineScripts()) {
0053         cmds.push_back(cmd);
0054     }
0055 
0056     for (KTextEditor::Command *cmd : std::as_const(cmds)) {
0057         QStringList l = cmd->cmds();
0058 
0059         for (int z = 0; z < l.count(); z++) {
0060             m_cmdDict.insert(l[z], cmd);
0061         }
0062 
0063         m_cmdCompletion.insertItems(l);
0064     }
0065 }
0066 
0067 bool CommandMode::handleKeyPress(const QKeyEvent *keyEvent)
0068 {
0069     if (keyEvent->modifiers() == CONTROL_MODIFIER && (keyEvent->key() == Qt::Key_D || keyEvent->key() == Qt::Key_F)) {
0070         CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0071         if (parsedSedExpression.parsedSuccessfully) {
0072             const bool clearFindTerm = (keyEvent->key() == Qt::Key_D);
0073             if (clearFindTerm) {
0074                 m_edit->setSelection(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
0075                 m_edit->insert(QString());
0076             } else {
0077                 // Clear replace term.
0078                 m_edit->setSelection(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
0079                 m_edit->insert(QString());
0080             }
0081         }
0082         return true;
0083     }
0084     return false;
0085 }
0086 
0087 void CommandMode::editTextChanged(const QString &newText)
0088 {
0089     Q_UNUSED(newText); // We read the current text from m_edit.
0090     if (m_completer->isCompletionActive()) {
0091         return;
0092     }
0093     // Command completion doesn't need to be manually invoked.
0094     if (!withoutRangeExpression().isEmpty() && !m_completer->isNextTextChangeDueToCompletionChange()) {
0095         // ... However, command completion mode should not be automatically invoked if this is not the current leading
0096         // word in the text edit (it gets annoying if completion pops up after ":s/se" etc).
0097         const bool commandBeforeCursorIsLeading = (commandBeforeCursorBegin() == rangeExpression().length());
0098         if (commandBeforeCursorIsLeading) {
0099             CompletionStartParams completionStartParams = activateCommandCompletion();
0100             startCompletion(completionStartParams);
0101         }
0102     }
0103 }
0104 
0105 void CommandMode::deactivate(bool wasAborted)
0106 {
0107     if (wasAborted) {
0108         // Appending the command to the history when it is executed is handled elsewhere; we can't
0109         // do it inside closed() as we may still be showing the command response display.
0110         viInputModeManager()->globalState()->commandHistory()->append(m_edit->text());
0111         // With Vim, aborting a command returns us to Normal mode, even if we were in Visual Mode.
0112         // If we switch from Visual to Normal mode, we need to clear the selection.
0113         view()->clearSelection();
0114     }
0115 }
0116 
0117 CompletionStartParams CommandMode::completionInvoked(Completer::CompletionInvocation invocationType)
0118 {
0119     CompletionStartParams completionStartParams;
0120     if (invocationType == Completer::CompletionInvocation::ExtraContext) {
0121         if (isCursorInFindTermOfSed()) {
0122             completionStartParams = activateSedFindHistoryCompletion();
0123         } else if (isCursorInReplaceTermOfSed()) {
0124             completionStartParams = activateSedReplaceHistoryCompletion();
0125         } else {
0126             completionStartParams = activateCommandHistoryCompletion();
0127         }
0128     } else {
0129         // Normal context, so boring, ordinary History completion.
0130         completionStartParams = activateCommandHistoryCompletion();
0131     }
0132     return completionStartParams;
0133 }
0134 
0135 void CommandMode::completionChosen()
0136 {
0137     QString commandToExecute = m_edit->text();
0138     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0139     if (parsedSedExpression.parsedSuccessfully) {
0140         const QString originalFindTerm = sedFindTerm();
0141         const QString convertedFindTerm = vimRegexToQtRegexPattern(originalFindTerm);
0142         const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(convertedFindTerm);
0143         viInputModeManager()->globalState()->searchHistory()->append(originalFindTerm);
0144         const QString replaceTerm = sedReplaceTerm();
0145         viInputModeManager()->globalState()->replaceHistory()->append(replaceTerm);
0146         commandToExecute = commandWithSedSearchRegexConverted;
0147     }
0148 
0149     const QString commandResponseMessage = executeCommand(commandToExecute);
0150     // Don't close the bar if executing the command switched us to Interactive Sed Replace mode.
0151     if (!m_interactiveSedReplaceMode->isActive()) {
0152         if (commandResponseMessage.isEmpty()) {
0153             emulatedCommandBar()->hideMe();
0154         } else {
0155             closeWithStatusMessage(commandResponseMessage);
0156         }
0157     }
0158     viInputModeManager()->globalState()->commandHistory()->append(m_edit->text());
0159 }
0160 
0161 QString CommandMode::executeCommand(const QString &commandToExecute)
0162 {
0163     // Silently ignore leading space characters and colon characters (for vi-heads).
0164     uint n = 0;
0165     const uint textlen = commandToExecute.length();
0166     while ((n < textlen) && commandToExecute[n].isSpace()) {
0167         n++;
0168     }
0169 
0170     if (n >= textlen) {
0171         return QString();
0172     }
0173 
0174     QString commandResponseMessage;
0175     QString cmd = commandToExecute.mid(n);
0176 
0177     KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(cmd, cmd);
0178 
0179     if (cmd.length() > 0) {
0180         KTextEditor::Command *p = queryCommand(cmd);
0181         if (p) {
0182             KateViCommandInterface *ci = dynamic_cast<KateViCommandInterface *>(p);
0183             if (ci) {
0184                 ci->setViInputModeManager(viInputModeManager());
0185                 ci->setViGlobal(viInputModeManager()->globalState());
0186             }
0187 
0188             // The following commands changes the focus themselves, so bar should be hidden before execution.
0189 
0190             // We got a range and a valid command, but the command does not support ranges.
0191             if (range.isValid() && !p->supportsRange(cmd)) {
0192                 commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd);
0193             } else {
0194                 if (p->exec(view(), cmd, commandResponseMessage, range)) {
0195                     if (commandResponseMessage.length() > 0) {
0196                         commandResponseMessage = i18n("Success: ") + commandResponseMessage;
0197                     }
0198                 } else {
0199                     if (commandResponseMessage.length() > 0) {
0200                         if (commandResponseMessage.contains(QLatin1Char('\n'))) {
0201                             // multiline error, use widget with more space
0202                             QWhatsThis::showText(emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), commandResponseMessage);
0203                         }
0204                     } else {
0205                         commandResponseMessage = i18n("Command \"%1\" failed.", cmd);
0206                     }
0207                 }
0208             }
0209         } else {
0210             commandResponseMessage = i18n("No such command: \"%1\"", cmd);
0211         }
0212     }
0213 
0214     // the following commands change the focus themselves
0215     static const QRegularExpression reCmds(
0216         QStringLiteral("^(?:buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst"
0217                        "|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$"));
0218     if (!reCmds.match(QStringView(cmd).left(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) {
0219         view()->setFocus();
0220     }
0221 
0222     viInputModeManager()->reset();
0223     return commandResponseMessage;
0224 }
0225 
0226 QString CommandMode::withoutRangeExpression()
0227 {
0228     const QString originalCommand = m_edit->text();
0229     return originalCommand.mid(rangeExpression().length());
0230 }
0231 
0232 QString CommandMode::rangeExpression()
0233 {
0234     const QString command = m_edit->text();
0235     return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command);
0236 }
0237 
0238 CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression()
0239 {
0240     const QString commandWithoutRangeExpression = withoutRangeExpression();
0241     ParsedSedExpression parsedSedExpression;
0242     QString delimiter;
0243     parsedSedExpression.parsedSuccessfully = SedReplace::parse(commandWithoutRangeExpression,
0244                                                                delimiter,
0245                                                                parsedSedExpression.findBeginPos,
0246                                                                parsedSedExpression.findEndPos,
0247                                                                parsedSedExpression.replaceBeginPos,
0248                                                                parsedSedExpression.replaceEndPos);
0249     if (parsedSedExpression.parsedSuccessfully) {
0250         parsedSedExpression.delimiter = delimiter.at(0);
0251         if (parsedSedExpression.replaceBeginPos == -1) {
0252             if (parsedSedExpression.findBeginPos != -1) {
0253                 // The replace term was empty, and a quirk of the regex used is that replaceBeginPos will be -1.
0254                 // It's actually the position after the first occurrence of the delimiter after the end of the find pos.
0255                 parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.findEndPos) + 1;
0256                 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
0257             } else {
0258                 // Both find and replace terms are empty; replace term is at the third occurrence of the delimiter.
0259                 parsedSedExpression.replaceBeginPos = 0;
0260                 for (int delimiterCount = 1; delimiterCount <= 3; delimiterCount++) {
0261                     parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.replaceBeginPos + 1);
0262                 }
0263                 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
0264             }
0265         }
0266         if (parsedSedExpression.findBeginPos == -1) {
0267             // The find term was empty, and a quirk of the regex used is that findBeginPos will be -1.
0268             // It's actually the position after the first occurrence of the delimiter.
0269             parsedSedExpression.findBeginPos = commandWithoutRangeExpression.indexOf(delimiter) + 1;
0270             parsedSedExpression.findEndPos = parsedSedExpression.findBeginPos - 1;
0271         }
0272     }
0273 
0274     if (parsedSedExpression.parsedSuccessfully) {
0275         parsedSedExpression.findBeginPos += rangeExpression().length();
0276         parsedSedExpression.findEndPos += rangeExpression().length();
0277         parsedSedExpression.replaceBeginPos += rangeExpression().length();
0278         parsedSedExpression.replaceEndPos += rangeExpression().length();
0279     }
0280     return parsedSedExpression;
0281 }
0282 
0283 QString CommandMode::sedFindTerm()
0284 {
0285     const QString command = m_edit->text();
0286     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0287     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
0288     return command.mid(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
0289 }
0290 
0291 QString CommandMode::sedReplaceTerm()
0292 {
0293     const QString command = m_edit->text();
0294     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0295     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
0296     return command.mid(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
0297 }
0298 
0299 QString CommandMode::withSedFindTermReplacedWith(const QString &newFindTerm)
0300 {
0301     const QString command = m_edit->text();
0302     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0303     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
0304     const QStringView strView(command);
0305     return strView.mid(0, parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(parsedSedExpression.findEndPos + 1);
0306 }
0307 
0308 QString CommandMode::withSedDelimiterEscaped(const QString &text)
0309 {
0310     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0311     QString delimiterEscaped = ensuredCharEscaped(text, parsedSedExpression.delimiter);
0312     return delimiterEscaped;
0313 }
0314 
0315 bool CommandMode::isCursorInFindTermOfSed()
0316 {
0317     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0318     return parsedSedExpression.parsedSuccessfully
0319         && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1);
0320 }
0321 
0322 bool CommandMode::isCursorInReplaceTermOfSed()
0323 {
0324     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0325     return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos
0326         && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1;
0327 }
0328 
0329 int CommandMode::commandBeforeCursorBegin()
0330 {
0331     const QString textWithoutRangeExpression = withoutRangeExpression();
0332     const int cursorPositionWithoutRangeExpression = m_edit->cursorPosition() - rangeExpression().length();
0333     int commandBeforeCursorBegin = cursorPositionWithoutRangeExpression - 1;
0334     while (commandBeforeCursorBegin >= 0
0335            && (textWithoutRangeExpression[commandBeforeCursorBegin].isLetterOrNumber()
0336                || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('_')
0337                || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('-'))) {
0338         commandBeforeCursorBegin--;
0339     }
0340     commandBeforeCursorBegin++;
0341     commandBeforeCursorBegin += rangeExpression().length();
0342     return commandBeforeCursorBegin;
0343 }
0344 
0345 CompletionStartParams CommandMode::activateCommandCompletion()
0346 {
0347     return CompletionStartParams::createModeSpecific(m_cmdCompletion.items(), commandBeforeCursorBegin());
0348 }
0349 
0350 CompletionStartParams CommandMode::activateCommandHistoryCompletion()
0351 {
0352     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->commandHistory()->items()), 0);
0353 }
0354 
0355 CompletionStartParams CommandMode::activateSedFindHistoryCompletion()
0356 {
0357     if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) {
0358         return CompletionStartParams::invalid();
0359     }
0360     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0361     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()),
0362                                                      parsedSedExpression.findBeginPos,
0363                                                      [this](const QString &completion) -> QString {
0364                                                          return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion));
0365                                                      });
0366 }
0367 
0368 CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion()
0369 {
0370     if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) {
0371         return CompletionStartParams::invalid();
0372     }
0373     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0374     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->replaceHistory()->items()),
0375                                                      parsedSedExpression.replaceBeginPos,
0376                                                      [this](const QString &completion) -> QString {
0377                                                          return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion));
0378                                                      });
0379 }
0380 
0381 KTextEditor::Command *CommandMode::queryCommand(const QString &cmd) const
0382 {
0383     // a command can be named ".*[\w\-]+" with the constrain that it must
0384     // contain at least one letter.
0385     int f = 0;
0386     bool b = false;
0387 
0388     // special case: '-' and '_' can be part of a command name, but if the
0389     // command is 's' (substitute), it should be considered the delimiter and
0390     // should not be counted as part of the command name
0391     if (cmd.length() >= 2 && cmd.at(0) == QLatin1Char('s') && (cmd.at(1) == QLatin1Char('-') || cmd.at(1) == QLatin1Char('_'))) {
0392         return m_cmdDict.value(QStringLiteral("s"));
0393     }
0394 
0395     for (; f < cmd.length(); f++) {
0396         if (cmd[f].isLetter()) {
0397             b = true;
0398         }
0399         if (b && (!cmd[f].isLetterOrNumber() && cmd[f] != QLatin1Char('-') && cmd[f] != QLatin1Char('_'))) {
0400             break;
0401         }
0402     }
0403     return m_cmdDict.value(cmd.left(f));
0404 }