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(pos: index, n: 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(haystack: str, needle: QString(QChar::fromLatin1(c: *magic)), index)) != -1) {
57 str.replace(i: index, len: 2, after: QChar::fromLatin1(c: replace));
58 ++index;
59 }
60 ++magic;
61 ++magic;
62 }
63}
64
65bool KateCommands::SedReplace::exec(class KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &r)
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(sedReplaceString: cmd, destDelim&: delimiter, destFindBeginPos&: findBeginPos, destFindEndPos&: findEndPos, destReplaceBeginPos&: replaceBeginPos, destReplaceEndPos&: replaceEndPos)) {
78 return false;
79 }
80
81 const QStringView searchParamsString = QStringView(cmd).mid(pos: cmd.lastIndexOf(s: delimiter));
82 const bool noCase = searchParamsString.contains(c: QLatin1Char('i'));
83 const bool repeat = searchParamsString.contains(c: QLatin1Char('g'));
84 const bool interactive = searchParamsString.contains(c: QLatin1Char('c'));
85
86 QString find = cmd.mid(position: findBeginPos, n: findEndPos - findBeginPos + 1);
87 qCDebug(LOG_KTE) << "SedReplace: find =" << find;
88
89 QString replace = cmd.mid(position: replaceBeginPos, n: replaceEndPos - replaceBeginPos + 1);
90 exchangeAbbrevs(str&: 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, interactiveSedReplace: 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(subject: sedReplaceString);
147 if (!match.hasMatch()) {
148 return false;
149 }
150
151 const QString d = match.captured(nth: 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})?$"),
156 QRegularExpression::UseUnicodePropertiesOption);
157 match = splitter.match(subject: sedReplaceString);
158 if (!match.hasMatch()) {
159 return false;
160 }
161
162 const QString find = match.captured(nth: 1);
163 const QString replace = match.captured(nth: 2);
164
165 destDelim = d;
166 destFindBeginPos = match.capturedStart(nth: 1);
167 destFindEndPos = match.capturedStart(nth: 1) + find.length() - 1;
168 destReplaceBeginPos = match.capturedStart(nth: 2);
169 destReplaceEndPos = match.capturedStart(nth: 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()) {
200 return KTextEditor::Range::invalid();
201 }
202
203 if (matches.first().start().line() > m_endLine) {
204 return KTextEditor::Range::invalid();
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(range: currentMatch);
223 const QString replacementText = replacementTextForCurrentMatch();
224
225 m_doc->editStart();
226 m_doc->removeText(range: currentMatch);
227 m_doc->insertText(position: currentMatch.start(), s: replacementText);
228 m_doc->editEnd();
229
230 // Begin next search from directly after replacement.
231 if (!replacementText.contains(c: 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(c: QLatin1Char('\n')),
238 replacementText.length() - replacementText.lastIndexOf(c: 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(c: QLatin1Char('\n'));
247 m_endLine -= currentMatchText.count(c: 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(c: 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()) {
283 return QList<KTextEditor::Range>();
284 }
285
286 QRegularExpression::PatternOptions options;
287 if (m_caseSensitive == Qt::CaseInsensitive) {
288 options |= (QRegularExpression::CaseInsensitiveOption);
289 }
290 return m_regExpSearch.search(pattern: m_findPattern, inputRange: KTextEditor::Range(m_currentSearchPos, m_doc->documentEnd()), backwards: 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(asize: captureRanges.size());
298 for (KTextEditor::Range captureRange : captureRanges) {
299 captureTexts << m_doc->text(range: captureRange);
300 }
301 const QString replacementText = m_regExpSearch.buildReplacement(text: m_replacePattern, capturedTexts: captureTexts, replacementCounter: 0);
302 return replacementText;
303}
304

source code of ktexteditor/src/utils/katesedcmd.cpp