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 ®Exp, 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 ®Exp, 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 ®Exp, 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)