1/*
2 * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com)
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17#include "searchdialog.h"
18#include "ui_searchdialog.h"
19#include <QTabBar>
20#include "../editor.h"
21#include "../mainwindow.h"
22#include "../editorlist.h"
23#include <qsynedit/Search.h>
24#include <qsynedit/SearchRegex.h>
25#include "../project.h"
26#include "../settings.h"
27#include <QMessageBox>
28#include <QDebug>
29
30
31SearchDialog::SearchDialog(QWidget *parent) :
32 QDialog(parent),
33 ui(new Ui::SearchDialog),
34 mSearchEngine()
35{
36 setWindowFlag(Qt::WindowContextHelpButtonHint,false);
37 ui->setupUi(this);
38 mTabBar = new QTabBar();
39 mTabBar->addTab(tr("Find"));
40 mTabBar->addTab(tr("Replace"));
41 mTabBar->addTab(tr("Find in files"));
42 mTabBar->addTab(tr("Replace in files"));
43 mTabBar->setExpanding(false);
44 ui->dialogLayout->insertWidget(0,mTabBar);
45 connect(mTabBar,&QTabBar::currentChanged,this, &SearchDialog::onTabChanged);
46 mSearchOptions&=0;
47 mBasicSearchEngine= QSynedit::PSynSearchBase(new QSynedit::BasicSearcher());
48 mRegexSearchEngine= QSynedit::PSynSearchBase(new QSynedit::RegexSearcher());
49}
50
51SearchDialog::~SearchDialog()
52{
53 delete ui;
54}
55
56void SearchDialog::find(const QString &text)
57{
58 if (mTabBar->currentIndex()==0) {
59 this->onTabChanged();
60 } else {
61 mTabBar->setCurrentIndex(0);
62 }
63 ui->cbFind->setCurrentText(text);
64 ui->cbFind->setFocus();
65 show();
66}
67
68void SearchDialog::findNext()
69{
70 if (mTabBar->currentIndex()==0) { // it's a find action
71
72 // Disable entire scope searching
73 ui->rbEntireScope->setChecked(false);
74
75 // Always search forwards
76 ui->rbForward->setChecked(true);
77
78 ui->btnExecute->click();
79 }
80}
81
82void SearchDialog::findInFiles(const QString &text)
83{
84 mTabBar->setCurrentIndex(2);
85 ui->cbFind->setCurrentText(text);
86 ui->cbFind->setFocus();
87 show();
88}
89
90void SearchDialog::findInFiles(const QString &keyword, SearchFileScope scope, QSynedit::SearchOptions options)
91{
92 mTabBar->setCurrentIndex(2);
93
94 ui->cbFind->setCurrentText(keyword);
95 ui->cbFind->setFocus();
96
97 switch(scope) {
98 case SearchFileScope::currentFile:
99 ui->rbCurrentFile->setChecked(true);
100 break;
101 case SearchFileScope::openedFiles:
102 ui->rbOpenFiles->setChecked(true);
103 break;
104 case SearchFileScope::wholeProject:
105 ui->rbProject->setChecked(true);
106 break;
107 }
108 // Apply options
109 ui->chkRegExp->setChecked(options.testFlag(QSynedit::ssoRegExp));
110 ui->chkCaseSensetive->setChecked(options.testFlag(QSynedit::ssoMatchCase));
111 ui->chkWholeWord->setChecked(options.testFlag(QSynedit::ssoWholeWord));
112 ui->chkWrapAround->setChecked(options.testFlag(QSynedit::ssoWholeWord));
113
114 show();
115}
116
117void SearchDialog::replace(const QString &sFind, const QString &sReplace)
118{
119 mTabBar->setCurrentIndex(1);
120 ui->cbFind->setCurrentText(sFind);
121 ui->cbReplace->setCurrentText(sReplace);
122 ui->cbFind->setFocus();
123 show();
124}
125
126void SearchDialog::onTabChanged()
127{
128 bool isfind = (mTabBar->currentIndex() == 0);
129 bool isfindfiles = (mTabBar->currentIndex() == 2 || mTabBar->currentIndex() == 3 );
130 bool isreplace = (mTabBar->currentIndex() == 1);
131
132 ui->lblReplace->setVisible(isreplace);
133 ui->cbReplace->setVisible(isreplace);
134
135 ui->grpOrigin->setVisible(isfind || isreplace);
136 ui->grpOrigin->setEnabled(isfind || isreplace);
137
138 ui->grpScope->setVisible(isfind || isreplace);
139 ui->grpScope->setEnabled(isreplace);
140 ui->grpWhere->setVisible(isfindfiles);
141 ui->grpWhere->setEnabled(isfindfiles);
142 ui->grpDirection->setVisible(isfind || isreplace);
143 ui->grpDirection->setEnabled(isfind || isreplace);
144
145 // grpOption is always visible
146
147 // Disable project search option when none is open
148// rbProjectFiles.Enabled := Assigned(MainForm.Project);
149 ui->rbProject->setEnabled(pMainWindow->project()!=nullptr);
150 ui->rbOpenFiles->setEnabled(pMainWindow->editorList()->pageCount()>0);
151// if not Assigned(MainForm.Project) then
152// rbOpenFiles.Checked := true;
153
154 // Disable prompt when doing finds
155 ui->chkPrompt->setEnabled(isreplace);
156 ui->chkPrompt->setVisible(isreplace);
157 ui->chkWrapAround->setEnabled(!isfindfiles);
158 ui->chkWrapAround->setVisible(!isfindfiles);
159
160 if (isfind || isfindfiles) {
161 ui->btnExecute->setText(tr("Find"));
162 } else {
163 ui->btnExecute->setText(tr("Replace"));
164 }
165 setWindowTitle(mTabBar->tabText(mTabBar->currentIndex()));
166}
167
168void SearchDialog::on_cbFind_currentTextChanged(const QString &)
169{
170 ui->btnExecute->setEnabled(!ui->cbFind->currentText().isEmpty());
171}
172
173void SearchDialog::on_btnCancel_clicked()
174{
175 this->close();
176}
177
178static void saveComboHistory(QComboBox* cb,const QString& text) {
179 QString s = text.trimmed();
180 if (s.isEmpty())
181 return;
182 int i = cb->findText(s);
183 if (i>=0) {
184 cb->removeItem(i);
185 }
186 cb->insertItem(0,s);
187 cb->setCurrentText(s);
188}
189
190void SearchDialog::on_btnExecute_clicked()
191{
192 int findCount = 0;
193 saveComboHistory(ui->cbFind,ui->cbFind->currentText());
194 saveComboHistory(ui->cbReplace,ui->cbReplace->currentText());
195
196 SearchAction actionType;
197 switch (mTabBar->currentIndex()) {
198 case 0:
199 actionType = SearchAction::Find;
200 break;
201 case 1:
202 actionType = SearchAction::Replace;
203 break;
204 case 2:
205 actionType = SearchAction::FindFiles;
206 break;
207 case 3:
208 actionType = SearchAction::ReplaceFiles;
209 break;
210 default:
211 return;
212 }
213
214 mSearchOptions&=0;
215
216 // Apply options
217 if (ui->chkRegExp->isChecked()) {
218 mSearchOptions.setFlag(QSynedit::ssoRegExp);
219 }
220 if (ui->chkCaseSensetive->isChecked()) {
221 mSearchOptions.setFlag(QSynedit::ssoMatchCase);
222 }
223 if (ui->chkWholeWord->isChecked()) {
224 mSearchOptions.setFlag(QSynedit::ssoWholeWord);
225 }
226 if (ui->chkWrapAround->isChecked()) {
227 mSearchOptions.setFlag(QSynedit::ssoWrapAround);
228 }
229
230 // Apply scope, when enabled
231 if (ui->grpScope->isEnabled()) {
232 if (ui->rbSelection->isChecked()) {
233 mSearchOptions.setFlag(QSynedit::ssoSelectedOnly);
234 }
235 }
236
237 // Apply direction, when enabled
238 if (ui->grpDirection->isEnabled()) {
239 if (ui->rbBackward->isChecked()) {
240 mSearchOptions.setFlag(QSynedit::ssoBackwards);
241 }
242 }
243
244 // Apply origin, when enabled
245 if (ui->grpOrigin->isEnabled()) {
246 if (ui->rbEntireScope->isChecked()) {
247 mSearchOptions.setFlag(QSynedit::ssoEntireScope);
248 }
249 }
250
251 // Use entire scope for file finding/replacing
252 if (actionType == SearchAction::FindFiles || actionType == SearchAction::ReplaceFiles) {
253 mSearchOptions.setFlag(QSynedit::ssoEntireScope);
254 }
255
256 this->close();
257
258 // Find the first one, then quit
259 if (actionType == SearchAction::Find) {
260 Editor *e = pMainWindow->editorList()->getEditor();
261 if (e!=nullptr) {
262 findCount+=execute(e,ui->cbFind->currentText(),"",nullptr,
263 [](){
264 return QMessageBox::question(pMainWindow,
265 tr("Continue Search"),
266 tr("End of file has been reached. ")
267 +tr("Do you want to continue from file's beginning?"),
268 QMessageBox::Yes|QMessageBox::No,
269 QMessageBox::Yes) == QMessageBox::Yes;
270 });
271 }
272 } else if (actionType == SearchAction::Replace) {
273 Editor *e = pMainWindow->editorList()->getEditor();
274 if (e!=nullptr) {
275 bool doPrompt = ui->chkPrompt->isChecked();
276 findCount+=execute(e,ui->cbFind->currentText(),ui->cbReplace->currentText(),
277 [&doPrompt](const QString& sSearch,
278 const QString& /*sReplace*/, int /*Line*/, int /*ch*/, int /*wordLen*/){
279 if (doPrompt) {
280 switch(QMessageBox::question(pMainWindow,
281 tr("Replace"),
282 tr("Replace this occurrence of ''%1''?").arg(sSearch),
283 QMessageBox::Yes|QMessageBox::YesAll|QMessageBox::No|QMessageBox::Cancel,
284 QMessageBox::Yes)) {
285 case QMessageBox::Yes:
286 return QSynedit::SearchAction::Replace;
287 case QMessageBox::YesAll:
288 return QSynedit::SearchAction::ReplaceAll;
289 case QMessageBox::No:
290 return QSynedit::SearchAction::Skip;
291 case QMessageBox::Cancel:
292 return QSynedit::SearchAction::Exit;
293 default:
294 return QSynedit::SearchAction::Exit;
295 }
296 } else {
297 return QSynedit::SearchAction::ReplaceAll;
298 }
299 },
300 [](){
301 return QMessageBox::question(pMainWindow,
302 tr("Continue Replace"),
303 tr("End of file has been reached. ")
304 +tr("Do you want to continue from file's beginning?"),
305 QMessageBox::Yes|QMessageBox::No,
306 QMessageBox::Yes) == QMessageBox::Yes;
307 });
308 }
309
310 } else if (actionType == SearchAction::FindFiles || actionType == SearchAction::ReplaceFiles) {
311 int fileSearched = 0;
312 int fileHitted = 0;
313 QString keyword = ui->cbFind->currentText();
314 if (ui->rbOpenFiles->isChecked()) {
315 PSearchResults results = pMainWindow->searchResultModel()->addSearchResults(
316 keyword,
317 mSearchOptions,
318 SearchFileScope::openedFiles
319 );
320 // loop through editors, add results to message control
321 for (int i=0;i<pMainWindow->editorList()->pageCount();i++) {
322 Editor * e=pMainWindow->editorList()->operator[](i);
323 if (e!=nullptr) {
324 fileSearched++;
325 PSearchResultTreeItem parentItem = batchFindInEditor(
326 e,
327 e->filename(),
328 keyword);
329 int t = parentItem->results.size();
330 findCount+=t;
331 if (t>0) {
332 fileHitted++;
333 results->results.append(parentItem);
334 }
335 }
336 }
337 pMainWindow->searchResultModel()->notifySearchResultsUpdated();
338 } else if (ui->rbCurrentFile->isChecked()) {
339 PSearchResults results = pMainWindow->searchResultModel()->addSearchResults(
340 keyword,
341 mSearchOptions,
342 SearchFileScope::currentFile
343 );
344 Editor * e= pMainWindow->editorList()->getEditor();
345 if (e!=nullptr) {
346 fileSearched++;
347 PSearchResultTreeItem parentItem = batchFindInEditor(
348 e,
349 e->filename(),
350 keyword);
351 int t = parentItem->results.size();
352 findCount+=t;
353 if (t>0) {
354 fileHitted++;
355 results->results.append(parentItem);
356 }
357 }
358 pMainWindow->searchResultModel()->notifySearchResultsUpdated();
359 } else if (ui->rbProject->isChecked()) {
360 PSearchResults results = pMainWindow->searchResultModel()->addSearchResults(
361 keyword,
362 mSearchOptions,
363 SearchFileScope::wholeProject
364 );
365 for (int i=0;i<pMainWindow->project()->units().count();i++) {
366 Editor * e = pMainWindow->project()->unitEditor(i);
367 QString curFilename = pMainWindow->project()->units()[i]->fileName();
368 if (e) {
369 fileSearched++;
370 PSearchResultTreeItem parentItem = batchFindInEditor(
371 e,
372 e->filename(),
373 keyword);
374 int t = parentItem->results.size();
375 findCount+=t;
376 if (t>0) {
377 fileHitted++;
378 results->results.append(parentItem);
379 }
380 } else if (fileExists(curFilename)) {
381 QSynedit::SynEdit editor;
382 QByteArray realEncoding;
383 editor.document()->loadFromFile(curFilename,ENCODING_AUTO_DETECT, realEncoding);
384 fileSearched++;
385 PSearchResultTreeItem parentItem = batchFindInEditor(
386 &editor,
387 curFilename,
388 keyword);
389 int t = parentItem->results.size();
390 findCount+=t;
391 if (t>0) {
392 fileHitted++;
393 results->results.append(parentItem);
394 }
395
396 }
397 }
398 pMainWindow->searchResultModel()->notifySearchResultsUpdated();
399 }
400 pMainWindow->showSearchPanel(actionType == SearchAction::ReplaceFiles);
401 }
402}
403
404int SearchDialog::execute(QSynedit::SynEdit *editor, const QString &sSearch, const QString &sReplace,
405 QSynedit::SearchMathedProc matchCallback,
406 QSynedit::SearchConfirmAroundProc confirmAroundCallback)
407{
408 if (editor==nullptr)
409 return 0;
410 // Modify the caret when using 'from cursor' and when the selection is ignored
411 if (!mSearchOptions.testFlag(QSynedit::ssoEntireScope) && !mSearchOptions.testFlag(QSynedit::ssoSelectedOnly)
412 && editor->selAvail()) {
413 // start at end of selection
414 if (mSearchOptions.testFlag(QSynedit::ssoBackwards)) {
415 editor->setCaretXY(editor->blockBegin());
416 } else {
417 editor->setCaretXY(editor->blockEnd());
418 }
419 }
420
421 if (mSearchOptions.testFlag(QSynedit::ssoRegExp)) {
422 mSearchEngine = mRegexSearchEngine;
423 } else {
424 mSearchEngine = mBasicSearchEngine;
425 }
426
427 return editor->searchReplace(sSearch, sReplace, mSearchOptions,
428 mSearchEngine, matchCallback, confirmAroundCallback);
429}
430
431std::shared_ptr<SearchResultTreeItem> SearchDialog::batchFindInEditor(QSynedit::SynEdit *e, const QString& filename,const QString &keyword)
432{
433 //backup
434 QSynedit::BufferCoord caretBackup = e->caretXY();
435 QSynedit::BufferCoord blockBeginBackup = e->blockBegin();
436 QSynedit::BufferCoord blockEndBackup = e->blockEnd();
437 int toplineBackup = e->topLine();
438 int leftCharBackup = e->leftChar();
439
440 PSearchResultTreeItem parentItem = std::make_shared<SearchResultTreeItem>();
441 parentItem->filename = filename;
442 parentItem->parent = nullptr;
443 execute(e,keyword,"",
444 [e,&parentItem, filename](const QString&,
445 const QString&, int Line, int ch, int wordLen){
446 PSearchResultTreeItem item = std::make_shared<SearchResultTreeItem>();
447 item->filename = filename;
448 item->line = Line;
449 item->start = ch;
450 item->len = wordLen;
451 item->parent = parentItem.get();
452 item->text = e->document()->getString(Line-1);
453 item->text.replace('\t',' ');
454 parentItem->results.append(item);
455 return QSynedit::SearchAction::Skip;
456 });
457
458 // restore
459 e->setCaretXY(caretBackup);
460 e->setTopLine(toplineBackup);
461 e->setLeftChar(leftCharBackup);
462 e->setCaretAndSelection(
463 caretBackup,
464 blockBeginBackup,
465 blockEndBackup
466 );
467 return parentItem;
468}
469
470void SearchDialog::showEvent(QShowEvent *event)
471{
472 QDialog::showEvent(event);
473 if (pSettings->environment().language()=="zh_CN") {
474 ui->txtRegExpHelp->setText(
475 QString("<html><head/><body><p><a href=\"%1\"><span style=\" text-decoration: underline; color:#0000ff;\">(?)</span></a></p></body></html>")
476 .arg("https://www.runoob.com/regexp/regexp-tutorial.html"));
477 } else {
478 ui->txtRegExpHelp->setText(
479 QString("<html><head/><body><p><a href=\"%1\"><span style=\" text-decoration: underline; color:#0000ff;\">(?)</span></a></p></body></html>")
480 .arg("https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference"));
481 }
482}
483
484QTabBar *SearchDialog::tabBar() const
485{
486 return mTabBar;
487}
488
489QSynedit::PSynSearchBase SearchDialog::searchEngine() const
490{
491 return mSearchEngine;
492}
493
494void SearchDialog::findPrevious()
495{
496 if (mTabBar->currentIndex()==0) { // it's a find action
497
498 // Disable entire scope searching
499 ui->rbEntireScope->setChecked(false);
500
501 // Always search backward
502 ui->rbBackward->setChecked(true);
503
504 ui->btnExecute->click();
505 }
506}
507