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 }