KTextEditor

commandmode.cpp
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{
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 cmds.push_back(KateCommands::EditingCommands::self());
52
53 for (KateCommandLineScript *cmd : KateScriptManager::self()->commandLineScripts()) {
54 cmds.push_back(cmd);
55 }
56
57 for (KTextEditor::Command *cmd : std::as_const(cmds)) {
58 QStringList l = cmd->cmds();
59
60 for (int z = 0; z < l.count(); z++) {
61 m_cmdDict.insert(l[z], cmd);
62 }
63
64 m_cmdCompletion.insertItems(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(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(originalFindTerm);
143 const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(convertedFindTerm);
144 viInputModeManager()->globalState()->searchHistory()->append(originalFindTerm);
145 const QString replaceTerm = sedReplaceTerm();
146 viInputModeManager()->globalState()->replaceHistory()->append(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(commandResponseMessage);
157 }
158 }
159 viInputModeManager()->globalState()->commandHistory()->append(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(n);
177
178 KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(cmd, 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(), 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.matchView(QStringView(cmd).left(cmd.indexOf(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(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(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
283QString 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
291QString 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
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(0, parsedSedExpression.findBeginPos) + newFindTerm + strView.mid(parsedSedExpression.findEndPos + 1);
306}
307
308QString CommandMode::withSedDelimiterEscaped(const QString &text)
309{
310 ParsedSedExpression parsedSedExpression = parseAsSedExpression();
311 QString delimiterEscaped = ensuredCharEscaped(text, 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(m_cmdCompletion.items(), commandBeforeCursorBegin());
348}
349
350CompletionStartParams CommandMode::activateCommandHistoryCompletion()
351{
352 return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->commandHistory()->items()), 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(reversed(viInputModeManager()->globalState()->searchHistory()->items()),
362 parsedSedExpression.findBeginPos,
363 [this](const QString &completion) -> QString {
364 return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(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(reversed(viInputModeManager()->globalState()->replaceHistory()->items()),
375 parsedSedExpression.replaceBeginPos,
376 [this](const QString &completion) -> QString {
377 return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(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(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}
QStringList items() const
An Editor command line command.
virtual bool supportsRange(const QString &cmd)
Find out if a given command can act on a range.
virtual bool exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &range=KTextEditor::Range::invalid())=0
Execute the command for the given view and cmd string.
An object representing a section of text, from one Cursor to another.
constexpr bool isValid() const noexcept
Validity check.
A specialized class for scripts that are of type ScriptType::Indentation.
static bool parse(const QString &sedReplaceString, QString &destDelim, int &destFindBeginPos, int &destFindEndPos, int &destReplaceBeginPos, int &destReplaceEndPos)
Parses sedReplaceString to see if it is a valid sed replace expression (e.g.
A KateViewBarWidget that attempts to emulate some of the features of Vim's own command bar,...
QString i18n(const char *text, const TYPE &arg...)
T value(const Key &key) const const
void insert(const QString &newText)
void setSelection(int start, int length)
qsizetype count() const const
void push_back(parameter_type value)
const QChar at(qsizetype position) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QTextStream & left(QTextStream &stream)
void keyEvent(KeyAction action, QWidget *widget, Qt::Key key, Qt::KeyboardModifiers modifier, int delay)
void showText(const QPoint &pos, const QString &text, QWidget *w)
void setFocus()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 4 2024 12:03:01 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.