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