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