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 | |
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 | |
67 | bool 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 | |
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 | |
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(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 | |
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 | |
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(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 | |
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(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 | |
225 | QString CommandMode::withoutRangeExpression() |
226 | { |
227 | const QString originalCommand = m_edit->text(); |
228 | return originalCommand.mid(position: rangeExpression().length()); |
229 | } |
230 | |
231 | QString CommandMode::rangeExpression() |
232 | { |
233 | const QString command = m_edit->text(); |
234 | return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command); |
235 | } |
236 | |
237 | CommandMode::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 | |
282 | QString 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 | |
290 | QString 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 | |
298 | QString 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 | |
307 | QString CommandMode::withSedDelimiterEscaped(const QString &text) |
308 | { |
309 | ParsedSedExpression parsedSedExpression = parseAsSedExpression(); |
310 | QString delimiterEscaped = ensuredCharEscaped(originalString: text, charToEscape: parsedSedExpression.delimiter); |
311 | return delimiterEscaped; |
312 | } |
313 | |
314 | bool 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 | |
321 | bool 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 | |
328 | int 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 | |
344 | CompletionStartParams CommandMode::activateCommandCompletion() |
345 | { |
346 | return CompletionStartParams::createModeSpecific(completions: m_cmdCompletion.items(), wordStartPos: commandBeforeCursorBegin()); |
347 | } |
348 | |
349 | CompletionStartParams CommandMode::activateCommandHistoryCompletion() |
350 | { |
351 | return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->commandHistory()->items()), wordStartPos: 0); |
352 | } |
353 | |
354 | CompletionStartParams 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 | |
367 | CompletionStartParams 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 | |
380 | KTextEditor::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 | |