1 | // SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. |
2 | // |
3 | // SPDX-License-Identifier: GPL-3.0-or-later |
4 | |
5 | #include "searchresultwindow.h" |
6 | #include "common/common.h" |
7 | |
8 | #include <QVBoxLayout> |
9 | #include <QPushButton> |
10 | #include <QStandardItemModel> |
11 | #include <QProcess> |
12 | #include <QMessageBox> |
13 | #include <QDebug> |
14 | #include <QCheckBox> |
15 | #include <QLabel> |
16 | #include <QLineEdit> |
17 | #include <QtConcurrent> |
18 | |
19 | class SearchResultTreeViewPrivate |
20 | { |
21 | SearchResultTreeViewPrivate() {} |
22 | QMap<QString, QString> projectInfoMap; |
23 | |
24 | friend class SearchResultTreeView; |
25 | }; |
26 | |
27 | SearchResultTreeView::SearchResultTreeView(QWidget *parent) |
28 | : QTreeView(parent) |
29 | , d(new SearchResultTreeViewPrivate()) |
30 | { |
31 | QAbstractItemModel *itemModel = new QStandardItemModel(); |
32 | setModel(itemModel); |
33 | |
34 | QObject::connect(this, &QTreeView::doubleClicked, [=](const QModelIndex &index){ |
35 | if (!index.isValid()) |
36 | return; |
37 | if (!index.parent().isValid()) |
38 | return; |
39 | QModelIndex parentIndex = index.parent(); |
40 | QString filePath = parentIndex.data(Qt::UserRole+1).toString().trimmed(); |
41 | int lineNumber = index.data(Qt::UserRole+1).toInt(); |
42 | qInfo() << filePath << lineNumber; |
43 | |
44 | foreach (QString key, d->projectInfoMap.keys()) { |
45 | if (filePath.contains(key, Qt::CaseInsensitive)) { |
46 | editor.jumpToLineWithKey(key, d->projectInfoMap.value(key), filePath, lineNumber); |
47 | break; |
48 | } |
49 | } |
50 | }); |
51 | } |
52 | |
53 | void SearchResultTreeView::setData(FindItemList &itemList, QMap<QString, QString> projectInfoMap) |
54 | { |
55 | auto model = qobject_cast<QStandardItemModel*>(SearchResultTreeView::model()); |
56 | model->clear(); |
57 | d->projectInfoMap = projectInfoMap; |
58 | |
59 | QHash<QString, QList<QPair<int, QString>>> findItemHash; |
60 | for (FindItem findItem : itemList) { |
61 | QString key = findItem.filePathName; |
62 | QPair<int, QString> value = QPair<int, QString>(findItem.lineNumber, findItem.context); |
63 | if (findItemHash.contains(key)) { |
64 | QList<QPair<int, QString>> valueList = findItemHash.value(key); |
65 | valueList.append(value); |
66 | findItemHash[key] = valueList; |
67 | } else { |
68 | findItemHash.insert(key, {value}); |
69 | } |
70 | } |
71 | |
72 | QHash<QString, QList<QPair<int, QString>>>::const_iterator iter = findItemHash.begin(); |
73 | for (; iter != findItemHash.end(); ++iter) { |
74 | QList<QPair<int, QString>> contentList = iter.value(); |
75 | QStandardItem *parentItem = new QStandardItem(iter.key() + " (" + QString::number(contentList.count()) + ")" ); |
76 | parentItem->setData(QVariant::fromValue<QString>(iter.key())); |
77 | parentItem->setEditable(false); |
78 | model->appendRow(parentItem); |
79 | for (QPair<int, QString> content : contentList) { |
80 | QString title = QString::number(content.first) + " " + content.second; |
81 | QStandardItem *childItem = new QStandardItem(title); |
82 | childItem->setEditable(false); |
83 | int lineNumber = content.first; |
84 | childItem->setData(QVariant::fromValue<int>(lineNumber)); |
85 | parentItem->appendRow(childItem); |
86 | } |
87 | } |
88 | } |
89 | |
90 | void SearchResultTreeView::clearData() |
91 | { |
92 | auto model = qobject_cast<QStandardItemModel*>(SearchResultTreeView::model()); |
93 | model->clear(); |
94 | } |
95 | |
96 | class SearchResultWindowPrivate |
97 | { |
98 | SearchResultWindowPrivate(){} |
99 | SearchResultTreeView *treeView{nullptr}; |
100 | QWidget *replaceWidget{nullptr}; |
101 | QLineEdit *replaceEdit{nullptr}; |
102 | QLabel *resultLabel{nullptr}; |
103 | |
104 | SearchParams searchParams; |
105 | |
106 | friend class SearchResultWindow; |
107 | }; |
108 | |
109 | SearchResultWindow::SearchResultWindow(QWidget *parent) |
110 | : QWidget(parent) |
111 | , d(new SearchResultWindowPrivate()) |
112 | { |
113 | setupUi(); |
114 | } |
115 | |
116 | void SearchResultWindow::setupUi() |
117 | { |
118 | d->replaceWidget = new QWidget(); |
119 | QHBoxLayout *replaceLayout = new QHBoxLayout(); |
120 | QLabel *replaceLabel = new QLabel(QLabel::tr("Replace with:" )); |
121 | replaceLabel->setAlignment(Qt::AlignRight); |
122 | replaceLabel->setFixedWidth(120); |
123 | d->replaceEdit = new QLineEdit(); |
124 | d->replaceEdit->setFixedWidth(500); |
125 | QPushButton *replaceBtn = new QPushButton(QPushButton::tr("Replace" )); |
126 | replaceBtn->setFixedHeight(30); |
127 | d->replaceWidget->setLayout(replaceLayout); |
128 | |
129 | replaceLayout->addWidget(replaceLabel, 0, Qt::AlignRight); |
130 | replaceLayout->addWidget(d->replaceEdit, 0, Qt::AlignLeft); |
131 | replaceLayout->addWidget(replaceBtn, 0, Qt::AlignLeft); |
132 | replaceLayout->addStretch(); |
133 | |
134 | QHBoxLayout *hLayout = new QHBoxLayout(); |
135 | QPushButton *cleanBtn = new QPushButton(QPushButton::tr("Clean && Return" )); |
136 | cleanBtn->setFixedHeight(30); |
137 | d->resultLabel = new QLabel(); |
138 | hLayout->addWidget(d->replaceWidget, 0, Qt::AlignLeft); |
139 | hLayout->addWidget(cleanBtn, 0, Qt::AlignLeft); |
140 | hLayout->addWidget(d->resultLabel, 0, Qt::AlignLeft); |
141 | hLayout->addStretch(0); |
142 | |
143 | d->treeView = new SearchResultTreeView(); |
144 | QVBoxLayout *vLayout = new QVBoxLayout(); |
145 | vLayout->addLayout(hLayout); |
146 | vLayout->addWidget(d->treeView); |
147 | |
148 | connect(cleanBtn, &QPushButton::clicked, this, &SearchResultWindow::clean); |
149 | connect(replaceBtn, &QPushButton::clicked, this, &SearchResultWindow::replace); |
150 | |
151 | setLayout(vLayout); |
152 | |
153 | setRepalceWidgtVisible(false); |
154 | } |
155 | |
156 | void SearchResultWindow::setRepalceWidgtVisible(bool visible) |
157 | { |
158 | d->replaceWidget->setVisible(visible); |
159 | } |
160 | |
161 | void SearchResultWindow::search(SearchParams *params) |
162 | { |
163 | d->treeView->clearData(); |
164 | showMsg(true, "Searching, please wait..." ); |
165 | // exam: grep -rn -i -w "main" --include="*.txt" --exclude="*.txt" /project/test |
166 | QString filePath; |
167 | for (QString path : params->filePathList) { |
168 | filePath += path; |
169 | filePath += " " ; |
170 | } |
171 | |
172 | QString sensitiveFlag = params->sensitiveFlag ? "" : " -i " ; |
173 | QString wholeWordsFlag = params->wholeWordsFlag ? " -w " : "" ; |
174 | QString patternList = "" ; |
175 | foreach (QString pattern, params->patternsList) { |
176 | patternList += " --include=" + pattern; |
177 | } |
178 | QString exPatternList = "" ; |
179 | foreach (QString expattern, params->exPatternsList) { |
180 | exPatternList += " --exclude=" + expattern; |
181 | } |
182 | |
183 | QString cmd = QString("grep -rn " + sensitiveFlag + wholeWordsFlag |
184 | + "\"" + params->searchText + "\" " |
185 | + patternList + exPatternList + " " + filePath); |
186 | |
187 | d->searchParams.filePathList = params->filePathList; |
188 | d->searchParams.searchText = params->searchText; |
189 | d->searchParams.sensitiveFlag = params->sensitiveFlag; |
190 | d->searchParams.wholeWordsFlag = params->wholeWordsFlag; |
191 | d->searchParams.patternsList = params->patternsList; |
192 | d->searchParams.exPatternsList = params->exPatternsList; |
193 | d->searchParams.projectInfoMap = params->projectInfoMap; |
194 | |
195 | QtConcurrent::run(this, &SearchResultWindow::startSearch, cmd, filePath, params->projectInfoMap); |
196 | } |
197 | |
198 | void SearchResultWindow::startSearch(const QString &cmd, const QString &filePath, QMap<QString, QString> projectInfoMap) |
199 | { |
200 | QProcess process; |
201 | connect(&process, static_cast<void (QProcess::*)(int,QProcess::ExitStatus)>(&QProcess::finished), |
202 | [&](int exitcode, QProcess::ExitStatus exitStatus) { |
203 | if (0 == exitcode && exitStatus == QProcess::ExitStatus::NormalExit) { |
204 | QString output = QString(process.readAllStandardOutput()); |
205 | QStringList outputList = output.split(QRegExp("[\r\n]" ),QString::SkipEmptyParts); |
206 | FindItemList findItemList; |
207 | int resultCount = 0; |
208 | foreach (QString line, outputList) { |
209 | //search in folder: "filepath:lineNumber:text" |
210 | const auto IN_FOLDER_REG = QRegularExpression(R"((.+):([0-9]+):(.+))" , QRegularExpression::NoPatternOption); |
211 | |
212 | //search in file: "lineNumber:text" |
213 | const auto IN_FILE_REG = QRegularExpression(R"(([0-9]+):(.+))" , QRegularExpression::NoPatternOption); |
214 | |
215 | QRegularExpressionMatch regMatch; |
216 | if ((regMatch = IN_FOLDER_REG.match(line)).hasMatch()) { |
217 | qInfo() << regMatch; |
218 | FindItem findItem; |
219 | findItem.filePathName = regMatch.captured(1).trimmed().toStdString().c_str(); |
220 | findItem.lineNumber = regMatch.captured(2).trimmed().toInt(); |
221 | findItem.context = regMatch.captured(3).trimmed().toStdString().c_str(); |
222 | findItemList.append(findItem); |
223 | resultCount++; |
224 | } else if ((regMatch = IN_FILE_REG.match(line)).hasMatch()) { |
225 | qInfo() << regMatch; |
226 | FindItem findItem; |
227 | findItem.filePathName = filePath; |
228 | findItem.lineNumber = regMatch.captured(1).trimmed().toInt(); |
229 | findItem.context = regMatch.captured(2).trimmed().toStdString().c_str(); |
230 | findItemList.append(findItem); |
231 | resultCount++; |
232 | } |
233 | } |
234 | d->treeView->setData(findItemList, projectInfoMap); |
235 | QString msg = QString::number(resultCount) + " matches found." ; |
236 | showMsg(true, msg); |
237 | } else { |
238 | showMsg(false, "Search failed!" ); |
239 | } |
240 | }); |
241 | |
242 | process.start(cmd); |
243 | process.waitForFinished(); |
244 | } |
245 | |
246 | void SearchResultWindow::clean() |
247 | { |
248 | d->treeView->clearData(); |
249 | emit back(); |
250 | } |
251 | |
252 | void SearchResultWindow::replace() |
253 | { |
254 | d->treeView->clearData(); |
255 | showMsg(true, "Replacing, please wait..." ); |
256 | QString replaceText = d->replaceEdit->text(); |
257 | if (replaceText.isEmpty()) { |
258 | if (QMessageBox::Yes != QMessageBox::warning(this, QMessageBox::tr("Warning" ), QMessageBox::tr("Repalce text is empty, will continue?" ), |
259 | QMessageBox::Yes, QMessageBox::No)) { |
260 | return; |
261 | } |
262 | } |
263 | |
264 | if (QMessageBox::Yes != QMessageBox::warning(this, QMessageBox::tr("Warning" ), QMessageBox::tr("Will replace permanent, continue?" ), |
265 | QMessageBox::Yes, QMessageBox::No)) { |
266 | return; |
267 | } |
268 | |
269 | QString filePath; |
270 | for (QString path : d->searchParams.filePathList) { |
271 | filePath += path; |
272 | filePath += " " ; |
273 | } |
274 | |
275 | //exam: sed -i "s/main/main1/g" `grep -rl "main" /project/test` |
276 | QString cmd = "sed -i \"s/" + d->searchParams.searchText |
277 | + "/" + replaceText + "/g\" `grep -rl \"" + d->searchParams.searchText |
278 | + "\" " + filePath + "`" ; |
279 | QStringList options; |
280 | options << "-c" << cmd; |
281 | |
282 | QtConcurrent::run(this, &SearchResultWindow::startReplace, options); |
283 | } |
284 | |
285 | void SearchResultWindow::startReplace(const QStringList &options) |
286 | { |
287 | QProcess process; |
288 | connect(&process, static_cast<void (QProcess::*)(int,QProcess::ExitStatus)>(&QProcess::finished), |
289 | [&](int exitcode, QProcess::ExitStatus exitStatus) { |
290 | if (0 == exitcode && exitStatus == QProcess::ExitStatus::NormalExit) { |
291 | QString output = QString(process.readAllStandardOutput()); |
292 | searchAgain(); |
293 | } else { |
294 | showMsg(false, "Replace failed!" ); |
295 | } |
296 | }); |
297 | |
298 | process.start("/bin/sh" , options); |
299 | process.waitForFinished(); |
300 | } |
301 | |
302 | void SearchResultWindow::searchAgain() |
303 | { |
304 | search(&d->searchParams); |
305 | } |
306 | |
307 | void SearchResultWindow::showMsg(bool succeed, QString msg) |
308 | { |
309 | if (succeed) { |
310 | d->resultLabel->setStyleSheet("color:white;" ); |
311 | } else { |
312 | d->resultLabel->setStyleSheet("color:red;" ); |
313 | } |
314 | d->resultLabel->setText(msg); |
315 | } |
316 | |