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"