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
31using namespace KateVi;
32
33CommandMode::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 QList<KTextEditor::Command *> cmds;
46 cmds.push_back(t: KateCommands::CoreCommands::self());
47 cmds.push_back(t: Commands::self());
48 cmds.push_back(t: AppCommands::self());
49 cmds.push_back(t: SedReplace::self());
50 cmds.push_back(t: BufferCommands::self());
51 cmds.push_back(t: KateCommands::EditingCommands::self());
52
53 for (KateCommandLineScript *cmd : KateScriptManager::self()->commandLineScripts()) {
54 cmds.push_back(t: cmd);
55 }
56
57 for (KTextEditor::Command *cmd : std::as_const(t&: cmds)) {
58 QStringList l = cmd->cmds();
59
60 for (int z = 0; z < l.count(); z++) {
61 m_cmdDict.insert(key: l[z], value: cmd);
62 }
63
64 m_cmdCompletion.insertItems(items: l);
65 }
66}
67
68bool CommandMode::handleKeyPress(const QKeyEvent *keyEvent)
69{
70 if (keyEvent->modifiers() == CONTROL_MODIFIER && (keyEvent->key() == Qt::Key_D || keyEvent->key() == Qt::Key_F)) {
71 CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
72 if (parsedSedExpression.parsedSuccessfully) {
73 const bool clearFindTerm = (keyEvent->key() == Qt::Key_D);
74 if (clearFindTerm) {
75 m_edit->setSelection(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
76 m_edit->insert(QString());
77 } else {
78 // Clear replace term.
79 m_edit->setSelection(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
80 m_edit->insert(QString());
81 }
82 }
83 return true;
84 }
85 return false;
86}
87
88void CommandMode::editTextChanged(const QString &newText)
89{
90 Q_UNUSED(newText); // We read the current text from m_edit.
91 if (m_completer->isCompletionActive()) {
92 return;
93 }
94 // Command completion doesn't need to be manually invoked.
95 if (!withoutRangeExpression().isEmpty() && !m_completer->isNextTextChangeDueToCompletionChange()) {
96 // ... However, command completion mode should not be automatically invoked if this is not the current leading
97 // word in the text edit (it gets annoying if completion pops up after ":s/se" etc).
98 const bool commandBeforeCursorIsLeading = (commandBeforeCursorBegin() == rangeExpression().length());
99 if (commandBeforeCursorIsLeading) {
100 CompletionStartParams completionStartParams = activateCommandCompletion();
101 startCompletion(completionStartParams);
102 }
103 }
104}
105
106void CommandMode::deactivate(bool wasAborted)
107{
108 if (wasAborted) {
109 // Appending the command to the history when it is executed is handled elsewhere; we can't
110 // do it inside closed() as we may still be showing the command response display.
111 viInputModeManager()->globalState()->commandHistory()->append(historyItem: m_edit->text());
112 // With Vim, aborting a command returns us to Normal mode, even if we were in Visual Mode.
113 // If we switch from Visual to Normal mode, we need to clear the selection.
114 view()->clearSelection();
115 }
116}
117
118CompletionStartParams CommandMode::completionInvoked(Completer::CompletionInvocation invocationType)
119{
120 CompletionStartParams completionStartParams;
121 if (invocationType == Completer::CompletionInvocation::ExtraContext) {
122 if (isCursorInFindTermOfSed()) {
123 completionStartParams = activateSedFindHistoryCompletion();
124 } else if (isCursorInReplaceTermOfSed()) {
125 completionStartParams = activateSedReplaceHistoryCompletion();
126 } else {
127 completionStartParams = activateCommandHistoryCompletion();
128 }
129 } else {
130 // Normal context, so boring, ordinary History completion.
131 completionStartParams = activateCommandHistoryCompletion();
132 }
133 return completionStartParams;
134}
135
136void CommandMode::completionChosen()
137{
138 QString commandToExecute = m_edit->text();
139 CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
140 if (parsedSedExpression.parsedSuccessfully) {
141 const QString originalFindTerm = sedFindTerm();
142 const QString convertedFindTerm = vimRegexToQtRegexPattern(vimRegexPattern: originalFindTerm);
143 const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(newFindTerm: convertedFindTerm);
144 viInputModeManager()->globalState()->searchHistory()->append(historyItem: originalFindTerm);
145 const QString replaceTerm = sedReplaceTerm();
146 viInputModeManager()->globalState()->replaceHistory()->append(historyItem: replaceTerm);
147 commandToExecute = commandWithSedSearchRegexConverted;
148 }
149
150 const QString commandResponseMessage = executeCommand(commandToExecute);
151 // Don't close the bar if executing the command switched us to Interactive Sed Replace mode.
152 if (!m_interactiveSedReplaceMode->isActive()) {
153 if (commandResponseMessage.isEmpty()) {
154 emulatedCommandBar()->hideMe();
155 } else {
156 closeWithStatusMessage(exitStatusMessage: commandResponseMessage);
157 }
158 }
159 viInputModeManager()->globalState()->commandHistory()->append(historyItem: m_edit->text());
160}
161
162QString CommandMode::executeCommand(const QString &commandToExecute)
163{
164 // Silently ignore leading space characters and colon characters (for vi-heads).
165 uint n = 0;
166 const uint textlen = commandToExecute.length();
167 while ((n < textlen) && commandToExecute[n].isSpace()) {
168 n++;
169 }
170
171 if (n >= textlen) {
172 return QString();
173 }
174
175 QString commandResponseMessage;
176 QString cmd = commandToExecute.mid(position: n);
177
178 KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(command: cmd, destTransformedCommand&: cmd);
179
180 if (cmd.length() > 0) {
181 KTextEditor::Command *p = queryCommand(cmd);
182 if (p) {
183 if (p == Commands::self() || p == SedReplace::self()) {
184 Commands::self()->setViInputModeManager(viInputModeManager());
185 SedReplace::self()->setViInputModeManager(viInputModeManager());
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: view(), cmd, msg&: commandResponseMessage, range)) {
195 if (commandResponseMessage.length() > 0) {
196 commandResponseMessage = i18n("Success: ") + commandResponseMessage;
197 }
198 } else {
199 if (commandResponseMessage.length() > 0) {
200 if (commandResponseMessage.contains(c: QLatin1Char('\n'))) {
201 // multiline error, use widget with more space
202 QWhatsThis::showText(pos: emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), text: 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.matchView(subjectView: QStringView(cmd).left(n: cmd.indexOf(ch: QLatin1Char(' ')))).hasMatch()) {
219 view()->setFocus();
220 }
221
222 viInputModeManager()->reset();
223 return commandResponseMessage;
224}
225
226QString CommandMode::withoutRangeExpression()
227{
228 const QString originalCommand = m_edit->text();
229 return originalCommand.mid(position: rangeExpression().length());
230}
231
232QString CommandMode::rangeExpression()
233{
234 const QString command = m_edit->text();
235 return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command);
236}
237
238CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression()
239{
240 const QString commandWithoutRangeExpression = withoutRangeExpression();
241 ParsedSedExpression parsedSedExpression;
242 QString delimiter;
243 parsedSedExpression.parsedSuccessfully = SedReplace::parse(sedReplaceString: commandWithoutRangeExpression,
244 destDelim&: delimiter,
245 destFindBeginPos&: parsedSedExpression.findBeginPos,
246 destFindEndPos&: parsedSedExpression.findEndPos,
247 destReplaceBeginPos&: parsedSedExpression.replaceBeginPos,
248 destReplaceEndPos&: parsedSedExpression.replaceEndPos);
249 if (parsedSedExpression.parsedSuccessfully) {
250 parsedSedExpression.delimiter = delimiter.at(i: 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(s: delimiter, from: 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(s: delimiter, from: 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(s: 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
283QString CommandMode::sedFindTerm()
284{
285 const QString command = m_edit->text();
286 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
287 Q_ASSERT(parsedSedExpression.parsedSuccessfully);
288 return command.mid(position: parsedSedExpression.findBeginPos, n: parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
289}
290
291QString CommandMode::sedReplaceTerm()
292{
293 const QString command = m_edit->text();
294 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
295 Q_ASSERT(parsedSedExpression.parsedSuccessfully);
296 return command.mid(position: parsedSedExpression.replaceBeginPos, n: parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
297}
298
299QString 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(pos: 0, n: parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(pos: parsedSedExpression.findEndPos + 1);
306}
307
308QString CommandMode::withSedDelimiterEscaped(const QString &text)
309{
310 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
311 QString delimiterEscaped = ensuredCharEscaped(originalString: text, charToEscape: parsedSedExpression.delimiter);
312 return delimiterEscaped;
313}
314
315bool 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
322bool 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
329int 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
345CompletionStartParams CommandMode::activateCommandCompletion()
346{
347 return CompletionStartParams::createModeSpecific(completions: m_cmdCompletion.items(), wordStartPos: commandBeforeCursorBegin());
348}
349
350CompletionStartParams CommandMode::activateCommandHistoryCompletion()
351{
352 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->commandHistory()->items()), wordStartPos: 0);
353}
354
355CompletionStartParams CommandMode::activateSedFindHistoryCompletion()
356{
357 if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) {
358 return CompletionStartParams::invalid();
359 }
360 CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
361 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->searchHistory()->items()),
362 wordStartPos: parsedSedExpression.findBeginPos,
363 completionTransform: [this](const QString &completion) -> QString {
364 return withCaseSensitivityMarkersStripped(originalSearchTerm: withSedDelimiterEscaped(text: completion));
365 });
366}
367
368CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion()
369{
370 if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) {
371 return CompletionStartParams::invalid();
372 }
373 CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
374 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->replaceHistory()->items()),
375 wordStartPos: parsedSedExpression.replaceBeginPos,
376 completionTransform: [this](const QString &completion) -> QString {
377 return withCaseSensitivityMarkersStripped(originalSearchTerm: withSedDelimiterEscaped(text: completion));
378 });
379}
380
381KTextEditor::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(i: 0) == QLatin1Char('s') && (cmd.at(i: 1) == QLatin1Char('-') || cmd.at(i: 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(key: cmd.left(n: f));
404}
405

source code of ktexteditor/src/vimode/emulatedcommandbar/commandmode.cpp