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
19class SearchResultTreeViewPrivate
20{
21 SearchResultTreeViewPrivate() {}
22 QMap<QString, QString> projectInfoMap;
23
24 friend class SearchResultTreeView;
25};
26
27SearchResultTreeView::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
53void 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
90void SearchResultTreeView::clearData()
91{
92 auto model = qobject_cast<QStandardItemModel*>(SearchResultTreeView::model());
93 model->clear();
94}
95
96class 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
109SearchResultWindow::SearchResultWindow(QWidget *parent)
110 : QWidget(parent)
111 , d(new SearchResultWindowPrivate())
112{
113 setupUi();
114}
115
116void 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
156void SearchResultWindow::setRepalceWidgtVisible(bool visible)
157{
158 d->replaceWidget->setVisible(visible);
159}
160
161void 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
198void 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
246void SearchResultWindow::clean()
247{
248 d->treeView->clearData();
249 emit back();
250}
251
252void 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
285void 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
302void SearchResultWindow::searchAgain()
303{
304 search(&d->searchParams);
305}
306
307void 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