plugin_search.cpp 96.3 KB
Newer Older
Kåre Särs's avatar
Kåre Särs committed
1
/*   Kate search plugin
2
 *
3
 * SPDX-FileCopyrightText: 2011-2020 Kåre Särs <kare.sars@iki.fi>
Kåre Särs's avatar
Kåre Särs committed
4
 *
5
 * SPDX-License-Identifier: GPL-2.0-or-later
Kåre Särs's avatar
Kåre Särs committed
6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 * 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"
19
#include "KateSearchCommand.h"
20 21
#include "htmldelegate.h"

22
#include <ktexteditor/configinterface.h>
Kåre Särs's avatar
Kåre Särs committed
23
#include <ktexteditor/document.h>
24
#include <ktexteditor/editor.h>
25 26 27
#include <ktexteditor/markinterface.h>
#include <ktexteditor/movinginterface.h>
#include <ktexteditor/movingrange.h>
28
#include <ktexteditor/view.h>
29
#include <ktexteditor_version.h>
Kåre Särs's avatar
Kåre Särs committed
30

31
#include "kacceleratormanager.h"
32 33 34 35 36 37 38
#include <KAboutData>
#include <KActionCollection>
#include <KColorScheme>
#include <KLineEdit>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KUrlCompletion>
39 40

#include <KConfigGroup>
41
#include <KXMLGUIFactory>
42

43
#include <QClipboard>
44 45 46 47 48
#include <QComboBox>
#include <QCompleter>
#include <QDir>
#include <QFileInfo>
#include <QKeyEvent>
49
#include <QMenu>
50
#include <QMetaObject>
Christoph Cullmann's avatar
Christoph Cullmann committed
51
#include <QPoint>
52
#include <QScrollBar>
53
#include <QTextDocument>
54

55
static QUrl localFileDirUp(const QUrl &url)
56 57 58
{
    if (!url.isLocalFile())
        return url;
59

60
    // else go up
61
    return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).dir().absolutePath());
62
}
63

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

66 67 68
/**
 * When the action is triggered the cursor will be placed between @p before and @p after.
 */
69
static QAction *menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore, QString menuAfter)
70
{
71 72 73 74
    if (menuBefore.isEmpty())
        menuBefore = before;
    if (menuAfter.isEmpty())
        menuAfter = after;
75

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

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

84 85 86
/**
 * adds items and separators for special chars in "replace" field
 */
87
static void addSpecialCharsHelperActionsForReplace(QSet<QAction *> *actionList, QMenu *menu)
88
{
89
    QSet<QAction *> &actionPointers = *actionList;
90 91
    QString emptyQSTring;

92 93
    actionPointers << menuEntry(menu, QStringLiteral("\\n"), emptyQSTring, i18n("Line break"));
    actionPointers << menuEntry(menu, QStringLiteral("\\t"), emptyQSTring, i18n("Tab"));
94 95 96 97 98
}

/**
 * adds items and separators for regex in "search" field
 */
99
static void addRegexHelperActionsForSearch(QSet<QAction *> *actionList, QMenu *menu)
100
{
101
    QSet<QAction *> &actionPointers = *actionList;
102 103
    QString emptyQSTring;

104 105
    actionPointers << menuEntry(menu, QStringLiteral("^"), emptyQSTring, i18n("Beginning of line"));
    actionPointers << menuEntry(menu, QStringLiteral("$"), emptyQSTring, i18n("End of line"));
106
    menu->addSeparator();
107 108
    actionPointers << menuEntry(menu, QStringLiteral("."), emptyQSTring, i18n("Any single character (excluding line breaks)"));
    actionPointers << menuEntry(menu, QStringLiteral("[.]"), emptyQSTring, i18n("Literal dot"));
109
    menu->addSeparator();
110 111 112
    actionPointers << menuEntry(menu, QStringLiteral("+"), emptyQSTring, i18n("One or more occurrences"));
    actionPointers << menuEntry(menu, QStringLiteral("*"), emptyQSTring, i18n("Zero or more occurrences"));
    actionPointers << menuEntry(menu, QStringLiteral("?"), emptyQSTring, i18n("Zero or one occurrences"));
113 114 115
    actionPointers << menuEntry(menu, QStringLiteral("{"), QStringLiteral(",}"), i18n("<a> through <b> occurrences"), QStringLiteral("{a"), QStringLiteral(",b}"));
    menu->addSeparator();
    actionPointers << menuEntry(menu, QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing"));
116
    actionPointers << menuEntry(menu, QStringLiteral("|"), emptyQSTring, i18n("Or"));
117 118 119 120 121 122 123
    actionPointers << menuEntry(menu, QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters"));
    actionPointers << menuEntry(menu, QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters"));
    actionPointers << menuEntry(menu, QStringLiteral("(?:"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:E"));
    actionPointers << menuEntry(menu, QStringLiteral("(?="), QStringLiteral(")"), i18n("Lookahead"), QStringLiteral("(?=E"));
    actionPointers << menuEntry(menu, QStringLiteral("(?!"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!E"));

    menu->addSeparator();
124 125 126 127 128 129 130 131 132 133
    actionPointers << menuEntry(menu, QStringLiteral("\\n"), emptyQSTring, i18n("Line break"));
    actionPointers << menuEntry(menu, QStringLiteral("\\t"), emptyQSTring, i18n("Tab"));
    actionPointers << menuEntry(menu, QStringLiteral("\\b"), emptyQSTring, i18n("Word boundary"));
    actionPointers << menuEntry(menu, QStringLiteral("\\B"), emptyQSTring, i18n("Not word boundary"));
    actionPointers << menuEntry(menu, QStringLiteral("\\d"), emptyQSTring, i18n("Digit"));
    actionPointers << menuEntry(menu, QStringLiteral("\\D"), emptyQSTring, i18n("Non-digit"));
    actionPointers << menuEntry(menu, QStringLiteral("\\s"), emptyQSTring, i18n("Whitespace (excluding line breaks)"));
    actionPointers << menuEntry(menu, QStringLiteral("\\S"), emptyQSTring, i18n("Non-whitespace (excluding line breaks)"));
    actionPointers << menuEntry(menu, QStringLiteral("\\w"), emptyQSTring, i18n("Word character (alphanumerics plus '_')"));
    actionPointers << menuEntry(menu, QStringLiteral("\\W"), emptyQSTring, i18n("Non-word character"));
134 135 136 137 138
}

/**
 * adds items and separators for regex in "replace" field
 */
139
static void addRegexHelperActionsForReplace(QSet<QAction *> *actionList, QMenu *menu)
140
{
141
    QSet<QAction *> &actionPointers = *actionList;
142 143 144
    QString emptyQSTring;

    menu->addSeparator();
145
    actionPointers << menuEntry(menu, QStringLiteral("\\0"), emptyQSTring, i18n("Regular expression capture 0 (whole match)"));
146 147 148
    actionPointers << menuEntry(menu, QStringLiteral("\\"), emptyQSTring, i18n("Regular expression capture 1-9"), QStringLiteral("\\#"));
    actionPointers << menuEntry(menu, QStringLiteral("\\{"), QStringLiteral("}"), i18n("Regular expression capture 0-999"), QStringLiteral("\\{#"));
    menu->addSeparator();
149
    actionPointers << menuEntry(menu, QStringLiteral("\\U\\"), emptyQSTring, i18n("Upper-cased capture 0-9"), QStringLiteral("\\U\\#"));
150
    actionPointers << menuEntry(menu, QStringLiteral("\\U\\{"), QStringLiteral("}"), i18n("Upper-cased capture 0-999"), QStringLiteral("\\U\\{###"));
151
    actionPointers << menuEntry(menu, QStringLiteral("\\L\\"), emptyQSTring, i18n("Lower-cased capture 0-9"), QStringLiteral("\\L\\#"));
152 153 154 155 156 157
    actionPointers << menuEntry(menu, QStringLiteral("\\L\\{"), QStringLiteral("}"), i18n("Lower-cased capture 0-999"), QStringLiteral("\\L\\{###"));
}

/**
 * inserts text and sets cursor position
 */
158
static void regexHelperActOnAction(QAction *resultAction, const QSet<QAction *> &actionList, QLineEdit *lineEdit)
159 160 161 162
{
    if (resultAction && actionList.contains(resultAction)) {
        const int cursorPos = lineEdit->cursorPosition();
        QStringList beforeAfter = resultAction->data().toString().split(QLatin1Char(' '));
163 164
        if (beforeAfter.size() != 2)
            return;
165 166 167 168 169 170
        lineEdit->insert(beforeAfter[0] + beforeAfter[1]);
        lineEdit->setCursorPosition(cursorPos + beforeAfter[0].count());
        lineEdit->setFocus();
    }
}

171 172
class TreeWidgetItem : public QTreeWidgetItem
{
173
public:
174 175 176 177 178 179 180 181 182 183 184 185 186
    TreeWidgetItem(QTreeWidget *parent)
        : QTreeWidgetItem(parent)
    {
    }
    TreeWidgetItem(QTreeWidget *parent, const QStringList &list)
        : QTreeWidgetItem(parent, list)
    {
    }
    TreeWidgetItem(QTreeWidgetItem *parent, const QStringList &list)
        : QTreeWidgetItem(parent, list)
    {
    }

187
private:
188 189
    bool operator<(const QTreeWidgetItem &other) const override
    {
190
        if (childCount() == 0) {
191 192 193 194
            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();
195 196 197 198 199 200 201 202
            if (line < oLine) {
                return true;
            }
            if ((line == oLine) && (column < oColumn)) {
                return true;
            }
            return false;
        }
203 204
        int sepCount = data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator());
        int oSepCount = other.data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator());
205 206 207 208
        if (sepCount < oSepCount)
            return true;
        if (sepCount > oSepCount)
            return false;
209
        return data(0, ReplaceMatches::FileUrlRole).toString().toLower() < other.data(0, ReplaceMatches::FileUrlRole).toString().toLower();
210 211 212
    }
};

213 214
Results::Results(QWidget *parent)
    : QWidget(parent)
215 216
{
    setupUi(this);
217

218 219 220
    tree->setItemDelegate(new SPHtmlDelegate(tree));
}

221
K_PLUGIN_FACTORY_WITH_JSON(KatePluginSearchFactory, "katesearch.json", registerPlugin<KatePluginSearch>();)
222

223 224
KatePluginSearch::KatePluginSearch(QObject *parent, const QList<QVariant> &)
    : KTextEditor::Plugin(parent)
Kåre Särs's avatar
Kåre Särs committed
225
{
226
    m_searchCommand = new KateSearchCommand(this);
Kåre Särs's avatar
Kåre Särs committed
227 228 229 230
}

KatePluginSearch::~KatePluginSearch()
{
231
    delete m_searchCommand;
Kåre Särs's avatar
Kåre Särs committed
232 233
}

234
QObject *KatePluginSearch::createView(KTextEditor::MainWindow *mainWindow)
Kåre Särs's avatar
Kåre Särs committed
235
{
236
    KatePluginSearchView *view = new KatePluginSearchView(this, mainWindow, KTextEditor::Editor::instance()->application());
237 238 239 240
    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);
241
    connect(m_searchCommand, SIGNAL(newTab()), view, SLOT(addTab()));
242
    return view;
Kåre Särs's avatar
Kåre Särs committed
243 244
}

245
bool ContainerWidget::focusNextPrevChild(bool next)
246
{
247
    QWidget *fw = focusWidget();
248 249
    bool found = false;
    emit nextFocus(fw, &found, next);
250

251 252 253 254
    if (found) {
        return true;
    }
    return QWidget::focusNextPrevChild(next);
255 256
}

257
void KatePluginSearchView::nextFocus(QWidget *currentWidget, bool *found, bool next)
258
{
259 260 261 262 263 264
    *found = false;

    if (!currentWidget) {
        return;
    }

265
    // we use the object names here because there can be multiple trees (on multiple result tabs)
266
    if (next) {
267
        if (currentWidget->objectName() == QLatin1String("tree") || currentWidget == m_ui.binaryCheckBox) {
268 269 270 271 272 273
            m_ui.searchCombo->setFocus();
            *found = true;
            return;
        }
        if (currentWidget == m_ui.excludeCombo && m_ui.searchPlaceCombo->currentIndex() > Folder) {
            m_ui.searchCombo->setFocus();
274 275 276
            *found = true;
            return;
        }
277 278
        if (currentWidget == m_ui.displayOptions) {
            if (m_ui.displayOptions->isChecked()) {
279 280 281 282 283 284 285 286 287 288 289 290 291
                if (m_ui.searchPlaceCombo->currentIndex() < Folder) {
                    m_ui.searchCombo->setFocus();
                    *found = true;
                    return;
                } else if (m_ui.searchPlaceCombo->currentIndex() == Folder) {
                    m_ui.folderRequester->setFocus();
                    *found = true;
                    return;
                } else {
                    m_ui.filterCombo->setFocus();
                    *found = true;
                    return;
                }
292
            } else {
293 294 295 296 297 298 299 300
                Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
                if (!res) {
                    return;
                }
                res->tree->setFocus();
                *found = true;
                return;
            }
301
        }
302
    } else {
303
        if (currentWidget == m_ui.searchCombo) {
304
            if (m_ui.displayOptions->isChecked()) {
305 306 307 308 309 310 311 312 313 314 315 316 317
                if (m_ui.searchPlaceCombo->currentIndex() < Folder) {
                    m_ui.displayOptions->setFocus();
                    *found = true;
                    return;
                } else if (m_ui.searchPlaceCombo->currentIndex() == Folder) {
                    m_ui.binaryCheckBox->setFocus();
                    *found = true;
                    return;
                } else {
                    m_ui.excludeCombo->setFocus();
                    *found = true;
                    return;
                }
318
            } else {
319 320 321 322 323 324 325 326
                Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
                if (!res) {
                    return;
                }
                res->tree->setFocus();
            }
            *found = true;
            return;
327
        } else {
328
            if (currentWidget->objectName() == QLatin1String("tree")) {
329 330
                m_ui.displayOptions->setFocus();
                *found = true;
331 332 333
                return;
            }
        }
334 335 336
    }
}

337 338 339 340
KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin, KTextEditor::Application *application)
    : QObject(mainWin)
    , m_kateApp(application)
    , m_mainWindow(mainWin)
Kåre Särs's avatar
Kåre Särs committed
341
{
342 343
    KXMLGUIClient::setComponentName(QStringLiteral("katesearch"), i18n("Kate Search & Replace"));
    setXMLFile(QStringLiteral("ui.rc"));
344

345
    m_toolView = mainWin->createToolView(plugin, QStringLiteral("kate_plugin_katesearch"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search and Replace"));
346

347
    ContainerWidget *container = new ContainerWidget(m_toolView);
348
    m_ui.setupUi(container);
Kåre Särs's avatar
Kåre Särs committed
349
    container->setFocusProxy(m_ui.searchCombo);
350
    connect(container, &ContainerWidget::nextFocus, this, &KatePluginSearchView::nextFocus);
351

Joseph Wenninger's avatar
Joseph Wenninger committed
352
    QAction *a = actionCollection()->addAction(QStringLiteral("search_in_files"));
353
    actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_F));
Kåre Särs's avatar
Kåre Särs committed
354
    a->setText(i18n("Search in Files"));
355
    connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView);
356

Joseph Wenninger's avatar
Joseph Wenninger committed
357
    a = actionCollection()->addAction(QStringLiteral("search_in_files_new_tab"));
358 359
    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
360 361
    connect(a, &QAction::triggered, this, &KatePluginSearchView::addTab);
    connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView);
362

Joseph Wenninger's avatar
Joseph Wenninger committed
363
    a = actionCollection()->addAction(QStringLiteral("go_to_next_match"));
364
    a->setText(i18n("Go to Next Match"));
365
    actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F6));
366
    connect(a, &QAction::triggered, this, &KatePluginSearchView::goToNextMatch);
367

Joseph Wenninger's avatar
Joseph Wenninger committed
368
    a = actionCollection()->addAction(QStringLiteral("go_to_prev_match"));
369
    a->setText(i18n("Go to Previous Match"));
370
    actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::SHIFT | Qt::Key_F6));
371
    connect(a, &QAction::triggered, this, &KatePluginSearchView::goToPreviousMatch);
372

373
    m_ui.resultTabWidget->tabBar()->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab);
374
    KAcceleratorManager::setNoAccel(m_ui.resultTabWidget);
375

376
    // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing.
377 378 379 380
    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")));
381 382

    m_ui.displayOptions->setIcon(dispOptIcon);
383
    m_ui.searchButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
384
    m_ui.nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
385
    m_ui.stopButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
386 387 388
    m_ui.matchCase->setIcon(matchCaseIcon);
    m_ui.useRegExp->setIcon(useRegExpIcon);
    m_ui.expandResults->setIcon(expandResultsIcon);
389 390 391
    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")));
392 393 394
    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")));
395

396 397
    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*\""));
398

399 400
    // the order here is important to get the tabBar hidden for only one tab
    addTab();
401
    m_ui.resultTabWidget->tabBar()->hide();
402 403

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

412 413 414
    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);
415

416 417 418
    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);
419

420 421
    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));
422
    connect(m_ui.matchCase, &QToolButton::toggled, this, [=] {
423 424 425 426 427
        Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
        if (res) {
            res->matchCase = m_ui.matchCase->isChecked();
        }
    });
428

429
    connect(m_ui.searchCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch);
430
    // connecting to returnPressed() of the folderRequester doesn't work, I haven't found out why yet. But connecting to the linedit works:
431 432 433 434
    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);
435

436 437
    connect(m_ui.displayOptions, &QToolButton::toggled, this, &KatePluginSearchView::toggleOptions);
    connect(m_ui.searchPlaceCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &KatePluginSearchView::searchPlaceChanged);
438 439 440 441 442
    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);
        }
    });
443

444
    connect(m_ui.stopButton, &QPushButton::clicked, this, &KatePluginSearchView::stopClicked);
445

446
    connect(m_ui.nextButton, &QToolButton::clicked, this, &KatePluginSearchView::goToNextMatch);
447

448 449 450
    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);
451

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

454 455
    connect(&m_searchOpenFiles, &SearchOpenFiles::matchFound, this, &KatePluginSearchView::matchFound);
    connect(&m_searchOpenFiles, &SearchOpenFiles::searchDone, this, &KatePluginSearchView::searchDone);
456
    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
457

458
    connect(&m_folderFilesList, &FolderFilesList::fileListReady, this, &KatePluginSearchView::folderFileListChanged);
459
    connect(&m_folderFilesList, &FolderFilesList::searching, this, &KatePluginSearchView::searching);
460

461 462
    connect(&m_searchDiskFiles, &SearchDiskFiles::matchFound, this, &KatePluginSearchView::matchFound);
    connect(&m_searchDiskFiles, &SearchDiskFiles::searchDone, this, &KatePluginSearchView::searchDone);
463
    connect(&m_searchDiskFiles, static_cast<void (SearchDiskFiles::*)(const QString &)>(&SearchDiskFiles::searching), this, &KatePluginSearchView::searching);
464

465
    connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch);
466

467
    connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_replacer, &ReplaceMatches::cancelReplace);
468

469
    connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, this, &KatePluginSearchView::clearDocMarks);
470

471 472
    connect(&m_replacer, &ReplaceMatches::replaceStatus, this, &KatePluginSearchView::replaceStatus);

473
    m_ui.searchCombo->lineEdit()->setPlaceholderText(i18n("Find"));
474 475
    // Hook into line edit context menus
    m_ui.searchCombo->setContextMenuPolicy(Qt::CustomContextMenu);
476
    connect(m_ui.searchCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::searchContextMenu);
477
    m_ui.searchCombo->completer()->setCompletionMode(QCompleter::PopupCompletion);
478
    m_ui.searchCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
479 480 481
    m_ui.searchCombo->setInsertPolicy(QComboBox::NoInsert);
    m_ui.searchCombo->lineEdit()->setClearButtonEnabled(true);
    m_ui.searchCombo->setMaxCount(25);
482
    QAction *searchComboActionForInsertRegexButton = m_ui.searchCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace"))), QLineEdit::TrailingPosition);
483 484
    connect(searchComboActionForInsertRegexButton, &QAction::triggered, this, [this]() {
        QMenu menu;
485
        QSet<QAction *> actionList;
486
        addRegexHelperActionsForSearch(&actionList, &menu);
487
        auto &&action = menu.exec(QCursor::pos());
488 489
        regexHelperActOnAction(action, actionList, m_ui.searchCombo->lineEdit());
    });
490

491 492
    m_ui.replaceCombo->lineEdit()->setPlaceholderText(i18n("Replace"));
    // Hook into line edit context menus
493 494
    m_ui.replaceCombo->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(m_ui.replaceCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::replaceContextMenu);
495
    m_ui.replaceCombo->completer()->setCompletionMode(QCompleter::PopupCompletion);
496
    m_ui.replaceCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
497 498 499
    m_ui.replaceCombo->setInsertPolicy(QComboBox::NoInsert);
    m_ui.replaceCombo->lineEdit()->setClearButtonEnabled(true);
    m_ui.replaceCombo->setMaxCount(25);
500
    QAction *replaceComboActionForInsertRegexButton = m_ui.replaceCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("code-context")), QLineEdit::TrailingPosition);
501 502
    connect(replaceComboActionForInsertRegexButton, &QAction::triggered, this, [this]() {
        QMenu menu;
503
        QSet<QAction *> actionList;
504
        addRegexHelperActionsForReplace(&actionList, &menu);
505
        auto &&action = menu.exec(QCursor::pos());
506 507
        regexHelperActOnAction(action, actionList, m_ui.replaceCombo->lineEdit());
    });
508
    QAction *replaceComboActionForInsertSpecialButton = m_ui.replaceCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("insert-text")), QLineEdit::TrailingPosition);
509 510
    connect(replaceComboActionForInsertSpecialButton, &QAction::triggered, this, [this]() {
        QMenu menu;
511
        QSet<QAction *> actionList;
512
        addSpecialCharsHelperActionsForReplace(&actionList, &menu);
513
        auto &&action = menu.exec(QCursor::pos());
514 515 516 517
        regexHelperActOnAction(action, actionList, m_ui.replaceCombo->lineEdit());
    });

    connect(m_ui.useRegExp, &QToolButton::toggled, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
518
    auto onRegexToggleChanged = [=] {
519 520 521 522 523 524 525 526 527 528 529 530 531
        Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
        if (res) {
            bool useRegExp = m_ui.useRegExp->isChecked();
            res->useRegExp = useRegExp;
            searchComboActionForInsertRegexButton->setVisible(useRegExp);
            replaceComboActionForInsertRegexButton->setVisible(useRegExp);
        }
    };
    connect(m_ui.useRegExp, &QToolButton::toggled, this, onRegexToggleChanged);
    onRegexToggleChanged(); // invoke initially
    m_changeTimer.setInterval(300);
    m_changeTimer.setSingleShot(true);
    connect(&m_changeTimer, &QTimer::timeout, this, &KatePluginSearchView::startSearchWhileTyping);
532

533 534
    m_toolView->setMinimumHeight(container->sizeHint().height());

535
    connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KatePluginSearchView::handleEsc);
536

537
    // watch for project plugin view creation/deletion
538
    connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, &KatePluginSearchView::slotPluginViewCreated);
539

540
    connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, &KatePluginSearchView::slotPluginViewDeleted);
541

542
    connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KatePluginSearchView::docViewChanged);
543

544 545 546
    // Connect signals from project plugin to our slots
    m_projectPluginView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
    slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView);
547

548
    m_replacer.setDocumentManager(m_kateApp);
549
    connect(&m_replacer, &ReplaceMatches::replaceDone, this, &KatePluginSearchView::replaceDone);
550

551 552
    searchPlaceChanged();

553 554
    m_toolView->installEventFilter(this);

555
    m_mainWindow->guiFactory()->addClient(this);
556 557

    m_updateSumaryTimer.setInterval(1);
558
    m_updateSumaryTimer.setSingleShot(true);
559
    connect(&m_updateSumaryTimer, &QTimer::timeout, this, &KatePluginSearchView::updateResultsRootItem);
Kåre Särs's avatar
Kåre Särs committed
560 561 562 563
}

KatePluginSearchView::~KatePluginSearchView()
{
564 565
    clearMarks();

566
    m_mainWindow->guiFactory()->removeClient(this);
567
    delete m_toolView;
Kåre Särs's avatar
Kåre Särs committed
568 569
}

570
void KatePluginSearchView::navigateFolderUp()
571 572
{
    // navigate one folder up
573
    m_ui.folderRequester->setUrl(localFileDirUp(m_ui.folderRequester->url()));
574 575
}

576 577
void KatePluginSearchView::setCurrentFolder()
{
578
    if (!m_mainWindow) {
579 580
        return;
    }
581
    KTextEditor::View *editView = m_mainWindow->activeView();
582 583
    if (editView && editView->document()) {
        // upUrl as we want the folder not the file
584
        m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url()));
585
    }
586
    m_ui.displayOptions->setChecked(true);
587 588
}

589
void KatePluginSearchView::openSearchView()
Kåre Särs's avatar
Kåre Särs committed
590
{
591
    if (!m_mainWindow) {
Kåre Särs's avatar
Kåre Särs committed
592 593 594
        return;
    }
    if (!m_toolView->isVisible()) {
595
        m_mainWindow->showToolView(m_toolView);
596
    }
597
    m_ui.searchCombo->setFocus(Qt::OtherFocusReason);
598 599 600
    if (m_ui.searchPlaceCombo->currentIndex() == Folder) {
        m_ui.displayOptions->setChecked(true);
    }
601

602
    KTextEditor::View *editView = m_mainWindow->activeView();
603
    if (editView && editView->document()) {
604
        if (m_ui.folderRequester->text().isEmpty()) {
605
            // upUrl as we want the folder not the file
606
            m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url()));
607
        }
608
        QString selection;
609
        if (editView->selection()) {
610
            selection = editView->selectionText();
611
            // remove possible trailing '\n'
612
            if (selection.endsWith(QLatin1Char('\n'))) {
613
                selection = selection.left(selection.size() - 1);
614
            }
615
        }
616
        if (selection.isEmpty()) {
617
            selection = editView->document()->wordAt(editView->cursorPosition());
618 619
        }

620
        if (!selection.isEmpty() && !selection.contains(QLatin1Char('\n'))) {
621 622 623 624 625
            m_ui.searchCombo->blockSignals(true);
            m_ui.searchCombo->lineEdit()->setText(selection);
            m_ui.searchCombo->blockSignals(false);
        }

626 627
        m_ui.searchCombo->lineEdit()->selectAll();
        m_searchJustOpened = true;
628
        startSearchWhileTyping();
Kåre Särs's avatar
Kåre Särs committed
629 630 631
    }
}

632 633
void KatePluginSearchView::handleEsc(QEvent *e)
{
634 635
    if (!m_mainWindow)
        return;
636 637 638

    QKeyEvent *k = static_cast<QKeyEvent *>(e);
    if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
639 640 641 642
        static ulong lastTimeStamp;
        if (lastTimeStamp == k->timestamp()) {
            // Same as previous... This looks like a bug somewhere...
            return;
643
        }
644 645
        lastTimeStamp = k->timestamp();
        if (!m_matchRanges.isEmpty()) {
646
            clearMarks();
647
        } else if (m_toolView->isVisible()) {
648 649
            m_mainWindow->hideToolView(m_toolView);
        }
650

651 652 653 654 655 656 657 658 659 660 661
        // 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;
        }
662
    }
663 664
}

665 666 667 668 669
void KatePluginSearchView::setSearchString(const QString &pattern)
{
    m_ui.searchCombo->lineEdit()->setText(pattern);
}

670 671
void KatePluginSearchView::toggleOptions(bool show)
{
672
    m_ui.stackedWidget->setCurrentIndex((show) ? 1 : 0);
673 674 675 676
}

void KatePluginSearchView::setSearchPlace(int place)
{
677 678 679 680 681 682
    if (place >= m_ui.searchPlaceCombo->count()) {
        // This probably means the project plugin is not active or no project loaded
        // fallback to search in folder
        qDebug() << place << "is not a valid search place index";
        place = Folder;
    }
683 684
    m_ui.searchPlaceCombo->setCurrentIndex(place);
}
685

686
QStringList KatePluginSearchView::filterFiles(const QStringList &files) const
687 688
{
    QString types = m_ui.filterCombo->currentText();
689
    QString excludes = m_ui.excludeCombo->currentText();
690
    if (((types.isEmpty() || types == QLatin1String("*"))) && (excludes.isEmpty())) {
691 692 693 694
        // shortcut for use all files
        return files;
    }

695
    QStringList tmpTypes = types.split(QLatin1Char(','));
696
    QVector<QRegExp> typeList(tmpTypes.size());
697
    for (int i = 0; i < tmpTypes.size(); i++) {
698
        QRegExp rx(tmpTypes[i].trimmed());
699 700 701 702
        rx.setPatternSyntax(QRegExp::Wildcard);
        typeList << rx;
    }

703
    QStringList tmpExcludes = excludes.split(QLatin1Char(','));
704
    QVector<QRegExp> excludeList(tmpExcludes.size());
705
    for (int i = 0; i < tmpExcludes.size(); i++) {
706
        QRegExp rx(tmpExcludes[i].trimmed());
707 708 709 710
        rx.setPatternSyntax(QRegExp::Wildcard);
        excludeList << rx;
    }

711
    QStringList filteredFiles;
712
    for (const QString &fileName : files) {
713
        bool isInSubDir = fileName.startsWith(m_resultBaseDir);
714
        QString nameToCheck = fileName;
715
        if (isInSubDir) {
716
            nameToCheck = fileName.mid(m_resultBaseDir.size());
717 718
        }

719
        bool skip = false;
720
        for (const auto &regex : qAsConst(excludeList)) {
721
            if (regex.exactMatch(nameToCheck)) {
722 723 724 725 726 727 728 729
                skip = true;
                break;
            }
        }
        if (skip) {
            continue;
        }