// Copyright (C) 2016 Orgad Shaneh . // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "gitgrep.h" #include "gitclient.h" #include "gittr.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace TextEditor; using namespace Utils; using namespace VcsBase; namespace Git::Internal { const char GitGrepRef[] = "GitGrepRef"; class GitGrepParameters { public: QString ref; bool recurseSubmodules = false; QString id() const { return recurseSubmodules ? ref + ".Rec" : ref; } }; static QStringView nextLine(QStringView *remainingInput) { const int newLinePos = remainingInput->indexOf('\n'); if (newLinePos < 0) { QStringView ret = *remainingInput; *remainingInput = QStringView(); return ret; } QStringView ret = remainingInput->left(newLinePos); *remainingInput = remainingInput->mid(newLinePos + 1); return ret; } struct Match { Match() = default; Match(int start, int length) : matchStart(start), matchLength(length) {} int matchStart = 0; int matchLength = 0; QStringList regexpCapturedTexts; }; static void processLine(QStringView line, SearchResultItems *resultList, const std::optional ®Exp, const QString &ref, const FilePath &directory) { if (line.isEmpty()) return; static const QLatin1String boldRed("\x1b[1;31m"); static const QLatin1String resetColor("\x1b[m"); SearchResultItem result; const int lineSeparator = line.indexOf(QChar::Null); QStringView filePath = line.left(lineSeparator); if (!ref.isEmpty() && filePath.startsWith(ref)) filePath = filePath.mid(ref.length()); result.setFilePath(directory.pathAppended(filePath.toString())); const int textSeparator = line.indexOf(QChar::Null, lineSeparator + 1); const int lineNumber = line.mid(lineSeparator + 1, textSeparator - lineSeparator - 1).toInt(); QString text = line.mid(textSeparator + 1).toString(); QList matches; while (true) { const int matchStart = text.indexOf(boldRed); if (matchStart == -1) break; const int matchTextStart = matchStart + boldRed.size(); const int matchEnd = text.indexOf(resetColor, matchTextStart); QTC_ASSERT(matchEnd != -1, break); const int matchLength = matchEnd - matchTextStart; Match match(matchStart, matchLength); const QString matchText = text.mid(matchTextStart, matchLength); if (regExp) match.regexpCapturedTexts = regExp->match(matchText).capturedTexts(); matches.append(match); text = text.left(matchStart) + matchText + text.mid(matchEnd + resetColor.size()); } result.setDisplayText(text); for (const auto &match : std::as_const(matches)) { result.setMainRange(lineNumber, match.matchStart, match.matchLength); result.setUserData(match.regexpCapturedTexts); result.setUseTextEditorFont(true); resultList->append(result); } } static SearchResultItems parse(const QFuture &future, const QString &input, const std::optional ®Exp, const QString &ref, const FilePath &directory) { SearchResultItems items; QStringView remainingInput(input); while (true) { if (future.isCanceled()) return {}; if (remainingInput.isEmpty()) break; const QStringView line = nextLine(&remainingInput); if (line.isEmpty()) continue; processLine(line, &items, regExp, ref, directory); } return items; } static void runGitGrep(QPromise &promise, const FileFindParameters ¶meters, const GitGrepParameters &gitParameters) { const auto setupProcess = [¶meters, gitParameters](Process &process) { const FilePath vcsBinary = gitClient().vcsBinary(parameters.searchDir); const Environment environment = gitClient().processEnvironment(vcsBinary); QStringList arguments = { "-c", "color.grep.match=bold red", "-c", "color.grep=always", "-c", "color.grep.filename=", "-c", "color.grep.lineNumber=", "grep", "-zn", "--no-full-name" }; if (!(parameters.flags & FindCaseSensitively)) arguments << "-i"; if (parameters.flags & FindWholeWords) arguments << "-w"; if (parameters.flags & FindRegularExpression) arguments << "-P"; else arguments << "-F"; arguments << "-e" << parameters.text; if (gitParameters.recurseSubmodules) arguments << "--recurse-submodules"; if (!gitParameters.ref.isEmpty()) { arguments << gitParameters.ref; } const QStringList filterArgs = parameters.nameFilters.isEmpty() ? QStringList("*") // needed for exclusion filters : parameters.nameFilters; const QStringList exclusionArgs = Utils::transform(parameters.exclusionFilters, [](const QString &filter) { return QString(":!" + filter); }); arguments << "--" << filterArgs << exclusionArgs; process.setEnvironment(environment); process.setCommand({vcsBinary, arguments}); process.setWorkingDirectory(parameters.searchDir); }; const QString ref = gitParameters.ref.isEmpty() ? QString() : gitParameters.ref + ':'; const auto outputParser = [&ref, ¶meters](const QFuture &future, const QString &input, const std::optional ®Exp) { return parse(future, input, regExp, ref, parameters.searchDir); }; TextEditor::searchInProcessOutput(promise, parameters, setupProcess, outputParser); } static bool isGitDirectory(const FilePath &path) { static IVersionControl *gitVc = VcsManager::versionControl(VcsBase::Constants::VCS_ID_GIT); QTC_ASSERT(gitVc, return false); return gitVc == VcsManager::findVersionControlForDirectory(path); } GitGrep::GitGrep() { m_widget = new QWidget; auto layout = new QHBoxLayout(m_widget); layout->setContentsMargins(0, 0, 0, 0); m_treeLineEdit = new FancyLineEdit; m_treeLineEdit->setPlaceholderText(Tr::tr("Tree (optional)")); m_treeLineEdit->setToolTip(Tr::tr("Can be HEAD, tag, local or remote branch, or a commit hash.\n" "Leave empty to search through the file system.")); const QRegularExpression refExpression("[\\S]*"); m_treeLineEdit->setValidator(new QRegularExpressionValidator(refExpression, this)); layout->addWidget(m_treeLineEdit); // asynchronously check git version, add "recurse submodules" option if available Utils::onResultReady(gitClient().gitVersion(), this, [this, pLayout = QPointer(layout)](const QVersionNumber &version) { if (version >= QVersionNumber{2, 13} && pLayout) { m_recurseSubmodules = new QCheckBox(Tr::tr("Recurse submodules")); pLayout->addWidget(m_recurseSubmodules); } }); FindInFiles *findInFiles = FindInFiles::instance(); QTC_ASSERT(findInFiles, return); connect(findInFiles, &FindInFiles::searchDirChanged, m_widget, [this](const FilePath &path) { setEnabled(isGitDirectory(path)); }); connect(this, &SearchEngine::enabledChanged, m_widget, &QWidget::setEnabled); findInFiles->addSearchEngine(this); } GitGrep::~GitGrep() { delete m_widget; } QString GitGrep::title() const { return Tr::tr("Git Grep"); } QString GitGrep::toolTip() const { const QString ref = m_treeLineEdit->text(); if (!ref.isEmpty()) return Tr::tr("Ref: %1\n%2").arg(ref); return QLatin1String("%1"); } QWidget *GitGrep::widget() const { return m_widget; } GitGrepParameters GitGrep::gitParameters() const { return {m_treeLineEdit->text(), m_recurseSubmodules && m_recurseSubmodules->isChecked()}; } void GitGrep::readSettings(const Store &s) { m_treeLineEdit->setText(s.value(GitGrepRef).toString()); } void GitGrep::writeSettings(Store &s) const { if (!m_treeLineEdit->text().isEmpty()) s.insert(GitGrepRef, m_treeLineEdit->text()); } SearchExecutor GitGrep::searchExecutor() const { return [gitParameters = gitParameters()](const FileFindParameters ¶meters) { return Utils::asyncRun(runGitGrep, parameters, gitParameters); }; } EditorOpener GitGrep::editorOpener() const { return [params = gitParameters()](const SearchResultItem &item, const FileFindParameters ¶meters) -> IEditor * { const QStringList &itemPath = item.path(); if (params.ref.isEmpty() || itemPath.isEmpty()) return nullptr; const FilePath path = FilePath::fromUserInput(itemPath.first()); IEditor *editor = gitClient().openShowEditor( parameters.searchDir, params.ref, path, GitClient::ShowEditor::OnlyIfDifferent); if (editor) editor->gotoLine(item.mainRange().begin.line, item.mainRange().begin.column); return editor; }; } } // Git::Internal