plugin_search.cpp 91.8 KB
Newer Older
Kåre Särs's avatar
Kåre Särs committed
1
/*   Kate search plugin
2
 *
3
 * Copyright (C) 2011-2013 by Kåre Särs <kare.sars@iki.fi>
Kåre Särs's avatar
Kåre Särs committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program in a file called COPYING; if not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include "plugin_search.h"

23 24
#include "htmldelegate.h"

25
#include <ktexteditor/application.h>
26
#include <ktexteditor/editor.h>
Kåre Särs's avatar
Kåre Särs committed
27 28
#include <ktexteditor/view.h>
#include <ktexteditor/document.h>
29 30 31
#include <ktexteditor/markinterface.h>
#include <ktexteditor/movinginterface.h>
#include <ktexteditor/movingrange.h>
32
#include <ktexteditor/configinterface.h>
Kåre Särs's avatar
Kåre Särs committed
33

34
#include "kacceleratormanager.h"
Kåre Särs's avatar
Kåre Särs committed
35
#include <kactioncollection.h>
36
#include <klocalizedstring.h>
Kåre Särs's avatar
Kåre Särs committed
37 38 39
#include <kpluginfactory.h>
#include <kpluginloader.h>
#include <kaboutdata.h>
40
#include <kurlcompletion.h>
41
#include <klineedit.h>
42
#include <kcolorscheme.h>
43 44 45

#include <KXMLGUIFactory>
#include <KConfigGroup>
46

47 48
#include <QKeyEvent>
#include <QClipboard>
49
#include <QMenu>
50
#include <QMetaObject>
51
#include <QTextDocument>
52
#include <QScrollBar>
53 54
#include <QFileInfo>
#include <QDir>
55 56
#include <QComboBox>
#include <QCompleter>
57 58 59 60 61

static QUrl localFileDirUp (const QUrl &url)
{
    if (!url.isLocalFile())
        return url;
62

63 64 65
    // else go up
    return QUrl::fromLocalFile (QFileInfo (url.toLocalFile()).dir().absolutePath());
}
66 67 68 69 70 71 72 73 74 75 76 77

static QAction *menuEntry(QMenu *menu,
                          const QString &before, const QString &after, const QString &desc,
                          QString menuBefore = QString(), QString menuAfter = QString());

static QAction *menuEntry(QMenu *menu,
                          const QString &before, const QString &after, const QString &desc,
                          QString menuBefore, QString menuAfter)
{
    if (menuBefore.isEmpty()) menuBefore = before;
    if (menuAfter.isEmpty())  menuAfter = after;

78
    QAction *const action = menu->addAction(menuBefore + menuAfter + QLatin1Char('\t') + desc);
79
    if (!action) return nullptr;
80

81
    action->setData(QString(before + QLatin1Char(' ') + after));
82 83
    return action;
}
Kåre Särs's avatar
Kåre Särs committed
84

85 86 87 88 89 90
class TreeWidgetItem : public QTreeWidgetItem {
public:
    TreeWidgetItem(QTreeWidget* parent):QTreeWidgetItem(parent){}
    TreeWidgetItem(QTreeWidget* parent, const QStringList &list):QTreeWidgetItem(parent, list){}
    TreeWidgetItem(QTreeWidgetItem* parent, const QStringList &list):QTreeWidgetItem(parent, list){}
private:
Kevin Funk's avatar
Kevin Funk committed
91
    bool operator<(const QTreeWidgetItem &other) const override {
92
        if (childCount() == 0) {
93 94 95 96
            int line = data(0, ReplaceMatches::StartLineRole).toInt();
            int column = data(0, ReplaceMatches::StartColumnRole).toInt();
            int oLine = other.data(0, ReplaceMatches::StartLineRole).toInt();
            int oColumn = other.data(0, ReplaceMatches::StartColumnRole).toInt();
97 98 99 100 101 102 103 104
            if (line < oLine) {
                return true;
            }
            if ((line == oLine) && (column < oColumn)) {
                return true;
            }
            return false;
        }
105 106
        int sepCount = data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator());
        int oSepCount = other.data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator());
107 108
        if (sepCount < oSepCount) return true;
        if (sepCount > oSepCount) return false;
109
        return data(0, ReplaceMatches::FileUrlRole).toString().toLower() < other.data(0, ReplaceMatches::FileUrlRole).toString().toLower();
110 111 112
    }
};

113
Results::Results(QWidget *parent): QWidget(parent), matches(0), useRegExp(false), searchPlaceIndex(0)
114 115
{
    setupUi(this);
116

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
    m_delegate = new SPHtmlDelegate(tree);
    tree->setItemDelegate(m_delegate);
}

int Results::itemMargin() const
{
    return m_delegate->itemMargin();
}

void Results::setItemMargin(int m)
{
    m_delegate->setItemMargin(m);
    // trick trigger full relayout
    tree->setItemDelegate(nullptr);
    tree->setItemDelegate(m_delegate);
    tree->update();
133 134 135
}


136
K_PLUGIN_FACTORY_WITH_JSON (KatePluginSearchFactory, "katesearch.json", registerPlugin<KatePluginSearch>();)
Kåre Särs's avatar
Kåre Särs committed
137 138

KatePluginSearch::KatePluginSearch(QObject* parent, const QList<QVariant>&)
139
    : KTextEditor::Plugin (parent),
140
    m_searchCommand(nullptr)
Kåre Särs's avatar
Kåre Särs committed
141
{
142
    m_searchCommand = new KateSearchCommand(this);
Kåre Särs's avatar
Kåre Särs committed
143 144 145 146
}

KatePluginSearch::~KatePluginSearch()
{
147
    delete m_searchCommand;
Kåre Särs's avatar
Kåre Särs committed
148 149
}

150
QObject *KatePluginSearch::createView(KTextEditor::MainWindow *mainWindow)
Kåre Särs's avatar
Kåre Särs committed
151
{
152
    KatePluginSearchView *view = new KatePluginSearchView(this, mainWindow, KTextEditor::Editor::instance()->application());
153 154 155 156
    connect(m_searchCommand, &KateSearchCommand::setSearchPlace, view, &KatePluginSearchView::setSearchPlace);
    connect(m_searchCommand, &KateSearchCommand::setCurrentFolder, view, &KatePluginSearchView::setCurrentFolder);
    connect(m_searchCommand, &KateSearchCommand::setSearchString, view, &KatePluginSearchView::setSearchString);
    connect(m_searchCommand, &KateSearchCommand::startSearch, view, &KatePluginSearchView::startSearch);
157
    connect(m_searchCommand, SIGNAL(newTab()), view, SLOT(addTab()));
158
    return view;
Kåre Särs's avatar
Kåre Särs committed
159 160 161
}


162
bool ContainerWidget::focusNextPrevChild (bool next)
163
{
164 165 166
    QWidget* fw = focusWidget();
    bool found = false;
    emit nextFocus(fw, &found, next);
167

168 169 170 171
    if (found) {
        return true;
    }
    return QWidget::focusNextPrevChild(next);
172 173
}

174
void KatePluginSearchView::nextFocus(QWidget *currentWidget, bool *found, bool next)
175
{
176 177 178 179 180 181
    *found = false;

    if (!currentWidget) {
        return;
    }

182
    // we use the object names here because there can be multiple replaceButtons (on multiple result tabs)
183
    if (next) {
184
        if (currentWidget->objectName() == QStringLiteral("tree") || currentWidget == m_ui.binaryCheckBox) {
185 186 187 188
            m_ui.newTabButton->setFocus();
            *found = true;
            return;
        }
189 190
        if (currentWidget == m_ui.displayOptions) {
            if (m_ui.displayOptions->isChecked()) {
191
                m_ui.folderRequester->setFocus();
192 193 194 195 196 197 198 199 200 201 202 203
                *found = true;
                return;
            }
            else {
                Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
                if (!res) {
                    return;
                }
                res->tree->setFocus();
                *found = true;
                return;
            }
204
        }
205
    }
206 207
    else {
        if (currentWidget == m_ui.newTabButton) {
208
            if (m_ui.displayOptions->isChecked()) {
209
                m_ui.binaryCheckBox->setFocus();
210 211 212 213 214 215 216 217 218 219
            }
            else {
                Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
                if (!res) {
                    return;
                }
                res->tree->setFocus();
            }
            *found = true;
            return;
220 221
        }
        else {
Joseph Wenninger's avatar
Joseph Wenninger committed
222
            if (currentWidget->objectName() == QStringLiteral("tree")) {
223 224
                m_ui.displayOptions->setFocus();
                *found = true;
225 226 227
                return;
            }
        }
228 229 230
    }
}

231
KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin, KTextEditor::Application* application)
232
: QObject (mainWin),
233
m_kateApp(application),
234
m_curResults(nullptr),
235
m_searchJustOpened(false),
236
m_switchToProjectModeWhenAvailable(false),
237 238
m_searchDiskFilesDone(true),
m_searchOpenFilesDone(true),
239
m_isSearchAsYouType(false),
240
m_isLeftRight(false),
241
m_projectPluginView(nullptr),
242
m_mainWindow (mainWin)
Kåre Särs's avatar
Kåre Särs committed
243
{
Joseph Wenninger's avatar
Joseph Wenninger committed
244 245
    KXMLGUIClient::setComponentName (QStringLiteral("katesearch"), i18n ("Kate Search & Replace"));
    setXMLFile( QStringLiteral("ui.rc") );
246

Joseph Wenninger's avatar
Joseph Wenninger committed
247
    m_toolView = mainWin->createToolView (plugin, QStringLiteral("kate_plugin_katesearch"),
248
                                          KTextEditor::MainWindow::Bottom,
249
                                          QIcon::fromTheme(QStringLiteral("edit-find")),
250 251
                                          i18n("Search and Replace"));

252
    ContainerWidget *container = new ContainerWidget(m_toolView);
253
    m_ui.setupUi(container);
Kåre Särs's avatar
Kåre Särs committed
254
    container->setFocusProxy(m_ui.searchCombo);
255
    connect(container, &ContainerWidget::nextFocus, this, &KatePluginSearchView::nextFocus);
256

Joseph Wenninger's avatar
Joseph Wenninger committed
257
    QAction *a = actionCollection()->addAction(QStringLiteral("search_in_files"));
258
    actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_F));
Kåre Särs's avatar
Kåre Särs committed
259
    a->setText(i18n("Search in Files"));
260
    connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView);
261

Joseph Wenninger's avatar
Joseph Wenninger committed
262
    a = actionCollection()->addAction(QStringLiteral("search_in_files_new_tab"));
263 264
    a->setText(i18n("Search in Files (in new tab)"));
    // first add tab, then open search view, since open search view switches to show the search options
265 266
    connect(a, &QAction::triggered, this, &KatePluginSearchView::addTab);
    connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView);
267

Joseph Wenninger's avatar
Joseph Wenninger committed
268
    a = actionCollection()->addAction(QStringLiteral("go_to_next_match"));
269
    a->setText(i18n("Go to Next Match"));
270
    actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F6));
271
    connect(a, &QAction::triggered, this, &KatePluginSearchView::goToNextMatch);
272

Joseph Wenninger's avatar
Joseph Wenninger committed
273
    a = actionCollection()->addAction(QStringLiteral("go_to_prev_match"));
274
    a->setText(i18n("Go to Previous Match"));
275
    actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_F6));
276
    connect(a, &QAction::triggered, this, &KatePluginSearchView::goToPreviousMatch);
277

278
    m_ui.resultTabWidget->tabBar()->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab);
279
    KAcceleratorManager::setNoAccel(m_ui.resultTabWidget);
280

281
    // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing.
282 283 284 285
    QIcon dispOptIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system")));
    QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold")));
    QIcon useRegExpIcon = QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace")));
    QIcon expandResultsIcon = QIcon::fromTheme(QStringLiteral("view-list-tree"), QIcon::fromTheme(QStringLiteral("format-indent-more")));
286 287

    m_ui.displayOptions->setIcon(dispOptIcon);
288
    m_ui.searchButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
289
    m_ui.nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
290
    m_ui.stopButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
291 292 293
    m_ui.matchCase->setIcon(matchCaseIcon);
    m_ui.useRegExp->setIcon(useRegExpIcon);
    m_ui.expandResults->setIcon(expandResultsIcon);
294 295 296
    m_ui.searchPlaceCombo->setItemIcon(CurrentFile, QIcon::fromTheme(QStringLiteral("text-plain")));
    m_ui.searchPlaceCombo->setItemIcon(OpenFiles, QIcon::fromTheme(QStringLiteral("text-plain")));
    m_ui.searchPlaceCombo->setItemIcon(Folder, QIcon::fromTheme(QStringLiteral("folder")));
297 298 299
    m_ui.folderUpButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
    m_ui.currentFolderButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
    m_ui.newTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new")));
300

301 302
    m_ui.filterCombo->setToolTip(i18n("Comma separated list of file types to search in. Example: \"*.cpp,*.h\"\n"));
    m_ui.excludeCombo->setToolTip(i18n("Comma separated list of files and directories to exclude from the search. Example: \"build*\""));
303

304 305
    // the order here is important to get the tabBar hidden for only one tab
    addTab();
306
    m_ui.resultTabWidget->tabBar()->hide();
307 308 309 310 311 312 313 314 315 316

    // get url-requester's combo box and sanely initialize
    KComboBox* cmbUrl = m_ui.folderRequester->comboBox();
    cmbUrl->setDuplicatesEnabled(false);
    cmbUrl->setEditable(true);
    m_ui.folderRequester->setMode(KFile::Directory | KFile::LocalOnly);
    KUrlCompletion* cmpl = new KUrlCompletion(KUrlCompletion::DirCompletion);
    cmbUrl->setCompletionObject(cmpl);
    cmbUrl->setAutoDeleteCompletionObject(true);

317 318 319
    connect(m_ui.newTabButton, &QToolButton::clicked, this, &KatePluginSearchView::addTab);
    connect(m_ui.resultTabWidget, &QTabWidget::tabCloseRequested, this, &KatePluginSearchView::tabCloseRequested);
    connect(m_ui.resultTabWidget, &QTabWidget::currentChanged, this, &KatePluginSearchView::resultTabChanged);
320

321 322 323
    connect(m_ui.folderUpButton, &QToolButton::clicked, this, &KatePluginSearchView::navigateFolderUp);
    connect(m_ui.currentFolderButton, &QToolButton::clicked, this, &KatePluginSearchView::setCurrentFolder);
    connect(m_ui.expandResults, &QToolButton::clicked, this, &KatePluginSearchView::expandResults);
324
    connect(m_ui.itemMargin, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &KatePluginSearchView::marginChanged);
325

326 327
    connect(m_ui.searchCombo, &QComboBox::editTextChanged, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
    connect(m_ui.matchCase, &QToolButton::toggled, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
328
    connect(m_ui.matchCase, &QToolButton::toggled, this, [=]{
329 330 331 332 333
        Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
        if (res) {
            res->matchCase = m_ui.matchCase->isChecked();
        }
    });
334
    connect(m_ui.useRegExp, &QToolButton::toggled, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
335
    connect(m_ui.useRegExp, &QToolButton::toggled, this, [=]{
336 337 338 339 340
        Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
        if (res) {
            res->useRegExp = m_ui.useRegExp->isChecked();
        }
    });
341
    m_changeTimer.setInterval(300);
Kåre Särs's avatar
Kåre Särs committed
342
    m_changeTimer.setSingleShot(true);
343
    connect(&m_changeTimer, &QTimer::timeout, this, &KatePluginSearchView::startSearchWhileTyping);
344

345
    connect(m_ui.searchCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch);
346
// connecting to returnPressed() of the folderRequester doesn't work, I haven't found out why yet. But connecting to the linedit works:
347 348 349 350
    connect(m_ui.folderRequester->comboBox()->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch);
    connect(m_ui.filterCombo, static_cast<void (KComboBox::*)()>(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch);
    connect(m_ui.excludeCombo, static_cast<void (KComboBox::*)()>(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch);
    connect(m_ui.searchButton, &QPushButton::clicked, this, &KatePluginSearchView::startSearch);
351

352 353
    connect(m_ui.displayOptions, &QToolButton::toggled, this, &KatePluginSearchView::toggleOptions);
    connect(m_ui.searchPlaceCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &KatePluginSearchView::searchPlaceChanged);
354 355 356 357 358
    connect(m_ui.searchPlaceCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this](int) {
        if (m_ui.searchPlaceCombo->currentIndex() == Folder) {
            m_ui.displayOptions->setChecked(true);
        }
    });
359

360 361 362 363
    connect(m_ui.stopButton, &QPushButton::clicked, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch);
    connect(m_ui.stopButton, &QPushButton::clicked, &m_searchDiskFiles, &SearchDiskFiles::cancelSearch);
    connect(m_ui.stopButton, &QPushButton::clicked, &m_folderFilesList, &FolderFilesList::cancelSearch);
    connect(m_ui.stopButton, &QPushButton::clicked, &m_replacer, &ReplaceMatches::cancelReplace);
364

365
    connect(m_ui.nextButton, &QToolButton::clicked, this, &KatePluginSearchView::goToNextMatch);
366

367 368 369
    connect(m_ui.replaceButton, &QPushButton::clicked, this, &KatePluginSearchView::replaceSingleMatch);
    connect(m_ui.replaceCheckedBtn, &QPushButton::clicked, this, &KatePluginSearchView::replaceChecked);
    connect(m_ui.replaceCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::replaceChecked);
370 371 372



Kåre Särs's avatar
Kåre Särs committed
373 374
    m_ui.displayOptions->setChecked(true);

375 376 377
    connect(&m_searchOpenFiles, &SearchOpenFiles::matchFound, this, &KatePluginSearchView::matchFound);
    connect(&m_searchOpenFiles, &SearchOpenFiles::searchDone, this, &KatePluginSearchView::searchDone);
    connect(&m_searchOpenFiles, static_cast<void (SearchOpenFiles::*)(const QString&)>(&SearchOpenFiles::searching), this, &KatePluginSearchView::searching);
Kåre Särs's avatar
Kåre Särs committed
378

379 380
    connect(&m_folderFilesList, &FolderFilesList::finished, this, &KatePluginSearchView::folderFileListChanged);
    connect(&m_folderFilesList, &FolderFilesList::searching, this, &KatePluginSearchView::searching);
381

382 383 384
    connect(&m_searchDiskFiles, &SearchDiskFiles::matchFound, this, &KatePluginSearchView::matchFound);
    connect(&m_searchDiskFiles, &SearchDiskFiles::searchDone, this, &KatePluginSearchView::searchDone);
    connect(&m_searchDiskFiles, static_cast<void (SearchDiskFiles::*)(const QString&)>(&SearchDiskFiles::searching), this, &KatePluginSearchView::searching);
385

386
    connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch);
387

388
    connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_replacer, &ReplaceMatches::cancelReplace);
389

390
    connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, this, &KatePluginSearchView::clearDocMarks);
391

392 393
    connect(&m_replacer, &ReplaceMatches::replaceStatus, this, &KatePluginSearchView::replaceStatus);

394 395
    // Hook into line edit context menus
    m_ui.searchCombo->setContextMenuPolicy(Qt::CustomContextMenu);
396
    connect(m_ui.searchCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::searchContextMenu);
397
    m_ui.searchCombo->completer()->setCompletionMode(QCompleter::PopupCompletion);
398
    m_ui.searchCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
399 400 401 402
    m_ui.searchCombo->setInsertPolicy(QComboBox::NoInsert);
    m_ui.searchCombo->lineEdit()->setClearButtonEnabled(true);
    m_ui.searchCombo->setMaxCount(25);

403 404
    m_ui.replaceCombo->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(m_ui.replaceCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::replaceContextMenu);
405
    m_ui.replaceCombo->completer()->setCompletionMode(QCompleter::PopupCompletion);
406
    m_ui.replaceCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
407 408 409 410
    m_ui.replaceCombo->setInsertPolicy(QComboBox::NoInsert);
    m_ui.replaceCombo->lineEdit()->setClearButtonEnabled(true);
    m_ui.replaceCombo->setMaxCount(25);

411 412
    m_toolView->setMinimumHeight(container->sizeHint().height());

413
    connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KatePluginSearchView::handleEsc);
414

415
    // watch for project plugin view creation/deletion
416
    connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, &KatePluginSearchView::slotPluginViewCreated);
417

418
    connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, &KatePluginSearchView::slotPluginViewDeleted);
419

420
    connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KatePluginSearchView::docViewChanged);
421

422 423 424
    // Connect signals from project plugin to our slots
    m_projectPluginView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
    slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView);
425

426
    m_replacer.setDocumentManager(m_kateApp);
427
    connect(&m_replacer, &ReplaceMatches::replaceDone, this, &KatePluginSearchView::replaceDone);
428

429 430
    searchPlaceChanged();

431 432
    m_toolView->installEventFilter(this);

433
    m_mainWindow->guiFactory()->addClient(this);
434 435

    m_updateSumaryTimer.setInterval(1);
436
    m_updateSumaryTimer.setSingleShot(true);
437
    connect(&m_updateSumaryTimer, &QTimer::timeout, this, &KatePluginSearchView::updateResultsRootItem);
Kåre Särs's avatar
Kåre Särs committed
438 439 440 441
}

KatePluginSearchView::~KatePluginSearchView()
{
442 443
    clearMarks();

444
    m_mainWindow->guiFactory()->removeClient(this);
445
    delete m_toolView;
Kåre Särs's avatar
Kåre Särs committed
446 447
}

448
void KatePluginSearchView::navigateFolderUp()
449 450
{
    // navigate one folder up
451
    m_ui.folderRequester->setUrl(localFileDirUp(m_ui.folderRequester->url()));
452 453
}

454 455
void KatePluginSearchView::setCurrentFolder()
{
456
    if (!m_mainWindow) {
457 458
        return;
    }
459
    KTextEditor::View* editView = m_mainWindow->activeView();
460 461
    if (editView && editView->document()) {
        // upUrl as we want the folder not the file
462
        m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url()));
463
    }
464
    m_ui.displayOptions->setChecked(true);
465 466
}

467
void KatePluginSearchView::openSearchView()
Kåre Särs's avatar
Kåre Särs committed
468
{
469
    if (!m_mainWindow) {
Kåre Särs's avatar
Kåre Särs committed
470 471 472
        return;
    }
    if (!m_toolView->isVisible()) {
473
        m_mainWindow->showToolView(m_toolView);
474
    }
475
    m_ui.searchCombo->setFocus(Qt::OtherFocusReason);
476 477 478
    if (m_ui.searchPlaceCombo->currentIndex() == Folder) {
        m_ui.displayOptions->setChecked(true);
    }
479

480
    KTextEditor::View* editView = m_mainWindow->activeView();
481
    if (editView && editView->document()) {
482
        if (m_ui.folderRequester->text().isEmpty()) {
483
            // upUrl as we want the folder not the file
484
            m_ui.folderRequester->setUrl(localFileDirUp (editView->document()->url()));
485
        }
486
        QString selection;
487
        if (editView->selection()) {
488
            selection = editView->selectionText();
489
            // remove possible trailing '\n'
490
            if (selection.endsWith(QLatin1Char('\n'))) {
491 492
                selection = selection.left(selection.size() -1);
            }
493
        }
494
        if (selection.isEmpty()) {
495
            selection = editView->document()->wordAt(editView->cursorPosition());
496 497
        }

498
        if (!selection.isEmpty() && !selection.contains(QLatin1Char('\n'))) {
499 500 501 502 503
            m_ui.searchCombo->blockSignals(true);
            m_ui.searchCombo->lineEdit()->setText(selection);
            m_ui.searchCombo->blockSignals(false);
        }

504 505
        m_ui.searchCombo->lineEdit()->selectAll();
        m_searchJustOpened = true;
506
        startSearchWhileTyping();
Kåre Särs's avatar
Kåre Särs committed
507 508 509
    }
}

510 511
void KatePluginSearchView::handleEsc(QEvent *e)
{
512
    if (!m_mainWindow) return;
513 514 515

    QKeyEvent *k = static_cast<QKeyEvent *>(e);
    if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
516 517 518 519
        static ulong lastTimeStamp;
        if (lastTimeStamp == k->timestamp()) {
            // Same as previous... This looks like a bug somewhere...
            return;
520
        }
521 522
        lastTimeStamp = k->timestamp();
        if (!m_matchRanges.isEmpty()) {
523 524
            clearMarks();
        }
525 526 527
        else if (m_toolView->isVisible()) {
            m_mainWindow->hideToolView(m_toolView);
        }
528

529 530 531 532 533 534 535 536 537 538 539
        // Remove check marks
        Results *curResults = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
        if (!curResults) {
            qWarning() << "This is a bug";
            return;
        }
        QTreeWidgetItemIterator it(curResults->tree);
        while (*it) {
            (*it)->setCheckState(0, Qt::Unchecked);
            ++it;
        }
540
    }
541 542
}

543 544 545 546 547
void KatePluginSearchView::setSearchString(const QString &pattern)
{
    m_ui.searchCombo->lineEdit()->setText(pattern);
}

548 549 550 551 552 553 554 555 556
void KatePluginSearchView::toggleOptions(bool show)
{
    m_ui.stackedWidget->setCurrentIndex((show) ? 1:0);
}

void KatePluginSearchView::setSearchPlace(int place)
{
    m_ui.searchPlaceCombo->setCurrentIndex(place);
}
557 558 559 560

QStringList KatePluginSearchView::filterFiles(const QStringList& files) const
{
    QString types = m_ui.filterCombo->currentText();
561
    QString excludes = m_ui.excludeCombo->currentText();
Joseph Wenninger's avatar
Joseph Wenninger committed
562
    if (((types.isEmpty() || types == QStringLiteral("*"))) && (excludes.isEmpty())) {
563 564 565 566
        // shortcut for use all files
        return files;
    }

567
    QStringList tmpTypes = types.split(QLatin1Char(','));
568
    QVector<QRegExp> typeList(tmpTypes.size());
569
    for (int i=0; i<tmpTypes.size(); i++) {
570
        QRegExp rx(tmpTypes[i].trimmed());
571 572 573 574
        rx.setPatternSyntax(QRegExp::Wildcard);
        typeList << rx;
    }

575
    QStringList tmpExcludes = excludes.split(QLatin1Char(','));
576
    QVector<QRegExp> excludeList(tmpExcludes.size());
577
    for (int i=0; i<tmpExcludes.size(); i++) {
578
        QRegExp rx(tmpExcludes[i].trimmed());
579 580 581 582
        rx.setPatternSyntax(QRegExp::Wildcard);
        excludeList << rx;
    }

583 584 585
    QStringList filteredFiles;
    foreach (QString fileName, files) {
        bool isInSubDir = fileName.startsWith(m_resultBaseDir);
586
        QString nameToCheck = fileName;
587
        if (isInSubDir) {
588
            nameToCheck = fileName.mid(m_resultBaseDir.size());
589 590
        }

591
        bool skip = false;
592 593
        for (const auto& regex : qAsConst(excludeList)) {
            if (regex.exactMatch(nameToCheck)) {
594 595 596 597 598 599 600 601 602
                skip = true;
                break;
            }
        }
        if (skip) {
            continue;
        }


603 604
        for (const auto& regex : qAsConst(typeList)) {
            if (regex.exactMatch(nameToCheck)) {
605 606 607 608 609 610 611 612
                filteredFiles << fileName;
                break;
            }
        }
    }
    return filteredFiles;
}

613
void KatePluginSearchView::folderFileListChanged()
Kåre Särs's avatar
Kåre Särs committed
614
{
615 616
    m_searchDiskFilesDone = false;
    m_searchOpenFilesDone = false;
617

618
    if (!m_curResults) {
619
        qWarning() << "This is a bug";
620 621 622
        m_searchDiskFilesDone = true;
        m_searchOpenFilesDone = true;
        searchDone();
623 624
        return;
    }
625
    QStringList fileList = m_folderFilesList.fileList();
626

627
    QList<KTextEditor::Document*> openList;
628
    for (int i=0; i<m_kateApp->documents().size(); i++) {
629
        int index = fileList.indexOf(m_kateApp->documents()[i]->url().toLocalFile());
630
        if (index != -1) {
631
            openList << m_kateApp->documents()[i];
632 633
            fileList.removeAt(index);
        }
Kåre Särs's avatar
Kåre Särs committed
634
    }
635 636 637 638 639 640

    // search order is important: Open files starts immediately and should finish
    // earliest after first event loop.
    // The DiskFile might finish immediately
    if (openList.size() > 0) {
        m_searchOpenFiles.startSearch(openList, m_curResults->regExp);
641 642
    }
    else {
643
        m_searchOpenFilesDone = true;
Kåre Särs's avatar
Kåre Särs committed
644 645
    }

646
    m_searchDiskFiles.startSearch(fileList, m_curResults->regExp);
Kåre Särs's avatar
Kåre Särs committed
647 648
}

649

Kåre Särs's avatar
Kåre Särs committed
650 651
void KatePluginSearchView::searchPlaceChanged()
{
652 653
    int searchPlace = m_ui.searchPlaceCombo->currentIndex();
    const bool inFolder = (searchPlace == Folder);
654

655 656
    m_ui.filterCombo->setEnabled(searchPlace >= Folder);
    m_ui.excludeCombo->setEnabled(searchPlace >= Folder);
657 658 659 660 661 662 663 664
    m_ui.folderRequester->setEnabled(inFolder);
    m_ui.folderUpButton->setEnabled(inFolder);
    m_ui.currentFolderButton->setEnabled(inFolder);
    m_ui.recursiveCheckBox->setEnabled(inFolder);
    m_ui.hiddenCheckBox->setEnabled(inFolder);
    m_ui.symLinkCheckBox->setEnabled(inFolder);
    m_ui.binaryCheckBox->setEnabled(inFolder);

665 666 667 668
    if (inFolder && sender() == m_ui.searchPlaceCombo) {
        setCurrentFolder();
    }

669 670 671 672
    // ... and the labels:
    m_ui.folderLabel->setEnabled(m_ui.folderRequester->isEnabled());
    m_ui.filterLabel->setEnabled(m_ui.filterCombo->isEnabled());
    m_ui.excludeLabel->setEnabled(m_ui.excludeCombo->isEnabled());
673 674 675 676 677

    Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
    if (res) {
        res->searchPlaceIndex = searchPlace;
    }
Kåre Särs's avatar
Kåre Särs committed
678 679
}

680
void KatePluginSearchView::addHeaderItem()
681
{
682
    QTreeWidgetItem *item = new QTreeWidgetItem(m_curResults->tree, QStringList());
683 684
    item->setCheckState(0, Qt::Checked);
    item->setFlags(item->flags() | Qt::ItemIsTristate);
685
    m_curResults->tree->expandItem(item);
686 687
}

688
QTreeWidgetItem * KatePluginSearchView::rootFileItem(const QString &url, const QString &fName)
689
{
690
    if (!m_curResults) {
691
        return nullptr;
692 693
    }

694
    QUrl fullUrl = QUrl::fromUserInput(url);
695
    QString path = fullUrl.isLocalFile() ? localFileDirUp(fullUrl).path() : fullUrl.url();
696 697 698
    if (!path.isEmpty() && !path.endsWith(QLatin1Char('/'))) {
        path += QLatin1Char('/');
    }
699
    path.replace(m_resultBaseDir, QString());
700
    QString name = fullUrl.fileName();
701 702 703
    if (url.isEmpty()) {
        name = fName;
    }
704

705
    // make sure we have a root item
706
    if (m_curResults->tree->topLevelItemCount() == 0) {
707
        addHeaderItem();
708 709 710
    }
    QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0);

711
    if (m_isSearchAsYouType) {
712 713 714
        return root;
    }

715
    for (int i=0; i<root->childCount(); i++) {
716
        //qDebug() << root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() << fName;
717 718
        if ((root->child(i)->data(0, ReplaceMatches::FileUrlRole).toString() == url)&&
            (root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() == fName)) {
719
            int matches = root->child(i)->data(0, ReplaceMatches::StartLineRole).toInt() + 1;
Laurent Montel's avatar
Laurent Montel committed
720
            QString tmpUrl = QStringLiteral("%1<b>%2</b>: <b>%3</b>").arg(path, name).arg(matches);
721
            root->child(i)->setData(0, Qt::DisplayRole, tmpUrl);
722
            root->child(i)->setData(0, ReplaceMatches::StartLineRole, matches);
723
            return root->child(i);
724 725
        }
    }
726

727
    // file item not found create a new one
Laurent Montel's avatar
Laurent Montel committed
728
    QString tmpUrl = QStringLiteral("%1<b>%2</b>: <b>%3</b>").arg(path, name).arg(1);
729

730
    TreeWidgetItem *item = new TreeWidgetItem(root, QStringList(tmpUrl));
731 732
    item->setData(0, ReplaceMatches::FileUrlRole, url);
    item->setData(0, ReplaceMatches::FileNameRole, fName);
733
    item->setData(0, ReplaceMatches::StartLineRole, 1);
734
    item->setCheckState(0, Qt::Checked);
735
    item->setFlags(item->flags() | Qt::ItemIsTristate);
736 737 738
    return item;
}

739
void KatePluginSearchView::addMatchMark(KTextEditor::Document* doc, QTreeWidgetItem *item)
740
{
741 742 743
    if (!doc || !item) {
        return;
    }
744

745
    KTextEditor::View* activeView = m_mainWindow->activeView();
746
    KTextEditor::MovingInterface* miface = qobject_cast<KTextEditor::MovingInterface*>(doc);
747
    KTextEditor::ConfigInterface* ciface = qobject_cast<KTextEditor::ConfigInterface*>(activeView);
748
    KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute());
749

750 751 752 753 754 755 756
    int line = item->data(0, ReplaceMatches::StartLineRole).toInt();
    int column = item->data(0, ReplaceMatches::StartColumnRole).toInt();
    int endLine = item->data(0, ReplaceMatches::EndLineRole).toInt();
    int endColumn = item->data(0, ReplaceMatches::EndColumnRole).toInt();
    bool isReplaced = item->data(0, ReplaceMatches::ReplacedRole).toBool();

    if (isReplaced) {
757
        QColor replaceColor(Qt::green);
Joseph Wenninger's avatar
Joseph Wenninger committed
758
        if (ciface) replaceColor = ciface->configValue(QStringLiteral("replace-highlight-color")).value<QColor>();
759
        attr->setBackground(replaceColor);
760 761 762 763

        if (activeView) {
            attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color());
        }
764 765
    }
    else {
766
        QColor searchColor(Qt::yellow);
Joseph Wenninger's avatar
Joseph Wenninger committed
767
        if (ciface) searchColor = ciface->configValue(QStringLiteral("search-highlight-color")).value<QColor>();
768
        attr->setBackground(searchColor);
769 770 771 772

        if (activeView) {
            attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color());
        }
773
    }
774

775
    KTextEditor::Range range(line, column, endLine, endColumn);
776

777 778 779 780 781 782 783 784 785 786
    // Check that the match still matches
    if (m_curResults) {
        if (!isReplaced) {
            // special handling for "(?=\\n)" in multi-line search
            QRegularExpression tmpReg = m_curResults->regExp;
            if (m_curResults->regExp.pattern().endsWith(QStringLiteral("(?=\\n)"))) {
                QString newPatern = tmpReg.pattern();
                newPatern.replace(QStringLiteral("(?=\\n)"), QStringLiteral("$"));
                tmpReg.setPattern(newPatern);
            }
787

788 789 790 791 792 793 794 795 796 797 798
            // Check that the match still matches ;)
            if (tmpReg.match(doc->text(range)).capturedStart() != 0) {
                qDebug() << doc->text(range) << "Does not match" << m_curResults->regExp.pattern();
                return;
            }
        }
        else {
            if (doc->text(range) != item->data(0, ReplaceMatches::ReplacedTextRole).toString()) {
                qDebug() << doc->text(range) << "Does not match" << item->data(0, ReplaceMatches::ReplacedTextRole).toString();
                return;
            }
799 800 801
        }
    }

802
    // Highlight the match
803 804 805 806 807 808
    KTextEditor::MovingRange* mr = miface->newMovingRange(range);
    mr->setAttribute(attr);
    mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection
    mr->setAttributeOnlyForViews(true);
    m_matchRanges.append(mr);

809
    // Add a match mark
810 811 812
    KTextEditor::MarkInterface* iface = qobject_cast<KTextEditor::MarkInterface*>(doc);
    if (!iface) return;
    iface->setMarkDescription(KTextEditor::MarkInterface::markType32, i18n("SearchHighLight"));
813
    iface->setMarkPixmap(KTextEditor::MarkInterface::markType32, QIcon().pixmap(0,0));
814 815 816 817 818 819
    iface->addMark(line, KTextEditor::MarkInterface::markType32);

    connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)),
            this, SLOT(clearMarks()), Qt::UniqueConnection);
}

820 821
static const int contextLen = 70;

822 823 824
void KatePluginSearchView::matchFound(const QString &url, const QString &fName,
                                      const QString &lineContent, int matchLen,
                                      int startLine, int startColumn, int endLine, int endColumn)
Kåre Särs's avatar
Kåre Särs committed
825
{
826
    if (!m_curResults) {
827 828
        return;
    }
829 830 831 832 833 834 835 836 837 838 839
    int preLen = contextLen;
    int preStart = startColumn - preLen;
    if (preStart < 0) {
        preLen += preStart;
        preStart = 0;
    }
    QString pre;
    if (preLen == contextLen) {
        pre = QStringLiteral("...");
    }
    pre += lineContent.mid(preStart, preLen).toHtmlEscaped();
840
    QString match = lineContent.mid(startColumn, matchLen).toHtmlEscaped();
Joseph Wenninger's avatar
Joseph Wenninger committed
841
    match.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
842 843 844 845 846
    QString post = lineContent.mid(startColumn + matchLen, contextLen);
    if (post.size() >= contextLen) {
        post += QStringLiteral("...");
    }
    post = post.toHtmlEscaped();
Kåre Särs's avatar
Kåre Särs committed
847
    QStringList row;
848
    row << i18n("Line: <b>%1</b> Column: <b>%2</b>: %3", startLine+1, startColumn+1, pre+QStringLiteral("<b>")+match+QStringLiteral("</b>")+post);
849

850 851
    TreeWidgetItem *item = new TreeWidgetItem(rootFileItem(url, fName), row);
    item->setData(0, ReplaceMatches::FileUrlRole, url);
852
    item->setData(0, Qt::ToolTipRole, url);
853
    item->setData(0, ReplaceMatches::FileNameRole, fName);
854 855
    item->setData(0, ReplaceMatches::StartLineRole, startLine);
    item->setData(0, ReplaceMatches::StartColumnRole, startColumn);
856 857 858 859
    item->setData(0, ReplaceMatches::MatchLenRole, matchLen);
    item->setData(0, ReplaceMatches::PreMatchRole, pre);
    item->setData(0, ReplaceMatches::MatchRole, match);
    item->setData(0, ReplaceMatches::PostMatchRole, post);
860 861
    item->setData(0, ReplaceMatches::EndLineRole, endLine);
    item->setData(0, ReplaceMatches::EndColumnRole, endColumn);
862
    item->setCheckState (0, Qt::Checked);
863 864

    m_curResults->matches++;
865 866 867 868
}

void KatePluginSearchView::clearMarks()
{
869
    foreach (KTextEditor::Document* doc, m_kateApp->documents()) {
870
        clearDocMarks(doc);
871 872 873
    }
    qDeleteAll(m_matchRanges);
    m_matchRanges.clear();
Kåre Särs's avatar
Kåre Särs committed
874 875
}

876 877 878 879 880 881 882 883 884
void KatePluginSearchView::clearDocMarks(KTextEditor::Document* doc)
{
    KTextEditor::MarkInterface* iface;
    iface = qobject_cast<KTextEditor::MarkInterface*>(doc);
    if (iface) {
        const QHash<int, KTextEditor::Mark*> marks = iface->marks();
        QHashIterator<int, KTextEditor::Mark*> i(marks);
        while (i.hasNext()) {
            i.next();
885 886
            if (i.value()->type & KTextEditor::MarkInterface::markType32) {
                iface->removeMark(i.value()->line, KTextEditor::MarkInterface::markType32);
887 888 889 890 891 892 893 894 895 896 897 898 899 900
            }
        }
    }

    int i = 0;
    while (i<m_matchRanges.size()) {
        if (m_matchRanges.at(i)->document() == doc) {
            delete m_matchRanges.at(i);
            m_matchRanges.removeAt(i);
        }
        else {
            i++;
        }
    }
901 902 903 904 905 906

    m_curResults = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
    if (!m_curResults) {
        qWarning() << "This is a bug";
        return;
    }
907 908
}

909 910 911
void KatePluginSearchView::startSearch()
{
    m_changeTimer.stop(); // make sure not to start a "while you type" search now
912
    m_mainWindow->showToolView(m_toolView); // in case we are invoked from the command interface
913 914 915 916 917 918
    m_switchToProjectModeWhenAvailable = false; // now that we started, don't switch back automatically

    if (m_ui.searchCombo->currentText().isEmpty()) {
        // return pressed in the folder combo or filter combo
        return;
    }
919