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
52 for (KateCommandLineScript *cmd : KateScriptManager::self()->commandLineScripts()) {
53 cmds.push_back(t: cmd);
54 }
55
56 for (KTextEditor::Command *cmd : std::as_const(t&: cmds)) {
57 QStringList l = cmd->cmds();
58
59 for (int z = 0; z < l.count(); z++) {
60 m_cmdDict.insert(key: l[z], value: cmd);
61 }
62
63 m_cmdCompletion.insertItems(items: l);
64 }
65}
66
67bool CommandMode::handleKeyPress(const QKeyEvent *keyEvent)
68{
69 if (keyEvent->modifiers() == CONTROL_MODIFIER && (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
87void 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
105void 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(historyItem: 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
117CompletionStartParams 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
135void 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(vimRegexPattern: originalFindTerm);
142 const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(newFindTerm: convertedFindTerm);
143 viInputModeManager()->globalState()->searchHistory()->append(historyItem: originalFindTerm);
144 const QString replaceTerm = sedReplaceTerm();
145 viInputModeManager()->globalState()->replaceHistory()->append(historyItem: 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(exitStatusMessage: commandResponseMessage);
156 }
157 }
158 viInputModeManager()->globalState()->commandHistory()->append(historyItem: m_edit->text());
159}
160
161QString 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(position: n);
176
177 KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(command: cmd, destTransformedCommand&: cmd);
178
179 if (cmd.length() > 0) {
180 KTextEditor::Command *p = queryCommand(cmd);
181 if (p) {
182 if (p == Commands::self() || p == SedReplace::self()) {
183 Commands::self()->setViInputModeManager(viInputModeManager());
184 SedReplace::self()->setViInputModeManager(viInputModeManager());
185 }
186
187 // The following commands changes the focus themselves, so bar should be hidden before execution.
188
189 // We got a range and a valid command, but the command does not support ranges.
190 if (range.isValid() && !p->supportsRange(cmd)) {
191 commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd);
192 } else {
193 if (p->exec(view: view(), cmd, msg&: commandResponseMessage, range)) {
194 if (commandResponseMessage.length() > 0) {
195 commandResponseMessage = i18n("Success: ") + commandResponseMessage;
196 }
197 } else {
198 if (commandResponseMessage.length() > 0) {
199 if (commandResponseMessage.contains(c: QLatin1Char('\n'))) {
200 // multiline error, use widget with more space
201 QWhatsThis::showText(pos: emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), text: commandResponseMessage);
202 }
203 } else {
204 commandResponseMessage = i18n("Command \"%1\" failed.", cmd);
205 }
206 }
207 }
208 } else {
209 commandResponseMessage = i18n("No such command: \"%1\"", cmd);
210 }
211 }
212
213 // the following commands change the focus themselves
214 static const QRegularExpression reCmds(
215 QStringLiteral("^(?:buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst"
216 "|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$"));
217 if (!reCmds.matchView(subjectView: QStringView(cmd).left(n: cmd.indexOf(c: QLatin1Char(' ')))).hasMatch()) {
218 view()->setFocus();
219 }
220
221 viInputModeManager()->reset();
222 return commandResponseMessage;
223}
224
225QString CommandMode::withoutRangeExpression()
226{
227 const QString originalCommand = m_edit->text();
228 return originalCommand.mid(position: rangeExpression().length());
229}
230
231QString CommandMode::rangeExpression()
232{
233 const QString command = m_edit->text();
234 return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command);
235}
236
237CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression()
238{
239 const QString commandWithoutRangeExpression = withoutRangeExpression();
240 ParsedSedExpression parsedSedExpression;
241 QString delimiter;
242 parsedSedExpression.parsedSuccessfully = SedReplace::parse(sedReplaceString: commandWithoutRangeExpression,
243 destDelim&: delimiter,
244 destFindBeginPos&: parsedSedExpression.findBeginPos,
245 destFindEndPos&: parsedSedExpression.findEndPos,
246 destReplaceBeginPos&: parsedSedExpression.replaceBeginPos,
247 destReplaceEndPos&: parsedSedExpression.replaceEndPos);
248 if (parsedSedExpression.parsedSuccessfully) {
249 parsedSedExpression.delimiter = delimiter.at(i: 0);
250 if (parsedSedExpression.replaceBeginPos == -1) {
251 if (parsedSedExpression.findBeginPos != -1) {
252 // The replace term was empty, and a quirk of the regex used is that replaceBeginPos will be -1.
253 // It's actually the position after the first occurrence of the delimiter after the end of the find pos.
254 parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(s: delimiter, from: parsedSedExpression.findEndPos) + 1;
255 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
256 } else {
257 // Both find and replace terms are empty; replace term is at the third occurrence of the delimiter.
258 parsedSedExpression.replaceBeginPos = 0;
259 for (int delimiterCount = 1; delimiterCount <= 3; delimiterCount++) {
260 parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(s: delimiter, from: parsedSedExpression.replaceBeginPos + 1);
261 }
262 parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1;
263 }
264 }
265 if (parsedSedExpression.findBeginPos == -1) {
266 // The find term was empty, and a quirk of the regex used is that findBeginPos will be -1.
267 // It's actually the position after the first occurrence of the delimiter.
268 parsedSedExpression.findBeginPos = commandWithoutRangeExpression.indexOf(s: delimiter) + 1;
269 parsedSedExpression.findEndPos = parsedSedExpression.findBeginPos - 1;
270 }
271 }
272
273 if (parsedSedExpression.parsedSuccessfully) {
274 parsedSedExpression.findBeginPos += rangeExpression().length();
275 parsedSedExpression.findEndPos += rangeExpression().length();
276 parsedSedExpression.replaceBeginPos += rangeExpression().length();
277 parsedSedExpression.replaceEndPos += rangeExpression().length();
278 }
279 return parsedSedExpression;
280}
281
282QString CommandMode::sedFindTerm()
283{
284 const QString command = m_edit->text();
285 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
286 Q_ASSERT(parsedSedExpression.parsedSuccessfully);
287 return command.mid(position: parsedSedExpression.findBeginPos, n: parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1);
288}
289
290QString CommandMode::sedReplaceTerm()
291{
292 const QString command = m_edit->text();
293 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
294 Q_ASSERT(parsedSedExpression.parsedSuccessfully);
295 return command.mid(position: parsedSedExpression.replaceBeginPos, n: parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1);
296}
297
298QString CommandMode::withSedFindTermReplacedWith(const QString &newFindTerm)
299{
300 const QString command = m_edit->text();
301 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
302 Q_ASSERT(parsedSedExpression.parsedSuccessfully);
303 const QStringView strView(command);
304 return strView.mid(pos: 0, n: parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(pos: parsedSedExpression.findEndPos + 1);
305}
306
307QString CommandMode::withSedDelimiterEscaped(const QString &text)
308{
309 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
310 QString delimiterEscaped = ensuredCharEscaped(originalString: text, charToEscape: parsedSedExpression.delimiter);
311 return delimiterEscaped;
312}
313
314bool CommandMode::isCursorInFindTermOfSed()
315{
316 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
317 return parsedSedExpression.parsedSuccessfully
318 && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1);
319}
320
321bool CommandMode::isCursorInReplaceTermOfSed()
322{
323 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
324 return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos
325 && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1;
326}
327
328int CommandMode::commandBeforeCursorBegin()
329{
330 const QString textWithoutRangeExpression = withoutRangeExpression();
331 const int cursorPositionWithoutRangeExpression = m_edit->cursorPosition() - rangeExpression().length();
332 int commandBeforeCursorBegin = cursorPositionWithoutRangeExpression - 1;
333 while (commandBeforeCursorBegin >= 0
334 && (textWithoutRangeExpression[commandBeforeCursorBegin].isLetterOrNumber()
335 || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('_')
336 || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('-'))) {
337 commandBeforeCursorBegin--;
338 }
339 commandBeforeCursorBegin++;
340 commandBeforeCursorBegin += rangeExpression().length();
341 return commandBeforeCursorBegin;
342}
343
344CompletionStartParams CommandMode::activateCommandCompletion()
345{
346 return CompletionStartParams::createModeSpecific(completions: m_cmdCompletion.items(), wordStartPos: commandBeforeCursorBegin());
347}
348
349CompletionStartParams CommandMode::activateCommandHistoryCompletion()
350{
351 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->commandHistory()->items()), wordStartPos: 0);
352}
353
354CompletionStartParams CommandMode::activateSedFindHistoryCompletion()
355{
356 if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) {
357 return CompletionStartParams::invalid();
358 }
359 CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
360 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->searchHistory()->items()),
361 wordStartPos: parsedSedExpression.findBeginPos,
362 completionTransform: [this](const QString &completion) -> QString {
363 return withCaseSensitivityMarkersStripped(originalSearchTerm: withSedDelimiterEscaped(text: completion));
364 });
365}
366
367CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion()
368{
369 if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) {
370 return CompletionStartParams::invalid();
371 }
372 CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression();
373 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->replaceHistory()->items()),
374 wordStartPos: parsedSedExpression.replaceBeginPos,
375 completionTransform: [this](const QString &completion) -> QString {
376 return withCaseSensitivityMarkersStripped(originalSearchTerm: withSedDelimiterEscaped(text: completion));
377 });
378}
379
380KTextEditor::Command *CommandMode::queryCommand(const QString &cmd) const
381{
382 // a command can be named ".*[\w\-]+" with the constrain that it must
383 // contain at least one letter.
384 int f = 0;
385 bool b = false;
386
387 // special case: '-' and '_' can be part of a command name, but if the
388 // command is 's' (substitute), it should be considered the delimiter and
389 // should not be counted as part of the command name
390 if (cmd.length() >= 2 && cmd.at(i: 0) == QLatin1Char('s') && (cmd.at(i: 1) == QLatin1Char('-') || cmd.at(i: 1) == QLatin1Char('_'))) {
391 return m_cmdDict.value(QStringLiteral("s"));
392 }
393
394 for (; f < cmd.length(); f++) {
395 if (cmd[f].isLetter()) {
396 b = true;
397 }
398 if (b && (!cmd[f].isLetterOrNumber() && cmd[f] != QLatin1Char('-') && cmd[f] != QLatin1Char('_'))) {
399 break;
400 }
401 }
402 return m_cmdDict.value(key: cmd.left(n: f));
403}
404

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