File indexing completed on 2024-04-28 04:38:53

0001 /*
0002     SPDX-FileCopyrightText: 1999-2001 Bernd Gehrmann <bernd@kdevelop.org>
0003     SPDX-FileCopyrightText: 2007 Dukju Ahn <dukjuahn@gmail.com>
0004     SPDX-FileCopyrightText: 2008 Hamish Rodda <rodda@kde.org>
0005     SPDX-FileCopyrightText: 2010 Silvère Lestang <silvere.lestang@gmail.com>
0006     SPDX-FileCopyrightText: 2010 Julien Desgats <julien.desgats@gmail.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "grepjob.h"
0012 #include "grepfindthread.h"
0013 #include "grepoutputmodel.h"
0014 #include "greputil.h"
0015 
0016 #include "debug.h"
0017 
0018 #include <QDebug>
0019 #include <QFile>
0020 #include <QTextStream>
0021 
0022 #include <KEncodingProber>
0023 #include <KLocalizedString>
0024 
0025 #include <serialization/indexedstring.h>
0026 #include <interfaces/icore.h>
0027 #include <interfaces/iuicontroller.h>
0028 
0029 using namespace KDevelop;
0030 
0031 QDebug operator<<(QDebug debug, const GrepJobSettings& s)
0032 {
0033     const QDebugStateSaver saver(debug);
0034 
0035     debug.nospace() << '{';
0036 
0037     bool firstDataMember = true;
0038     const auto printDataMember = [&debug, &firstDataMember](const char* name, const auto& value) {
0039         if (firstDataMember) {
0040             firstDataMember = false;
0041         } else {
0042             // Separate the members with vertical bars,
0043             // because commas and semicolons separate items within string data members.
0044             debug << " | ";
0045         }
0046         debug << name << ": " << value;
0047     };
0048 
0049 #define p(memberName) printDataMember(#memberName, s.memberName)
0050     p(fromHistory);
0051 
0052     p(projectFilesOnly);
0053     p(caseSensitive);
0054     p(regexp);
0055 
0056     p(depth);
0057 
0058     p(pattern);
0059     p(searchTemplate);
0060     p(replacementTemplate);
0061     p(files);
0062     p(exclude);
0063     p(searchPaths);
0064 #undef p
0065 
0066     debug << '}';
0067 
0068     return debug;
0069 }
0070 
0071 GrepOutputItem::List grepFile(const QString &filename, const QRegExp &re)
0072 {
0073     GrepOutputItem::List res;
0074     QFile file(filename);
0075 
0076     if(!file.open(QIODevice::ReadOnly))
0077         return res;
0078     int lineno = 0;
0079 
0080 
0081     // detect encoding (unicode files can be feed forever, stops when confidence reachs 99%
0082     KEncodingProber prober;
0083     while(!file.atEnd() && prober.state() == KEncodingProber::Probing && prober.confidence() < 0.99) {
0084         prober.feed(file.read(0xFF));
0085     }
0086 
0087     // reads file with detected encoding
0088     file.seek(0);
0089     QTextStream stream(&file);
0090     if(prober.confidence()>0.7)
0091         stream.setCodec(prober.encoding().constData());
0092     while( !stream.atEnd() )
0093     {
0094         QString data = stream.readLine();
0095 
0096         // remove line terminators (in order to not match them)
0097         for (int pos = data.length()-1; pos >= 0 && (data[pos] == QLatin1Char('\r') || data[pos] == QLatin1Char('\n')); pos--) {
0098             data.chop(1);
0099         }
0100 
0101         int offset = 0;
0102         // allow empty string matching result in an infinite loop !
0103         while( re.indexIn(data, offset)!=-1 && re.cap(0).length() > 0 )
0104         {
0105             int start = re.pos(0);
0106             int end = start + re.cap(0).length();
0107 
0108             DocumentChangePointer change = DocumentChangePointer(new DocumentChange(
0109                 IndexedString(filename),
0110                 KTextEditor::Range(lineno, start, lineno, end),
0111                 re.cap(0), QString()));
0112 
0113             res << GrepOutputItem(change, data, false);
0114             offset = end;
0115         }
0116         lineno++;
0117     }
0118     file.close();
0119     return res;
0120 }
0121 
0122 GrepJob::GrepJob( QObject* parent )
0123     : KJob( parent )
0124     , m_workState(WorkUnstarted)
0125     , m_fileIndex(0)
0126     , m_findThread(nullptr)
0127     , m_findSomething(false)
0128 {
0129     qRegisterMetaType<GrepOutputItem::List>();
0130 
0131     setCapabilities(Killable);
0132     KDevelop::ICore::self()->uiController()->registerStatus(this);
0133 
0134     connect(this, &GrepJob::result, this, &GrepJob::testFinishState);
0135 }
0136 
0137 GrepJob::~GrepJob()
0138 {
0139     Q_ASSERT(m_workState == WorkDead);
0140 }
0141 
0142 QString GrepJob::statusName() const
0143 {
0144     return i18n("Find in Files");
0145 }
0146 
0147 void GrepJob::slotFindFinished()
0148 {
0149     Q_ASSERT(m_findThread && m_findThread->isFinished());
0150 
0151     if (m_workState == WorkCancelled) {
0152         dieAfterCancellation();
0153         return;
0154     }
0155     Q_ASSERT(m_workState == WorkCollectFiles);
0156 
0157     m_fileList = m_findThread->takeFiles();
0158     m_findThread->deleteLater();
0159     m_findThread = nullptr;
0160 
0161     if(m_fileList.isEmpty())
0162     {
0163         m_errorMessage = i18n("No files found matching the wildcard patterns");
0164         die();
0165         return;
0166     }
0167 
0168     if(!m_settings.regexp)
0169     {
0170         m_settings.pattern = QRegExp::escape(m_settings.pattern);
0171     }
0172 
0173     if(m_settings.regexp && QRegExp(m_settings.pattern).captureCount() > 0)
0174     {
0175         m_errorMessage = i18nc("Capture is the text which is \"captured\" with () in regular expressions "
0176                                     "see https://doc.qt.io/qt-5/qregexp.html#capturedTexts",
0177                                     "Captures are not allowed in pattern string");
0178         die();
0179         return;
0180     }
0181 
0182     QString pattern = substitudePattern(m_settings.searchTemplate, m_settings.pattern);
0183     m_regExp.setPattern(pattern);
0184     m_regExp.setPatternSyntax(QRegExp::RegExp2);
0185     m_regExp.setCaseSensitivity( m_settings.caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
0186     if(pattern == QRegExp::escape(pattern))
0187     {
0188         // enable wildcard mode when possible
0189         // if pattern has already been escaped (raw text serch) a second escape will result in a different string anyway
0190         m_regExp.setPatternSyntax(QRegExp::Wildcard);
0191     }
0192 
0193     if (m_outputModel) {
0194         m_outputModel->setRegExp(m_regExp);
0195         m_outputModel->setReplacementTemplate(m_settings.replacementTemplate);
0196     }
0197 
0198     emit showMessage(this, i18np("Searching for <b>%2</b> in one file",
0199                                  "Searching for <b>%2</b> in %1 files",
0200                                  m_fileList.length(),
0201                                  m_regExp.pattern().toHtmlEscaped()));
0202 
0203     m_workState = WorkGrep;
0204     QMetaObject::invokeMethod( this, "slotWork", Qt::QueuedConnection);
0205 }
0206 
0207 void GrepJob::slotWork()
0208 {
0209     Q_ASSERT(!m_findThread);
0210 
0211     switch(m_workState)
0212     {
0213         case WorkUnstarted:
0214         case WorkDead:
0215             Q_UNREACHABLE();
0216             break;
0217         case WorkStarting:
0218             m_workState = WorkCollectFiles;
0219             emit showProgress(this, 0,0,0);
0220             QMetaObject::invokeMethod(this, "slotWork", Qt::QueuedConnection);
0221             break;
0222         case WorkCollectFiles:
0223             m_findThread = new GrepFindFilesThread(this, m_directoryChoice, m_settings.depth, m_settings.files, m_settings.exclude, m_settings.projectFilesOnly);
0224             emit showMessage(this, i18n("Collecting files..."));
0225             connect(m_findThread, &GrepFindFilesThread::finished, this, &GrepJob::slotFindFinished);
0226             m_findThread->start();
0227             break;
0228         case WorkGrep:
0229             if(m_fileIndex < m_fileList.length())
0230             {
0231                 emit showProgress(this, 0, m_fileList.length(), m_fileIndex);
0232                 if(m_fileIndex < m_fileList.length()) {
0233                     QString file = m_fileList[m_fileIndex].toLocalFile();
0234                     GrepOutputItem::List items = grepFile(file, m_regExp);
0235 
0236                     if(!items.isEmpty())
0237                     {
0238                         m_findSomething = true;
0239                         emit foundMatches(file, items);
0240                     }
0241 
0242                     m_fileIndex++;
0243                 }
0244                 QMetaObject::invokeMethod(this, "slotWork", Qt::QueuedConnection);
0245             }
0246             else
0247             {
0248                 die();
0249             }
0250             break;
0251         case WorkCancelled:
0252             dieAfterCancellation();
0253             break;
0254     }
0255 }
0256 
0257 void GrepJob::die()
0258 {
0259     emit hideProgress(this);
0260     emit clearMessage(this);
0261     m_workState = WorkDead;
0262     emitResult();
0263 }
0264 
0265 void GrepJob::dieAfterCancellation()
0266 {
0267     Q_ASSERT(m_workState == WorkCancelled);
0268     m_errorMessage = i18n("Search aborted");
0269     die();
0270 }
0271 
0272 void GrepJob::start()
0273 {
0274     if (m_workState != WorkUnstarted) {
0275         qCWarning(PLUGIN_GREPVIEW) << "cannot start a grep job more than once, current state:" << m_workState;
0276         return;
0277     }
0278 
0279     m_workState = WorkStarting;
0280 
0281     m_outputModel->clear();
0282 
0283     connect(this, &GrepJob::foundMatches,
0284             m_outputModel, &GrepOutputModel::appendOutputs, Qt::QueuedConnection);
0285 
0286     QMetaObject::invokeMethod(this, "slotWork", Qt::QueuedConnection);
0287 }
0288 
0289 bool GrepJob::doKill()
0290 {
0291     if (m_workState == WorkUnstarted || m_workState == WorkDead) {
0292         m_workState = WorkDead;
0293         return true;
0294     }
0295     if (m_workState != WorkCancelled) {
0296         if (m_findThread) {
0297             Q_ASSERT(m_workState == WorkCollectFiles);
0298             m_findThread->tryAbort();
0299         }
0300         m_workState = WorkCancelled;
0301     }
0302     // Do not let KJob finish immediately if the state was neither Unstarted nor Dead:
0303     // * If m_findThread != nullptr, let it finish first.
0304     // * Otherwise, slotWork() is about to be invoked. Don't want this to be destroyed by KJob before that happens.
0305     return false;
0306 }
0307 
0308 void GrepJob::testFinishState(KJob *job)
0309 {
0310     Q_ASSERT(m_workState == WorkDead);
0311     if(!job->error())
0312     {
0313         if (!m_errorMessage.isEmpty()) {
0314             emit showErrorMessage(i18n("Failed: %1", m_errorMessage));
0315         }
0316         else if (!m_findSomething) {
0317             emit showMessage(this, i18n("No results found"));
0318         }
0319     }
0320 }
0321 
0322 void GrepJob::setOutputModel(GrepOutputModel* model)
0323 {
0324     m_outputModel = model;
0325 }
0326 
0327 void GrepJob::setDirectoryChoice(const QList<QUrl>& choice)
0328 {
0329     m_directoryChoice = choice;
0330 }
0331 
0332 void GrepJob::setSettings(const GrepJobSettings& settings)
0333 {
0334     m_settings = settings;
0335 
0336     setObjectName(i18n("Grep: %1", m_settings.pattern));
0337 }
0338 
0339 GrepJobSettings GrepJob::settings() const
0340 {
0341     return m_settings;
0342 }
0343 
0344 #include "moc_grepjob.cpp"