gitgrep.cpp 11.3 KB
Newer Older
Orgad Shaneh's avatar
Orgad Shaneh committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
/****************************************************************************
**
** Copyright (C) 2016 Orgad Shaneh <orgads@gmail.com>.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "gitgrep.h"
#include "gitclient.h"
#include "gitplugin.h"

30
#include <coreplugin/editormanager/editormanager.h>
31
#include <coreplugin/progressmanager/progressmanager.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
32
#include <coreplugin/vcsmanager.h>
33
#include <texteditor/findinfiles.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
34
#include <vcsbase/vcscommand.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
35 36
#include <vcsbase/vcsbaseconstants.h>

37
#include <utils/fancylineedit.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
38 39 40 41
#include <utils/filesearch.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <utils/runextensions.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
42
#include <utils/synchronousprocess.h>
43
#include <utils/textfileformat.h>
Orgad Shaneh's avatar
Orgad Shaneh committed
44

45
#include <QCheckBox>
Orgad Shaneh's avatar
Orgad Shaneh committed
46 47
#include <QFuture>
#include <QFutureWatcher>
48 49
#include <QHBoxLayout>
#include <QRegularExpressionValidator>
Orgad Shaneh's avatar
Orgad Shaneh committed
50
#include <QScopedPointer>
Orgad Shaneh's avatar
Orgad Shaneh committed
51
#include <QSettings>
Orgad Shaneh's avatar
Orgad Shaneh committed
52
#include <QTextStream>
Orgad Shaneh's avatar
Orgad Shaneh committed
53 54 55 56

namespace Git {
namespace Internal {

57 58 59 60 61 62 63
class GitGrepParameters
{
public:
    QString ref;
    bool isEnabled = false;
};

Orgad Shaneh's avatar
Orgad Shaneh committed
64
using namespace Core;
Orgad Shaneh's avatar
Orgad Shaneh committed
65
using namespace Utils;
Orgad Shaneh's avatar
Orgad Shaneh committed
66
using VcsBase::VcsCommand;
Orgad Shaneh's avatar
Orgad Shaneh committed
67 68 69

namespace {

70
const char EnableGitGrep[] = "EnableGitGrep";
71
const char GitGrepRef[] = "GitGrepRef";
72

Orgad Shaneh's avatar
Orgad Shaneh committed
73 74 75 76 77 78 79 80 81 82 83 84 85
class GitGrepRunner : public QObject
{
    using FutureInterfaceType = QFutureInterface<FileSearchResultList>;

public:
    GitGrepRunner(FutureInterfaceType &fi,
                  const TextEditor::FileFindParameters &parameters) :
        m_fi(fi),
        m_parameters(parameters)
    {
        m_directory = parameters.additionalParameters.toString();
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
86
    void processLine(const QString &line, FileSearchResultList *resultList) const
Orgad Shaneh's avatar
Orgad Shaneh committed
87 88 89
    {
        if (line.isEmpty())
            return;
Orgad Shaneh's avatar
Orgad Shaneh committed
90 91
        static const QLatin1String boldRed("\x1b[1;31m");
        static const QLatin1String resetColor("\x1b[m");
Orgad Shaneh's avatar
Orgad Shaneh committed
92
        FileSearchResult single;
Orgad Shaneh's avatar
Orgad Shaneh committed
93
        const int lineSeparator = line.indexOf(QChar::Null);
94 95 96 97
        QString filePath = line.left(lineSeparator);
        if (!m_ref.isEmpty() && filePath.startsWith(m_ref))
            filePath.remove(0, m_ref.length());
        single.fileName = m_directory + QLatin1Char('/') + filePath;
Orgad Shaneh's avatar
Orgad Shaneh committed
98
        const int textSeparator = line.indexOf(QChar::Null, lineSeparator + 1);
Orgad Shaneh's avatar
Orgad Shaneh committed
99
        single.lineNumber = line.mid(lineSeparator + 1, textSeparator - lineSeparator - 1).toInt();
Orgad Shaneh's avatar
Orgad Shaneh committed
100
        QString text = line.mid(textSeparator + 1);
Orgad Shaneh's avatar
Orgad Shaneh committed
101 102 103 104 105
        QVector<QPair<int, int>> matches;
        for (;;) {
            const int matchStart = text.indexOf(boldRed);
            if (matchStart == -1)
                break;
Orgad Shaneh's avatar
Orgad Shaneh committed
106
            const int matchTextStart = matchStart + boldRed.size();
Orgad Shaneh's avatar
Orgad Shaneh committed
107 108 109 110 111
            const int matchEnd = text.indexOf(resetColor, matchTextStart);
            QTC_ASSERT(matchEnd != -1, break);
            const int matchLength = matchEnd - matchTextStart;
            matches.append(qMakePair(matchStart, matchLength));
            text = text.left(matchStart) + text.mid(matchTextStart, matchLength)
Orgad Shaneh's avatar
Orgad Shaneh committed
112
                    + text.mid(matchEnd + resetColor.size());
Orgad Shaneh's avatar
Orgad Shaneh committed
113
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
114
        single.matchingLine = text;
Orgad Shaneh's avatar
Orgad Shaneh committed
115 116 117 118 119 120 121
        foreach (auto match, matches) {
            single.matchStart = match.first;
            single.matchLength = match.second;
            resultList->append(single);
        }
    }

Orgad Shaneh's avatar
Orgad Shaneh committed
122
    void read(const QString &text)
Orgad Shaneh's avatar
Orgad Shaneh committed
123 124
    {
        FileSearchResultList resultList;
Orgad Shaneh's avatar
Orgad Shaneh committed
125 126 127 128
        QString t = text;
        QTextStream stream(&t);
        while (!stream.atEnd() && !m_fi.isCanceled())
            processLine(stream.readLine(), &resultList);
Orgad Shaneh's avatar
Orgad Shaneh committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
        if (!resultList.isEmpty())
            m_fi.reportResult(resultList);
    }

    void exec()
    {
        QStringList arguments;
        arguments << QLatin1String("-c") << QLatin1String("color.grep.match=bold red")
                  << QLatin1String("grep") << QLatin1String("-zn")
                  << QLatin1String("--color=always");
        if (!(m_parameters.flags & FindCaseSensitively))
            arguments << QLatin1String("-i");
        if (m_parameters.flags & FindWholeWords)
            arguments << QLatin1String("-w");
        if (m_parameters.flags & FindRegularExpression)
            arguments << QLatin1String("-P");
        else
            arguments << QLatin1String("-F");
        arguments << m_parameters.text;
148 149 150 151 152
        GitGrepParameters params = m_parameters.extensionParameters.value<GitGrepParameters>();
        if (!params.ref.isEmpty()) {
            arguments << params.ref;
            m_ref = params.ref + QLatin1Char(':');
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
153
        arguments << QLatin1String("--") << m_parameters.nameFilters;
Tobias Hunger's avatar
Tobias Hunger committed
154
        QScopedPointer<VcsCommand> command(GitPlugin::client()->createCommand(m_directory));
Orgad Shaneh's avatar
Orgad Shaneh committed
155 156
        command->addFlags(VcsCommand::SilentOutput);
        command->setProgressiveOutput(true);
Orgad Shaneh's avatar
Orgad Shaneh committed
157 158 159
        QFutureWatcher<FileSearchResultList> watcher;
        watcher.setFuture(m_fi.future());
        connect(&watcher, &QFutureWatcher<FileSearchResultList>::canceled,
Orgad Shaneh's avatar
Orgad Shaneh committed
160 161
                command.data(), &VcsCommand::cancel);
        connect(command.data(), &VcsCommand::stdOutText, this, &GitGrepRunner::read);
Tobias Hunger's avatar
Tobias Hunger committed
162
        SynchronousProcessResponse resp = command->runCommand(GitPlugin::client()->vcsBinary(), arguments, 0);
163 164 165 166
        switch (resp.result) {
        case SynchronousProcessResponse::TerminatedAbnormally:
        case SynchronousProcessResponse::StartFailed:
        case SynchronousProcessResponse::Hang:
Orgad Shaneh's avatar
Orgad Shaneh committed
167
            m_fi.reportCanceled();
168 169 170 171 172 173 174
            break;
        case SynchronousProcessResponse::Finished:
        case SynchronousProcessResponse::FinishedError:
            // When no results are found, git-grep exits with non-zero status.
            // Do not consider this as an error.
            break;
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
175 176 177 178 179 180
    }

    static void run(QFutureInterface<FileSearchResultList> &fi,
                    TextEditor::FileFindParameters parameters)
    {
        GitGrepRunner runner(fi, parameters);
181
        Core::ProgressTimer progress(fi, 20);
Orgad Shaneh's avatar
Orgad Shaneh committed
182 183 184 185 186 187
        runner.exec();
    }

private:
    FutureInterfaceType m_fi;
    QString m_directory;
188
    QString m_ref;
Orgad Shaneh's avatar
Orgad Shaneh committed
189 190 191 192 193
    const TextEditor::FileFindParameters &m_parameters;
};

} // namespace

194
static bool validateDirectory(const QString &path)
Orgad Shaneh's avatar
Orgad Shaneh committed
195
{
196 197 198
    static IVersionControl *gitVc = VcsManager::versionControl(VcsBase::Constants::VCS_ID_GIT);
    QTC_ASSERT(gitVc, return false);
    return gitVc == VcsManager::findVersionControlForDirectory(path, 0);
Orgad Shaneh's avatar
Orgad Shaneh committed
199 200
}

201
GitGrep::GitGrep()
Orgad Shaneh's avatar
Orgad Shaneh committed
202
{
203 204 205 206 207 208 209 210 211 212 213 214
    m_widget = new QWidget;
    auto layout = new QHBoxLayout(m_widget);
    layout->setMargin(0);
    m_enabledCheckBox = new QCheckBox(tr("&Use Git Grep"));
    m_enabledCheckBox->setToolTip(tr("Use Git Grep for searching. This includes only files "
                                     "that are managed by Git."));
    layout->addWidget(m_enabledCheckBox);
    m_treeLineEdit = new FancyLineEdit;
    m_treeLineEdit->setPlaceholderText(
                tr("Tree: add reference here or leave empty to search through the file system)"));
    m_treeLineEdit->setToolTip(
                tr("Reference can be HEAD, tag, local or remote branch, or a commit hash."));
Orgad Shaneh's avatar
Orgad Shaneh committed
215
    const QRegularExpression refExpression(QLatin1String("[\\S]*"));
216 217
    m_treeLineEdit->setValidator(new QRegularExpressionValidator(refExpression, this));
    layout->addWidget(m_treeLineEdit);
218 219
    TextEditor::FindInFiles *findInFiles = TextEditor::FindInFiles::instance();
    QTC_ASSERT(findInFiles, return);
Orgad Shaneh's avatar
Orgad Shaneh committed
220 221
    connect(findInFiles, &TextEditor::FindInFiles::pathChanged,
            m_widget, [this](const QString &path) {
222 223 224
        m_widget->setEnabled(validateDirectory(path));
    });
    findInFiles->setFindExtension(this);
Orgad Shaneh's avatar
Orgad Shaneh committed
225 226
}

227
GitGrep::~GitGrep()
Orgad Shaneh's avatar
Orgad Shaneh committed
228
{
Orgad Shaneh's avatar
Orgad Shaneh committed
229
    delete m_widget;
Orgad Shaneh's avatar
Orgad Shaneh committed
230 231
}

232
QString GitGrep::title() const
Orgad Shaneh's avatar
Orgad Shaneh committed
233
{
234
    return tr("Git Grep");
Orgad Shaneh's avatar
Orgad Shaneh committed
235 236
}

237 238 239 240 241 242 243 244
QString GitGrep::toolTip() const
{
    const QString ref = m_treeLineEdit->text();
    if (!ref.isEmpty())
        return tr("Ref: %1\n%2").arg(ref);
    return QLatin1String("%1");
}

245
QWidget *GitGrep::widget() const
Orgad Shaneh's avatar
Orgad Shaneh committed
246
{
Orgad Shaneh's avatar
Orgad Shaneh committed
247
    return m_widget;
Orgad Shaneh's avatar
Orgad Shaneh committed
248 249
}

250
bool GitGrep::isEnabled() const
Orgad Shaneh's avatar
Orgad Shaneh committed
251
{
252
    return m_widget->isEnabled() && m_enabledCheckBox->isChecked();
Orgad Shaneh's avatar
Orgad Shaneh committed
253 254
}

255
bool GitGrep::isEnabled(const TextEditor::FileFindParameters &parameters) const
Orgad Shaneh's avatar
Orgad Shaneh committed
256
{
257
    return parameters.extensionParameters.value<GitGrepParameters>().isEnabled;
Orgad Shaneh's avatar
Orgad Shaneh committed
258 259
}

260
QVariant GitGrep::parameters() const
Orgad Shaneh's avatar
Orgad Shaneh committed
261
{
262 263 264 265
    GitGrepParameters params;
    params.isEnabled = isEnabled();
    params.ref = m_treeLineEdit->text();
    return qVariantFromValue(params);
Orgad Shaneh's avatar
Orgad Shaneh committed
266 267 268 269
}

void GitGrep::readSettings(QSettings *settings)
{
270 271
    m_enabledCheckBox->setChecked(settings->value(QLatin1String(EnableGitGrep), false).toBool());
    m_treeLineEdit->setText(settings->value(QLatin1String(GitGrepRef)).toString());
Orgad Shaneh's avatar
Orgad Shaneh committed
272 273
}

274
void GitGrep::writeSettings(QSettings *settings) const
Orgad Shaneh's avatar
Orgad Shaneh committed
275
{
276 277
    settings->setValue(QLatin1String(EnableGitGrep), m_enabledCheckBox->isChecked());
    settings->setValue(QLatin1String(GitGrepRef), m_treeLineEdit->text());
Orgad Shaneh's avatar
Orgad Shaneh committed
278 279
}

280 281
QFuture<FileSearchResultList> GitGrep::executeSearch(
        const TextEditor::FileFindParameters &parameters)
Orgad Shaneh's avatar
Orgad Shaneh committed
282
{
283
    return Utils::runAsync(GitGrepRunner::run, parameters);
Orgad Shaneh's avatar
Orgad Shaneh committed
284 285
}

286 287 288 289 290 291 292 293 294 295
IEditor *GitGrep::openEditor(const SearchResultItem &item,
                             const TextEditor::FileFindParameters &parameters)
{
    GitGrepParameters params = parameters.extensionParameters.value<GitGrepParameters>();
    if (!params.isEnabled || params.ref.isEmpty() || item.path.isEmpty())
        return nullptr;
    const QString path = QDir::fromNativeSeparators(item.path.first());
    QByteArray content;
    const QString topLevel = parameters.additionalParameters.toString();
    const QString relativePath = QDir(topLevel).relativeFilePath(path);
Tobias Hunger's avatar
Tobias Hunger committed
296 297
    if (!GitPlugin::client()->synchronousShow(topLevel, params.ref + QLatin1String(":./") + relativePath,
                                              &content, nullptr)) {
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
        return nullptr;
    }
    if (content.isEmpty())
        return nullptr;
    QByteArray fileContent;
    if (TextFileFormat::readFileUTF8(path, 0, &fileContent, 0) == TextFileFormat::ReadSuccess) {
        if (fileContent == content)
            return nullptr; // open the file for read/write
    }
    QString title = tr("Git Show %1:%2").arg(params.ref).arg(relativePath);
    IEditor *editor = EditorManager::openEditorWithContents(Id(), &title, content, title,
                                                            EditorManager::DoNotSwitchToDesignMode);
    editor->gotoLine(item.lineNumber, item.textMarkPos);
    editor->document()->setTemporary(true);
    return editor;
}

Orgad Shaneh's avatar
Orgad Shaneh committed
315 316
} // Internal
} // Git
317 318

Q_DECLARE_METATYPE(Git::Internal::GitGrepParameters)