File indexing completed on 2024-04-28 05:49:12

0001 /*
0002     SPDX-FileCopyrightText: 2021 Kåre Särs <kare.sars@iki.fi>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #pragma once
0008 
0009 #include <QAbstractItemModel>
0010 #include <QPointer>
0011 #include <QRegularExpression>
0012 #include <QString>
0013 #include <QTimer>
0014 #include <QUrl>
0015 
0016 #include <KTextEditor/Cursor>
0017 #include <KTextEditor/MovingRange>
0018 #include <KTextEditor/Range>
0019 #include <ktexteditor/application.h>
0020 
0021 /**
0022  * data holder for one match in one file
0023  * used to transfer and hold multiple matches at once via signals to avoid heavy costs for files with a lot of matches
0024  */
0025 class KateSearchMatch
0026 {
0027 public:
0028     QString preMatchStr;
0029     QString matchStr;
0030     QString postMatchStr;
0031     QString replaceText;
0032     KTextEditor::Range range;
0033     bool checked;
0034     bool matchesFilter;
0035 };
0036 
0037 class MatchModel : public QAbstractItemModel
0038 {
0039     Q_OBJECT
0040 public:
0041     enum SearchPlaces { CurrentFile, OpenFiles, Folder, Project, AllProjects };
0042     Q_ENUM(SearchPlaces)
0043 
0044     enum SearchState { SearchDone, Preparing, Searching, Replacing };
0045     Q_ENUM(SearchState)
0046 
0047     enum MatchDataRoles {
0048         FileUrlRole = Qt::UserRole,
0049         DocumentRole,
0050         FileNameRole,
0051         StartLineRole,
0052         StartColumnRole,
0053         EndLineRole,
0054         EndColumnRole,
0055         PreMatchRole,
0056         MatchRole,
0057         PostMatchRole,
0058         ReplacedRole,
0059         ReplaceTextRole,
0060         PlainTextRole,
0061         MatchItemRole,
0062         LastMatchedRangeInFileRole,
0063     };
0064     Q_ENUM(MatchDataRoles)
0065 
0066     static constexpr int PreContextLen = 80;
0067     static constexpr int PostContextLen = 100;
0068 
0069     typedef KateSearchMatch Match;
0070 
0071     /// Utility function that is used to figure out how much context text we want to show
0072     /// before and after the match
0073     /// @p lineLength length of the line we are in
0074     /// @return a pair, first = preContextStart i.e., the start position in line
0075     /// second = postContextLen i.e., how much of post text we will show
0076     static constexpr std::pair<int, int> contextLengths(int lineLength, int matchStart, int matchEnd)
0077     {
0078         int preContextStart = 0;
0079         const int requiredPostLen = lineLength - matchEnd;
0080         if (requiredPostLen < MatchModel::PostContextLen) {
0081             int extra = MatchModel::PostContextLen - requiredPostLen;
0082             preContextStart = qMax(0, matchStart - (MatchModel::PreContextLen + extra));
0083         } else {
0084             preContextStart = qMax(0, matchStart - MatchModel::PreContextLen);
0085         }
0086 
0087         int postContextLen = 0;
0088         const int requiredPreLen = matchStart;
0089         if (matchStart < MatchModel::PreContextLen) {
0090             int extra = MatchModel::PreContextLen - requiredPreLen;
0091             postContextLen = MatchModel::PostContextLen + extra;
0092         } else {
0093             postContextLen = MatchModel::PostContextLen;
0094         }
0095 
0096         return {preContextStart, postContextLen};
0097     }
0098 
0099 private:
0100     struct MatchFile {
0101         QUrl fileUrl;
0102         QList<KateSearchMatch> matches;
0103         QPointer<KTextEditor::Document> doc;
0104         Qt::CheckState checkState = Qt::Checked;
0105     };
0106 
0107 public:
0108     MatchModel(QObject *parent = nullptr);
0109     ~MatchModel() override;
0110 
0111     void setDocumentManager(KTextEditor::Application *manager);
0112 
0113     void setMatchColors(const QString &foreground, const QString &background, const QString &replaceBackground);
0114 
0115     void setSearchPlace(MatchModel::SearchPlaces searchPlace);
0116 
0117     void setSearchState(MatchModel::SearchState searchState);
0118 
0119     void setBaseSearchPath(const QString &baseSearchPath);
0120 
0121     void setProjectName(const QString &projectName);
0122 
0123     /** This function clears all matches in all files */
0124     void clear();
0125 
0126     bool isEmpty() const
0127     {
0128         return m_matchFiles.isEmpty();
0129     }
0130 
0131     const QList<KateSearchMatch> &fileMatches(KTextEditor::Document *doc) const;
0132 
0133     void updateMatchRanges(const QList<KTextEditor::MovingRange *> &ranges);
0134 
0135     void uncheckAll();
0136 
0137     static QString generateReplaceString(const QRegularExpressionMatch &match, const QString &replaceString);
0138 
0139 public Q_SLOTS:
0140 
0141     /** This function returns the row index of the specified file.
0142      * If the file does not exist in the model, the file will be added to the model. */
0143     int matchFileRow(const QUrl &fileUrl, KTextEditor::Document *doc) const;
0144 
0145     /** This function is used to add a new file */
0146     /** @p doc may be null if we are searching disk files for instance */
0147     void addMatches(const QUrl &fileUrl, const QList<KateSearchMatch> &searchMatches, KTextEditor::Document *doc);
0148 
0149     /** This function is used to set the last added file to the search list.
0150      * This is done to update the match tree when we generate the search file list. */
0151     void setFileListUpdate(const QString &path);
0152 
0153     /** Initiate a replace of all matches that have been checked.
0154      * The actual replacing is split up into slot calls that are added to the event loop */
0155     void replaceChecked(const QRegularExpression &regExp, const QString &replaceString);
0156 
0157     /** Cancel the replacing of checked matches. NOTE: This will only be handled when the next file is handled */
0158     void cancelReplace();
0159 
0160 Q_SIGNALS:
0161     void replaceDone();
0162 
0163     // QModelIndex api. Use with care if you are accessing it directly or access through 'Results' instead
0164 public:
0165     static bool isMatch(const QModelIndex &itemIndex);
0166     QModelIndex fileIndex(const QUrl &url, KTextEditor::Document *doc) const;
0167     QModelIndex firstMatch() const;
0168     QModelIndex lastMatch() const;
0169     QModelIndex firstFileMatch(KTextEditor::Document *doc) const;
0170     QModelIndex closestMatchAfter(KTextEditor::Document *doc, const KTextEditor::Cursor &cursor) const;
0171     QModelIndex closestMatchBefore(KTextEditor::Document *doc, const KTextEditor::Cursor &cursor) const;
0172     QModelIndex nextMatch(const QModelIndex &itemIndex) const;
0173     QModelIndex prevMatch(const QModelIndex &itemIndex) const;
0174 
0175     KTextEditor::Range matchRange(const QModelIndex &matchIndex) const;
0176 
0177     /** This function is used to replace a single match */
0178     bool replaceSingleMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString);
0179 
0180     void setFilterText(const QString &text);
0181     bool matchesFilter(const QModelIndex &index);
0182 
0183     // Model-View model functions
0184     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
0185     bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
0186     Qt::ItemFlags flags(const QModelIndex &index) const override;
0187     QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override;
0188     QModelIndex parent(const QModelIndex &child) const override;
0189     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
0190     int columnCount(const QModelIndex &parent = QModelIndex()) const override;
0191 
0192     /** This function is used to verify that the range has not been edited since the search and returns the match object with the needed captures.
0193      * @param rangeText is the QString for the range.
0194      * @param regExp is the regular-expression to check.
0195      * @return the match object that has captures on success or none on failure to match the previously found range.*/
0196     static QRegularExpressionMatch rangeTextMatches(const QString &rangeText, QRegularExpression regExp);
0197 
0198 private Q_SLOTS:
0199     void doReplaceNextMatch();
0200 
0201 private:
0202     bool replaceMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString);
0203 
0204     QString matchPath(const MatchFile &matchFile) const;
0205     QString infoHtmlString() const;
0206     QString fileToHtmlString(const MatchFile &matchFile) const;
0207     QString matchToHtmlString(const Match &match) const;
0208 
0209     QString infoToPlainText() const;
0210     QString fileToPlainText(const MatchFile &matchFile) const;
0211     static QString matchToPlainText(const Match &match);
0212 
0213     bool setFileChecked(int fileRow, bool checked);
0214 
0215     Match *matchFromIndex(const QModelIndex &matchIndex);
0216 
0217     QList<MatchFile> m_matchFiles;
0218     QHash<QUrl, int> m_matchFileIndexHash;
0219     // for unsaved documents with no url
0220     QHash<KTextEditor::Document *, int> m_matchUnsavedFileIndexHash;
0221     QString m_searchBackgroundColor;
0222     QString m_foregroundColor;
0223     QString m_replaceHighlightColor;
0224 
0225     Qt::CheckState m_infoCheckState = Qt::Checked;
0226     SearchPlaces m_searchPlace = CurrentFile;
0227     SearchState m_searchState = SearchDone;
0228     QString m_resultBaseDir;
0229     QString m_projectName;
0230     QUrl m_lastMatchUrl;
0231     QString m_lastSearchPath;
0232     QTimer m_infoUpdateTimer;
0233     QString m_filterText;
0234 
0235     // Replacing related objects
0236     KTextEditor::Application *m_docManager = nullptr;
0237     int m_replaceFile = -1;
0238     QRegularExpression m_regExp;
0239     QString m_replaceText;
0240     bool m_cancelReplace = true;
0241 };
0242 
0243 // tests
0244 // output = {start, postContextLen}
0245 
0246 // precontext starts from 0 instead of at a higher place as post context is small
0247 static_assert(MatchModel::contextLengths(140, 110, 114) == std::pair<int, int>{0, 100});
0248 static_assert(MatchModel::contextLengths(140, 90, 114) == std::pair<int, int>{0, 100});
0249 
0250 // post context length increases from hundered as precontext is small here
0251 static_assert(MatchModel::contextLengths(140, 40, 50) == std::pair<int, int>{0, 140});
0252 static_assert(MatchModel::contextLengths(140, 20, 50) == std::pair<int, int>{0, 160});
0253 
0254 Q_DECLARE_METATYPE(KateSearchMatch)