File indexing completed on 2024-04-21 11:37:04

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