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 | |
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 | 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 | |
68 | bool 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 | |
88 | void 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 | |
106 | void 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 | |
118 | CompletionStartParams 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 | |
136 | void 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 | |
162 | QString 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 | |
226 | QString CommandMode::withoutRangeExpression() |
227 | { |
228 | const QString originalCommand = m_edit->text(); |
229 | return originalCommand.mid(position: rangeExpression().length()); |
230 | } |
231 | |
232 | QString CommandMode::rangeExpression() |
233 | { |
234 | const QString command = m_edit->text(); |
235 | return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command); |
236 | } |
237 | |
238 | CommandMode::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 | |
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(position: parsedSedExpression.findBeginPos, n: parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1); |
289 | } |
290 | |
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(position: parsedSedExpression.replaceBeginPos, n: parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1); |
297 | } |
298 | |
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(pos: 0, n: parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(pos: parsedSedExpression.findEndPos + 1); |
306 | } |
307 | |
308 | QString CommandMode::withSedDelimiterEscaped(const QString &text) |
309 | { |
310 | ParsedSedExpression parsedSedExpression = parseAsSedExpression(); |
311 | QString delimiterEscaped = ensuredCharEscaped(originalString: text, charToEscape: parsedSedExpression.delimiter); |
312 | return delimiterEscaped; |
313 | } |
314 | |
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 | |
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 | |
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 | |
345 | CompletionStartParams CommandMode::activateCommandCompletion() |
346 | { |
347 | return CompletionStartParams::createModeSpecific(completions: m_cmdCompletion.items(), wordStartPos: commandBeforeCursorBegin()); |
348 | } |
349 | |
350 | CompletionStartParams CommandMode::activateCommandHistoryCompletion() |
351 | { |
352 | return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->commandHistory()->items()), wordStartPos: 0); |
353 | } |
354 | |
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(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 | |
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(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 | |
381 | 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(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 | |