| 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 | |