1 /*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
4 SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
5
6 SPDX-License-Identifier: LGPL-2.0-only
7 */
8
9 #include "kreplace.h"
10
11 #include "kfind_p.h"
12 #include "kreplacedialog.h"
13
14 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70)
15 #include <QRegExp>
16 #endif
17
18 #include <QDialogButtonBox>
19 #include <QLabel>
20 #include <QPushButton>
21 #include <QRegularExpression>
22 #include <QVBoxLayout>
23
24 #include <KLocalizedString>
25 #include <KMessageBox>
26
27 //#define DEBUG_REPLACE
28 #define INDEX_NOMATCH -1
29
30 class KReplaceNextDialog : public QDialog
31 {
32 Q_OBJECT
33 public:
34 explicit KReplaceNextDialog(QWidget *parent);
35 void setLabel(const QString &pattern, const QString &replacement);
36
37 QPushButton *replaceAllButton() const;
38 QPushButton *skipButton() const;
39 QPushButton *replaceButton() const;
40
41 private:
42 QLabel *m_mainLabel = nullptr;
43 QPushButton *m_allButton = nullptr;
44 QPushButton *m_skipButton = nullptr;
45 QPushButton *m_replaceButton = nullptr;
46 };
47
KReplaceNextDialog(QWidget * parent)48 KReplaceNextDialog::KReplaceNextDialog(QWidget *parent)
49 : QDialog(parent)
50 {
51 setModal(false);
52 setWindowTitle(i18n("Replace"));
53
54 QVBoxLayout *layout = new QVBoxLayout(this);
55
56 m_mainLabel = new QLabel(this);
57 layout->addWidget(m_mainLabel);
58
59 m_allButton = new QPushButton(i18nc("@action:button Replace all occurrences", "&All"));
60 m_allButton->setObjectName(QStringLiteral("allButton"));
61 m_skipButton = new QPushButton(i18n("&Skip"));
62 m_skipButton->setObjectName(QStringLiteral("skipButton"));
63 m_replaceButton = new QPushButton(i18n("Replace"));
64 m_replaceButton->setObjectName(QStringLiteral("replaceButton"));
65 m_replaceButton->setDefault(true);
66
67 QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
68 buttonBox->addButton(m_allButton, QDialogButtonBox::ActionRole);
69 buttonBox->addButton(m_skipButton, QDialogButtonBox::ActionRole);
70 buttonBox->addButton(m_replaceButton, QDialogButtonBox::ActionRole);
71 buttonBox->setStandardButtons(QDialogButtonBox::Close);
72 layout->addWidget(buttonBox);
73
74 connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
75 connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
76 }
77
setLabel(const QString & pattern,const QString & replacement)78 void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement)
79 {
80 m_mainLabel->setText(i18n("Replace '%1' with '%2'?", pattern, replacement));
81 }
82
replaceAllButton() const83 QPushButton *KReplaceNextDialog::replaceAllButton() const
84 {
85 return m_allButton;
86 }
87
skipButton() const88 QPushButton *KReplaceNextDialog::skipButton() const
89 {
90 return m_skipButton;
91 }
92
replaceButton() const93 QPushButton *KReplaceNextDialog::replaceButton() const
94 {
95 return m_replaceButton;
96 }
97
98 ////
99
100 class KReplacePrivate : public KFindPrivate
101 {
102 Q_DECLARE_PUBLIC(KReplace)
103
104 public:
KReplacePrivate(KReplace * q,const QString & replacement)105 KReplacePrivate(KReplace *q, const QString &replacement)
106 : KFindPrivate(q)
107 , m_replacement(replacement)
108 {
109 }
110
111 KReplaceNextDialog *nextDialog();
112 void doReplace();
113
114 void slotSkip();
115 void slotReplace();
116 void slotReplaceAll();
117
118 QString m_replacement;
119 int m_replacements = 0;
120 QRegularExpressionMatch m_match;
121 };
122
123 ////
124
KReplace(const QString & pattern,const QString & replacement,long options,QWidget * parent)125 KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent)
126 : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent)
127 {
128 }
129
KReplace(const QString & pattern,const QString & replacement,long options,QWidget * parent,QWidget * dlg)130 KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *dlg)
131 : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent, dlg)
132 {
133 }
134
135 KReplace::~KReplace() = default;
136
numReplacements() const137 int KReplace::numReplacements() const
138 {
139 Q_D(const KReplace);
140
141 return d->m_replacements;
142 }
143
replaceNextDialog(bool create)144 QDialog *KReplace::replaceNextDialog(bool create)
145 {
146 Q_D(KReplace);
147
148 if (d->dialog || create) {
149 return d->nextDialog();
150 }
151 return nullptr;
152 }
153
nextDialog()154 KReplaceNextDialog *KReplacePrivate::nextDialog()
155 {
156 Q_Q(KReplace);
157
158 if (!dialog) {
159 auto *nextDialog = new KReplaceNextDialog(q->parentWidget());
160 q->connect(nextDialog->replaceAllButton(), &QPushButton::clicked, q, [this]() {
161 slotReplaceAll();
162 });
163 q->connect(nextDialog->skipButton(), &QPushButton::clicked, q, [this]() {
164 slotSkip();
165 });
166 q->connect(nextDialog->replaceButton(), &QPushButton::clicked, q, [this]() {
167 slotReplace();
168 });
169 q->connect(nextDialog, &QDialog::finished, q, [this]() {
170 slotDialogClosed();
171 });
172 dialog = nextDialog;
173 }
174 return static_cast<KReplaceNextDialog *>(dialog);
175 }
176
displayFinalDialog() const177 void KReplace::displayFinalDialog() const
178 {
179 Q_D(const KReplace);
180
181 if (!d->m_replacements) {
182 KMessageBox::information(parentWidget(), i18n("No text was replaced."));
183 } else {
184 KMessageBox::information(parentWidget(), i18np("1 replacement done.", "%1 replacements done.", d->m_replacements));
185 }
186 }
187
188 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70)
replaceHelper(QString & text,const QString & replacement,int index,long options,int length,const QRegExp * regExp)189 static int replaceHelper(QString &text, const QString &replacement, int index, long options, int length, const QRegExp *regExp)
190 {
191 QString rep(replacement);
192 if (options & KReplaceDialog::BackReference) {
193 // Backreferences: replace \0 with the right portion of 'text'
194 rep.replace(QLatin1String("\\0"), text.mid(index, length));
195
196 // Other backrefs
197 if (regExp) {
198 const QStringList caps = regExp->capturedTexts();
199 for (int i = 0; i < caps.count(); ++i) {
200 rep.replace(QLatin1String("\\") + QString::number(i), caps.at(i));
201 }
202 }
203 }
204
205 // Then replace rep into the text
206 text.replace(index, length, rep);
207 return rep.length();
208 }
209 #endif
210
replaceHelper(QString & text,const QString & replacement,int index,long options,const QRegularExpressionMatch * match,int length)211 static int replaceHelper(QString &text, const QString &replacement, int index, long options, const QRegularExpressionMatch *match, int length)
212 {
213 QString rep(replacement);
214 if (options & KReplaceDialog::BackReference) {
215 // Handle backreferences
216 if (options & KFind::RegularExpression) { // regex search
217 Q_ASSERT(match);
218 const int capNum = match->regularExpression().captureCount();
219 for (int i = 0; i <= capNum; ++i) {
220 rep.replace(QLatin1String("\\") + QString::number(i), match->captured(i));
221 }
222 } else { // with non-regex search only \0 is supported, replace it with the
223 // right portion of 'text'
224 rep.replace(QLatin1String("\\0"), text.mid(index, length));
225 }
226 }
227
228 // Then replace rep into the text
229 text.replace(index, length, rep);
230 return rep.length();
231 }
232
replace()233 KFind::Result KReplace::replace()
234 {
235 Q_D(KReplace);
236
237 #ifdef DEBUG_REPLACE
238 // qDebug() << "d->index=" << d->index;
239 #endif
240 if (d->index == INDEX_NOMATCH && d->lastResult == Match) {
241 d->lastResult = NoMatch;
242 return NoMatch;
243 }
244
245 do { // this loop is only because validateMatch can fail
246 #ifdef DEBUG_REPLACE
247 // qDebug() << "beginning of loop: d->index=" << d->index;
248 #endif
249 // Find the next match.
250 d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, d->options & KFind::RegularExpression ? &d->m_match : nullptr);
251
252 #ifdef DEBUG_REPLACE
253 // qDebug() << "KFind::find returned d->index=" << d->index;
254 #endif
255 if (d->index != -1) {
256 // Flexibility: the app can add more rules to validate a possible match
257 if (validateMatch(d->text, d->index, d->matchedLength)) {
258 if (d->options & KReplaceDialog::PromptOnReplace) {
259 #ifdef DEBUG_REPLACE
260 // qDebug() << "PromptOnReplace";
261 #endif
262 // Display accurate initial string and replacement string, they can vary
263 QString matchedText(d->text.mid(d->index, d->matchedLength));
264 QString rep(matchedText);
265 replaceHelper(rep, d->m_replacement, 0, d->options, d->options & KFind::RegularExpression ? &d->m_match : nullptr, d->matchedLength);
266 d->nextDialog()->setLabel(matchedText, rep);
267 d->nextDialog()->show(); // TODO kde5: virtual void showReplaceNextDialog(QString,QString), so that kreplacetest can skip the show()
268
269 // Tell the world about the match we found, in case someone wants to
270 // highlight it.
271 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 81)
272 Q_EMIT highlight(d->text, d->index, d->matchedLength);
273 #endif
274 Q_EMIT textFound(d->text, d->index, d->matchedLength);
275
276 d->lastResult = Match;
277 return Match;
278 } else {
279 d->doReplace(); // this moves on too
280 }
281 } else {
282 // not validated -> move on
283 if (d->options & KFind::FindBackwards) {
284 d->index--;
285 } else {
286 d->index++;
287 }
288 }
289 } else {
290 d->index = INDEX_NOMATCH; // will exit the loop
291 }
292 } while (d->index != INDEX_NOMATCH);
293
294 d->lastResult = NoMatch;
295 return NoMatch;
296 }
297
replace(QString & text,const QString & pattern,const QString & replacement,int index,long options,int * replacedLength)298 int KReplace::replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength)
299 {
300 int matchedLength;
301 QRegularExpressionMatch match;
302 index = KFind::find(text, pattern, index, options, &matchedLength, options & KFind::RegularExpression ? &match : nullptr);
303
304 if (index != -1) {
305 *replacedLength = replaceHelper(text, replacement, index, options, options & KFind::RegularExpression ? &match : nullptr, matchedLength);
306 if (options & KFind::FindBackwards) {
307 index--;
308 } else {
309 index += *replacedLength;
310 }
311 }
312 return index;
313 }
314
315 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70)
replace(QString & text,const QRegExp & pattern,const QString & replacement,int index,long options,int * replacedLength)316 int KReplace::replace(QString &text, const QRegExp &pattern, const QString &replacement, int index, long options, int *replacedLength)
317 {
318 int matchedLength;
319
320 index = KFind::find(text, pattern, index, options, &matchedLength);
321 if (index != -1) {
322 *replacedLength = replaceHelper(text, replacement, index, options, matchedLength, &pattern);
323 if (options & KFind::FindBackwards) {
324 index--;
325 } else {
326 index += *replacedLength;
327 }
328 }
329 return index;
330 }
331 #endif
332
slotReplaceAll()333 void KReplacePrivate::slotReplaceAll()
334 {
335 Q_Q(KReplace);
336
337 doReplace();
338 options &= ~KReplaceDialog::PromptOnReplace;
339 Q_EMIT q->optionsChanged();
340 Q_EMIT q->findNext();
341 }
342
slotSkip()343 void KReplacePrivate::slotSkip()
344 {
345 Q_Q(KReplace);
346
347 if (options & KFind::FindBackwards) {
348 index--;
349 } else {
350 index++;
351 }
352 if (dialogClosed) {
353 dialog->deleteLater();
354 dialog = nullptr; // hide it again
355 } else {
356 Q_EMIT q->findNext();
357 }
358 }
359
slotReplace()360 void KReplacePrivate::slotReplace()
361 {
362 Q_Q(KReplace);
363
364 doReplace();
365 if (dialogClosed) {
366 dialog->deleteLater();
367 dialog = nullptr; // hide it again
368 } else {
369 Q_EMIT q->findNext();
370 }
371 }
372
doReplace()373 void KReplacePrivate::doReplace()
374 {
375 Q_Q(KReplace);
376
377 Q_ASSERT(index >= 0);
378 const int replacedLength = replaceHelper(text, m_replacement, index, options, &m_match, matchedLength);
379
380 // Tell the world about the replacement we made, in case someone wants to
381 // highlight it.
382 #if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
383 Q_EMIT q->replace(text, index, replacedLength, matchedLength);
384 #endif
385 Q_EMIT q->textReplaced(text, index, replacedLength, matchedLength);
386
387 #ifdef DEBUG_REPLACE
388 // qDebug() << "after replace() signal: d->index=" << d->index << " replacedLength=" << replacedLength;
389 #endif
390 m_replacements++;
391 if (options & KFind::FindBackwards) {
392 Q_ASSERT(index >= 0);
393 index--;
394 } else {
395 index += replacedLength;
396 // when replacing the empty pattern, move on. See also kjs/regexp.cpp for how this should be done for regexps.
397 if (pattern.isEmpty()) {
398 ++index;
399 }
400 }
401 #ifdef DEBUG_REPLACE
402 // qDebug() << "after adjustment: d->index=" << d->index;
403 #endif
404 }
405
resetCounts()406 void KReplace::resetCounts()
407 {
408 Q_D(KReplace);
409
410 KFind::resetCounts();
411 d->m_replacements = 0;
412 }
413
shouldRestart(bool forceAsking,bool showNumMatches) const414 bool KReplace::shouldRestart(bool forceAsking, bool showNumMatches) const
415 {
416 Q_D(const KReplace);
417
418 // Only ask if we did a "find from cursor", otherwise it's pointless.
419 // ... Or if the prompt-on-replace option was set.
420 // Well, unless the user can modify the document during a search operation,
421 // hence the force boolean.
422 if (!forceAsking && (d->options & KFind::FromCursor) == 0 && (d->options & KReplaceDialog::PromptOnReplace) == 0) {
423 displayFinalDialog();
424 return false;
425 }
426 QString message;
427 if (showNumMatches) {
428 if (!d->m_replacements) {
429 message = i18n("No text was replaced.");
430 } else {
431 message = i18np("1 replacement done.", "%1 replacements done.", d->m_replacements);
432 }
433 } else {
434 if (d->options & KFind::FindBackwards) {
435 message = i18n("Beginning of document reached.");
436 } else {
437 message = i18n("End of document reached.");
438 }
439 }
440
441 message += QLatin1Char('\n');
442 // Hope this word puzzle is ok, it's a different sentence
443 message +=
444 (d->options & KFind::FindBackwards) ? i18n("Do you want to restart search from the end?") : i18n("Do you want to restart search at the beginning?");
445
446 int ret = KMessageBox::questionYesNo(parentWidget(),
447 message,
448 QString(),
449 KGuiItem(i18nc("@action:button Restart find & replace", "Restart")),
450 KGuiItem(i18nc("@action:button Stop find & replace", "Stop")));
451 return (ret == KMessageBox::Yes);
452 }
453
closeReplaceNextDialog()454 void KReplace::closeReplaceNextDialog()
455 {
456 closeFindNextDialog();
457 }
458
459 #include "kreplace.moc"
460 #include "moc_kreplace.cpp"
461