basefilefind.cpp 16.6 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
Eike Ziller's avatar
Eike Ziller committed
3 4
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://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
Eike Ziller's avatar
Eike Ziller committed
12 13
** a written agreement between you and The Qt Company.  For licensing terms and
** conditions see http://www.qt.io/terms-conditions.  For further information
Eike Ziller's avatar
Eike Ziller committed
14
** use the contact form at http://www.qt.io/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
** Alternatively, this file may be used under the terms of the GNU Lesser
Eike Ziller's avatar
Eike Ziller committed
18 19 20 21 22 23
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
hjk's avatar
hjk committed
24
**
Eike Ziller's avatar
Eike Ziller committed
25 26
** In addition, as a special exception, The Qt Company gives you certain additional
** rights.  These rights are described in The Qt Company LGPL Exception
con's avatar
con committed
27 28
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
29
****************************************************************************/
hjk's avatar
hjk committed
30

con's avatar
con committed
31
#include "basefilefind.h"
32
#include "basefilefind_p.h"
33
#include "textdocument.h"
con's avatar
con committed
34

35
#include <aggregation/aggregate.h>
36
#include <coreplugin/icore.h>
37
#include <coreplugin/progressmanager/progressmanager.h>
38
#include <coreplugin/progressmanager/futureprogress.h>
39
#include <coreplugin/dialogs/readonlyfilesdialog.h>
40
#include <coreplugin/documentmanager.h>
41
#include <coreplugin/editormanager/editormanager.h>
42
#include <coreplugin/find/ifindsupport.h>
43
#include <texteditor/texteditor.h>
44
#include <texteditor/refactoringchanges.h>
45
#include <utils/fadingindicator.h>
46
#include <utils/filesearch.h>
47 48
#include <utils/qtcassert.h>
#include <utils/stylehelper.h>
con's avatar
con committed
49

50 51 52 53
#include <QDebug>
#include <QSettings>
#include <QHash>
#include <QPair>
54 55 56
#include <QStringListModel>
#include <QFutureWatcher>
#include <QPointer>
57
#include <QComboBox>
58
#include <QLabel>
59
#include <QLabel>
con's avatar
con committed
60

61 62 63
using namespace Utils;
using namespace Core;

64 65
namespace TextEditor {
namespace Internal {
66 67 68

class BaseFileFindPrivate
{
69 70 71
public:
    BaseFileFindPrivate() : m_resultLabel(0), m_filterCombo(0) {}

72 73
    QMap<QFutureWatcher<FileSearchResultList> *, QPointer<SearchResult> > m_watchers;
    QPointer<IFindSupport> m_currentFindSupport;
74 75 76 77 78 79

    QLabel *m_resultLabel;
    QStringListModel m_filterStrings;
    QString m_filterSetting;
    QPointer<QComboBox> m_filterCombo;
};
80

81 82
} // namespace Internal

83
using namespace Internal;
84 85

BaseFileFind::BaseFileFind() : d(new BaseFileFindPrivate)
con's avatar
con committed
86 87 88
{
}

89 90
BaseFileFind::~BaseFileFind()
{
91
    delete d;
92 93
}

con's avatar
con committed
94 95
bool BaseFileFind::isEnabled() const
{
96
    return true;
con's avatar
con committed
97 98
}

dt's avatar
dt committed
99 100
void BaseFileFind::cancel()
{
101 102
    SearchResult *search = qobject_cast<SearchResult *>(sender());
    QTC_ASSERT(search, return);
103
    QFutureWatcher<FileSearchResultList> *watcher = d->m_watchers.key(search);
104 105
    QTC_ASSERT(watcher, return);
    watcher->cancel();
dt's avatar
dt committed
106 107
}

108 109 110 111
void BaseFileFind::setPaused(bool paused)
{
    SearchResult *search = qobject_cast<SearchResult *>(sender());
    QTC_ASSERT(search, return);
112
    QFutureWatcher<FileSearchResultList> *watcher = d->m_watchers.key(search);
113 114 115 116 117
    QTC_ASSERT(watcher, return);
    if (!paused || watcher->isRunning()) // guard against pausing when the search is finished
        watcher->setPaused(paused);
}

con's avatar
con committed
118 119 120
QStringList BaseFileFind::fileNameFilters() const
{
    QStringList filters;
121 122
    if (d->m_filterCombo && !d->m_filterCombo->currentText().isEmpty()) {
        const QStringList parts = d->m_filterCombo->currentText().split(QLatin1Char(','));
con's avatar
con committed
123
        foreach (const QString &part, parts) {
124
            const QString filter = part.trimmed();
125
            if (!filter.isEmpty())
con's avatar
con committed
126 127 128 129 130 131
                filters << filter;
        }
    }
    return filters;
}

132
void BaseFileFind::runNewSearch(const QString &txt, FindFlags findFlags,
133
                                    SearchResultWindow::SearchMode searchMode)
con's avatar
con committed
134
{
135 136 137
    d->m_currentFindSupport = 0;
    if (d->m_filterCombo)
        updateComboEntries(d->m_filterCombo, true);
138 139 140
    SearchResult *search = SearchResultWindow::instance()->startNewSearch(label(),
                           toolTip().arg(IFindFilter::descriptionForFindFlags(findFlags)),
                           txt, searchMode, SearchResultWindow::PreserveCaseEnabled,
141
                           QString::fromLatin1("TextEditor"));
142
    search->setTextToReplace(txt);
143 144 145 146 147 148 149
    search->setSearchAgainSupported(true);
    FileFindParameters parameters;
    parameters.text = txt;
    parameters.flags = findFlags;
    parameters.nameFilters = fileNameFilters();
    parameters.additionalParameters = additionalParameters();
    search->setUserData(qVariantFromValue(parameters));
150 151 152 153 154 155 156 157 158
    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);
159

160 161 162
    runSearch(search);
}

163
void BaseFileFind::runSearch(SearchResult *search)
164 165
{
    FileFindParameters parameters = search->userData().value<FileFindParameters>();
166
    CountingLabel *label = new CountingLabel;
167
    connect(search, &SearchResult::countChanged, label, &CountingLabel::updateCount);
168
    CountingLabel *statusLabel = new CountingLabel;
169
    connect(search, &SearchResult::countChanged, statusLabel, &CountingLabel::updateCount);
170
    SearchResultWindow::instance()->popup(IOutputPane::Flags(IOutputPane::ModeSwitch|IOutputPane::WithFocus));
171
    QFutureWatcher<FileSearchResultList> *watcher = new QFutureWatcher<FileSearchResultList>();
172
    d->m_watchers.insert(watcher, search);
173
    watcher->setPendingResultsLimit(1);
174 175
    connect(watcher, &QFutureWatcherBase::resultReadyAt, this, &BaseFileFind::displayResult);
    connect(watcher, &QFutureWatcherBase::finished, this, &BaseFileFind::searchFinished);
176
    if (parameters.flags & FindRegularExpression) {
177 178 179
        watcher->setFuture(Utils::findInFilesRegExp(parameters.text,
            files(parameters.nameFilters, parameters.additionalParameters),
            textDocumentFlagsForFindFlags(parameters.flags),
180
            TextDocument::openedTextDocumentContents()));
181
    } else {
182 183 184
        watcher->setFuture(Utils::findInFiles(parameters.text,
            files(parameters.nameFilters, parameters.additionalParameters),
            textDocumentFlagsForFindFlags(parameters.flags),
185
            TextDocument::openedTextDocumentContents()));
186
    }
187
    FutureProgress *progress =
188
        ProgressManager::addTask(watcher->future(), tr("Searching"), Constants::TASK_SEARCH);
189
    progress->setWidget(label);
190
    progress->setStatusBarWidget(statusLabel);
191
    connect(progress, &FutureProgress::clicked, search, &SearchResult::popup);
con's avatar
con committed
192 193
}

194
void BaseFileFind::findAll(const QString &txt, FindFlags findFlags)
195 196 197 198
{
    runNewSearch(txt, findFlags, SearchResultWindow::SearchOnly);
}

199
void BaseFileFind::replaceAll(const QString &txt, FindFlags findFlags)
200
{
201
    runNewSearch(txt, findFlags, SearchResultWindow::SearchAndReplace);
202 203 204
}

void BaseFileFind::doReplace(const QString &text,
205
                             const QList<SearchResultItem> &items,
206
                             bool preserveCase)
207
{
208
    QStringList files = replaceAll(text, items, preserveCase);
209
    if (!files.isEmpty()) {
210
        Utils::FadingIndicator::showText(ICore::mainWindow(),
211
            tr("%n occurrences replaced.", 0, items.size()),
212
            Utils::FadingIndicator::SmallText);
213
        DocumentManager::notifyFilesChangedInternally(files);
214
        SearchResultWindow::instance()->hide();
215 216 217
    }
}

con's avatar
con committed
218
void BaseFileFind::displayResult(int index) {
219 220
    QFutureWatcher<FileSearchResultList> *watcher =
            static_cast<QFutureWatcher<FileSearchResultList> *>(sender());
221
    SearchResult *search = d->m_watchers.value(watcher);
222 223 224
    if (!search) {
        // search was removed from search history while the search is running
        watcher->cancel();
225 226
        return;
    }
227 228 229 230
    FileSearchResultList results = watcher->resultAt(index);
    QList<SearchResultItem> items;
    foreach (const FileSearchResult &result, results) {
        SearchResultItem item;
231
        item.path = QStringList() << QDir::toNativeSeparators(result.fileName);
232
        item.lineNumber = result.lineNumber;
233 234 235 236
        item.text = result.matchingLine;
        item.textMarkLength = result.matchLength;
        item.textMarkPos = result.matchStart;
        item.useTextEditorFont = true;
237 238 239
        item.userData = result.regexpCapturedTexts;
        items << item;
    }
240
    search->addResults(items, SearchResult::AddOrdered);
con's avatar
con committed
241 242 243 244
}

void BaseFileFind::searchFinished()
{
245 246
    QFutureWatcher<FileSearchResultList> *watcher =
            static_cast<QFutureWatcher<FileSearchResultList> *>(sender());
247
    SearchResult *search = d->m_watchers.value(watcher);
248
    if (search)
Eike Ziller's avatar
Eike Ziller committed
249
        search->finishSearch(watcher->isCanceled());
250
    d->m_watchers.remove(watcher);
251
    watcher->deleteLater();
con's avatar
con committed
252 253 254 255 256
}

QWidget *BaseFileFind::createPatternWidget()
{
    QString filterToolTip = tr("List of comma separated wildcard filters");
257 258 259 260 261 262 263 264 265 266 267
    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
268 269 270 271
}

void BaseFileFind::writeCommonSettings(QSettings *settings)
{
272 273 274
    settings->setValue(QLatin1String("filters"), d->m_filterStrings.stringList());
    if (d->m_filterCombo)
        settings->setValue(QLatin1String("currentFilter"), d->m_filterCombo->currentText());
con's avatar
con committed
275 276 277 278
}

void BaseFileFind::readCommonSettings(QSettings *settings, const QString &defaultFilter)
{
279
    QStringList filters = settings->value(QLatin1String("filters")).toStringList();
280
    d->m_filterSetting = settings->value(QLatin1String("currentFilter")).toString();
con's avatar
con committed
281 282
    if (filters.isEmpty())
        filters << defaultFilter;
283 284 285 286 287
    if (d->m_filterSetting.isEmpty())
        d->m_filterSetting = filters.first();
    d->m_filterStrings.setStringList(filters);
    if (d->m_filterCombo)
        syncComboWithSettings(d->m_filterCombo, d->m_filterSetting);
con's avatar
con committed
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
}

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) {
305
        if (onTop)
con's avatar
con committed
306
            combo->insertItem(0, combo->currentText());
307
        else
con's avatar
con committed
308 309 310 311 312
            combo->addItem(combo->currentText());
        combo->setCurrentIndex(combo->findText(combo->currentText()));
    }
}

313
void BaseFileFind::openEditor(const SearchResultItem &item)
con's avatar
con committed
314
{
315
    SearchResult *result = qobject_cast<SearchResult *>(sender());
316
    IEditor *openedEditor = 0;
317
    if (item.path.size() > 0) {
318
        openedEditor = EditorManager::openEditorAt(QDir::fromNativeSeparators(item.path.first()),
319 320 321
                                                   item.lineNumber,
                                                   item.textMarkPos, Id(),
                                                   EditorManager::DoNotSwitchToDesignMode);
322
    } else {
323
        openedEditor = EditorManager::openEditor(QDir::fromNativeSeparators(item.text));
324
    }
325
    if (d->m_currentFindSupport)
326
        d->m_currentFindSupport->clearHighlights();
327
    d->m_currentFindSupport = 0;
328 329 330 331 332
    if (!openedEditor)
        return;
    // highlight results
    if (IFindSupport *findSupport = Aggregation::query<IFindSupport>(openedEditor->widget())) {
        if (result) {
333
            FileFindParameters parameters = result->userData().value<FileFindParameters>();
334 335
            d->m_currentFindSupport = findSupport;
            d->m_currentFindSupport->highlightAll(parameters.text, parameters.flags);
336
        }
337
    }
con's avatar
con committed
338
}
339

340 341
void BaseFileFind::hideHighlightAll(bool visible)
{
342
    if (!visible && d->m_currentFindSupport)
343
        d->m_currentFindSupport->clearHighlights();
344 345
}

346 347 348
void BaseFileFind::searchAgain()
{
    SearchResult *search = qobject_cast<SearchResult *>(sender());
Eike Ziller's avatar
Eike Ziller committed
349
    search->restart();
350 351 352
    runSearch(search);
}

353 354 355 356 357 358 359 360
void BaseFileFind::recheckEnabled()
{
    SearchResult *search = qobject_cast<SearchResult *>(sender());
    if (!search)
        return;
    search->setSearchAgainEnabled(isEnabled());
}

361
QStringList BaseFileFind::replaceAll(const QString &text,
362
                                     const QList<SearchResultItem> &items,
363
                                     bool preserveCase)
364
{
365
    if (items.isEmpty())
366 367
        return QStringList();

368
    RefactoringChanges refactoring;
369

370 371
    QHash<QString, QList<SearchResultItem> > changes;
    foreach (const SearchResultItem &item, items)
372
        changes[QDir::fromNativeSeparators(item.path.first())].append(item);
373

374
    // Checking for files without write permissions
375
    QHashIterator<QString, QList<SearchResultItem> > it(changes);
376 377 378 379 380 381 382 383 384 385
    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
386
        ReadOnlyFilesDialog roDialog(roFiles.toList(), ICore::mainWindow());
387
        roDialog.setShowFailWarning(true, tr("Aborting replace."));
hjk's avatar
hjk committed
388
        if (roDialog.exec() == ReadOnlyFilesDialog::RO_Cancel)
389 390 391 392
            return QStringList();
    }

    it.toFront();
393 394 395
    while (it.hasNext()) {
        it.next();
        const QString fileName = it.key();
396
        const QList<SearchResultItem> changeItems = it.value();
397

398 399 400
        ChangeSet changeSet;
        RefactoringFilePtr file = refactoring.file(fileName);
        QSet<QPair<int, int> > processed;
401
        foreach (const SearchResultItem &item, changeItems) {
402 403 404 405 406 407
            const QPair<int, int> &p = qMakePair(item.lineNumber, item.textMarkPos);
            if (processed.contains(p))
                continue;
            processed.insert(p);

            QString replacement;
408
            if (item.userData.canConvert<QStringList>() && !item.userData.toStringList().isEmpty()) {
409
                replacement = Utils::expandRegExpReplacement(text, item.userData.toStringList());
410 411 412 413 414
            } else if (preserveCase) {
                const QString originalText = (item.textMarkLength == 0) ? item.text
                                                                        : item.text.mid(item.textMarkPos, item.textMarkLength);
                replacement = Utils::matchCaseReplacement(originalText, text);
            } else {
415
                replacement = text;
416
            }
417 418 419 420 421

            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);
422
        }
423 424
        file->setChangeSet(changeSet);
        file->apply();
425 426 427 428
    }

    return changes.keys();
}
429

430 431 432 433 434
QVariant BaseFileFind::getAdditionalParameters(SearchResult *search)
{
    return search->userData().value<FileFindParameters>().additionalParameters;
}

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
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()));
    updateCount(0);
}

void CountingLabel::updateCount(int count)
{
    setText(tr("%1 found").arg(count));
}
451 452

} // namespace TextEditor