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

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     SPDX-FileCopyrightText: 2004 Arend van Beelen jr. <arend@auton.nl>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "kfind.h"
0011 #include "kfind_p.h"
0012 
0013 #include "kfinddialog.h"
0014 
0015 #include <KGuiItem>
0016 #include <KLocalizedString>
0017 #include <KMessageBox>
0018 
0019 #include <QDialog>
0020 #include <QDialogButtonBox>
0021 #include <QHash>
0022 #include <QLabel>
0023 #include <QPushButton>
0024 #include <QRegularExpression>
0025 #include <QVBoxLayout>
0026 
0027 // #define DEBUG_FIND
0028 
0029 static const int INDEX_NOMATCH = -1;
0030 
0031 class KFindNextDialog : public QDialog
0032 {
0033     Q_OBJECT
0034 public:
0035     explicit KFindNextDialog(const QString &pattern, QWidget *parent);
0036 
0037     QPushButton *findButton() const;
0038 
0039 private:
0040     QPushButton *m_findButton = nullptr;
0041 };
0042 
0043 // Create the dialog.
0044 KFindNextDialog::KFindNextDialog(const QString &pattern, QWidget *parent)
0045     : QDialog(parent)
0046 {
0047     setModal(false);
0048     setWindowTitle(i18n("Find Next"));
0049 
0050     QVBoxLayout *layout = new QVBoxLayout(this);
0051 
0052     layout->addWidget(new QLabel(i18n("<qt>Find next occurrence of '<b>%1</b>'?</qt>", pattern), this));
0053 
0054     m_findButton = new QPushButton;
0055     KGuiItem::assign(m_findButton, KStandardGuiItem::find());
0056     m_findButton->setDefault(true);
0057 
0058     QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
0059     buttonBox->addButton(m_findButton, QDialogButtonBox::ActionRole);
0060     buttonBox->setStandardButtons(QDialogButtonBox::Close);
0061     layout->addWidget(buttonBox);
0062 
0063     connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0064     connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0065 }
0066 
0067 QPushButton *KFindNextDialog::findButton() const
0068 {
0069     return m_findButton;
0070 }
0071 
0072 ////
0073 
0074 KFind::KFind(const QString &pattern, long options, QWidget *parent)
0075     : KFind(*new KFindPrivate(this), pattern, options, parent)
0076 {
0077 }
0078 
0079 KFind::KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent)
0080     : QObject(parent)
0081     , d_ptr(&dd)
0082 {
0083     Q_D(KFind);
0084 
0085     d->options = options;
0086     d->init(pattern);
0087 }
0088 
0089 KFind::KFind(const QString &pattern, long options, QWidget *parent, QWidget *findDialog)
0090     : KFind(*new KFindPrivate(this), pattern, options, parent, findDialog)
0091 {
0092 }
0093 
0094 KFind::KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent, QWidget *findDialog)
0095     : QObject(parent)
0096     , d_ptr(&dd)
0097 {
0098     Q_D(KFind);
0099 
0100     d->findDialog = findDialog;
0101     d->options = options;
0102     d->init(pattern);
0103 }
0104 
0105 void KFindPrivate::init(const QString &_pattern)
0106 {
0107     Q_Q(KFind);
0108 
0109     matches = 0;
0110     pattern = _pattern;
0111     dialog = nullptr;
0112     dialogClosed = false;
0113     index = INDEX_NOMATCH;
0114     lastResult = KFind::NoMatch;
0115 
0116     // TODO: KF6 change this comment once d->regExp is removed
0117     // set options and create d->regExp with the right options
0118     q->setOptions(options);
0119 }
0120 
0121 KFind::~KFind() = default;
0122 
0123 bool KFind::needData() const
0124 {
0125     Q_D(const KFind);
0126 
0127     // always true when d->text is empty.
0128     if (d->options & KFind::FindBackwards)
0129     // d->index==-1 and d->lastResult==Match means we haven't answered nomatch yet
0130     // This is important in the "replace with a prompt" case.
0131     {
0132         return (d->index < 0 && d->lastResult != Match);
0133     } else
0134     // "index over length" test removed: we want to get a nomatch before we set data again
0135     // This is important in the "replace with a prompt" case.
0136     {
0137         return d->index == INDEX_NOMATCH;
0138     }
0139 }
0140 
0141 void KFind::setData(const QString &data, int startPos)
0142 {
0143     setData(-1, data, startPos);
0144 }
0145 
0146 void KFind::setData(int id, const QString &data, int startPos)
0147 {
0148     Q_D(KFind);
0149 
0150     // cache the data for incremental find
0151     if (d->options & KFind::FindIncremental) {
0152         if (id != -1) {
0153             d->customIds = true;
0154         } else {
0155             id = d->currentId + 1;
0156         }
0157 
0158         Q_ASSERT(id <= d->data.size());
0159 
0160         if (id == d->data.size()) {
0161             d->data.append(KFindPrivate::Data(id, data, true));
0162         } else {
0163             d->data.replace(id, KFindPrivate::Data(id, data, true));
0164         }
0165         Q_ASSERT(d->data.at(id).text == data);
0166     }
0167 
0168     if (!(d->options & KFind::FindIncremental) || needData()) {
0169         d->text = data;
0170 
0171         if (startPos != -1) {
0172             d->index = startPos;
0173         } else if (d->options & KFind::FindBackwards) {
0174             d->index = d->text.length();
0175         } else {
0176             d->index = 0;
0177         }
0178 #ifdef DEBUG_FIND
0179         // qDebug() << "setData: '" << d->text << "' d->index=" << d->index;
0180 #endif
0181         Q_ASSERT(d->index != INDEX_NOMATCH);
0182         d->lastResult = NoMatch;
0183 
0184         d->currentId = id;
0185     }
0186 }
0187 
0188 QDialog *KFind::findNextDialog(bool create)
0189 {
0190     Q_D(KFind);
0191 
0192     if (!d->dialog && create) {
0193         KFindNextDialog *dialog = new KFindNextDialog(d->pattern, parentWidget());
0194         connect(dialog->findButton(), &QPushButton::clicked, this, [d]() {
0195             d->slotFindNext();
0196         });
0197         connect(dialog, &QDialog::finished, this, [d]() {
0198             d->slotDialogClosed();
0199         });
0200         d->dialog = dialog;
0201     }
0202     return d->dialog;
0203 }
0204 
0205 KFind::Result KFind::find()
0206 {
0207     Q_D(KFind);
0208 
0209     Q_ASSERT(d->index != INDEX_NOMATCH || d->patternChanged);
0210 
0211     if (d->lastResult == Match && !d->patternChanged) {
0212         // Move on before looking for the next match, _if_ we just found a match
0213         if (d->options & KFind::FindBackwards) {
0214             d->index--;
0215             if (d->index == -1) { // don't call KFind::find with -1, it has a special meaning
0216                 d->lastResult = NoMatch;
0217                 return NoMatch;
0218             }
0219         } else {
0220             d->index++;
0221         }
0222     }
0223     d->patternChanged = false;
0224 
0225     if (d->options & KFind::FindIncremental) {
0226         // if the current pattern is shorter than the matchedPattern we can
0227         // probably look up the match in the incrementalPath
0228         if (d->pattern.length() < d->matchedPattern.length()) {
0229             KFindPrivate::Match match;
0230             if (!d->pattern.isEmpty()) {
0231                 match = d->incrementalPath.value(d->pattern);
0232             } else if (d->emptyMatch) {
0233                 match = *d->emptyMatch;
0234             }
0235             QString previousPattern(d->matchedPattern);
0236             d->matchedPattern = d->pattern;
0237             if (!match.isNull()) {
0238                 bool clean = true;
0239 
0240                 // find the first result backwards on the path that isn't dirty
0241                 while (d->data.at(match.dataId).dirty == true && !d->pattern.isEmpty()) {
0242                     d->pattern.truncate(d->pattern.length() - 1);
0243 
0244                     match = d->incrementalPath.value(d->pattern);
0245 
0246                     clean = false;
0247                 }
0248 
0249                 // remove all matches that lie after the current match
0250                 while (d->pattern.length() < previousPattern.length()) {
0251                     d->incrementalPath.remove(previousPattern);
0252                     previousPattern.truncate(previousPattern.length() - 1);
0253                 }
0254 
0255                 // set the current text, index, etc. to the found match
0256                 d->text = d->data.at(match.dataId).text;
0257                 d->index = match.index;
0258                 d->matchedLength = match.matchedLength;
0259                 d->currentId = match.dataId;
0260 
0261                 // if the result is clean we can return it now
0262                 if (clean) {
0263                     if (d->customIds) {
0264                         Q_EMIT textFoundAtId(d->currentId, d->index, d->matchedLength);
0265                     } else {
0266                         Q_EMIT textFound(d->text, d->index, d->matchedLength);
0267                     }
0268 
0269                     d->lastResult = Match;
0270                     d->matchedPattern = d->pattern;
0271                     return Match;
0272                 }
0273             }
0274             // if we couldn't look up the match, the new pattern isn't a
0275             // substring of the matchedPattern, so we start a new search
0276             else {
0277                 d->startNewIncrementalSearch();
0278             }
0279         }
0280         // if the new pattern is longer than the matchedPattern we might be
0281         // able to proceed from the last search
0282         else if (d->pattern.length() > d->matchedPattern.length()) {
0283             // continue from the previous pattern
0284             if (d->pattern.startsWith(d->matchedPattern)) {
0285                 // we can't proceed from the previous position if the previous
0286                 // position already failed
0287                 if (d->index == INDEX_NOMATCH) {
0288                     return NoMatch;
0289                 }
0290 
0291                 QString temp(d->pattern);
0292                 d->pattern.truncate(d->matchedPattern.length() + 1);
0293                 d->matchedPattern = temp;
0294             }
0295             // start a new search
0296             else {
0297                 d->startNewIncrementalSearch();
0298             }
0299         }
0300         // if the new pattern is as long as the matchedPattern, we reset if
0301         // they are not equal
0302         else if (d->pattern != d->matchedPattern) {
0303             d->startNewIncrementalSearch();
0304         }
0305     }
0306 
0307 #ifdef DEBUG_FIND
0308     // qDebug() << "d->index=" << d->index;
0309 #endif
0310     do {
0311         // if we have multiple data blocks in our cache, walk through these
0312         // blocks till we either searched all blocks or we find a match
0313         do {
0314             // Find the next candidate match.
0315             d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, nullptr);
0316 
0317             if (d->options & KFind::FindIncremental) {
0318                 d->data[d->currentId].dirty = false;
0319             }
0320 
0321             if (d->index == -1 && d->currentId < d->data.count() - 1) {
0322                 d->text = d->data.at(++d->currentId).text;
0323 
0324                 if (d->options & KFind::FindBackwards) {
0325                     d->index = d->text.length();
0326                 } else {
0327                     d->index = 0;
0328                 }
0329             } else {
0330                 break;
0331             }
0332         } while (!(d->options & KFind::RegularExpression));
0333 
0334         if (d->index != -1) {
0335             // Flexibility: the app can add more rules to validate a possible match
0336             if (validateMatch(d->text, d->index, d->matchedLength)) {
0337                 bool done = true;
0338 
0339                 if (d->options & KFind::FindIncremental) {
0340                     if (d->pattern.isEmpty()) {
0341                         delete d->emptyMatch;
0342                         d->emptyMatch = new KFindPrivate::Match(d->currentId, d->index, d->matchedLength);
0343                     } else {
0344                         d->incrementalPath.insert(d->pattern, KFindPrivate::Match(d->currentId, d->index, d->matchedLength));
0345                     }
0346 
0347                     if (d->pattern.length() < d->matchedPattern.length()) {
0348                         d->pattern += QStringView(d->matchedPattern).mid(d->pattern.length(), 1);
0349                         done = false;
0350                     }
0351                 }
0352 
0353                 if (done) {
0354                     d->matches++;
0355                     // Tell the world about the match we found, in case someone wants to
0356                     // highlight it.
0357                     if (d->customIds) {
0358                         Q_EMIT textFoundAtId(d->currentId, d->index, d->matchedLength);
0359                     } else {
0360                         Q_EMIT textFound(d->text, d->index, d->matchedLength);
0361                     }
0362 
0363                     if (!d->dialogClosed) {
0364                         findNextDialog(true)->show();
0365                     }
0366 
0367 #ifdef DEBUG_FIND
0368                     // qDebug() << "Match. Next d->index=" << d->index;
0369 #endif
0370                     d->lastResult = Match;
0371                     return Match;
0372                 }
0373             } else { // Skip match
0374                 if (d->options & KFind::FindBackwards) {
0375                     d->index--;
0376                 } else {
0377                     d->index++;
0378                 }
0379             }
0380         } else {
0381             if (d->options & KFind::FindIncremental) {
0382                 QString temp(d->pattern);
0383                 temp.truncate(temp.length() - 1);
0384                 d->pattern = d->matchedPattern;
0385                 d->matchedPattern = temp;
0386             }
0387 
0388             d->index = INDEX_NOMATCH;
0389         }
0390     } while (d->index != INDEX_NOMATCH);
0391 
0392 #ifdef DEBUG_FIND
0393     // qDebug() << "NoMatch. d->index=" << d->index;
0394 #endif
0395     d->lastResult = NoMatch;
0396     return NoMatch;
0397 }
0398 
0399 void KFindPrivate::startNewIncrementalSearch()
0400 {
0401     KFindPrivate::Match *match = emptyMatch;
0402     if (match == nullptr) {
0403         text.clear();
0404         index = 0;
0405         currentId = 0;
0406     } else {
0407         text = data.at(match->dataId).text;
0408         index = match->index;
0409         currentId = match->dataId;
0410     }
0411     matchedLength = 0;
0412     incrementalPath.clear();
0413     delete emptyMatch;
0414     emptyMatch = nullptr;
0415     matchedPattern = pattern;
0416     pattern.clear();
0417 }
0418 
0419 static bool isInWord(QChar ch)
0420 {
0421     return ch.isLetter() || ch.isDigit() || ch == QLatin1Char('_');
0422 }
0423 
0424 static bool isWholeWords(const QString &text, int starts, int matchedLength)
0425 {
0426     if (starts == 0 || !isInWord(text.at(starts - 1))) {
0427         const int ends = starts + matchedLength;
0428         if (ends == text.length() || !isInWord(text.at(ends))) {
0429             return true;
0430         }
0431     }
0432     return false;
0433 }
0434 
0435 static bool matchOk(const QString &text, int index, int matchedLength, long options)
0436 {
0437     if (options & KFind::WholeWordsOnly) {
0438         // Is the match delimited correctly?
0439         if (isWholeWords(text, index, matchedLength)) {
0440             return true;
0441         }
0442     } else {
0443         // Non-whole-word search: this match is good
0444         return true;
0445     }
0446     return false;
0447 }
0448 
0449 static int findRegex(const QString &text, const QString &pattern, int index, long options, int *matchedLength, QRegularExpressionMatch *rmatch)
0450 {
0451     QString _pattern = pattern;
0452 
0453     // Always enable Unicode support in QRegularExpression
0454     QRegularExpression::PatternOptions opts = QRegularExpression::UseUnicodePropertiesOption;
0455     // instead of this rudimentary test, add a checkbox to toggle MultilineOption ?
0456     if (pattern.startsWith(QLatin1Char('^')) || pattern.endsWith(QLatin1Char('$'))) {
0457         opts |= QRegularExpression::MultilineOption;
0458     } else if (options & KFind::WholeWordsOnly) { // WholeWordsOnly makes no sense with multiline
0459         _pattern = QLatin1String("\\b") + pattern + QLatin1String("\\b");
0460     }
0461 
0462     if (!(options & KFind::CaseSensitive)) {
0463         opts |= QRegularExpression::CaseInsensitiveOption;
0464     }
0465 
0466     QRegularExpression re(_pattern, opts);
0467     QRegularExpressionMatch match;
0468     if (options & KFind::FindBackwards) {
0469         // Backward search, until the beginning of the line...
0470         (void)text.lastIndexOf(re, index, &match);
0471     } else {
0472         // Forward search, until the end of the line...
0473         match = re.match(text, index);
0474     }
0475 
0476     // index is -1 if no match is found
0477     index = match.capturedStart(0);
0478     // matchedLength is 0 if no match is found
0479     *matchedLength = match.capturedLength(0);
0480 
0481     if (rmatch) {
0482         *rmatch = match;
0483     }
0484 
0485     return index;
0486 }
0487 
0488 // static
0489 int KFind::find(const QString &text, const QString &pattern, int index, long options, int *matchedLength, QRegularExpressionMatch *rmatch)
0490 {
0491     // Handle regular expressions in the appropriate way.
0492     if (options & KFind::RegularExpression) {
0493         return findRegex(text, pattern, index, options, matchedLength, rmatch);
0494     }
0495 
0496     // In Qt4 QString("aaaaaa").lastIndexOf("a",6) returns -1; we need
0497     // to start at text.length() - pattern.length() to give a valid index to QString.
0498     if (options & KFind::FindBackwards) {
0499         index = qMin(qMax(0, text.length() - pattern.length()), index);
0500     }
0501 
0502     Qt::CaseSensitivity caseSensitive = (options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive;
0503 
0504     if (options & KFind::FindBackwards) {
0505         // Backward search, until the beginning of the line...
0506         while (index >= 0) {
0507             // ...find the next match.
0508             index = text.lastIndexOf(pattern, index, caseSensitive);
0509             if (index == -1) {
0510                 break;
0511             }
0512 
0513             if (matchOk(text, index, pattern.length(), options)) {
0514                 break;
0515             }
0516             index--;
0517             // qDebug() << "decrementing:" << index;
0518         }
0519     } else {
0520         // Forward search, until the end of the line...
0521         while (index <= text.length()) {
0522             // ...find the next match.
0523             index = text.indexOf(pattern, index, caseSensitive);
0524             if (index == -1) {
0525                 break;
0526             }
0527 
0528             if (matchOk(text, index, pattern.length(), options)) {
0529                 break;
0530             }
0531             index++;
0532         }
0533         if (index > text.length()) { // end of line
0534             // qDebug() << "at" << index << "-> not found";
0535             index = -1; // not found
0536         }
0537     }
0538     if (index <= -1) {
0539         *matchedLength = 0;
0540     } else {
0541         *matchedLength = pattern.length();
0542     }
0543     return index;
0544 }
0545 
0546 void KFindPrivate::slotFindNext()
0547 {
0548     Q_Q(KFind);
0549 
0550     Q_EMIT q->findNext();
0551 }
0552 
0553 void KFindPrivate::slotDialogClosed()
0554 {
0555     Q_Q(KFind);
0556 
0557 #ifdef DEBUG_FIND
0558     // qDebug() << " Begin";
0559 #endif
0560     Q_EMIT q->dialogClosed();
0561     dialogClosed = true;
0562 #ifdef DEBUG_FIND
0563     // qDebug() << " End";
0564 #endif
0565 }
0566 
0567 void KFind::displayFinalDialog() const
0568 {
0569     Q_D(const KFind);
0570 
0571     QString message;
0572     if (numMatches()) {
0573         message = i18np("1 match found.", "%1 matches found.", numMatches());
0574     } else {
0575         message = i18n("<qt>No matches found for '<b>%1</b>'.</qt>", d->pattern.toHtmlEscaped());
0576     }
0577     KMessageBox::information(dialogsParent(), message);
0578 }
0579 
0580 bool KFind::shouldRestart(bool forceAsking, bool showNumMatches) const
0581 {
0582     Q_D(const KFind);
0583 
0584     // Only ask if we did a "find from cursor", otherwise it's pointless.
0585     // Well, unless the user can modify the document during a search operation,
0586     // hence the force boolean.
0587     if (!forceAsking && (d->options & KFind::FromCursor) == 0) {
0588         displayFinalDialog();
0589         return false;
0590     }
0591     QString message;
0592     if (showNumMatches) {
0593         if (numMatches()) {
0594             message = i18np("1 match found.", "%1 matches found.", numMatches());
0595         } else {
0596             message = i18n("No matches found for '<b>%1</b>'.", d->pattern.toHtmlEscaped());
0597         }
0598     } else {
0599         if (d->options & KFind::FindBackwards) {
0600             message = i18n("Beginning of document reached.");
0601         } else {
0602             message = i18n("End of document reached.");
0603         }
0604     }
0605 
0606     message += QLatin1String("<br><br>"); // can't be in the i18n() of the first if() because of the plural form.
0607     // Hope this word puzzle is ok, it's a different sentence
0608     message += (d->options & KFind::FindBackwards) ? i18n("Continue from the end?") : i18n("Continue from the beginning?");
0609 
0610     int ret = KMessageBox::questionTwoActions(dialogsParent(),
0611                                               QStringLiteral("<qt>%1</qt>").arg(message),
0612                                               QString(),
0613                                               KStandardGuiItem::cont(),
0614                                               KStandardGuiItem::stop());
0615     bool yes = (ret == KMessageBox::PrimaryAction);
0616     if (yes) {
0617         const_cast<KFindPrivate *>(d)->options &= ~KFind::FromCursor; // clear FromCursor option
0618     }
0619     return yes;
0620 }
0621 
0622 long KFind::options() const
0623 {
0624     Q_D(const KFind);
0625 
0626     return d->options;
0627 }
0628 
0629 void KFind::setOptions(long options)
0630 {
0631     Q_D(KFind);
0632 
0633     d->options = options;
0634 }
0635 
0636 void KFind::closeFindNextDialog()
0637 {
0638     Q_D(KFind);
0639 
0640     if (d->dialog) {
0641         d->dialog->deleteLater();
0642         d->dialog = nullptr;
0643     }
0644     d->dialogClosed = true;
0645 }
0646 
0647 int KFind::index() const
0648 {
0649     Q_D(const KFind);
0650 
0651     return d->index;
0652 }
0653 
0654 QString KFind::pattern() const
0655 {
0656     Q_D(const KFind);
0657 
0658     return d->pattern;
0659 }
0660 
0661 void KFind::setPattern(const QString &pattern)
0662 {
0663     Q_D(KFind);
0664 
0665     if (d->pattern != pattern) {
0666         d->patternChanged = true;
0667         d->matches = 0;
0668     }
0669 
0670     d->pattern = pattern;
0671 
0672     // TODO: KF6 change this comment once d->regExp is removed
0673     // set the options and rebuild d->regeExp if necessary
0674     setOptions(options());
0675 }
0676 
0677 int KFind::numMatches() const
0678 {
0679     Q_D(const KFind);
0680 
0681     return d->matches;
0682 }
0683 
0684 void KFind::resetCounts()
0685 {
0686     Q_D(KFind);
0687 
0688     d->matches = 0;
0689 }
0690 
0691 bool KFind::validateMatch(const QString &, int, int)
0692 {
0693     return true;
0694 }
0695 
0696 QWidget *KFind::parentWidget() const
0697 {
0698     return static_cast<QWidget *>(parent());
0699 }
0700 
0701 QWidget *KFind::dialogsParent() const
0702 {
0703     Q_D(const KFind);
0704 
0705     // If the find dialog is still up, it should get the focus when closing a message box
0706     // Otherwise, maybe the "find next?" dialog is up
0707     // Otherwise, the "view" is the parent.
0708     return d->findDialog ? static_cast<QWidget *>(d->findDialog) : (d->dialog ? d->dialog : parentWidget());
0709 }
0710 
0711 #include "kfind.moc"
0712 #include "moc_kfind.cpp"