plugin_search.cpp 66.4 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 46
#include <kiconloader.h>

#include <KXMLGUIFactory>
#include <KConfigGroup>
47

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

static QUrl localFileDirUp (const QUrl &url)
{
    if (!url.isLocalFile())
        return url;
    
    // else go up
    return QUrl::fromLocalFile (QFileInfo (url.toLocalFile()).dir().absolutePath());
}
65 66 67 68 69 70 71 72 73 74 75 76

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;

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

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 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:
    bool operator<(const QTreeWidgetItem &other)const {
91
        if (childCount() == 0) {
92 93 94 95
            int line = data(0, ReplaceMatches::LineRole).toInt();
            int column = data(0, ReplaceMatches::ColumnRole).toInt();
            int oLine = other.data(0, ReplaceMatches::LineRole).toInt();
            int oColumn = other.data(0, ReplaceMatches::ColumnRole).toInt();
96 97 98 99 100 101 102 103
            if (line < oLine) {
                return true;
            }
            if ((line == oLine) && (column < oColumn)) {
                return true;
            }
            return false;
        }
104 105
        int sepCount = data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator());
        int oSepCount = other.data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator());
106 107
        if (sepCount < oSepCount) return true;
        if (sepCount > oSepCount) return false;
108
        return data(0, ReplaceMatches::FileUrlRole).toString().toLower() < other.data(0, ReplaceMatches::FileUrlRole).toString().toLower();
109 110 111
    }
};

112 113 114
Results::Results(QWidget *parent): QWidget(parent), matches(0)
{
    setupUi(this);
115

116 117 118 119
    tree->setItemDelegate(new SPHtmlDelegate(tree));
}


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

KatePluginSearch::KatePluginSearch(QObject* parent, const QList<QVariant>&)
123
    : KTextEditor::Plugin (parent),
124
    m_searchCommand(0)
Kåre Särs's avatar
Kåre Särs committed
125
{
126 127
    // FIXME KF5
    //KGlobal::locale()->insertCatalog("katesearch");
128

129 130
    m_searchCommand = new KateSearchCommand(this);
    KTextEditor::Editor::instance()->registerCommand(m_searchCommand);
Kåre Särs's avatar
Kåre Särs committed
131 132 133 134
}

KatePluginSearch::~KatePluginSearch()
{
135 136
    if (m_searchCommand) {
        KTextEditor::Editor::instance()->unregisterCommand(m_searchCommand);
137
    }
Kåre Särs's avatar
Kåre Särs committed
138 139
}

140
QObject *KatePluginSearch::createView(KTextEditor::MainWindow *mainWindow)
Kåre Särs's avatar
Kåre Särs committed
141
{
142
    KatePluginSearchView *view = new KatePluginSearchView(this, mainWindow, KTextEditor::Editor::instance()->application());
143 144 145 146
    connect(m_searchCommand, SIGNAL(setSearchPlace(int)), view, SLOT(setSearchPlace(int)));
    connect(m_searchCommand, SIGNAL(setCurrentFolder()), view, SLOT(setCurrentFolder()));
    connect(m_searchCommand, SIGNAL(setSearchString(QString)), view, SLOT(setSearchString(QString)));
    connect(m_searchCommand, SIGNAL(startSearch()), view, SLOT(startSearch()));
147
    connect(m_searchCommand, SIGNAL(newTab()), view, SLOT(addTab()));
148

149
    return view;
Kåre Särs's avatar
Kåre Särs committed
150 151 152
}


153
bool ContainerWidget::focusNextPrevChild (bool next)
154
{
155 156 157
    QWidget* fw = focusWidget();
    bool found = false;
    emit nextFocus(fw, &found, next);
158

159 160 161 162
    if (found) {
        return true;
    }
    return QWidget::focusNextPrevChild(next);
163 164
}

165
void KatePluginSearchView::nextFocus(QWidget *currentWidget, bool *found, bool next)
166
{
167 168 169 170 171 172
    *found = false;

    if (!currentWidget) {
        return;
    }

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

222
KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin, KTextEditor::Application* application)
223
: QObject (mainWin),
224
m_kateApp(application),
225
m_curResults(0),
226
m_searchJustOpened(false),
227
m_switchToProjectModeWhenAvailable(false),
228 229
m_searchDiskFilesDone(true),
m_searchOpenFilesDone(true),
230 231
m_projectPluginView(0),
m_mainWindow (mainWin)
Kåre Särs's avatar
Kåre Särs committed
232
{
Joseph Wenninger's avatar
Joseph Wenninger committed
233 234
    KXMLGUIClient::setComponentName (QStringLiteral("katesearch"), i18n ("Kate Search & Replace"));
    setXMLFile( QStringLiteral("ui.rc") );
235
  
Joseph Wenninger's avatar
Joseph Wenninger committed
236
    m_toolView = mainWin->createToolView (plugin, QStringLiteral("kate_plugin_katesearch"),
237
                                          KTextEditor::MainWindow::Bottom,
Joseph Wenninger's avatar
Joseph Wenninger committed
238
                                          SmallIcon(QStringLiteral("edit-find")),
239 240
                                          i18n("Search and Replace"));

241
    ContainerWidget *container = new ContainerWidget(m_toolView);
242
    m_ui.setupUi(container);
Kåre Särs's avatar
Kåre Särs committed
243
    container->setFocusProxy(m_ui.searchCombo);
244
    connect(container, SIGNAL(nextFocus(QWidget*,bool*,bool)), this, SLOT(nextFocus(QWidget*,bool*,bool)));
245

Joseph Wenninger's avatar
Joseph Wenninger committed
246
    QAction *a = actionCollection()->addAction(QStringLiteral("search_in_files"));
Kåre Särs's avatar
Kåre Särs committed
247
    a->setText(i18n("Search in Files"));
248
    connect(a, SIGNAL(triggered(bool)), this, SLOT(openSearchView()));
249

Joseph Wenninger's avatar
Joseph Wenninger committed
250
    a = actionCollection()->addAction(QStringLiteral("search_in_files_new_tab"));
251 252
    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
253
    connect(a, SIGNAL(triggered(bool)), this, SLOT(addTab()));
254
    connect(a, SIGNAL(triggered(bool)), this, SLOT(openSearchView()));
255

Joseph Wenninger's avatar
Joseph Wenninger committed
256
    a = actionCollection()->addAction(QStringLiteral("go_to_next_match"));
257 258 259
    a->setText(i18n("Go to Next Match"));
    connect(a, SIGNAL(triggered(bool)), this, SLOT(goToNextMatch()));

Joseph Wenninger's avatar
Joseph Wenninger committed
260
    a = actionCollection()->addAction(QStringLiteral("go_to_prev_match"));
261 262 263
    a->setText(i18n("Go to Previous Match"));
    connect(a, SIGNAL(triggered(bool)), this, SLOT(goToPreviousMatch()));

264
    m_ui.resultTabWidget->tabBar()->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab);
265
    KAcceleratorManager::setNoAccel(m_ui.resultTabWidget);
266

Joseph Wenninger's avatar
Joseph Wenninger committed
267 268 269 270 271 272 273 274
    m_ui.displayOptions->setIcon(SmallIcon(QStringLiteral("arrow-down-double")));
    m_ui.searchButton->setIcon(SmallIcon(QStringLiteral("edit-find")));
    m_ui.stopButton->setIcon(SmallIcon(QStringLiteral("process-stop")));
    m_ui.searchPlaceCombo->setItemIcon(0, SmallIcon(QStringLiteral("text-plain")));
    m_ui.searchPlaceCombo->setItemIcon(1, SmallIcon(QStringLiteral("folder")));
    m_ui.folderUpButton->setIcon(SmallIcon(QStringLiteral("go-up")));
    m_ui.currentFolderButton->setIcon(SmallIcon(QStringLiteral("view-refresh")));
    m_ui.newTabButton->setIcon(SmallIcon(QStringLiteral("tab-new")));
275

276 277
    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*\""));
278

279 280
    // the order here is important to get the tabBar hidden for only one tab
    addTab();
281
    m_ui.resultTabWidget->tabBar()->hide();
282 283 284 285 286 287 288 289 290 291

    // 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);

292
    connect(m_ui.newTabButton,     SIGNAL(clicked()), this, SLOT(addTab()));
Christoph Cullmann's avatar
Christoph Cullmann committed
293
    connect(m_ui.resultTabWidget,  SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int)));
294
    connect(m_ui.resultTabWidget,  SIGNAL(currentChanged(int)), this, SLOT(resultTabChanged(int)));
295

296
    connect(m_ui.folderUpButton,   SIGNAL(clicked()), this, SLOT(navigateFolderUp()));
297 298
    connect(m_ui.currentFolderButton, SIGNAL(clicked()), this, SLOT(setCurrentFolder()));

299 300 301 302
    connect(m_ui.searchCombo,      SIGNAL(editTextChanged(QString)), &m_changeTimer, SLOT(start()));
    connect(m_ui.matchCase,        SIGNAL(stateChanged(int)), &m_changeTimer, SLOT(start()));
    connect(m_ui.useRegExp,        SIGNAL(stateChanged(int)), &m_changeTimer, SLOT(start()));
    m_changeTimer.setInterval(300);
Kåre Särs's avatar
Kåre Särs committed
303
    m_changeTimer.setSingleShot(true);
304 305 306 307 308 309 310 311 312 313
    connect(&m_changeTimer, SIGNAL(timeout()), this, SLOT(startSearchWhileTyping()));

    connect(m_ui.searchCombo,      SIGNAL(returnPressed()), this, SLOT(startSearch()));
    connect(m_ui.folderRequester,  SIGNAL(returnPressed()), this, SLOT(startSearch()));
    connect(m_ui.filterCombo,      SIGNAL(returnPressed()), this, SLOT(startSearch()));
    connect(m_ui.excludeCombo,     SIGNAL(returnPressed()), this, SLOT(startSearch()));
    connect(m_ui.searchButton,     SIGNAL(clicked()),       this, SLOT(startSearch()));

    connect(m_ui.displayOptions,   SIGNAL(toggled(bool)), this, SLOT(toggleOptions(bool)));
    connect(m_ui.searchPlaceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(searchPlaceChanged()));
314

315
    connect(m_ui.stopButton,       SIGNAL(clicked()), &m_searchOpenFiles, SLOT(cancelSearch()));
316 317
    connect(m_ui.stopButton,       SIGNAL(clicked()), &m_searchDiskFiles, SLOT(cancelSearch()));
    connect(m_ui.stopButton,       SIGNAL(clicked()), &m_folderFilesList, SLOT(cancelSearch()));
318
    connect(m_ui.stopButton,       SIGNAL(clicked()), &m_replacer,        SLOT(cancelReplace()));
319

320 321 322 323 324 325 326 327
    connect(m_ui.nextButton,       SIGNAL(clicked()), this, SLOT(goToNextMatch()));

    connect(m_ui.replaceButton,     SIGNAL(clicked(bool)),   this, SLOT(replaceSingleMatch()));
    connect(m_ui.replaceCheckedBtn, SIGNAL(clicked(bool)),   this, SLOT(replaceChecked()));
    connect(m_ui.replaceCombo,      SIGNAL(returnPressed()), this, SLOT(replaceChecked()));



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

330 331
    connect(&m_searchOpenFiles, SIGNAL(matchFound(QString,QString,int,int,QString,int)),
            this,                 SLOT(matchFound(QString,QString,int,int,QString,int)));
Kåre Särs's avatar
Kåre Särs committed
332
    connect(&m_searchOpenFiles, SIGNAL(searchDone()),  this, SLOT(searchDone()));
333
    connect(&m_searchOpenFiles, SIGNAL(searching(QString)), this, SLOT(searching(QString)));
Kåre Särs's avatar
Kåre Särs committed
334

335
    connect(&m_folderFilesList, SIGNAL(finished()),  this, SLOT(folderFileListChanged()));
336

337 338
    connect(&m_searchDiskFiles, SIGNAL(matchFound(QString,QString,int,int,QString,int)),
            this,                 SLOT(matchFound(QString,QString,int,int,QString,int)));
339
    connect(&m_searchDiskFiles, SIGNAL(searchDone()),  this, SLOT(searchDone()));
340
    connect(&m_searchDiskFiles, SIGNAL(searching(QString)), this, SLOT(searching(QString)));
341

342
    connect(m_kateApp, SIGNAL(documentWillBeDeleted(KTextEditor::Document*)),
343
            &m_searchOpenFiles, SLOT(cancelSearch()));
344

345
    connect(m_kateApp, SIGNAL(documentWillBeDeleted(KTextEditor::Document*)),
346 347
            &m_replacer, SLOT(cancelReplace()));

348
    connect(m_kateApp, SIGNAL(documentWillBeDeleted(KTextEditor::Document*)),
349 350
            this, SLOT(clearDocMarks(KTextEditor::Document*)));

351 352 353
    connect(&m_replacer, SIGNAL(matchReplaced(KTextEditor::Document*,int,int,int)),
            this, SLOT(addMatchMark(KTextEditor::Document*,int,int,int)));

354 355 356 357 358
    // Hook into line edit context menus
    m_ui.searchCombo->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(m_ui.searchCombo, SIGNAL(customContextMenuRequested(QPoint)), this,
            SLOT(searchContextMenu(QPoint)));

359
    connect(m_mainWindow, SIGNAL(unhandledShortcutOverride(QEvent*)),
360 361
            this, SLOT(handleEsc(QEvent*)));

362
    // watch for project plugin view creation/deletion
Christoph Cullmann's avatar
Christoph Cullmann committed
363 364
    connect(m_mainWindow, SIGNAL(pluginViewCreated (const QString &, QObject *))
        , this, SLOT(slotPluginViewCreated (const QString &, QObject *)));
365

Christoph Cullmann's avatar
Christoph Cullmann committed
366 367
    connect(m_mainWindow, SIGNAL(pluginViewDeleted (const QString &, QObject *))
        , this, SLOT(slotPluginViewDeleted (const QString &, QObject *)));
368

Christoph Cullmann's avatar
Christoph Cullmann committed
369
    connect(m_mainWindow, SIGNAL(viewChanged(KTextEditor::View *)), this, SLOT(docViewChanged()));
370 371


372
    // update once project plugin state manually
Joseph Wenninger's avatar
Joseph Wenninger committed
373
    m_projectPluginView = m_mainWindow->pluginView (QStringLiteral("kateprojectplugin"));
374 375
    slotProjectFileNameChanged ();

376
    m_replacer.setDocumentManager(m_kateApp);
377
    connect(&m_replacer, SIGNAL(replaceDone()), this, SLOT(replaceDone()));
378

379 380
    searchPlaceChanged();

381 382
    m_toolView->installEventFilter(this);

383
    m_mainWindow->guiFactory()->addClient(this);
Kåre Särs's avatar
Kåre Särs committed
384 385 386 387
}

KatePluginSearchView::~KatePluginSearchView()
{
388 389
    clearMarks();

390
    m_mainWindow->guiFactory()->removeClient(this);
391
    delete m_toolView;
Kåre Särs's avatar
Kåre Särs committed
392 393
}

394
void KatePluginSearchView::navigateFolderUp()
395 396
{
    // navigate one folder up
397
    m_ui.folderRequester->setUrl(localFileDirUp (m_ui.folderRequester->url()));
398 399
}

400 401
void KatePluginSearchView::setCurrentFolder()
{
402
    if (!m_mainWindow) {
403 404
        return;
    }
405
    KTextEditor::View* editView = m_mainWindow->activeView();
406 407
    if (editView && editView->document()) {
        // upUrl as we want the folder not the file
408
        m_ui.folderRequester->setUrl(localFileDirUp (editView->document()->url()));
409 410 411
    }
}

412 413 414 415 416 417 418 419 420 421 422 423

QString KatePluginSearchView::currentWord(const KTextEditor::Document& document, const KTextEditor::Cursor& cursor ) const
{
    QString textLine = document.line(cursor.line());

    int len = textLine.length();

    if (cursor.column() > len) {       // Probably because of non-wrapping cursor mode.
        return QString();
    }

    int start = cursor.column();
424
    for(int currPos = cursor.column()-1; currPos >= 0; currPos--) {
425
        if (textLine.at(currPos).isLetterOrNumber() || (textLine[currPos]==QLatin1Char('_')) || (textLine[currPos]==QLatin1Char('~'))) {
426 427 428 429 430 431 432 433 434
            start = currPos;
        }
        else {
            break;
        }
    }

    int end = cursor.column();
    while (end < len && (textLine.at(end).isLetterOrNumber()
435
                     || (textLine[end]==QLatin1Char('_')) || (textLine[end]==QLatin1Char('~')))) {
436 437 438 439 440 441
        end++;
    }

    return textLine.mid(start, (end - start));
}

442
void KatePluginSearchView::openSearchView()
Kåre Särs's avatar
Kåre Särs committed
443
{
444
    if (!m_mainWindow) {
Kåre Särs's avatar
Kåre Särs committed
445 446 447
        return;
    }
    if (!m_toolView->isVisible()) {
448
        m_mainWindow->showToolView(m_toolView);
449
    }
450
    m_ui.searchCombo->setFocus(Qt::OtherFocusReason);
451
    m_ui.displayOptions->setChecked(true);
452

453
    KTextEditor::View* editView = m_mainWindow->activeView();
454
    if (editView && editView->document()) {
455
        if (m_ui.folderRequester->text().isEmpty()) {
456
            // upUrl as we want the folder not the file
457
            m_ui.folderRequester->setUrl(localFileDirUp (editView->document()->url()));
458
        }
459
        QString selection;
460
        if (editView->selection()) {
461
            selection = editView->selectionText();
462
            // remove possible trailing '\n'
463
            if (selection.endsWith(QLatin1Char('\n'))) {
464 465
                selection = selection.left(selection.size() -1);
            }
466
        }
467 468 469 470
        if (selection.isEmpty()) {
            selection = currentWord(*editView->document(), editView->cursorPosition());
        }

471
        if (!selection.isEmpty() && !selection.contains(QLatin1Char('\n'))) {
472 473 474 475 476
            m_ui.searchCombo->blockSignals(true);
            m_ui.searchCombo->lineEdit()->setText(selection);
            m_ui.searchCombo->blockSignals(false);
        }

477 478
        m_ui.searchCombo->lineEdit()->selectAll();
        m_searchJustOpened = true;
479
        startSearchWhileTyping();
Kåre Särs's avatar
Kåre Särs committed
480 481 482
    }
}

483 484
void KatePluginSearchView::handleEsc(QEvent *e)
{
485
    if (!m_mainWindow) return;
486 487 488 489 490

    QKeyEvent *k = static_cast<QKeyEvent *>(e);
    if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {

        if (m_toolView->isVisible()) {
491
            m_mainWindow->hideToolView(m_toolView);
492 493 494 495 496 497 498
        }
        else {
            clearMarks();
        }
    }
}

499 500 501 502 503
void KatePluginSearchView::setSearchString(const QString &pattern)
{
    m_ui.searchCombo->lineEdit()->setText(pattern);
}

504 505 506 507 508 509 510 511 512
void KatePluginSearchView::toggleOptions(bool show)
{
    m_ui.stackedWidget->setCurrentIndex((show) ? 1:0);
}

void KatePluginSearchView::setSearchPlace(int place)
{
    m_ui.searchPlaceCombo->setCurrentIndex(place);
}
513 514 515 516

QStringList KatePluginSearchView::filterFiles(const QStringList& files) const
{
    QString types = m_ui.filterCombo->currentText();
517
    QString excludes = m_ui.excludeCombo->currentText();
Joseph Wenninger's avatar
Joseph Wenninger committed
518
    if (((types.isEmpty() || types == QStringLiteral("*"))) && (excludes.isEmpty())) {
519 520 521 522
        // shortcut for use all files
        return files;
    }

523
    QStringList tmpTypes = types.split(QLatin1Char(','));
524 525 526 527 528 529 530
    QVector<QRegExp> typeList;
    for (int i=0; i<tmpTypes.size(); i++) {
        QRegExp rx(tmpTypes[i]);
        rx.setPatternSyntax(QRegExp::Wildcard);
        typeList << rx;
    }

531
    QStringList tmpExcludes = excludes.split(QLatin1Char(','));
532 533 534 535 536 537 538
    QVector<QRegExp> excludeList;
    for (int i=0; i<tmpExcludes.size(); i++) {
        QRegExp rx(tmpExcludes[i]);
        rx.setPatternSyntax(QRegExp::Wildcard);
        excludeList << rx;
    }

539 540 541
    QStringList filteredFiles;
    foreach (QString fileName, files) {
        bool isInSubDir = fileName.startsWith(m_resultBaseDir);
542
        QString nameToCheck = fileName;
543
        if (isInSubDir) {
544
            nameToCheck = fileName.mid(m_resultBaseDir.size());
545 546
        }

547 548 549 550 551 552 553 554 555 556 557 558
        bool skip = false;
        for (int i=0; i<excludeList.size(); i++) {
            if (excludeList[i].exactMatch(nameToCheck)) {
                skip = true;
                break;
            }
        }
        if (skip) {
            continue;
        }


559
        for (int i=0; i<typeList.size(); i++) {
560
            if (typeList[i].exactMatch(nameToCheck)) {
561 562 563 564 565 566 567 568
                filteredFiles << fileName;
                break;
            }
        }
    }
    return filteredFiles;
}

569
void KatePluginSearchView::folderFileListChanged()
Kåre Särs's avatar
Kåre Särs committed
570
{
571 572
    m_searchDiskFilesDone = false;
    m_searchOpenFilesDone = false;
573

574
    if (!m_curResults) {
575
        qWarning() << "This is a bug";
576 577 578
        m_searchDiskFilesDone = true;
        m_searchOpenFilesDone = true;
        searchDone();
579 580
        return;
    }
581
    QStringList fileList = m_folderFilesList.fileList();
582

583
    QList<KTextEditor::Document*> openList;
584 585
    for (int i=0; i<m_kateApp->documents().size(); i++) {
        int index = fileList.indexOf(m_kateApp->documents()[i]->url().toString());
586
        if (index != -1) {
587
            openList << m_kateApp->documents()[i];
588 589
            fileList.removeAt(index);
        }
Kåre Särs's avatar
Kåre Särs committed
590
    }
591 592 593 594 595 596

    // 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);
597 598
    }
    else {
599
        m_searchOpenFilesDone = true;
Kåre Särs's avatar
Kåre Särs committed
600 601
    }

602
    m_searchDiskFiles.startSearch(fileList, m_curResults->regExp);
Kåre Särs's avatar
Kåre Särs committed
603 604
}

605

Kåre Särs's avatar
Kåre Särs committed
606 607
void KatePluginSearchView::searchPlaceChanged()
{
608
    m_ui.displayOptions->setChecked(true);
609

610 611 612 613 614
    const bool inFolder = (m_ui.searchPlaceCombo->currentIndex() == 1);
    const bool inProject = (m_ui.searchPlaceCombo->currentIndex() == 2);

    m_ui.filterCombo->setEnabled(inFolder || inProject);

615
    m_ui.excludeCombo->setEnabled(inFolder || inProject);
616 617 618 619 620 621 622 623 624 625 626 627
    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);

    // ... 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());
Kåre Särs's avatar
Kåre Särs committed
628 629
}

630
void KatePluginSearchView::addHeaderItem()
631
{
632
    QTreeWidgetItem *item = new QTreeWidgetItem(m_curResults->tree, QStringList());
633 634
    item->setCheckState(0, Qt::Checked);
    item->setFlags(item->flags() | Qt::ItemIsTristate);
635
    m_curResults->tree->expandItem(item);
636 637
}

638
QTreeWidgetItem * KatePluginSearchView::rootFileItem(const QString &url, const QString &fName)
639
{
640
    if (!m_curResults) {
641 642 643
        return 0;
    }

644
    // FIXME KF5
645
    QUrl kurl = QUrl::fromUserInput(url);
646
    QString path = kurl.isLocalFile() ? localFileDirUp (kurl).path() : kurl.url();
647
    path.replace(m_resultBaseDir, QString());
648
    QString name = kurl.fileName();
649 650 651
    if (url.isEmpty()) {
        name = fName;
    }
652

653
    // make sure we have a root item
654
    if (m_curResults->tree->topLevelItemCount() == 0) {
655
        addHeaderItem();
656 657 658
    }
    QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0);

659 660 661
    if (root->data(0, ReplaceMatches::FileNameRole).toString() == fName) {
        // The root item contains the document name ->
        // this is search as you type, return the root item
662 663 664
        return root;
    }

665
    for (int i=0; i<root->childCount(); i++) {
666
        //qDebug() << root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() << fName;
667 668 669
        if ((root->child(i)->data(0, ReplaceMatches::FileUrlRole).toString() == url)&&
            (root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() == fName)) {
            int matches = root->child(i)->data(0, ReplaceMatches::LineRole).toInt() + 1;
670
            QString tmpUrl = QString::fromLatin1("%1<b>%2</b>: <b>%3</b>").arg(path).arg(name).arg(matches);
671
            root->child(i)->setData(0, Qt::DisplayRole, tmpUrl);
672
            root->child(i)->setData(0, ReplaceMatches::LineRole, matches);
673
            return root->child(i);
674 675
        }
    }
676

677
    // file item not found create a new one
678
    QString tmpUrl = QString::fromLatin1("%1<b>%2</b>: <b>%3</b>").arg(path).arg(name).arg(1);
679

680
    TreeWidgetItem *item = new TreeWidgetItem(root, QStringList(tmpUrl));
681 682 683
    item->setData(0, ReplaceMatches::FileUrlRole, url);
    item->setData(0, ReplaceMatches::FileNameRole, fName);
    item->setData(0, ReplaceMatches::LineRole, 1);
684
    item->setCheckState(0, Qt::Checked);
685
    item->setFlags(item->flags() | Qt::ItemIsTristate);
686 687 688
    return item;
}

689 690 691 692 693
void KatePluginSearchView::addMatchMark(KTextEditor::Document* doc, int line, int column, int matchLen)
{
    if (!doc) return;

    KTextEditor::MovingInterface* miface = qobject_cast<KTextEditor::MovingInterface*>(doc);
694
    KTextEditor::ConfigInterface* ciface = qobject_cast<KTextEditor::ConfigInterface*>(m_mainWindow->activeView());
695
    KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute());
696 697 698

    bool replace = ((sender() == &m_replacer) || (sender() == 0) || (sender() == m_ui.replaceButton));
    if (replace) {
699
        QColor replaceColor(Qt::green);
Joseph Wenninger's avatar
Joseph Wenninger committed
700
        if (ciface) replaceColor = ciface->configValue(QStringLiteral("replace-highlight-color")).value<QColor>();
701
        attr->setBackground(replaceColor);
702 703
    }
    else {
704
        QColor searchColor(Qt::yellow);
Joseph Wenninger's avatar
Joseph Wenninger committed
705
        if (ciface) searchColor = ciface->configValue(QStringLiteral("search-highlight-color")).value<QColor>();
706
        attr->setBackground(searchColor);
707
    }
708 709 710 711 712 713 714 715
    // calculate end line in case of multi-line match
    int endLine = line;
    int endColumn = column+matchLen;
    while ((endLine < doc->lines()) &&  (endColumn > doc->line(endLine).size())) {
        endColumn -= doc->line(endLine).size();
        endColumn--; // remove one for '\n'
        endLine++;
    }
716

717
    KTextEditor::Range range(line, column, endLine, endColumn);
718

719
    if (m_curResults && !replace) {
720 721
        // special handling for "(?=\\n)" in multi-line search
        QRegExp tmpReg = m_curResults->regExp;
Joseph Wenninger's avatar
Joseph Wenninger committed
722
        if (m_curResults->regExp.pattern().endsWith(QStringLiteral("(?=\\n)"))) {
723
            QString newPatern = tmpReg.pattern();
Joseph Wenninger's avatar
Joseph Wenninger committed
724
            newPatern.replace(QStringLiteral("(?=\\n)"), QStringLiteral("$"));
725 726 727 728
            tmpReg.setPattern(newPatern);
        }

        if (tmpReg.indexIn(doc->text(range)) != 0) {
729
            qDebug() << doc->text(range) << "Does not match" << m_curResults->regExp.pattern();
730 731 732 733
            return;
        }
    }

734 735 736 737 738 739 740 741 742 743
    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);

    KTextEditor::MarkInterface* iface = qobject_cast<KTextEditor::MarkInterface*>(doc);
    if (!iface) return;
    iface->setMarkDescription(KTextEditor::MarkInterface::markType32, i18n("SearchHighLight"));
    iface->setMarkPixmap(KTextEditor::MarkInterface::markType32,
744
                         QIcon().pixmap(0,0));
745 746 747 748 749 750
    iface->addMark(line, KTextEditor::MarkInterface::markType32);

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

751
void KatePluginSearchView::matchFound(const QString &url, const QString &fName, int line, int column,
752
                                      const QString &lineContent, int matchLen)
Kåre Särs's avatar
Kåre Särs committed
753
{
754
    if (!m_curResults) {
755 756
        return;
    }
757

758 759
    QString pre = lineContent.left(column).toHtmlEscaped();
    QString match = lineContent.mid(column, matchLen).toHtmlEscaped();
Joseph Wenninger's avatar
Joseph Wenninger committed
760
    match.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
761
    QString post = lineContent.mid(column + matchLen).toHtmlEscaped();
Kåre Särs's avatar
Kåre Särs committed
762
    QStringList row;
Joseph Wenninger's avatar
Joseph Wenninger committed
763
    row << i18n("Line: <b>%1</b>: %2", line+1, pre+QStringLiteral("<b>")+match+QStringLiteral("</b>")+post);
764

765 766
    TreeWidgetItem *item = new TreeWidgetItem(rootFileItem(url, fName), row);
    item->setData(0, ReplaceMatches::FileUrlRole, url);
767
    item->setData(0, Qt::ToolTipRole, url);
768 769 770 771 772 773 774
    item->setData(0, ReplaceMatches::FileNameRole, fName);
    item->setData(0, ReplaceMatches::LineRole, line);
    item->setData(0, ReplaceMatches::ColumnRole, column);
    item->setData(0, ReplaceMatches::MatchLenRole, matchLen);
    item->setData(0, ReplaceMatches::PreMatchRole, pre);
    item->setData(0, ReplaceMatches::MatchRole, match);
    item->setData(0, ReplaceMatches::PostMatchRole, post);
775
    item->setCheckState (0, Qt::Checked);
776 777 778

    m_curResults->matches++;

779
    // Add mark if the document is open
780 781 782 783 784
    KTextEditor::Document* doc;
    if (url.isEmpty()) {
        doc = m_replacer.findNamed(fName);
    }
    else {
785
        doc = m_kateApp->findUrl(QUrl::fromUserInput(url));
786
    }
787
    addMatchMark(doc, line, column, matchLen);
788 789 790 791
}

void KatePluginSearchView::clearMarks()
{
792
    // FIXME: check for ongoing search...
793
    KTextEditor::MarkInterface* iface;
794
    foreach (KTextEditor::Document* doc, m_kateApp->documents()) {
795 796 797 798 799 800
        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();
801 802
                if (i.value()->type & KTextEditor::MarkInterface::markType32) {
                    iface->removeMark(i.value()->line, KTextEditor::MarkInterface::markType32);
803 804 805 806 807 808
                }
            }
        }
    }
    qDeleteAll(m_matchRanges);
    m_matchRanges.clear();
Kåre Särs's avatar
Kåre Särs committed
809 810
}

811 812
void KatePluginSearchView::clearDocMarks(KTextEditor::Document* doc)
{
813
    //qDebug() << sender();
814 815 816 817 818 819 820 821
    // FIXME: check for ongoing search...
    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();
822 823
            if (i.value()->type & KTextEditor::MarkInterface::markType32) {
                iface->removeMark(i.value()->line, KTextEditor::MarkInterface::markType32);
824 825 826 827 828 829 830
            }
        }
    }

    int i = 0;
    while (i<m_matchRanges.size()) {
        if (m_matchRanges.at(i)->document() == doc) {
831
            //qDebug() << "removing mark in" << doc->url();
832 833 834 835 836 837 838 839 840
            delete m_matchRanges.at(i);
            m_matchRanges.removeAt(i);
        }
        else {
            i++;
        }
    }
}

841 842 843
void KatePluginSearchView::startSearch()
{
    m_changeTimer.stop(); // make sure not to start a "while you type" search now
844
    m_mainWindow->showToolView(m_toolView); // in case we are invoked from the command interface
845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
    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;
    }
    m_ui.searchCombo->addToHistory(m_ui.searchCombo->currentText());
    if(m_ui.filterCombo->findText(m_ui.filterCombo->currentText()) == -1) {
        m_ui.filterCombo->insertItem(0, m_ui.filterCombo->currentText());
        m_ui.filterCombo->setCurrentIndex(0);
    }
    if(m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText()) == -1) {
        m_ui.excludeCombo->insertItem(0, m_ui.excludeCombo->currentText());
        m_ui.excludeCombo->setCurrentIndex(0);
    }
    m_curResults = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget());
    if (!m_curResults) {
862
        qWarning() << "This is a bug";
863 864 865 866 867 868 869 870 871 872 873
        return;
    }

    m_ui.newTabButton->setDisabled(true);
    m_ui.searchCombo->setDisabled(true);
    m_ui.searchButton->setDisabled(true);
    m_ui.displayOptions->setChecked (false);
    m_ui.displayOptions->setDisabled(true);
    m_ui.replaceCheckedBtn->setDisabled(true);
    m_ui.replaceButton->setDisabled(true);
    m_ui.nextAndStop->setCurrentIndex(1);
874
    m_ui.replaceCombo->setDisabled(true);
875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895


    QRegExp reg(m_ui.searchCombo->currentText(),
                m_ui.matchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive,
                m_ui.useRegExp->isChecked() ? QRegExp::RegExp : QRegExp::FixedString);
    m_curResults->regExp = reg;

    clearMarks();
    m_curResults->tree->clear();
    m_curResults->matches = 0;

    m_ui.resultTabWidget->setTabText(m_ui.resultTabWidget->currentIndex(),
                                     m_ui.searchCombo->currentText());

    m_toolView->setCursor(Qt::WaitCursor);
    m_searchDiskFilesDone = false;
    m_searchOpenFilesDone = false;

    if (m_ui.searchPlaceCombo->currentIndex() ==  0) {
        m_searchDiskFilesDone = true;
        m_resultBaseDir.clear();
896
        const QList<KTextEditor::Document*> documents = m_kateApp->documents();