KTextEditor

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

KDE's Doxygen guidelines are available online.