File indexing completed on 2024-05-05 03:58:35

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     QList<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 (KateCommandLineScript *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             if (p == Commands::self() || p == SedReplace::self()) {
0183                 Commands::self()->setViInputModeManager(viInputModeManager());
0184                 SedReplace::self()->setViInputModeManager(viInputModeManager());
0185             }
0186 
0187             // The following commands changes the focus themselves, so bar should be hidden before execution.
0188 
0189             // We got a range and a valid command, but the command does not support ranges.
0190             if (range.isValid() && !p->supportsRange(cmd)) {
0191                 commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd);
0192             } else {
0193                 if (p->exec(view(), cmd, commandResponseMessage, range)) {
0194                     if (commandResponseMessage.length() > 0) {
0195                         commandResponseMessage = i18n("Success: ") + commandResponseMessage;
0196                     }
0197                 } else {
0198                     if (commandResponseMessage.length() > 0) {
0199                         if (commandResponseMessage.contains(QLatin1Char('\n'))) {
0200                             // multiline error, use widget with more space
0201                             QWhatsThis::showText(emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), commandResponseMessage);
0202                         }
0203                     } else {
0204                         commandResponseMessage = i18n("Command \"%1\" failed.", cmd);
0205                     }
0206                 }
0207             }
0208         } else {
0209             commandResponseMessage = i18n("No such command: \"%1\"", cmd);
0210         }
0211     }
0212 
0213     // the following commands change the focus themselves
0214     static const QRegularExpression reCmds(
0215         QStringLiteral("^(?:buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst"
0216                        "|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$"));
0217     if (!reCmds.matchView(QStringView(cmd).left(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) {
0218         view()->setFocus();
0219     }
0220 
0221     viInputModeManager()->reset();
0222     return commandResponseMessage;
0223 }
0224 
0225 QString CommandMode::withoutRangeExpression()
0226 {
0227     const QString originalCommand = m_edit->text();
0228     return originalCommand.mid(rangeExpression().length());
0229 }
0230 
0231 QString CommandMode::rangeExpression()
0232 {
0233     const QString command = m_edit->text();
0234     return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command);
0235 }
0236 
0237 CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression()
0238 {
0239     const QString commandWithoutRangeExpression = withoutRangeExpression();
0240     ParsedSedExpression parsedSedExpression;
0241     QString delimiter;
0242     parsedSedExpression.parsedSuccessfully = SedReplace::parse(commandWithoutRangeExpression,
0243                                                                delimiter,
0244                                                                parsedSedExpression.findBeginPos,
0245                                                                parsedSedExpression.findEndPos,
0246                                                                parsedSedExpression.replaceBeginPos,
0247                                                                parsedSedExpression.replaceEndPos);
0248     if (parsedSedExpression.parsedSuccessfully) {
0249         parsedSedExpression.delimiter = delimiter.at(0);
0250         if (parsedSedExpression.replaceBeginPos == -1) {
0251             if (parsedSedExpression.findBeginPos != -1) {
0252                 // The replace term was empty, and a quirk of the regex used is that replaceBeginPos will be -1.
0253                 // It's actually the position after the first occurrence of the delimiter after the end of the find pos.
0254                 parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.findEndPos) + 1;
0255                 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
0256             } else {
0257                 // Both find and replace terms are empty; replace term is at the third occurrence of the delimiter.
0258                 parsedSedExpression.replaceBeginPos = 0;
0259                 for (int delimiterCount = 1; delimiterCount <= 3; delimiterCount++) {
0260                     parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.replaceBeginPos + 1);
0261                 }
0262                 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
0263             }
0264         }
0265         if (parsedSedExpression.findBeginPos == -1) {
0266             // The find term was empty, and a quirk of the regex used is that findBeginPos will be -1.
0267             // It's actually the position after the first occurrence of the delimiter.
0268             parsedSedExpression.findBeginPos = commandWithoutRangeExpression.indexOf(delimiter) + 1;
0269             parsedSedExpression.findEndPos = parsedSedExpression.findBeginPos - 1;
0270         }
0271     }
0272 
0273     if (parsedSedExpression.parsedSuccessfully) {
0274         parsedSedExpression.findBeginPos += rangeExpression().length();
0275         parsedSedExpression.findEndPos += rangeExpression().length();
0276         parsedSedExpression.replaceBeginPos += rangeExpression().length();
0277         parsedSedExpression.replaceEndPos += rangeExpression().length();
0278     }
0279     return parsedSedExpression;
0280 }
0281 
0282 QString CommandMode::sedFindTerm()
0283 {
0284     const QString command = m_edit->text();
0285     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0286     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
0287     return command.mid(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
0288 }
0289 
0290 QString CommandMode::sedReplaceTerm()
0291 {
0292     const QString command = m_edit->text();
0293     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0294     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
0295     return command.mid(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
0296 }
0297 
0298 QString CommandMode::withSedFindTermReplacedWith(const QString &newFindTerm)
0299 {
0300     const QString command = m_edit->text();
0301     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0302     Q_ASSERT(parsedSedExpression.parsedSuccessfully);
0303     const QStringView strView(command);
0304     return strView.mid(0, parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(parsedSedExpression.findEndPos + 1);
0305 }
0306 
0307 QString CommandMode::withSedDelimiterEscaped(const QString &text)
0308 {
0309     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0310     QString delimiterEscaped = ensuredCharEscaped(text, parsedSedExpression.delimiter);
0311     return delimiterEscaped;
0312 }
0313 
0314 bool CommandMode::isCursorInFindTermOfSed()
0315 {
0316     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0317     return parsedSedExpression.parsedSuccessfully
0318         && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1);
0319 }
0320 
0321 bool CommandMode::isCursorInReplaceTermOfSed()
0322 {
0323     ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0324     return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos
0325         && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1;
0326 }
0327 
0328 int CommandMode::commandBeforeCursorBegin()
0329 {
0330     const QString textWithoutRangeExpression = withoutRangeExpression();
0331     const int cursorPositionWithoutRangeExpression = m_edit->cursorPosition() - rangeExpression().length();
0332     int commandBeforeCursorBegin = cursorPositionWithoutRangeExpression - 1;
0333     while (commandBeforeCursorBegin >= 0
0334            && (textWithoutRangeExpression[commandBeforeCursorBegin].isLetterOrNumber()
0335                || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('_')
0336                || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('-'))) {
0337         commandBeforeCursorBegin--;
0338     }
0339     commandBeforeCursorBegin++;
0340     commandBeforeCursorBegin += rangeExpression().length();
0341     return commandBeforeCursorBegin;
0342 }
0343 
0344 CompletionStartParams CommandMode::activateCommandCompletion()
0345 {
0346     return CompletionStartParams::createModeSpecific(m_cmdCompletion.items(), commandBeforeCursorBegin());
0347 }
0348 
0349 CompletionStartParams CommandMode::activateCommandHistoryCompletion()
0350 {
0351     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->commandHistory()->items()), 0);
0352 }
0353 
0354 CompletionStartParams CommandMode::activateSedFindHistoryCompletion()
0355 {
0356     if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) {
0357         return CompletionStartParams::invalid();
0358     }
0359     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0360     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()),
0361                                                      parsedSedExpression.findBeginPos,
0362                                                      [this](const QString &completion) -> QString {
0363                                                          return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion));
0364                                                      });
0365 }
0366 
0367 CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion()
0368 {
0369     if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) {
0370         return CompletionStartParams::invalid();
0371     }
0372     CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
0373     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->replaceHistory()->items()),
0374                                                      parsedSedExpression.replaceBeginPos,
0375                                                      [this](const QString &completion) -> QString {
0376                                                          return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion));
0377                                                      });
0378 }
0379 
0380 KTextEditor::Command *CommandMode::queryCommand(const QString &cmd) const
0381 {
0382     // a command can be named ".*[\w\-]+" with the constrain that it must
0383     // contain at least one letter.
0384     int f = 0;
0385     bool b = false;
0386 
0387     // special case: '-' and '_' can be part of a command name, but if the
0388     // command is 's' (substitute), it should be considered the delimiter and
0389     // should not be counted as part of the command name
0390     if (cmd.length() >= 2 && cmd.at(0) == QLatin1Char('s') && (cmd.at(1) == QLatin1Char('-') || cmd.at(1) == QLatin1Char('_'))) {
0391         return m_cmdDict.value(QStringLiteral("s"));
0392     }
0393 
0394     for (; f < cmd.length(); f++) {
0395         if (cmd[f].isLetter()) {
0396             b = true;
0397         }
0398         if (b && (!cmd[f].isLetterOrNumber() && cmd[f] != QLatin1Char('-') && cmd[f] != QLatin1Char('_'))) {
0399             break;
0400         }
0401     }
0402     return m_cmdDict.value(cmd.left(f));
0403 }