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 | |
31 | SearchDialog::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 | |
51 | SearchDialog::~SearchDialog() |
52 | { |
53 | delete ui; |
54 | } |
55 | |
56 | void 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 | |
68 | void 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 | |
82 | void SearchDialog::findInFiles(const QString &text) |
83 | { |
84 | mTabBar->setCurrentIndex(2); |
85 | ui->cbFind->setCurrentText(text); |
86 | ui->cbFind->setFocus(); |
87 | show(); |
88 | } |
89 | |
90 | void 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 | |
117 | void 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 | |
126 | void 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 | |
168 | void SearchDialog::on_cbFind_currentTextChanged(const QString &) |
169 | { |
170 | ui->btnExecute->setEnabled(!ui->cbFind->currentText().isEmpty()); |
171 | } |
172 | |
173 | void SearchDialog::on_btnCancel_clicked() |
174 | { |
175 | this->close(); |
176 | } |
177 | |
178 | static 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 | |
190 | void 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 | |
404 | int 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 | |
431 | std::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 | |
470 | void 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 | |
484 | QTabBar *SearchDialog::tabBar() const |
485 | { |
486 | return mTabBar; |
487 | } |
488 | |
489 | QSynedit::PSynSearchBase SearchDialog::searchEngine() const |
490 | { |
491 | return mSearchEngine; |
492 | } |
493 | |
494 | void 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 | |