KTextEditor

katesedcmd.cpp
1 /*
2  SPDX-FileCopyrightText: 2003-2005 Anders Lund <[email protected]>
3  SPDX-FileCopyrightText: 2001-2010 Christoph Cullmann <[email protected]>
4  SPDX-FileCopyrightText: 2001 Charles Samuels <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "katesedcmd.h"
10 
11 #include "katecmd.h"
12 #include "katedocument.h"
13 #include "kateglobal.h"
14 #include "katepartdebug.h"
15 #include "kateview.h"
16 
17 #include <KLocalizedString>
18 
19 #include <QDir>
20 #include <QRegularExpression>
21 #include <QUrl>
22 
23 KateCommands::SedReplace *KateCommands::SedReplace::m_instance = nullptr;
24 
25 static int backslashString(const QString &haystack, const QString &needle, int index)
26 {
27  int len = haystack.length();
28  int searchlen = needle.length();
29  bool evenCount = true;
30  while (index < len) {
31  if (haystack[index] == QLatin1Char('\\')) {
32  evenCount = !evenCount;
33  } else {
34  // isn't a slash
35  if (!evenCount) {
36  if (haystack.midRef(index, searchlen) == needle) {
37  return index - 1;
38  }
39  }
40  evenCount = true;
41  }
42  ++index;
43  }
44 
45  return -1;
46 }
47 
48 // exchange "\t" for the actual tab character, for example
49 static void exchangeAbbrevs(QString &str)
50 {
51  // the format is (findreplace)*[nullzero]
52  const char *magic = "a\x07t\tn\n";
53 
54  while (*magic) {
55  int index = 0;
56  char replace = magic[1];
57  while ((index = backslashString(str, QString(QChar::fromLatin1(*magic)), index)) != -1) {
58  str.replace(index, 2, QChar::fromLatin1(replace));
59  ++index;
60  }
61  ++magic;
62  ++magic;
63  }
64 }
65 
67 {
68  qCDebug(LOG_KTE) << "SedReplace::execCmd( " << cmd << " )";
69  if (r.isValid()) {
70  qCDebug(LOG_KTE) << "Range: " << r;
71  }
72 
73  int findBeginPos = -1;
74  int findEndPos = -1;
75  int replaceBeginPos = -1;
76  int replaceEndPos = -1;
77  QString delimiter;
78  if (!parse(cmd, delimiter, findBeginPos, findEndPos, replaceBeginPos, replaceEndPos)) {
79  return false;
80  }
81 
82  const QStringRef searchParamsString = cmd.midRef(cmd.lastIndexOf(delimiter));
83  const bool noCase = searchParamsString.contains(QLatin1Char('i'));
84  const bool repeat = searchParamsString.contains(QLatin1Char('g'));
85  const bool interactive = searchParamsString.contains(QLatin1Char('c'));
86 
87  QString find = cmd.mid(findBeginPos, findEndPos - findBeginPos + 1);
88  qCDebug(LOG_KTE) << "SedReplace: find =" << find;
89 
90  QString replace = cmd.mid(replaceBeginPos, replaceEndPos - replaceBeginPos + 1);
91  exchangeAbbrevs(replace);
92  qCDebug(LOG_KTE) << "SedReplace: replace =" << replace;
93 
94  if (find.isEmpty()) {
95  // Nothing to do.
96  return true;
97  }
98 
99  KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view);
100  KTextEditor::DocumentPrivate *doc = kateView->doc();
101  if (!doc) {
102  return false;
103  }
104  // Only current line ...
105  int startLine = kateView->cursorPosition().line();
106  int endLine = kateView->cursorPosition().line();
107  // ... unless a range was provided.
108  if (r.isValid()) {
109  startLine = r.start().line();
110  endLine = r.end().line();
111  }
112 
113  QSharedPointer<InteractiveSedReplacer> interactiveSedReplacer(new InteractiveSedReplacer(doc, find, replace, !noCase, !repeat, startLine, endLine));
114 
115  if (interactive) {
116  const bool hasInitialMatch = interactiveSedReplacer->currentMatch().isValid();
117  if (!hasInitialMatch) {
118  // Can't start an interactive sed replace if there is no initial match!
119  msg = interactiveSedReplacer->finalStatusReportMessage();
120  return false;
121  }
122  interactiveSedReplace(kateView, interactiveSedReplacer);
123  return true;
124  }
125 
126  interactiveSedReplacer->replaceAllRemaining();
127  msg = interactiveSedReplacer->finalStatusReportMessage();
128 
129  return true;
130 }
131 
132 bool KateCommands::SedReplace::interactiveSedReplace(KTextEditor::ViewPrivate *, QSharedPointer<InteractiveSedReplacer>)
133 {
134  qCDebug(LOG_KTE) << "Interactive sedreplace is only currently supported with Vi mode plus Vi emulated command bar.";
135  return false;
136 }
137 
138 bool KateCommands::SedReplace::parse(const QString &sedReplaceString, QString &destDelim, int &destFindBeginPos, int &destFindEndPos, int &destReplaceBeginPos, int &destReplaceEndPos)
139 {
140  // valid delimiters are all non-word, non-space characters plus '_'
141  QRegularExpression delim(QStringLiteral("^s\\s*([^\\w\\s]|_)"));
142  auto match = delim.match(sedReplaceString);
143  if (!match.hasMatch()) {
144  return false;
145  }
146 
147  const QString d = match.captured(1);
148  qCDebug(LOG_KTE) << "SedReplace: delimiter is '" << d << "'";
149 
150  QRegularExpression splitter(QStringLiteral("^s\\s*") + d + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)\\") + d + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)(\\") + d + QLatin1String("[igc]{0,3})?$"));
151  match = splitter.match(sedReplaceString);
152  if (!match.hasMatch()) {
153  return false;
154  }
155 
156  const QString find = match.captured(1);
157  const QString replace = match.captured(2);
158 
159  destDelim = d;
160  destFindBeginPos = match.capturedStart(1);
161  destFindEndPos = match.capturedStart(1) + find.length() - 1;
162  destReplaceBeginPos = match.capturedStart(2);
163  destReplaceEndPos = match.capturedStart(2) + replace.length() - 1;
164 
165  return true;
166 }
167 
168 KateCommands::SedReplace::InteractiveSedReplacer::InteractiveSedReplacer(KTextEditor::DocumentPrivate *doc, const QString &findPattern, const QString &replacePattern, bool caseSensitive, bool onlyOnePerLine, int startLine, int endLine)
169  : m_findPattern(findPattern)
170  , m_replacePattern(replacePattern)
171  , m_onlyOnePerLine(onlyOnePerLine)
172  , m_endLine(endLine)
173  , m_doc(doc)
174  , m_regExpSearch(doc)
175  , m_caseSensitive(caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive)
176  , m_numReplacementsDone(0)
177  , m_numLinesTouched(0)
178  , m_lastChangedLineNum(-1)
179 {
180  m_currentSearchPos = KTextEditor::Cursor(startLine, 0);
181 }
182 
183 KTextEditor::Range KateCommands::SedReplace::InteractiveSedReplacer::currentMatch()
184 {
185  QVector<KTextEditor::Range> matches = fullCurrentMatch();
186 
187  if (matches.isEmpty()) {
189  }
190 
191  if (matches.first().start().line() > m_endLine) {
193  }
194 
195  return matches.first();
196 }
197 
198 void KateCommands::SedReplace::InteractiveSedReplacer::skipCurrentMatch()
199 {
200  const KTextEditor::Range currentMatch = this->currentMatch();
201  m_currentSearchPos = currentMatch.end();
202  if (m_onlyOnePerLine && currentMatch.start().line() == m_currentSearchPos.line()) {
203  m_currentSearchPos = KTextEditor::Cursor(m_currentSearchPos.line() + 1, 0);
204  }
205 }
206 
207 void KateCommands::SedReplace::InteractiveSedReplacer::replaceCurrentMatch()
208 {
209  const KTextEditor::Range currentMatch = this->currentMatch();
210  const QString currentMatchText = m_doc->text(currentMatch);
211  const QString replacementText = replacementTextForCurrentMatch();
212 
213  m_doc->editBegin();
214  m_doc->removeText(currentMatch);
215  m_doc->insertText(currentMatch.start(), replacementText);
216  m_doc->editEnd();
217 
218  // Begin next search from directly after replacement.
219  if (!replacementText.contains(QLatin1Char('\n'))) {
220  const int moveChar = currentMatch.isEmpty() ? 1 : 0; // if the search was for \s*, make sure we advance a char
221  const int col = currentMatch.start().column() + replacementText.length() + moveChar;
222 
223  m_currentSearchPos = KTextEditor::Cursor(currentMatch.start().line(), col);
224  } else {
225  m_currentSearchPos = KTextEditor::Cursor(currentMatch.start().line() + replacementText.count(QLatin1Char('\n')), replacementText.length() - replacementText.lastIndexOf(QLatin1Char('\n')) - 1);
226  }
227  if (m_onlyOnePerLine) {
228  // Drop down to next line.
229  m_currentSearchPos = KTextEditor::Cursor(m_currentSearchPos.line() + 1, 0);
230  }
231 
232  // Adjust end line down by the number of new newlines just added, minus the number taken away.
233  m_endLine += replacementText.count(QLatin1Char('\n'));
234  m_endLine -= currentMatchText.count(QLatin1Char('\n'));
235 
236  m_numReplacementsDone++;
237  if (m_lastChangedLineNum != currentMatch.start().line()) {
238  // Counting "swallowed" lines as being "touched".
239  m_numLinesTouched += currentMatchText.count(QLatin1Char('\n')) + 1;
240  }
241  m_lastChangedLineNum = m_currentSearchPos.line();
242 }
243 
244 void KateCommands::SedReplace::InteractiveSedReplacer::replaceAllRemaining()
245 {
246  m_doc->editBegin();
247  while (currentMatch().isValid()) {
248  replaceCurrentMatch();
249  }
250  m_doc->editEnd();
251 }
252 
253 QString KateCommands::SedReplace::InteractiveSedReplacer::currentMatchReplacementConfirmationMessage()
254 {
255  return i18n("replace with %1?", replacementTextForCurrentMatch().replace(QLatin1Char('\n'), QLatin1String("\\n")));
256 }
257 
258 QString KateCommands::SedReplace::InteractiveSedReplacer::finalStatusReportMessage()
259 {
260  return i18ncp("%2 is the translation of the next message", "1 replacement done on %2", "%1 replacements done on %2", m_numReplacementsDone, i18ncp("substituted into the previous message", "1 line", "%1 lines", m_numLinesTouched));
261 }
262 
263 const QVector<KTextEditor::Range> KateCommands::SedReplace::InteractiveSedReplacer::fullCurrentMatch()
264 {
265  if (m_currentSearchPos > m_doc->documentEnd()) {
267  }
268 
270  if (m_caseSensitive == Qt::CaseInsensitive) {
272  }
273  return m_regExpSearch.search(m_findPattern, KTextEditor::Range(m_currentSearchPos, m_doc->documentEnd()), false /* search backwards */, options);
274 }
275 
276 QString KateCommands::SedReplace::InteractiveSedReplacer::replacementTextForCurrentMatch()
277 {
278  const QVector<KTextEditor::Range> captureRanges = fullCurrentMatch();
279  QStringList captureTexts;
280  for (KTextEditor::Range captureRange : captureRanges) {
281  captureTexts << m_doc->text(captureRange);
282  }
283  const QString replacementText = m_regExpSearch.buildReplacement(m_replacePattern, captureTexts, 0);
284  return replacementText;
285 }
QRegularExpressionMatch match(const QString &subject, int offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
bool exec(class KTextEditor::View *view, const QString &cmd, QString &errorMsg, const KTextEditor::Range &r) override
Execute command.
Definition: katesedcmd.cpp:66
constexpr bool isEmpty() const Q_DECL_NOEXCEPT
Returns true if this range contains no characters, ie.
Support vim/sed style search and replace.
Definition: katesedcmd.h:34
T & first()
The Cursor represents a position in a Document.
Definition: cursor.h:71
constexpr bool isValid() const Q_DECL_NOEXCEPT
Validity check.
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QChar fromLatin1(char c)
CaseSensitive
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
constexpr int column() const Q_DECL_NOEXCEPT
Retrieve the column on which this cursor is situated.
Definition: cursor.h:203
constexpr Cursor start() const Q_DECL_NOEXCEPT
Get the start position of this range.
const QList< QKeySequence > & replace()
An object representing a section of text, from one Cursor to another.
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QStringRef midRef(int position, int n) const const
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString & replace(int position, int n, QChar after)
QString mid(int position, int n) const const
bool isEmpty() const const
constexpr Cursor end() const Q_DECL_NOEXCEPT
Get the end position of this range.
int count() const const
constexpr int line() const Q_DECL_NOEXCEPT
Retrieve the line on which this cursor is situated.
Definition: cursor.h:185
int length() const const
static constexpr Range invalid() Q_DECL_NOEXCEPT
Returns an invalid range.
A text widget with KXMLGUIClient that represents a Document.
Definition: view.h:143
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.
Definition: katesedcmd.cpp:138
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Thu Sep 17 2020 22:57:35 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.