basefilefind.cpp 18.9 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3 4
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8 9 10 11
** 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
12 13 14
** 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.
15
**
16 17 18 19 20 21 22
** 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.
con's avatar
con committed
23
**
hjk's avatar
hjk committed
24
****************************************************************************/
hjk's avatar
hjk committed
25

con's avatar
con committed
26
#include "basefilefind.h"
27
#include "textdocument.h"
con's avatar
con committed
28

29
#include <aggregation/aggregate.h>
30
#include <coreplugin/icore.h>
31
#include <coreplugin/progressmanager/progressmanager.h>
32
#include <coreplugin/progressmanager/futureprogress.h>
33
#include <coreplugin/dialogs/readonlyfilesdialog.h>
34
#include <coreplugin/documentmanager.h>
35
#include <coreplugin/editormanager/editormanager.h>
36
#include <coreplugin/find/ifindsupport.h>
37
#include <texteditor/texteditor.h>
38
#include <texteditor/refactoringchanges.h>
39
#include <utils/fadingindicator.h>
40
#include <utils/filesearch.h>
41 42
#include <utils/qtcassert.h>
#include <utils/stylehelper.h>
con's avatar
con committed
43

44 45 46 47
#include <QDebug>
#include <QSettings>
#include <QHash>
#include <QPair>
48 49 50
#include <QStringListModel>
#include <QFutureWatcher>
#include <QPointer>
51
#include <QComboBox>
52
#include <QLabel>
con's avatar
con committed
53

54 55 56
using namespace Utils;
using namespace Core;

57 58
namespace TextEditor {
namespace Internal {
59

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
namespace {
class InternalEngine : public TextEditor::SearchEngine
{
public:
    InternalEngine() : m_widget(new QWidget) {}
    ~InternalEngine() override { delete m_widget;}
    QString title() const override { return tr("Internal"); }
    QString toolTip() const override { return QString(); }
    QWidget *widget() const override { return m_widget; }
    bool isEnabled() const override { return true; }
    QVariant parameters() const override { return QVariant(); }
    void readSettings(QSettings */*settings*/) override {}
    void writeSettings(QSettings */*settings*/) const override {}
    QFuture<Utils::FileSearchResultList> executeSearch(
            const TextEditor::FileFindParameters &parameters,
            BaseFileFind *baseFileFind) override
    {
        auto func = parameters.flags & FindRegularExpression
                ? Utils::findInFilesRegExp
                : Utils::findInFiles;

        return func(parameters.text,
                    baseFileFind->files(parameters.nameFilters, parameters.additionalParameters),
                    textDocumentFlagsForFindFlags(parameters.flags),
                    TextDocument::openedTextDocumentContents());

    }
    Core::IEditor *openEditor(const Core::SearchResultItem &/*item*/,
                              const TextEditor::FileFindParameters &/*parameters*/) override
    {
        return nullptr;
    }

private:
    QWidget *m_widget;
};
}


Orgad Shaneh's avatar
Orgad Shaneh committed
99 100 101 102 103 104 105
class CountingLabel : public QLabel
{
public:
    CountingLabel();
    void updateCount(int count);
};

106 107
class BaseFileFindPrivate
{
108
public:
109
    ~BaseFileFindPrivate() { delete m_internalSearchEngine; }
110 111
    QMap<QFutureWatcher<FileSearchResultList> *, QPointer<SearchResult> > m_watchers;
    QPointer<IFindSupport> m_currentFindSupport;
112

113
    QLabel *m_resultLabel = 0;
114 115 116
    QStringListModel m_filterStrings;
    QString m_filterSetting;
    QPointer<QComboBox> m_filterCombo;
117 118 119
    QVector<SearchEngine *> m_searchEngines;
    SearchEngine *m_internalSearchEngine;
    int m_currentSearchEngineIndex = 0;
120
};
121

122 123
} // namespace Internal

124
using namespace Internal;
125 126

BaseFileFind::BaseFileFind() : d(new BaseFileFindPrivate)
con's avatar
con committed
127
{
128 129
    d->m_internalSearchEngine = new InternalEngine;
    addSearchEngine(d->m_internalSearchEngine);
con's avatar
con committed
130 131
}

132 133
BaseFileFind::~BaseFileFind()
{
134
    delete d;
135 136
}

con's avatar
con committed
137 138
bool BaseFileFind::isEnabled() const
{
139
    return true;
con's avatar
con committed
140 141
}

dt's avatar
dt committed
142 143
void BaseFileFind::cancel()
{
144 145
    SearchResult *search = qobject_cast<SearchResult *>(sender());
    QTC_ASSERT(search, return);
146
    QFutureWatcher<FileSearchResultList> *watcher = d->m_watchers.key(search);
147 148
    QTC_ASSERT(watcher, return);
    watcher->cancel();
dt's avatar
dt committed
149 150
}

151 152 153 154
void BaseFileFind::setPaused(bool paused)
{
    SearchResult *search = qobject_cast<SearchResult *>(sender());
    QTC_ASSERT(search, return);
155
    QFutureWatcher<FileSearchResultList> *watcher = d->m_watchers.key(search);
156 157 158 159 160
    QTC_ASSERT(watcher, return);
    if (!paused || watcher->isRunning()) // guard against pausing when the search is finished
        watcher->setPaused(paused);
}

con's avatar
con committed
161 162 163
QStringList BaseFileFind::fileNameFilters() const
{
    QStringList filters;
164
    if (d->m_filterCombo && !d->m_filterCombo->currentText().isEmpty()) {
165
        const QStringList parts = d->m_filterCombo->currentText().split(',');
con's avatar
con committed
166
        foreach (const QString &part, parts) {
167
            const QString filter = part.trimmed();
168
            if (!filter.isEmpty())
con's avatar
con committed
169 170 171 172 173 174
                filters << filter;
        }
    }
    return filters;
}

175
SearchEngine *BaseFileFind::currentSearchEngine() const
176
{
177 178 179 180 181 182 183 184 185 186 187 188 189
    if (d->m_searchEngines.isEmpty() || d->m_currentSearchEngineIndex == -1)
        return nullptr;
    return d->m_searchEngines[d->m_currentSearchEngineIndex];
}

QVector<SearchEngine *> BaseFileFind::searchEngines() const
{
    return d->m_searchEngines;
}

void BaseFileFind::setCurrentSearchEngine(int index)
{
    d->m_currentSearchEngineIndex = index;
190 191
}

192
void BaseFileFind::runNewSearch(const QString &txt, FindFlags findFlags,
193
                                    SearchResultWindow::SearchMode searchMode)
con's avatar
con committed
194
{
195 196 197
    d->m_currentFindSupport = 0;
    if (d->m_filterCombo)
        updateComboEntries(d->m_filterCombo, true);
198
    QString tooltip = toolTip();
199 200 201 202 203 204

    SearchResult *search = SearchResultWindow::instance()->startNewSearch(
                label(),
                tooltip.arg(IFindFilter::descriptionForFindFlags(findFlags)),
                txt, searchMode, SearchResultWindow::PreserveCaseEnabled,
                QString::fromLatin1("TextEditor"));
205
    search->setTextToReplace(txt);
206 207 208 209 210 211
    search->setSearchAgainSupported(true);
    FileFindParameters parameters;
    parameters.text = txt;
    parameters.flags = findFlags;
    parameters.nameFilters = fileNameFilters();
    parameters.additionalParameters = additionalParameters();
212 213
    parameters.searchEngineParameters = currentSearchEngine()->parameters();
    parameters.searchEngineIndex = d->m_currentSearchEngineIndex;
214
    search->setUserData(qVariantFromValue(parameters));
215 216 217 218 219 220 221 222 223
    connect(search, &SearchResult::activated, this, &BaseFileFind::openEditor);
    if (searchMode == SearchResultWindow::SearchAndReplace)
        connect(search, &SearchResult::replaceButtonClicked, this, &BaseFileFind::doReplace);
    connect(search, &SearchResult::visibilityChanged, this, &BaseFileFind::hideHighlightAll);
    connect(search, &SearchResult::cancelled, this, &BaseFileFind::cancel);
    connect(search, &SearchResult::paused, this, &BaseFileFind::setPaused);
    connect(search, &SearchResult::searchAgainRequested, this, &BaseFileFind::searchAgain);
    connect(this, &BaseFileFind::enabledChanged, search, &SearchResult::requestEnabledCheck);
    connect(search, &SearchResult::requestEnabledCheck, this, &BaseFileFind::recheckEnabled);
224

225 226 227
    runSearch(search);
}

228
void BaseFileFind::runSearch(SearchResult *search)
229 230
{
    FileFindParameters parameters = search->userData().value<FileFindParameters>();
231
    CountingLabel *label = new CountingLabel;
232
    connect(search, &SearchResult::countChanged, label, &CountingLabel::updateCount);
233
    CountingLabel *statusLabel = new CountingLabel;
234
    connect(search, &SearchResult::countChanged, statusLabel, &CountingLabel::updateCount);
235
    SearchResultWindow::instance()->popup(IOutputPane::Flags(IOutputPane::ModeSwitch|IOutputPane::WithFocus));
236
    QFutureWatcher<FileSearchResultList> *watcher = new QFutureWatcher<FileSearchResultList>();
237
    d->m_watchers.insert(watcher, search);
238
    watcher->setPendingResultsLimit(1);
239 240
    connect(watcher, &QFutureWatcherBase::resultReadyAt, this, &BaseFileFind::displayResult);
    connect(watcher, &QFutureWatcherBase::finished, this, &BaseFileFind::searchFinished);
Orgad Shaneh's avatar
Orgad Shaneh committed
241
    watcher->setFuture(executeSearch(parameters));
242
    FutureProgress *progress =
243
        ProgressManager::addTask(watcher->future(), tr("Searching"), Constants::TASK_SEARCH);
244
    progress->setWidget(label);
245
    progress->setStatusBarWidget(statusLabel);
246
    connect(progress, &FutureProgress::clicked, search, &SearchResult::popup);
con's avatar
con committed
247 248
}

249
void BaseFileFind::findAll(const QString &txt, FindFlags findFlags)
250 251 252 253
{
    runNewSearch(txt, findFlags, SearchResultWindow::SearchOnly);
}

254
void BaseFileFind::replaceAll(const QString &txt, FindFlags findFlags)
255
{
256
    runNewSearch(txt, findFlags, SearchResultWindow::SearchAndReplace);
257 258
}

259
void BaseFileFind::addSearchEngine(SearchEngine *searchEngine)
260
{
261
    d->m_searchEngines.push_back(searchEngine);
262 263
}

264
void BaseFileFind::doReplace(const QString &text,
265
                             const QList<SearchResultItem> &items,
266
                             bool preserveCase)
267
{
268
    QStringList files = replaceAll(text, items, preserveCase);
269
    if (!files.isEmpty()) {
270
        Utils::FadingIndicator::showText(ICore::mainWindow(),
271
            tr("%n occurrences replaced.", 0, items.size()),
272
            Utils::FadingIndicator::SmallText);
273
        DocumentManager::notifyFilesChangedInternally(files);
274
        SearchResultWindow::instance()->hide();
275 276 277
    }
}

con's avatar
con committed
278
void BaseFileFind::displayResult(int index) {
279 280
    QFutureWatcher<FileSearchResultList> *watcher =
            static_cast<QFutureWatcher<FileSearchResultList> *>(sender());
281
    SearchResult *search = d->m_watchers.value(watcher);
282 283 284
    if (!search) {
        // search was removed from search history while the search is running
        watcher->cancel();
285 286
        return;
    }
287 288 289 290
    FileSearchResultList results = watcher->resultAt(index);
    QList<SearchResultItem> items;
    foreach (const FileSearchResult &result, results) {
        SearchResultItem item;
291
        item.path = QStringList() << QDir::toNativeSeparators(result.fileName);
292
        item.lineNumber = result.lineNumber;
293 294 295 296
        item.text = result.matchingLine;
        item.textMarkLength = result.matchLength;
        item.textMarkPos = result.matchStart;
        item.useTextEditorFont = true;
297 298 299
        item.userData = result.regexpCapturedTexts;
        items << item;
    }
300
    search->addResults(items, SearchResult::AddOrdered);
con's avatar
con committed
301 302 303 304
}

void BaseFileFind::searchFinished()
{
305 306
    QFutureWatcher<FileSearchResultList> *watcher =
            static_cast<QFutureWatcher<FileSearchResultList> *>(sender());
307
    SearchResult *search = d->m_watchers.value(watcher);
308
    if (search)
Eike Ziller's avatar
Eike Ziller committed
309
        search->finishSearch(watcher->isCanceled());
310
    d->m_watchers.remove(watcher);
311
    watcher->deleteLater();
con's avatar
con committed
312 313 314 315 316
}

QWidget *BaseFileFind::createPatternWidget()
{
    QString filterToolTip = tr("List of comma separated wildcard filters");
317 318 319 320 321 322 323 324 325 326 327
    d->m_filterCombo = new QComboBox;
    d->m_filterCombo->setEditable(true);
    d->m_filterCombo->setModel(&d->m_filterStrings);
    d->m_filterCombo->setMaxCount(10);
    d->m_filterCombo->setMinimumContentsLength(10);
    d->m_filterCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
    d->m_filterCombo->setInsertPolicy(QComboBox::InsertAtBottom);
    d->m_filterCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    d->m_filterCombo->setToolTip(filterToolTip);
    syncComboWithSettings(d->m_filterCombo, d->m_filterSetting);
    return d->m_filterCombo;
con's avatar
con committed
328 329 330 331
}

void BaseFileFind::writeCommonSettings(QSettings *settings)
{
332
    settings->setValue("filters", d->m_filterStrings.stringList());
333
    if (d->m_filterCombo)
334 335 336 337 338
        settings->setValue("currentFilter", d->m_filterCombo->currentText());

    foreach (SearchEngine *searchEngine, d->m_searchEngines)
        searchEngine->writeSettings(settings);
    settings->setValue("currentSearchEngineIndex", d->m_currentSearchEngineIndex);
con's avatar
con committed
339 340 341 342
}

void BaseFileFind::readCommonSettings(QSettings *settings, const QString &defaultFilter)
{
343 344
    QStringList filters = settings->value("filters").toStringList();
    const QVariant currentFilter = settings->value("currentFilter");
345
    d->m_filterSetting = currentFilter.toString();
con's avatar
con committed
346 347
    if (filters.isEmpty())
        filters << defaultFilter;
348
    if (!currentFilter.isValid())
349 350 351 352
        d->m_filterSetting = filters.first();
    d->m_filterStrings.setStringList(filters);
    if (d->m_filterCombo)
        syncComboWithSettings(d->m_filterCombo, d->m_filterSetting);
353 354 355 356 357

    foreach (SearchEngine* searchEngine, d->m_searchEngines)
        searchEngine->readSettings(settings);
    const int currentSearchEngineIndex = settings->value("currentSearchEngineIndex", 0).toInt();
    syncSearchEngineCombo(currentSearchEngineIndex);
con's avatar
con committed
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
}

void BaseFileFind::syncComboWithSettings(QComboBox *combo, const QString &setting)
{
    if (!combo)
        return;
    int index = combo->findText(setting);
    if (index < 0)
        combo->setEditText(setting);
    else
        combo->setCurrentIndex(index);
}

void BaseFileFind::updateComboEntries(QComboBox *combo, bool onTop)
{
    int index = combo->findText(combo->currentText());
    if (index < 0) {
375
        if (onTop)
con's avatar
con committed
376
            combo->insertItem(0, combo->currentText());
377
        else
con's avatar
con committed
378 379 380 381 382
            combo->addItem(combo->currentText());
        combo->setCurrentIndex(combo->findText(combo->currentText()));
    }
}

383
void BaseFileFind::openEditor(const SearchResultItem &item)
con's avatar
con committed
384
{
385
    SearchResult *result = qobject_cast<SearchResult *>(sender());
386
    FileFindParameters parameters = result->userData().value<FileFindParameters>();
387 388
    IEditor *openedEditor =
            d->m_searchEngines[parameters.searchEngineIndex]->openEditor(item, parameters);
389 390 391 392 393 394 395 396 397
    if (!openedEditor) {
        if (item.path.size() > 0) {
            openedEditor = EditorManager::openEditorAt(QDir::fromNativeSeparators(item.path.first()),
                                                       item.lineNumber,
                                                       item.textMarkPos, Id(),
                                                       EditorManager::DoNotSwitchToDesignMode);
        } else {
            openedEditor = EditorManager::openEditor(QDir::fromNativeSeparators(item.text));
        }
398
    }
399
    if (d->m_currentFindSupport)
400
        d->m_currentFindSupport->clearHighlights();
401
    d->m_currentFindSupport = 0;
402 403 404 405
    if (!openedEditor)
        return;
    // highlight results
    if (IFindSupport *findSupport = Aggregation::query<IFindSupport>(openedEditor->widget())) {
406 407
        d->m_currentFindSupport = findSupport;
        d->m_currentFindSupport->highlightAll(parameters.text, parameters.flags);
408
    }
con's avatar
con committed
409
}
410

411 412
void BaseFileFind::hideHighlightAll(bool visible)
{
413
    if (!visible && d->m_currentFindSupport)
414
        d->m_currentFindSupport->clearHighlights();
415 416
}

417 418 419
void BaseFileFind::searchAgain()
{
    SearchResult *search = qobject_cast<SearchResult *>(sender());
Eike Ziller's avatar
Eike Ziller committed
420
    search->restart();
421 422 423
    runSearch(search);
}

424 425 426 427 428 429 430 431
void BaseFileFind::recheckEnabled()
{
    SearchResult *search = qobject_cast<SearchResult *>(sender());
    if (!search)
        return;
    search->setSearchAgainEnabled(isEnabled());
}

432
QStringList BaseFileFind::replaceAll(const QString &text,
433
                                     const QList<SearchResultItem> &items,
434
                                     bool preserveCase)
435
{
436
    if (items.isEmpty())
437 438
        return QStringList();

439
    RefactoringChanges refactoring;
440

441 442
    QHash<QString, QList<SearchResultItem> > changes;
    foreach (const SearchResultItem &item, items)
443
        changes[QDir::fromNativeSeparators(item.path.first())].append(item);
444

445
    // Checking for files without write permissions
446
    QHashIterator<QString, QList<SearchResultItem> > it(changes);
447 448 449 450 451 452 453 454 455 456
    QSet<QString> roFiles;
    while (it.hasNext()) {
        it.next();
        const QFileInfo fileInfo(it.key());
        if (!fileInfo.isWritable())
            roFiles.insert(it.key());
    }

    // Query the user for permissions
    if (!roFiles.isEmpty()) {
hjk's avatar
hjk committed
457
        ReadOnlyFilesDialog roDialog(roFiles.toList(), ICore::mainWindow());
458
        roDialog.setShowFailWarning(true, tr("Aborting replace."));
hjk's avatar
hjk committed
459
        if (roDialog.exec() == ReadOnlyFilesDialog::RO_Cancel)
460 461 462 463
            return QStringList();
    }

    it.toFront();
464 465 466
    while (it.hasNext()) {
        it.next();
        const QString fileName = it.key();
467
        const QList<SearchResultItem> changeItems = it.value();
468

469 470 471
        ChangeSet changeSet;
        RefactoringFilePtr file = refactoring.file(fileName);
        QSet<QPair<int, int> > processed;
472
        foreach (const SearchResultItem &item, changeItems) {
473 474 475 476 477 478
            const QPair<int, int> &p = qMakePair(item.lineNumber, item.textMarkPos);
            if (processed.contains(p))
                continue;
            processed.insert(p);

            QString replacement;
479
            if (item.userData.canConvert<QStringList>() && !item.userData.toStringList().isEmpty()) {
480
                replacement = Utils::expandRegExpReplacement(text, item.userData.toStringList());
481 482 483 484 485
            } else if (preserveCase) {
                const QString originalText = (item.textMarkLength == 0) ? item.text
                                                                        : item.text.mid(item.textMarkPos, item.textMarkLength);
                replacement = Utils::matchCaseReplacement(originalText, text);
            } else {
486
                replacement = text;
487
            }
488 489 490 491 492

            const int start = file->position(item.lineNumber, item.textMarkPos + 1);
            const int end = file->position(item.lineNumber,
                                           item.textMarkPos + item.textMarkLength + 1);
            changeSet.replace(start, end, replacement);
493
        }
494 495
        file->setChangeSet(changeSet);
        file->apply();
496 497 498 499
    }

    return changes.keys();
}
500

501 502 503 504 505
QVariant BaseFileFind::getAdditionalParameters(SearchResult *search)
{
    return search->userData().value<FileFindParameters>().additionalParameters;
}

Orgad Shaneh's avatar
Orgad Shaneh committed
506 507
QFuture<FileSearchResultList> BaseFileFind::executeSearch(const FileFindParameters &parameters)
{
508
    return d->m_searchEngines[parameters.searchEngineIndex]->executeSearch(parameters,this);
Orgad Shaneh's avatar
Orgad Shaneh committed
509 510 511 512
}

namespace Internal {

513 514 515 516 517 518 519 520 521
CountingLabel::CountingLabel()
{
    setAlignment(Qt::AlignCenter);
    // ### TODO this setup should be done by style
    QFont f = font();
    f.setBold(true);
    f.setPointSizeF(StyleHelper::sidebarFontSize());
    setFont(f);
    setPalette(StyleHelper::sidebarFontPalette(palette()));
522
    setProperty("_q_custom_style_disabled", QVariant(true));
523 524 525 526 527
    updateCount(0);
}

void CountingLabel::updateCount(int count)
{
Jaroslaw Kobus's avatar
Jaroslaw Kobus committed
528
    setText(BaseFileFind::tr("%n found", nullptr, count));
529
}
530

Orgad Shaneh's avatar
Orgad Shaneh committed
531
} // namespace Internal
532
} // namespace TextEditor