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