File indexing completed on 2024-12-01 03:43:35

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
0004     SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only
0007 */
0008 
0009 #include "kreplace.h"
0010 
0011 #include "kfind_p.h"
0012 #include "kreplacedialog.h"
0013 
0014 #include <QDialogButtonBox>
0015 #include <QLabel>
0016 #include <QPushButton>
0017 #include <QRegularExpression>
0018 #include <QVBoxLayout>
0019 
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 
0023 //#define DEBUG_REPLACE
0024 #define INDEX_NOMATCH -1
0025 
0026 class KReplaceNextDialog : public QDialog
0027 {
0028     Q_OBJECT
0029 public:
0030     explicit KReplaceNextDialog(QWidget *parent);
0031     void setLabel(const QString &pattern, const QString &replacement);
0032 
0033     QPushButton *replaceAllButton() const;
0034     QPushButton *skipButton() const;
0035     QPushButton *replaceButton() const;
0036 
0037 private:
0038     QLabel *m_mainLabel = nullptr;
0039     QPushButton *m_allButton = nullptr;
0040     QPushButton *m_skipButton = nullptr;
0041     QPushButton *m_replaceButton = nullptr;
0042 };
0043 
0044 KReplaceNextDialog::KReplaceNextDialog(QWidget *parent)
0045     : QDialog(parent)
0046 {
0047     setModal(false);
0048     setWindowTitle(i18n("Replace"));
0049 
0050     QVBoxLayout *layout = new QVBoxLayout(this);
0051 
0052     m_mainLabel = new QLabel(this);
0053     layout->addWidget(m_mainLabel);
0054 
0055     m_allButton = new QPushButton(i18nc("@action:button Replace all occurrences", "&All"));
0056     m_allButton->setObjectName(QStringLiteral("allButton"));
0057     m_skipButton = new QPushButton(i18n("&Skip"));
0058     m_skipButton->setObjectName(QStringLiteral("skipButton"));
0059     m_replaceButton = new QPushButton(i18n("Replace"));
0060     m_replaceButton->setObjectName(QStringLiteral("replaceButton"));
0061     m_replaceButton->setDefault(true);
0062 
0063     QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
0064     buttonBox->addButton(m_allButton, QDialogButtonBox::ActionRole);
0065     buttonBox->addButton(m_skipButton, QDialogButtonBox::ActionRole);
0066     buttonBox->addButton(m_replaceButton, QDialogButtonBox::ActionRole);
0067     buttonBox->setStandardButtons(QDialogButtonBox::Close);
0068     layout->addWidget(buttonBox);
0069 
0070     connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0071     connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0072 }
0073 
0074 void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement)
0075 {
0076     m_mainLabel->setText(i18n("Replace '%1' with '%2'?", pattern, replacement));
0077 }
0078 
0079 QPushButton *KReplaceNextDialog::replaceAllButton() const
0080 {
0081     return m_allButton;
0082 }
0083 
0084 QPushButton *KReplaceNextDialog::skipButton() const
0085 {
0086     return m_skipButton;
0087 }
0088 
0089 QPushButton *KReplaceNextDialog::replaceButton() const
0090 {
0091     return m_replaceButton;
0092 }
0093 
0094 ////
0095 
0096 class KReplacePrivate : public KFindPrivate
0097 {
0098     Q_DECLARE_PUBLIC(KReplace)
0099 
0100 public:
0101     KReplacePrivate(KReplace *q, const QString &replacement)
0102         : KFindPrivate(q)
0103         , m_replacement(replacement)
0104     {
0105     }
0106 
0107     KReplaceNextDialog *nextDialog();
0108     void doReplace();
0109 
0110     void slotSkip();
0111     void slotReplace();
0112     void slotReplaceAll();
0113 
0114     QString m_replacement;
0115     int m_replacements = 0;
0116     QRegularExpressionMatch m_match;
0117 };
0118 
0119 ////
0120 
0121 KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent)
0122     : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent)
0123 {
0124 }
0125 
0126 KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *dlg)
0127     : KFind(*new KReplacePrivate(this, replacement), pattern, options, parent, dlg)
0128 {
0129 }
0130 
0131 KReplace::~KReplace() = default;
0132 
0133 int KReplace::numReplacements() const
0134 {
0135     Q_D(const KReplace);
0136 
0137     return d->m_replacements;
0138 }
0139 
0140 QDialog *KReplace::replaceNextDialog(bool create)
0141 {
0142     Q_D(KReplace);
0143 
0144     if (d->dialog || create) {
0145         return d->nextDialog();
0146     }
0147     return nullptr;
0148 }
0149 
0150 KReplaceNextDialog *KReplacePrivate::nextDialog()
0151 {
0152     Q_Q(KReplace);
0153 
0154     if (!dialog) {
0155         auto *nextDialog = new KReplaceNextDialog(q->parentWidget());
0156         q->connect(nextDialog->replaceAllButton(), &QPushButton::clicked, q, [this]() {
0157             slotReplaceAll();
0158         });
0159         q->connect(nextDialog->skipButton(), &QPushButton::clicked, q, [this]() {
0160             slotSkip();
0161         });
0162         q->connect(nextDialog->replaceButton(), &QPushButton::clicked, q, [this]() {
0163             slotReplace();
0164         });
0165         q->connect(nextDialog, &QDialog::finished, q, [this]() {
0166             slotDialogClosed();
0167         });
0168         dialog = nextDialog;
0169     }
0170     return static_cast<KReplaceNextDialog *>(dialog);
0171 }
0172 
0173 void KReplace::displayFinalDialog() const
0174 {
0175     Q_D(const KReplace);
0176 
0177     if (!d->m_replacements) {
0178         KMessageBox::information(parentWidget(), i18n("No text was replaced."));
0179     } else {
0180         KMessageBox::information(parentWidget(), i18np("1 replacement done.", "%1 replacements done.", d->m_replacements));
0181     }
0182 }
0183 
0184 static int replaceHelper(QString &text, const QString &replacement, int index, long options, const QRegularExpressionMatch *match, int length)
0185 {
0186     QString rep(replacement);
0187     if (options & KReplaceDialog::BackReference) {
0188         // Handle backreferences
0189         if (options & KFind::RegularExpression) { // regex search
0190             Q_ASSERT(match);
0191             const int capNum = match->regularExpression().captureCount();
0192             for (int i = 0; i <= capNum; ++i) {
0193                 rep.replace(QLatin1String("\\") + QString::number(i), match->captured(i));
0194             }
0195         } else { // with non-regex search only \0 is supported, replace it with the
0196                  // right portion of 'text'
0197             rep.replace(QLatin1String("\\0"), text.mid(index, length));
0198         }
0199     }
0200 
0201     // Then replace rep into the text
0202     text.replace(index, length, rep);
0203     return rep.length();
0204 }
0205 
0206 KFind::Result KReplace::replace()
0207 {
0208     Q_D(KReplace);
0209 
0210 #ifdef DEBUG_REPLACE
0211     // qDebug() << "d->index=" << d->index;
0212 #endif
0213     if (d->index == INDEX_NOMATCH && d->lastResult == Match) {
0214         d->lastResult = NoMatch;
0215         return NoMatch;
0216     }
0217 
0218     do { // this loop is only because validateMatch can fail
0219 #ifdef DEBUG_REPLACE
0220          // qDebug() << "beginning of loop: d->index=" << d->index;
0221 #endif
0222          // Find the next match.
0223         d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, d->options & KFind::RegularExpression ? &d->m_match : nullptr);
0224 
0225 #ifdef DEBUG_REPLACE
0226         // qDebug() << "KFind::find returned d->index=" << d->index;
0227 #endif
0228         if (d->index != -1) {
0229             // Flexibility: the app can add more rules to validate a possible match
0230             if (validateMatch(d->text, d->index, d->matchedLength)) {
0231                 if (d->options & KReplaceDialog::PromptOnReplace) {
0232 #ifdef DEBUG_REPLACE
0233                     // qDebug() << "PromptOnReplace";
0234 #endif
0235                     // Display accurate initial string and replacement string, they can vary
0236                     QString matchedText(d->text.mid(d->index, d->matchedLength));
0237                     QString rep(matchedText);
0238                     replaceHelper(rep, d->m_replacement, 0, d->options, d->options & KFind::RegularExpression ? &d->m_match : nullptr, d->matchedLength);
0239                     d->nextDialog()->setLabel(matchedText, rep);
0240                     d->nextDialog()->show(); // TODO kde5: virtual void showReplaceNextDialog(QString,QString), so that kreplacetest can skip the show()
0241 
0242                     // Tell the world about the match we found, in case someone wants to
0243                     // highlight it.
0244                     Q_EMIT textFound(d->text, d->index, d->matchedLength);
0245 
0246                     d->lastResult = Match;
0247                     return Match;
0248                 } else {
0249                     d->doReplace(); // this moves on too
0250                 }
0251             } else {
0252                 // not validated -> move on
0253                 if (d->options & KFind::FindBackwards) {
0254                     d->index--;
0255                 } else {
0256                     d->index++;
0257                 }
0258             }
0259         } else {
0260             d->index = INDEX_NOMATCH; // will exit the loop
0261         }
0262     } while (d->index != INDEX_NOMATCH);
0263 
0264     d->lastResult = NoMatch;
0265     return NoMatch;
0266 }
0267 
0268 int KReplace::replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength)
0269 {
0270     int matchedLength;
0271     QRegularExpressionMatch match;
0272     index = KFind::find(text, pattern, index, options, &matchedLength, options & KFind::RegularExpression ? &match : nullptr);
0273 
0274     if (index != -1) {
0275         *replacedLength = replaceHelper(text, replacement, index, options, options & KFind::RegularExpression ? &match : nullptr, matchedLength);
0276         if (options & KFind::FindBackwards) {
0277             index--;
0278         } else {
0279             index += *replacedLength;
0280         }
0281     }
0282     return index;
0283 }
0284 
0285 void KReplacePrivate::slotReplaceAll()
0286 {
0287     Q_Q(KReplace);
0288 
0289     doReplace();
0290     options &= ~KReplaceDialog::PromptOnReplace;
0291     Q_EMIT q->optionsChanged();
0292     Q_EMIT q->findNext();
0293 }
0294 
0295 void KReplacePrivate::slotSkip()
0296 {
0297     Q_Q(KReplace);
0298 
0299     if (options & KFind::FindBackwards) {
0300         index--;
0301     } else {
0302         index++;
0303     }
0304     if (dialogClosed) {
0305         dialog->deleteLater();
0306         dialog = nullptr; // hide it again
0307     } else {
0308         Q_EMIT q->findNext();
0309     }
0310 }
0311 
0312 void KReplacePrivate::slotReplace()
0313 {
0314     Q_Q(KReplace);
0315 
0316     doReplace();
0317     if (dialogClosed) {
0318         dialog->deleteLater();
0319         dialog = nullptr; // hide it again
0320     } else {
0321         Q_EMIT q->findNext();
0322     }
0323 }
0324 
0325 void KReplacePrivate::doReplace()
0326 {
0327     Q_Q(KReplace);
0328 
0329     Q_ASSERT(index >= 0);
0330     const int replacedLength = replaceHelper(text, m_replacement, index, options, &m_match, matchedLength);
0331 
0332     // Tell the world about the replacement we made, in case someone wants to
0333     // highlight it.
0334     Q_EMIT q->textReplaced(text, index, replacedLength, matchedLength);
0335 
0336 #ifdef DEBUG_REPLACE
0337     // qDebug() << "after replace() signal: d->index=" << d->index << " replacedLength=" << replacedLength;
0338 #endif
0339     m_replacements++;
0340     if (options & KFind::FindBackwards) {
0341         Q_ASSERT(index >= 0);
0342         index--;
0343     } else {
0344         index += replacedLength;
0345         // when replacing the empty pattern, move on. See also kjs/regexp.cpp for how this should be done for regexps.
0346         if (pattern.isEmpty()) {
0347             ++index;
0348         }
0349     }
0350 #ifdef DEBUG_REPLACE
0351     // qDebug() << "after adjustment: d->index=" << d->index;
0352 #endif
0353 }
0354 
0355 void KReplace::resetCounts()
0356 {
0357     Q_D(KReplace);
0358 
0359     KFind::resetCounts();
0360     d->m_replacements = 0;
0361 }
0362 
0363 bool KReplace::shouldRestart(bool forceAsking, bool showNumMatches) const
0364 {
0365     Q_D(const KReplace);
0366 
0367     // Only ask if we did a "find from cursor", otherwise it's pointless.
0368     // ... Or if the prompt-on-replace option was set.
0369     // Well, unless the user can modify the document during a search operation,
0370     // hence the force boolean.
0371     if (!forceAsking && (d->options & KFind::FromCursor) == 0 && (d->options & KReplaceDialog::PromptOnReplace) == 0) {
0372         displayFinalDialog();
0373         return false;
0374     }
0375     QString message;
0376     if (showNumMatches) {
0377         if (!d->m_replacements) {
0378             message = i18n("No text was replaced.");
0379         } else {
0380             message = i18np("1 replacement done.", "%1 replacements done.", d->m_replacements);
0381         }
0382     } else {
0383         if (d->options & KFind::FindBackwards) {
0384             message = i18n("Beginning of document reached.");
0385         } else {
0386             message = i18n("End of document reached.");
0387         }
0388     }
0389 
0390     message += QLatin1Char('\n');
0391     // Hope this word puzzle is ok, it's a different sentence
0392     message +=
0393         (d->options & KFind::FindBackwards) ? i18n("Do you want to restart search from the end?") : i18n("Do you want to restart search at the beginning?");
0394 
0395     int ret = KMessageBox::questionTwoActions(parentWidget(),
0396                                               message,
0397                                               QString(),
0398                                               KGuiItem(i18nc("@action:button Restart find & replace", "Restart")),
0399                                               KGuiItem(i18nc("@action:button Stop find & replace", "Stop")));
0400     return (ret == KMessageBox::PrimaryAction);
0401 }
0402 
0403 void KReplace::closeReplaceNextDialog()
0404 {
0405     closeFindNextDialog();
0406 }
0407 
0408 #include "kreplace.moc"
0409 #include "moc_kreplace.cpp"