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 }