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 | |
22 | KateCommands::SedReplace *KateCommands::SedReplace::m_instance = nullptr; |
23 | |
24 | static 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 |
48 | static 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 | |
65 | bool 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 | |
131 | bool 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 | |
137 | bool 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 | |
174 | KateCommands::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 | |
195 | KTextEditor::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 | |
210 | void 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 | |
219 | void 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 | |
257 | void KateCommands::SedReplace::InteractiveSedReplacer::replaceAllRemaining() |
258 | { |
259 | m_doc->editStart(); |
260 | while (currentMatch().isValid()) { |
261 | replaceCurrentMatch(); |
262 | } |
263 | m_doc->editEnd(); |
264 | } |
265 | |
266 | QString KateCommands::SedReplace::InteractiveSedReplacer::currentMatchReplacementConfirmationMessage() |
267 | { |
268 | return i18n("replace with %1?" , replacementTextForCurrentMatch().replace(QLatin1Char('\n'), QLatin1String("\\n" ))); |
269 | } |
270 | |
271 | QString 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 | |
280 | const 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 | |
293 | QString 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 | |