1 /****************************************************************************
2 **
3 ** Copyright (C) 2006-2009 fullmetalcoder <fullmetalcoder@hotmail.fr>
4 **
5 ** This file is part of the Edyuk project <http://edyuk.org>
6 **
7 ** This file may be used under the terms of the GNU General Public License
8 ** version 3 as published by the Free Software Foundation and appearing in the
9 ** file GPL.txt included in the packaging of this file.
10 **
11 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
12 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
13 **
14 ****************************************************************************/
15
16 /*!
17 \file qdocumentsearch.cpp
18 \brief Implementation of QDocumentSearch
19 */
20
21 #include "qdocumentsearch.h"
22
23 /*!
24 \ingroup document
25 @{
26 */
27
28 #include "qeditor.h"
29 #include "qdocument.h"
30 #include "qdocument_p.h"
31 #include "qdocumentline.h"
32 #include "qformatscheme.h"
33
34 #include <QMessageBox>
35
36 /*!
37 \class QDocumentSearch
38 \brief An helper class to perform search in document
39
40 QDocumentSearch offer means to perform complex search in documents.
41 */
42
QDocumentSearch(QEditor * e,const QString & f,Options opt,const QString & r)43 QDocumentSearch::QDocumentSearch(QEditor *e, const QString& f, Options opt, const QString& r)
44 : m_group(-1), m_option(opt), m_string(f), m_replace(r), m_editor(e)
45 {
46
47 }
48
~QDocumentSearch()49 QDocumentSearch::~QDocumentSearch()
50 {
51 clearMatches();
52 }
53
options() const54 QDocumentSearch::Options QDocumentSearch::options() const
55 {
56 return m_option;
57 }
58
59 /*!
60 \brief Position of the current match among the indexed matches
61 */
currentMatchIndex() const62 int QDocumentSearch::currentMatchIndex() const
63 {
64 return m_highlight.count() ? m_index : -1;
65 }
66
67 /*!
68 \brief Number of availables indexed matches
69
70 Indexed matches are only available when the whole scope is searched,
71 i.e when either the HighlightAll option is set to true or when next()
72 is called with the all parameter set to true.
73 */
indexedMatchCount() const74 int QDocumentSearch::indexedMatchCount() const
75 {
76 return m_highlight.count();
77 }
78
79 /*!
80 \return A cursor pointing to the n-th index match
81 \param idx index of the match to lookup
82
83 The cursor returned, if valid, delimits the match through its selection.
84 */
match(int idx) const85 QDocumentCursor QDocumentSearch::match(int idx) const
86 {
87 return idx >= 0 && idx < m_highlight.count() ? m_highlight.at(idx) : QDocumentCursor();
88 }
89
90 /*!
91 \brief Clear matches
92
93 This function should be called anytime you perform a search with the HighlightAll option,
94 once you're done iterating over the matches.
95 */
clearMatches()96 void QDocumentSearch::clearMatches()
97 {
98 if ( !m_editor || !m_editor->document() )
99 return;
100
101 //qDebug("clearing matches");
102 m_cursor = QDocumentCursor();
103
104 if ( m_group != -1 )
105 {
106 m_editor->document()->clearMatches(m_group);
107 m_editor->document()->flushMatches(m_group);
108 m_group = -1;
109 }
110
111 m_highlight.clear();
112 }
113
114 /*!
115 \return The search pattern
116 */
searchText() const117 QString QDocumentSearch::searchText() const
118 {
119 return m_string;
120 }
121
122 /*!
123 \brief Set the search pattern
124 */
setSearchText(const QString & f)125 void QDocumentSearch::setSearchText(const QString& f)
126 {
127 m_string = f;
128
129 clearMatches();
130 }
131
132 /*!
133 \brief Test whether a given option is enabled
134 */
hasOption(Option opt) const135 bool QDocumentSearch::hasOption(Option opt) const
136 {
137 return m_option & opt;
138 }
139
140 /*!
141 \brief Set a search option
142 \param opt option to set
143 \param on whether to enable the option
144 */
setOption(Option opt,bool on)145 void QDocumentSearch::setOption(Option opt, bool on)
146 {
147 if ( on )
148 m_option |= opt;
149 else
150 m_option &= ~opt;
151
152 if ( (opt & QDocumentSearch::HighlightAll) && m_highlight.count() )
153 {
154 QDocument *d = m_editor->document();
155
156 if ( m_group != -1 && !on )
157 {
158 d->clearMatches(m_group);
159 d->flushMatches(m_group);
160 m_group = -1;
161 } else if ( m_group == -1 && on ) {
162 m_group = d->getNextGroupId();
163
164 QFormatScheme *f = d->formatScheme();
165
166 if ( !f )
167 f = QDocument::formatFactory();
168
169 if ( !f )
170 {
171 qWarning("No format scheme set to the document and no global default one available.\n"
172 "-> highlighting of search matches disabled.");
173 return;
174 }
175
176 int sid = f->id("search");
177
178 foreach ( const QDocumentCursor& c, m_highlight )
179 {
180 //QFormatRange r(c.anchorColumnNumber(), c.columnNumber() - c.anchorColumnNumber(), sid);
181
182 d->addMatch(m_group,
183 c.lineNumber(),
184 c.anchorColumnNumber(),
185 c.columnNumber() - c.anchorColumnNumber(),
186 sid);
187 }
188
189 //qDebug("%i matches in group %i", indexedMatchCount(), m_group);
190 d->flushMatches(m_group);
191 }
192 } else if (
193 (m_option & QDocumentSearch::HighlightAll)
194 &&
195 (
196 (opt & QDocumentSearch::RegExp)
197 ||
198 (opt & QDocumentSearch::WholeWords)
199 ||
200 (opt & QDocumentSearch::CaseSensitive)
201 )
202 )
203 {
204 // matches may have become invalid : update them
205 clearMatches();
206 next(false);
207 }
208 }
209
210 /*!
211 \return the replacement text
212 */
replaceText() const213 QString QDocumentSearch::replaceText() const
214 {
215 return m_replace;
216 }
217
218 /*!
219 \brief Set the replacement text
220 */
setReplaceText(const QString & r)221 void QDocumentSearch::setReplaceText(const QString& r)
222 {
223 m_replace = r;
224
225 clearMatches();
226 }
227
228 /*!
229 \return The current cursor position
230
231 This is useful to examine matches after performing a search.
232 */
origin() const233 QDocumentCursor QDocumentSearch::origin() const
234 {
235 return m_origin;
236 }
237
238 /*!
239 \brief Set the cursor
240
241 If the related option is set, search will start from that cursor position
242
243 This also changes the cursor()
244 */
setOrigin(const QDocumentCursor & c)245 void QDocumentSearch::setOrigin(const QDocumentCursor& c)
246 {
247 m_cursor = QDocumentCursor();
248
249 if ( c == m_origin )
250 return;
251
252 m_origin = c;
253
254 clearMatches();
255 }
256
257 /*!
258 \return The current cursor position
259
260 This is useful to examine matches after performing a search.
261 */
cursor() const262 QDocumentCursor QDocumentSearch::cursor() const
263 {
264 return m_cursor;
265 }
266
267 /*!
268 \brief Set the cursor
269
270 If the related option is set, search will start from that cursor position
271 */
setCursor(const QDocumentCursor & c)272 void QDocumentSearch::setCursor(const QDocumentCursor& c)
273 {
274 m_cursor = c;
275 }
276
277 /*!
278 \return The scope of the search
279
280 An invalid cursor indicate that the scope is the whole document, otherwise
281 the scope is the selection of the returned cursor.
282 */
scope() const283 QDocumentCursor QDocumentSearch::scope() const
284 {
285 return m_scope;
286 }
287
288 /*!
289 \brief Set the search scope
290
291 If the given cursor has no selection (a fortiori if it is invalid) then
292 the scope is the whole document.
293 */
setScope(const QDocumentCursor & c)294 void QDocumentSearch::setScope(const QDocumentCursor& c)
295 {
296 if ( c == m_scope )
297 return;
298
299 if ( c.hasSelection() )
300 m_scope = c;
301 else
302 m_scope = QDocumentCursor();
303
304 clearMatches();
305 }
306
307 /*!
308 \brief Test whether the end of the search scope has been reached
309 */
end(bool backward) const310 bool QDocumentSearch::end(bool backward) const
311 {
312 bool absEnd = backward ? m_cursor.atStart() : m_cursor.atEnd();
313
314 if ( m_scope.isValid() && m_scope.hasSelection() )
315 {
316 absEnd |= !m_scope.isWithinSelection(m_cursor);
317 /*
318 qDebug(
319 "(%i, %i, %i) %s in {(%i, %i), (%i, %i)}",
320 m_cursor.lineNumber(),
321 m_cursor.anchorColumnNumber(),
322 m_cursor.columnNumber(),
323 absEnd ? "is not" : "is",
324 m_scope.selectionStart().lineNumber(),
325 m_scope.selectionStart().columnNumber(),
326 m_scope.selectionEnd().lineNumber(),
327 m_scope.selectionEnd().columnNumber()
328 );
329 */
330 }
331
332 return absEnd;
333 }
334
335 /*!
336 \brief Perform a search
337 \param backward whether to go backward or forward
338 \param all if true, the whole document will be searched first, all matches recorded and available for further navigation
339
340 \note Technically speaking the all parameter make search behave similarly to the HighlightAll option, except that the former
341 option does not alter the formatting of the document.
342 */
next(bool backward,bool all)343 void QDocumentSearch::next(bool backward, bool all)
344 {
345 if ( m_string.isEmpty() )
346 return;
347
348 if ( !hasOption(Replace) && (all || hasOption(HighlightAll)) && m_highlight.count() )
349 {
350 if ( !backward )
351 ++m_index;
352
353 //m_index = m_index + (backward ? -1 : 1);
354
355 if ( (m_index < 0 || m_index >= m_highlight.count()) )
356 {
357 if ( hasOption(Silent) )
358 {
359 m_cursor = QDocumentCursor();
360 return;
361 }
362
363 int ret =
364 QMessageBox::question(
365 m_editor,
366 tr("Failure"),
367 tr(
368 "End of matches reached.\n"
369 "Restart from the begining ?"
370 ),
371 QMessageBox::Yes
372 | QMessageBox::No,
373 QMessageBox::Yes
374 );
375
376 if ( ret == QMessageBox::Yes )
377 {
378 m_index = backward ? m_highlight.count() : 0;
379 --m_index;
380 next(backward);
381 return;
382 }
383 } else {
384 m_cursor = m_highlight.at(m_index);
385
386 if ( m_editor && !hasOption(Silent) )
387 m_editor->setCursor(m_cursor);
388 }
389
390 if ( backward )
391 --m_index;
392
393 return;
394 }
395
396 if ( m_cursor.isNull() )
397 {
398 m_cursor = m_origin;
399 }
400
401 if ( m_cursor.isNull() )
402 {
403 if ( m_scope.isValid() && m_scope.hasSelection() )
404 {
405 if ( backward )
406 m_cursor = m_scope.selectionEnd();
407 else
408 m_cursor = m_scope.selectionStart();
409 } else if ( m_editor ) {
410
411 m_cursor = QDocumentCursor(m_editor->document());
412
413 if ( backward )
414 m_cursor.movePosition(1, QDocumentCursor::End);
415
416 } else {
417 QMessageBox::warning(0, 0, "Unable to perform search operation");
418 }
419 }
420
421 /*
422 qDebug(
423 "searching %s from line %i (column %i)",
424 backward ? "backward" : "forward",
425 m_cursor.lineNumber(),
426 m_cursor.columnNumber()
427 );
428 */
429
430 m_index = 0;
431 QRegExp m_regexp;
432 Qt::CaseSensitivity cs = hasOption(CaseSensitive)
433 ?
434 Qt::CaseSensitive
435 :
436 Qt::CaseInsensitive;
437
438 if ( hasOption(RegExp) )
439 {
440 m_regexp = QRegExp(m_string, cs, QRegExp::RegExp);
441 } else if ( hasOption(WholeWords) ) {
442 m_regexp = QRegExp(
443 QString("\\b%1\\b").arg(QRegExp::escape(m_string)),
444 cs,
445 QRegExp::RegExp
446 );
447 } else {
448 m_regexp = QRegExp(m_string, cs, QRegExp::FixedString);
449 }
450
451 bool found = false;
452 QDocumentCursor::MoveOperation move;
453 QDocument *d = m_editor ? m_editor->document() : m_origin.document();
454 QFormatScheme *f = d->formatScheme() ? d->formatScheme() : QDocument::formatFactory();
455 int sid = f ? f->id("search") : 0;
456
457 if ( !sid )
458 qWarning("Highlighting of search matches disabled due to unavailability of a format scheme.");
459
460 move = backward ? QDocumentCursor::PreviousBlock : QDocumentCursor::NextBlock;
461
462 QDocumentSelection boundaries;
463 bool bounded = m_scope.isValid() && m_scope.hasSelection();
464
465 // condition only to avoid debug messages...
466 if ( bounded )
467 boundaries = m_scope.selection();
468
469 while ( !end(backward) )
470 {
471 if ( backward && !m_cursor.columnNumber() )
472 {
473 m_cursor.movePosition(1, QDocumentCursor::PreviousCharacter);
474 continue;
475 }
476
477 int ln = m_cursor.lineNumber();
478 QDocumentLine l = m_cursor.line();
479
480 int coloffset = 0;
481 QString s = l.text();
482
483 if ( backward )
484 {
485 if ( bounded && (boundaries.startLine == ln) )
486 {
487 s = s.mid(boundaries.start);
488 coloffset = boundaries.start;
489 }
490
491 s = s.left(m_cursor.columnNumber());
492 } else {
493 if ( bounded && (boundaries.endLine == ln) )
494 s = s.left(boundaries.end);
495
496 }
497
498 int column = backward
499 ?
500 m_regexp.lastIndexIn(s, m_cursor.columnNumber() - 1)
501 :
502 m_regexp.indexIn(s, m_cursor.columnNumber())
503 ;
504
505 /*
506 qDebug("searching %s in %s => %i",
507 qPrintable(m_regexp.pattern()),
508 qPrintable(s),
509 column);
510 */
511
512 if ( column != -1 && (backward || column >= m_cursor.columnNumber()) )
513 {
514 column += coloffset;
515
516 if ( backward )
517 {
518 m_cursor.setColumnNumber(column + m_regexp.matchedLength());
519 m_cursor.setColumnNumber(column, QDocumentCursor::KeepAnchor);
520
521 /*
522 m_cursor.movePosition(m_regexp.matchedLength(),
523 QDocumentCursor::PreviousCharacter,
524 QDocumentCursor::KeepAnchor);
525 */
526 } else {
527 m_cursor.setColumnNumber(column);
528 m_cursor.setColumnNumber(column + m_regexp.matchedLength(), QDocumentCursor::KeepAnchor);
529
530 /*
531 m_cursor.movePosition(m_regexp.matchedLength(),
532 QDocumentCursor::NextCharacter,
533 QDocumentCursor::KeepAnchor);
534 */
535 }
536
537 if ( m_editor && !hasOption(Silent) && !hasOption(HighlightAll) )
538 m_editor->setCursor(m_cursor);
539
540 if ( hasOption(Replace) )
541 {
542 bool rep = true;
543
544 if ( hasOption(Prompt) )
545 {
546 int ret = QMessageBox::question(m_editor, tr("Replacement prompt"),
547 tr("Shall it be replaced?"),
548 QMessageBox::Yes
549 | QMessageBox::No
550 | QMessageBox::Cancel,
551 QMessageBox::Yes);
552
553 if ( ret == QMessageBox::Yes )
554 {
555 rep = true;
556 } else if ( ret == QMessageBox::No ) {
557 rep = false;
558 } else if ( QMessageBox::Cancel ) {
559 //m_cursor.setColumnNumber(m_cursor.columnNumber());
560 return;
561 }
562 }
563
564 //
565 if ( rep )
566 {
567 QString replacement = m_replace;
568
569 for ( int i = m_regexp.numCaptures(); i >= 0; --i )
570 replacement.replace(QString("\\") + QString::number(i),
571 m_regexp.cap(i));
572
573 m_cursor.beginEditBlock();
574 m_cursor.removeSelectedText();
575 m_cursor.insertText(replacement);
576 m_cursor.endEditBlock();
577
578 if ( backward )
579 m_cursor.movePosition(replacement.length(), QDocumentCursor::PreviousCharacter);
580 } else {
581 //qDebug("no rep");
582 }
583 } else if ( all || hasOption(HighlightAll) ) {
584
585 if ( sid && hasOption(HighlightAll) )
586 {
587 if ( m_group == -1 )
588 m_group = d->getNextGroupId();
589
590 d->addMatch(m_group,
591 m_cursor.lineNumber(),
592 m_cursor.anchorColumnNumber(),
593 m_cursor.columnNumber() - m_cursor.anchorColumnNumber(),
594 sid);
595 //QFormatRange r(
596 // m_cursor.anchorColumnNumber(),
597 // m_cursor.columnNumber() - m_cursor.anchorColumnNumber(),
598 // m_editor->document()->formatScheme()->id("search")
599 // );
600
601 //qDebug("(%i, %i, %i)", r.offset, r.length, r.format);
602 //m_cursor.line().addOverlay(r);
603 }
604
605 m_highlight << m_cursor;
606 m_highlight.last().setAutoUpdated(true);
607 }
608
609 found = true;
610
611 if ( !(all || hasOption(HighlightAll)) )
612 break;
613
614 } else {
615 m_cursor.movePosition(1, move);
616 }
617 }
618
619 if ( !hasOption(Replace) && hasOption(HighlightAll) && m_highlight.count() )
620 {
621 //qDebug("%i matches in group %i", indexedMatchCount(), m_group);
622 if ( indexedMatchCount() )
623 {
624 m_editor->document()->flushMatches(m_group);
625 } else {
626 m_editor->document()->releaseGroupId(m_group);
627 m_group = -1;
628 }
629
630 m_index = backward ? m_highlight.count() : 0;
631 --m_index;
632 return next(backward);
633 }
634
635 if ( !found )
636 {
637 m_cursor = QDocumentCursor();
638
639 if ( hasOption(Silent) )
640 return;
641
642 int ret =
643 QMessageBox::question(
644 m_editor,
645 tr("Failure"),
646 tr(
647 "End of scope reached with no match.\n"
648 "Restart from the begining ?"
649 ),
650 QMessageBox::Yes
651 | QMessageBox::No,
652 QMessageBox::Yes
653 );
654
655 if ( ret == QMessageBox::Yes )
656 {
657 m_origin = QDocumentCursor();
658 next(backward);
659 }
660 }
661 }
662
663 /*! @} */
664